@hyperlane-xyz/rebalancer 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,6 @@ import Sinon from 'sinon';
5
5
 
6
6
  import { EthJsonRpcBlockParameterTag } from '@hyperlane-xyz/sdk';
7
7
 
8
- import type { ConfirmedBlockTags } from '../interfaces/IMonitor.js';
9
8
  import type { ExplorerMessage } from '../utils/ExplorerClient.js';
10
9
 
11
10
  import { ActionTracker, type ActionTrackerConfig } from './ActionTracker.js';
@@ -46,19 +45,19 @@ describe('ActionTracker', () => {
46
45
  getInflightRebalanceActions: explorerGetInflightRebalanceActions,
47
46
  } as any;
48
47
 
49
- // Create stub for mailbox
48
+ // Create stub for adapter (used by MultiProtocolCore)
50
49
  mailboxStub = {
51
- delivered: Sinon.stub().resolves(false),
50
+ isDelivered: Sinon.stub().resolves(false),
52
51
  };
53
52
 
54
- // Create stub for HyperlaneCore
55
- const coreGetContracts = Sinon.stub().returns({ mailbox: mailboxStub });
53
+ // Create stub for MultiProtocolCore
56
54
  const multiProviderGetChainName = Sinon.stub().callsFake(
57
55
  (domain: number) => `chain${domain}`,
58
56
  );
57
+ const adapterStub = Sinon.stub().returns(mailboxStub);
59
58
 
60
59
  core = {
61
- getContracts: coreGetContracts,
60
+ adapter: adapterStub,
62
61
  multiProvider: {
63
62
  getChainName: multiProviderGetChainName,
64
63
  },
@@ -107,7 +106,7 @@ describe('ActionTracker', () => {
107
106
  explorerClient.getInflightUserTransfers.resolves([]);
108
107
 
109
108
  // Ensure mailbox returns false so action stays in_progress
110
- mailboxStub.delivered.resolves(false);
109
+ mailboxStub.isDelivered.resolves(false);
111
110
 
112
111
  await tracker.initialize();
113
112
 
@@ -259,7 +258,7 @@ describe('ActionTracker', () => {
259
258
  });
260
259
 
261
260
  explorerClient.getInflightUserTransfers.resolves([]);
262
- mailboxStub.delivered.resolves(true);
261
+ mailboxStub.isDelivered.resolves(true);
263
262
 
264
263
  await tracker.syncTransfers();
265
264
 
@@ -338,7 +337,7 @@ describe('ActionTracker', () => {
338
337
  await rebalanceIntentStore.save(intent);
339
338
  await rebalanceActionStore.save(action);
340
339
 
341
- mailboxStub.delivered.resolves(true);
340
+ mailboxStub.isDelivered.resolves(true);
342
341
 
343
342
  await tracker.syncRebalanceActions();
344
343
 
@@ -367,7 +366,7 @@ describe('ActionTracker', () => {
367
366
 
368
367
  await rebalanceActionStore.save(action);
369
368
 
370
- mailboxStub.delivered.resolves(false);
369
+ mailboxStub.isDelivered.resolves(false);
371
370
 
372
371
  await tracker.syncRebalanceActions();
373
372
 
@@ -658,8 +657,8 @@ describe('ActionTracker', () => {
658
657
  });
659
658
  });
660
659
 
661
- describe('confirmedBlockTags synchronization', () => {
662
- it('should use provided blockTag in syncTransfers delivery check', async () => {
660
+ describe('delivery check synchronization', () => {
661
+ it('should check delivery status in syncTransfers using adapter', async () => {
663
662
  await transferStore.save({
664
663
  id: '0xmsg1',
665
664
  status: 'in_progress',
@@ -674,21 +673,19 @@ describe('ActionTracker', () => {
674
673
  });
675
674
 
676
675
  explorerClient.getInflightUserTransfers.resolves([]);
677
- mailboxStub.delivered.resolves(true);
676
+ mailboxStub.isDelivered.resolves(true);
678
677
 
679
- const confirmedBlockTags = { chain2: 12345 };
680
- await tracker.syncTransfers(confirmedBlockTags);
678
+ await tracker.syncTransfers();
681
679
 
682
- expect(mailboxStub.delivered.calledOnce).to.be.true;
683
- const call = mailboxStub.delivered.firstCall;
680
+ expect(mailboxStub.isDelivered.calledOnce).to.be.true;
681
+ const call = mailboxStub.isDelivered.firstCall;
684
682
  expect(call.args[0]).to.equal('0xmsg1');
685
- expect(call.args[1]).to.deep.equal({ blockTag: 12345 });
686
683
 
687
684
  const transfer = await transferStore.get('0xmsg1');
688
685
  expect(transfer?.status).to.equal('complete');
689
686
  });
690
687
 
691
- it('should use provided blockTag in syncRebalanceActions delivery check', async () => {
688
+ it('should check delivery status in syncRebalanceActions using adapter', async () => {
692
689
  const intent: RebalanceIntent = {
693
690
  id: 'intent-1',
694
691
  status: 'in_progress',
@@ -716,21 +713,65 @@ describe('ActionTracker', () => {
716
713
  await rebalanceActionStore.save(action);
717
714
 
718
715
  explorerClient.getInflightRebalanceActions.resolves([]);
719
- mailboxStub.delivered.resolves(true);
716
+ mailboxStub.isDelivered.resolves(true);
720
717
 
721
- const confirmedBlockTags = { chain2: 99999 };
722
- await tracker.syncRebalanceActions(confirmedBlockTags);
718
+ await tracker.syncRebalanceActions();
723
719
 
724
- expect(mailboxStub.delivered.calledOnce).to.be.true;
725
- const call = mailboxStub.delivered.firstCall;
720
+ expect(mailboxStub.isDelivered.calledOnce).to.be.true;
721
+ const call = mailboxStub.isDelivered.firstCall;
726
722
  expect(call.args[0]).to.equal('0xmsg1');
727
- expect(call.args[1]).to.deep.equal({ blockTag: 99999 });
728
723
 
729
724
  const updatedAction = await rebalanceActionStore.get('action-1');
730
725
  expect(updatedAction?.status).to.equal('complete');
731
726
  });
732
727
 
733
- it('should handle string blockTags (like "safe" or "finalized")', async () => {
728
+ it('should keep transfer in_progress when not delivered', async () => {
729
+ await transferStore.save({
730
+ id: '0xmsg1',
731
+ status: 'in_progress',
732
+ messageId: '0xmsg1',
733
+ origin: 1,
734
+ destination: 2,
735
+ amount: 100n,
736
+ sender: '0xuser1',
737
+ recipient: '0xuser2',
738
+ createdAt: Date.now(),
739
+ updatedAt: Date.now(),
740
+ });
741
+
742
+ explorerClient.getInflightUserTransfers.resolves([]);
743
+ mailboxStub.isDelivered.resolves(false);
744
+
745
+ await tracker.syncTransfers();
746
+
747
+ expect(mailboxStub.isDelivered.calledOnce).to.be.true;
748
+ const transfer = await transferStore.get('0xmsg1');
749
+ expect(transfer?.status).to.equal('in_progress');
750
+ });
751
+
752
+ it('should check delivery for multiple destinations', async () => {
753
+ await transferStore.save({
754
+ id: '0xmsg1',
755
+ status: 'in_progress',
756
+ messageId: '0xmsg1',
757
+ origin: 1,
758
+ destination: 3,
759
+ amount: 100n,
760
+ sender: '0xuser1',
761
+ recipient: '0xuser2',
762
+ createdAt: Date.now(),
763
+ updatedAt: Date.now(),
764
+ });
765
+
766
+ explorerClient.getInflightUserTransfers.resolves([]);
767
+ mailboxStub.isDelivered.resolves(false);
768
+
769
+ await tracker.syncTransfers();
770
+
771
+ expect(mailboxStub.isDelivered.calledOnce).to.be.true;
772
+ });
773
+
774
+ it('should pass blockTag when checking delivery in syncTransfers', async () => {
734
775
  await transferStore.save({
735
776
  id: '0xmsg1',
736
777
  status: 'in_progress',
@@ -745,25 +786,65 @@ describe('ActionTracker', () => {
745
786
  });
746
787
 
747
788
  explorerClient.getInflightUserTransfers.resolves([]);
748
- mailboxStub.delivered.resolves(false);
789
+ mailboxStub.isDelivered.resolves(true);
749
790
 
750
- const confirmedBlockTags: ConfirmedBlockTags = {
791
+ const confirmedBlockTags = {
751
792
  chain2: EthJsonRpcBlockParameterTag.Finalized,
752
793
  };
753
794
  await tracker.syncTransfers(confirmedBlockTags);
754
795
 
755
- expect(mailboxStub.delivered.calledOnce).to.be.true;
756
- const call = mailboxStub.delivered.firstCall;
757
- expect(call.args[1]).to.deep.equal({ blockTag: 'finalized' });
796
+ expect(mailboxStub.isDelivered.calledOnce).to.be.true;
797
+ const call = mailboxStub.isDelivered.firstCall;
798
+ expect(call.args[0]).to.equal('0xmsg1');
799
+ expect(call.args[1]).to.equal(EthJsonRpcBlockParameterTag.Finalized);
800
+ });
801
+
802
+ it('should pass blockTag when checking delivery in syncRebalanceActions', async () => {
803
+ const intent: RebalanceIntent = {
804
+ id: 'intent-1',
805
+ status: 'in_progress',
806
+ origin: 1,
807
+ destination: 2,
808
+ amount: 100n,
809
+ fulfilledAmount: 0n,
810
+ createdAt: Date.now(),
811
+ updatedAt: Date.now(),
812
+ };
813
+
814
+ const action: RebalanceAction = {
815
+ id: 'action-1',
816
+ status: 'in_progress',
817
+ intentId: 'intent-1',
818
+ messageId: '0xmsg1',
819
+ origin: 1,
820
+ destination: 2,
821
+ amount: 100n,
822
+ createdAt: Date.now(),
823
+ updatedAt: Date.now(),
824
+ };
825
+
826
+ await rebalanceIntentStore.save(intent);
827
+ await rebalanceActionStore.save(action);
828
+
829
+ explorerClient.getInflightRebalanceActions.resolves([]);
830
+ mailboxStub.isDelivered.resolves(true);
831
+
832
+ const confirmedBlockTags = { chain2: 12345 };
833
+ await tracker.syncRebalanceActions(confirmedBlockTags);
834
+
835
+ expect(mailboxStub.isDelivered.calledOnce).to.be.true;
836
+ const call = mailboxStub.isDelivered.firstCall;
837
+ expect(call.args[0]).to.equal('0xmsg1');
838
+ expect(call.args[1]).to.equal(12345);
758
839
  });
759
840
 
760
- it('should handle undefined blockTag for chain not in confirmedBlockTags', async () => {
841
+ it('should pass undefined blockTag when confirmedBlockTags not provided', async () => {
761
842
  await transferStore.save({
762
843
  id: '0xmsg1',
763
844
  status: 'in_progress',
764
845
  messageId: '0xmsg1',
765
846
  origin: 1,
766
- destination: 3,
847
+ destination: 2,
767
848
  amount: 100n,
768
849
  sender: '0xuser1',
769
850
  recipient: '0xuser2',
@@ -772,12 +853,14 @@ describe('ActionTracker', () => {
772
853
  });
773
854
 
774
855
  explorerClient.getInflightUserTransfers.resolves([]);
775
- mailboxStub.delivered.resolves(false);
856
+ mailboxStub.isDelivered.resolves(false);
776
857
 
777
- const confirmedBlockTags = { chain2: 12345 };
778
- await tracker.syncTransfers(confirmedBlockTags);
858
+ await tracker.syncTransfers(); // No confirmedBlockTags
779
859
 
780
- expect(mailboxStub.delivered.calledOnce).to.be.true;
860
+ expect(mailboxStub.isDelivered.calledOnce).to.be.true;
861
+ const call = mailboxStub.isDelivered.firstCall;
862
+ expect(call.args[0]).to.equal('0xmsg1');
863
+ expect(call.args[1]).to.be.undefined;
781
864
  });
782
865
  });
783
866
  });
@@ -1,18 +1,16 @@
1
1
  import type { Logger } from 'pino';
2
2
  import { v4 as uuidv4 } from 'uuid';
3
3
 
4
- import type { HyperlaneCore } from '@hyperlane-xyz/sdk';
4
+ import type { MultiProtocolCore } from '@hyperlane-xyz/sdk';
5
5
  import type { Address, Domain } from '@hyperlane-xyz/utils';
6
6
  import { parseWarpRouteMessage } from '@hyperlane-xyz/utils';
7
7
 
8
- import type {
9
- ConfirmedBlockTag,
10
- ConfirmedBlockTags,
11
- } from '../interfaces/IMonitor.js';
8
+ import type { ConfirmedBlockTags } from '../interfaces/IMonitor.js';
12
9
  import type {
13
10
  ExplorerClient,
14
11
  ExplorerMessage,
15
12
  } from '../utils/ExplorerClient.js';
13
+ import { getConfirmedBlockTag } from '../utils/blockTag.js';
16
14
 
17
15
  import type {
18
16
  CreateRebalanceActionParams,
@@ -43,7 +41,7 @@ export class ActionTracker implements IActionTracker {
43
41
  private readonly rebalanceIntentStore: IRebalanceIntentStore,
44
42
  private readonly rebalanceActionStore: IRebalanceActionStore,
45
43
  private readonly explorerClient: ExplorerClient,
46
- private readonly core: HyperlaneCore,
44
+ private readonly core: MultiProtocolCore,
47
45
  private readonly config: ActionTrackerConfig,
48
46
  private readonly logger: Logger,
49
47
  ) {}
@@ -171,11 +169,10 @@ export class ActionTracker implements IActionTracker {
171
169
 
172
170
  const existingTransfers = await this.getInProgressTransfers();
173
171
  for (const transfer of existingTransfers) {
174
- const chainName = this.core.multiProvider.getChainName(
172
+ const blockTag = await this.getConfirmedBlockTag(
175
173
  transfer.destination,
174
+ confirmedBlockTags,
176
175
  );
177
- const blockTag = confirmedBlockTags?.[chainName];
178
-
179
176
  const delivered = await this.isMessageDelivered(
180
177
  transfer.messageId,
181
178
  transfer.destination,
@@ -263,11 +260,10 @@ export class ActionTracker implements IActionTracker {
263
260
  const inProgressActions =
264
261
  await this.rebalanceActionStore.getByStatus('in_progress');
265
262
  for (const action of inProgressActions) {
266
- const chainName = this.core.multiProvider.getChainName(
263
+ const blockTag = await this.getConfirmedBlockTag(
267
264
  action.destination,
265
+ confirmedBlockTags,
268
266
  );
269
- const blockTag = confirmedBlockTags?.[chainName];
270
-
271
267
  const delivered = await this.isMessageDelivered(
272
268
  action.messageId,
273
269
  action.destination,
@@ -515,45 +511,43 @@ export class ActionTracker implements IActionTracker {
515
511
 
516
512
  // === Private Helpers ===
517
513
 
514
+ /**
515
+ * Get the confirmed block tag for delivery checks.
516
+ * Uses cached value from Monitor event if available, otherwise computes on-demand.
517
+ */
518
518
  private async getConfirmedBlockTag(
519
- chainName: string,
520
- ): Promise<ConfirmedBlockTag> {
521
- try {
522
- const metadata = this.core.multiProvider.getChainMetadata(chainName);
523
- const reorgPeriod = metadata.blocks?.reorgPeriod ?? 32;
524
-
525
- if (typeof reorgPeriod === 'string') {
526
- return reorgPeriod as ConfirmedBlockTag;
527
- }
519
+ destination: Domain,
520
+ confirmedBlockTags?: ConfirmedBlockTags,
521
+ ): Promise<string | number | undefined> {
522
+ const chainName = this.core.multiProvider.getChainName(destination);
528
523
 
529
- const provider = this.core.multiProvider.getProvider(chainName);
530
- const latestBlock = await provider.getBlockNumber();
531
- return Math.max(0, latestBlock - reorgPeriod);
532
- } catch (error) {
533
- this.logger.warn(
534
- { chain: chainName, error: (error as Error).message },
535
- 'Failed to get confirmed block, using latest',
536
- );
537
- return undefined;
524
+ // If tags provided (from Monitor event), use cached value
525
+ if (confirmedBlockTags) {
526
+ return confirmedBlockTags[chainName];
538
527
  }
528
+
529
+ // Otherwise compute on-demand (e.g., during initialize())
530
+ return getConfirmedBlockTag(
531
+ this.core.multiProvider,
532
+ chainName,
533
+ this.logger,
534
+ );
539
535
  }
540
536
 
541
537
  private async isMessageDelivered(
542
538
  messageId: string,
543
539
  destination: Domain,
544
- providedBlockTag?: ConfirmedBlockTag,
540
+ blockTag?: string | number,
545
541
  ): Promise<boolean> {
546
542
  try {
547
543
  const chainName = this.core.multiProvider.getChainName(destination);
548
- const mailbox = this.core.getContracts(chainName).mailbox;
549
-
550
- const blockTag =
551
- providedBlockTag ?? (await this.getConfirmedBlockTag(chainName));
552
- const delivered = await mailbox.delivered(messageId, { blockTag });
544
+ const delivered = await this.core
545
+ .adapter(chainName)
546
+ .isDelivered(messageId, blockTag);
553
547
 
554
548
  this.logger.debug(
555
- { messageId, destination: chainName, blockTag, delivered },
556
- 'Checked message delivery at confirmed block',
549
+ { messageId, destination: chainName, delivered, blockTag },
550
+ 'Checked message delivery',
557
551
  );
558
552
 
559
553
  return delivered;
@@ -0,0 +1,42 @@
1
+ import type { Logger } from 'pino';
2
+
3
+ import {
4
+ EthJsonRpcBlockParameterTag,
5
+ MultiProtocolProvider,
6
+ } from '@hyperlane-xyz/sdk';
7
+
8
+ import type { ConfirmedBlockTag } from '../interfaces/IMonitor.js';
9
+
10
+ /**
11
+ * Get the confirmed block tag for a chain, accounting for reorg period.
12
+ * Returns a block number that is safe from reorgs, or a named tag like 'finalized'.
13
+ *
14
+ * @param multiProvider - MultiProtocolProvider instance
15
+ * @param chainName - Name of the chain
16
+ * @param logger - Optional logger for warnings
17
+ * @returns Confirmed block tag (number, named tag, or undefined on error)
18
+ */
19
+ export async function getConfirmedBlockTag(
20
+ multiProvider: MultiProtocolProvider,
21
+ chainName: string,
22
+ logger?: Logger,
23
+ ): Promise<ConfirmedBlockTag> {
24
+ try {
25
+ const metadata = multiProvider.getChainMetadata(chainName);
26
+ const reorgPeriod = metadata.blocks?.reorgPeriod ?? 32;
27
+
28
+ if (typeof reorgPeriod === 'string') {
29
+ return reorgPeriod as EthJsonRpcBlockParameterTag;
30
+ }
31
+
32
+ const provider = multiProvider.getEthersV5Provider(chainName);
33
+ const latestBlock = await provider.getBlockNumber();
34
+ return Math.max(0, latestBlock - reorgPeriod);
35
+ } catch (error) {
36
+ logger?.warn(
37
+ { chain: chainName, error: (error as Error).message },
38
+ 'Failed to get confirmed block, using latest',
39
+ );
40
+ return undefined;
41
+ }
42
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './balanceUtils.js';
2
+ export * from './blockTag.js';
2
3
  export * from './bridgeUtils.js';
3
4
  export * from './tokenUtils.js';