Bladeren bron

feat: 余额检查按 mint 汇总、接口增加 needSwap 参数默认不换币

- jupiter: getTokenBalance 改为 getParsedTokenAccountsByOwner 按 mint 汇总,解决 pump 等代币在非 ATA 时误判为 0 的问题
- jupiter: swapIfNeeded 增加 current/required 余额日志
- lp-copy API: 新增 needSwap 参数,默认 false,为 false 时仅检查余额不自动换币
- 前端 lp-copy 与 DataTable 请求显式传 needSwap: false

Co-authored-by: Cursor <cursoragent@cursor.com>
lushdog@outlook.com 1 maand geleden
bovenliggende
commit
392e95a0b2
4 gewijzigde bestanden met toevoegingen van 144 en 154 verwijderingen
  1. 60 62
      src/app/api/lp-copy/route.ts
  2. 2 2
      src/app/components/DataTable.tsx
  3. 1 0
      src/app/lp-copy/page.tsx
  4. 81 90
      src/lib/jupiter.ts

+ 60 - 62
src/app/api/lp-copy/route.ts

@@ -10,7 +10,7 @@ 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'
-import { ensureSufficientBalances } from '@/lib/jupiter'
+import { ensureSufficientBalances, getTokenBalance } from '@/lib/jupiter'
 
 async function copyLPPosition(
 	request: NextRequest,
@@ -23,6 +23,7 @@ async function copyLPPosition(
 		nftMintAddress,
 		priceLower: priceLowerPct = 0,
 		priceUpper: priceUpperPct = 0,
+		needSwap = false, // 是否需要换币,默认否:不换币时仅检查余额,不足则报错
 	} = body
 
 	try {
@@ -537,82 +538,79 @@ async function copyLPPosition(
 		const userKeypair = Keypair.fromSecretKey(bs58.decode(secretKey))
 		const userAddress = userKeypair.publicKey
 
-		// ========== 检查并确保余额充足 ==========
-		console.log('\n--- Checking Wallet Balances ---')
-
-		// 计算实际需要的最小代币数量(不包含 slippage)
-		const neededBaseAmount = baseAmount
-		const neededOtherAmount =
-			base === 'MintA'
-				? otherAmountMax.mul(new BN(10000)).div(new BN(10200)) // 减去 2% slippage
-				: otherAmountMax.mul(new BN(10000)).div(new BN(10500)) // 减去 5% slippage
+		const tokenAValueUsd =
+			finalUiAmountA.toNumber() * tokenAPriceUsd
+		const tokenBValueUsd =
+			finalUiAmountB.toNumber() * tokenBPriceUsd
 
-		// 准备代币信息
 		const tokenAInfo = {
 			mint: poolInfo.mintA.toBase58(),
-			amount:
-				base === 'MintA'
-					? new Decimal(neededBaseAmount.toString())
-							.div(10 ** poolInfo.mintDecimalsA)
-							.toNumber()
-					: new Decimal(neededOtherAmount.toString())
-							.div(10 ** poolInfo.mintDecimalsA)
-							.toNumber(),
-			decimals: poolInfo.mintDecimalsA,
+			valueUsd: tokenAValueUsd,
 		}
 
 		const tokenBInfo = {
 			mint: poolInfo.mintB.toBase58(),
-			amount:
-				base === 'MintB'
-					? new Decimal(neededBaseAmount.toString())
-							.div(10 ** poolInfo.mintDecimalsB)
-							.toNumber()
-					: new Decimal(neededOtherAmount.toString())
-							.div(10 ** poolInfo.mintDecimalsB)
-							.toNumber(),
-			decimals: poolInfo.mintDecimalsB,
+			valueUsd: tokenBValueUsd,
 		}
 
-		console.log(`Token A (${tokenAInfo.mint.slice(0, 8)}...):`, {
-			required: tokenAInfo.amount.toFixed(6),
-			decimals: tokenAInfo.decimals,
-		})
-		console.log(`Token B (${tokenBInfo.mint.slice(0, 8)}...):`, {
-			required: tokenBInfo.amount.toFixed(6),
-			decimals: tokenBInfo.decimals,
-		})
-
-		// 创建 connection
 		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 }
+		if (needSwap) {
+			// ========== 按 valueUsd 换币(ExactIn)==========
+			console.log('\n--- Swapping by valueUsd (ExactIn) ---')
+			console.log(`Token A (${tokenAInfo.mint.slice(0, 8)}...): swap $${tokenAValueUsd.toFixed(2)} USDC`)
+			console.log(`Token B (${tokenBInfo.mint.slice(0, 8)}...): swap $${tokenBValueUsd.toFixed(2)} USDC`)
+
+			const balanceCheck = await ensureSufficientBalances(
+				connection,
+				userKeypair,
+				tokenAInfo,
+				tokenBInfo
 			)
-		}
 
-		if (balanceCheck.swapTxids.length > 0) {
-			console.log('\n--- Swap Transactions ---')
-			balanceCheck.swapTxids.forEach((txid, index) => {
-				console.log(`Swap ${index + 1}: https://solscan.io/tx/${txid}`)
-			})
+			if (!balanceCheck.success) {
+				console.error('Failed to ensure sufficient balances:', balanceCheck.error)
+				return NextResponse.json(
+					{
+						error: '余额不足且自动换币失败',
+						details: balanceCheck.error,
+						swapTxids: balanceCheck.swapTxids,
+					},
+					{ status: 400 }
+				)
+			}
+
+			if (balanceCheck.swapTxids.length > 0) {
+				console.log('\n--- Swap Transactions ---')
+				balanceCheck.swapTxids.forEach((txid, index) => {
+					console.log(`Swap ${index + 1}: https://solscan.io/tx/${txid}`)
+				})
+			}
+		} else {
+			// ========== 不换币:仅检查余额是否足够 ==========
+			console.log('\n--- Checking balances (no swap) ---')
+			const requiredA = finalUiAmountA.toNumber()
+			const requiredB = finalUiAmountB.toNumber()
+			const [balanceA, balanceB] = await Promise.all([
+				getTokenBalance(connection, userAddress, tokenAInfo.mint),
+				getTokenBalance(connection, userAddress, tokenBInfo.mint),
+			])
+			console.log(`Token A: balance=${balanceA.toFixed(6)}, required=${requiredA.toFixed(6)}`)
+			console.log(`Token B: balance=${balanceB.toFixed(6)}, required=${requiredB.toFixed(6)}`)
+			if (balanceA < requiredA || balanceB < requiredB) {
+				return NextResponse.json(
+					{
+						error: '余额不足',
+						details: {
+							tokenA: { balance: balanceA, required: requiredA, mint: tokenAInfo.mint },
+							tokenB: { balance: balanceB, required: requiredB, mint: tokenBInfo.mint },
+						},
+					},
+					{ status: 400 }
+				)
+			}
 		}
 
 		console.log('\n--- Balances Sufficient, Proceeding ---')

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

@@ -356,8 +356,7 @@ function DataTableContent() {
 				positionAddress: record.positionAddress,
 				nftMintAddress: record.nftMintAddress,
 				maxUsdValue: quickCopyAmount,
-				// priceLower,
-				// priceUpper,
+				needSwap: false,
 			}),
 		})
 			.then((res) => res.json())
