@@ -56,7 +56,8 @@ async function retry429<T>(fn: () => Promise<T>, attempts = 6): Promise<T> {
5656 } catch ( err ) {
5757 lastErr = err ;
5858 const msg = String ( ( err as Error ) ?. message ?? "" ) ;
59- if ( ! msg . includes ( "Status: 429" ) && ! msg . includes ( "rate limit" ) ) throw err ;
59+ if ( ! msg . includes ( "Status: 429" ) && ! msg . includes ( "rate limit" ) )
60+ throw err ;
6061 // Exponential backoff: 0.5s → 1s → 2s → 4s → 8s → 16s.
6162 await new Promise ( ( r ) => setTimeout ( r , delayMs ) ) ;
6263 delayMs = Math . min ( delayMs * 2 , 16_000 ) ;
@@ -102,6 +103,37 @@ interface GrpcBlockHeader {
102103 hash : string ;
103104}
104105
106+ type RpcQuantity = `0x${string } `;
107+ type RpcHash = `0x${string } `;
108+
109+ interface RpcBlock {
110+ baseFeePerGas ?: RpcQuantity | null ;
111+ gasLimit ?: RpcQuantity | null ;
112+ gasUsed ?: RpcQuantity | null ;
113+ hash ?: RpcHash | null ;
114+ miner ?: `0x${string } ` | null ;
115+ number ?: RpcQuantity | null ;
116+ parentHash : RpcHash ;
117+ stateRoot ?: RpcHash | null ;
118+ timestamp : RpcQuantity ;
119+ transactions : ( RpcHash | { hash : RpcHash } ) [ ] ;
120+ }
121+
122+ function quantityToBigInt ( value : RpcQuantity | null | undefined ) : bigint {
123+ return value == null ? 0n : BigInt ( value ) ;
124+ }
125+
126+ function bigintToQuantity ( value : bigint ) : RpcQuantity {
127+ return `0x${ value . toString ( 16 ) } ` ;
128+ }
129+
130+ export class BlockNotFoundError extends Error {
131+ constructor ( readonly height : bigint ) {
132+ super ( `Block at number "${ height } " could not be found.` ) ;
133+ this . name = "BlockNotFoundError" ;
134+ }
135+ }
136+
105137/**
106138 * Tip watcher backed by the side-car gRPC `GetBlock {latest:true}`. Called
107139 * once per `intervalMs`; emits the new tip height whenever it advances.
@@ -135,17 +167,17 @@ export interface NativeTransaction {
135167 block_index : number ;
136168 block_timestamp : number ;
137169 transaction : {
138- amount : number ; // sentri — 1 SRX = 1e8 sentri
170+ amount : number ; // sentri — 1 SRX = 1e8 sentri
139171 chain_id : number ;
140172 data : string ;
141- fee : number ; // sentri
173+ fee : number ; // sentri
142174 from_address : string ; // 0x… hex OR 'COINBASE' sentinel
143175 nonce : number ;
144176 public_key : string ;
145177 signature : string ;
146178 timestamp : number ;
147179 to_address : string ;
148- txid : string ; // bare hex, NOT 0x-prefixed
180+ txid : string ; // bare hex, NOT 0x-prefixed
149181 } ;
150182}
151183
@@ -241,13 +273,19 @@ export class SentrixClient {
241273 // chain genuinely doesn't have the tx.
242274 let delayMs = 250 ;
243275 for ( let attempt = 0 ; attempt < 4 ; attempt ++ ) {
276+ const controller = new AbortController ( ) ;
277+ const timeout = setTimeout ( ( ) => controller . abort ( ) , 10_000 ) ;
244278 try {
245- const r = await fetch ( `${ this . restBase } /transactions/${ bareHash } ` ) ;
279+ const r = await fetch ( `${ this . restBase } /transactions/${ bareHash } ` , {
280+ signal : controller . signal ,
281+ } ) ;
246282 if ( r . status === 404 ) return null ;
247283 if ( r . ok ) return ( await r . json ( ) ) as NativeTransaction ;
248284 // Non-2xx, non-404 → retry. Falls through to backoff below.
249285 } catch {
250286 // Network error / aborted fetch → retry. Falls through.
287+ } finally {
288+ clearTimeout ( timeout ) ;
251289 }
252290 if ( attempt < 3 ) {
253291 await new Promise ( ( r ) => setTimeout ( r , delayMs ) ) ;
@@ -284,7 +322,9 @@ export class SentrixClient {
284322 // proto-loader returns `index` as string when longs:String. Hash is
285323 // { value: Buffer }.
286324 const idx = BigInt ( resp . index ) ;
287- const hash = Buffer . from ( resp . hash ?. value ?? new Uint8Array ( ) ) . toString ( "hex" ) ;
325+ const hash = Buffer . from (
326+ resp . hash ?. value ?? new Uint8Array ( ) ,
327+ ) . toString ( "hex" ) ;
288328 resolve ( { index : idx , hash } ) ;
289329 } ,
290330 ) ;
@@ -323,7 +363,10 @@ export class SentrixClient {
323363 | { kind : "block" ; height : bigint ; hash : string ; latencyMs : number }
324364 | { kind : "lagged" ; skipped : bigint } ,
325365 ) => void ,
326- opts : { onError ?: ( err : unknown ) => void ; onReconnect ?: ( attempt : number ) => void } = { } ,
366+ opts : {
367+ onError ?: ( err : unknown ) => void ;
368+ onReconnect ?: ( attempt : number ) => void ;
369+ } = { } ,
327370 ) : BlockStreamSub {
328371 let stopped = false ;
329372 let backoffMs = 500 ;
@@ -352,7 +395,9 @@ export class SentrixClient {
352395 onBlock ( {
353396 kind : "block" ,
354397 height : BigInt ( b . index ) ,
355- hash : Buffer . from ( b . hash ?. value ?? new Uint8Array ( ) ) . toString ( "hex" ) ,
398+ hash : Buffer . from ( b . hash ?. value ?? new Uint8Array ( ) ) . toString (
399+ "hex" ,
400+ ) ,
356401 latencyMs,
357402 } ) ;
358403 } else if ( msg . lagged ) {
@@ -468,16 +513,46 @@ export class SentrixClient {
468513 * string entries (`typeof t === "string" → continue`), so the blocks
469514 * table populates fine and the transactions table stays empty until
470515 * the chain RPC is brought into spec OR the indexer reads tx via the
471- * REST `/transactions/<hash>` shape with a native-format adapter. See
472- * Sentriscloud/indexer issue tracker.
516+ * REST `/transactions/<hash>` shape with a native-format adapter. Viem's
517+ * `getBlock` parser can reject historical Sentrix block responses even
518+ * when raw `eth_getBlockByNumber` succeeds, so this method intentionally
519+ * stays on raw JSON-RPC and maps only the fields the indexer uses.
473520 */
474521 async getBlock ( height : bigint ) : Promise < Block < bigint , true > > {
475- return retry429 ( ( ) =>
476- this . http . getBlock ( { blockNumber : height , includeTransactions : true } ) ,
477- ) ;
522+ const block = ( await retry429 ( ( ) =>
523+ this . http . request ( {
524+ method : "eth_getBlockByNumber" ,
525+ params : [ bigintToQuantity ( height ) , true ] ,
526+ } ) ,
527+ ) ) as RpcBlock | null ;
528+
529+ if ( ! block ) {
530+ throw new BlockNotFoundError ( height ) ;
531+ }
532+
533+ return {
534+ baseFeePerGas :
535+ block . baseFeePerGas == null
536+ ? null
537+ : quantityToBigInt ( block . baseFeePerGas ) ,
538+ gasLimit : quantityToBigInt ( block . gasLimit ) ,
539+ gasUsed : quantityToBigInt ( block . gasUsed ) ,
540+ hash : block . hash ?? null ,
541+ miner : block . miner ?? null ,
542+ number : block . number == null ? null : quantityToBigInt ( block . number ) ,
543+ parentHash : block . parentHash ,
544+ stateRoot : block . stateRoot ?? null ,
545+ timestamp : quantityToBigInt ( block . timestamp ) ,
546+ transactions : block . transactions . map ( ( tx ) =>
547+ typeof tx === "string" ? tx : { hash : tx . hash } ,
548+ ) ,
549+ } as Block < bigint , true > ;
478550 }
479551
480- async getLogsRange ( fromBlock : bigint , toBlock : bigint ) : Promise < GetLogsReturnType > {
552+ async getLogsRange (
553+ fromBlock : bigint ,
554+ toBlock : bigint ,
555+ ) : Promise < GetLogsReturnType > {
481556 return retry429 ( ( ) => this . http . getLogs ( { fromBlock, toBlock } ) ) ;
482557 }
483558
0 commit comments