route.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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 { nftMintAddress, addUsdValue } = body
  11. // 验证输入
  12. if (!nftMintAddress) {
  13. return NextResponse.json(
  14. { error: 'Position NFT address is required' },
  15. { status: 400 }
  16. )
  17. }
  18. if (!addUsdValue || addUsdValue <= 0) {
  19. return NextResponse.json(
  20. { error: 'Add USD value must be greater than 0' },
  21. { status: 400 }
  22. )
  23. }
  24. const nftMint = new PublicKey(nftMintAddress)
  25. // 获取仓位信息
  26. const positionInfo = await chain.getPositionInfoByNftMint(nftMint)
  27. if (!positionInfo) {
  28. return NextResponse.json({ error: 'Position not found' }, { status: 404 })
  29. }
  30. const { rawPoolInfo } = positionInfo
  31. const poolInfo = rawPoolInfo
  32. // 使用 currentPrice (A/B 价格) 来计算 token 的 USD 价格
  33. const currentPrice = poolInfo.currentPrice
  34. // 稳定币地址
  35. const USDC_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
  36. const USDT_ADDRESS = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'
  37. const USD1_ADDRESS = 'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB'
  38. const tokenAAddress = poolInfo.mintA.toBase58()
  39. const tokenBAddress = poolInfo.mintB.toBase58()
  40. // 判断哪个 token 是稳定币
  41. const isTokenAStable =
  42. tokenAAddress === USDC_ADDRESS ||
  43. tokenAAddress === USDT_ADDRESS ||
  44. tokenAAddress === USD1_ADDRESS
  45. const isTokenBStable =
  46. tokenBAddress === USDC_ADDRESS ||
  47. tokenBAddress === USDT_ADDRESS ||
  48. tokenBAddress === USD1_ADDRESS
  49. // 计算 token 的 USD 价格
  50. let tokenAPriceUsd = 0
  51. let tokenBPriceUsd = 0
  52. let priceFromApi = false
  53. // 尝试从 ByReal API 获取真实价格
  54. try {
  55. const response = await fetch(
  56. `https://api2.byreal.io/byreal/api/dex/v2/position/detail?address=${nftMintAddress}`,
  57. {
  58. method: 'GET',
  59. headers: {
  60. accept: 'application/json',
  61. },
  62. }
  63. )
  64. if (response.ok) {
  65. const data = await response.json()
  66. const poolData = data.result?.data?.pool
  67. if (poolData?.mintA?.price && poolData?.mintB?.price) {
  68. tokenAPriceUsd = parseFloat(poolData.mintA.price)
  69. tokenBPriceUsd = parseFloat(poolData.mintB.price)
  70. priceFromApi = true
  71. console.log('Using prices from ByReal API:', {
  72. tokenAPriceUsd,
  73. tokenBPriceUsd,
  74. })
  75. }
  76. }
  77. } catch (error) {
  78. console.warn('Failed to fetch prices from API:', error)
  79. }
  80. // 如果 API 获取失败,使用稳定币逻辑或默认逻辑
  81. if (!priceFromApi) {
  82. if (isTokenBStable) {
  83. tokenBPriceUsd = 1
  84. tokenAPriceUsd = currentPrice
  85. } else if (isTokenAStable) {
  86. tokenAPriceUsd = 1
  87. tokenBPriceUsd = 1 / currentPrice
  88. } else {
  89. // 非稳定币对且无法获取价格时,使用相对价格
  90. // 以 tokenB 为基准(价格为1),tokenA 价格为 currentPrice
  91. tokenBPriceUsd = 1
  92. tokenAPriceUsd = currentPrice
  93. console.warn(
  94. 'Warning: Non-stablecoin pair without API prices. Using relative pricing.'
  95. )
  96. }
  97. }
  98. // 获取仓位的当前价格范围
  99. const priceLower = positionInfo.priceLower
  100. const priceUpper = positionInfo.priceUpper
  101. // 使用较小的 token 作为 base
  102. const useTokenAAsBase = tokenAPriceUsd <= tokenBPriceUsd
  103. const base = useTokenAAsBase ? 'MintA' : 'MintB'
  104. // 计算 baseAmount
  105. const targetValue = addUsdValue * 0.995 // 99.5%,更接近最大值
  106. let baseAmount: BN
  107. let otherAmountMax: BN
  108. if (base === 'MintA') {
  109. const estimatedPricePerBase =
  110. tokenAPriceUsd +
  111. ((priceLower.toNumber() + priceUpper.toNumber()) / 2) * tokenBPriceUsd
  112. // 迭代调整 baseAmount
  113. let low = new BN(0)
  114. let high = new BN(
  115. Math.ceil(
  116. (targetValue / estimatedPricePerBase) *
  117. 10 ** poolInfo.mintDecimalsA *
  118. 1.5
  119. )
  120. )
  121. let bestBaseAmount = new BN(0)
  122. let bestValue = 0
  123. for (let i = 0; i < 30; i++) {
  124. const mid = low.add(high).div(new BN(2))
  125. if (mid.eq(low) || mid.eq(high)) break
  126. const testBaseAmount = mid
  127. const testOtherAmount = chain.getAmountBFromAmountA({
  128. priceLower,
  129. priceUpper,
  130. amountA: testBaseAmount,
  131. poolInfo,
  132. })
  133. const testUiAmountA = new Decimal(testBaseAmount.toString()).div(
  134. 10 ** poolInfo.mintDecimalsA
  135. )
  136. const testUiAmountB = new Decimal(testOtherAmount.toString()).div(
  137. 10 ** poolInfo.mintDecimalsB
  138. )
  139. const testValue =
  140. testUiAmountA.toNumber() * tokenAPriceUsd +
  141. testUiAmountB.toNumber() * tokenBPriceUsd
  142. if (testValue <= targetValue && testValue > bestValue) {
  143. bestValue = testValue
  144. bestBaseAmount = testBaseAmount
  145. }
  146. if (testValue > targetValue) {
  147. high = mid
  148. } else {
  149. low = mid
  150. }
  151. }
  152. baseAmount = bestBaseAmount.gt(new BN(0)) ? bestBaseAmount : low
  153. // 计算需要的 tokenB 数量
  154. const otherAmountNeeded = chain.getAmountBFromAmountA({
  155. priceLower,
  156. priceUpper,
  157. amountA: baseAmount,
  158. poolInfo,
  159. })
  160. // 添加 2% slippage
  161. otherAmountMax = otherAmountNeeded.mul(new BN(10200)).div(new BN(10000))
  162. } else {
  163. const midPrice = priceLower.add(priceUpper).div(2)
  164. const estimatedPricePerBase =
  165. tokenBPriceUsd + (1 / midPrice.toNumber()) * tokenAPriceUsd
  166. let low = new BN(0)
  167. let high = new BN(
  168. Math.ceil(
  169. (targetValue / estimatedPricePerBase) *
  170. 10 ** poolInfo.mintDecimalsB *
  171. 1.5
  172. )
  173. )
  174. let bestBaseAmount = new BN(0)
  175. let bestValue = 0
  176. for (let i = 0; i < 30; i++) {
  177. const mid = low.add(high).div(new BN(2))
  178. if (mid.eq(low) || mid.eq(high)) break
  179. const testBaseAmount = mid
  180. const testOtherAmount = chain.getAmountAFromAmountB({
  181. priceLower,
  182. priceUpper,
  183. amountB: testBaseAmount,
  184. poolInfo,
  185. })
  186. const testUiAmountA = new Decimal(testOtherAmount.toString()).div(
  187. 10 ** poolInfo.mintDecimalsA
  188. )
  189. const testUiAmountB = new Decimal(testBaseAmount.toString()).div(
  190. 10 ** poolInfo.mintDecimalsB
  191. )
  192. const testValue =
  193. testUiAmountA.toNumber() * tokenAPriceUsd +
  194. testUiAmountB.toNumber() * tokenBPriceUsd
  195. if (testValue <= targetValue && testValue > bestValue) {
  196. bestValue = testValue
  197. bestBaseAmount = testBaseAmount
  198. }
  199. if (testValue > targetValue) {
  200. high = mid
  201. } else {
  202. low = mid
  203. }
  204. }
  205. baseAmount = bestBaseAmount.gt(new BN(0)) ? bestBaseAmount : low
  206. // 计算需要的 tokenA 数量
  207. const otherAmountNeeded = chain.getAmountAFromAmountB({
  208. priceLower,
  209. priceUpper,
  210. amountB: baseAmount,
  211. poolInfo,
  212. })
  213. // 添加 2% slippage
  214. otherAmountMax = otherAmountNeeded.mul(new BN(10200)).div(new BN(10000))
  215. }
  216. // 检查代币数量是否为0
  217. if (baseAmount.eq(new BN(0)) || otherAmountMax.eq(new BN(0))) {
  218. console.error('Error: One of the token amounts is zero')
  219. return NextResponse.json(
  220. {
  221. error:
  222. '添加流动性失败:其中一个代币数量为0。请确保两个代币都有数量。',
  223. details: {
  224. baseAmount: baseAmount.toString(),
  225. otherAmountMax: otherAmountMax.toString(),
  226. base,
  227. },
  228. },
  229. { status: 400 }
  230. )
  231. }
  232. // 计算最终 UI 金额
  233. const uiAmountA =
  234. base === 'MintA'
  235. ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsA)
  236. : new Decimal(otherAmountMax.toString()).div(
  237. 10 ** poolInfo.mintDecimalsA
  238. )
  239. const uiAmountB =
  240. base === 'MintB'
  241. ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsB)
  242. : new Decimal(otherAmountMax.toString()).div(
  243. 10 ** poolInfo.mintDecimalsB
  244. )
  245. // 打印所有信息
  246. console.log('\n========== Add Liquidity Information ==========')
  247. console.log('Position NFT Address:', nftMint.toBase58())
  248. console.log('\n--- Pool Information ---')
  249. console.log('Pool Address:', poolInfo.poolId.toBase58())
  250. console.log('Token A Address:', poolInfo.mintA.toBase58())
  251. console.log('Token B Address:', poolInfo.mintB.toBase58())
  252. console.log('Token A Decimals:', poolInfo.mintDecimalsA)
  253. console.log('Token B Decimals:', poolInfo.mintDecimalsB)
  254. console.log('Current Price (A/B):', currentPrice)
  255. console.log('Token A Price (USD):', tokenAPriceUsd)
  256. console.log('Token B Price (USD):', tokenBPriceUsd)
  257. console.log('\n--- Position Range ---')
  258. console.log('Price Lower:', priceLower.toString())
  259. console.log('Price Upper:', priceUpper.toString())
  260. console.log('\n--- Investment Details ---')
  261. console.log('Add USD Value:', addUsdValue)
  262. console.log('Base Token:', base)
  263. console.log('Base Amount (raw):', baseAmount.toString())
  264. console.log('Other Amount Max (raw):', otherAmountMax.toString())
  265. console.log('Token A Amount (UI):', uiAmountA.toString())
  266. console.log('Token B Amount (UI):', uiAmountB.toString())
  267. console.log(
  268. 'Token A Value (USD):',
  269. (uiAmountA.toNumber() * tokenAPriceUsd).toFixed(2)
  270. )
  271. console.log(
  272. 'Token B Value (USD):',
  273. (uiAmountB.toNumber() * tokenBPriceUsd).toFixed(2)
  274. )
  275. console.log('==========================================\n')
  276. // 从环境变量读取私钥
  277. const secretKey = process.env.SOL_SECRET_KEY
  278. if (!secretKey) {
  279. return NextResponse.json(
  280. { error: 'SOL_SECRET_KEY not configured' },
  281. { status: 500 }
  282. )
  283. }
  284. const userKeypair = Keypair.fromSecretKey(bs58.decode(secretKey))
  285. const userAddress = userKeypair.publicKey
  286. console.log('User Address:', userAddress.toBase58())
  287. console.log('\n--- Executing Transaction ---')
  288. console.log('Adding liquidity on-chain...')
  289. // 执行实际上链操作
  290. const signerCallback = async (tx: VersionedTransaction) => {
  291. tx.sign([userKeypair])
  292. return tx
  293. }
  294. const txid = await chain.addLiquidity({
  295. userAddress,
  296. nftMint,
  297. base,
  298. baseAmount,
  299. otherAmountMax,
  300. signerCallback,
  301. })
  302. console.log('Transaction ID:', txid)
  303. console.log('Liquidity added successfully!')
  304. console.log('==========================================\n')
  305. return NextResponse.json({
  306. success: true,
  307. txid,
  308. positionInfo: {
  309. nftMint: nftMint.toBase58(),
  310. poolAddress: poolInfo.poolId.toBase58(),
  311. priceLower: priceLower.toString(),
  312. priceUpper: priceUpper.toString(),
  313. base,
  314. baseAmount: baseAmount.toString(),
  315. otherAmountMax: otherAmountMax.toString(),
  316. tokenA: {
  317. address: poolInfo.mintA.toBase58(),
  318. amount: uiAmountA.toString(),
  319. valueUsd: (uiAmountA.toNumber() * tokenAPriceUsd).toFixed(2),
  320. },
  321. tokenB: {
  322. address: poolInfo.mintB.toBase58(),
  323. amount: uiAmountB.toString(),
  324. valueUsd: (uiAmountB.toNumber() * tokenBPriceUsd).toFixed(2),
  325. },
  326. userAddress: userAddress?.toBase58() || 'N/A',
  327. },
  328. })
  329. } catch (error: unknown) {
  330. console.error('Add Liquidity Error:', error)
  331. const errorMessage =
  332. error instanceof Error ? error.message : 'Failed to add liquidity'
  333. // 如果错误包含 "insufficient funds",返回友好的余额不足提示
  334. if (errorMessage.includes('insufficient funds')) {
  335. return NextResponse.json({ error: '余额不足' }, { status: 400 })
  336. }
  337. return NextResponse.json({ error: errorMessage }, { status: 500 })
  338. }
  339. }