|
@@ -0,0 +1,388 @@
|
|
|
|
|
+import { NextRequest, NextResponse } from 'next/server'
|
|
|
|
|
+import { Keypair, PublicKey, VersionedTransaction } from '@solana/web3.js'
|
|
|
|
|
+import BN from 'bn.js'
|
|
|
|
|
+import { Decimal } from 'decimal.js'
|
|
|
|
|
+import bs58 from 'bs58'
|
|
|
|
|
+import { chain } from '@/lib/config'
|
|
|
|
|
+
|
|
|
|
|
+export async function POST(request: NextRequest) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const body = await request.json()
|
|
|
|
|
+ const { nftMintAddress, addUsdValue } = body
|
|
|
|
|
+
|
|
|
|
|
+ // 验证输入
|
|
|
|
|
+ if (!nftMintAddress) {
|
|
|
|
|
+ return NextResponse.json(
|
|
|
|
|
+ { error: 'Position NFT address is required' },
|
|
|
|
|
+ { status: 400 }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!addUsdValue || addUsdValue <= 0) {
|
|
|
|
|
+ return NextResponse.json(
|
|
|
|
|
+ { error: 'Add USD value must be greater than 0' },
|
|
|
|
|
+ { status: 400 }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const nftMint = new PublicKey(nftMintAddress)
|
|
|
|
|
+
|
|
|
|
|
+ // 获取仓位信息
|
|
|
|
|
+ const positionInfo = await chain.getPositionInfoByNftMint(nftMint)
|
|
|
|
|
+ if (!positionInfo) {
|
|
|
|
|
+ return NextResponse.json({ error: 'Position not found' }, { status: 404 })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { rawPoolInfo } = positionInfo
|
|
|
|
|
+ const poolInfo = rawPoolInfo
|
|
|
|
|
+
|
|
|
|
|
+ // 使用 currentPrice (A/B 价格) 来计算 token 的 USD 价格
|
|
|
|
|
+ const currentPrice = poolInfo.currentPrice
|
|
|
|
|
+
|
|
|
|
|
+ // 稳定币地址
|
|
|
|
|
+ const USDC_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
|
|
|
|
|
+ const USDT_ADDRESS = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'
|
|
|
|
|
+ const USD1_ADDRESS = 'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB'
|
|
|
|
|
+
|
|
|
|
|
+ const tokenAAddress = poolInfo.mintA.toBase58()
|
|
|
|
|
+ const tokenBAddress = poolInfo.mintB.toBase58()
|
|
|
|
|
+
|
|
|
|
|
+ // 判断哪个 token 是稳定币
|
|
|
|
|
+ const isTokenAStable =
|
|
|
|
|
+ tokenAAddress === USDC_ADDRESS ||
|
|
|
|
|
+ tokenAAddress === USDT_ADDRESS ||
|
|
|
|
|
+ tokenAAddress === USD1_ADDRESS
|
|
|
|
|
+ const isTokenBStable =
|
|
|
|
|
+ tokenBAddress === USDC_ADDRESS ||
|
|
|
|
|
+ tokenBAddress === USDT_ADDRESS ||
|
|
|
|
|
+ tokenBAddress === USD1_ADDRESS
|
|
|
|
|
+
|
|
|
|
|
+ // 计算 token 的 USD 价格
|
|
|
|
|
+ let tokenAPriceUsd = 0
|
|
|
|
|
+ let tokenBPriceUsd = 0
|
|
|
|
|
+ let priceFromApi = false
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试从 ByReal API 获取真实价格
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(
|
|
|
|
|
+ `https://api2.byreal.io/byreal/api/dex/v2/position/detail?address=${nftMintAddress}`,
|
|
|
|
|
+ {
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ accept: 'application/json',
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ const data = await response.json()
|
|
|
|
|
+ const poolData = data.result?.data?.pool
|
|
|
|
|
+
|
|
|
|
|
+ if (poolData?.mintA?.price && poolData?.mintB?.price) {
|
|
|
|
|
+ tokenAPriceUsd = parseFloat(poolData.mintA.price)
|
|
|
|
|
+ tokenBPriceUsd = parseFloat(poolData.mintB.price)
|
|
|
|
|
+ priceFromApi = true
|
|
|
|
|
+ console.log('Using prices from ByReal API:', {
|
|
|
|
|
+ tokenAPriceUsd,
|
|
|
|
|
+ tokenBPriceUsd,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.warn('Failed to fetch prices from API:', error)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果 API 获取失败,使用稳定币逻辑或默认逻辑
|
|
|
|
|
+ if (!priceFromApi) {
|
|
|
|
|
+ if (isTokenBStable) {
|
|
|
|
|
+ tokenBPriceUsd = 1
|
|
|
|
|
+ tokenAPriceUsd = currentPrice
|
|
|
|
|
+ } else if (isTokenAStable) {
|
|
|
|
|
+ tokenAPriceUsd = 1
|
|
|
|
|
+ tokenBPriceUsd = 1 / currentPrice
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 非稳定币对且无法获取价格时,使用相对价格
|
|
|
|
|
+ // 以 tokenB 为基准(价格为1),tokenA 价格为 currentPrice
|
|
|
|
|
+ tokenBPriceUsd = 1
|
|
|
|
|
+ tokenAPriceUsd = currentPrice
|
|
|
|
|
+ console.warn(
|
|
|
|
|
+ 'Warning: Non-stablecoin pair without API prices. Using relative pricing.'
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取仓位的当前价格范围
|
|
|
|
|
+ const priceLower = positionInfo.priceLower
|
|
|
|
|
+ const priceUpper = positionInfo.priceUpper
|
|
|
|
|
+
|
|
|
|
|
+ // 使用较小的 token 作为 base
|
|
|
|
|
+ const useTokenAAsBase = tokenAPriceUsd <= tokenBPriceUsd
|
|
|
|
|
+ const base = useTokenAAsBase ? 'MintA' : 'MintB'
|
|
|
|
|
+
|
|
|
|
|
+ // 计算 baseAmount
|
|
|
|
|
+ const targetValue = addUsdValue * 0.995 // 99.5%,更接近最大值
|
|
|
|
|
+ let baseAmount: BN
|
|
|
|
|
+ let otherAmountMax: BN
|
|
|
|
|
+
|
|
|
|
|
+ if (base === 'MintA') {
|
|
|
|
|
+ const estimatedPricePerBase =
|
|
|
|
|
+ tokenAPriceUsd +
|
|
|
|
|
+ ((priceLower.toNumber() + priceUpper.toNumber()) / 2) * tokenBPriceUsd
|
|
|
|
|
+
|
|
|
|
|
+ // 迭代调整 baseAmount
|
|
|
|
|
+ let low = new BN(0)
|
|
|
|
|
+ let high = new BN(
|
|
|
|
|
+ Math.ceil(
|
|
|
|
|
+ (targetValue / estimatedPricePerBase) *
|
|
|
|
|
+ 10 ** poolInfo.mintDecimalsA *
|
|
|
|
|
+ 1.5
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ let bestBaseAmount = new BN(0)
|
|
|
|
|
+ let bestValue = 0
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < 30; i++) {
|
|
|
|
|
+ const mid = low.add(high).div(new BN(2))
|
|
|
|
|
+ if (mid.eq(low) || mid.eq(high)) break
|
|
|
|
|
+
|
|
|
|
|
+ const testBaseAmount = mid
|
|
|
|
|
+ const testOtherAmount = chain.getAmountBFromAmountA({
|
|
|
|
|
+ priceLower,
|
|
|
|
|
+ priceUpper,
|
|
|
|
|
+ amountA: testBaseAmount,
|
|
|
|
|
+ poolInfo,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const testUiAmountA = new Decimal(testBaseAmount.toString()).div(
|
|
|
|
|
+ 10 ** poolInfo.mintDecimalsA
|
|
|
|
|
+ )
|
|
|
|
|
+ const testUiAmountB = new Decimal(testOtherAmount.toString()).div(
|
|
|
|
|
+ 10 ** poolInfo.mintDecimalsB
|
|
|
|
|
+ )
|
|
|
|
|
+ const testValue =
|
|
|
|
|
+ testUiAmountA.toNumber() * tokenAPriceUsd +
|
|
|
|
|
+ testUiAmountB.toNumber() * tokenBPriceUsd
|
|
|
|
|
+
|
|
|
|
|
+ if (testValue <= targetValue && testValue > bestValue) {
|
|
|
|
|
+ bestValue = testValue
|
|
|
|
|
+ bestBaseAmount = testBaseAmount
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (testValue > targetValue) {
|
|
|
|
|
+ high = mid
|
|
|
|
|
+ } else {
|
|
|
|
|
+ low = mid
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ baseAmount = bestBaseAmount.gt(new BN(0)) ? bestBaseAmount : low
|
|
|
|
|
+
|
|
|
|
|
+ // 计算需要的 tokenB 数量
|
|
|
|
|
+ const otherAmountNeeded = chain.getAmountBFromAmountA({
|
|
|
|
|
+ priceLower,
|
|
|
|
|
+ priceUpper,
|
|
|
|
|
+ amountA: baseAmount,
|
|
|
|
|
+ poolInfo,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 添加 2% slippage
|
|
|
|
|
+ otherAmountMax = otherAmountNeeded.mul(new BN(10200)).div(new BN(10000))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const midPrice = priceLower.add(priceUpper).div(2)
|
|
|
|
|
+ const estimatedPricePerBase =
|
|
|
|
|
+ tokenBPriceUsd + (1 / midPrice.toNumber()) * tokenAPriceUsd
|
|
|
|
|
+
|
|
|
|
|
+ let low = new BN(0)
|
|
|
|
|
+ let high = new BN(
|
|
|
|
|
+ Math.ceil(
|
|
|
|
|
+ (targetValue / estimatedPricePerBase) *
|
|
|
|
|
+ 10 ** poolInfo.mintDecimalsB *
|
|
|
|
|
+ 1.5
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ let bestBaseAmount = new BN(0)
|
|
|
|
|
+ let bestValue = 0
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < 30; i++) {
|
|
|
|
|
+ const mid = low.add(high).div(new BN(2))
|
|
|
|
|
+ if (mid.eq(low) || mid.eq(high)) break
|
|
|
|
|
+
|
|
|
|
|
+ const testBaseAmount = mid
|
|
|
|
|
+ const testOtherAmount = chain.getAmountAFromAmountB({
|
|
|
|
|
+ priceLower,
|
|
|
|
|
+ priceUpper,
|
|
|
|
|
+ amountB: testBaseAmount,
|
|
|
|
|
+ poolInfo,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const testUiAmountA = new Decimal(testOtherAmount.toString()).div(
|
|
|
|
|
+ 10 ** poolInfo.mintDecimalsA
|
|
|
|
|
+ )
|
|
|
|
|
+ const testUiAmountB = new Decimal(testBaseAmount.toString()).div(
|
|
|
|
|
+ 10 ** poolInfo.mintDecimalsB
|
|
|
|
|
+ )
|
|
|
|
|
+ const testValue =
|
|
|
|
|
+ testUiAmountA.toNumber() * tokenAPriceUsd +
|
|
|
|
|
+ testUiAmountB.toNumber() * tokenBPriceUsd
|
|
|
|
|
+
|
|
|
|
|
+ if (testValue <= targetValue && testValue > bestValue) {
|
|
|
|
|
+ bestValue = testValue
|
|
|
|
|
+ bestBaseAmount = testBaseAmount
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (testValue > targetValue) {
|
|
|
|
|
+ high = mid
|
|
|
|
|
+ } else {
|
|
|
|
|
+ low = mid
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ baseAmount = bestBaseAmount.gt(new BN(0)) ? bestBaseAmount : low
|
|
|
|
|
+
|
|
|
|
|
+ // 计算需要的 tokenA 数量
|
|
|
|
|
+ const otherAmountNeeded = chain.getAmountAFromAmountB({
|
|
|
|
|
+ priceLower,
|
|
|
|
|
+ priceUpper,
|
|
|
|
|
+ amountB: baseAmount,
|
|
|
|
|
+ poolInfo,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 添加 2% slippage
|
|
|
|
|
+ otherAmountMax = otherAmountNeeded.mul(new BN(10200)).div(new BN(10000))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查代币数量是否为0
|
|
|
|
|
+ if (baseAmount.eq(new BN(0)) || otherAmountMax.eq(new BN(0))) {
|
|
|
|
|
+ console.error('Error: One of the token amounts is zero')
|
|
|
|
|
+ return NextResponse.json(
|
|
|
|
|
+ {
|
|
|
|
|
+ error:
|
|
|
|
|
+ '添加流动性失败:其中一个代币数量为0。请确保两个代币都有数量。',
|
|
|
|
|
+ details: {
|
|
|
|
|
+ baseAmount: baseAmount.toString(),
|
|
|
|
|
+ otherAmountMax: otherAmountMax.toString(),
|
|
|
|
|
+ base,
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ { status: 400 }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算最终 UI 金额
|
|
|
|
|
+ const uiAmountA =
|
|
|
|
|
+ base === 'MintA'
|
|
|
|
|
+ ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsA)
|
|
|
|
|
+ : new Decimal(otherAmountMax.toString()).div(
|
|
|
|
|
+ 10 ** poolInfo.mintDecimalsA
|
|
|
|
|
+ )
|
|
|
|
|
+ const uiAmountB =
|
|
|
|
|
+ base === 'MintB'
|
|
|
|
|
+ ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsB)
|
|
|
|
|
+ : new Decimal(otherAmountMax.toString()).div(
|
|
|
|
|
+ 10 ** poolInfo.mintDecimalsB
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // 打印所有信息
|
|
|
|
|
+ console.log('\n========== Add Liquidity Information ==========')
|
|
|
|
|
+ console.log('Position NFT Address:', nftMint.toBase58())
|
|
|
|
|
+ console.log('\n--- Pool Information ---')
|
|
|
|
|
+ console.log('Pool Address:', poolInfo.poolId.toBase58())
|
|
|
|
|
+ console.log('Token A Address:', poolInfo.mintA.toBase58())
|
|
|
|
|
+ console.log('Token B Address:', poolInfo.mintB.toBase58())
|
|
|
|
|
+ console.log('Token A Decimals:', poolInfo.mintDecimalsA)
|
|
|
|
|
+ console.log('Token B Decimals:', poolInfo.mintDecimalsB)
|
|
|
|
|
+ console.log('Current Price (A/B):', currentPrice)
|
|
|
|
|
+ console.log('Token A Price (USD):', tokenAPriceUsd)
|
|
|
|
|
+ console.log('Token B Price (USD):', tokenBPriceUsd)
|
|
|
|
|
+ console.log('\n--- Position Range ---')
|
|
|
|
|
+ console.log('Price Lower:', priceLower.toString())
|
|
|
|
|
+ console.log('Price Upper:', priceUpper.toString())
|
|
|
|
|
+ console.log('\n--- Investment Details ---')
|
|
|
|
|
+ console.log('Add USD Value:', addUsdValue)
|
|
|
|
|
+ console.log('Base Token:', base)
|
|
|
|
|
+ console.log('Base Amount (raw):', baseAmount.toString())
|
|
|
|
|
+ console.log('Other Amount Max (raw):', otherAmountMax.toString())
|
|
|
|
|
+ console.log('Token A Amount (UI):', uiAmountA.toString())
|
|
|
|
|
+ console.log('Token B Amount (UI):', uiAmountB.toString())
|
|
|
|
|
+ console.log(
|
|
|
|
|
+ 'Token A Value (USD):',
|
|
|
|
|
+ (uiAmountA.toNumber() * tokenAPriceUsd).toFixed(2)
|
|
|
|
|
+ )
|
|
|
|
|
+ console.log(
|
|
|
|
|
+ 'Token B Value (USD):',
|
|
|
|
|
+ (uiAmountB.toNumber() * tokenBPriceUsd).toFixed(2)
|
|
|
|
|
+ )
|
|
|
|
|
+ console.log('==========================================\n')
|
|
|
|
|
+
|
|
|
|
|
+ // 从环境变量读取私钥
|
|
|
|
|
+ const secretKey = process.env.SOL_SECRET_KEY
|
|
|
|
|
+ if (!secretKey) {
|
|
|
|
|
+ return NextResponse.json(
|
|
|
|
|
+ { error: 'SOL_SECRET_KEY not configured' },
|
|
|
|
|
+ { status: 500 }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const userKeypair = Keypair.fromSecretKey(bs58.decode(secretKey))
|
|
|
|
|
+ const userAddress = userKeypair.publicKey
|
|
|
|
|
+
|
|
|
|
|
+ console.log('User Address:', userAddress.toBase58())
|
|
|
|
|
+ console.log('\n--- Executing Transaction ---')
|
|
|
|
|
+ console.log('Adding liquidity on-chain...')
|
|
|
|
|
+
|
|
|
|
|
+ // 执行实际上链操作
|
|
|
|
|
+ const signerCallback = async (tx: VersionedTransaction) => {
|
|
|
|
|
+ tx.sign([userKeypair])
|
|
|
|
|
+ return tx
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const txid = await chain.addLiquidity({
|
|
|
|
|
+ userAddress,
|
|
|
|
|
+ nftMint,
|
|
|
|
|
+ base,
|
|
|
|
|
+ baseAmount,
|
|
|
|
|
+ otherAmountMax,
|
|
|
|
|
+ signerCallback,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ console.log('Transaction ID:', txid)
|
|
|
|
|
+ console.log('Liquidity added successfully!')
|
|
|
|
|
+ console.log('==========================================\n')
|
|
|
|
|
+
|
|
|
|
|
+ return NextResponse.json({
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ txid,
|
|
|
|
|
+ positionInfo: {
|
|
|
|
|
+ nftMint: nftMint.toBase58(),
|
|
|
|
|
+ poolAddress: poolInfo.poolId.toBase58(),
|
|
|
|
|
+ priceLower: priceLower.toString(),
|
|
|
|
|
+ priceUpper: priceUpper.toString(),
|
|
|
|
|
+ base,
|
|
|
|
|
+ baseAmount: baseAmount.toString(),
|
|
|
|
|
+ otherAmountMax: otherAmountMax.toString(),
|
|
|
|
|
+ tokenA: {
|
|
|
|
|
+ address: poolInfo.mintA.toBase58(),
|
|
|
|
|
+ amount: uiAmountA.toString(),
|
|
|
|
|
+ valueUsd: (uiAmountA.toNumber() * tokenAPriceUsd).toFixed(2),
|
|
|
|
|
+ },
|
|
|
|
|
+ tokenB: {
|
|
|
|
|
+ address: poolInfo.mintB.toBase58(),
|
|
|
|
|
+ amount: uiAmountB.toString(),
|
|
|
|
|
+ valueUsd: (uiAmountB.toNumber() * tokenBPriceUsd).toFixed(2),
|
|
|
|
|
+ },
|
|
|
|
|
+ userAddress: userAddress?.toBase58() || 'N/A',
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (error: unknown) {
|
|
|
|
|
+ console.error('Add Liquidity Error:', error)
|
|
|
|
|
+ const errorMessage =
|
|
|
|
|
+ error instanceof Error ? error.message : 'Failed to add liquidity'
|
|
|
|
|
+
|
|
|
|
|
+ // 如果错误包含 "insufficient funds",返回友好的余额不足提示
|
|
|
|
|
+ if (errorMessage.includes('insufficient funds')) {
|
|
|
|
|
+ return NextResponse.json({ error: '余额不足' }, { status: 400 })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return NextResponse.json({ error: errorMessage }, { status: 500 })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|