From d248d605fe2cf4c56f4946c27250bf8ce25b768c Mon Sep 17 00:00:00 2001 From: Marty Alcala Date: Thu, 4 Jun 2026 14:28:24 -0400 Subject: [PATCH] fix duplicate native EVM receive history rows --- .../evm/api/internalTxTransform.ts | 12 ++- .../test/integration/matic/csp.test.ts | 93 +++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/packages/bitcore-node/src/providers/chain-state/evm/api/internalTxTransform.ts b/packages/bitcore-node/src/providers/chain-state/evm/api/internalTxTransform.ts index 37ba39ca663..06c6f7a8b99 100644 --- a/packages/bitcore-node/src/providers/chain-state/evm/api/internalTxTransform.ts +++ b/packages/bitcore-node/src/providers/chain-state/evm/api/internalTxTransform.ts @@ -24,7 +24,9 @@ export class InternalTxRelatedFilterTransform extends TransformWithEventPipe { let internalTxsToProcess: Effect[] = []; if (tx.effects && tx.effects.length) { const walletRelatedInternalTxs = tx.effects.filter((internalTx: any) => - walletAddresses.includes(internalTx.to) && !internalTx.contractAddress + walletAddresses.includes(internalTx.to) && + !internalTx.contractAddress && + !this.isRootNativeTransfer(tx, internalTx) ); const refundTxs = walletRelatedInternalTxs.filter(i => i.to === tx.from); @@ -67,6 +69,14 @@ export class InternalTxRelatedFilterTransform extends TransformWithEventPipe { return done(); } + private isRootNativeTransfer(tx: MongoBound, internalTx: Effect) { + const callStack = internalTx.callStack || ''; + return (callStack === '0' || callStack === '') + && internalTx.from?.toLowerCase() === tx.from?.toLowerCase() + && internalTx.to?.toLowerCase() === tx.to?.toLowerCase() + && Number(internalTx.amount) === Number(tx.value); + } + async getWalletAddresses(tx) { if (!this.walletAddresses.length) { const cursor = WalletAddressStorage.collection diff --git a/packages/bitcore-node/test/integration/matic/csp.test.ts b/packages/bitcore-node/test/integration/matic/csp.test.ts index 20658c4da86..19750c83ca3 100644 --- a/packages/bitcore-node/test/integration/matic/csp.test.ts +++ b/packages/bitcore-node/test/integration/matic/csp.test.ts @@ -337,6 +337,57 @@ describe('Polygon/MATIC API', function() { it('should stream wallet\'s valid & invalid MATIC transactions', async () => await streamWalletTransactionsTest(chain, network, true) ); + + it('should not duplicate top-level native EVM receives with root effects', async () => { + const sender = '0x9F1a8A1bCc8e45Db2aD542e98b7174a9b519A45C'; + const amount = 17000; + const txid = '0xf1b8ad69d71cc68eb73a3778457fcb74f9a4471df9afae03dedd3e0de7ae7f58'; + + await EVMTransactionStorage.collection.insertOne({ + chain, + network, + txid, + blockHeight: 1, + blockHash: '0xblock', + blockTime: new Date('2024-01-01T00:00:00.000Z'), + blockTimeNormalized: new Date('2024-01-01T00:00:00.000Z'), + fee: 21000, + value: amount, + wallets: [wallet._id as ObjectId], + to: address, + from: sender, + gasLimit: 21000, + gasPrice: 1, + nonce: 1, + transactionIndex: 0, + data: Buffer.from(''), + internal: [], + calls: [], + receipt: { + status: true, + transactionHash: txid, + transactionIndex: 0, + blockHash: '0xblock', + blockNumber: 1, + cumulativeGasUsed: 21000, + gasUsed: 21000, + logs: [] + }, + effects: [{ + to: address, + from: sender, + amount: String(amount), + callStack: '0' + }] + } as IEVMTransactionInProcess); + + const txs = await streamWalletTransactionRows(chain, network, wallet); + + expect(txs).to.have.lengthOf(1); + expect(txs[0].txid).to.equal(txid); + expect(txs[0].category).to.equal('receive'); + expect(txs[0].satoshis).to.equal(String(amount)); + }); }); }); @@ -426,3 +477,45 @@ const streamWalletTransactionsTest = async (chain: string, network: string, incl expect(counter).to.eq(includeInvalidTxs ? txCount * 2 : txCount); sandbox.restore(); }; + +const streamWalletTransactionRows = async (chain: string, network: string, wallet: IWallet) => { + const chunks: string[] = []; + const req = (new Writable({ + write: function(_data, _, cb) { + cb(); + } + }) as unknown) as Request; + + const res = (new Writable({ + write: function(data, _, cb) { + chunks.push(data.toString()); + cb(); + } + }) as unknown) as Response; + res.type = () => res; + + const err = await new Promise(r => { + res + .on('error', r) + .on('finish', r); + + MATIC.streamWalletTransactions({ + chain, + network, + wallet, + req, + res, + args: {} + } as StreamWalletTransactionsParams) + .catch(e => r(e)); + }); + + expect(err).to.not.exist; + + return chunks + .join('') + .trim() + .split('\n') + .filter(Boolean) + .map(line => JSON.parse(line)); +};