33// Copyright (C) 2020-2026 Intergalactic, Limited (GIB).
44// SPDX-License-Identifier: Apache-2.0
55
6- //! Event → synthetic-eth-tx translation for the node-indexing variant.
6+ //! Pure event → synthetic-eth-tx translation for the node-indexing variant.
77//!
8- //! Pure translation: `synthetic_txs_from_records` turns a block's
9- //! `EventRecord`s into synthetic `(Transaction, TransactionStatus, Receipt)`
10- //! triples using only pure helpers (`account_to_evm_address`,
11- //! `asset_evm_address`, the `build_*`/`assemble_synth_txs` primitives) — no
12- //! runtime-state reads. That's what lets the SAME code serve two callers:
13- //!
14- //! - `SyntheticEthLogsApi` (this runtime), gathering events from
15- //! `frame_system::Events` — fast path for the current+ runtime; and
16- //! - the node's client-side indexer, which reads `System::Events` from state
17- //! and calls `synthetic_txs_from_records` directly — works against ANY
18- //! runtime version (it never invokes a runtime API), so blocks produced
19- //! before this runtime shipped are still indexed (bounded only by whether
20- //! their events still decode).
21- //!
22- //! Scope: every balance movement (orml-tokens + native HDX via pallet-balances)
23- //! — transfer, mint, burn/slash/dust, reserve/unreserve, repatriate, and
24- //! lock/freeze — mapped to erc20 `Transfer` so a holder's aggregated transfers
25- //! reconstruct its transferable balance (reserved and frozen amounts move to
26- //! distinct per-owner sentinels). Plus `Swapped3` → uniswap-v2 `Swap`, and
27- //! `pallet_evm::Event::Log` from internal `Executor::call` paths (deduped
28- //! against real eth txs). This is the off-chain replica of what the removed
29- //! on-chain synthetic-logs hooks emitted.
8+ //! `synthetic_txs_from_records` turns a block's `EventRecord`s into
9+ //! `(Transaction, TransactionStatus, Receipt)` triples with no state reads, so
10+ //! the node's client-side indexer can call it for any runtime version. Balance
11+ //! movements map to erc20 `Transfer` (reserved/frozen go to per-owner sentinels
12+ //! so aggregated transfers equal an account's transferable balance), plus
13+ //! `Swapped3` → uniswap-v2 `Swap` and internal `pallet_evm::Log` (deduped vs
14+ //! real eth txs).
3015
3116use crate :: { Runtime , RuntimeEvent } ;
3217use frame_support:: traits:: Get ;
@@ -88,11 +73,8 @@ fn is_asset_address(addr: H160) -> bool {
8873 addr. 0 [ ..15 ] == [ 0u8 ; 15 ] && addr. 0 [ 15 ] == 1
8974}
9075
91- /// One erc20 `Transfer(from, to, amount)` on `asset`'s evm address, or empty for
92- /// a zero amount. The single primitive every balance movement below maps onto:
93- /// mint = `from` zero, burn = `to` zero, reserve/lock = `to` the owner's
94- /// reserved/frozen sentinel. Aggregating these per (asset, holder) reconstructs
95- /// the holder's transferable balance.
76+ /// One erc20 `Transfer` on `asset`'s evm address (empty for zero). Mint = `from`
77+ /// zero, burn = `to` zero, reserve/lock = `to` the owner's sentinel.
9678fn transfer ( asset : u32 , from : H160 , to : H160 , amount : u128 ) -> Vec < ( H160 , ethereum:: Log ) > {
9779 if amount == 0 {
9880 return Vec :: new ( ) ;
@@ -101,14 +83,7 @@ fn transfer(asset: u32, from: H160, to: H160, amount: u128) -> Vec<(H160, ethere
10183 sp_std:: vec![ ( addr, build_erc20_transfer_log( addr, from, to, U256 :: from( amount) ) ) ]
10284}
10385
104- /// Pure: evm logs an indexer should see for one runtime event.
105- ///
106- /// Covers every balance movement the on-chain synthetic-logs hooks did, now
107- /// driven off-chain from events: transfer, deposit/mint (`0x0 → who`),
108- /// withdraw/slash/burn/dust (`who → 0x0`), reserve/unreserve (`who ↔
109- /// reserved_address_of(who)`), reserve repatriation, plus lock/freeze (`who ↔
110- /// frozen_address_of(who)`, by frozen delta) so a holder's aggregated transfers
111- /// equal its transferable balance.
86+ /// Pure: the evm logs an indexer should see for one runtime event.
11287pub fn logs_from_event ( event : & RuntimeEvent ) -> Vec < ( H160 , ethereum:: Log ) > {
11388 use orml_tokens:: Event as Tokens ;
11489 use pallet_balances:: Event as Balances ;
@@ -206,12 +181,8 @@ pub fn logs_from_event(event: &RuntimeEvent) -> Vec<(H160, ethereum::Log)> {
206181 | RuntimeEvent :: Balances ( Balances :: DustLost { account : who, amount } ) => {
207182 transfer ( CORE_ASSET_ID , evm_addr ( who) , ZERO , * amount)
208183 }
209- // Native `Slashed` can't tell free from reserved at the event level, but in
210- // this runtime native slashing only ever hits *reserved* balance (governance
211- // bonds/deposits via democracy & elections `slash_reserved`); there are no
212- // free native-slash call sites. So we burn from the reserved sentinel, which
213- // keeps transferable reconstruction correct. Revisit if a free native-slash
214- // path is ever introduced. (orml `Slashed` splits free/reserved explicitly.)
184+ // Native slashing here only ever hits reserved balance (governance bonds via
185+ // democracy/elections); the event can't distinguish, so burn the reserved sentinel.
215186 RuntimeEvent :: Balances ( Balances :: Slashed { who, amount } ) => {
216187 transfer ( CORE_ASSET_ID , reserved_address_of ( evm_addr ( who) ) , ZERO , * amount)
217188 }
@@ -350,13 +321,9 @@ pub fn synthetic_txs_from_records(
350321) -> Vec < ( Transaction , TransactionStatus , Receipt ) > {
351322 let base_tx_index = real_statuses. len ( ) as u32 ;
352323
353- // Per-log dedup: the stack runner emits an `EVM::Log` event for every log,
354- // AND a real eth tx records the same logs in its receipt — so an `EVM::Log`
355- // already present in the real tx of its extrinsic is a duplicate. Map each
356- // eth-tx extrinsic to a multiset of its receipt logs and drop matches
357- // one-for-one; any EXTRA logs in that extrinsic (a separate internal
358- // `Executor::call`) survive and are synthesized. Real tx statuses are in the
359- // same order as their `Executed` events.
324+ // An EVM log is also recorded in the receipt of the real eth tx in its extrinsic,
325+ // so drop EVM::Log events matching that receipt (one-for-one); extra logs from a
326+ // separate internal Executor::call survive. Real statuses follow Executed order.
360327 let mut real_logs_by_ext: BTreeMap < u32 , BTreeMap < LogKey , u32 > > = BTreeMap :: new ( ) ;
361328 let mut nth_eth_tx = 0usize ;
362329 for rec in records. iter ( ) {
0 commit comments