|
@@ -5,7 +5,7 @@ import { ByrealAPI } from './byreal.js';
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Onchain Monitor - Monitor target wallet for Byreal LP opening transactions
|
|
* Onchain Monitor - Monitor target wallet for Byreal LP opening transactions
|
|
|
- * Detects transactions, waits 60s for Byreal API to sync, then fetches position details
|
|
|
|
|
|
|
+ * Detects transactions, extracts token transfers based on parent position info
|
|
|
*/
|
|
*/
|
|
|
export class OnchainMonitor {
|
|
export class OnchainMonitor {
|
|
|
constructor(callback) {
|
|
constructor(callback) {
|
|
@@ -13,15 +13,12 @@ export class OnchainMonitor {
|
|
|
this.targetWallet = new PublicKey(CONFIG.TARGET_WALLET);
|
|
this.targetWallet = new PublicKey(CONFIG.TARGET_WALLET);
|
|
|
this.byrealProgramId = new PublicKey('REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2');
|
|
this.byrealProgramId = new PublicKey('REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2');
|
|
|
this.memoProgramId = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr');
|
|
this.memoProgramId = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr');
|
|
|
- this.callback = callback; // Callback when new position detected
|
|
|
|
|
|
|
+ this.callback = callback;
|
|
|
this.isRunning = false;
|
|
this.isRunning = false;
|
|
|
- this.processedSignatures = new Set(); // Track processed transactions
|
|
|
|
|
|
|
+ this.processedSignatures = new Set();
|
|
|
this.lastSignature = null;
|
|
this.lastSignature = null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * Start monitoring for new transactions
|
|
|
|
|
- */
|
|
|
|
|
async start() {
|
|
async start() {
|
|
|
if (this.isRunning) {
|
|
if (this.isRunning) {
|
|
|
logger.warn('Onchain monitor already running');
|
|
logger.warn('Onchain monitor already running');
|
|
@@ -32,13 +29,11 @@ export class OnchainMonitor {
|
|
|
logger.info('═══════════════════════════════════════════');
|
|
logger.info('═══════════════════════════════════════════');
|
|
|
logger.info('🔗 Onchain Monitor Started');
|
|
logger.info('🔗 Onchain Monitor Started');
|
|
|
logger.info(`🎯 Target: ${CONFIG.TARGET_WALLET}`);
|
|
logger.info(`🎯 Target: ${CONFIG.TARGET_WALLET}`);
|
|
|
- logger.info(`📋 Mode: Detect transaction -> Wait 60s -> Fetch from Byreal API`);
|
|
|
|
|
|
|
+ logger.info(`📋 Mode: Parse transaction -> Get parent info -> Extract token transfers`);
|
|
|
logger.info('═══════════════════════════════════════════\n');
|
|
logger.info('═══════════════════════════════════════════\n');
|
|
|
|
|
|
|
|
- // Get initial last signature to avoid processing old transactions
|
|
|
|
|
await this.getLatestSignature();
|
|
await this.getLatestSignature();
|
|
|
|
|
|
|
|
- // Start monitoring loop
|
|
|
|
|
while (this.isRunning) {
|
|
while (this.isRunning) {
|
|
|
try {
|
|
try {
|
|
|
await this.checkNewTransactions();
|
|
await this.checkNewTransactions();
|
|
@@ -50,9 +45,6 @@ export class OnchainMonitor {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * Get the latest signature to establish baseline
|
|
|
|
|
- */
|
|
|
|
|
async getLatestSignature() {
|
|
async getLatestSignature() {
|
|
|
try {
|
|
try {
|
|
|
const signatures = await this.connection.getSignaturesForAddress(
|
|
const signatures = await this.connection.getSignaturesForAddress(
|
|
@@ -69,9 +61,6 @@ export class OnchainMonitor {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * Check for new transactions from target wallet
|
|
|
|
|
- */
|
|
|
|
|
async checkNewTransactions() {
|
|
async checkNewTransactions() {
|
|
|
try {
|
|
try {
|
|
|
const signatures = await this.connection.getSignaturesForAddress(
|
|
const signatures = await this.connection.getSignaturesForAddress(
|
|
@@ -86,27 +75,22 @@ export class OnchainMonitor {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Update last signature for next iteration
|
|
|
|
|
this.lastSignature = signatures[0].signature;
|
|
this.lastSignature = signatures[0].signature;
|
|
|
|
|
|
|
|
- // Process signatures in reverse order (oldest first)
|
|
|
|
|
for (const sigInfo of signatures.reverse()) {
|
|
for (const sigInfo of signatures.reverse()) {
|
|
|
const signature = sigInfo.signature;
|
|
const signature = sigInfo.signature;
|
|
|
|
|
|
|
|
- // Skip already processed
|
|
|
|
|
if (this.processedSignatures.has(signature)) {
|
|
if (this.processedSignatures.has(signature)) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
this.processedSignatures.add(signature);
|
|
this.processedSignatures.add(signature);
|
|
|
|
|
|
|
|
- // Limit set size to prevent memory issues
|
|
|
|
|
if (this.processedSignatures.size > 1000) {
|
|
if (this.processedSignatures.size > 1000) {
|
|
|
const iterator = this.processedSignatures.values();
|
|
const iterator = this.processedSignatures.values();
|
|
|
this.processedSignatures.delete(iterator.next().value);
|
|
this.processedSignatures.delete(iterator.next().value);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Process the transaction
|
|
|
|
|
await this.handleNewTransaction(signature);
|
|
await this.handleNewTransaction(signature);
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -115,7 +99,7 @@ export class OnchainMonitor {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Handle new transaction - extract parent position, wait 60s, fetch from Byreal API
|
|
|
|
|
|
|
+ * Handle new transaction - extract parent position, get parent info, parse token transfers
|
|
|
*/
|
|
*/
|
|
|
async handleNewTransaction(signature) {
|
|
async handleNewTransaction(signature) {
|
|
|
try {
|
|
try {
|
|
@@ -131,7 +115,7 @@ export class OnchainMonitor {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Check if transaction involves Byreal program
|
|
|
|
|
|
|
+ // Check if Byreal program
|
|
|
const message = tx.transaction.message;
|
|
const message = tx.transaction.message;
|
|
|
const accountKeys = message.staticAccountKeys || message.accountKeys || [];
|
|
const accountKeys = message.staticAccountKeys || message.accountKeys || [];
|
|
|
|
|
|
|
@@ -140,7 +124,7 @@ export class OnchainMonitor {
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
if (!hasByrealProgram) {
|
|
if (!hasByrealProgram) {
|
|
|
- logger.debug(`No Byreal program involvement: ${signature.slice(0, 16)}`);
|
|
|
|
|
|
|
+ logger.debug(`No Byreal program: ${signature.slice(0, 16)}`);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -150,54 +134,54 @@ export class OnchainMonitor {
|
|
|
const parentPosition = this.extractParentPosition(tx);
|
|
const parentPosition = this.extractParentPosition(tx);
|
|
|
|
|
|
|
|
if (!parentPosition) {
|
|
if (!parentPosition) {
|
|
|
- logger.info(`No parent position found in memo, skipping...`);
|
|
|
|
|
|
|
+ logger.info(`No parent position in memo, skipping...`);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- logger.success(`Found parent position from memo: ${parentPosition}`);
|
|
|
|
|
|
|
+ logger.success(`Found parent position: ${parentPosition}`);
|
|
|
|
|
|
|
|
- // Wait 60 seconds for Byreal API to sync
|
|
|
|
|
- logger.info(`Waiting 60 seconds for Byreal API to sync...`);
|
|
|
|
|
|
|
+ // Wait 60s for Byreal API
|
|
|
|
|
+ logger.info(`Waiting 60s for Byreal API...`);
|
|
|
await sleep(60000);
|
|
await sleep(60000);
|
|
|
|
|
|
|
|
- // Fetch target wallet's positions to find the copied position
|
|
|
|
|
- logger.info(`Fetching target wallet positions to find copied position...`);
|
|
|
|
|
- const { positions } = await ByrealAPI.fetchTargetPositions(CONFIG.TARGET_WALLET);
|
|
|
|
|
-
|
|
|
|
|
- // Find the position that references this parent position
|
|
|
|
|
- const copiedPosition = positions.find(p =>
|
|
|
|
|
- p.parentPositionAddress === parentPosition
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ // Get parent position details to know which tokens
|
|
|
|
|
+ logger.info(`Fetching parent position details...`);
|
|
|
|
|
+ const parentDetail = await ByrealAPI.getPositionDetail(parentPosition);
|
|
|
|
|
|
|
|
- if (!copiedPosition) {
|
|
|
|
|
- logger.error(`Could not find copied position for parent ${parentPosition} in target wallet`);
|
|
|
|
|
|
|
+ if (!parentDetail || !parentDetail.pool) {
|
|
|
|
|
+ logger.error(`Failed to get parent position details`);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- logger.info(`Found copied position: ${copiedPosition.positionAddress}`);
|
|
|
|
|
|
|
+ const tokenAMint = parentDetail.pool.mintA?.address;
|
|
|
|
|
+ const tokenBMint = parentDetail.pool.mintB?.address;
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(`Parent position tokens:`);
|
|
|
|
|
+ logger.info(` Token A: ${parentDetail.pool.mintA?.symbol} (${tokenAMint})`);
|
|
|
|
|
+ logger.info(` Token B: ${parentDetail.pool.mintB?.symbol} (${tokenBMint})`);
|
|
|
|
|
|
|
|
- // Fetch the copied position details (this is the position created by target wallet)
|
|
|
|
|
- logger.info(`Fetching copied position details from Byreal API...`);
|
|
|
|
|
- const positionDetail = await ByrealAPI.getPositionDetail(copiedPosition.positionAddress);
|
|
|
|
|
|
|
+ // Parse transaction to extract token transfers
|
|
|
|
|
+ logger.info(`Parsing transaction for token transfers...`);
|
|
|
|
|
+ const tokenTransfers = this.extractTokenTransfers(tx, tokenAMint, tokenBMint);
|
|
|
|
|
|
|
|
- if (!positionDetail) {
|
|
|
|
|
- logger.error(`Failed to fetch copied position detail from Byreal API: ${copiedPosition.positionAddress}`);
|
|
|
|
|
|
|
+ if (!tokenTransfers) {
|
|
|
|
|
+ logger.error(`Failed to extract token transfers from transaction`);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- logger.success(`Copied position details fetched successfully!`);
|
|
|
|
|
- logger.info(` Pool: ${positionDetail.pool?.mintA?.symbol}/${positionDetail.pool?.mintB?.symbol}`);
|
|
|
|
|
- logger.info(` NFT Mint: ${positionDetail.nftMintAddress || positionDetail.nftMint}`);
|
|
|
|
|
- logger.info(` Total Deposit (Target's amount): $${positionDetail.totalDeposit || positionDetail.totalUsdValue || positionDetail.liquidityUsd || 0}`);
|
|
|
|
|
- logger.info(` Parent Position: ${positionDetail.parentPositionAddress}`);
|
|
|
|
|
|
|
+ logger.success(`Token transfers extracted:`);
|
|
|
|
|
+ logger.info(` Token A: ${tokenTransfers.tokenA.amount} ${parentDetail.pool.mintA?.symbol}`);
|
|
|
|
|
+ logger.info(` Token B: ${tokenTransfers.tokenB.amount} ${parentDetail.pool.mintB?.symbol}`);
|
|
|
|
|
+ logger.info(` Total USD Value: $${tokenTransfers.totalUsdValue.toFixed(2)}`);
|
|
|
|
|
|
|
|
- // Call the callback with position info from Byreal API
|
|
|
|
|
|
|
+ // Call callback
|
|
|
if (this.callback) {
|
|
if (this.callback) {
|
|
|
await this.callback({
|
|
await this.callback({
|
|
|
parentPositionAddress: parentPosition,
|
|
parentPositionAddress: parentPosition,
|
|
|
- targetPositionAddress: copiedPosition.positionAddress, // Target's copied position
|
|
|
|
|
transactionSignature: signature,
|
|
transactionSignature: signature,
|
|
|
- positionDetail: positionDetail // Full position detail from Byreal API (TARGET's position)
|
|
|
|
|
|
|
+ parentDetail: parentDetail,
|
|
|
|
|
+ tokenTransfers: tokenTransfers,
|
|
|
|
|
+ targetUsdValue: tokenTransfers.totalUsdValue
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -207,43 +191,114 @@ export class OnchainMonitor {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Extract parent position address from Memo instruction
|
|
|
|
|
|
|
+ * Extract token transfers from transaction based on token mints
|
|
|
*/
|
|
*/
|
|
|
|
|
+ extractTokenTransfers(tx, tokenAMint, tokenBMint) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = {
|
|
|
|
|
+ tokenA: { mint: tokenAMint, amount: 0, decimals: 6 },
|
|
|
|
|
+ tokenB: { mint: tokenBMint, amount: 0, decimals: 6 },
|
|
|
|
|
+ totalUsdValue: 0
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (!tx.meta?.preTokenBalances || !tx.meta?.postTokenBalances) {
|
|
|
|
|
+ logger.warn('No token balance data in transaction');
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build pre-balance map
|
|
|
|
|
+ const preBalances = {};
|
|
|
|
|
+ tx.meta.preTokenBalances.forEach(balance => {
|
|
|
|
|
+ if (balance.mint === tokenAMint || balance.mint === tokenBMint) {
|
|
|
|
|
+ const key = `${balance.accountIndex}-${balance.mint}`;
|
|
|
|
|
+ preBalances[key] = {
|
|
|
|
|
+ amount: parseFloat(balance.uiTokenAmount?.uiAmountString || 0),
|
|
|
|
|
+ decimals: balance.decimals
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Find transfers by comparing pre/post balances
|
|
|
|
|
+ const transfers = [];
|
|
|
|
|
+ tx.meta.postTokenBalances.forEach(balance => {
|
|
|
|
|
+ if (balance.mint === tokenAMint || balance.mint === tokenBMint) {
|
|
|
|
|
+ const key = `${balance.accountIndex}-${balance.mint}`;
|
|
|
|
|
+ const pre = preBalances[key];
|
|
|
|
|
+
|
|
|
|
|
+ if (pre) {
|
|
|
|
|
+ const diff = pre.amount - parseFloat(balance.uiTokenAmount?.uiAmountString || 0);
|
|
|
|
|
+ if (diff > 0.000001) {
|
|
|
|
|
+ transfers.push({
|
|
|
|
|
+ mint: balance.mint,
|
|
|
|
|
+ amount: diff,
|
|
|
|
|
+ decimals: balance.decimals || pre.decimals || 6
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Assign to tokenA and tokenB
|
|
|
|
|
+ transfers.forEach(t => {
|
|
|
|
|
+ if (t.mint === tokenAMint) {
|
|
|
|
|
+ result.tokenA.amount = t.amount;
|
|
|
|
|
+ result.tokenA.decimals = t.decimals;
|
|
|
|
|
+ } else if (t.mint === tokenBMint) {
|
|
|
|
|
+ result.tokenB.amount = t.amount;
|
|
|
|
|
+ result.tokenB.decimals = t.decimals;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate USD value (simplified - uses fixed prices for common tokens)
|
|
|
|
|
+ const TOKEN_PRICES = {
|
|
|
|
|
+ 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': 1, // USDC
|
|
|
|
|
+ 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': 1, // USDT
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Try to get prices or use token amount as-is for calculation
|
|
|
|
|
+ const priceA = TOKEN_PRICES[tokenAMint] || 0;
|
|
|
|
|
+ const priceB = TOKEN_PRICES[tokenBMint] || 0;
|
|
|
|
|
+
|
|
|
|
|
+ result.totalUsdValue = (result.tokenA.amount * priceA) + (result.tokenB.amount * priceB);
|
|
|
|
|
+
|
|
|
|
|
+ // If no prices available, use sum of amounts (for non-stable pairs)
|
|
|
|
|
+ if (result.totalUsdValue === 0) {
|
|
|
|
|
+ result.totalUsdValue = result.tokenA.amount + result.tokenB.amount;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Error extracting token transfers:', error.message);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
extractParentPosition(tx) {
|
|
extractParentPosition(tx) {
|
|
|
try {
|
|
try {
|
|
|
const message = tx.transaction.message;
|
|
const message = tx.transaction.message;
|
|
|
const instructions = message.compiledInstructions || message.instructions || [];
|
|
const instructions = message.compiledInstructions || message.instructions || [];
|
|
|
|
|
|
|
|
for (const instruction of instructions) {
|
|
for (const instruction of instructions) {
|
|
|
- // Check if it's a memo instruction
|
|
|
|
|
const programId = message.staticAccountKeys
|
|
const programId = message.staticAccountKeys
|
|
|
? message.staticAccountKeys[instruction.programIdIndex].toString()
|
|
? message.staticAccountKeys[instruction.programIdIndex].toString()
|
|
|
: instruction.programId?.toString();
|
|
: instruction.programId?.toString();
|
|
|
|
|
|
|
|
if (programId === this.memoProgramId.toString()) {
|
|
if (programId === this.memoProgramId.toString()) {
|
|
|
- // Parse memo data
|
|
|
|
|
let memoData;
|
|
let memoData;
|
|
|
|
|
|
|
|
if (instruction.data) {
|
|
if (instruction.data) {
|
|
|
- // Try to decode base64 data
|
|
|
|
|
try {
|
|
try {
|
|
|
memoData = Buffer.from(instruction.data, 'base64').toString('utf8');
|
|
memoData = Buffer.from(instruction.data, 'base64').toString('utf8');
|
|
|
} catch {
|
|
} catch {
|
|
|
- // Try as string directly
|
|
|
|
|
memoData = instruction.data;
|
|
memoData = instruction.data;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Check if it's already parsed
|
|
|
|
|
if (instruction.parsed) {
|
|
if (instruction.parsed) {
|
|
|
memoData = instruction.parsed;
|
|
memoData = instruction.parsed;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (memoData) {
|
|
if (memoData) {
|
|
|
- logger.debug(`Memo data: ${memoData}`);
|
|
|
|
|
-
|
|
|
|
|
- // Extract referer_position from memo
|
|
|
|
|
- // Format: referer_position=GdMQaDtzcaAh4XVcHsUJV8yQShkVXDp9sJLNwvd23TxQ
|
|
|
|
|
const match = memoData.match(/referer_position=([A-Za-z0-9]+)/);
|
|
const match = memoData.match(/referer_position=([A-Za-z0-9]+)/);
|
|
|
if (match) {
|
|
if (match) {
|
|
|
return match[1];
|
|
return match[1];
|
|
@@ -252,7 +307,6 @@ export class OnchainMonitor {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Also check log messages as fallback
|
|
|
|
|
if (tx.meta.logMessages) {
|
|
if (tx.meta.logMessages) {
|
|
|
for (const log of tx.meta.logMessages) {
|
|
for (const log of tx.meta.logMessages) {
|
|
|
const match = log.match(/referer_position=([A-Za-z0-9]+)/);
|
|
const match = log.match(/referer_position=([A-Za-z0-9]+)/);
|
|
@@ -269,9 +323,6 @@ export class OnchainMonitor {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * Stop monitoring
|
|
|
|
|
- */
|
|
|
|
|
stop() {
|
|
stop() {
|
|
|
logger.info('Stopping onchain monitor...');
|
|
logger.info('Stopping onchain monitor...');
|
|
|
this.isRunning = false;
|
|
this.isRunning = false;
|