Browse Source

refactor: optimize swap logic and improve error handling

- jupiter.js: Improve swap logic with better error handling and ExactOut mode
- sniper.js: Clean up executeCopy flow and remove redundant code
- byreal.js: Add debug logging for API responses
- test-copy.js: Simplify test script and remove unused code
lushdog@outlook.com 1 tháng trước cách đây
mục cha
commit
3edc4f49fb
4 tập tin đã thay đổi với 93 bổ sung134 xóa
  1. 15 37
      src/core/sniper.js
  2. 1 0
      src/services/byreal.js
  3. 66 47
      src/services/jupiter.js
  4. 11 50
      test-copy.js

+ 15 - 37
src/core/sniper.js

@@ -94,53 +94,31 @@ export class SniperEngine {
     const tokenA = calculation.tokenA;
     const tokenB = calculation.tokenB;
 
-    logger.success(`Calculation complete:`);
-    logger.info(`  Token A: ${tokenA.amount} ${tokenA.symbol || ''} (${formatUsd(tokenA.valueUsd)})`);
-    logger.info(`  Token B: ${tokenB.amount} ${tokenB.symbol || ''} (${formatUsd(tokenB.valueUsd)})`);
+    logger.success(`Calculation complete (using valueUsd for swap):`);
+    logger.info(`  Token A: ${tokenA.symbol || ''} ${formatUsd(tokenA.valueUsd)}`);
+    logger.info(`  Token B: ${tokenB.symbol || ''} ${formatUsd(tokenB.valueUsd)}`);
     logger.info(`  Estimated Total Value: ${formatUsd(calculation.estimatedValue)}`);
 
-    // Step 2: Check and swap Token A (add 10% buffer for slippage)
-    const tokenABalance = await this.swapper.getTokenBalance(tokenA.address);
-    const requiredAmountA = parseFloat(tokenA.amount);
-    const swapAmountA = requiredAmountA * 1.1; // Add 10% buffer
-    
-    if (tokenABalance < requiredAmountA * 0.95) {
-      logger.info(`Token A balance insufficient: ${tokenABalance.toFixed(4)} < ${requiredAmountA.toFixed(4)}`);
-      logger.info(`Swapping with 10% buffer: ${swapAmountA.toFixed(4)} (required: ${requiredAmountA.toFixed(4)})`);
-        const success = await this.swapper.swapIfNeeded(
-          'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // Use USDC as source
-          tokenA.address,
-          swapAmountA,
-          tokenA.decimals
-        );
+    // Step 2: Swap by USD value (ExactIn) - no amount, use valueUsd only
+    const valueA = parseFloat(tokenA.valueUsd) || 0;
+    if (valueA > 0) {
+      logger.info(`Swapping $${valueA} USDC -> Token A (${tokenA.symbol || ''})...`);
+      const success = await this.swapper.swapIfNeeded(tokenA.address, valueA);
       if (!success) {
-        logger.error('Failed to acquire sufficient Token A');
+        logger.error('Failed to acquire Token A');
         return false;
       }
-    } else {
-      logger.success(`Token A balance sufficient: ${tokenABalance.toFixed(4)} >= ${requiredAmountA.toFixed(4)}`);
     }
 
-    // Step 3: Check and swap Token B (add 10% buffer for slippage)
-    const tokenBBalance = await this.swapper.getTokenBalance(tokenB.address);
-    const requiredAmountB = parseFloat(tokenB.amount);
-    const swapAmountB = requiredAmountB * 1.1; // Add 10% buffer
-    
-    if (tokenBBalance < requiredAmountB * 0.95) {
-      logger.info(`Token B balance insufficient: ${tokenBBalance.toFixed(4)} < ${requiredAmountB.toFixed(4)}`);
-      logger.info(`Swapping with 10% buffer: ${swapAmountB.toFixed(4)} (required: ${requiredAmountB.toFixed(4)})`);
-      const success = await this.swapper.swapIfNeeded(
-        'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // Use USDC as source
-        tokenB.address,
-        swapAmountB,
-        tokenB.decimals
-      );
+    // Step 3: Swap by USD value (ExactIn)
+    const valueB = parseFloat(tokenB.valueUsd) || 0;
+    if (valueB > 0) {
+      logger.info(`Swapping $${valueB} USDC -> Token B (${tokenB.symbol || ''})...`);
+      const success = await this.swapper.swapIfNeeded(tokenB.address, valueB);
       if (!success) {
-        logger.error('Failed to acquire sufficient Token B');
+        logger.error('Failed to acquire Token B');
         return false;
       }
-    } else {
-      logger.success(`Token B balance sufficient: ${tokenBBalance.toFixed(4)} >= ${requiredAmountB.toFixed(4)}`);
     }
 
     // Step 4: Execute copy of PARENT position

+ 1 - 0
src/services/byreal.js

@@ -124,6 +124,7 @@ export class ByrealAPI {
         nftMintAddress: nftMintAddress,
         positionAddress: positionAddress,
         maxUsdValue: maxUsdValue,
+        needSwap: false
       };
 
       const headers = {

+ 66 - 47
src/services/jupiter.js

@@ -75,24 +75,31 @@ export class JupiterSwapper {
 
   /**
    * Fetch quote from Jupiter Metis API with retry logic
+   * amount: raw amount (lamports/atomic units); must be string for API (ExactIn=input, ExactOut=output)
+   * restrictIntermediate: if true, only use liquid intermediate tokens; if false, allow more routes (use when NO_ROUTES_FOUND)
    */
-  async fetchQuote(inputMint, outputMint, amount, retries = 3, swapMode = 'ExactIn') {
+  async fetchQuote(inputMint, outputMint, amount, retries = 3, swapMode = 'ExactIn', restrictIntermediate = true) {
+    const rawAmount = typeof amount === 'string' ? amount : String(Math.floor(Number(amount)));
+    if (!rawAmount || Number(rawAmount) <= 0) {
+      throw new Error(`Invalid quote amount: ${amount}`);
+    }
+
     for (let attempt = 1; attempt <= retries; attempt++) {
       try {
         logger.info(`Fetching quote from Jupiter Metis API (attempt ${attempt}/${retries})...`);
-        logger.info(`  Mode: ${swapMode}, Amount: ${amount}`);
+        logger.info(`  Mode: ${swapMode}, Amount (raw): ${rawAmount}, restrictIntermediate: ${restrictIntermediate}`);
         
         const response = await axios.get(
           `${this.jupiterBaseUrl}/quote`,
           {
             params: {
-              inputMint: inputMint,
-              outputMint: outputMint,
-              amount: amount,
-              swapMode: swapMode,
+              inputMint,
+              outputMint,
+              amount: rawAmount,
+              swapMode,
               slippageBps: CONFIG.SLIPPAGE_BPS,
               onlyDirectRoutes: false,
-              restrictIntermediateTokens: true,
+              restrictIntermediateTokens: restrictIntermediate,
             },
             timeout: 30000,
             headers: this.getHeaders(),
@@ -105,14 +112,21 @@ export class JupiterSwapper {
         }
       } catch (error) {
         const status = error.response?.status;
-        const message = error.response?.data?.message || error.message;
-        
+        const data = error.response?.data;
+        const message = data?.message || data?.error || error.message;
+        const fullBody = data ? JSON.stringify(data) : '';
+
         if (status === 401) {
           logger.error('Jupiter API authentication failed. Please check your API key at https://portal.jup.ag');
           throw new Error('Jupiter API Key invalid or missing');
         }
-        
-        logger.warn(`Quote attempt ${attempt} failed: ${message}`);
+
+        if (status === 400) {
+          logger.warn(`Quote 400 Bad Request: ${message}`);
+          if (fullBody) logger.warn(`Response: ${fullBody}`);
+        } else {
+          logger.warn(`Quote attempt ${attempt} failed: ${message}`);
+        }
         
         if (attempt < retries) {
           await sleep(2000 * attempt);
@@ -173,19 +187,20 @@ export class JupiterSwapper {
     }
   }
 
-  async swapIfNeeded(inputMint, outputMint, requiredAmount, decimals = 6) {
-    const currentBalance = await this.getTokenBalance(outputMint);
-    
-    if (currentBalance >= requiredAmount) {
-      logger.success(`Sufficient balance: ${currentBalance.toFixed(4)} >= ${requiredAmount.toFixed(4)}`);
+  /**
+   * Swap a fixed USD amount of USDC into the output token (ExactIn).
+   * Uses valueUsd from calculation API - no ExactOut, no token amount.
+   * @param {string} outputMint - Token mint to receive
+   * @param {number} usdValue - USD value to swap (e.g. 5.5 = $5.5 USDC in)
+   * @returns {Promise<boolean>}
+   */
+  async swapIfNeeded(outputMint, usdValue) {
+    const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
+    if (!usdValue || Number(usdValue) <= 0) {
+      logger.info(`Skip swap: USD value is ${usdValue}`);
       return true;
     }
 
-    const neededAmount = requiredAmount - currentBalance;
-    logger.warn(`Insufficient balance. Need ${neededAmount.toFixed(4)} more.`);
-    logger.info(`Initiating swap from ${inputMint} to ${outputMint}...`);
-
-    // Check if API key is configured
     if (!this.apiKey) {
       logger.error('═══════════════════════════════════════════════════');
       logger.error('Jupiter API Key is required!');
@@ -195,58 +210,62 @@ export class JupiterSwapper {
       return false;
     }
 
+    const inputAmountRaw = Math.floor(Number(usdValue) * 1e6);
+    if (inputAmountRaw < 1e6) {
+      logger.warn(`Skip swap: USD value too small (${usdValue})`);
+      return true;
+    }
+
+    logger.info(`Swap: $${usdValue} USDC -> ${outputMint.slice(0, 8)}... (ExactIn)`);
+
     try {
-      // Use ExactOut mode: we need exactly `neededAmount` of the output token
-      // Convert to raw amount with correct decimals (from API)
-      const outputAmount = Math.ceil(neededAmount * Math.pow(10, decimals));
-      
-      logger.info(`Swap: USDC -> ${outputMint.slice(0, 8)}...`);
-      logger.info(`Required output: ${neededAmount} (decimals: ${decimals}, raw: ${outputAmount})`);
-      
-      // Fetch quote with ExactOut swap mode
-      const quoteData = await this.fetchQuote(
-        inputMint,
-        outputMint,
-        outputAmount,
-        3,
-        'ExactOut'
-      );
+      const tryQuote = async (restrictIntermediate = true) => {
+        try {
+          return await this.fetchQuote(
+            USDC_MINT,
+            outputMint,
+            inputAmountRaw,
+            2,
+            'ExactIn',
+            restrictIntermediate
+          );
+        } catch (e) {
+          if (e.message && e.message.includes('No routes found') && restrictIntermediate) {
+            logger.warn('No routes with restrictIntermediate=true, retrying with allow all intermediates...');
+            return await this.fetchQuote(USDC_MINT, outputMint, inputAmountRaw, 2, 'ExactIn', false);
+          }
+          throw e;
+        }
+      };
+
+      const quoteData = await tryQuote();
 
-      // Log route info
       if (quoteData.routePlan && quoteData.routePlan.length > 0) {
         const routeLabels = quoteData.routePlan.map(r => r.swapInfo?.label || 'Unknown').join(' -> ');
         logger.info(`Route: ${routeLabels}`);
-        logger.info(`Expected output: ${quoteData.outAmount} (${quoteData.swapMode})`);
-        logger.info(`Price impact: ${quoteData.priceImpactPct}%`);
+        logger.info(`Expected output: ${quoteData.outAmount} (ExactIn), price impact: ${quoteData.priceImpactPct}%`);
       }
 
-      // Execute swap with retry
       const swapData = await this.executeSwap(quoteData);
-
       const swapTransactionBuf = Buffer.from(swapData.swapTransaction, 'base64');
       const transaction = VersionedTransaction.deserialize(swapTransactionBuf);
-      
       transaction.sign([this.keypair]);
 
       const signature = await this.connection.sendTransaction(transaction, {
         maxRetries: 3,
         skipPreflight: false,
       });
-
       logger.info(`Swap transaction sent: ${signature}`);
 
       const confirmation = await this.connection.confirmTransaction(signature, 'confirmed');
-      
       if (confirmation.value.err) {
         throw new Error(`Transaction failed: ${confirmation.value.err}`);
       }
 
       logger.success(`Swap confirmed: https://solscan.io/tx/${signature}`);
-      
       const newBalance = await this.getTokenBalance(outputMint);
       logger.info(`New balance: ${newBalance.toFixed(4)}`);
-      
-      return newBalance >= requiredAmount;
+      return true;
     } catch (error) {
       logger.error('Swap failed:', error.message);
       return false;

+ 11 - 50
test-copy.js

@@ -121,71 +121,32 @@ async function testCopy() {
   console.log(`    Token B valueUsd field: ${calculation.tokenB?.valueUsd}`);
   console.log(`    If amount ≈ valueUsd, then amount is USD value, not token quantity!`);
 
-  // Step 4: Check balances and swap
-  console.log('\nStep 4: Checking balances and swapping...');
+  // Step 4: Swap by USD value (ExactIn) - use valueUsd only, no amount
+  console.log('\nStep 4: Swapping by valueUsd (ExactIn)...');
   const swapper = new JupiterSwapper();
-  
   const tokenA = calculation.tokenA;
   const tokenB = calculation.tokenB;
-  
-  // Token A
-  console.log(`\n  Token A (${tokenA.symbol}):`);
-  const tokenABalance = await swapper.getTokenBalance(tokenA.address);
-  const requiredAmountA = parseFloat(tokenA.amount);
-  const swapAmountA = requiredAmountA * 1.1;
-  
-  console.log(`    Balance: ${tokenABalance.toFixed(6)}`);
-  console.log(`    Required: ${requiredAmountA.toFixed(6)}`);
-  console.log(`    With 10% buffer: ${swapAmountA.toFixed(6)}`);
-  
-  if (tokenABalance < requiredAmountA * 0.95) {
-    console.log(`    ⚠️  Insufficient balance, need to swap from USDC`);
 
-    // Ask for confirmation
-    console.log('\n    Ready to swap. Continue? (Ctrl+C to cancel)');
+  const valueA = parseFloat(tokenA.valueUsd) || 0;
+  if (valueA > 0) {
+    console.log(`\n  Token A (${tokenA.symbol}): swap $${valueA} USDC -> ${tokenA.symbol}`);
+    console.log('    Ready to swap. Continue? (Ctrl+C to cancel)');
     await new Promise(resolve => setTimeout(resolve, 3000));
-
-    const success = await swapper.swapIfNeeded(
-      'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
-      tokenA.address,
-      swapAmountA,
-      tokenA.decimals
-    );
-    
+    const success = await swapper.swapIfNeeded(tokenA.address, valueA);
     if (!success) {
       console.error('❌ Failed to acquire Token A');
       process.exit(1);
     }
-  } else {
-    console.log(`    ✅ Balance sufficient`);
   }
 
-  // Token B
-  console.log(`\n  Token B (${tokenB.symbol}):`);
-  const tokenBBalance = await swapper.getTokenBalance(tokenB.address);
-  const requiredAmountB = parseFloat(tokenB.amount);
-  const swapAmountB = requiredAmountB * 1.1;
-  
-  console.log(`    Balance: ${tokenBBalance.toFixed(6)}`);
-  console.log(`    Required: ${requiredAmountB.toFixed(6)}`);
-  console.log(`    With 10% buffer: ${swapAmountB.toFixed(6)}`);
-  
-  if (tokenBBalance < requiredAmountB * 0.95) {
-    console.log(`    ⚠️  Insufficient balance, need to swap from USDC`);
-    
-    const success = await swapper.swapIfNeeded(
-      'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
-      tokenB.address,
-      swapAmountB,
-      tokenB.decimals
-    );
-    
+  const valueB = parseFloat(tokenB.valueUsd) || 0;
+  if (valueB > 0) {
+    console.log(`\n  Token B (${tokenB.symbol}): swap $${valueB} USDC -> ${tokenB.symbol}`);
+    const success = await swapper.swapIfNeeded(tokenB.address, valueB);
     if (!success) {
       console.error('❌ Failed to acquire Token B');
       process.exit(1);
     }
-  } else {
-    console.log(`    ✅ Balance sufficient`);
   }
 
   // Step 5: Execute copy