@@ -14,7 +14,7 @@ import {
1414 merge ,
1515 filter ,
1616 startWith ,
17- delay ,
17+ timer ,
1818} from "rxjs" ;
1919import { StorageAdapterBlock , StoreEventsLog , SyncFilter } from "./common" ;
2020import { watchLogs } from "./watchLogs" ;
@@ -24,9 +24,11 @@ import { isLogsApiResponse } from "./indexer-client/isLogsApiResponse";
2424import { toStorageAdapterBlock } from "./indexer-client/toStorageAdapterBlock" ;
2525import { fetchAndStoreLogs } from "./fetchAndStoreLogs" ;
2626import { storeEventsAbi } from "@latticexyz/store" ;
27- import { bigIntMax , isDefined } from "@latticexyz/common/utils" ;
27+ import { bigIntMax , groupBy , isDefined } from "@latticexyz/common/utils" ;
2828import { getRpcClient , GetRpcClientOptions } from "@latticexyz/block-logs-stream" ;
29- import { debug } from "./debug" ;
29+ import { debug as parentDebug } from "./debug" ;
30+
31+ const debug = parentDebug . extend ( "createPreconfirmedBlockStream" ) ;
3032
3133type PreconfirmedBlockStreamOptions = GetRpcClientOptions & {
3234 fromBlock : bigint ;
@@ -43,19 +45,23 @@ export function createPreconfirmedBlockStream(opts: PreconfirmedBlockStreamOptio
4345 const recreatePreconfirmedStream$ = new Subject < void > ( ) ;
4446 const recreateLatestStream$ = new Subject < void > ( ) ;
4547
46- let restartBlockNumber = opts . fromBlock ;
48+ let processedLatestBlockNumber = opts . fromBlock - 1n ;
4749 let initialCatchUpBlockNumber : bigint | undefined = undefined ;
48- getRpcClient ( opts )
49- . request ( { method : "eth_blockNumber" } )
50- . then ( ( blockNumber ) => {
51- console . log ( "initial catch up block number" , BigInt ( blockNumber ) ) ;
52- initialCatchUpBlockNumber = BigInt ( blockNumber ) ;
53- } ) ;
5450
5551 const latestBlock$ = recreateLatestStream$ . pipe (
5652 startWith ( undefined ) ,
53+ tap ( ( ) => {
54+ debug ( "initializing latest block stream" ) ;
55+ initialCatchUpBlockNumber = undefined ;
56+ getRpcClient ( opts )
57+ . request ( { method : "eth_blockNumber" } )
58+ . then ( ( blockNumber ) => {
59+ debug ( "initial catch up block number" , BigInt ( blockNumber ) ) ;
60+ initialCatchUpBlockNumber = BigInt ( blockNumber ) ;
61+ } ) ;
62+ } ) ,
5763 switchMap ( ( ) =>
58- createLatestBlockStream ( { ...opts , fromBlock : restartBlockNumber } ) . pipe (
64+ createLatestBlockStream ( { ...opts , fromBlock : processedLatestBlockNumber + 1n } ) . pipe (
5965 catchError ( ( e ) => {
6066 debug ( "Error in latest block stream, recreating" , e ) ;
6167 recreateLatestStream$ . next ( ) ;
@@ -65,22 +71,25 @@ export function createPreconfirmedBlockStream(opts: PreconfirmedBlockStreamOptio
6571 ) ,
6672 ) ;
6773
68- let processedBlockLogs : { [ blockNumber : string ] : { [ logIndex : number ] : boolean } } = { } ;
74+ let preconfirmedTransactionLogs : { [ txHash : string ] : Partial < StoreEventsLog > [ ] } = { } ;
6975 let preconfirmedLogsState : "initializing" | "initialized" | "waiting" = "waiting" ;
7076 let attempt = 0 ;
71- const preconfirmedLogs $ = recreatePreconfirmedStream$ . pipe (
77+ const preconfirmedBlockLogs $ = recreatePreconfirmedStream$ . pipe (
7278 tap ( ( ) => {
73- debug ( `initializing preconfirmed logs stream in ${ attempt * 500 } ms`) ;
79+ if ( attempt !== 0 ) debug ( `waiting ${ attempt * 500 } ms before initializing preconfirmed logs stream `) ;
7480 preconfirmedLogsState = "initializing" ;
75- processedBlockLogs = { } ;
81+ preconfirmedTransactionLogs = { } ;
82+ } ) ,
83+ switchMap ( ( ) => timer ( attempt * 500 ) ) ,
84+ tap ( ( ) => {
85+ debug ( `initializing preconfirmed logs stream` ) ;
86+ attempt ++ ;
7687 } ) ,
77- delay ( attempt * 500 ) ,
78- tap ( ( ) => attempt ++ ) ,
7988 switchMap ( ( ) =>
8089 watchLogs ( {
8190 ...opts ,
8291 url : opts . preconfirmedLogsUrl ,
83- fromBlock : restartBlockNumber ,
92+ fromBlock : processedLatestBlockNumber + 1n ,
8493 } ) . logs$ . pipe (
8594 catchError ( ( e ) => {
8695 debug ( "Error in preconfirmed logs stream, recreating" , e ) ;
@@ -90,36 +99,82 @@ export function createPreconfirmedBlockStream(opts: PreconfirmedBlockStreamOptio
9099 ) ,
91100 ) ,
92101 filter ( ( block ) : block is StorageAdapterBlock => block != null ) ,
102+ filter ( ( block ) => {
103+ if ( initialCatchUpBlockNumber == null || block . blockNumber <= initialCatchUpBlockNumber ) {
104+ debug (
105+ "skipping preconfirmed block" ,
106+ block . blockNumber ,
107+ "before initial catch up block" ,
108+ initialCatchUpBlockNumber ,
109+ ) ;
110+ return false ;
111+ }
112+ const isProcessedBlock = block . blockNumber <= processedLatestBlockNumber ;
113+ if ( isProcessedBlock ) debug ( "skipping already processed block in preconfirmed stream" , block . blockNumber ) ;
114+ return ! isProcessedBlock ;
115+ } ) ,
93116 tap ( ( block ) => {
94117 debug ( "preconfirmed block" , block . blockNumber , "with" , block . logs . length , "logs" ) ;
95118 preconfirmedLogsState = "initialized" ;
96119 attempt = 0 ;
97- restartBlockNumber = block . blockNumber ;
98- const seenLogs = ( processedBlockLogs [ String ( block . blockNumber ) ] ??= { } ) ;
99120 block . logs . forEach ( ( log ) => {
100- seenLogs [ log . logIndex ! ] = true ;
121+ const txHash = log . transactionHash ;
122+ if ( txHash == null ) {
123+ debug ( "unexpected null transaction hash" , log ) ;
124+ return ;
125+ }
126+ preconfirmedTransactionLogs [ txHash ] ??= [ ] ;
127+ preconfirmedTransactionLogs [ txHash ] . push ( log ) ;
101128 } ) ;
102- debug ( "got preconfirmed block" , block . blockNumber , "with" , block . logs . length , "logs" ) ;
103129 } ) ,
104130 ) ;
105131
106- const missingLogs $ = latestBlock$ . pipe (
132+ const latestBlockLogs $ = latestBlock$ . pipe (
107133 map ( ( block ) => {
108- const missingBlock = processedBlockLogs [ String ( block . blockNumber ) ] == null ;
109- const seenLogs = processedBlockLogs [ String ( block . blockNumber ) ] ?? { } ;
110- const missingLogs = block . logs . filter ( ( log ) => ! seenLogs [ log . logIndex ! ] ) ;
111- delete processedBlockLogs [ String ( block . blockNumber ) ] ;
112- restartBlockNumber = block . blockNumber + 1n ;
134+ processedLatestBlockNumber = block . blockNumber ;
135+
136+ const mismatchingTransactions : string [ ] = [ ] ;
137+ if ( preconfirmedLogsState === "initialized" ) {
138+ const logsByTransaction = groupBy (
139+ block . logs . filter ( ( log ) => log . transactionHash ) as StoreEventsLog [ ] ,
140+ ( log ) => log . transactionHash ,
141+ ) ;
142+ for ( const [ txHash , latestLogs ] of logsByTransaction . entries ( ) ) {
143+ const preconfirmedLogs = preconfirmedTransactionLogs [ txHash ] ;
144+ delete preconfirmedTransactionLogs [ txHash ] ;
145+
146+ if ( ! preconfirmedLogs || preconfirmedLogs . length !== latestLogs . length ) {
147+ debug (
148+ "found mismatching transaction" ,
149+ JSON . stringify (
150+ {
151+ txHash,
152+ numPreconfirmedLogs : preconfirmedLogs ?. length ,
153+ numLatestLogs : latestLogs . length ,
154+ missingLogs : latestLogs . filter (
155+ ( log ) => ! preconfirmedLogs . find ( ( preconfirmedLog ) => log . logIndex === preconfirmedLog . logIndex ) ,
156+ ) ,
157+ } ,
158+ ( _ , value ) => ( typeof value === "bigint" ? value . toString ( ) : value ) ,
159+ 2 ,
160+ ) ,
161+ ) ;
162+ mismatchingTransactions . push ( txHash ) ;
163+ }
164+ }
165+ }
113166
114167 debug (
115168 "got latest block" ,
116169 block . blockNumber ,
117170 "with" ,
118171 block . logs . length ,
119- "logs (" ,
120- missingBlock ? "missing block," : "block seen," ,
121- `${ missingLogs . length } new logs` ,
122- ")" ,
172+ "logs" ,
173+ preconfirmedLogsState === "initialized"
174+ ? `(${
175+ mismatchingTransactions . length ? mismatchingTransactions . length + " txs mismatching" : "all preconfirmed"
176+ } )`
177+ : "" ,
123178 ) ;
124179
125180 if ( preconfirmedLogsState === "waiting" ) {
@@ -141,24 +196,25 @@ export function createPreconfirmedBlockStream(opts: PreconfirmedBlockStreamOptio
141196 return block ;
142197 }
143198
144- // If the preconfirmed logs stream is initialized but there are missing logs, recreate it and pass the block through.
145- // Pass all logs from this block, not just the missing ones, to make sure they appear in the right order.
146- if ( preconfirmedLogsState === "initialized" && ( missingLogs . length > 0 || missingBlock ) ) {
147- debug ( "missing logs found in latest block" , block . blockNumber , "recreating preconfirmed stream" , {
148- missingLogs : missingLogs . length ,
149- missingBlock,
150- } ) ;
199+ // If the preconfirmed logs stream is initialized but there are mismatching logs, recreate it and pass the block through.
200+ // Pass all logs from this block, not just the mismatching ones, to make sure they appear in the right order.
201+ if ( preconfirmedLogsState === "initialized" && mismatchingTransactions . length > 0 ) {
202+ debug ( "mismatching transactions found in latest block" , block . blockNumber , "recreating preconfirmed stream" ) ;
151203 recreatePreconfirmedStream$ . next ( ) ;
152204 return block ;
153205 }
154206
155- debug ( "no missing logs found in latest block" , block . blockNumber , "not recreating preconfirmed stream" ) ;
207+ debug (
208+ "no mismatching transactions found in latest block" ,
209+ block . blockNumber ,
210+ "not recreating preconfirmed stream" ,
211+ ) ;
156212 return ;
157213 } ) ,
158214 filter ( isDefined ) ,
159215 ) ;
160216
161- return merge ( preconfirmedLogs $, missingLogs $) ;
217+ return merge ( preconfirmedBlockLogs $, latestBlockLogs $) ;
162218}
163219
164220// TODO: refactor to reduce duplication with indexer/rpc stream in `createStoreSync.ts`
0 commit comments