Преглед на файлове

feat: new api calculate

lushdog@outlook.com преди 1 месец
родител
ревизия
213ce25cd0
променени са 2 файла, в които са добавени 457 реда и са изтрити 0 реда
  1. 456 0
      src/app/api/lp-copy/calculate/route.ts
  2. 1 0
      src/app/my-lp/page.tsx

+ 456 - 0
src/app/api/lp-copy/calculate/route.ts

@@ -0,0 +1,456 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { PublicKey } from '@solana/web3.js'
+import BN from 'bn.js'
+import { Decimal } from 'decimal.js'
+import { chain } from '@/lib/config'
+import { TickMath } from '@/lib/byreal-clmm-sdk/src/instructions/utils/tickMath'
+
+export async function POST(request: NextRequest): Promise<NextResponse> {
+	const body = await request.json()
+	const {
+		positionAddress,
+		maxUsdValue,
+		nftMintAddress,
+		priceLower: priceLowerPct = 0,
+		priceUpper: priceUpperPct = 0,
+	} = body
+
+	try {
+		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 } = positionInfo
+		const poolInfo = rawPoolInfo
+		const tickSpacing = poolInfo.tickSpacing
+
+		// 检查 LP 是否在区间内
+		const currentTick = TickMath.getTickWithPriceAndTickspacing(
+			new Decimal(poolInfo.currentPrice),
+			tickSpacing,
+			poolInfo.mintDecimalsA,
+			poolInfo.mintDecimalsB
+		)
+		const positionTickLower = positionInfo.rawPositionInfo.tickLower
+		const positionTickUpper = positionInfo.rawPositionInfo.tickUpper
+
+		if (currentTick < positionTickLower || currentTick > positionTickUpper) {
+			return NextResponse.json(
+				{
+					error:
+						'Position is out of range (current price is outside the position tick range)',
+					currentTick,
+					tickLower: positionTickLower,
+					tickUpper: positionTickUpper,
+				},
+				{ status: 400 }
+			)
+		}
+
+		// 使用 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 获取真实 USD 价格
+		try {
+			const detailData = await fetch(
+				`https://api2.byreal.io/byreal/api/dex/v2/position/detail?address=${positionAddress}`
+			).then((res) => res.json())
+
+			const poolData = detailData.result?.data?.pool
+			if (poolData?.mintA?.price && poolData?.mintB?.price) {
+				tokenAPriceUsd = parseFloat(poolData.mintA.price)
+				tokenBPriceUsd = parseFloat(poolData.mintB.price)
+				priceFromApi = true
+			}
+		} 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 {
+				return NextResponse.json(
+					{
+						error:
+							'无法获取代币价格:非稳定币交易对且 API 价格获取失败。请确保交易对包含 USDC/USDT 或稍后重试。',
+						tokenA: tokenAAddress,
+						tokenB: tokenBAddress,
+					},
+					{ status: 400 }
+				)
+			}
+		}
+
+		// 计算比例,使得总价值不超过 maxUsdValue
+		let tickLower = positionInfo.rawPositionInfo.tickLower
+		let tickUpper = positionInfo.rawPositionInfo.tickUpper
+		let priceLower = positionInfo.priceLower
+		let priceUpper = positionInfo.priceUpper
+
+		const shouldOverrideTicks =
+			Number(priceLowerPct) !== 0 && Number(priceUpperPct) !== 0
+
+		if (shouldOverrideTicks) {
+			const currentPriceDecimal = new Decimal(currentPrice)
+			const lowerMultiplier = new Decimal(1).sub(
+				new Decimal(priceLowerPct).div(100)
+			)
+			const upperMultiplier = new Decimal(1).add(
+				new Decimal(priceUpperPct).div(100)
+			)
+
+			if (lowerMultiplier.lte(0) || upperMultiplier.lte(0)) {
+				return NextResponse.json(
+					{ error: 'Invalid priceLower/priceUpper percent' },
+					{ status: 400 }
+				)
+			}
+
+			const lowerPrice = currentPriceDecimal.mul(lowerMultiplier)
+			const upperPrice = currentPriceDecimal.mul(upperMultiplier)
+
+			const alignedLower = TickMath.getTickWithPriceAndTickspacing(
+				lowerPrice,
+				tickSpacing,
+				poolInfo.mintDecimalsA,
+				poolInfo.mintDecimalsB
+			)
+			const alignedUpper = TickMath.getTickWithPriceAndTickspacing(
+				upperPrice,
+				tickSpacing,
+				poolInfo.mintDecimalsA,
+				poolInfo.mintDecimalsB
+			)
+
+			tickLower = Math.min(alignedLower, alignedUpper)
+			tickUpper = Math.max(alignedLower, alignedUpper)
+
+			priceLower = TickMath.getPriceFromTick({
+				tick: tickLower,
+				decimalsA: poolInfo.mintDecimalsA,
+				decimalsB: poolInfo.mintDecimalsB,
+				baseIn: true,
+			})
+			priceUpper = TickMath.getPriceFromTick({
+				tick: tickUpper,
+				decimalsA: poolInfo.mintDecimalsA,
+				decimalsB: poolInfo.mintDecimalsB,
+				baseIn: true,
+			})
+		}
+
+		// 使用较小的 token 价格作为 base
+		const useTokenAAsBase = tokenAPriceUsd <= tokenBPriceUsd
+		const base = useTokenAAsBase ? 'MintA' : 'MintB'
+
+		const midPrice = priceLower.add(priceUpper).div(2)
+		const targetValue = maxUsdValue * 0.995
+
+		let baseAmount: BN
+		let otherAmountMax: BN
+
+		if (base === 'MintA') {
+			const estimatedPricePerBase =
+				tokenAPriceUsd + midPrice.toNumber() * tokenBPriceUsd
+
+			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
+			const otherAmountNeeded = chain.getAmountBFromAmountA({
+				priceLower,
+				priceUpper,
+				amountA: baseAmount,
+				poolInfo,
+			})
+			otherAmountMax = otherAmountNeeded.mul(new BN(10200)).div(new BN(10000))
+		} else {
+			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
+			const otherAmountNeeded = chain.getAmountAFromAmountB({
+				priceLower,
+				priceUpper,
+				amountB: baseAmount,
+				poolInfo,
+			})
+			otherAmountMax = otherAmountNeeded.mul(new BN(10500)).div(new BN(10000))
+		}
+
+		// 计算最终 UI 金额
+		const actualOtherAmount = otherAmountMax
+			.mul(new BN(10000))
+			.div(new BN(base === 'MintA' ? 10200 : 10500))
+
+		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))
+				const otherAmountNeeded = chain.getAmountBFromAmountA({
+					priceLower,
+					priceUpper,
+					amountA: baseAmount,
+					poolInfo,
+				})
+				otherAmountMax = otherAmountNeeded.mul(new BN(10200)).div(new BN(10000))
+			} else {
+				baseAmount = baseAmount
+					.mul(new BN(Math.floor(scale * 10000)))
+					.div(new BN(10000))
+				const otherAmountNeeded = chain.getAmountAFromAmountB({
+					priceLower,
+					priceUpper,
+					amountB: baseAmount,
+					poolInfo,
+				})
+				otherAmountMax = otherAmountNeeded.mul(new BN(10200)).div(new BN(10000))
+			}
+
+			// 重新计算
+			const finalOtherAmount = otherAmountMax
+				.mul(new BN(10000))
+				.div(new BN(10200))
+			const finalUiAmountA =
+				base === 'MintA'
+					? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsA)
+					: new Decimal(finalOtherAmount.toString()).div(
+							10 ** poolInfo.mintDecimalsA
+						)
+			const finalUiAmountB =
+				base === 'MintB'
+					? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsB)
+					: new Decimal(finalOtherAmount.toString()).div(
+							10 ** poolInfo.mintDecimalsB
+						)
+			totalValue =
+				finalUiAmountA.toNumber() * tokenAPriceUsd +
+				finalUiAmountB.toNumber() * tokenBPriceUsd
+		}
+
+		// 最终金额
+		const finalOtherAmount = otherAmountMax
+			.mul(new BN(10000))
+			.div(new BN(10200))
+		const finalUiAmountA =
+			base === 'MintA'
+				? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsA)
+				: new Decimal(finalOtherAmount.toString()).div(
+						10 ** poolInfo.mintDecimalsA
+					)
+		const finalUiAmountB =
+			base === 'MintB'
+				? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsB)
+				: new Decimal(finalOtherAmount.toString()).div(
+						10 ** poolInfo.mintDecimalsB
+					)
+		totalValue =
+			finalUiAmountA.toNumber() * tokenAPriceUsd +
+			finalUiAmountB.toNumber() * tokenBPriceUsd
+
+		return NextResponse.json({
+			success: true,
+			calculation: {
+				poolAddress: poolInfo.poolId.toBase58(),
+				priceLower: priceLower.toString(),
+				priceUpper: priceUpper.toString(),
+				tickLower,
+				tickUpper,
+				currentPrice,
+				estimatedValue: totalValue,
+				tokenA: {
+					address: poolInfo.mintA.toBase58(),
+					decimals: poolInfo.mintDecimalsA,
+					amount: finalUiAmountA.toString(),
+					amountRaw: (base === 'MintA'
+						? baseAmount
+						: otherAmountMax
+					).toString(),
+					priceUsd: tokenAPriceUsd,
+					valueUsd: (finalUiAmountA.toNumber() * tokenAPriceUsd).toFixed(2),
+				},
+				tokenB: {
+					address: poolInfo.mintB.toBase58(),
+					decimals: poolInfo.mintDecimalsB,
+					amount: finalUiAmountB.toString(),
+					amountRaw: (base === 'MintB'
+						? baseAmount
+						: otherAmountMax
+					).toString(),
+					priceUsd: tokenBPriceUsd,
+					valueUsd: (finalUiAmountB.toNumber() * tokenBPriceUsd).toFixed(2),
+				},
+				refererPosition: positionAddress,
+			},
+		})
+	} catch (error: unknown) {
+		console.error('LP copy calculate error:', error)
+		const errorMessage =
+			error instanceof Error ? error.message : 'Failed to calculate LP copy'
+		return NextResponse.json({ error: errorMessage }, { status: 500 })
+	}
+}

+ 1 - 0
src/app/my-lp/page.tsx

@@ -62,6 +62,7 @@ interface RecordInfo {
 	isInRange?: boolean
 	isParentPositionClosed?: boolean
 	parentLiquidityUsd?: string
+	bonusUsd?: string
 	bonusInfo?: {
 		fromCreatorWallet: string
 		fromCreatorPositionStatus: number