Ver Fonte

feat: custom price range

lushdog@outlook.com há 2 semanas atrás
pai
commit
cd88ae3957
2 ficheiros alterados com 101 adições e 7 exclusões
  1. 78 5
      src/app/api/lp-copy/route.ts
  2. 23 2
      src/app/components/DataTable.tsx

+ 78 - 5
src/app/api/lp-copy/route.ts

@@ -4,11 +4,18 @@ import BN from 'bn.js'
 import { Decimal } from 'decimal.js'
 import bs58 from 'bs58'
 import { chain } from '@/lib/config'
+import { TickMath } from '@/lib/byreal-clmm-sdk/src/instructions/utils/tickMath'
 
 export async function POST(request: NextRequest) {
 	try {
 		const body = await request.json()
-		const { positionAddress, maxUsdValue, nftMintAddress } = body
+		const {
+			positionAddress,
+			maxUsdValue,
+			nftMintAddress,
+			priceLower: priceLowerPct = 0,
+			priceUpper: priceUpperPct = 0,
+		} = body
 
 		if (!positionAddress) {
 			return NextResponse.json(
@@ -41,8 +48,9 @@ export async function POST(request: NextRequest) {
 			return NextResponse.json({ error: 'Position not found' }, { status: 404 })
 		}
 
-		const { rawPoolInfo, priceLower, priceUpper } = positionInfo
+		const { rawPoolInfo } = positionInfo
 		const poolInfo = rawPoolInfo
+		const tickSpacing = poolInfo.tickSpacing
 
 		// 使用 currentPrice (A/B 价格) 来计算 token 的 USD 价格
 		// currentPrice 是 tokenA / tokenB 的比率
@@ -87,9 +95,69 @@ export async function POST(request: NextRequest) {
 		console.log('tokenBPriceUsd', tokenBPriceUsd)
 
 		// 计算比例,使得总价值不超过 maxUsdValue
-		// 使用 position 的 tickLower 和 tickUpper
-		const tickLower = positionInfo.rawPositionInfo.tickLower
-		const tickUpper = positionInfo.rawPositionInfo.tickUpper
+		// tickLower/tickUpper 默认使用父仓位的范围;当传入 priceLower/priceUpper(百分比) 且都不为 0 时覆盖
+		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)
+			// 语义:
+			// - priceLower: 当前价格下拉百分比(10 表示下拉 10%,即 current * 0.9)
+			// - priceUpper: 当前价格上浮百分比(10 表示上浮 10%,即 current * 1.1)
+			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)
+
+			// 对齐到 tickSpacing,避免 on-chain 创建失败
+			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/priceUpper,保证后续 amount 计算与 tick 范围一致
+			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
@@ -347,6 +415,11 @@ export async function POST(request: NextRequest) {
 		console.log('Tick Upper:', tickUpper)
 		console.log('Price Lower:', priceLower.toString())
 		console.log('Price Upper:', priceUpper.toString())
+		if (shouldOverrideTicks) {
+			console.log('\n--- Range Override (Percent) ---')
+			console.log('Input priceLower(% of current):', priceLowerPct)
+			console.log('Input priceUpper(% of current):', priceUpperPct)
+		}
 		console.log('\n--- Investment Details ---')
 		console.log('Max USD Value:', maxUsdValue)
 		console.log('Base Token:', base)

+ 23 - 2
src/app/components/DataTable.tsx

@@ -87,6 +87,9 @@ function DataTableContent() {
 	const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
 	const [batchCopying, setBatchCopying] = useState(false)
 
+	const [priceLower, setPriceLower] = useState<number>(0)
+	const [priceUpper, setPriceUpper] = useState<number>(0)
+
 	// 获取 pools 列表
 	const fetchPoolsList = async () => {
 		try {
@@ -209,6 +212,8 @@ function DataTableContent() {
 				positionAddress: record.positionAddress,
 				nftMintAddress: record.nftMintAddress,
 				maxUsdValue: quickCopyAmount,
+				priceLower,
+				priceUpper,
 			}),
 		})
 			.then((res) => res.json())
@@ -265,6 +270,8 @@ function DataTableContent() {
 						positionAddress: record.positionAddress,
 						nftMintAddress: record.nftMintAddress,
 						maxUsdValue: quickCopyAmount,
+						priceLower,
+						priceUpper,
 					}),
 				})
 
@@ -467,7 +474,6 @@ function DataTableContent() {
 			| unknown
 	) => {
 		let currentSortField = sortField
-		let shouldResetPage = false
 		if (newPagination.current && newPagination.current !== pagination.current) {
 			const targetPage = newPagination.current
 			const targetPageSize = newPagination.pageSize || pagination.pageSize
@@ -501,7 +507,6 @@ function DataTableContent() {
 					const newSortField = fieldMap[sorterObj.field] || sortField
 					if (newSortField !== sortField) {
 						currentSortField = newSortField
-						shouldResetPage = true
 					}
 				}
 			}
@@ -729,6 +734,22 @@ function DataTableContent() {
 					step={0.1}
 					onChange={(value) => setQuickCopyAmount(value as number)}
 				/>
+				<span className="mr-2 ml-2">价格下拉(%):</span>
+				<InputNumber
+					placeholder="价格下拉(%)"
+					value={priceLower}
+					min={0}
+					max={99.99}
+					onChange={(value) => setPriceLower(value as number)}
+				/>
+				<span className="mr-2 ml-2">价格上浮(%):</span>
+				<InputNumber
+					placeholder="价格上浮(%)"
+					value={priceUpper}
+					min={0}
+					max={100000}
+					onChange={(value) => setPriceUpper(value as number)}
+				/>
 				{selectedRowKeys.length > 0 && (
 					<Button
 						type="primary"