|
@@ -1,6 +1,7 @@
|
|
|
import { CONFIG } from '../config/index.js';
|
|
import { CONFIG } from '../config/index.js';
|
|
|
import { PositionCache, logger, sleep, formatUsd } from '../utils/index.js';
|
|
import { PositionCache, logger, sleep, formatUsd } from '../utils/index.js';
|
|
|
-import { ByrealAPI, JupiterSwapper } from '../services/index.js';
|
|
|
|
|
|
|
+import { ByrealAPI, JupiterSwapper, OnchainMonitor } from '../services/index.js';
|
|
|
|
|
+import { DiscordWebhook } from '../utils/discord.js';
|
|
|
|
|
|
|
|
export class SniperEngine {
|
|
export class SniperEngine {
|
|
|
constructor() {
|
|
constructor() {
|
|
@@ -12,6 +13,9 @@ export class SniperEngine {
|
|
|
this.swapper = new JupiterSwapper();
|
|
this.swapper = new JupiterSwapper();
|
|
|
this.isRunning = false;
|
|
this.isRunning = false;
|
|
|
this.myWallet = CONFIG.MY_WALLET || this.swapper.getWalletAddress();
|
|
this.myWallet = CONFIG.MY_WALLET || this.swapper.getWalletAddress();
|
|
|
|
|
+ this.onchainMonitor = null;
|
|
|
|
|
+ // Initialize Discord webhook
|
|
|
|
|
+ this.discord = new DiscordWebhook(CONFIG.DISCORD_WEBHOOK_URL);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async analyzePosition(positionDetail, poolInfo) {
|
|
async analyzePosition(positionDetail, poolInfo) {
|
|
@@ -39,16 +43,26 @@ export class SniperEngine {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async executeCopy(parentDetail, poolInfo, parentPositionAddress, targetPositionAddress) {
|
|
|
|
|
- const targetUsdValue = parseFloat(parentDetail.totalUsdValue || parentDetail.liquidityUsd || 0);
|
|
|
|
|
|
|
+ async executeCopy(parentDetail, poolInfo, parentPositionAddress, targetPositionAddress, targetUsdValue = 0) {
|
|
|
|
|
+ // Use provided target value (from onchain transaction), fallback to parent position value
|
|
|
|
|
+ const copyBaseValue = targetUsdValue > 0
|
|
|
|
|
+ ? targetUsdValue
|
|
|
|
|
+ : parseFloat(parentDetail.totalDeposit || parentDetail.totalUsdValue || parentDetail.liquidityUsd || 0);
|
|
|
|
|
|
|
|
- if (targetUsdValue <= 0) {
|
|
|
|
|
- logger.warn('Parent position has no USD value, skipping');
|
|
|
|
|
|
|
+ if (copyBaseValue <= 0) {
|
|
|
|
|
+ logger.warn('Position has no USD value, skipping');
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // Log which value we're using
|
|
|
|
|
+ if (targetUsdValue > 0) {
|
|
|
|
|
+ logger.info(`Using TARGET position value: ${formatUsd(targetUsdValue)}`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ logger.info(`Using PARENT position value: ${formatUsd(copyBaseValue)}`);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
const copyUsdValue = Math.min(
|
|
const copyUsdValue = Math.min(
|
|
|
- targetUsdValue * CONFIG.COPY_MULTIPLIER,
|
|
|
|
|
|
|
+ copyBaseValue * CONFIG.COPY_MULTIPLIER,
|
|
|
CONFIG.MAX_USD_VALUE
|
|
CONFIG.MAX_USD_VALUE
|
|
|
);
|
|
);
|
|
|
|
|
|
|
@@ -57,15 +71,19 @@ export class SniperEngine {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- logger.info(`Copying PARENT position with ${CONFIG.COPY_MULTIPLIER}x multiplier`);
|
|
|
|
|
- logger.info(`Parent Target: ${formatUsd(targetUsdValue)} → Copy: ${formatUsd(copyUsdValue)}`);
|
|
|
|
|
|
|
+ logger.info(`Copying with ${CONFIG.COPY_MULTIPLIER}x multiplier`);
|
|
|
|
|
+ logger.info(`Base Value: ${formatUsd(copyBaseValue)} → Copy: ${formatUsd(copyUsdValue)}`);
|
|
|
|
|
+
|
|
|
|
|
+ // Get correct field names from API response
|
|
|
|
|
+ const nftMint = parentDetail.nftMintAddress || parentDetail.nftMint;
|
|
|
|
|
+ const poolAddress = parentDetail.pool?.poolAddress || parentDetail.poolAddress;
|
|
|
|
|
|
|
|
// Step 1: Calculate exact token amounts needed using the calculate API
|
|
// Step 1: Calculate exact token amounts needed using the calculate API
|
|
|
logger.info('Calculating exact token amounts from parent position...');
|
|
logger.info('Calculating exact token amounts from parent position...');
|
|
|
const calculation = await ByrealAPI.calculatePosition(
|
|
const calculation = await ByrealAPI.calculatePosition(
|
|
|
parentPositionAddress,
|
|
parentPositionAddress,
|
|
|
copyUsdValue,
|
|
copyUsdValue,
|
|
|
- parentDetail.nftMint
|
|
|
|
|
|
|
+ nftMint
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
if (!calculation) {
|
|
if (!calculation) {
|
|
@@ -89,12 +107,12 @@ export class SniperEngine {
|
|
|
if (tokenABalance < requiredAmountA * 0.95) {
|
|
if (tokenABalance < requiredAmountA * 0.95) {
|
|
|
logger.info(`Token A balance insufficient: ${tokenABalance.toFixed(4)} < ${requiredAmountA.toFixed(4)}`);
|
|
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)})`);
|
|
logger.info(`Swapping with 10% buffer: ${swapAmountA.toFixed(4)} (required: ${requiredAmountA.toFixed(4)})`);
|
|
|
- const success = await this.swapper.swapIfNeeded(
|
|
|
|
|
- 'So11111111111111111111111111111111111111112', // Use SOL as source
|
|
|
|
|
- tokenA.address,
|
|
|
|
|
- swapAmountA,
|
|
|
|
|
- tokenA.decimals
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ const success = await this.swapper.swapIfNeeded(
|
|
|
|
|
+ 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // Use USDC as source
|
|
|
|
|
+ tokenA.address,
|
|
|
|
|
+ swapAmountA,
|
|
|
|
|
+ tokenA.decimals
|
|
|
|
|
+ );
|
|
|
if (!success) {
|
|
if (!success) {
|
|
|
logger.error('Failed to acquire sufficient Token A');
|
|
logger.error('Failed to acquire sufficient Token A');
|
|
|
return false;
|
|
return false;
|
|
@@ -127,21 +145,40 @@ export class SniperEngine {
|
|
|
|
|
|
|
|
// Step 4: Execute copy of PARENT position
|
|
// Step 4: Execute copy of PARENT position
|
|
|
const success = await ByrealAPI.copyPosition(
|
|
const success = await ByrealAPI.copyPosition(
|
|
|
- parentDetail.nftMint,
|
|
|
|
|
|
|
+ nftMint,
|
|
|
parentPositionAddress,
|
|
parentPositionAddress,
|
|
|
copyUsdValue
|
|
copyUsdValue
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
if (success) {
|
|
if (success) {
|
|
|
this.copiedCache.add(parentPositionAddress, {
|
|
this.copiedCache.add(parentPositionAddress, {
|
|
|
- poolAddress: parentDetail.poolAddress,
|
|
|
|
|
- targetUsdValue,
|
|
|
|
|
|
|
+ poolAddress,
|
|
|
|
|
+ targetUsdValue: copyBaseValue,
|
|
|
copiedAt: new Date().toISOString(),
|
|
copiedAt: new Date().toISOString(),
|
|
|
targetPositionAddress: targetPositionAddress,
|
|
targetPositionAddress: targetPositionAddress,
|
|
|
calculation: calculation, // Store calculation details
|
|
calculation: calculation, // Store calculation details
|
|
|
});
|
|
});
|
|
|
// Map target's position to the parent position we copied
|
|
// Map target's position to the parent position we copied
|
|
|
this.targetToParentMap.set(targetPositionAddress, parentPositionAddress);
|
|
this.targetToParentMap.set(targetPositionAddress, parentPositionAddress);
|
|
|
|
|
+
|
|
|
|
|
+ // Send Discord notification
|
|
|
|
|
+ this.discord.notifyCopySuccess({
|
|
|
|
|
+ parentPosition: parentPositionAddress,
|
|
|
|
|
+ poolAddress: poolAddress,
|
|
|
|
|
+ poolName: `${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`,
|
|
|
|
|
+ tokenA: {
|
|
|
|
|
+ amount: tokenA.amount,
|
|
|
|
|
+ symbol: tokenA.symbol || '',
|
|
|
|
|
+ valueUsd: tokenA.valueUsd || 0,
|
|
|
|
|
+ },
|
|
|
|
|
+ tokenB: {
|
|
|
|
|
+ amount: tokenB.amount,
|
|
|
|
|
+ symbol: tokenB.symbol || '',
|
|
|
|
|
+ valueUsd: tokenB.valueUsd || 0,
|
|
|
|
|
+ },
|
|
|
|
|
+ copyUsdValue: copyUsdValue,
|
|
|
|
|
+ txSignature: null, // Could capture from API response if available
|
|
|
|
|
+ }).catch(err => logger.error('Discord notification failed:', err));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return success;
|
|
return success;
|
|
@@ -174,6 +211,88 @@ export class SniperEngine {
|
|
|
logger.success(`Initialization complete - monitoring for ${this.initialPositions.size} existing positions and new additions only\n`);
|
|
logger.success(`Initialization complete - monitoring for ${this.initialPositions.size} existing positions and new additions only\n`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Handle new position detected from onchain monitor
|
|
|
|
|
+ * Onchain mode: Detect via chain, wait 60s, then query parent detail via API
|
|
|
|
|
+ * Copy amount is based on TARGET position value, not parent position value
|
|
|
|
|
+ */
|
|
|
|
|
+ async handleOnchainPosition(positionInfo) {
|
|
|
|
|
+ const { parentPositionAddress, targetPositionAddress, transactionSignature, positionDetail } = positionInfo;
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(`\n🔗 Onchain position detected!`);
|
|
|
|
|
+ logger.info(` Parent Position: ${parentPositionAddress}`);
|
|
|
|
|
+ logger.info(` Transaction: ${transactionSignature.slice(0, 16)}...`);
|
|
|
|
|
+
|
|
|
|
|
+ // Check 1: Skip if in closed cache
|
|
|
|
|
+ if (this.closedCache.has(parentPositionAddress)) {
|
|
|
|
|
+ logger.info(`Parent position ${parentPositionAddress} was closed, skipping...`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check 2: Local cache check (Priority: cache first)
|
|
|
|
|
+ if (this.copiedCache.has(parentPositionAddress)) {
|
|
|
|
|
+ logger.info(`Parent position ${parentPositionAddress} already in cache, skipping...`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Position detail already fetched from Byreal API (passed from onchain monitor)
|
|
|
|
|
+ if (!positionDetail) {
|
|
|
|
|
+ logger.error(`Position detail not available from Byreal API: ${parentPositionAddress}`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const poolInfo = positionDetail.pool;
|
|
|
|
|
+ if (!poolInfo) {
|
|
|
|
|
+ logger.warn(`Pool info not found for parent position ${parentPositionAddress}`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(`Position details from Byreal API:`);
|
|
|
|
|
+ logger.info(` Pool: ${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`);
|
|
|
|
|
+ logger.info(` NFT Mint: ${positionDetail.nftMintAddress || positionDetail.nftMint}`);
|
|
|
|
|
+ logger.info(` Total Deposit: $${positionDetail.totalDeposit || 0}`);
|
|
|
|
|
+ logger.info(` Liquidity: ${positionDetail.liquidity}`);
|
|
|
|
|
+ logger.info(` Price Range: ${positionDetail.priceLower} - ${positionDetail.priceUpper}`);
|
|
|
|
|
+ logger.info(` Current Price: ${positionDetail.currentPrice}`);
|
|
|
|
|
+
|
|
|
|
|
+ // Check 3: API check via CHECK_COPY_URL (as fallback)
|
|
|
|
|
+ logger.info(`Checking copy status via API...`);
|
|
|
|
|
+ const poolAddress = positionDetail.pool?.poolAddress || positionDetail.poolAddress;
|
|
|
|
|
+ const isCopiedViaApi = await ByrealAPI.checkIfCopied(
|
|
|
|
|
+ poolAddress,
|
|
|
|
|
+ parentPositionAddress,
|
|
|
|
|
+ this.myWallet
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (isCopiedViaApi) {
|
|
|
|
|
+ logger.info('Parent position already copied (confirmed by API)');
|
|
|
|
|
+ // Sync cache with API result
|
|
|
|
|
+ this.copiedCache.add(parentPositionAddress, { poolAddress });
|
|
|
|
|
+ this.targetToParentMap.set(targetPositionAddress, parentPositionAddress);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ logger.success('Parent position ready to copy!');
|
|
|
|
|
+
|
|
|
|
|
+ // Execute copy using position detail from Byreal API
|
|
|
|
|
+ const success = await this.executeCopy(
|
|
|
|
|
+ positionDetail,
|
|
|
|
|
+ poolInfo,
|
|
|
|
|
+ parentPositionAddress,
|
|
|
|
|
+ targetPositionAddress,
|
|
|
|
|
+ positionDetail.totalDeposit || 0 // Use totalDeposit from Byreal API
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (success) {
|
|
|
|
|
+ logger.success(`Successfully copied position via onchain detection!`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Mark as processed
|
|
|
|
|
+ if (targetPositionAddress) {
|
|
|
|
|
+ this.initialPositions.add(targetPositionAddress);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
async scanForNewPositions() {
|
|
async scanForNewPositions() {
|
|
|
logger.info(`Scanning for target wallet positions: ${CONFIG.TARGET_WALLET}`);
|
|
logger.info(`Scanning for target wallet positions: ${CONFIG.TARGET_WALLET}`);
|
|
|
|
|
|
|
@@ -209,26 +328,43 @@ export class SniperEngine {
|
|
|
|
|
|
|
|
logger.info(`\n🆕 New position detected in target wallet: ${positionAddress}`);
|
|
logger.info(`\n🆕 New position detected in target wallet: ${positionAddress}`);
|
|
|
|
|
|
|
|
- // Fetch position detail to check if it's a copied position
|
|
|
|
|
- logger.info('Fetching position detail to check parent...');
|
|
|
|
|
- const positionDetail = await ByrealAPI.getPositionDetail(positionAddress);
|
|
|
|
|
|
|
+ // Fetch position detail with retry logic (byreal interface has delay)
|
|
|
|
|
+ let positionDetail = null;
|
|
|
|
|
+ let parentPositionAddress = null;
|
|
|
|
|
+ const maxRetries = 10;
|
|
|
|
|
+ const retryIntervalMs = 60000; // 30 seconds
|
|
|
|
|
|
|
|
- if (!positionDetail) {
|
|
|
|
|
- logger.error(`Failed to fetch detail for position ${positionAddress}`);
|
|
|
|
|
- continue;
|
|
|
|
|
|
|
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
|
|
|
+ logger.info(`Fetching position detail (attempt ${attempt}/${maxRetries})...`);
|
|
|
|
|
+ positionDetail = await ByrealAPI.getPositionDetail(positionAddress);
|
|
|
|
|
+
|
|
|
|
|
+ if (!positionDetail) {
|
|
|
|
|
+ logger.error(`Failed to fetch detail for position ${positionAddress}`);
|
|
|
|
|
+ if (attempt < maxRetries) {
|
|
|
|
|
+ logger.info(`Waiting ${retryIntervalMs / 1000}s before retry...`);
|
|
|
|
|
+ await sleep(retryIntervalMs);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Check if this is a copied position (has parentPositionAddress)
|
|
|
|
|
+ parentPositionAddress = positionDetail.parentPositionAddress;
|
|
|
|
|
+
|
|
|
|
|
+ if (parentPositionAddress) {
|
|
|
|
|
+ logger.success(`Found copied position! Parent: ${parentPositionAddress}`);
|
|
|
|
|
+ break;
|
|
|
|
|
+ } else if (attempt < maxRetries) {
|
|
|
|
|
+ logger.info(`Position is not a copy yet (no parent), retrying in ${retryIntervalMs / 1000}s...`);
|
|
|
|
|
+ await sleep(retryIntervalMs);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // Check if this is a copied position (has parentPositionAddress)
|
|
|
|
|
- const parentPositionAddress = positionDetail.parentPositionAddress;
|
|
|
|
|
|
|
|
|
|
|
|
+ // After 10 attempts, if still no parent, mark as processed and skip
|
|
|
if (!parentPositionAddress) {
|
|
if (!parentPositionAddress) {
|
|
|
- logger.info('Position is not a copy (no parent), skipping...');
|
|
|
|
|
- // Mark as processed so we don't check again
|
|
|
|
|
|
|
+ logger.info(`Position ${positionAddress} has no parent after ${maxRetries} attempts, marking as processed and skipping...`);
|
|
|
this.initialPositions.add(positionAddress);
|
|
this.initialPositions.add(positionAddress);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- logger.success(`Found copied position! Parent: ${parentPositionAddress}`);
|
|
|
|
|
|
|
|
|
|
// Now we copy the PARENT position, not the target's position
|
|
// Now we copy the PARENT position, not the target's position
|
|
|
const parentDetail = await ByrealAPI.getPositionDetail(parentPositionAddress);
|
|
const parentDetail = await ByrealAPI.getPositionDetail(parentPositionAddress);
|
|
@@ -245,22 +381,26 @@ export class SniperEngine {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
logger.info(`Parent pool: ${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`);
|
|
logger.info(`Parent pool: ${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`);
|
|
|
- logger.info(`Parent position NFT: ${parentDetail.nftMint}`);
|
|
|
|
|
|
|
+ // Use nftMintAddress instead of nftMint
|
|
|
|
|
+ const nftMint = parentDetail.nftMintAddress || parentDetail.nftMint;
|
|
|
|
|
+ logger.info(`Parent position NFT: ${nftMint}`);
|
|
|
|
|
|
|
|
const analysis = await this.analyzePosition(parentDetail, poolInfo);
|
|
const analysis = await this.analyzePosition(parentDetail, poolInfo);
|
|
|
|
|
|
|
|
if (analysis.inRange) {
|
|
if (analysis.inRange) {
|
|
|
logger.success('Parent position is IN RANGE - ready to copy!');
|
|
logger.success('Parent position is IN RANGE - ready to copy!');
|
|
|
|
|
|
|
|
|
|
+ // Use pool.poolAddress instead of poolAddress
|
|
|
|
|
+ const poolAddress = parentDetail.pool?.poolAddress || parentDetail.poolAddress;
|
|
|
const isCopied = await ByrealAPI.checkIfCopied(
|
|
const isCopied = await ByrealAPI.checkIfCopied(
|
|
|
- parentDetail.poolAddress,
|
|
|
|
|
|
|
+ poolAddress,
|
|
|
parentPositionAddress,
|
|
parentPositionAddress,
|
|
|
this.myWallet
|
|
this.myWallet
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
if (isCopied) {
|
|
if (isCopied) {
|
|
|
logger.info('Parent position already copied (confirmed via API)');
|
|
logger.info('Parent position already copied (confirmed via API)');
|
|
|
- this.copiedCache.add(parentPositionAddress, { poolAddress: parentDetail.poolAddress });
|
|
|
|
|
|
|
+ this.copiedCache.add(parentPositionAddress, { poolAddress });
|
|
|
// Still map target's position to parent even if already copied
|
|
// Still map target's position to parent even if already copied
|
|
|
this.targetToParentMap.set(positionAddress, parentPositionAddress);
|
|
this.targetToParentMap.set(positionAddress, parentPositionAddress);
|
|
|
} else {
|
|
} else {
|
|
@@ -296,12 +436,24 @@ export class SniperEngine {
|
|
|
const success = await ByrealAPI.closePosition(parentPositionAddress);
|
|
const success = await ByrealAPI.closePosition(parentPositionAddress);
|
|
|
|
|
|
|
|
if (success) {
|
|
if (success) {
|
|
|
|
|
+ const copiedData = this.copiedCache.get(parentPositionAddress);
|
|
|
this.closedCache.add(parentPositionAddress, {
|
|
this.closedCache.add(parentPositionAddress, {
|
|
|
closedAt: new Date().toISOString(),
|
|
closedAt: new Date().toISOString(),
|
|
|
reason: `Target closed their position ${targetPositionAddress}`,
|
|
reason: `Target closed their position ${targetPositionAddress}`,
|
|
|
});
|
|
});
|
|
|
this.copiedCache.remove(parentPositionAddress);
|
|
this.copiedCache.remove(parentPositionAddress);
|
|
|
this.targetToParentMap.delete(targetPositionAddress);
|
|
this.targetToParentMap.delete(targetPositionAddress);
|
|
|
|
|
+
|
|
|
|
|
+ // Send Discord notification
|
|
|
|
|
+ this.discord.notifyCloseSuccess({
|
|
|
|
|
+ positionAddress: parentPositionAddress,
|
|
|
|
|
+ poolName: copiedData?.calculation?.tokenA?.symbol && copiedData?.calculation?.tokenB?.symbol
|
|
|
|
|
+ ? `${copiedData.calculation.tokenA.symbol}/${copiedData.calculation.tokenB.symbol}`
|
|
|
|
|
+ : 'Unknown Pool',
|
|
|
|
|
+ closedAt: new Date().toISOString(),
|
|
|
|
|
+ reason: `Target closed their position`,
|
|
|
|
|
+ txSignature: null,
|
|
|
|
|
+ }).catch(err => logger.error('Discord notification failed:', err));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -321,12 +473,53 @@ export class SniperEngine {
|
|
|
logger.info(`💼 My Wallet: ${this.myWallet}`);
|
|
logger.info(`💼 My Wallet: ${this.myWallet}`);
|
|
|
logger.info(`📈 Copy Multiplier: ${CONFIG.COPY_MULTIPLIER}x`);
|
|
logger.info(`📈 Copy Multiplier: ${CONFIG.COPY_MULTIPLIER}x`);
|
|
|
logger.info(`💰 Max Position: ${formatUsd(CONFIG.MAX_USD_VALUE)}`);
|
|
logger.info(`💰 Max Position: ${formatUsd(CONFIG.MAX_USD_VALUE)}`);
|
|
|
- logger.info(`⏱️ Poll Interval: ${CONFIG.POLL_INTERVAL_MS / 1000}s`);
|
|
|
|
|
|
|
+ logger.info(`📋 Monitor Mode: ${CONFIG.MONITOR_MODE}`);
|
|
|
logger.info('═══════════════════════════════════════════\n');
|
|
logger.info('═══════════════════════════════════════════\n');
|
|
|
|
|
|
|
|
// Initialize: record existing positions but don't copy them
|
|
// Initialize: record existing positions but don't copy them
|
|
|
await this.initialize();
|
|
await this.initialize();
|
|
|
|
|
|
|
|
|
|
+ // Choose monitoring mode based on configuration
|
|
|
|
|
+ if (CONFIG.MONITOR_MODE === 'onchain') {
|
|
|
|
|
+ logger.info('Using onchain transaction monitoring mode...\n');
|
|
|
|
|
+ await this.startOnchainMonitoring();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ logger.info('Using API polling mode...\n');
|
|
|
|
|
+ await this.startApiPolling();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Start onchain transaction monitoring
|
|
|
|
|
+ */
|
|
|
|
|
+ async startOnchainMonitoring() {
|
|
|
|
|
+ // Create onchain monitor with callback
|
|
|
|
|
+ this.onchainMonitor = new OnchainMonitor(
|
|
|
|
|
+ this.handleOnchainPosition.bind(this)
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // Start monitoring in parallel with closed position checking
|
|
|
|
|
+ const monitorPromise = this.onchainMonitor.start();
|
|
|
|
|
+
|
|
|
|
|
+ // Continue checking for closed positions in background
|
|
|
|
|
+ while (this.isRunning) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.scanForClosedPositions();
|
|
|
|
|
+ await sleep(CONFIG.POLL_INTERVAL_MS);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Closed position check error:', error.message);
|
|
|
|
|
+ await sleep(CONFIG.POLL_INTERVAL_MS);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Wait for monitor to finish (it won't unless stopped)
|
|
|
|
|
+ await monitorPromise;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Start API polling mode (original method)
|
|
|
|
|
+ */
|
|
|
|
|
+ async startApiPolling() {
|
|
|
while (this.isRunning) {
|
|
while (this.isRunning) {
|
|
|
try {
|
|
try {
|
|
|
await this.scanForNewPositions();
|
|
await this.scanForNewPositions();
|
|
@@ -344,11 +537,17 @@ export class SniperEngine {
|
|
|
stop() {
|
|
stop() {
|
|
|
logger.info('Stopping sniper bot...');
|
|
logger.info('Stopping sniper bot...');
|
|
|
this.isRunning = false;
|
|
this.isRunning = false;
|
|
|
|
|
+
|
|
|
|
|
+ // Stop onchain monitor if running
|
|
|
|
|
+ if (this.onchainMonitor) {
|
|
|
|
|
+ this.onchainMonitor.stop();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
getStatus() {
|
|
getStatus() {
|
|
|
return {
|
|
return {
|
|
|
isRunning: this.isRunning,
|
|
isRunning: this.isRunning,
|
|
|
|
|
+ monitorMode: CONFIG.MONITOR_MODE,
|
|
|
copiedPositions: this.copiedCache.getAll(),
|
|
copiedPositions: this.copiedCache.getAll(),
|
|
|
closedPositions: this.closedCache.getAll(),
|
|
closedPositions: this.closedCache.getAll(),
|
|
|
walletAddress: this.myWallet,
|
|
walletAddress: this.myWallet,
|
|
@@ -356,4 +555,4 @@ export class SniperEngine {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export default SniperEngine;
|
|
|
|
|
|
|
+export default SniperEngine;
|