route.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. import { NextRequest, NextResponse } from 'next/server'
  2. import { Keypair, PublicKey, VersionedTransaction } from '@solana/web3.js'
  3. import BN from 'bn.js'
  4. import { Decimal } from 'decimal.js'
  5. import bs58 from 'bs58'
  6. import { chain } from '@/lib/config'
  7. export async function POST(request: NextRequest) {
  8. try {
  9. const body = await request.json()
  10. const { positionAddress, maxUsdValue, nftMintAddress } = body
  11. if (!positionAddress) {
  12. return NextResponse.json(
  13. { error: 'Position address is required' },
  14. { status: 400 }
  15. )
  16. }
  17. if (!maxUsdValue || maxUsdValue <= 0) {
  18. return NextResponse.json(
  19. { error: 'Max USD value must be greater than 0' },
  20. { status: 400 }
  21. )
  22. }
  23. let nftMint = undefined
  24. if (nftMintAddress) {
  25. nftMint = new PublicKey(nftMintAddress)
  26. } else {
  27. const detailData = await fetch(
  28. `https://api2.byreal.io/byreal/api/dex/v2/position/detail?address=${positionAddress}`
  29. ).then((res) => res.json())
  30. nftMint = new PublicKey(detailData.result.data.nftMintAddress)
  31. }
  32. // 获取 position 信息
  33. const positionInfo = await chain.getPositionInfoByNftMint(nftMint)
  34. if (!positionInfo) {
  35. return NextResponse.json({ error: 'Position not found' }, { status: 404 })
  36. }
  37. const { rawPoolInfo, priceLower, priceUpper } = positionInfo
  38. const poolInfo = rawPoolInfo
  39. // 使用 currentPrice (A/B 价格) 来计算 token 的 USD 价格
  40. // currentPrice 是 tokenA / tokenB 的比率
  41. const currentPrice = poolInfo.currentPrice
  42. // 稳定币地址
  43. const USDC_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
  44. const USDT_ADDRESS = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'
  45. const tokenAAddress = poolInfo.mintA.toBase58()
  46. const tokenBAddress = poolInfo.mintB.toBase58()
  47. // 判断哪个 token 是稳定币
  48. const isTokenAStable =
  49. tokenAAddress === USDC_ADDRESS || tokenAAddress === USDT_ADDRESS
  50. const isTokenBStable =
  51. tokenBAddress === USDC_ADDRESS || tokenBAddress === USDT_ADDRESS
  52. // 计算 token 的 USD 价格
  53. let tokenAPriceUsd: number
  54. let tokenBPriceUsd: number
  55. if (isTokenBStable) {
  56. // tokenB 是稳定币,价格为 1 USD
  57. tokenBPriceUsd = 1
  58. // tokenA 的价格 = currentPrice (A/B) * tokenB 价格
  59. tokenAPriceUsd = currentPrice
  60. } else if (isTokenAStable) {
  61. // tokenA 是稳定币,价格为 1 USD
  62. tokenAPriceUsd = 1
  63. // tokenB 的价格 = 1 / currentPrice (因为 currentPrice = A/B)
  64. tokenBPriceUsd = 1 / currentPrice
  65. } else {
  66. // 如果都不是稳定币,假设其中一个的价格为 1(简化处理)
  67. // 或者可以从价格 API 获取实际价格
  68. // 这里假设 tokenB 价格为 1,tokenA 价格为 currentPrice
  69. tokenBPriceUsd = 1
  70. tokenAPriceUsd = currentPrice
  71. }
  72. console.log('tokenAPriceUsd', tokenAPriceUsd)
  73. console.log('tokenBPriceUsd', tokenBPriceUsd)
  74. // 计算比例,使得总价值不超过 maxUsdValue
  75. // 使用 position 的 tickLower 和 tickUpper
  76. const tickLower = positionInfo.rawPositionInfo.tickLower
  77. const tickUpper = positionInfo.rawPositionInfo.tickUpper
  78. // 计算需要投入的金额
  79. // 假设使用较小的 token 作为 base
  80. const useTokenAAsBase = tokenAPriceUsd <= tokenBPriceUsd
  81. const base = useTokenAAsBase ? 'MintA' : 'MintB'
  82. // 计算 baseAmount,使得总价值尽可能接近 maxUsdValue
  83. // 使用迭代方法精确计算,确保总价值接近但不超过 maxUsdValue
  84. const midPrice = priceLower.add(priceUpper).div(2)
  85. let baseAmount: BN
  86. let otherAmountMax: BN
  87. // 使用二分法或迭代方法找到最接近 maxUsdValue 的金额
  88. // 目标值设为接近 100%,尽可能接近最大值
  89. // 由于 slippage 和实际价格曲线的影响,最终值会略低于 maxUsdValue
  90. const targetValue = maxUsdValue * 0.995 // 99.5%,更接近最大值
  91. if (base === 'MintA') {
  92. // 使用 tokenA 作为 base
  93. // 估算:总价值 ≈ baseAmount * (tokenAPriceUsd + midPrice * tokenBPriceUsd)
  94. const estimatedPricePerBase =
  95. tokenAPriceUsd + midPrice.toNumber() * tokenBPriceUsd
  96. // 使用更精确的计算,考虑实际的价格曲线
  97. // 迭代调整 baseAmount 直到总价值接近 targetValue
  98. let low = new BN(0)
  99. let high = new BN(
  100. Math.ceil(
  101. (targetValue / estimatedPricePerBase) *
  102. 10 ** poolInfo.mintDecimalsA *
  103. 1.5
  104. )
  105. )
  106. let bestBaseAmount = new BN(0)
  107. let bestValue = 0
  108. // 二分查找最接近目标值的 baseAmount,增加迭代次数以提高精度
  109. for (let i = 0; i < 30; i++) {
  110. const mid = low.add(high).div(new BN(2))
  111. if (mid.eq(low) || mid.eq(high)) break
  112. const testBaseAmount = mid
  113. const testOtherAmount = chain.getAmountBFromAmountA({
  114. priceLower,
  115. priceUpper,
  116. amountA: testBaseAmount,
  117. poolInfo,
  118. })
  119. const testUiAmountA = new Decimal(testBaseAmount.toString()).div(
  120. 10 ** poolInfo.mintDecimalsA
  121. )
  122. const testUiAmountB = new Decimal(testOtherAmount.toString()).div(
  123. 10 ** poolInfo.mintDecimalsB
  124. )
  125. const testValue =
  126. testUiAmountA.toNumber() * tokenAPriceUsd +
  127. testUiAmountB.toNumber() * tokenBPriceUsd
  128. if (testValue <= targetValue && testValue > bestValue) {
  129. bestValue = testValue
  130. bestBaseAmount = testBaseAmount
  131. }
  132. if (testValue > targetValue) {
  133. high = mid
  134. } else {
  135. low = mid
  136. }
  137. }
  138. baseAmount = bestBaseAmount.gt(new BN(0)) ? bestBaseAmount : low
  139. // 计算需要的 tokenB 数量(实际需要的,未加 slippage)
  140. const otherAmountNeeded = chain.getAmountBFromAmountA({
  141. priceLower,
  142. priceUpper,
  143. amountA: baseAmount,
  144. poolInfo,
  145. })
  146. // 添加 2% slippage 作为 otherAmountMax(允许的最大值)
  147. otherAmountMax = otherAmountNeeded.mul(new BN(10200)).div(new BN(10000))
  148. } else {
  149. // 使用 tokenB 作为 base
  150. const estimatedPricePerBase =
  151. tokenBPriceUsd + (1 / midPrice.toNumber()) * tokenAPriceUsd
  152. // 迭代调整 baseAmount
  153. let low = new BN(0)
  154. let high = new BN(
  155. Math.ceil(
  156. (targetValue / estimatedPricePerBase) *
  157. 10 ** poolInfo.mintDecimalsB *
  158. 1.5
  159. )
  160. )
  161. let bestBaseAmount = new BN(0)
  162. let bestValue = 0
  163. // 二分查找,增加迭代次数以提高精度
  164. for (let i = 0; i < 30; i++) {
  165. const mid = low.add(high).div(new BN(2))
  166. if (mid.eq(low) || mid.eq(high)) break
  167. const testBaseAmount = mid
  168. const testOtherAmount = chain.getAmountAFromAmountB({
  169. priceLower,
  170. priceUpper,
  171. amountB: testBaseAmount,
  172. poolInfo,
  173. })
  174. const testUiAmountA = new Decimal(testOtherAmount.toString()).div(
  175. 10 ** poolInfo.mintDecimalsA
  176. )
  177. const testUiAmountB = new Decimal(testBaseAmount.toString()).div(
  178. 10 ** poolInfo.mintDecimalsB
  179. )
  180. const testValue =
  181. testUiAmountA.toNumber() * tokenAPriceUsd +
  182. testUiAmountB.toNumber() * tokenBPriceUsd
  183. if (testValue <= targetValue && testValue > bestValue) {
  184. bestValue = testValue
  185. bestBaseAmount = testBaseAmount
  186. }
  187. if (testValue > targetValue) {
  188. high = mid
  189. } else {
  190. low = mid
  191. }
  192. }
  193. baseAmount = bestBaseAmount.gt(new BN(0)) ? bestBaseAmount : low
  194. // 计算需要的 tokenA 数量(实际需要的,未加 slippage)
  195. const otherAmountNeeded = chain.getAmountAFromAmountB({
  196. priceLower,
  197. priceUpper,
  198. amountB: baseAmount,
  199. poolInfo,
  200. })
  201. // 添加 5% slippage 作为 otherAmountMax(允许的最大值)
  202. otherAmountMax = otherAmountNeeded.mul(new BN(10500)).div(new BN(10000))
  203. }
  204. // 计算最终总价值(使用实际投入的金额,而不是加了 slippage 的 otherAmountMax)
  205. // 实际投入的 otherAmount 是 otherAmountMax / 1.02(因为 otherAmountMax 包含了 2% slippage)
  206. const actualOtherAmount = otherAmountMax
  207. .mul(new BN(10000))
  208. .div(new BN(10200))
  209. const uiAmountA =
  210. base === 'MintA'
  211. ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsA)
  212. : new Decimal(actualOtherAmount.toString()).div(
  213. 10 ** poolInfo.mintDecimalsA
  214. )
  215. const uiAmountB =
  216. base === 'MintB'
  217. ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsB)
  218. : new Decimal(actualOtherAmount.toString()).div(
  219. 10 ** poolInfo.mintDecimalsB
  220. )
  221. let totalValue =
  222. uiAmountA.toNumber() * tokenAPriceUsd +
  223. uiAmountB.toNumber() * tokenBPriceUsd
  224. // 如果总价值仍然超过 maxUsdValue,按比例缩小
  225. if (totalValue > maxUsdValue) {
  226. const scale = maxUsdValue / totalValue
  227. if (base === 'MintA') {
  228. baseAmount = baseAmount
  229. .mul(new BN(Math.floor(scale * 10000)))
  230. .div(new BN(10000))
  231. otherAmountMax = chain
  232. .getAmountBFromAmountA({
  233. priceLower,
  234. priceUpper,
  235. amountA: baseAmount,
  236. poolInfo,
  237. })
  238. .mul(new BN(10200))
  239. .div(new BN(10000))
  240. } else {
  241. baseAmount = baseAmount
  242. .mul(new BN(Math.floor(scale * 10000)))
  243. .div(new BN(10000))
  244. otherAmountMax = chain
  245. .getAmountAFromAmountB({
  246. priceLower,
  247. priceUpper,
  248. amountB: baseAmount,
  249. poolInfo,
  250. })
  251. .mul(new BN(10200))
  252. .div(new BN(10000))
  253. }
  254. // 重新计算总价值
  255. const finalUiAmountA =
  256. base === 'MintA'
  257. ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsA)
  258. : new Decimal(otherAmountMax.toString()).div(
  259. 10 ** poolInfo.mintDecimalsA
  260. )
  261. const finalUiAmountB =
  262. base === 'MintB'
  263. ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsB)
  264. : new Decimal(otherAmountMax.toString()).div(
  265. 10 ** poolInfo.mintDecimalsB
  266. )
  267. totalValue =
  268. finalUiAmountA.toNumber() * tokenAPriceUsd +
  269. finalUiAmountB.toNumber() * tokenBPriceUsd
  270. }
  271. // 重新计算最终的 UI 金额和总价值
  272. const finalUiAmountA =
  273. base === 'MintA'
  274. ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsA)
  275. : new Decimal(otherAmountMax.toString()).div(
  276. 10 ** poolInfo.mintDecimalsA
  277. )
  278. const finalUiAmountB =
  279. base === 'MintB'
  280. ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsB)
  281. : new Decimal(otherAmountMax.toString()).div(
  282. 10 ** poolInfo.mintDecimalsB
  283. )
  284. totalValue =
  285. finalUiAmountA.toNumber() * tokenAPriceUsd +
  286. finalUiAmountB.toNumber() * tokenBPriceUsd
  287. // 打印所有信息
  288. console.log('\n========== LP Copy Information ==========')
  289. console.log('Original Position Address:', positionAddress)
  290. console.log('Referer Position (NFT Mint):', nftMint.toBase58())
  291. console.log('\n--- Pool Information ---')
  292. console.log('Pool Address:', poolInfo.poolId.toBase58())
  293. console.log('Token A Address:', poolInfo.mintA.toBase58())
  294. console.log('Token B Address:', poolInfo.mintB.toBase58())
  295. console.log('Token A Decimals:', poolInfo.mintDecimalsA)
  296. console.log('Token B Decimals:', poolInfo.mintDecimalsB)
  297. console.log('Current Price (A/B):', currentPrice)
  298. console.log('Token A Price (USD):', tokenAPriceUsd)
  299. console.log('Token B Price (USD):', tokenBPriceUsd)
  300. console.log('\n--- Position Range ---')
  301. console.log('Tick Lower:', tickLower)
  302. console.log('Tick Upper:', tickUpper)
  303. console.log('Price Lower:', priceLower.toString())
  304. console.log('Price Upper:', priceUpper.toString())
  305. console.log('\n--- Investment Details ---')
  306. console.log('Max USD Value:', maxUsdValue)
  307. console.log('Base Token:', base)
  308. console.log('Base Amount (raw):', baseAmount.toString())
  309. console.log('Other Amount Max (raw):', otherAmountMax.toString())
  310. console.log('Token A Amount (UI):', finalUiAmountA.toString())
  311. console.log('Token B Amount (UI):', finalUiAmountB.toString())
  312. console.log(
  313. 'Token A Value (USD):',
  314. (finalUiAmountA.toNumber() * tokenAPriceUsd).toFixed(2)
  315. )
  316. console.log(
  317. 'Token B Value (USD):',
  318. (finalUiAmountB.toNumber() * tokenBPriceUsd).toFixed(2)
  319. )
  320. console.log('Total Value (USD):', totalValue.toFixed(2))
  321. console.log('==========================================\n')
  322. // 从环境变量读取私钥
  323. const secretKey = process.env.SOL_SECRET_KEY
  324. if (!secretKey) {
  325. return NextResponse.json(
  326. { error: 'SOL_SECRET_KEY not configured' },
  327. { status: 500 }
  328. )
  329. }
  330. const userKeypair = Keypair.fromSecretKey(bs58.decode(secretKey))
  331. const userAddress = userKeypair.publicKey
  332. console.log('User Address:', userAddress.toBase58())
  333. console.log('\n--- Executing Transaction ---')
  334. console.log('Creating position on-chain...')
  335. // 执行实际上链操作
  336. const signerCallback = async (tx: VersionedTransaction) => {
  337. tx.sign([userKeypair])
  338. return tx
  339. }
  340. const txid = await chain.createPosition({
  341. userAddress,
  342. poolInfo,
  343. tickLower,
  344. tickUpper,
  345. base,
  346. baseAmount,
  347. otherAmountMax,
  348. refererPosition: positionAddress, // 添加 referer position
  349. signerCallback,
  350. })
  351. console.log('Transaction ID:', txid)
  352. console.log('Position created successfully!')
  353. console.log('==========================================\n')
  354. return NextResponse.json({
  355. success: true,
  356. txid,
  357. positionInfo: {
  358. poolAddress: poolInfo.poolId.toBase58(),
  359. priceLower: priceLower.toString(),
  360. priceUpper: priceUpper.toString(),
  361. tickLower,
  362. tickUpper,
  363. base,
  364. baseAmount: baseAmount.toString(),
  365. otherAmountMax: otherAmountMax.toString(),
  366. estimatedValue: totalValue,
  367. tokenA: {
  368. address: poolInfo.mintA.toBase58(),
  369. amount: finalUiAmountA.toString(),
  370. valueUsd: (finalUiAmountA.toNumber() * tokenAPriceUsd).toFixed(2),
  371. },
  372. tokenB: {
  373. address: poolInfo.mintB.toBase58(),
  374. amount: finalUiAmountB.toString(),
  375. valueUsd: (finalUiAmountB.toNumber() * tokenBPriceUsd).toFixed(2),
  376. },
  377. refererPosition: positionAddress,
  378. userAddress: userAddress?.toBase58() || 'N/A',
  379. },
  380. })
  381. } catch (error: unknown) {
  382. console.error('LP copy error:', error)
  383. const errorMessage =
  384. error instanceof Error ? error.message : 'Failed to copy LP position'
  385. return NextResponse.json({ error: errorMessage }, { status: 500 })
  386. }
  387. }