| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- 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 { positionAddress, maxUsdValue, nftMintAddress } = body
- if (!positionAddress) {
- return NextResponse.json(
- { error: 'Position address is required' },
- { status: 400 }
- )
- }
- if (!maxUsdValue || maxUsdValue <= 0) {
- return NextResponse.json(
- { error: 'Max USD value must be greater than 0' },
- { status: 400 }
- )
- }
- let nftMint = undefined
- if (nftMintAddress) {
- nftMint = new PublicKey(nftMintAddress)
- } else {
- const detailData = await fetch(
- `https://api2.byreal.io/byreal/api/dex/v2/position/detail?address=${positionAddress}`
- ).then((res) => res.json())
- nftMint = new PublicKey(detailData.result.data.nftMintAddress)
- }
- // 获取 position 信息
- const positionInfo = await chain.getPositionInfoByNftMint(nftMint)
- if (!positionInfo) {
- return NextResponse.json({ error: 'Position not found' }, { status: 404 })
- }
- const { rawPoolInfo, priceLower, priceUpper } = positionInfo
- const poolInfo = rawPoolInfo
- // 使用 currentPrice (A/B 价格) 来计算 token 的 USD 价格
- // currentPrice 是 tokenA / tokenB 的比率
- const currentPrice = poolInfo.currentPrice
- // 稳定币地址
- const USDC_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
- const USDT_ADDRESS = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'
- const tokenAAddress = poolInfo.mintA.toBase58()
- const tokenBAddress = poolInfo.mintB.toBase58()
- // 判断哪个 token 是稳定币
- const isTokenAStable =
- tokenAAddress === USDC_ADDRESS || tokenAAddress === USDT_ADDRESS
- const isTokenBStable =
- tokenBAddress === USDC_ADDRESS || tokenBAddress === USDT_ADDRESS
- // 计算 token 的 USD 价格
- let tokenAPriceUsd: number
- let tokenBPriceUsd: number
- if (isTokenBStable) {
- // tokenB 是稳定币,价格为 1 USD
- tokenBPriceUsd = 1
- // tokenA 的价格 = currentPrice (A/B) * tokenB 价格
- tokenAPriceUsd = currentPrice
- } else if (isTokenAStable) {
- // tokenA 是稳定币,价格为 1 USD
- tokenAPriceUsd = 1
- // tokenB 的价格 = 1 / currentPrice (因为 currentPrice = A/B)
- tokenBPriceUsd = 1 / currentPrice
- } else {
- // 如果都不是稳定币,假设其中一个的价格为 1(简化处理)
- // 或者可以从价格 API 获取实际价格
- // 这里假设 tokenB 价格为 1,tokenA 价格为 currentPrice
- tokenBPriceUsd = 1
- tokenAPriceUsd = currentPrice
- }
- console.log('tokenAPriceUsd', tokenAPriceUsd)
- console.log('tokenBPriceUsd', tokenBPriceUsd)
- // 计算比例,使得总价值不超过 maxUsdValue
- // 使用 position 的 tickLower 和 tickUpper
- const tickLower = positionInfo.rawPositionInfo.tickLower
- const tickUpper = positionInfo.rawPositionInfo.tickUpper
- // 计算需要投入的金额
- // 假设使用较小的 token 作为 base
- const useTokenAAsBase = tokenAPriceUsd <= tokenBPriceUsd
- const base = useTokenAAsBase ? 'MintA' : 'MintB'
- // 计算 baseAmount,使得总价值尽可能接近 maxUsdValue
- // 使用迭代方法精确计算,确保总价值接近但不超过 maxUsdValue
- const midPrice = priceLower.add(priceUpper).div(2)
- let baseAmount: BN
- let otherAmountMax: BN
- // 使用二分法或迭代方法找到最接近 maxUsdValue 的金额
- // 目标值设为接近 100%,尽可能接近最大值
- // 由于 slippage 和实际价格曲线的影响,最终值会略低于 maxUsdValue
- const targetValue = maxUsdValue * 0.995 // 99.5%,更接近最大值
- if (base === 'MintA') {
- // 使用 tokenA 作为 base
- // 估算:总价值 ≈ baseAmount * (tokenAPriceUsd + midPrice * tokenBPriceUsd)
- const estimatedPricePerBase =
- tokenAPriceUsd + midPrice.toNumber() * tokenBPriceUsd
- // 使用更精确的计算,考虑实际的价格曲线
- // 迭代调整 baseAmount 直到总价值接近 targetValue
- 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
- // 二分查找最接近目标值的 baseAmount,增加迭代次数以提高精度
- 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 数量(实际需要的,未加 slippage)
- const otherAmountNeeded = chain.getAmountBFromAmountA({
- priceLower,
- priceUpper,
- amountA: baseAmount,
- poolInfo,
- })
- // 添加 2% slippage 作为 otherAmountMax(允许的最大值)
- otherAmountMax = otherAmountNeeded.mul(new BN(10200)).div(new BN(10000))
- } else {
- // 使用 tokenB 作为 base
- const estimatedPricePerBase =
- tokenBPriceUsd + (1 / midPrice.toNumber()) * tokenAPriceUsd
- // 迭代调整 baseAmount
- 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 数量(实际需要的,未加 slippage)
- const otherAmountNeeded = chain.getAmountAFromAmountB({
- priceLower,
- priceUpper,
- amountB: baseAmount,
- poolInfo,
- })
- // 添加 5% slippage 作为 otherAmountMax(允许的最大值)
- otherAmountMax = otherAmountNeeded.mul(new BN(10500)).div(new BN(10000))
- }
- // 计算最终总价值(使用实际投入的金额,而不是加了 slippage 的 otherAmountMax)
- // 实际投入的 otherAmount 是 otherAmountMax / 1.02(因为 otherAmountMax 包含了 2% slippage)
- const actualOtherAmount = otherAmountMax
- .mul(new BN(10000))
- .div(new BN(10200))
- const uiAmountA =
- base === 'MintA'
- ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsA)
- : new Decimal(actualOtherAmount.toString()).div(
- 10 ** poolInfo.mintDecimalsA
- )
- const uiAmountB =
- base === 'MintB'
- ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsB)
- : new Decimal(actualOtherAmount.toString()).div(
- 10 ** poolInfo.mintDecimalsB
- )
- let totalValue =
- uiAmountA.toNumber() * tokenAPriceUsd +
- uiAmountB.toNumber() * tokenBPriceUsd
- // 如果总价值仍然超过 maxUsdValue,按比例缩小
- if (totalValue > maxUsdValue) {
- const scale = maxUsdValue / totalValue
- if (base === 'MintA') {
- baseAmount = baseAmount
- .mul(new BN(Math.floor(scale * 10000)))
- .div(new BN(10000))
- otherAmountMax = chain
- .getAmountBFromAmountA({
- priceLower,
- priceUpper,
- amountA: baseAmount,
- poolInfo,
- })
- .mul(new BN(10200))
- .div(new BN(10000))
- } else {
- baseAmount = baseAmount
- .mul(new BN(Math.floor(scale * 10000)))
- .div(new BN(10000))
- otherAmountMax = chain
- .getAmountAFromAmountB({
- priceLower,
- priceUpper,
- amountB: baseAmount,
- poolInfo,
- })
- .mul(new BN(10200))
- .div(new BN(10000))
- }
- // 重新计算总价值
- const finalUiAmountA =
- base === 'MintA'
- ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsA)
- : new Decimal(otherAmountMax.toString()).div(
- 10 ** poolInfo.mintDecimalsA
- )
- const finalUiAmountB =
- base === 'MintB'
- ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsB)
- : new Decimal(otherAmountMax.toString()).div(
- 10 ** poolInfo.mintDecimalsB
- )
- totalValue =
- finalUiAmountA.toNumber() * tokenAPriceUsd +
- finalUiAmountB.toNumber() * tokenBPriceUsd
- }
- // 重新计算最终的 UI 金额和总价值
- const finalUiAmountA =
- base === 'MintA'
- ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsA)
- : new Decimal(otherAmountMax.toString()).div(
- 10 ** poolInfo.mintDecimalsA
- )
- const finalUiAmountB =
- base === 'MintB'
- ? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsB)
- : new Decimal(otherAmountMax.toString()).div(
- 10 ** poolInfo.mintDecimalsB
- )
- totalValue =
- finalUiAmountA.toNumber() * tokenAPriceUsd +
- finalUiAmountB.toNumber() * tokenBPriceUsd
- // 打印所有信息
- console.log('\n========== LP Copy Information ==========')
- console.log('Original Position Address:', positionAddress)
- console.log('Referer Position (NFT Mint):', 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('Tick Lower:', tickLower)
- console.log('Tick Upper:', tickUpper)
- console.log('Price Lower:', priceLower.toString())
- console.log('Price Upper:', priceUpper.toString())
- console.log('\n--- Investment Details ---')
- console.log('Max USD Value:', maxUsdValue)
- 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):', finalUiAmountA.toString())
- console.log('Token B Amount (UI):', finalUiAmountB.toString())
- console.log(
- 'Token A Value (USD):',
- (finalUiAmountA.toNumber() * tokenAPriceUsd).toFixed(2)
- )
- console.log(
- 'Token B Value (USD):',
- (finalUiAmountB.toNumber() * tokenBPriceUsd).toFixed(2)
- )
- console.log('Total Value (USD):', totalValue.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('Creating position on-chain...')
- // 执行实际上链操作
- const signerCallback = async (tx: VersionedTransaction) => {
- tx.sign([userKeypair])
- return tx
- }
- const txid = await chain.createPosition({
- userAddress,
- poolInfo,
- tickLower,
- tickUpper,
- base,
- baseAmount,
- otherAmountMax,
- refererPosition: positionAddress, // 添加 referer position
- signerCallback,
- })
- console.log('Transaction ID:', txid)
- console.log('Position created successfully!')
- console.log('==========================================\n')
- return NextResponse.json({
- success: true,
- txid,
- positionInfo: {
- poolAddress: poolInfo.poolId.toBase58(),
- priceLower: priceLower.toString(),
- priceUpper: priceUpper.toString(),
- tickLower,
- tickUpper,
- base,
- baseAmount: baseAmount.toString(),
- otherAmountMax: otherAmountMax.toString(),
- estimatedValue: totalValue,
- tokenA: {
- address: poolInfo.mintA.toBase58(),
- amount: finalUiAmountA.toString(),
- valueUsd: (finalUiAmountA.toNumber() * tokenAPriceUsd).toFixed(2),
- },
- tokenB: {
- address: poolInfo.mintB.toBase58(),
- amount: finalUiAmountB.toString(),
- valueUsd: (finalUiAmountB.toNumber() * tokenBPriceUsd).toFixed(2),
- },
- refererPosition: positionAddress,
- userAddress: userAddress?.toBase58() || 'N/A',
- },
- })
- } catch (error: unknown) {
- console.error('LP copy error:', error)
- const errorMessage =
- error instanceof Error ? error.message : 'Failed to copy LP position'
- return NextResponse.json({ error: errorMessage }, { status: 500 })
- }
- }
|