From 445fe3ed8e0fe3b9a0c51b8e06e232338c1b3911 Mon Sep 17 00:00:00 2001 From: bingyuyap Date: Tue, 20 Aug 2024 17:03:56 -0600 Subject: [PATCH] ft_watcher: check for multiple ixs in tx and inline deconstructor Signed-off-by: bingyuyap --- database/fast-transfer-schema.sql | 2 +- .../src/fastTransfer/swapLayer/solParser.ts | 83 ++++++++++--------- .../SolanaSwapLayerParser.test.ts | 21 +++-- 3 files changed, 57 insertions(+), 49 deletions(-) diff --git a/database/fast-transfer-schema.sql b/database/fast-transfer-schema.sql index cffb1e6e..39460067 100644 --- a/database/fast-transfer-schema.sql +++ b/database/fast-transfer-schema.sql @@ -60,7 +60,7 @@ CREATE TABLE fast_transfer_executions ( execution_slot BIGINT, execution_time TIMESTAMP, -- fill_id can be a vaa id (cctp) or solana account pubkey (local) - fill_id VARCHAR(255), + fill_id VARCHAR(255) ); -- Settlement is created when the settlement is created in the `settleFastTransfer` diff --git a/watcher/src/fastTransfer/swapLayer/solParser.ts b/watcher/src/fastTransfer/swapLayer/solParser.ts index 272d912c..262ee2bb 100644 --- a/watcher/src/fastTransfer/swapLayer/solParser.ts +++ b/watcher/src/fastTransfer/swapLayer/solParser.ts @@ -51,41 +51,45 @@ export class SwapLayerParser { */ private async processTransaction( transaction: VersionedTransactionResponse - ): Promise { + ): Promise { const sig = transaction.transaction.signatures[0]; const programInstructions = this.getProgramInstructions(transaction); - for (const { ix } of programInstructions) { - const decoded = this.swapLayerBorshCoder.instruction.decode(Buffer.from(ix.data)); - if (!decoded) continue; - - try { - switch (decoded.name) { - case 'complete_swap_direct': - case 'complete_swap_relay': - case 'complete_swap_payload': - return await this.parseSwapInstruction(transaction, ix, decoded.name); - - case 'complete_transfer_direct': - case 'complete_transfer_relay': - case 'complete_transfer_payload': - return await this.parseTransferInstruction(transaction, ix, decoded.name); - - case 'release_inbound': - return await this.parseReleaseInbound(transaction, ix, decoded.name); - - default: - // we will not log when there are unknown instructions to prevent log congestion - continue; + const results = await Promise.all( + programInstructions.map(async ({ ix }) => { + const decoded = this.swapLayerBorshCoder.instruction.decode(Buffer.from(ix.data)); + if (!decoded) return null; + + try { + switch (decoded.name) { + case 'complete_swap_direct': + case 'complete_swap_relay': + case 'complete_swap_payload': + return await this.parseSwapInstruction(transaction, ix, decoded.name); + + case 'complete_transfer_direct': + case 'complete_transfer_relay': + case 'complete_transfer_payload': + return await this.parseTransferInstruction(transaction, ix, decoded.name); + + case 'release_inbound': + return await this.parseReleaseInbound(transaction, ix, decoded.name); + + default: + // Skip unknown instructions + // we will not log when there are unknown instructions to prevent log congestion + return null; + } + } catch (error) { + console.error(`Error processing ${decoded.name} in transaction ${sig}:`, error); + // Continue to the next instruction if there's an error + return null; } - } catch (error) { - console.error(`Error processing ${decoded.name} in transaction ${sig}:`, error); - // Continue to the next instruction if there's an error - continue; - } - } + }) + ); - return null; + // Filter out any null results + return results.filter((result): result is TransferCompletion => result !== null); } /** @@ -93,14 +97,14 @@ export class SwapLayerParser { * * @param signature - The signature of the transaction to fetch and process. * - * @returns A `TransferCompletion` object containing parsed details from the transaction, - * or `null` if no relevant instructions were found. + * @returns An array of `TransferCompletion` objects containing parsed details from the transaction. + * If no relevant instructions were found, an empty array is returned. */ - async parseTransaction(signature: string): Promise { + async parseTransaction(signature: string): Promise { const transaction = await this.connection.getTransaction(signature, { maxSupportedTransactionVersion: 0, }); - if (!transaction) return null; + if (!transaction) return []; return this.processTransaction(transaction); } @@ -123,15 +127,14 @@ export class SwapLayerParser { (tx): tx is VersionedTransactionResponse => tx !== null ); - // Process each transaction and filter out null results + // Process each transaction and gather the results const promises = nonNullTransactions.map(async (tx) => await this.processTransaction(tx)); const results = await Promise.all(promises); - // Filter out null results from the processed transactions - return results.filter((res): res is TransferCompletion => res !== null); + // Flatten the array and filter out any null values + return results.flat().filter((res): res is TransferCompletion => res !== null); } - // === parsing logic === /** @@ -223,9 +226,7 @@ export class SwapLayerParser { throw new Error(`Transaction block time not found: ${sig}`); } - const instructionConfig = this.getInstructionConfig(instructionName); - - const { fillAccountIndex, recipientIndex } = instructionConfig; + const { fillAccountIndex, recipientIndex } = this.getInstructionConfig(instructionName); if (ix.accountKeyIndexes.length <= recipientIndex) { throw new Error(`${INSUFFICIENT_ACCOUNTS} for ${instructionName} in ${sig}`); diff --git a/watcher/src/watchers/__tests__/fastTransfer/SolanaSwapLayerParser.test.ts b/watcher/src/watchers/__tests__/fastTransfer/SolanaSwapLayerParser.test.ts index 5f959665..d4767a5a 100644 --- a/watcher/src/watchers/__tests__/fastTransfer/SolanaSwapLayerParser.test.ts +++ b/watcher/src/watchers/__tests__/fastTransfer/SolanaSwapLayerParser.test.ts @@ -156,6 +156,7 @@ describe('SwapLayerParser', () => { const txHash = '32goGrEsPb6Kky65Z4wX6wswzjDbT9pBWs1HSZFsfWhxoA1fnSsoE9hJgtepPL8VyKQJUdRrfGWPrXCizDufArwR'; const result = await parser.parseTransaction(txHash); + expect(result.length).toBe(1); const expected = { fill_id: 'BkWHY4H2kEVevdUeiRmFYNtg5zURRbTEtjt29KWdbjzV', output_token: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', @@ -168,7 +169,7 @@ describe('SwapLayerParser', () => { relaying_fee: '0', }; - const serializedRes = seralizedRes(result); + const serializedRes = seralizedRes(result[0]); expect(serializedRes).toEqual(expected); }); @@ -177,6 +178,7 @@ describe('SwapLayerParser', () => { '4EWH6ZetTTjdYSbxqXddKNKLKDpBctELAhqChmkey2jwunZaj1Digj1fQxBMxtw6uhDeqkX3ev2vucu7jrexhWka'; const result = await parser.parseTransaction(txHash); + expect(result.length).toBe(1); const expected = { recipient: 'FQ4PBuykgHqemPhqqktJL9y1L7oTbShYiwGkwgM1VceF', @@ -190,7 +192,7 @@ describe('SwapLayerParser', () => { staged_inbound: undefined, }; - const serializedRes = seralizedRes(result); + const serializedRes = seralizedRes(result[0]); expect(serializedRes).toEqual(expected); }); @@ -199,6 +201,7 @@ describe('SwapLayerParser', () => { '3Ufce773W4xgVsZiGBhSRPQssfaNdrEWeTBPLTnQSFZHsVx9ADaSN9yQBF6kcQMyDAoAnM3BVU88tQ2TbDZn1kUJ'; const result = await parser.parseTransaction(txHash); + expect(result.length).toBe(1); const expected = { recipient: 'GcppBeM1UYGU4b7aX9uPAqL4ZEUThNHt5FpxPtzBE1xx', tx_hash: @@ -210,7 +213,7 @@ describe('SwapLayerParser', () => { output_amount: '49564106', staged_inbound: undefined, }; - const serializedRes = seralizedRes(result); + const serializedRes = seralizedRes(result[0]); expect(serializedRes).toEqual(expected); }); @@ -219,6 +222,7 @@ describe('SwapLayerParser', () => { '39K8aHVDmyAjne6J4PBFkvmKZH9CQR9QpbmTFafeiTLxeWg5n5RgcRdX5AYhebLR9shiUHrDeqg4YSD1EhRZNpS1'; const result = await parser.parseTransaction(txHash); + expect(result.length).toBe(1); const expected = { fill_id: 'Hru6CBfyXtG18zF33DnXEjmECjgj1eMjNfPRaESBqpUr', output_token: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', @@ -230,7 +234,7 @@ describe('SwapLayerParser', () => { '39K8aHVDmyAjne6J4PBFkvmKZH9CQR9QpbmTFafeiTLxeWg5n5RgcRdX5AYhebLR9shiUHrDeqg4YSD1EhRZNpS1', relaying_fee: '0', }; - const serializedRes = seralizedRes(result); + const serializedRes = seralizedRes(result[0]); expect(serializedRes).toEqual(expected); }); @@ -239,6 +243,7 @@ describe('SwapLayerParser', () => { 'eo2CugBsJ9Efbtg9TAiYyBvvZZsbh93ZZcLDxxjbmbEpZojCF8BDphVVrCjXtMkSLaP2EGQE5zSrjU4r6fxsxRP'; const result = await parser.parseTransaction(txHash); + expect(result.length).toBe(1); const expected = { recipient: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', tx_hash: @@ -250,7 +255,7 @@ describe('SwapLayerParser', () => { output_amount: '0', staged_inbound: 'ECiEWJndTfUJaEQ59gYgy6e4331mkrh1USQCmDcBwBvj', }; - const serializedRes = seralizedRes(result); + const serializedRes = seralizedRes(result[0]); expect(serializedRes).toEqual(expected); }); @@ -259,6 +264,7 @@ describe('SwapLayerParser', () => { '4yCcw8MJ1BokhPJM2fQC3BMfoezteM4MkaHLfjPrLG25AEW4EeNxcNsrgU3ECkwQ1sy3AKFseafxM2mfjdwbzo8x'; const result = await parser.parseTransaction(txHash); + expect(result.length).toBe(1); const expected = { fill_id: 'ESccxJbedTgsu7kwK6uNWnMrg3GiD7pgexXfWeyZNK3J', output_token: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', @@ -270,7 +276,7 @@ describe('SwapLayerParser', () => { '4yCcw8MJ1BokhPJM2fQC3BMfoezteM4MkaHLfjPrLG25AEW4EeNxcNsrgU3ECkwQ1sy3AKFseafxM2mfjdwbzo8x', relaying_fee: '0', }; - const serializedRes = seralizedRes(result); + const serializedRes = seralizedRes(result[0]); expect(serializedRes).toEqual(expected); }); @@ -279,6 +285,7 @@ describe('SwapLayerParser', () => { '2EFLPdYpdJzeoe4HD4fNRWwphhy9HyEHFj3EQtY9agUPmQ5LjJkXFjEt5dnshS9sSTby9nN2QF9BaCbVyiBFGLxj'; const result = await parser.parseTransaction(txHash); + expect(result.length).toBe(1); const expected = { tx_hash: '2EFLPdYpdJzeoe4HD4fNRWwphhy9HyEHFj3EQtY9agUPmQ5LjJkXFjEt5dnshS9sSTby9nN2QF9BaCbVyiBFGLxj', @@ -290,7 +297,7 @@ describe('SwapLayerParser', () => { output_amount: '6900000000', staged_inbound: 'GFJ6699xu2BER8t98S4Vy6ZQam4mvr539AaqvHHBh9i3', }; - const serializedRes = seralizedRes(result); + const serializedRes = seralizedRes(result[0]); expect(serializedRes).toEqual(expected); }); });