11import { ethers , providers } from 'ethers' ;
22import { Logger } from 'pino' ;
3- import { z } from 'zod' ;
43
54import {
65 ChainMap ,
@@ -15,10 +14,8 @@ import {
1514} from '@hyperlane-xyz/sdk' ;
1615import {
1716 Address ,
18- ParsedMessage ,
1917 WithAddress ,
2018 assert ,
21- bytes32ToAddress ,
2219 messageId ,
2320 objMap ,
2421 objMerge ,
@@ -29,95 +26,21 @@ import {
2926
3027import { BaseMetadataBuilder } from '../metadata/builder.js' ;
3128
29+ import { RelayerCache } from './cache.js' ;
30+ import { RelayerObserver } from './events.js' ;
31+ import { messageMatchesWhitelist } from './whitelist.js' ;
32+
3233type DerivedHookConfig = WithAddress < Exclude < HookConfig , Address > > ;
3334type DerivedIsmConfig = WithAddress < Exclude < IsmConfig , Address > > ;
3435
35- /**
36- * Callbacks for relayer events, useful for metrics and monitoring
37- */
38- export interface RelayerEventCallbacks {
39- onMessageRelayed ?: (
40- originChain : string ,
41- destinationChain : string ,
42- messageId : string ,
43- durationMs : number ,
44- ) => void ;
45- onMessageFailed ?: (
46- originChain : string ,
47- destinationChain : string ,
48- messageId : string ,
49- error : Error ,
50- ) => void ;
51- onMessageSkipped ?: (
52- originChain : string ,
53- destinationChain : string ,
54- messageId : string ,
55- reason : 'whitelist' | 'already_delivered' ,
56- ) => void ;
57- onRetry ?: (
58- originChain : string ,
59- destinationChain : string ,
60- messageId : string ,
61- attempt : number ,
62- ) => void ;
63- onBacklogUpdate ?: ( size : number ) => void ;
64- }
65-
66- const BacklogMessageSchema = z . object ( {
67- attempts : z . number ( ) ,
68- lastAttempt : z . number ( ) ,
69- message : z . string ( ) ,
70- dispatchTx : z . string ( ) ,
71- } ) ;
72-
73- const MessageBacklogSchema = z . array ( BacklogMessageSchema ) ;
74-
75- export const RelayerCacheSchema = z . object ( {
76- hook : z . record ( z . record ( z . any ( ) ) ) ,
77- ism : z . record ( z . record ( z . any ( ) ) ) ,
78- backlog : MessageBacklogSchema ,
79- } ) ;
80-
81- export type RelayerCache = z . infer < typeof RelayerCacheSchema > ;
82-
83- type MessageWhitelist = ChainMap < Set < Address > > ;
84-
85- export function messageMatchesWhitelist (
86- whitelist : MessageWhitelist ,
87- message : ParsedMessage ,
88- ) : boolean {
89- const originAddresses = whitelist [ message . originChain ?? message . origin ] ;
90- if ( ! originAddresses ) {
91- return false ;
92- }
93-
94- const sender = bytes32ToAddress ( message . sender ) ;
95- if ( originAddresses . size !== 0 && ! originAddresses . has ( sender ) ) {
96- return false ;
97- }
98-
99- const destinationAddresses =
100- whitelist [ message . destinationChain ?? message . destination ] ;
101- if ( ! destinationAddresses ) {
102- return false ;
103- }
104-
105- const recipient = bytes32ToAddress ( message . recipient ) ;
106- if ( destinationAddresses . size !== 0 && ! destinationAddresses . has ( recipient ) ) {
107- return false ;
108- }
109-
110- return true ;
111- }
112-
11336export class HyperlaneRelayer {
11437 protected multiProvider : MultiProvider ;
11538 protected metadataBuilder : BaseMetadataBuilder ;
11639 protected readonly core : HyperlaneCore ;
11740 protected readonly retryTimeout : number ;
11841
11942 protected readonly whitelist : ChainMap < Set < Address > > | undefined ;
120- protected readonly eventCallbacks : RelayerEventCallbacks ;
43+ protected readonly observer : RelayerObserver ;
12144
12245 public backlog : RelayerCache [ 'backlog' ] ;
12346 public cache : RelayerCache | undefined ;
@@ -131,20 +54,20 @@ export class HyperlaneRelayer {
13154 caching = true ,
13255 retryTimeout = 1000 ,
13356 whitelist = undefined ,
134- eventCallbacks = { } ,
57+ observer = { } ,
13558 } : {
13659 core : HyperlaneCore ;
13760 caching ?: boolean ;
13861 retryTimeout ?: number ;
13962 whitelist ?: ChainMap < Address [ ] > ;
140- eventCallbacks ?: RelayerEventCallbacks ;
63+ observer ?: RelayerObserver ;
14164 } ) {
14265 this . core = core ;
14366 this . retryTimeout = retryTimeout ;
14467 this . logger = core . logger . child ( { module : 'Relayer' } ) ;
14568 this . metadataBuilder = new BaseMetadataBuilder ( core ) ;
14669 this . multiProvider = core . multiProvider ;
147- this . eventCallbacks = eventCallbacks ;
70+ this . observer = observer ;
14871 if ( whitelist ) {
14972 this . whitelist = objMap (
15073 whitelist ,
@@ -247,9 +170,11 @@ export class HyperlaneRelayer {
247170 destinationMap [ message . parsed . destination ] . push ( message ) ;
248171 } ) ;
249172
173+ // parallelize relaying to different destinations
250174 return promiseObjAll (
251175 objMap ( destinationMap , async ( _destination , messages ) => {
252176 const receipts : ethers . ContractReceipt [ ] = [ ] ;
177+ // serially relay messages to the same destination
253178 for ( const message of messages ) {
254179 try {
255180 const receipt = await this . relayMessage (
@@ -276,18 +201,22 @@ export class HyperlaneRelayer {
276201 const destinationChain = this . core . getDestination ( message ) ;
277202
278203 if ( this . whitelist ) {
204+ // add human readable names for use in whitelist checks
279205 message . parsed = {
280206 originChain,
281207 destinationChain,
282208 ...message . parsed ,
283209 } ;
284210 if ( ! messageMatchesWhitelist ( this . whitelist , message . parsed ) ) {
285- this . eventCallbacks . onMessageSkipped ?.(
211+ this . observer . onEvent ?.( {
212+ type : 'messageSkipped' ,
213+ message,
286214 originChain,
287215 destinationChain,
288- message . id ,
289- 'whitelist' ,
290- ) ;
216+ messageId : message . id ,
217+ reason : 'whitelist' ,
218+ dispatchTx,
219+ } ) ;
291220 throw new Error ( `Message ${ message . id } does not match whitelist` ) ;
292221 }
293222 }
@@ -297,12 +226,15 @@ export class HyperlaneRelayer {
297226 const isDelivered = await this . core . isDelivered ( message ) ;
298227 if ( isDelivered ) {
299228 this . logger . info ( `Message ${ message . id } already delivered` ) ;
300- this . eventCallbacks . onMessageSkipped ?.(
229+ this . observer . onEvent ?.( {
230+ type : 'messageSkipped' ,
231+ message,
301232 originChain,
302233 destinationChain,
303- message . id ,
304- 'already_delivered' ,
305- ) ;
234+ messageId : message . id ,
235+ reason : 'already_delivered' ,
236+ dispatchTx,
237+ } ) ;
306238 return this . core . getProcessedReceipt ( message ) ;
307239 }
308240
@@ -311,6 +243,7 @@ export class HyperlaneRelayer {
311243 this . logger . debug ( { message } , `Simulating recipient message handling` ) ;
312244 await this . core . estimateHandle ( message ) ;
313245
246+ // parallelizable because configs are on different chains
314247 const [ ism , hook ] = await Promise . all ( [
315248 this . getRecipientIsmConfig ( message ) ,
316249 this . getSenderHookConfig ( message ) ,
@@ -328,20 +261,26 @@ export class HyperlaneRelayer {
328261
329262 const receipt = await this . core . deliver ( message , metadata ) ;
330263 const durationMs = Date . now ( ) - startTime ;
331- this . eventCallbacks . onMessageRelayed ?.(
264+ this . observer . onEvent ?.( {
265+ type : 'messageRelayed' ,
266+ message,
332267 originChain,
333268 destinationChain,
334- message . id ,
269+ messageId : message . id ,
335270 durationMs,
336- ) ;
271+ dispatchTx,
272+ } ) ;
337273 return receipt ;
338274 } catch ( error ) {
339- this . eventCallbacks . onMessageFailed ?.(
275+ this . observer . onEvent ?.( {
276+ type : 'messageFailed' ,
277+ message,
340278 originChain,
341279 destinationChain,
342- message . id ,
343- error as Error ,
344- ) ;
280+ messageId : message . id ,
281+ error : error as Error ,
282+ dispatchTx,
283+ } ) ;
345284 throw error ;
346285 }
347286 }
@@ -351,6 +290,7 @@ export class HyperlaneRelayer {
351290 this . cache = objMerge ( this . cache , cache ) ;
352291 }
353292
293+ // fill cache with default ISM and hook configs for quicker relaying (optional)
354294 async hydrateDefaults ( ) : Promise < void > {
355295 assert ( this . cache , 'Caching not enabled' ) ;
356296
@@ -368,7 +308,10 @@ export class HyperlaneRelayer {
368308
369309 protected async flushBacklog ( ) : Promise < void > {
370310 while ( this . stopRelayingHandler ) {
371- this . eventCallbacks . onBacklogUpdate ?.( this . backlog . length ) ;
311+ this . observer . onEvent ?.( {
312+ type : 'backlog' ,
313+ size : this . backlog . length ,
314+ } ) ;
372315
373316 const backlogMsg = this . backlog . shift ( ) ;
374317
@@ -378,6 +321,7 @@ export class HyperlaneRelayer {
378321 continue ;
379322 }
380323
324+ // linear backoff (attempts * retryTimeout)
381325 const backoffTime =
382326 backlogMsg . lastAttempt + backlogMsg . attempts * this . retryTimeout ;
383327 if ( Date . now ( ) < backoffTime ) {
@@ -397,6 +341,7 @@ export class HyperlaneRelayer {
397341 String ( parsed . destination ) ;
398342
399343 try {
344+ // TODO: handle batching
400345 const dispatchReceipt = await this . multiProvider
401346 . getProvider ( parsed . origin )
402347 . getTransactionReceipt ( dispatchTx ) ;
@@ -407,12 +352,14 @@ export class HyperlaneRelayer {
407352 this . logger . error (
408353 `Failed to relay message ${ id } (attempt #${ newAttempts } )` ,
409354 ) ;
410- this . eventCallbacks . onRetry ?.(
355+ this . observer . onEvent ?.( {
356+ type : 'retry' ,
357+ message : dispatchMsg ,
411358 originChain,
412359 destinationChain,
413- id ,
414- newAttempts ,
415- ) ;
360+ messageId : id ,
361+ attempt : newAttempts ,
362+ } ) ;
416363 this . backlog . push ( {
417364 ...backlogMsg ,
418365 attempts : newAttempts ,
0 commit comments