Explorar o código

feat: lp-add 支持 needSwap 自动换币,my-lp 狙击列体验优化

- lp-add: 新增 needSwap 参数(默认 false),余额不足时调用 ensureSufficientBalances 自动换币
- my-lp: 狙击列 InputNumber 显示最终金额(基础×倍数),随 Slider 滑动更新
- my-lp: 狙击列 InputNumber 保留 2 位小数 (precision=2)

Co-authored-by: Cursor <cursoragent@cursor.com>
lushdog@outlook.com hai 1 mes
pai
achega
8edf5a0b73

+ 60 - 15
src/app/api/lp-index/lp-add/route.ts

@@ -1,15 +1,23 @@
 import { NextRequest, NextResponse } from 'next/server'
-import { Keypair, PublicKey, VersionedTransaction } from '@solana/web3.js'
+import {
+	Keypair,
+	PublicKey,
+	VersionedTransaction,
+	Connection,
+} from '@solana/web3.js'
 import BN from 'bn.js'
 import { Decimal } from 'decimal.js'
 import bs58 from 'bs58'
 import { chain } from '@/lib/config'
-import { getTokenPricesFromJupiter } from '@/lib/jupiter'
+import {
+	getTokenPricesFromJupiter,
+	ensureSufficientBalances,
+} from '@/lib/jupiter'
 
 export async function POST(request: NextRequest) {
 	try {
 		const body = await request.json()
-		const { nftMintAddress, addUsdValue } = body
+		const { nftMintAddress, addUsdValue, needSwap = false } = body
 
 		// 验证输入
 		if (!nftMintAddress) {
@@ -300,6 +308,55 @@ export async function POST(request: NextRequest) {
 						10 ** poolInfo.mintDecimalsB
 					)
 
+		// 从环境变量读取私钥(余额检查/换币需要用到)
+		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
+
+		const tokenAValueUsd = uiAmountA.toNumber() * tokenAPriceUsd
+		const tokenBValueUsd = uiAmountB.toNumber() * tokenBPriceUsd
+		const tokenAInfo = {
+			mint: tokenAAddress,
+			valueUsd: tokenAValueUsd,
+		}
+		const tokenBInfo = {
+			mint: tokenBAddress,
+			valueUsd: tokenBValueUsd,
+		}
+
+		if (needSwap) {
+			const rpcUrl =
+				process.env.SOL_RPC_URL || 'https://api.mainnet-beta.solana.com'
+			const connection = new Connection(rpcUrl, 'confirmed')
+			const balanceCheck = await ensureSufficientBalances(
+				connection,
+				userKeypair,
+				tokenAInfo,
+				tokenBInfo
+			)
+			if (!balanceCheck.success) {
+				console.error(
+					'Failed to ensure sufficient balances:',
+					balanceCheck.error
+				)
+				return NextResponse.json(
+					{
+						error: '余额不足且自动换币失败',
+						details: balanceCheck.error,
+						swapTxids: balanceCheck.swapTxids,
+					},
+					{ status: 400 }
+				)
+			}
+		}
+
 		// 打印所有信息
 		console.log('\n========== Add Liquidity Information ==========')
 		console.log('Position NFT Address:', nftMint.toBase58())
@@ -332,18 +389,6 @@ export async function POST(request: NextRequest) {
 		)
 		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...')

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

@@ -356,7 +356,7 @@ function DataTableContent() {
 				positionAddress: record.positionAddress,
 				nftMintAddress: record.nftMintAddress,
 				maxUsdValue: quickCopyAmount,
-				needSwap: false,
+				needSwap: true,
 			}),
 		})
 			.then((res) => res.json())
@@ -412,7 +412,7 @@ function DataTableContent() {
 						positionAddress: record.positionAddress,
 						nftMintAddress: record.nftMintAddress,
 						maxUsdValue: quickCopyAmount,
-						needSwap: false,
+						needSwap: true,
 					}),
 				})
 

+ 1 - 1
src/app/lp-copy/page.tsx

@@ -48,7 +48,7 @@ function LpCopyPageContent() {
 				body: JSON.stringify({
 					positionAddress: values.positionAddress,
 					maxUsdValue: values.maxUsdValue,
-					needSwap: false,
+					needSwap: true,
 				}),
 			})
 

+ 18 - 11
src/app/my-lp/page.tsx

@@ -400,7 +400,10 @@ function MyLPPageContent() {
 		else return 'red'
 	}
 
-	function formatPositionAgeMs(ms: number | undefined, openTime?: number): string {
+	function formatPositionAgeMs(
+		ms: number | undefined,
+		openTime?: number
+	): string {
 		if (ms != null && !Number.isNaN(ms)) {
 			const days = Math.floor(ms / (24 * 60 * 60 * 1000))
 			const hours = Math.floor((ms % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000))
@@ -487,30 +490,34 @@ function MyLPPageContent() {
 		{
 			title: '狙击',
 			key: 'snipe',
-			width: 280,
+			width: 380,
 			render: (_: unknown, record: RecordInfo) => {
 				const key = record.positionAddress
 				const baseAmount = Number(record.liquidityUsd) || 0
 				const amount = snipeChildState[key]?.amount ?? baseAmount
 				const multiplier = snipeChildState[key]?.multiplier ?? 1
 				const finalAmount = amount * multiplier
+				// InputNumber 显示最终金额(基础×倍数),滑动时随倍数变化
 				return (
-					<div className="flex items-center gap-2 flex-wrap">
+					<div className="flex items-center gap-4 flex-wrap">
 						<InputNumber
 							size="small"
-							className="font-mono w-24"
 							min={0}
 							step={0.01}
-							value={amount}
-							onChange={(val) =>
+							precision={2}
+							style={{ width: '90px' }}
+							value={finalAmount}
+							onChange={(val) => {
+								const newFinal = Number(val) ?? 0
+								const mult = snipeChildState[key]?.multiplier ?? 1
 								setSnipeChildState((prev) => ({
 									...prev,
 									[key]: {
-										amount: Number(val) ?? 0,
-										multiplier: prev[key]?.multiplier ?? 1,
+										amount: mult > 0 ? newFinal / mult : newFinal,
+										multiplier: mult,
 									},
 								}))
-							}
+							}}
 						/>
 						<Slider
 							className="w-20! m-0! shrink-0"
@@ -528,10 +535,10 @@ function MyLPPageContent() {
 								}))
 							}
 						/>
-						<span className="font-mono text-xs text-gray-500 w-6">
+						<span className="font-mono text-xs text-gray-500 w-6 mr-2">
 							×{multiplier}
 						</span>
-						<Tooltip title="狙击">
+						<Tooltip title="狙击(倍数×金额)">
 							<Typography.Link
 								onClick={() => handleSnipeAddPosition(record, finalAmount)}
 								className="inline-flex items-center"