Kaynağa Gözat

fix: fallback to ExactIn when ExactOut quote is unavailable

Jupiter ExactOut is only supported by a few AMMs. When ExactOut
quote returns 400, we now estimate the needed input amount via a
reference ExactIn quote and add a 15% buffer to ensure sufficient
output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
zhangchunrui 2 hafta önce
ebeveyn
işleme
e156bfb22a
1 değiştirilmiş dosya ile 58 ekleme ve 7 silme
  1. 58 7
      src/lib/copier/swap.ts

+ 58 - 7
src/lib/copier/swap.ts

@@ -214,7 +214,9 @@ async function signAndExecuteSwap(
 }
 
 /**
- * ExactOut swap via Jupiter Swap v1 API (quote → swap → sign → send)
+ * ExactOut swap via Jupiter Swap v1 API.
+ * If ExactOut is not supported for the pair (400), falls back to ExactIn
+ * by estimating the input amount from a reference quote + 15% buffer.
  */
 async function signAndExecuteSwapV1(
   inputMint: string,
@@ -227,14 +229,27 @@ async function signAndExecuteSwapV1(
   const connection = (await import('../solana/connection')).getConnection()
 
   try {
-    // 1. Get quote
-    const quoteResponse = await fetchSwapQuote(inputMint, outputMint, amount, slippageBps, swapMode)
-    console.log(`[Swap] v1 ${swapMode} quote: in=${quoteResponse.inAmount}, out=${quoteResponse.outAmount}`)
+    let quoteResponse: Record<string, unknown>
 
-    // 2. Get swap transaction
+    if (swapMode === 'ExactOut') {
+      try {
+        // Try ExactOut first
+        quoteResponse = await fetchSwapQuote(inputMint, outputMint, amount, slippageBps, 'ExactOut')
+      } catch {
+        // ExactOut not supported for this pair, fall back to ExactIn
+        console.log('[Swap] ExactOut not available, estimating ExactIn amount via reference quote')
+        quoteResponse = await estimateExactInQuote(inputMint, outputMint, amount, slippageBps)
+      }
+    } else {
+      quoteResponse = await fetchSwapQuote(inputMint, outputMint, amount, slippageBps, swapMode)
+    }
+
+    console.log(`[Swap] v1 quote: in=${quoteResponse.inAmount}, out=${quoteResponse.outAmount}, mode=${quoteResponse.swapMode}`)
+
+    // Get swap transaction
     const { swapTransaction } = await fetchSwapTransaction(quoteResponse, keypair.publicKey.toBase58())
 
-    // 3. Sign and send
+    // Sign and send
     const txBuf = Buffer.from(swapTransaction, 'base64')
     const transaction = VersionedTransaction.deserialize(txBuf)
     transaction.sign([keypair])
@@ -244,7 +259,7 @@ async function signAndExecuteSwapV1(
       maxRetries: 3,
     })
 
-    // 4. Confirm
+    // Confirm
     const latestBlockhash = await connection.getLatestBlockhash('confirmed')
     await connection.confirmTransaction(
       { signature, ...latestBlockhash },
@@ -260,6 +275,42 @@ async function signAndExecuteSwapV1(
   }
 }
 
+/**
+ * ExactOut 不可用时,通过参考报价估算所需 ExactIn 输入量。
+ * 1. 用小额 ExactIn 报价获取汇率
+ * 2. 按汇率反推所需输入量,加 15% buffer
+ * 3. 用计算出的输入量获取正式 ExactIn 报价
+ */
+async function estimateExactInQuote(
+  inputMint: string,
+  outputMint: string,
+  desiredOutputAmount: string,
+  slippageBps: number,
+): Promise<Record<string, unknown>> {
+  // Use 10 USDC (10_000_000 raw) as reference to get the exchange rate.
+  // For non-USDC input tokens, use 10 units of whatever the input is (6-decimal assumed).
+  const refInputAmount = '10000000'
+  const refQuote = await fetchSwapQuote(inputMint, outputMint, refInputAmount, slippageBps, 'ExactIn')
+
+  const refIn = BigInt(refQuote.inAmount as string)
+  const refOut = BigInt(refQuote.outAmount as string)
+
+  if (refOut === 0n) {
+    throw new Error('Reference quote returned 0 output, cannot estimate rate')
+  }
+
+  // estimatedInput = desiredOutput * (refIn / refOut) * 1.15
+  const desiredOut = BigInt(desiredOutputAmount)
+  const estimatedInput = (desiredOut * refIn * 115n) / (refOut * 100n)
+
+  console.log(
+    `[Swap] Rate estimate: ${refIn}→${refOut}, need ${desiredOut} out, estimated input ${estimatedInput} (+15% buffer)`,
+  )
+
+  // Get actual ExactIn quote with estimated input amount
+  return fetchSwapQuote(inputMint, outputMint, estimatedInput.toString(), slippageBps, 'ExactIn')
+}
+
 /**
  * 获取代币余额(raw amount)
  */