Pārlūkot izejas kodu

fix: use Jupiter Swap v1 API for ExactOut swaps

Jupiter Ultra API does not support swapMode=ExactOut and returns 400.
ExactOut (used for balance top-up before open/add) now goes through
the standard Jupiter Swap v1 API (quote + swap + sign + send).
ExactIn (used for post-close USDC recovery) stays on Ultra.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
zhangchunrui 2 nedēļas atpakaļ
vecāks
revīzija
4666a0a1c2
1 mainītis faili ar 105 papildinājumiem un 1 dzēšanām
  1. 105 1
      src/lib/copier/swap.ts

+ 105 - 1
src/lib/copier/swap.ts

@@ -15,6 +15,7 @@ const STABLECOIN_MINTS = new Set([USDC_MINT, USDT_MINT, USD1_MINT])
 const SKIP_SWAP_BACK_MINTS = new Set([USDC_MINT, USDT_MINT, SOL_MINT, USD1_MINT])
 
 const ULTRA_API_URL = 'https://api.jup.ag/ultra/v1'
+const SWAP_API_URL = 'https://api.jup.ag/swap/v1'
 
 interface UltraOrderResponse {
   requestId: string
@@ -110,7 +111,58 @@ async function executeUltraSwap(
 }
 
 /**
- * 签名并执行 Ultra swap
+ * Jupiter Swap v1 API: GET /quote (supports ExactOut)
+ */
+async function fetchSwapQuote(
+  inputMint: string,
+  outputMint: string,
+  amount: string,
+  slippageBps: number,
+  swapMode: 'ExactIn' | 'ExactOut',
+): Promise<Record<string, unknown>> {
+  return ky
+    .get(`${SWAP_API_URL}/quote`, {
+      searchParams: {
+        inputMint,
+        outputMint,
+        amount,
+        slippageBps: String(slippageBps),
+        swapMode,
+      },
+      timeout: 30000,
+      headers: getJupiterHeaders(),
+    })
+    .json<Record<string, unknown>>()
+}
+
+/**
+ * Jupiter Swap v1 API: POST /swap (returns unsigned transaction)
+ */
+async function fetchSwapTransaction(
+  quoteResponse: Record<string, unknown>,
+  userPublicKey: string,
+): Promise<{ swapTransaction: string; lastValidBlockHeight: number }> {
+  return ky
+    .post(`${SWAP_API_URL}/swap`, {
+      json: {
+        quoteResponse,
+        userPublicKey,
+        wrapAndUnwrapSol: true,
+        dynamicComputeUnitLimit: true,
+      },
+      timeout: 30000,
+      headers: {
+        ...getJupiterHeaders(),
+        'Content-Type': 'application/json',
+      },
+    })
+    .json<{ swapTransaction: string; lastValidBlockHeight: number }>()
+}
+
+/**
+ * 签名并执行 swap
+ * - ExactIn: 使用 Jupiter Ultra API(更快)
+ * - ExactOut: 使用 Jupiter Swap v1 API(Ultra 不支持 ExactOut)
  */
 async function signAndExecuteSwap(
   inputMint: string,
@@ -122,6 +174,11 @@ async function signAndExecuteSwap(
   const keypair = getKeypair()
 
   try {
+    if (swapMode === 'ExactOut') {
+      return await signAndExecuteSwapV1(inputMint, outputMint, amount, slippageBps, swapMode)
+    }
+
+    // ExactIn: use Ultra API
     const orderData = await fetchUltraOrder(inputMint, outputMint, amount, keypair.publicKey.toBase58(), slippageBps, swapMode)
 
     if (!orderData.transaction) {
@@ -156,6 +213,53 @@ async function signAndExecuteSwap(
   }
 }
 
+/**
+ * ExactOut swap via Jupiter Swap v1 API (quote → swap → sign → send)
+ */
+async function signAndExecuteSwapV1(
+  inputMint: string,
+  outputMint: string,
+  amount: string,
+  slippageBps: number,
+  swapMode: 'ExactIn' | 'ExactOut',
+): Promise<{ success: boolean; txid?: string; error?: string }> {
+  const keypair = getKeypair()
+  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}`)
+
+    // 2. Get swap transaction
+    const { swapTransaction } = await fetchSwapTransaction(quoteResponse, keypair.publicKey.toBase58())
+
+    // 3. Sign and send
+    const txBuf = Buffer.from(swapTransaction, 'base64')
+    const transaction = VersionedTransaction.deserialize(txBuf)
+    transaction.sign([keypair])
+
+    const signature = await connection.sendRawTransaction(transaction.serialize(), {
+      skipPreflight: true,
+      maxRetries: 3,
+    })
+
+    // 4. Confirm
+    const latestBlockhash = await connection.getLatestBlockhash('confirmed')
+    await connection.confirmTransaction(
+      { signature, ...latestBlockhash },
+      'confirmed',
+    )
+
+    console.log(`[Swap] v1 Confirmed: ${signature}`)
+    return { success: true, txid: signature }
+  } catch (error) {
+    const errorMessage = error instanceof Error ? error.message : 'Unknown swap error'
+    console.error(`[Swap] v1 Failed:`, errorMessage)
+    return { success: false, error: errorMessage }
+  }
+}
+
 /**
  * 获取代币余额(raw amount)
  */