1212 * • closest includes trace-only and tx-only blocks; computed by a LOOP (not Math.max(...spread), which
1313 * RangeErrors on ~100k+ keys — was C9).
1414 */
15+
16+ import type { Address } from 'viem' ;
1517import type {
1618 SyncBlock ,
1719 SyncBlockHeader ,
1820 SyncLog ,
1921 SyncTrace ,
2022 SyncTransaction ,
2123 SyncTransactionReceipt ,
22- } from " @/internal/types.js" ;
24+ } from ' @/internal/types.js' ;
2325import {
2426 isAddressFactory ,
2527 isAddressMatched ,
2628 isTraceFilterMatched ,
2729 isTransactionFilterMatched ,
2830 isTransferFilterMatched ,
29- } from "@/runtime/filter.js" ;
30- import type { Interval } from "@/utils/interval.js" ;
31- import type { Address } from "viem" ;
32- import type { FetchSpec , ChildAddresses } from "./portal-filters.js" ;
33- import { invariant , invariantStrict } from "./portal-invariant.js" ;
31+ } from '@/runtime/filter.js' ;
32+ import type { Interval } from '@/utils/interval.js' ;
33+ import type { ChildAddresses , FetchSpec } from './portal-filters.js' ;
34+ import { invariantStrict } from './portal-invariant.js' ;
3435import {
36+ cmpTraceAddr ,
37+ hx ,
38+ parityToCallFrame ,
3539 type RawHeader ,
3640 type RawLog ,
3741 type RawTrace ,
3842 type RawTx ,
39- cmpTraceAddr ,
40- hx ,
41- parityToCallFrame ,
4243 toSyncBlockHeader ,
4344 toSyncLog ,
4445 toSyncReceipt ,
4546 toSyncTransaction ,
46- } from " ./portal-transform.js" ;
47+ } from ' ./portal-transform.js' ;
4748
4849/** The per-chunk buffered wire data assembled from the Portal streams (built by the shell's dataChunk). */
4950export type ChunkData = {
5051 headers : Map < number , RawHeader > ;
5152 logs : Map < number , RawLog [ ] > ;
5253 txs : Map < number , RawTx [ ] > ;
5354 // for trace/transfer sources: full block + all its traces + its txs, by block number
54- traceBlocks : Map < number , { header : RawHeader ; traces : RawTrace [ ] ; txs : RawTx [ ] } > ;
55+ traceBlocks : Map <
56+ number ,
57+ { header : RawHeader ; traces : RawTrace [ ] ; txs : RawTx [ ] }
58+ > ;
5559 // for block-interval sources: headers of blocks matching a BlockFilter (interval/offset)
5660 blockHeaders : Map < number , RawHeader > ;
5761 // for account transaction sources: blocks + their from/to-matched txs, by block number
5862 txBlocks : Map < number , { header : RawHeader ; txs : RawTx [ ] } > ;
5963} ;
6064
6165export const createChunkData = ( ) : ChunkData => ( {
62- headers : new Map ( ) , logs : new Map ( ) , txs : new Map ( ) , traceBlocks : new Map ( ) , blockHeaders : new Map ( ) , txBlocks : new Map ( ) ,
66+ headers : new Map ( ) ,
67+ logs : new Map ( ) ,
68+ txs : new Map ( ) ,
69+ traceBlocks : new Map ( ) ,
70+ blockHeaders : new Map ( ) ,
71+ txBlocks : new Map ( ) ,
6372} ) ;
6473
65- /** Count the buffered records held by a chunk (for the row budget). */
66- export const chunkRowCount = ( cd : ChunkData ) : number => {
67- let rc = cd . blockHeaders . size ;
68- for ( const a of cd . logs . values ( ) ) rc += a . length ;
69- for ( const a of cd . txs . values ( ) ) rc += a . length ;
70- for ( const b of cd . traceBlocks . values ( ) ) rc += b . traces . length + b . txs . length ;
71- for ( const b of cd . txBlocks . values ( ) ) rc += b . txs . length ;
72- return rc ;
73- } ;
74-
7574/** A geth-style CallFrame produced by parityToCallFrame (index = its DFS rank). */
76- type CallFrame = { index : number ; from : string ; to ?: string ; type : string } & Record < string , unknown > ;
75+ type CallFrame = {
76+ index : number ;
77+ from : string ;
78+ to ?: string ;
79+ type : string ;
80+ } & Record < string , unknown > ;
7781export type RankedTrace = { frame : CallFrame ; index : number } ;
7882
7983/**
@@ -83,16 +87,18 @@ export type RankedTrace = { frame: CallFrame; index: number };
8387 * filter-local one.
8488 */
8589export function rankTraces ( traces : RawTrace [ ] ) : RankedTrace [ ] {
86- const sorted = [ ...traces ] . sort ( ( x , y ) => cmpTraceAddr ( x . traceAddress ?? [ ] , y . traceAddress ?? [ ] ) ) ;
90+ const sorted = [ ...traces ] . sort ( ( x , y ) =>
91+ cmpTraceAddr ( x . traceAddress ?? [ ] , y . traceAddress ?? [ ] ) ,
92+ ) ;
8793 const out : RankedTrace [ ] = [ ] ;
8894 sorted . forEach ( ( t , i ) => {
8995 const frame = parityToCallFrame ( t , i ) as CallFrame | undefined ;
9096 if ( frame ) out . push ( { frame, index : i } ) ;
9197 } ) ;
9298 invariantStrict (
93- " INV-5" ,
99+ ' INV-5' ,
94100 ( ) => out . every ( ( r , i ) => i === 0 || r . index > out [ i - 1 ] ! . index ) ,
95- " trace ranks not strictly increasing" ,
101+ ' trace ranks not strictly increasing' ,
96102 ( ) => ( { ranks : out . map ( ( r ) => r . index ) } ) ,
97103 ) ;
98104 return out ;
@@ -103,19 +109,59 @@ type Matchers = {
103109 txFilterMatched : ( tx : SyncTransaction , bn : number ) => boolean ;
104110} ;
105111
106- const buildMatchers = ( spec : FetchSpec , childAddresses : ChildAddresses ) : Matchers => {
107- const factoryAddrOk = ( filterAddr : unknown , addr : string | undefined , bn : number ) : boolean =>
112+ const buildMatchers = (
113+ spec : FetchSpec ,
114+ childAddresses : ChildAddresses ,
115+ ) : Matchers => {
116+ const factoryAddrOk = (
117+ filterAddr : unknown ,
118+ addr : string | undefined ,
119+ bn : number ,
120+ ) : boolean =>
108121 ! isAddressFactory ( filterAddr as never ) ||
109- isAddressMatched ( { address : addr as Address , blockNumber : bn , childAddresses : childAddresses . get ( ( filterAddr as { id : string } ) . id ) ! } ) ;
122+ isAddressMatched ( {
123+ address : addr as Address ,
124+ blockNumber : bn ,
125+ childAddresses : childAddresses . get ( ( filterAddr as { id : string } ) . id ) ! ,
126+ } ) ;
110127 return {
111128 traceMatched : ( frame , bn ) => {
112129 const blk = { number : BigInt ( bn ) } as never ;
113- for ( const f of spec . transferFilters ) if ( isTransferFilterMatched ( { filter : f , trace : frame as never , block : blk } ) && factoryAddrOk ( f . fromAddress , frame . from , bn ) && factoryAddrOk ( f . toAddress , frame . to , bn ) ) return true ;
114- for ( const f of spec . traceFilters ) if ( isTraceFilterMatched ( { filter : f , trace : frame as never , block : blk } ) && factoryAddrOk ( f . fromAddress , frame . from , bn ) && factoryAddrOk ( f . toAddress , frame . to , bn ) ) return true ;
130+ for ( const f of spec . transferFilters )
131+ if (
132+ isTransferFilterMatched ( {
133+ filter : f ,
134+ trace : frame as never ,
135+ block : blk ,
136+ } ) &&
137+ factoryAddrOk ( f . fromAddress , frame . from , bn ) &&
138+ factoryAddrOk ( f . toAddress , frame . to , bn )
139+ )
140+ return true ;
141+ for ( const f of spec . traceFilters )
142+ if (
143+ isTraceFilterMatched ( {
144+ filter : f ,
145+ trace : frame as never ,
146+ block : blk ,
147+ } ) &&
148+ factoryAddrOk ( f . fromAddress , frame . from , bn ) &&
149+ factoryAddrOk ( f . toAddress , frame . to , bn )
150+ )
151+ return true ;
115152 return false ;
116153 } ,
117154 txFilterMatched : ( tx , bn ) =>
118- spec . transactionFilters . some ( ( f ) => isTransactionFilterMatched ( { filter : f , transaction : tx } ) && factoryAddrOk ( f . fromAddress , tx . from , bn ) && factoryAddrOk ( f . toAddress , ( tx . to ?? undefined ) as string | undefined , bn ) ) ,
155+ spec . transactionFilters . some (
156+ ( f ) =>
157+ isTransactionFilterMatched ( { filter : f , transaction : tx } ) &&
158+ factoryAddrOk ( f . fromAddress , tx . from , bn ) &&
159+ factoryAddrOk (
160+ f . toAddress ,
161+ ( tx . to ?? undefined ) as string | undefined ,
162+ bn ,
163+ ) ,
164+ ) ,
119165 } ;
120166} ;
121167
@@ -126,7 +172,11 @@ const buildTraces = (
126172 hi : number ,
127173 matchers : Matchers ,
128174) : { trace : SyncTrace ; block : SyncBlock ; transaction : SyncTransaction } [ ] => {
129- const out : { trace : SyncTrace ; block : SyncBlock ; transaction : SyncTransaction } [ ] = [ ] ;
175+ const out : {
176+ trace : SyncTrace ;
177+ block : SyncBlock ;
178+ transaction : SyncTransaction ;
179+ } [ ] = [ ] ;
130180 for ( const [ bn , tb ] of cd . traceBlocks ) {
131181 if ( bn < lo || bn > hi || ! tb . traces ?. length ) continue ;
132182 const block = toSyncBlockHeader ( tb . header ) as unknown as SyncBlock ; // encodeTrace only reads block.number
@@ -135,15 +185,25 @@ const buildTraces = (
135185 const byTx = new Map < number , RawTrace [ ] > ( ) ;
136186 // callTracer has no block-reward frames; skip reward/no-tx traces so `?? 0` can't fold them into tx 0
137187 // and shift its DFS ranks (now that we fetch the full, unfiltered trace set).
138- for ( const t of tb . traces ) { if ( t . transactionIndex == null || t . type === "reward" ) continue ; const k = t . transactionIndex ; if ( ! byTx . has ( k ) ) byTx . set ( k , [ ] ) ; byTx . get ( k ) ! . push ( t ) ; }
188+ for ( const t of tb . traces ) {
189+ if ( t . transactionIndex == null || t . type === 'reward' ) continue ;
190+ const k = t . transactionIndex ;
191+ if ( ! byTx . has ( k ) ) byTx . set ( k , [ ] ) ;
192+ byTx . get ( k ) ! . push ( t ) ;
193+ }
139194 for ( const [ txIndex , traces ] of byTx ) {
140195 const rawTx = txByIdx . get ( txIndex ) ;
141196 for ( const { frame } of rankTraces ( traces ) ) {
142197 if ( ! matchers . traceMatched ( frame , bn ) ) continue ;
143198 out . push ( {
144- trace : { trace : frame , transactionHash : rawTx ?. hash } as unknown as SyncTrace ,
199+ trace : {
200+ trace : frame ,
201+ transactionHash : rawTx ?. hash ,
202+ } as unknown as SyncTrace ,
145203 block,
146- transaction : rawTx ? toSyncTransaction ( rawTx , tb . header ) : ( { transactionIndex : hx ( txIndex ) } as unknown as SyncTransaction ) ,
204+ transaction : rawTx
205+ ? toSyncTransaction ( rawTx , tb . header )
206+ : ( { transactionIndex : hx ( txIndex ) } as unknown as SyncTransaction ) ,
147207 } ) ;
148208 }
149209 }
@@ -156,7 +216,11 @@ export type AssembledRange = {
156216 blocks : SyncBlockHeader [ ] ;
157217 txs : SyncTransaction [ ] ;
158218 receipts : SyncTransactionReceipt [ ] ;
159- traces : { trace : SyncTrace ; block : SyncBlock ; transaction : SyncTransaction } [ ] ;
219+ traces : {
220+ trace : SyncTrace ;
221+ block : SyncBlock ;
222+ transaction : SyncTransaction ;
223+ } [ ] ;
160224 closest : SyncBlock | undefined ;
161225} ;
162226
@@ -180,50 +244,79 @@ export function assembleRange(
180244 const syncReceipts : SyncTransactionReceipt [ ] = [ ] ;
181245 const seenTx = new Set < string > ( ) ;
182246
183- for ( const cd of chunks ) for ( const [ bn , hdr ] of cd . headers ) {
184- if ( ! inRange ( bn ) ) continue ;
185- const logs = cd . logs . get ( bn ) ?? [ ] ;
186- if ( logs . length ) {
187- invariant ( "INV-2" , inRange ( bn ) , "assembled log block outside interval" , ( ) => ( { bn, lo, hi } ) ) ;
188- blocksByNumber . set ( bn , toSyncBlockHeader ( hdr ) ) ;
189- for ( const raw of logs ) syncLogs . push ( toSyncLog ( raw , hdr ) ) ;
190- for ( const tx of cd . txs . get ( bn ) ?? [ ] ) if ( tx . hash && ! seenTx . has ( tx . hash ) ) {
191- seenTx . add ( tx . hash ) ;
192- syncTxs . push ( toSyncTransaction ( tx , hdr ) ) ;
193- if ( spec . needReceipts ) syncReceipts . push ( toSyncReceipt ( tx , hdr ) ) ;
247+ // INV-2 is enforced BY CONSTRUCTION here: every emitting branch below sits behind the single
248+ // `inRange` predicate (there is deliberately no redundant per-row assert — it could never fire),
249+ // and the exactness property is proven against a brute-force model in portal-assemble.test.ts.
250+ for ( const cd of chunks )
251+ for ( const [ bn , hdr ] of cd . headers ) {
252+ if ( ! inRange ( bn ) ) continue ;
253+ const logs = cd . logs . get ( bn ) ?? [ ] ;
254+ if ( logs . length ) {
255+ blocksByNumber . set ( bn , toSyncBlockHeader ( hdr ) ) ;
256+ for ( const raw of logs ) syncLogs . push ( toSyncLog ( raw , hdr ) ) ;
257+ for ( const tx of cd . txs . get ( bn ) ?? [ ] )
258+ if ( tx . hash && ! seenTx . has ( tx . hash ) ) {
259+ seenTx . add ( tx . hash ) ;
260+ syncTxs . push ( toSyncTransaction ( tx , hdr ) ) ;
261+ if ( spec . needReceipts ) syncReceipts . push ( toSyncReceipt ( tx , hdr ) ) ;
262+ }
194263 }
195264 }
196- }
197265
198266 // block-interval sources: ensure each matched block is in the blocks table (cd.blockHeaders already
199267 // holds ONLY the BlockFilter-matched headers — the shell filters at fetch time to avoid buffering the
200268 // whole includeAllBlocks scan).
201- if ( spec . needBlocks ) for ( const cd of chunks ) for ( const [ bn , hdr ] of cd . blockHeaders ) {
202- if ( inRange ( bn ) && ! blocksByNumber . has ( bn ) ) blocksByNumber . set ( bn , toSyncBlockHeader ( hdr ) ) ;
203- }
269+ if ( spec . needBlocks )
270+ for ( const cd of chunks )
271+ for ( const [ bn , hdr ] of cd . blockHeaders ) {
272+ if ( inRange ( bn ) && ! blocksByNumber . has ( bn ) )
273+ blocksByNumber . set ( bn , toSyncBlockHeader ( hdr ) ) ;
274+ }
204275
205276 // account transaction sources: re-match Portal's from/to-filtered txs (+ factory + range), insert tx/receipt/block
206- if ( spec . needTxFilter ) for ( const cd of chunks ) for ( const [ bn , tb ] of cd . txBlocks ) {
207- if ( ! inRange ( bn ) ) continue ;
208- for ( const raw of tb . txs ) {
209- if ( raw . hash && seenTx . has ( raw . hash ) ) continue ;
210- const tx = toSyncTransaction ( raw , tb . header ) ;
211- if ( ! matchers . txFilterMatched ( tx , bn ) ) continue ;
212- if ( raw . hash ) seenTx . add ( raw . hash ) ;
213- blocksByNumber . set ( bn , toSyncBlockHeader ( tb . header ) ) ;
214- syncTxs . push ( tx ) ;
215- if ( spec . needReceipts ) syncReceipts . push ( toSyncReceipt ( raw , tb . header ) ) ;
216- }
217- }
277+ if ( spec . needTxFilter )
278+ for ( const cd of chunks )
279+ for ( const [ bn , tb ] of cd . txBlocks ) {
280+ if ( ! inRange ( bn ) ) continue ;
281+ for ( const raw of tb . txs ) {
282+ if ( raw . hash && seenTx . has ( raw . hash ) ) continue ;
283+ const tx = toSyncTransaction ( raw , tb . header ) ;
284+ if ( ! matchers . txFilterMatched ( tx , bn ) ) continue ;
285+ if ( raw . hash ) seenTx . add ( raw . hash ) ;
286+ blocksByNumber . set ( bn , toSyncBlockHeader ( tb . header ) ) ;
287+ syncTxs . push ( tx ) ;
288+ if ( spec . needReceipts )
289+ syncReceipts . push ( toSyncReceipt ( raw , tb . header ) ) ;
290+ }
291+ }
218292
219- const traces = spec . needTraces ? chunks . flatMap ( ( cd ) => buildTraces ( cd , lo , hi , matchers ) ) : [ ] ;
293+ const traces = spec . needTraces
294+ ? chunks . flatMap ( ( cd ) => buildTraces ( cd , lo , hi , matchers ) )
295+ : [ ] ;
220296
221297 // C9: highest block with data — a LOOP (Math.max(...spread) RangeErrors on ~100k+ keys) — INCLUDING
222298 // trace-only blocks so `closest` doesn't understate the synced tip.
223299 let closest : SyncBlock | undefined ;
224300 let maxBn = - 1 ;
225- for ( const [ bn , hdr ] of blocksByNumber ) if ( bn > maxBn ) { maxBn = bn ; closest = hdr as unknown as SyncBlock ; }
226- for ( const t of traces ) { const bn = Number ( ( t . block as { number : unknown } ) . number ) ; if ( bn > maxBn ) { maxBn = bn ; closest = t . block ; } }
301+ for ( const [ bn , hdr ] of blocksByNumber )
302+ if ( bn > maxBn ) {
303+ maxBn = bn ;
304+ closest = hdr as unknown as SyncBlock ;
305+ }
306+ for ( const t of traces ) {
307+ const bn = Number ( ( t . block as { number : unknown } ) . number ) ;
308+ if ( bn > maxBn ) {
309+ maxBn = bn ;
310+ closest = t . block ;
311+ }
312+ }
227313
228- return { logs : syncLogs , blocks : [ ...blocksByNumber . values ( ) ] , txs : syncTxs , receipts : syncReceipts , traces, closest } ;
314+ return {
315+ logs : syncLogs ,
316+ blocks : [ ...blocksByNumber . values ( ) ] ,
317+ txs : syncTxs ,
318+ receipts : syncReceipts ,
319+ traces,
320+ closest,
321+ } ;
229322}
0 commit comments