|
|
@@ -8,19 +8,20 @@ export class SniperEngine {
|
|
|
this.closedCache = new PositionCache('.closed-positions.json', CONFIG.DATA_DIR);
|
|
|
this.initialPositions = new Set(); // Store positions at startup
|
|
|
this.hasInitialized = false; // Track if initialization is complete
|
|
|
+ this.targetToParentMap = new Map(); // Map target's position -> parent position we copied
|
|
|
this.swapper = new JupiterSwapper();
|
|
|
this.isRunning = false;
|
|
|
this.myWallet = CONFIG.MY_WALLET || this.swapper.getWalletAddress();
|
|
|
}
|
|
|
|
|
|
- async analyzePosition(position, poolInfo) {
|
|
|
+ async analyzePosition(positionDetail, poolInfo) {
|
|
|
const decimalsA = poolInfo.mintA?.decimals || 6;
|
|
|
const decimalsB = poolInfo.mintB?.decimals || 6;
|
|
|
- const currentPrice = parseFloat(poolInfo.mintB?.price || 0);
|
|
|
+ const currentPrice = parseFloat(poolInfo.mintB?.price || poolInfo.baseMint?.price || 0);
|
|
|
|
|
|
try {
|
|
|
- const lowerPrice = await ByrealAPI.getPriceFromTick(position.lowerTick, decimalsA, decimalsB);
|
|
|
- const upperPrice = await ByrealAPI.getPriceFromTick(position.upperTick, decimalsA, decimalsB);
|
|
|
+ const lowerPrice = await ByrealAPI.getPriceFromTick(positionDetail.lowerTick, decimalsA, decimalsB);
|
|
|
+ const upperPrice = await ByrealAPI.getPriceFromTick(positionDetail.upperTick, decimalsA, decimalsB);
|
|
|
|
|
|
const min = Math.min(lowerPrice, upperPrice);
|
|
|
const max = Math.max(lowerPrice, upperPrice);
|
|
|
@@ -38,11 +39,11 @@ export class SniperEngine {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- async executeCopy(position, poolInfo) {
|
|
|
- const targetUsdValue = parseFloat(position.totalUsdValue || position.liquidityUsd || 0);
|
|
|
+ async executeCopy(parentDetail, poolInfo, parentPositionAddress, targetPositionAddress) {
|
|
|
+ const targetUsdValue = parseFloat(parentDetail.totalUsdValue || parentDetail.liquidityUsd || 0);
|
|
|
|
|
|
if (targetUsdValue <= 0) {
|
|
|
- logger.warn('Position has no USD value, skipping');
|
|
|
+ logger.warn('Parent position has no USD value, skipping');
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@@ -56,8 +57,8 @@ export class SniperEngine {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- logger.info(`Copying position with ${CONFIG.COPY_MULTIPLIER}x multiplier`);
|
|
|
- logger.info(`Target: ${formatUsd(targetUsdValue)} → Copy: ${formatUsd(copyUsdValue)}`);
|
|
|
+ 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;
|
|
|
@@ -107,21 +108,24 @@ export class SniperEngine {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Execute copy
|
|
|
+ // Execute copy of PARENT position
|
|
|
const success = await ByrealAPI.copyPosition(
|
|
|
- position.nftMintAddress,
|
|
|
- position.positionAddress,
|
|
|
+ parentDetail.nftMint,
|
|
|
+ parentPositionAddress,
|
|
|
copyUsdValue
|
|
|
);
|
|
|
|
|
|
if (success) {
|
|
|
- this.copiedCache.add(position.positionAddress, {
|
|
|
- poolAddress: position.poolAddress,
|
|
|
+ this.copiedCache.add(parentPositionAddress, {
|
|
|
+ poolAddress: parentDetail.poolAddress,
|
|
|
targetUsdValue,
|
|
|
copiedAt: new Date().toISOString(),
|
|
|
+ targetPositionAddress: targetPositionAddress, // Store which target position triggered this copy
|
|
|
});
|
|
|
+ // Map target's position to the parent position we copied
|
|
|
+ this.targetToParentMap.set(targetPositionAddress, parentPositionAddress);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return success;
|
|
|
}
|
|
|
|
|
|
@@ -185,36 +189,72 @@ export class SniperEngine {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- const poolInfo = poolMap[position.poolAddress];
|
|
|
+ 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);
|
|
|
+
|
|
|
+ if (!positionDetail) {
|
|
|
+ logger.error(`Failed to fetch detail for position ${positionAddress}`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if this is a copied position (has parentPositionAddress)
|
|
|
+ const parentPositionAddress = positionDetail.parentPositionAddress;
|
|
|
+
|
|
|
+ if (!parentPositionAddress) {
|
|
|
+ logger.info('Position is not a copy (no parent), skipping...');
|
|
|
+ // Mark as processed so we don't check again
|
|
|
+ this.initialPositions.add(positionAddress);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.success(`Found copied position! Parent: ${parentPositionAddress}`);
|
|
|
+
|
|
|
+ // Now we copy the PARENT position, not the target's position
|
|
|
+ const parentDetail = await ByrealAPI.getPositionDetail(parentPositionAddress);
|
|
|
+
|
|
|
+ if (!parentDetail) {
|
|
|
+ logger.error(`Failed to fetch parent position detail: ${parentPositionAddress}`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const poolInfo = parentDetail.pool;
|
|
|
if (!poolInfo) {
|
|
|
- logger.warn(`Pool info not found for ${position.poolAddress}`);
|
|
|
+ logger.warn(`Pool info not found for parent position ${parentPositionAddress}`);
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- logger.info(`\n🆕 New position detected: ${positionAddress}`);
|
|
|
- logger.info(`Pool: ${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`);
|
|
|
+ logger.info(`Parent pool: ${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`);
|
|
|
+ logger.info(`Parent position NFT: ${parentDetail.nftMint}`);
|
|
|
|
|
|
- const analysis = await this.analyzePosition(position, poolInfo);
|
|
|
+ const analysis = await this.analyzePosition(parentDetail, poolInfo);
|
|
|
|
|
|
if (analysis.inRange) {
|
|
|
- logger.success('Position is IN RANGE - ready to copy!');
|
|
|
+ logger.success('Parent position is IN RANGE - ready to copy!');
|
|
|
|
|
|
const isCopied = await ByrealAPI.checkIfCopied(
|
|
|
- position.poolAddress,
|
|
|
- positionAddress,
|
|
|
+ parentDetail.poolAddress,
|
|
|
+ parentPositionAddress,
|
|
|
this.myWallet
|
|
|
);
|
|
|
|
|
|
if (isCopied) {
|
|
|
- logger.info('Already copied (confirmed via API)');
|
|
|
- this.copiedCache.add(positionAddress, { poolAddress: position.poolAddress });
|
|
|
+ logger.info('Parent position already copied (confirmed via API)');
|
|
|
+ this.copiedCache.add(parentPositionAddress, { poolAddress: parentDetail.poolAddress });
|
|
|
+ // Still map target's position to parent even if already copied
|
|
|
+ this.targetToParentMap.set(positionAddress, parentPositionAddress);
|
|
|
} else {
|
|
|
- await this.executeCopy(position, poolInfo);
|
|
|
+ await this.executeCopy(parentDetail, poolInfo, parentPositionAddress, positionAddress);
|
|
|
}
|
|
|
} else {
|
|
|
- logger.info('Position is OUT OF RANGE');
|
|
|
+ logger.info('Parent position is OUT OF RANGE');
|
|
|
}
|
|
|
|
|
|
+ // Mark target's position as processed
|
|
|
+ this.initialPositions.add(positionAddress);
|
|
|
+
|
|
|
await sleep(500);
|
|
|
}
|
|
|
}
|
|
|
@@ -225,21 +265,25 @@ export class SniperEngine {
|
|
|
const { positions } = await ByrealAPI.fetchTargetPositions(CONFIG.TARGET_WALLET);
|
|
|
const targetPositionAddresses = new Set(positions.map(p => p.positionAddress));
|
|
|
|
|
|
- const myCopiedPositions = this.copiedCache.getAll();
|
|
|
-
|
|
|
- for (const positionAddress of myCopiedPositions.positions) {
|
|
|
- if (!targetPositionAddresses.has(positionAddress) && !this.closedCache.has(positionAddress)) {
|
|
|
- logger.warn(`Target closed position: ${positionAddress}`);
|
|
|
- logger.info('Closing my copy...');
|
|
|
+ // Check if any target positions that triggered our copies are now closed
|
|
|
+ for (const [targetPositionAddress, parentPositionAddress] of this.targetToParentMap.entries()) {
|
|
|
+ // If target's position is no longer active and we haven't closed our copy yet
|
|
|
+ if (!targetPositionAddresses.has(targetPositionAddress) &&
|
|
|
+ !this.closedCache.has(parentPositionAddress) &&
|
|
|
+ this.copiedCache.has(parentPositionAddress)) {
|
|
|
+
|
|
|
+ logger.warn(`Target closed their copied position: ${targetPositionAddress}`);
|
|
|
+ logger.info(`Closing our copy of parent position: ${parentPositionAddress}`);
|
|
|
|
|
|
- const success = await ByrealAPI.closePosition(positionAddress);
|
|
|
+ const success = await ByrealAPI.closePosition(parentPositionAddress);
|
|
|
|
|
|
if (success) {
|
|
|
- this.closedCache.add(positionAddress, {
|
|
|
+ this.closedCache.add(parentPositionAddress, {
|
|
|
closedAt: new Date().toISOString(),
|
|
|
- reason: 'Target closed position',
|
|
|
+ reason: `Target closed their position ${targetPositionAddress}`,
|
|
|
});
|
|
|
- this.copiedCache.remove(positionAddress);
|
|
|
+ this.copiedCache.remove(parentPositionAddress);
|
|
|
+ this.targetToParentMap.delete(targetPositionAddress);
|
|
|
}
|
|
|
}
|
|
|
}
|