feat: add TransferIndex for unique transfer identification#79
Conversation
- Add TransferIndex + BlockHash fields to types.Transaction - Add TransactionIndex to evm.Txn (parsed from RPC response) - Add hexIndexToDecimal helper for hex→decimal index conversion - Set TransferIndex in ExtractTransfers (native: txIndex, ERC20: txIndex:logIndex) - Set TransferIndex in ExtractSafeTransfers (txIndex:safe:0) - Set TransferIndex in ExtractInternalTransfers (txIndex:trace:N with DFS counter) - Set BlockHash on all transfers in convertBlock (post-extraction loop) - Add TransferIndex to DedupTransfers key - Update Transaction.Hash() with BlockHash + TransferIndex for NATS idempotency - EVM: NetworkId|TxHash|BlockHash|TransferIndex|Direction - Non-EVM fallback: NetworkId|TxHash|BlockHash|From|To|Timestamp|Direction Closes #78
transfer_index_test.go (10 tests): - hexIndexToDecimal: normal, edge cases, malformed input - ExtractTransfers: native (txIndex), ERC20 log (txIndex:logIndex), ERC20 input (txIndex) - ExtractSafeTransfers: txIndex:safe:0 format - ExtractInternalTransfers: sequential trace counters, DFS ordering - DedupTransfers: same amount different index (not deduped), same index (deduped) hash_test.go (6 tests): - Different TransferIndex → different Hash - Two-way indexing in/out → different Hash - Same position → same Hash (idempotent) - Non-EVM fallback: different addresses → different Hash - Non-EVM fallback: same everything → same Hash - Reorg (different BlockHash) → different Hash
transfer_index_integration_test.go (2 tests, need TRACE_RPC_URL): - BatchSendNative: 12 internal transfers each get unique sequential TransferIndex - SafeTx: trace transfers use trace format, Safe heuristic uses safe:0 format emit_test.go (3 tests): - TwoWayIndexing: in/out events for same transfer produce different Hash() - DuplicateAmountTransfers: same amount to same address at different positions produce different Hash() — NATS delivers both - MixedSources: trace/safe/erc20/native transfers in same tx all have unique Hash()
Test Results — All PassUnit Tests (19 tests)Integration Tests (2 tests, need RPC_URL)Run with: RPC_URL=<your-debug-rpc-url> go test ./internal/rpc/evm/ -v -run "TestEndToEnd_.*TransferIndex" -count=1Optionally set Test Coverage for Changed Code
* |
SummaryAdds Review Checklist
Findings
Verdictcorrect — clean implementation with thorough tests. One informational note below about the Hash() backward compatibility, but it's low-impact and likely acceptable. |
| builder.WriteString(t.TxHash) | ||
| builder.WriteByte('|') | ||
| builder.WriteString(t.FromAddress) | ||
| builder.WriteString(t.BlockHash) |
There was a problem hiding this comment.
📘 note — Non-EVM hash backward compatibility
The comment says "preserve current hash behavior" for the non-EVM fallback, but the old Hash() produced NetworkId|TxHash|From|To|Timestamp|Direction while the new one produces NetworkId|TxHash|BlockHash|From|To|Timestamp|Direction. For chains that don't set BlockHash (e.g. TRON), this inserts an empty string between two | delimiters, changing every existing hash.
This means all NATS idempotency keys for non-EVM chains will change on deployment. In practice this is low-risk since NATS dedup windows are short, but it could cause a brief window of duplicate deliveries for in-flight messages. Worth noting in deployment docs or release notes so operators are aware.
No code change needed — just calling it out as a known behavioral change.
SummaryThis PR adds Review Checklist
Findings
Verdictneeds attention — the |
…nt hash collisions
Closes #78
Summary
Add
TransferIndexandBlockHashfields totypes.Transactionfor unique transfer identification within transactions. Enables proper dedup for batch transfers, reorg-aware NATS delivery, and stable consumer upsert keys.Two identity levels
NetworkId + TxHash + TransferIndex + DirectionNetworkId + TxHash + BlockHash + TransferIndex + DirectionHash())TODO Checklist
Phase 1: Add fields + parse positional data
TransferIndex+BlockHashtotypes.TransactionTransactionIndextoTxnhexIndexToDecimalhelperPhase 2: Set TransferIndex in extraction functions
ExtractTransfers(native + ERC20)ExtractSafeTransfersExtractInternalTransfers+walkCallTracePhase 3: Update call sites
BlockHashon all transfers inconvertBlock(post-extraction loop)DedupTransferskeyBlockHash+TransferIndextoTransaction.Hash()(NATS idempotency key)Phase 4: Tests — Unit (16 tests)
TestHexIndexToDecimal— normal, edge, malformed inputTestExtractTransfers_TransferIndex_NativeTransferTestExtractTransfers_TransferIndex_ERC20LogTestExtractTransfers_TransferIndex_ERC20InputTestExtractSafeTransfers_TransferIndexTestExtractInternalTransfers_TransferIndexTestExtractInternalTransfers_TransferIndex_OrderingTestDedupTransfers_SameAmountDifferentIndexTestDedupTransfers_SameIndexTestTransactionHash_IncludesTransferIndexTestTransactionHash_TwoWayIndexing_DirectionDiffersTestTransactionHash_SamePositionSameHashTestTransactionHash_NoTransferIndex_FallbackBehaviorTestTransactionHash_NoTransferIndex_SameAddresses_SameHashTestTransactionHash_ReorgProducesDifferentHashPhase 4: Tests — Worker-level (3 tests)
TestEmitBlock_TwoWayIndexing_UniqueHashesTestEmitBlock_DuplicateAmountTransfers_UniqueHashesTestEmitBlock_MixedSources_UniqueHashesPhase 4: Tests — Integration (2 tests, need TRACE_RPC_URL)
TestEndToEnd_BatchSendNative_TransferIndexTestEndToEnd_SafeTx_TransferIndexImplementation Plan
See implementation plan comment on the issue.