| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- 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 })
- }
- }
|