Переглянути джерело

feat: use calculate API to get exact token amounts for LP positions

- Add COPY_CALCULATE_URL config for /api/lp-copy/calculate endpoint
- Add calculatePosition() method to ByrealAPI service
- Update executeCopy() to use calculate API instead of 50/50 split
- Get exact tokenA and tokenB amounts from parent position calculation
- Swap exact amounts needed for each token individually
- Store calculation details in cache for reference
zhangchunrui 1 місяць тому
батько
коміт
dba81b6c8f
3 змінених файлів з 80 додано та 29 видалено
  1. 1 0
      src/config/index.js
  2. 43 29
      src/core/sniper.js
  3. 36 0
      src/services/byreal.js

+ 1 - 0
src/config/index.js

@@ -31,6 +31,7 @@ export const CONFIG = {
   BYREAL_API_BASE: 'https://api2.byreal.io/byreal/api/dex/v2',
   TICK_API: 'https://love.hdlife.me/api/tick-to-price',
   COPY_ACTION_URL: 'https://love.hdlife.me/api/lp-copy',
+  COPY_CALCULATE_URL: 'https://love.hdlife.me/api/lp-copy/calculate',
   CLOSE_ACTION_URL: 'https://love.hdlife.me/api/lp-close',
   JUPITER_API: 'https://quote-api.jup.ag/v6',
   

+ 43 - 29
src/core/sniper.js

@@ -60,55 +60,68 @@ export class SniperEngine {
     logger.info(`Copying PARENT position with ${CONFIG.COPY_MULTIPLIER}x multiplier`);
     logger.info(`Parent Target: ${formatUsd(targetUsdValue)} → Copy: ${formatUsd(copyUsdValue)}`);
 
-    const mintA = poolInfo.mintA?.address;
-    const mintB = poolInfo.mintB?.address;
-    
-    if (!mintA || !mintB) {
-      logger.error('Pool info missing token addresses');
+    // Step 1: Calculate exact token amounts needed using the calculate API
+    logger.info('Calculating exact token amounts from parent position...');
+    const calculation = await ByrealAPI.calculatePosition(
+      parentPositionAddress,
+      copyUsdValue,
+      parentDetail.nftMint
+    );
+
+    if (!calculation) {
+      logger.error('Failed to calculate token amounts, aborting copy');
       return false;
     }
 
-    // Get token prices and prepare swap
-    const tokenPrices = await this.swapper.getTokenPrices([mintA, mintB]);
-    const priceA = tokenPrices[mintA]?.price || 1;
-    const priceB = tokenPrices[mintB]?.price || 1;
-
-    const halfUsd = copyUsdValue / 2;
-    const amountA = halfUsd / priceA;
-    const amountB = halfUsd / priceB;
+    const tokenA = calculation.tokenA;
+    const tokenB = calculation.tokenB;
 
-    logger.info(`Estimated token needs: ${amountA.toFixed(4)} TokenA, ${amountB.toFixed(4)} 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.info(`  Estimated Total Value: ${formatUsd(calculation.estimatedValue)}`);
 
-    // Check and swap tokens
-    const tokenABalance = await this.swapper.getTokenBalance(mintA);
-    if (tokenABalance < amountA * 0.95) {
+    // Step 2: Check and swap Token A
+    const tokenABalance = await this.swapper.getTokenBalance(tokenA.address);
+    const requiredAmountA = parseFloat(tokenA.amount);
+    
+    if (tokenABalance < requiredAmountA * 0.95) {
+      logger.info(`Token A balance insufficient: ${tokenABalance.toFixed(4)} < ${requiredAmountA.toFixed(4)}`);
       const success = await this.swapper.swapIfNeeded(
-        'So11111111111111111111111111111111111111112',
-        mintA,
-        amountA,
-        poolInfo.mintA?.decimals || 6
+        'So11111111111111111111111111111111111111112', // Use SOL as source
+        tokenA.address,
+        requiredAmountA,
+        tokenA.decimals
       );
       if (!success) {
         logger.error('Failed to acquire sufficient Token A');
         return false;
       }
+    } else {
+      logger.success(`Token A balance sufficient: ${tokenABalance.toFixed(4)} >= ${requiredAmountA.toFixed(4)}`);
     }
 
-    const tokenBBalance = await this.swapper.getTokenBalance(mintB);
-    if (tokenBBalance < amountB * 0.95) {
+    // Step 3: Check and swap Token B
+    const tokenBBalance = await this.swapper.getTokenBalance(tokenB.address);
+    const requiredAmountB = parseFloat(tokenB.amount);
+    
+    if (tokenBBalance < requiredAmountB * 0.95) {
+      logger.info(`Token B balance insufficient: ${tokenBBalance.toFixed(4)} < ${requiredAmountB.toFixed(4)}`);
       const success = await this.swapper.swapIfNeeded(
-        'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
-        mintB,
-        amountB,
-        poolInfo.mintB?.decimals || 6
+        'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // Use USDC as source
+        tokenB.address,
+        requiredAmountB,
+        tokenB.decimals
       );
       if (!success) {
         logger.error('Failed to acquire sufficient Token B');
         return false;
       }
+    } else {
+      logger.success(`Token B balance sufficient: ${tokenBBalance.toFixed(4)} >= ${requiredAmountB.toFixed(4)}`);
     }
 
-    // Execute copy of PARENT position
+    // Step 4: Execute copy of PARENT position
     const success = await ByrealAPI.copyPosition(
       parentDetail.nftMint,
       parentPositionAddress,
@@ -120,7 +133,8 @@ export class SniperEngine {
         poolAddress: parentDetail.poolAddress,
         targetUsdValue,
         copiedAt: new Date().toISOString(),
-        targetPositionAddress: targetPositionAddress, // Store which target position triggered this copy
+        targetPositionAddress: targetPositionAddress,
+        calculation: calculation, // Store calculation details
       });
       // Map target's position to the parent position we copied
       this.targetToParentMap.set(targetPositionAddress, parentPositionAddress);

+ 36 - 0
src/services/byreal.js

@@ -83,6 +83,42 @@ export class ByrealAPI {
     }
   }
 
+  static async calculatePosition(positionAddress, maxUsdValue, nftMintAddress = null) {
+    try {
+      const body = {
+        positionAddress: positionAddress,
+        maxUsdValue: maxUsdValue,
+      };
+
+      if (nftMintAddress) {
+        body.nftMintAddress = nftMintAddress;
+      }
+
+      const headers = {
+        Authorization: CONFIG.AUTH_HEADER,
+        'Content-Type': 'application/json',
+      };
+
+      logger.info('Calculating token amounts for position...');
+      
+      const response = await axios.post(CONFIG.COPY_CALCULATE_URL, body, { headers });
+      
+      if (response.data?.success) {
+        logger.success(`Calculation completed for position: ${positionAddress}`);
+        return response.data.calculation;
+      } else {
+        logger.error('Calculation request failed:', response.data);
+        return null;
+      }
+    } catch (error) {
+      logger.error('Error calculating position:', error.message);
+      if (error.response) {
+        logger.error('Server response:', error.response.data);
+      }
+      return null;
+    }
+  }
+
   static async copyPosition(nftMintAddress, positionAddress, maxUsdValue) {
     try {
       const body = {