|
|
@@ -0,0 +1,274 @@
|
|
|
+#!/usr/bin/env node
|
|
|
+
|
|
|
+/**
|
|
|
+ * Test: Parse transaction and extract copy parameters
|
|
|
+ * Usage: node test-tx-parse.js <transaction_signature>
|
|
|
+ *
|
|
|
+ * Example: node test-tx-parse.js 5SnXmX8T4sytTFu8xeG7RXmui1eK65no8B3eJ3knTtdXqr3PqmDma5CaY9C7NG2oycvNs7KKZ75pPNDXB2T5XYQE
|
|
|
+ */
|
|
|
+
|
|
|
+import { Connection, PublicKey } from '@solana/web3.js';
|
|
|
+import { CONFIG } from './src/config/index.js';
|
|
|
+import { ByrealAPI } from './src/services/byreal.js';
|
|
|
+import { logger, setLogLevel } from './src/utils/index.js';
|
|
|
+
|
|
|
+setLogLevel('info');
|
|
|
+
|
|
|
+const TX_SIGNATURE = process.argv[2];
|
|
|
+
|
|
|
+if (!TX_SIGNATURE) {
|
|
|
+ console.error('Usage: node test-tx-parse.js <transaction_signature>');
|
|
|
+ console.error('Example: node test-tx-parse.js 5SnXmX8T4sytTFu8xeG7RXmui1eK65no8B3eJ3knTtdXqr3PqmDma5CaY9C7NG2oycvNs7KKZ75pPNDXB2T5XYQE');
|
|
|
+ process.exit(1);
|
|
|
+}
|
|
|
+
|
|
|
+const BYREAL_PROGRAM_ID = 'REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2';
|
|
|
+const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
|
|
|
+
|
|
|
+async function testTransactionParse() {
|
|
|
+ console.log('═══════════════════════════════════════════════════');
|
|
|
+ console.log('🧪 Testing Transaction Parse for Copy Parameters');
|
|
|
+ console.log(`📍 Transaction: ${TX_SIGNATURE.slice(0, 20)}...`);
|
|
|
+ console.log('═══════════════════════════════════════════════════\n');
|
|
|
+
|
|
|
+ const connection = new Connection(CONFIG.RPC_URL, 'confirmed');
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Step 1: Fetch transaction
|
|
|
+ console.log('Step 1: Fetching transaction from Solana...');
|
|
|
+ const tx = await connection.getTransaction(TX_SIGNATURE, {
|
|
|
+ commitment: 'confirmed',
|
|
|
+ maxSupportedTransactionVersion: 0
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!tx || !tx.transaction || !tx.meta) {
|
|
|
+ console.error('❌ Transaction not found or invalid');
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('✅ Transaction found\n');
|
|
|
+
|
|
|
+ // Step 2: Check Byreal program
|
|
|
+ console.log('Step 2: Checking Byreal program involvement...');
|
|
|
+ const message = tx.transaction.message;
|
|
|
+ const accountKeys = message.staticAccountKeys || message.accountKeys || [];
|
|
|
+
|
|
|
+ const hasByrealProgram = accountKeys.some(
|
|
|
+ key => key.toString() === BYREAL_PROGRAM_ID
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!hasByrealProgram) {
|
|
|
+ console.error('❌ Transaction does not involve Byreal program');
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+ console.log('✅ Byreal program found\n');
|
|
|
+
|
|
|
+ // Step 3: Extract parent position from memo
|
|
|
+ console.log('Step 3: Extracting parent position from memo...');
|
|
|
+ let parentPosition = null;
|
|
|
+ const instructions = message.compiledInstructions || message.instructions || [];
|
|
|
+
|
|
|
+ for (const instruction of instructions) {
|
|
|
+ const programId = accountKeys[instruction.programIdIndex]?.toString();
|
|
|
+
|
|
|
+ if (programId === MEMO_PROGRAM_ID) {
|
|
|
+ let memoData;
|
|
|
+
|
|
|
+ if (instruction.data) {
|
|
|
+ try {
|
|
|
+ memoData = Buffer.from(instruction.data, 'base64').toString('utf8');
|
|
|
+ } catch {
|
|
|
+ memoData = instruction.data;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (instruction.parsed) {
|
|
|
+ memoData = instruction.parsed;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (memoData) {
|
|
|
+ console.log(` Memo data: ${memoData}`);
|
|
|
+ const match = memoData.match(/referer_position=([A-Za-z0-9]+)/);
|
|
|
+ if (match) {
|
|
|
+ parentPosition = match[1];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check log messages as fallback
|
|
|
+ if (!parentPosition && tx.meta.logMessages) {
|
|
|
+ for (const log of tx.meta.logMessages) {
|
|
|
+ const match = log.match(/referer_position=([A-Za-z0-9]+)/);
|
|
|
+ if (match) {
|
|
|
+ parentPosition = match[1];
|
|
|
+ console.log(` Found in logs: ${memoData}`);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!parentPosition) {
|
|
|
+ console.error('❌ No parent position found in memo');
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`✅ Parent Position: ${parentPosition}\n`);
|
|
|
+
|
|
|
+ // Step 4: Fetch parent position details
|
|
|
+ console.log('Step 4: Fetching parent position details from Byreal API...');
|
|
|
+ const parentDetail = await ByrealAPI.getPositionDetail(parentPosition);
|
|
|
+
|
|
|
+ if (!parentDetail || !parentDetail.pool) {
|
|
|
+ console.error('❌ Failed to fetch parent position details');
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('✅ Parent position details:');
|
|
|
+ console.log(` Pool: ${parentDetail.pool.mintA?.symbol}/${parentDetail.pool.mintB?.symbol}`);
|
|
|
+ console.log(` Token A: ${parentDetail.pool.mintA?.address}`);
|
|
|
+ console.log(` Token B: ${parentDetail.pool.mintB?.address}`);
|
|
|
+ console.log(` NFT Mint: ${parentDetail.nftMintAddress || parentDetail.nftMint}`);
|
|
|
+ console.log(` Pool Address: ${parentDetail.pool?.poolAddress || parentDetail.poolAddress}\n`);
|
|
|
+
|
|
|
+ const tokenAMint = parentDetail.pool.mintA?.address;
|
|
|
+ const tokenBMint = parentDetail.pool.mintB?.address;
|
|
|
+
|
|
|
+ // Step 5: Parse transaction for token transfers
|
|
|
+ console.log('Step 5: Parsing transaction for token transfers...');
|
|
|
+
|
|
|
+ if (!tx.meta.preTokenBalances || !tx.meta.postTokenBalances) {
|
|
|
+ console.error('❌ No token balance data in transaction');
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log(` Pre-balances found: ${Object.keys(preBalances).length}`);
|
|
|
+
|
|
|
+ // Find transfers by comparing pre/post
|
|
|
+ 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,
|
|
|
+ symbol: balance.mint === tokenAMint ? parentDetail.pool.mintA?.symbol : parentDetail.pool.mintB?.symbol
|
|
|
+ });
|
|
|
+ console.log(` Transfer detected: ${diff} ${balance.mint === tokenAMint ? parentDetail.pool.mintA?.symbol : parentDetail.pool.mintB?.symbol}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (transfers.length === 0) {
|
|
|
+ console.error('❌ No token transfers found for target tokens');
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Assign to tokenA and tokenB
|
|
|
+ const tokenA = transfers.find(t => t.mint === tokenAMint) || { mint: tokenAMint, amount: 0, decimals: 6, symbol: parentDetail.pool.mintA?.symbol };
|
|
|
+ const tokenB = transfers.find(t => t.mint === tokenBMint) || { mint: tokenBMint, amount: 0, decimals: 6, symbol: parentDetail.pool.mintB?.symbol };
|
|
|
+
|
|
|
+ // Calculate USD value (simplified)
|
|
|
+ const TOKEN_PRICES = {
|
|
|
+ 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': 1, // USDC
|
|
|
+ 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': 1, // USDT
|
|
|
+ };
|
|
|
+
|
|
|
+ const priceA = TOKEN_PRICES[tokenAMint] || 0;
|
|
|
+ const priceB = TOKEN_PRICES[tokenBMint] || 0;
|
|
|
+ const totalUsdValue = (tokenA.amount * priceA) + (tokenB.amount * priceB);
|
|
|
+
|
|
|
+ if (totalUsdValue === 0) {
|
|
|
+ // For non-stable pairs, use sum
|
|
|
+ console.log(' ⚠️ No price data available, using sum of amounts');
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`✅ Token transfers extracted:\n`);
|
|
|
+
|
|
|
+ // Step 6: Print copy parameters
|
|
|
+ console.log('═══════════════════════════════════════════════════');
|
|
|
+ console.log('📋 COPY PARAMETERS');
|
|
|
+ console.log('═══════════════════════════════════════════════════\n');
|
|
|
+
|
|
|
+ console.log('Parent Position:');
|
|
|
+ console.log(` Address: ${parentPosition}`);
|
|
|
+ console.log(` NFT Mint: ${parentDetail.nftMintAddress || parentDetail.nftMint}`);
|
|
|
+ console.log(` Pool Address: ${parentDetail.pool?.poolAddress || parentDetail.poolAddress}`);
|
|
|
+ console.log(` Pool: ${parentDetail.pool.mintA?.symbol}/${parentDetail.pool.mintB?.symbol}\n`);
|
|
|
+
|
|
|
+ console.log('Token Transfers (Target deposited):');
|
|
|
+ console.log(` Token A: ${tokenA.amount} ${tokenA.symbol}`);
|
|
|
+ console.log(` Mint: ${tokenA.mint}`);
|
|
|
+ console.log(` Decimals: ${tokenA.decimals}`);
|
|
|
+ console.log(` Token B: ${tokenB.amount} ${tokenB.symbol}`);
|
|
|
+ console.log(` Mint: ${tokenB.mint}`);
|
|
|
+ console.log(` Decimals: ${tokenB.decimals}\n`);
|
|
|
+
|
|
|
+ console.log('Calculated Values:');
|
|
|
+ console.log(` Token A USD: $${(tokenA.amount * priceA).toFixed(2)}`);
|
|
|
+ console.log(` Token B USD: $${(tokenB.amount * priceB).toFixed(2)}`);
|
|
|
+ console.log(` Total USD Value: $${totalUsdValue.toFixed(2)}\n`);
|
|
|
+
|
|
|
+ const copyMultiplier = CONFIG.COPY_MULTIPLIER;
|
|
|
+ const maxUsd = CONFIG.MAX_USD_VALUE;
|
|
|
+ const copyUsd = Math.min(totalUsdValue * copyMultiplier, maxUsd);
|
|
|
+
|
|
|
+ console.log('Copy Calculation:');
|
|
|
+ console.log(` Target Value: $${totalUsdValue.toFixed(2)}`);
|
|
|
+ console.log(` Multiplier: ${copyMultiplier}x`);
|
|
|
+ console.log(` Calculated: $${(totalUsdValue * copyMultiplier).toFixed(2)}`);
|
|
|
+ console.log(` Max Limit: $${maxUsd}`);
|
|
|
+ console.log(` ✅ Final Copy Amount: $${copyUsd.toFixed(2)}\n`);
|
|
|
+
|
|
|
+ console.log('═══════════════════════════════════════════════════');
|
|
|
+ console.log('✅ Test completed successfully!');
|
|
|
+ console.log('═══════════════════════════════════════════════════');
|
|
|
+
|
|
|
+ // Return data structure matching what sniper expects
|
|
|
+ return {
|
|
|
+ parentPositionAddress: parentPosition,
|
|
|
+ transactionSignature: TX_SIGNATURE,
|
|
|
+ parentDetail: parentDetail,
|
|
|
+ tokenTransfers: {
|
|
|
+ tokenA: tokenA,
|
|
|
+ tokenB: tokenB,
|
|
|
+ totalUsdValue: totalUsdValue
|
|
|
+ },
|
|
|
+ targetUsdValue: totalUsdValue,
|
|
|
+ copyUsdValue: copyUsd
|
|
|
+ };
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ Test failed:', error);
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+testTransactionParse().then(data => {
|
|
|
+ console.log('\n📦 Full data structure (as passed to sniper):');
|
|
|
+ console.log(JSON.stringify({
|
|
|
+ parentPositionAddress: data.parentPositionAddress,
|
|
|
+ transactionSignature: data.transactionSignature,
|
|
|
+ targetUsdValue: data.targetUsdValue,
|
|
|
+ copyUsdValue: data.copyUsdValue
|
|
|
+ }, null, 2));
|
|
|
+});
|