|
@@ -86,25 +86,30 @@ function getJupiterHeaders(): Record<string, string> {
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 从 Jupiter API 获取 quote
|
|
* 从 Jupiter API 获取 quote
|
|
|
|
|
+ * @param restrictIntermediate - 为 false 时允许更多中间代币路由(用于 NO_ROUTES_FOUND 时重试)
|
|
|
*/
|
|
*/
|
|
|
export async function fetchJupiterQuote(
|
|
export async function fetchJupiterQuote(
|
|
|
inputMint: string,
|
|
inputMint: string,
|
|
|
outputMint: 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> {
|
|
): Promise<JupiterQuote> {
|
|
|
const jupiterBaseUrl =
|
|
const jupiterBaseUrl =
|
|
|
process.env.JUPITER_API_URL || 'https://api.jup.ag/swap/v1'
|
|
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({
|
|
const searchParams = new URLSearchParams({
|
|
|
inputMint,
|
|
inputMint,
|
|
|
outputMint,
|
|
outputMint,
|
|
|
- amount: String(amount),
|
|
|
|
|
|
|
+ amount: rawAmount,
|
|
|
swapMode,
|
|
swapMode,
|
|
|
slippageBps: String(slippageBps),
|
|
slippageBps: String(slippageBps),
|
|
|
onlyDirectRoutes: 'false',
|
|
onlyDirectRoutes: 'false',
|
|
|
- restrictIntermediateTokens: 'true',
|
|
|
|
|
|
|
+ restrictIntermediateTokens: restrictIntermediate ? 'true' : 'false',
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const response = await ky
|
|
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(
|
|
export async function swapIfNeeded(
|
|
|
connection: Connection,
|
|
connection: Connection,
|
|
|
keypair: Keypair,
|
|
keypair: Keypair,
|
|
|
outputMint: string,
|
|
outputMint: string,
|
|
|
- requiredAmount: number,
|
|
|
|
|
- decimals: number,
|
|
|
|
|
|
|
+ usdValue: number,
|
|
|
inputMint: string = USDC_MINT
|
|
inputMint: string = USDC_MINT
|
|
|
): Promise<{ success: boolean; txid?: string; error?: string }> {
|
|
): Promise<{ success: boolean; txid?: string; error?: string }> {
|
|
|
const walletAddress = keypair.publicKey
|
|
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 }
|
|
return { success: true }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 增加 10% buffer 防止价格波动导致余额不足
|
|
|
|
|
- const neededAmount = (requiredAmount - currentBalance) * 1.1
|
|
|
|
|
console.log(
|
|
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) {
|
|
if (quoteData.routePlan && quoteData.routePlan.length > 0) {
|
|
|
const routeLabels = quoteData.routePlan
|
|
const routeLabels = quoteData.routePlan
|
|
|
.map((r) => r.swapInfo?.label || 'Unknown')
|
|
.map((r) => r.swapInfo?.label || 'Unknown')
|
|
|
.join(' -> ')
|
|
.join(' -> ')
|
|
|
console.log(`Route: ${routeLabels}`)
|
|
console.log(`Route: ${routeLabels}`)
|
|
|
console.log(
|
|
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(
|
|
const swapData = await executeJupiterSwap(
|
|
|
quoteData,
|
|
quoteData,
|
|
|
walletAddress.toBase58()
|
|
walletAddress.toBase58()
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- // 反序列化并签名交易
|
|
|
|
|
const swapTransactionBuf = Buffer.from(swapData.swapTransaction, 'base64')
|
|
const swapTransactionBuf = Buffer.from(swapData.swapTransaction, 'base64')
|
|
|
const transaction = VersionedTransaction.deserialize(swapTransactionBuf)
|
|
const transaction = VersionedTransaction.deserialize(swapTransactionBuf)
|
|
|
-
|
|
|
|
|
transaction.sign([keypair])
|
|
transaction.sign([keypair])
|
|
|
|
|
|
|
|
- // 发送交易
|
|
|
|
|
const signature = await connection.sendTransaction(transaction, {
|
|
const signature = await connection.sendTransaction(transaction, {
|
|
|
maxRetries: 3,
|
|
maxRetries: 3,
|
|
|
skipPreflight: false,
|
|
skipPreflight: false,
|
|
@@ -241,7 +249,6 @@ export async function swapIfNeeded(
|
|
|
|
|
|
|
|
console.log(`Swap transaction sent: ${signature}`)
|
|
console.log(`Swap transaction sent: ${signature}`)
|
|
|
|
|
|
|
|
- // 确认交易
|
|
|
|
|
const confirmation = await connection.confirmTransaction(
|
|
const confirmation = await connection.confirmTransaction(
|
|
|
signature,
|
|
signature,
|
|
|
'confirmed'
|
|
'confirmed'
|
|
@@ -252,8 +259,6 @@ export async function swapIfNeeded(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
console.log(`Swap confirmed: https://solscan.io/tx/${signature}`)
|
|
console.log(`Swap confirmed: https://solscan.io/tx/${signature}`)
|
|
|
-
|
|
|
|
|
- // 检查新余额
|
|
|
|
|
const newBalance = await getTokenBalance(
|
|
const newBalance = await getTokenBalance(
|
|
|
connection,
|
|
connection,
|
|
|
walletAddress,
|
|
walletAddress,
|
|
@@ -261,14 +266,6 @@ export async function swapIfNeeded(
|
|
|
)
|
|
)
|
|
|
console.log(`New balance: ${newBalance.toFixed(6)}`)
|
|
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 }
|
|
return { success: true, txid: signature }
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
const errorMessage =
|
|
const errorMessage =
|
|
@@ -283,13 +280,13 @@ export async function swapIfNeeded(
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 确保 LP 所需的两种代币都有足够余额
|
|
* 确保 LP 所需的两种代币都有足够余额
|
|
|
- * 如果不足,尝试使用 USDC 兑换
|
|
|
|
|
|
|
+ * 按 valueUsd 换币:使用 ExactIn,花 $valueUsd USDC 换目标代币,不使用 amount
|
|
|
*/
|
|
*/
|
|
|
export async function ensureSufficientBalances(
|
|
export async function ensureSufficientBalances(
|
|
|
connection: Connection,
|
|
connection: Connection,
|
|
|
keypair: Keypair,
|
|
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<{
|
|
): Promise<{
|
|
|
success: boolean
|
|
success: boolean
|
|
|
swapTxids: string[]
|
|
swapTxids: string[]
|
|
@@ -297,26 +294,25 @@ export async function ensureSufficientBalances(
|
|
|
}> {
|
|
}> {
|
|
|
const swapTxids: string[] = []
|
|
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(
|
|
const resultA = await swapIfNeeded(
|
|
|
connection,
|
|
connection,
|
|
|
keypair,
|
|
keypair,
|
|
|
tokenA.mint,
|
|
tokenA.mint,
|
|
|
- tokenA.amount,
|
|
|
|
|
- tokenA.decimals,
|
|
|
|
|
|
|
+ tokenA.valueUsd,
|
|
|
USDC_MINT
|
|
USDC_MINT
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
if (!resultA.success) {
|
|
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(
|
|
const resultA_USDT = await swapIfNeeded(
|
|
|
connection,
|
|
connection,
|
|
|
keypair,
|
|
keypair,
|
|
|
tokenA.mint,
|
|
tokenA.mint,
|
|
|
- tokenA.amount,
|
|
|
|
|
- tokenA.decimals,
|
|
|
|
|
|
|
+ tokenA.valueUsd,
|
|
|
USDT_MINT
|
|
USDT_MINT
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -324,37 +320,34 @@ export async function ensureSufficientBalances(
|
|
|
return {
|
|
return {
|
|
|
success: false,
|
|
success: false,
|
|
|
swapTxids,
|
|
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) {
|
|
} else if (resultA.txid) {
|
|
|
swapTxids.push(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(
|
|
const resultB = await swapIfNeeded(
|
|
|
connection,
|
|
connection,
|
|
|
keypair,
|
|
keypair,
|
|
|
tokenB.mint,
|
|
tokenB.mint,
|
|
|
- tokenB.amount,
|
|
|
|
|
- tokenB.decimals,
|
|
|
|
|
|
|
+ tokenB.valueUsd,
|
|
|
USDC_MINT
|
|
USDC_MINT
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
if (!resultB.success) {
|
|
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(
|
|
const resultB_USDT = await swapIfNeeded(
|
|
|
connection,
|
|
connection,
|
|
|
keypair,
|
|
keypair,
|
|
|
tokenB.mint,
|
|
tokenB.mint,
|
|
|
- tokenB.amount,
|
|
|
|
|
- tokenB.decimals,
|
|
|
|
|
|
|
+ tokenB.valueUsd,
|
|
|
USDT_MINT
|
|
USDT_MINT
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -362,13 +355,11 @@ export async function ensureSufficientBalances(
|
|
|
return {
|
|
return {
|
|
|
success: false,
|
|
success: false,
|
|
|
swapTxids,
|
|
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) {
|
|
} else if (resultB.txid) {
|
|
|
swapTxids.push(resultB.txid)
|
|
swapTxids.push(resultB.txid)
|
|
|
}
|
|
}
|