@@ -413,6 +412,7 @@ function DataTableContent() {
 						positionAddress: record.positionAddress,
 						nftMintAddress: record.nftMintAddress,
 						maxUsdValue: quickCopyAmount,
+						needSwap: false,
 					}),
 				})
 

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

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

+ 81 - 90
src/lib/jupiter.ts

@@ -86,25 +86,30 @@ function getJupiterHeaders(): Record<string, string> {
 
 /**
  * 从 Jupiter API 获取 quote
+ * @param restrictIntermediate - 为 false 时允许更多中间代币路由(用于 NO_ROUTES_FOUND 时重试)
  */
 export async function fetchJupiterQuote(
 	inputMint: string,
 	outputMint: string,
-	amount: number,
-	swapMode: 'ExactIn' | 'ExactOut' = 'ExactOut',
-	slippageBps: number = 200 // 2%
+	amount: string | number,
+	swapMode: 'ExactIn' | 'ExactOut' = 'ExactIn',
+	slippageBps: number = 200, // 2%
+	restrictIntermediate: boolean = true
 ): Promise<JupiterQuote> {
 	const jupiterBaseUrl =
 		process.env.JUPITER_API_URL || 'https://api.jup.ag/swap/v1'
 
+	const rawAmount =
+		typeof amount === 'string' ? amount : String(Math.floor(Number(amount)))
+
 	const searchParams = new URLSearchParams({
 		inputMint,
 		outputMint,
-		amount: String(amount),
+		amount: rawAmount,
 		swapMode,
 		slippageBps: String(slippageBps),
 		onlyDirectRoutes: 'false',
-		restrictIntermediateTokens: 'true',
+		restrictIntermediateTokens: restrictIntermediate ? 'true' : 'false',
 	})
 
 	const response = await ky
@@ -151,89 +156,92 @@ export async function executeJupiterSwap(
 }
 
 /**
- * 如果需要,执行 swap 以获取足够的代币余额
- * @returns 是否成功获取足够的余额
+ * 按 USD 金额执行 swap(ExactIn:花掉指定美元 USDC,换回目标代币)
+ * 不使用 ExactOut,不使用代币数量 amount,仅使用 valueUsd。
+ * @param usdValue - 要换入的美元数(例如 5.5 表示花 $5.5 USDC)
  */
 export async function swapIfNeeded(
 	connection: Connection,
 	keypair: Keypair,
 	outputMint: string,
-	requiredAmount: number,
-	decimals: number,
+	usdValue: number,
 	inputMint: string = USDC_MINT
 ): Promise<{ success: boolean; txid?: string; error?: string }> {
 	const walletAddress = keypair.publicKey
 
-	// 检查当前余额(按 mint 汇总所有代币账户,不限于 ATA)
-	const currentBalance = await getTokenBalance(
-		connection,
-		walletAddress,
-		outputMint
-	)
-
-	console.log(
-		`Balance check: current=${currentBalance.toFixed(6)}, required=${requiredAmount.toFixed(6)}`
-	)
+	if (!usdValue || Number(usdValue) <= 0) {
+		console.log(`Skip swap: USD value is ${usdValue}`)
+		return { success: true }
+	}
 
-	if (currentBalance >= requiredAmount) {
-		console.log(`Sufficient balance: ${currentBalance.toFixed(6)} >= ${requiredAmount.toFixed(6)}`)
+	const inputAmountRaw = Math.floor(Number(usdValue) * 1e6)
+	if (inputAmountRaw < 1e6) {
+		console.log(`Skip swap: USD value too small (${usdValue})`)
 		return { success: true }
 	}
 
-	// 增加 10% buffer 防止价格波动导致余额不足
-	const neededAmount = (requiredAmount - currentBalance) * 1.1
 	console.log(
-		`Insufficient balance. Need ${neededAmount.toFixed(6)} more of ${outputMint.slice(0, 8)}... (with 10% buffer)`
+		`Swap: $${usdValue} USDC -> ${outputMint.slice(0, 8)}... (ExactIn)`
 	)
-	console.log(
-		`Initiating swap from ${inputMint.slice(0, 8)}... to ${outputMint.slice(0, 8)}...`
-	)
-
-	try {
-		// 计算需要的 raw amount
-		const outputAmount = Math.ceil(neededAmount * Math.pow(10, decimals))
 
-		console.log(
-			`Swap: ${inputMint.slice(0, 8)}... -> ${outputMint.slice(0, 8)}...`
-		)
-		console.log(
-			`Required output: ${neededAmount} (decimals: ${decimals}, raw: ${outputAmount})`
-		)
+	const slippageBps = 200 // 2%
+
+	const fetchQuoteWithRetry = async (
+		restrictIntermediate: boolean
+	): Promise<JupiterQuote> => {
+		try {
+			return await fetchJupiterQuote(
+				inputMint,
+				outputMint,
+				inputAmountRaw,
+				'ExactIn',
+				slippageBps,
+				restrictIntermediate
+			)
+		} catch (e) {
+			const msg = e instanceof Error ? e.message : String(e)
+			if (
+				msg.includes('No routes found') &&
+				restrictIntermediate
+			) {
+				console.log(
+					'No routes with restrictIntermediate=true, retrying with allow all intermediates...'
+				)
+				return fetchJupiterQuote(
+					inputMint,
+					outputMint,
+					inputAmountRaw,
+					'ExactIn',
+					slippageBps,
+					false
+				)
+			}
+			throw e
+		}
+	}
 
-		// 获取 quote (使用 ExactOut 模式)
-		const quoteData = await fetchJupiterQuote(
-			inputMint,
-			outputMint,
-			outputAmount,
-			'ExactOut',
-			200 // 2% slippage
-		)
+	try {
+		const quoteData = await fetchQuoteWithRetry(true)
 
-		// 记录路由信息
 		if (quoteData.routePlan && quoteData.routePlan.length > 0) {
 			const routeLabels = quoteData.routePlan
 				.map((r) => r.swapInfo?.label || 'Unknown')
 				.join(' -> ')
 			console.log(`Route: ${routeLabels}`)
 			console.log(
-				`Expected output: ${quoteData.outAmount} (${quoteData.swapMode})`
+				`Expected output: ${quoteData.outAmount} (ExactIn), price impact: ${quoteData.priceImpactPct}%`
 			)
-			console.log(`Price impact: ${quoteData.priceImpactPct}%`)
 		}
 
-		// 执行 swap
 		const swapData = await executeJupiterSwap(
 			quoteData,
 			walletAddress.toBase58()
 		)
 
-		// 反序列化并签名交易
 		const swapTransactionBuf = Buffer.from(swapData.swapTransaction, 'base64')
 		const transaction = VersionedTransaction.deserialize(swapTransactionBuf)
-
 		transaction.sign([keypair])
 
-		// 发送交易
 		const signature = await connection.sendTransaction(transaction, {
 			maxRetries: 3,
 			skipPreflight: false,
@@ -241,7 +249,6 @@ export async function swapIfNeeded(
 
 		console.log(`Swap transaction sent: ${signature}`)
 
-		// 确认交易
 		const confirmation = await connection.confirmTransaction(
 			signature,
 			'confirmed'
@@ -252,8 +259,6 @@ export async function swapIfNeeded(
 		}
 
 		console.log(`Swap confirmed: https://solscan.io/tx/${signature}`)
-
-		// 检查新余额
 		const newBalance = await getTokenBalance(
 			connection,
 			walletAddress,
@@ -261,14 +266,6 @@ export async function swapIfNeeded(
 		)
 		console.log(`New balance: ${newBalance.toFixed(6)}`)
 
-		if (newBalance < requiredAmount) {
-			return {
-				success: false,
-				txid: signature,
-				error: `Swap completed but balance still insufficient: ${newBalance.toFixed(6)} < ${requiredAmount.toFixed(6)}`,
-			}
-		}
-
 		return { success: true, txid: signature }
 	} catch (error) {
 		const errorMessage =
@@ -283,13 +280,13 @@ export async function swapIfNeeded(
 
 /**
  * 确保 LP 所需的两种代币都有足够余额
- * 如果不足,尝试使用 USDC 兑换
+ * 按 valueUsd 换币:使用 ExactIn,花 $valueUsd USDC 换目标代币,不使用 amount
  */
 export async function ensureSufficientBalances(
 	connection: Connection,
 	keypair: Keypair,
-	tokenA: { mint: string; amount: number; decimals: number },
-	tokenB: { mint: string; amount: number; decimals: number }
+	tokenA: { mint: string; valueUsd: number },
+	tokenB: { mint: string; valueUsd: number }
 ): Promise<{
 	success: boolean
 	swapTxids: string[]
@@ -297,26 +294,25 @@ export async function ensureSufficientBalances(
 }> {
 	const swapTxids: string[] = []
 
-	// 检查 tokenA
-	console.log(`\n--- Checking Token A (${tokenA.mint.slice(0, 8)}...) ---`)
+	// Token A:按 valueUsd 换
+	console.log(
+		`\n--- Token A (${tokenA.mint.slice(0, 8)}...): swap $${tokenA.valueUsd} USDC ---`
+	)
 	const resultA = await swapIfNeeded(
 		connection,
 		keypair,
 		tokenA.mint,
-		tokenA.amount,
-		tokenA.decimals,
+		tokenA.valueUsd,
 		USDC_MINT
 	)
 
 	if (!resultA.success) {
-		// 如果 USDC 失败,尝试使用 USDT
-		console.log('USDC swap failed, trying USDT...')
+		console.log('USDC swap failed for Token A, trying USDT...')
 		const resultA_USDT = await swapIfNeeded(
 			connection,
 			keypair,
 			tokenA.mint,
-			tokenA.amount,
-			tokenA.decimals,
+			tokenA.valueUsd,
 			USDT_MINT
 		)
 
@@ -324,37 +320,34 @@ export async function ensureSufficientBalances(
 			return {
 				success: false,
 				swapTxids,
-				error: `Failed to get sufficient balance for Token A: ${resultA.error || resultA_USDT.error}`,
+				error: `Failed to get Token A: ${resultA.error || resultA_USDT.error}`,
 			}
 		}
 
-		if (resultA_USDT.txid) {
-			swapTxids.push(resultA_USDT.txid)
-		}
+		if (resultA_USDT.txid) swapTxids.push(resultA_USDT.txid)
 	} else if (resultA.txid) {
 		swapTxids.push(resultA.txid)
 	}
 
-	// 检查 tokenB
-	console.log(`\n--- Checking Token B (${tokenB.mint.slice(0, 8)}...) ---`)
+	// Token B:按 valueUsd 换
+	console.log(
+		`\n--- Token B (${tokenB.mint.slice(0, 8)}...): swap $${tokenB.valueUsd} USDC ---`
+	)
 	const resultB = await swapIfNeeded(
 		connection,
 		keypair,
 		tokenB.mint,
-		tokenB.amount,
-		tokenB.decimals,
+		tokenB.valueUsd,
 		USDC_MINT
 	)
 
 	if (!resultB.success) {
-		// 如果 USDC 失败,尝试使用 USDT
-		console.log('USDC swap failed, trying USDT...')
+		console.log('USDC swap failed for Token B, trying USDT...')
 		const resultB_USDT = await swapIfNeeded(
 			connection,
 			keypair,
 			tokenB.mint,
-			tokenB.amount,
-			tokenB.decimals,
+			tokenB.valueUsd,
 			USDT_MINT
 		)
 
@@ -362,13 +355,11 @@ export async function ensureSufficientBalances(
 			return {
 				success: false,
 				swapTxids,
-				error: `Failed to get sufficient balance for Token B: ${resultB.error || resultB_USDT.error}`,
+				error: `Failed to get Token B: ${resultB.error || resultB_USDT.error}`,
 			}
 		}
 
-		if (resultB_USDT.txid) {
-			swapTxids.push(resultB_USDT.txid)
-		}
+		if (resultB_USDT.txid) swapTxids.push(resultB_USDT.txid)
 	} else if (resultB.txid) {
 		swapTxids.push(resultB.txid)
 	}