@hyperlane-xyz/rebalancer 0.1.2 → 1.0.0

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.
Files changed (223) hide show
  1. package/README.md +134 -14
  2. package/dist/config/RebalancerConfig.d.ts +2 -2
  3. package/dist/config/RebalancerConfig.d.ts.map +1 -1
  4. package/dist/config/RebalancerConfig.js +4 -3
  5. package/dist/config/RebalancerConfig.js.map +1 -1
  6. package/dist/config/RebalancerConfig.test.js +434 -163
  7. package/dist/config/RebalancerConfig.test.js.map +1 -1
  8. package/dist/config/types.d.ts +1650 -290
  9. package/dist/config/types.d.ts.map +1 -1
  10. package/dist/config/types.js +124 -46
  11. package/dist/config/types.js.map +1 -1
  12. package/dist/core/Rebalancer.d.ts +14 -7
  13. package/dist/core/Rebalancer.d.ts.map +1 -1
  14. package/dist/core/Rebalancer.js +168 -99
  15. package/dist/core/Rebalancer.js.map +1 -1
  16. package/dist/core/Rebalancer.test.d.ts +2 -0
  17. package/dist/core/Rebalancer.test.d.ts.map +1 -0
  18. package/dist/core/Rebalancer.test.js +391 -0
  19. package/dist/core/Rebalancer.test.js.map +1 -0
  20. package/dist/core/RebalancerService.d.ts +16 -2
  21. package/dist/core/RebalancerService.d.ts.map +1 -1
  22. package/dist/core/RebalancerService.js +164 -21
  23. package/dist/core/RebalancerService.js.map +1 -1
  24. package/dist/core/RebalancerService.test.d.ts +2 -0
  25. package/dist/core/RebalancerService.test.d.ts.map +1 -0
  26. package/dist/core/RebalancerService.test.js +809 -0
  27. package/dist/core/RebalancerService.test.js.map +1 -0
  28. package/dist/factories/RebalancerContextFactory.d.ts +11 -0
  29. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
  30. package/dist/factories/RebalancerContextFactory.js +60 -13
  31. package/dist/factories/RebalancerContextFactory.js.map +1 -1
  32. package/dist/index.d.ts +6 -6
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/interfaces/IMonitor.d.ts +6 -8
  37. package/dist/interfaces/IMonitor.d.ts.map +1 -1
  38. package/dist/interfaces/IMonitor.js.map +1 -1
  39. package/dist/interfaces/IRebalancer.d.ts +20 -4
  40. package/dist/interfaces/IRebalancer.d.ts.map +1 -1
  41. package/dist/interfaces/IStrategy.d.ts +18 -2
  42. package/dist/interfaces/IStrategy.d.ts.map +1 -1
  43. package/dist/metrics/Metrics.d.ts +4 -2
  44. package/dist/metrics/Metrics.d.ts.map +1 -1
  45. package/dist/metrics/Metrics.js +21 -1
  46. package/dist/metrics/Metrics.js.map +1 -1
  47. package/dist/metrics/scripts/metrics.d.ts +2 -0
  48. package/dist/metrics/scripts/metrics.d.ts.map +1 -1
  49. package/dist/metrics/scripts/metrics.js +12 -0
  50. package/dist/metrics/scripts/metrics.js.map +1 -1
  51. package/dist/monitor/Monitor.d.ts +8 -3
  52. package/dist/monitor/Monitor.d.ts.map +1 -1
  53. package/dist/monitor/Monitor.js +75 -15
  54. package/dist/monitor/Monitor.js.map +1 -1
  55. package/dist/strategy/BaseStrategy.d.ts +51 -5
  56. package/dist/strategy/BaseStrategy.d.ts.map +1 -1
  57. package/dist/strategy/BaseStrategy.js +199 -19
  58. package/dist/strategy/BaseStrategy.js.map +1 -1
  59. package/dist/strategy/CollateralDeficitStrategy.d.ts +65 -0
  60. package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -0
  61. package/dist/strategy/CollateralDeficitStrategy.js +245 -0
  62. package/dist/strategy/CollateralDeficitStrategy.js.map +1 -0
  63. package/dist/strategy/CollateralDeficitStrategy.test.d.ts +2 -0
  64. package/dist/strategy/CollateralDeficitStrategy.test.d.ts.map +1 -0
  65. package/dist/strategy/CollateralDeficitStrategy.test.js +364 -0
  66. package/dist/strategy/CollateralDeficitStrategy.test.js.map +1 -0
  67. package/dist/strategy/CompositeStrategy.d.ts +18 -0
  68. package/dist/strategy/CompositeStrategy.d.ts.map +1 -0
  69. package/dist/strategy/CompositeStrategy.js +63 -0
  70. package/dist/strategy/CompositeStrategy.js.map +1 -0
  71. package/dist/strategy/CompositeStrategy.test.d.ts +2 -0
  72. package/dist/strategy/CompositeStrategy.test.d.ts.map +1 -0
  73. package/dist/strategy/CompositeStrategy.test.js +265 -0
  74. package/dist/strategy/CompositeStrategy.test.js.map +1 -0
  75. package/dist/strategy/MinAmountStrategy.d.ts +12 -5
  76. package/dist/strategy/MinAmountStrategy.d.ts.map +1 -1
  77. package/dist/strategy/MinAmountStrategy.js +23 -14
  78. package/dist/strategy/MinAmountStrategy.js.map +1 -1
  79. package/dist/strategy/MinAmountStrategy.test.js +88 -20
  80. package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
  81. package/dist/strategy/StrategyFactory.d.ts +15 -6
  82. package/dist/strategy/StrategyFactory.d.ts.map +1 -1
  83. package/dist/strategy/StrategyFactory.js +48 -10
  84. package/dist/strategy/StrategyFactory.js.map +1 -1
  85. package/dist/strategy/StrategyFactory.test.js +2 -2
  86. package/dist/strategy/StrategyFactory.test.js.map +1 -1
  87. package/dist/strategy/WeightedStrategy.d.ts +13 -4
  88. package/dist/strategy/WeightedStrategy.d.ts.map +1 -1
  89. package/dist/strategy/WeightedStrategy.js +18 -6
  90. package/dist/strategy/WeightedStrategy.js.map +1 -1
  91. package/dist/strategy/WeightedStrategy.test.js +108 -18
  92. package/dist/strategy/WeightedStrategy.test.js.map +1 -1
  93. package/dist/strategy/index.d.ts +2 -0
  94. package/dist/strategy/index.d.ts.map +1 -1
  95. package/dist/strategy/index.js +2 -0
  96. package/dist/strategy/index.js.map +1 -1
  97. package/dist/test/helpers.d.ts +93 -3
  98. package/dist/test/helpers.d.ts.map +1 -1
  99. package/dist/test/helpers.js +267 -10
  100. package/dist/test/helpers.js.map +1 -1
  101. package/dist/tracking/ActionTracker.d.ts +49 -0
  102. package/dist/tracking/ActionTracker.d.ts.map +1 -0
  103. package/dist/tracking/ActionTracker.js +422 -0
  104. package/dist/tracking/ActionTracker.js.map +1 -0
  105. package/dist/tracking/ActionTracker.test.d.ts +2 -0
  106. package/dist/tracking/ActionTracker.test.d.ts.map +1 -0
  107. package/dist/tracking/ActionTracker.test.js +637 -0
  108. package/dist/tracking/ActionTracker.test.js.map +1 -0
  109. package/dist/tracking/IActionTracker.d.ts +101 -0
  110. package/dist/tracking/IActionTracker.d.ts.map +1 -0
  111. package/dist/tracking/IActionTracker.js +2 -0
  112. package/dist/tracking/IActionTracker.js.map +1 -0
  113. package/dist/tracking/InflightContextAdapter.d.ts +18 -0
  114. package/dist/tracking/InflightContextAdapter.d.ts.map +1 -0
  115. package/dist/tracking/InflightContextAdapter.js +35 -0
  116. package/dist/tracking/InflightContextAdapter.js.map +1 -0
  117. package/dist/tracking/InflightContextAdapter.test.d.ts +2 -0
  118. package/dist/tracking/InflightContextAdapter.test.d.ts.map +1 -0
  119. package/dist/tracking/InflightContextAdapter.test.js +172 -0
  120. package/dist/tracking/InflightContextAdapter.test.js.map +1 -0
  121. package/dist/tracking/index.d.ts +7 -0
  122. package/dist/tracking/index.d.ts.map +1 -0
  123. package/dist/tracking/index.js +6 -0
  124. package/dist/tracking/index.js.map +1 -0
  125. package/dist/tracking/store/IStore.d.ts +41 -0
  126. package/dist/tracking/store/IStore.d.ts.map +1 -0
  127. package/dist/tracking/store/IStore.js +2 -0
  128. package/dist/tracking/store/IStore.js.map +1 -0
  129. package/dist/tracking/store/InMemoryStore.d.ts +21 -0
  130. package/dist/tracking/store/InMemoryStore.d.ts.map +1 -0
  131. package/dist/tracking/store/InMemoryStore.js +40 -0
  132. package/dist/tracking/store/InMemoryStore.js.map +1 -0
  133. package/dist/tracking/store/InMemoryStore.test.d.ts +2 -0
  134. package/dist/tracking/store/InMemoryStore.test.d.ts.map +1 -0
  135. package/dist/tracking/store/InMemoryStore.test.js +290 -0
  136. package/dist/tracking/store/InMemoryStore.test.js.map +1 -0
  137. package/dist/tracking/store/index.d.ts +3 -0
  138. package/dist/tracking/store/index.d.ts.map +1 -0
  139. package/dist/tracking/store/index.js +2 -0
  140. package/dist/tracking/store/index.js.map +1 -0
  141. package/dist/tracking/types.d.ts +43 -0
  142. package/dist/tracking/types.d.ts.map +1 -0
  143. package/dist/tracking/types.js +2 -0
  144. package/dist/tracking/types.js.map +1 -0
  145. package/dist/utils/ExplorerClient.d.ts +39 -1
  146. package/dist/utils/ExplorerClient.d.ts.map +1 -1
  147. package/dist/utils/ExplorerClient.js +205 -2
  148. package/dist/utils/ExplorerClient.js.map +1 -1
  149. package/dist/utils/balanceUtils.js +2 -2
  150. package/dist/utils/balanceUtils.js.map +1 -1
  151. package/dist/utils/balanceUtils.test.js +1 -0
  152. package/dist/utils/balanceUtils.test.js.map +1 -1
  153. package/dist/utils/bridgeUtils.d.ts +1 -3
  154. package/dist/utils/bridgeUtils.d.ts.map +1 -1
  155. package/dist/utils/bridgeUtils.js +1 -5
  156. package/dist/utils/bridgeUtils.js.map +1 -1
  157. package/dist/utils/bridgeUtils.test.js +3 -14
  158. package/dist/utils/bridgeUtils.test.js.map +1 -1
  159. package/package.json +11 -9
  160. package/src/config/RebalancerConfig.test.ts +459 -163
  161. package/src/config/RebalancerConfig.ts +5 -3
  162. package/src/config/types.ts +159 -52
  163. package/src/core/Rebalancer.test.ts +632 -0
  164. package/src/core/Rebalancer.ts +247 -157
  165. package/src/core/RebalancerService.test.ts +1144 -0
  166. package/src/core/RebalancerService.ts +245 -23
  167. package/src/factories/RebalancerContextFactory.ts +115 -14
  168. package/src/index.ts +16 -4
  169. package/src/interfaces/IMonitor.ts +15 -8
  170. package/src/interfaces/IRebalancer.ts +22 -4
  171. package/src/interfaces/IStrategy.ts +23 -2
  172. package/src/metrics/Metrics.ts +26 -5
  173. package/src/metrics/scripts/metrics.ts +14 -0
  174. package/src/monitor/Monitor.ts +109 -22
  175. package/src/strategy/BaseStrategy.ts +316 -26
  176. package/src/strategy/CollateralDeficitStrategy.test.ts +551 -0
  177. package/src/strategy/CollateralDeficitStrategy.ts +390 -0
  178. package/src/strategy/CompositeStrategy.test.ts +405 -0
  179. package/src/strategy/CompositeStrategy.ts +102 -0
  180. package/src/strategy/MinAmountStrategy.test.ts +189 -88
  181. package/src/strategy/MinAmountStrategy.ts +44 -13
  182. package/src/strategy/StrategyFactory.test.ts +2 -2
  183. package/src/strategy/StrategyFactory.ts +91 -8
  184. package/src/strategy/WeightedStrategy.test.ts +187 -72
  185. package/src/strategy/WeightedStrategy.ts +41 -7
  186. package/src/strategy/index.ts +2 -0
  187. package/src/test/helpers.ts +418 -14
  188. package/src/tracking/ActionTracker.test.ts +783 -0
  189. package/src/tracking/ActionTracker.ts +647 -0
  190. package/src/tracking/IActionTracker.ts +140 -0
  191. package/src/tracking/InflightContextAdapter.test.ts +203 -0
  192. package/src/tracking/InflightContextAdapter.ts +42 -0
  193. package/src/tracking/index.ts +36 -0
  194. package/src/tracking/store/IStore.ts +48 -0
  195. package/src/tracking/store/InMemoryStore.test.ts +338 -0
  196. package/src/tracking/store/InMemoryStore.ts +58 -0
  197. package/src/tracking/store/index.ts +2 -0
  198. package/src/tracking/types.ts +74 -0
  199. package/src/utils/ExplorerClient.ts +266 -3
  200. package/src/utils/balanceUtils.test.ts +1 -0
  201. package/src/utils/balanceUtils.ts +2 -2
  202. package/src/utils/bridgeUtils.test.ts +3 -15
  203. package/src/utils/bridgeUtils.ts +0 -10
  204. package/dist/core/WithInflightGuard.d.ts +0 -20
  205. package/dist/core/WithInflightGuard.d.ts.map +0 -1
  206. package/dist/core/WithInflightGuard.js +0 -47
  207. package/dist/core/WithInflightGuard.js.map +0 -1
  208. package/dist/core/WithInflightGuard.test.d.ts +0 -2
  209. package/dist/core/WithInflightGuard.test.d.ts.map +0 -1
  210. package/dist/core/WithInflightGuard.test.js +0 -64
  211. package/dist/core/WithInflightGuard.test.js.map +0 -1
  212. package/dist/core/WithSemaphore.d.ts +0 -22
  213. package/dist/core/WithSemaphore.d.ts.map +0 -1
  214. package/dist/core/WithSemaphore.js +0 -67
  215. package/dist/core/WithSemaphore.js.map +0 -1
  216. package/dist/core/WithSemaphore.test.d.ts +0 -2
  217. package/dist/core/WithSemaphore.test.d.ts.map +0 -1
  218. package/dist/core/WithSemaphore.test.js +0 -83
  219. package/dist/core/WithSemaphore.test.js.map +0 -1
  220. package/src/core/WithInflightGuard.test.ts +0 -131
  221. package/src/core/WithInflightGuard.ts +0 -67
  222. package/src/core/WithSemaphore.test.ts +0 -111
  223. package/src/core/WithSemaphore.ts +0 -92
@@ -0,0 +1,637 @@
1
+ import chai, { expect } from 'chai';
2
+ import chaiAsPromised from 'chai-as-promised';
3
+ import { pino } from 'pino';
4
+ import Sinon from 'sinon';
5
+ import { EthJsonRpcBlockParameterTag } from '@hyperlane-xyz/sdk';
6
+ import { ActionTracker } from './ActionTracker.js';
7
+ import { InMemoryStore } from './store/InMemoryStore.js';
8
+ chai.use(chaiAsPromised);
9
+ const testLogger = pino({ level: 'silent' });
10
+ describe('ActionTracker', () => {
11
+ let transferStore;
12
+ let rebalanceIntentStore;
13
+ let rebalanceActionStore;
14
+ let explorerClient;
15
+ let core;
16
+ let config;
17
+ let tracker;
18
+ let mailboxStub;
19
+ beforeEach(() => {
20
+ transferStore = new InMemoryStore();
21
+ rebalanceIntentStore = new InMemoryStore();
22
+ rebalanceActionStore = new InMemoryStore();
23
+ // Create stub for ExplorerClient methods with default return values
24
+ const explorerGetInflightUserTransfers = Sinon.stub().resolves([]);
25
+ const explorerGetInflightRebalanceActions = Sinon.stub().resolves([]);
26
+ explorerClient = {
27
+ getInflightUserTransfers: explorerGetInflightUserTransfers,
28
+ getInflightRebalanceActions: explorerGetInflightRebalanceActions,
29
+ };
30
+ // Create stub for mailbox
31
+ mailboxStub = {
32
+ delivered: Sinon.stub().resolves(false),
33
+ };
34
+ // Create stub for HyperlaneCore
35
+ const coreGetContracts = Sinon.stub().returns({ mailbox: mailboxStub });
36
+ const multiProviderGetChainName = Sinon.stub().callsFake((domain) => `chain${domain}`);
37
+ core = {
38
+ getContracts: coreGetContracts,
39
+ multiProvider: {
40
+ getChainName: multiProviderGetChainName,
41
+ },
42
+ };
43
+ config = {
44
+ routersByDomain: {
45
+ 1: '0xrouter1',
46
+ 2: '0xrouter2',
47
+ 3: '0xrouter3',
48
+ },
49
+ bridges: ['0xbridge1', '0xbridge2'],
50
+ rebalancerAddress: '0xrebalancer',
51
+ };
52
+ tracker = new ActionTracker(transferStore, rebalanceIntentStore, rebalanceActionStore, explorerClient, core, config, testLogger);
53
+ });
54
+ describe('initialize', () => {
55
+ it('should query for inflight rebalance messages and create synthetic entities', async () => {
56
+ const inflightMessages = [
57
+ {
58
+ msg_id: '0xmsg1',
59
+ origin_domain_id: 1,
60
+ destination_domain_id: 2,
61
+ sender: '0xrouter1',
62
+ recipient: '0xrouter2',
63
+ origin_tx_hash: '0xtx1',
64
+ origin_tx_sender: '0xrebalancer',
65
+ origin_tx_recipient: '0xrouter1',
66
+ is_delivered: false,
67
+ message_body: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064',
68
+ },
69
+ ];
70
+ explorerClient.getInflightRebalanceActions.resolves(inflightMessages);
71
+ explorerClient.getInflightUserTransfers.resolves([]);
72
+ // Ensure mailbox returns false so action stays in_progress
73
+ mailboxStub.delivered.resolves(false);
74
+ await tracker.initialize();
75
+ // Verify ExplorerClient was called twice:
76
+ // 1. During startup recovery in initialize()
77
+ // 2. During syncRebalanceActions() called from initialize()
78
+ expect(explorerClient.getInflightRebalanceActions.callCount).to.equal(2);
79
+ // Verify synthetic intent and action were created
80
+ const intents = await rebalanceIntentStore.getAll();
81
+ expect(intents).to.have.lengthOf(1);
82
+ expect(intents[0].status).to.equal('in_progress');
83
+ expect(intents[0].amount).to.equal(100n);
84
+ const actions = await rebalanceActionStore.getAll();
85
+ expect(actions).to.have.lengthOf(1);
86
+ expect(actions[0].id).to.equal('0xmsg1');
87
+ expect(actions[0].status).to.equal('in_progress');
88
+ expect(actions[0].messageId).to.equal('0xmsg1');
89
+ });
90
+ it('should skip creating action if it already exists', async () => {
91
+ const inflightMessages = [
92
+ {
93
+ msg_id: '0xmsg1',
94
+ origin_domain_id: 1,
95
+ destination_domain_id: 2,
96
+ sender: '0xrouter1',
97
+ recipient: '0xrouter2',
98
+ origin_tx_hash: '0xtx1',
99
+ origin_tx_sender: '0xrebalancer',
100
+ origin_tx_recipient: '0xrouter1',
101
+ is_delivered: false,
102
+ message_body: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064',
103
+ },
104
+ ];
105
+ // Pre-create action
106
+ await rebalanceActionStore.save({
107
+ id: '0xmsg1',
108
+ status: 'in_progress',
109
+ intentId: 'existing-intent',
110
+ messageId: '0xmsg1',
111
+ origin: 1,
112
+ destination: 2,
113
+ amount: 100n,
114
+ createdAt: Date.now(),
115
+ updatedAt: Date.now(),
116
+ });
117
+ explorerClient.getInflightRebalanceActions.resolves(inflightMessages);
118
+ explorerClient.getInflightUserTransfers.resolves([]);
119
+ await tracker.initialize();
120
+ // Verify no additional action was created
121
+ const actions = await rebalanceActionStore.getAll();
122
+ expect(actions).to.have.lengthOf(1);
123
+ // Verify no intent was created either
124
+ const intents = await rebalanceIntentStore.getAll();
125
+ expect(intents).to.have.lengthOf(0);
126
+ });
127
+ });
128
+ describe('syncTransfers', () => {
129
+ it('should create new transfers from Explorer messages', async () => {
130
+ const inflightMessages = [
131
+ {
132
+ msg_id: '0xmsg1',
133
+ origin_domain_id: 1,
134
+ destination_domain_id: 2,
135
+ sender: '0xuser1',
136
+ recipient: '0xuser2',
137
+ origin_tx_hash: '0xtx1',
138
+ origin_tx_sender: '0xuser1',
139
+ origin_tx_recipient: '0xrouter1',
140
+ is_delivered: false,
141
+ message_body: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064',
142
+ },
143
+ ];
144
+ explorerClient.getInflightUserTransfers.resolves(inflightMessages);
145
+ await tracker.syncTransfers();
146
+ const transfers = await transferStore.getAll();
147
+ expect(transfers).to.have.lengthOf(1);
148
+ expect(transfers[0].id).to.equal('0xmsg1');
149
+ expect(transfers[0].status).to.equal('in_progress');
150
+ expect(transfers[0].sender).to.equal('0xuser1');
151
+ expect(transfers[0].amount).to.equal(100n);
152
+ });
153
+ it('should not duplicate transfers that already exist', async () => {
154
+ // Pre-create transfer
155
+ await transferStore.save({
156
+ id: '0xmsg1',
157
+ status: 'in_progress',
158
+ messageId: '0xmsg1',
159
+ origin: 1,
160
+ destination: 2,
161
+ amount: 100n,
162
+ sender: '0xuser1',
163
+ recipient: '0xuser2',
164
+ createdAt: Date.now(),
165
+ updatedAt: Date.now(),
166
+ });
167
+ const inflightMessages = [
168
+ {
169
+ msg_id: '0xmsg1',
170
+ origin_domain_id: 1,
171
+ destination_domain_id: 2,
172
+ sender: '0xuser1',
173
+ recipient: '0xuser2',
174
+ origin_tx_hash: '0xtx1',
175
+ origin_tx_sender: '0xuser1',
176
+ origin_tx_recipient: '0xrouter1',
177
+ is_delivered: false,
178
+ message_body: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064',
179
+ },
180
+ ];
181
+ explorerClient.getInflightUserTransfers.resolves(inflightMessages);
182
+ await tracker.syncTransfers();
183
+ const transfers = await transferStore.getAll();
184
+ expect(transfers).to.have.lengthOf(1);
185
+ });
186
+ it('should mark transfers as complete when delivered', async () => {
187
+ // Pre-create transfer
188
+ await transferStore.save({
189
+ id: '0xmsg1',
190
+ status: 'in_progress',
191
+ messageId: '0xmsg1',
192
+ origin: 1,
193
+ destination: 2,
194
+ amount: 100n,
195
+ sender: '0xuser1',
196
+ recipient: '0xuser2',
197
+ createdAt: Date.now(),
198
+ updatedAt: Date.now(),
199
+ });
200
+ explorerClient.getInflightUserTransfers.resolves([]);
201
+ mailboxStub.delivered.resolves(true);
202
+ await tracker.syncTransfers();
203
+ const transfer = await transferStore.get('0xmsg1');
204
+ expect(transfer?.status).to.equal('complete');
205
+ });
206
+ });
207
+ describe('syncRebalanceIntents', () => {
208
+ it('should mark intents as complete when fully fulfilled', async () => {
209
+ const intent = {
210
+ id: 'intent-1',
211
+ status: 'in_progress',
212
+ origin: 1,
213
+ destination: 2,
214
+ amount: 100n,
215
+ fulfilledAmount: 100n,
216
+ createdAt: Date.now(),
217
+ updatedAt: Date.now(),
218
+ };
219
+ await rebalanceIntentStore.save(intent);
220
+ await tracker.syncRebalanceIntents();
221
+ const updated = await rebalanceIntentStore.get('intent-1');
222
+ expect(updated?.status).to.equal('complete');
223
+ });
224
+ it('should not mark intents as complete if not fully fulfilled', async () => {
225
+ const intent = {
226
+ id: 'intent-1',
227
+ status: 'in_progress',
228
+ origin: 1,
229
+ destination: 2,
230
+ amount: 100n,
231
+ fulfilledAmount: 50n,
232
+ createdAt: Date.now(),
233
+ updatedAt: Date.now(),
234
+ };
235
+ await rebalanceIntentStore.save(intent);
236
+ await tracker.syncRebalanceIntents();
237
+ const updated = await rebalanceIntentStore.get('intent-1');
238
+ expect(updated?.status).to.equal('in_progress');
239
+ });
240
+ });
241
+ describe('syncRebalanceActions', () => {
242
+ it('should mark actions as complete when delivered and update parent intent', async () => {
243
+ const intent = {
244
+ id: 'intent-1',
245
+ status: 'in_progress',
246
+ origin: 1,
247
+ destination: 2,
248
+ amount: 100n,
249
+ fulfilledAmount: 0n,
250
+ createdAt: Date.now(),
251
+ updatedAt: Date.now(),
252
+ };
253
+ const action = {
254
+ id: 'action-1',
255
+ status: 'in_progress',
256
+ intentId: 'intent-1',
257
+ messageId: '0xmsg1',
258
+ origin: 1,
259
+ destination: 2,
260
+ amount: 100n,
261
+ createdAt: Date.now(),
262
+ updatedAt: Date.now(),
263
+ };
264
+ await rebalanceIntentStore.save(intent);
265
+ await rebalanceActionStore.save(action);
266
+ mailboxStub.delivered.resolves(true);
267
+ await tracker.syncRebalanceActions();
268
+ // Action should be complete
269
+ const updatedAction = await rebalanceActionStore.get('action-1');
270
+ expect(updatedAction?.status).to.equal('complete');
271
+ // Intent should be updated and complete
272
+ const updatedIntent = await rebalanceIntentStore.get('intent-1');
273
+ expect(updatedIntent?.fulfilledAmount).to.equal(100n);
274
+ expect(updatedIntent?.status).to.equal('complete');
275
+ });
276
+ it('should not mark actions as complete if not delivered', async () => {
277
+ const action = {
278
+ id: 'action-1',
279
+ status: 'in_progress',
280
+ intentId: 'intent-1',
281
+ messageId: '0xmsg1',
282
+ origin: 1,
283
+ destination: 2,
284
+ amount: 100n,
285
+ createdAt: Date.now(),
286
+ updatedAt: Date.now(),
287
+ };
288
+ await rebalanceActionStore.save(action);
289
+ mailboxStub.delivered.resolves(false);
290
+ await tracker.syncRebalanceActions();
291
+ const updatedAction = await rebalanceActionStore.get('action-1');
292
+ expect(updatedAction?.status).to.equal('in_progress');
293
+ });
294
+ });
295
+ describe('getInProgressTransfers', () => {
296
+ it('should return only in_progress transfers', async () => {
297
+ await transferStore.save({
298
+ id: 'transfer-1',
299
+ status: 'in_progress',
300
+ messageId: '0xmsg1',
301
+ origin: 1,
302
+ destination: 2,
303
+ amount: 100n,
304
+ sender: '0xsender1',
305
+ recipient: '0xrecipient1',
306
+ createdAt: Date.now(),
307
+ updatedAt: Date.now(),
308
+ });
309
+ await transferStore.save({
310
+ id: 'transfer-2',
311
+ status: 'complete',
312
+ messageId: '0xmsg2',
313
+ origin: 2,
314
+ destination: 3,
315
+ amount: 200n,
316
+ sender: '0xsender2',
317
+ recipient: '0xrecipient2',
318
+ createdAt: Date.now(),
319
+ updatedAt: Date.now(),
320
+ });
321
+ const result = await tracker.getInProgressTransfers();
322
+ expect(result).to.have.lengthOf(1);
323
+ expect(result[0].id).to.equal('transfer-1');
324
+ });
325
+ });
326
+ describe('getActiveRebalanceIntents', () => {
327
+ it('should return only in_progress intents (origin tx confirmed)', async () => {
328
+ await rebalanceIntentStore.save({
329
+ id: 'intent-1',
330
+ status: 'not_started',
331
+ origin: 1,
332
+ destination: 2,
333
+ amount: 100n,
334
+ fulfilledAmount: 0n,
335
+ createdAt: Date.now(),
336
+ updatedAt: Date.now(),
337
+ });
338
+ await rebalanceIntentStore.save({
339
+ id: 'intent-2',
340
+ status: 'in_progress',
341
+ origin: 2,
342
+ destination: 3,
343
+ amount: 200n,
344
+ fulfilledAmount: 50n,
345
+ createdAt: Date.now(),
346
+ updatedAt: Date.now(),
347
+ });
348
+ await rebalanceIntentStore.save({
349
+ id: 'intent-3',
350
+ status: 'complete',
351
+ origin: 3,
352
+ destination: 1,
353
+ amount: 300n,
354
+ fulfilledAmount: 300n,
355
+ createdAt: Date.now(),
356
+ updatedAt: Date.now(),
357
+ });
358
+ // Only in_progress intents are returned - their origin tx is confirmed
359
+ // so simulation only needs to add to destination (origin already deducted on-chain)
360
+ const result = await tracker.getActiveRebalanceIntents();
361
+ expect(result).to.have.lengthOf(1);
362
+ expect(result[0].id).to.equal('intent-2');
363
+ });
364
+ });
365
+ describe('createRebalanceIntent', () => {
366
+ it('should create a new intent with status not_started', async () => {
367
+ const result = await tracker.createRebalanceIntent({
368
+ origin: 1,
369
+ destination: 2,
370
+ amount: 100n,
371
+ priority: 1,
372
+ strategyType: 'MinAmountStrategy',
373
+ });
374
+ expect(result.status).to.equal('not_started');
375
+ expect(result.origin).to.equal(1);
376
+ expect(result.destination).to.equal(2);
377
+ expect(result.amount).to.equal(100n);
378
+ expect(result.fulfilledAmount).to.equal(0n);
379
+ expect(result.priority).to.equal(1);
380
+ expect(result.strategyType).to.equal('MinAmountStrategy');
381
+ const stored = await rebalanceIntentStore.get(result.id);
382
+ expect(stored).to.deep.equal(result);
383
+ });
384
+ });
385
+ describe('createRebalanceAction', () => {
386
+ it('should create action and transition intent from not_started to in_progress', async () => {
387
+ const intent = {
388
+ id: 'intent-1',
389
+ status: 'not_started',
390
+ origin: 1,
391
+ destination: 2,
392
+ amount: 100n,
393
+ fulfilledAmount: 0n,
394
+ createdAt: Date.now(),
395
+ updatedAt: Date.now(),
396
+ };
397
+ await rebalanceIntentStore.save(intent);
398
+ const result = await tracker.createRebalanceAction({
399
+ intentId: 'intent-1',
400
+ origin: 1,
401
+ destination: 2,
402
+ amount: 100n,
403
+ messageId: '0xmsg1',
404
+ txHash: '0xtx1',
405
+ });
406
+ expect(result.status).to.equal('in_progress');
407
+ expect(result.intentId).to.equal('intent-1');
408
+ expect(result.messageId).to.equal('0xmsg1');
409
+ const updatedIntent = await rebalanceIntentStore.get('intent-1');
410
+ expect(updatedIntent?.status).to.equal('in_progress');
411
+ });
412
+ it('should not transition intent if already in_progress', async () => {
413
+ const intent = {
414
+ id: 'intent-1',
415
+ status: 'in_progress',
416
+ origin: 1,
417
+ destination: 2,
418
+ amount: 100n,
419
+ fulfilledAmount: 50n,
420
+ createdAt: Date.now(),
421
+ updatedAt: Date.now(),
422
+ };
423
+ await rebalanceIntentStore.save(intent);
424
+ await tracker.createRebalanceAction({
425
+ intentId: 'intent-1',
426
+ origin: 1,
427
+ destination: 2,
428
+ amount: 50n,
429
+ messageId: '0xmsg2',
430
+ txHash: '0xtx2',
431
+ });
432
+ const updatedIntent = await rebalanceIntentStore.get('intent-1');
433
+ expect(updatedIntent?.status).to.equal('in_progress');
434
+ expect(updatedIntent?.fulfilledAmount).to.equal(50n); // Should not change
435
+ });
436
+ });
437
+ describe('completeRebalanceAction', () => {
438
+ it('should mark action as complete and update parent intent fulfilledAmount', async () => {
439
+ const intent = {
440
+ id: 'intent-1',
441
+ status: 'in_progress',
442
+ origin: 1,
443
+ destination: 2,
444
+ amount: 100n,
445
+ fulfilledAmount: 0n,
446
+ createdAt: Date.now(),
447
+ updatedAt: Date.now(),
448
+ };
449
+ const action = {
450
+ id: 'action-1',
451
+ status: 'in_progress',
452
+ intentId: 'intent-1',
453
+ messageId: '0xmsg1',
454
+ origin: 1,
455
+ destination: 2,
456
+ amount: 100n,
457
+ createdAt: Date.now(),
458
+ updatedAt: Date.now(),
459
+ };
460
+ await rebalanceIntentStore.save(intent);
461
+ await rebalanceActionStore.save(action);
462
+ await tracker.completeRebalanceAction('action-1');
463
+ const updatedAction = await rebalanceActionStore.get('action-1');
464
+ expect(updatedAction?.status).to.equal('complete');
465
+ const updatedIntent = await rebalanceIntentStore.get('intent-1');
466
+ expect(updatedIntent?.fulfilledAmount).to.equal(100n);
467
+ expect(updatedIntent?.status).to.equal('complete');
468
+ });
469
+ it('should throw error when action not found', async () => {
470
+ await expect(tracker.completeRebalanceAction('non-existent')).to.be.rejectedWith('RebalanceAction non-existent not found');
471
+ });
472
+ });
473
+ describe('cancelRebalanceIntent', () => {
474
+ it('should mark intent as cancelled', async () => {
475
+ const intent = {
476
+ id: 'intent-1',
477
+ status: 'not_started',
478
+ origin: 1,
479
+ destination: 2,
480
+ amount: 100n,
481
+ fulfilledAmount: 0n,
482
+ createdAt: Date.now(),
483
+ updatedAt: Date.now(),
484
+ };
485
+ await rebalanceIntentStore.save(intent);
486
+ await tracker.cancelRebalanceIntent('intent-1');
487
+ const updated = await rebalanceIntentStore.get('intent-1');
488
+ expect(updated?.status).to.equal('cancelled');
489
+ });
490
+ });
491
+ describe('failRebalanceAction', () => {
492
+ it('should mark action as failed', async () => {
493
+ const action = {
494
+ id: 'action-1',
495
+ status: 'in_progress',
496
+ intentId: 'intent-1',
497
+ messageId: '0xmsg1',
498
+ origin: 1,
499
+ destination: 2,
500
+ amount: 100n,
501
+ createdAt: Date.now(),
502
+ updatedAt: Date.now(),
503
+ };
504
+ await rebalanceActionStore.save(action);
505
+ await tracker.failRebalanceAction('action-1');
506
+ const updated = await rebalanceActionStore.get('action-1');
507
+ expect(updated?.status).to.equal('failed');
508
+ });
509
+ });
510
+ describe('Explorer query parameters', () => {
511
+ it('should pass routersByDomain to getInflightRebalanceActions for warp route filtering', async () => {
512
+ explorerClient.getInflightRebalanceActions.resolves([]);
513
+ explorerClient.getInflightUserTransfers.resolves([]);
514
+ await tracker.initialize();
515
+ const call = explorerClient.getInflightRebalanceActions.firstCall;
516
+ expect(call).to.not.be.null;
517
+ const params = call.args[0];
518
+ expect(params.routersByDomain).to.deep.equal(config.routersByDomain);
519
+ expect(params.bridges).to.deep.equal(config.bridges);
520
+ expect(params.rebalancerAddress).to.equal(config.rebalancerAddress);
521
+ });
522
+ it('should pass routersByDomain to getInflightUserTransfers for warp route filtering', async () => {
523
+ explorerClient.getInflightRebalanceActions.resolves([]);
524
+ explorerClient.getInflightUserTransfers.resolves([]);
525
+ await tracker.initialize();
526
+ const call = explorerClient.getInflightUserTransfers.firstCall;
527
+ expect(call).to.not.be.null;
528
+ const params = call.args[0];
529
+ expect(params.routersByDomain).to.deep.equal(config.routersByDomain);
530
+ expect(params.excludeTxSender).to.equal(config.rebalancerAddress);
531
+ });
532
+ });
533
+ describe('confirmedBlockTags synchronization', () => {
534
+ it('should use provided blockTag in syncTransfers delivery check', async () => {
535
+ await transferStore.save({
536
+ id: '0xmsg1',
537
+ status: 'in_progress',
538
+ messageId: '0xmsg1',
539
+ origin: 1,
540
+ destination: 2,
541
+ amount: 100n,
542
+ sender: '0xuser1',
543
+ recipient: '0xuser2',
544
+ createdAt: Date.now(),
545
+ updatedAt: Date.now(),
546
+ });
547
+ explorerClient.getInflightUserTransfers.resolves([]);
548
+ mailboxStub.delivered.resolves(true);
549
+ const confirmedBlockTags = { chain2: 12345 };
550
+ await tracker.syncTransfers(confirmedBlockTags);
551
+ expect(mailboxStub.delivered.calledOnce).to.be.true;
552
+ const call = mailboxStub.delivered.firstCall;
553
+ expect(call.args[0]).to.equal('0xmsg1');
554
+ expect(call.args[1]).to.deep.equal({ blockTag: 12345 });
555
+ const transfer = await transferStore.get('0xmsg1');
556
+ expect(transfer?.status).to.equal('complete');
557
+ });
558
+ it('should use provided blockTag in syncRebalanceActions delivery check', async () => {
559
+ const intent = {
560
+ id: 'intent-1',
561
+ status: 'in_progress',
562
+ origin: 1,
563
+ destination: 2,
564
+ amount: 100n,
565
+ fulfilledAmount: 0n,
566
+ createdAt: Date.now(),
567
+ updatedAt: Date.now(),
568
+ };
569
+ const action = {
570
+ id: 'action-1',
571
+ status: 'in_progress',
572
+ intentId: 'intent-1',
573
+ messageId: '0xmsg1',
574
+ origin: 1,
575
+ destination: 2,
576
+ amount: 100n,
577
+ createdAt: Date.now(),
578
+ updatedAt: Date.now(),
579
+ };
580
+ await rebalanceIntentStore.save(intent);
581
+ await rebalanceActionStore.save(action);
582
+ explorerClient.getInflightRebalanceActions.resolves([]);
583
+ mailboxStub.delivered.resolves(true);
584
+ const confirmedBlockTags = { chain2: 99999 };
585
+ await tracker.syncRebalanceActions(confirmedBlockTags);
586
+ expect(mailboxStub.delivered.calledOnce).to.be.true;
587
+ const call = mailboxStub.delivered.firstCall;
588
+ expect(call.args[0]).to.equal('0xmsg1');
589
+ expect(call.args[1]).to.deep.equal({ blockTag: 99999 });
590
+ const updatedAction = await rebalanceActionStore.get('action-1');
591
+ expect(updatedAction?.status).to.equal('complete');
592
+ });
593
+ it('should handle string blockTags (like "safe" or "finalized")', async () => {
594
+ await transferStore.save({
595
+ id: '0xmsg1',
596
+ status: 'in_progress',
597
+ messageId: '0xmsg1',
598
+ origin: 1,
599
+ destination: 2,
600
+ amount: 100n,
601
+ sender: '0xuser1',
602
+ recipient: '0xuser2',
603
+ createdAt: Date.now(),
604
+ updatedAt: Date.now(),
605
+ });
606
+ explorerClient.getInflightUserTransfers.resolves([]);
607
+ mailboxStub.delivered.resolves(false);
608
+ const confirmedBlockTags = {
609
+ chain2: EthJsonRpcBlockParameterTag.Finalized,
610
+ };
611
+ await tracker.syncTransfers(confirmedBlockTags);
612
+ expect(mailboxStub.delivered.calledOnce).to.be.true;
613
+ const call = mailboxStub.delivered.firstCall;
614
+ expect(call.args[1]).to.deep.equal({ blockTag: 'finalized' });
615
+ });
616
+ it('should handle undefined blockTag for chain not in confirmedBlockTags', async () => {
617
+ await transferStore.save({
618
+ id: '0xmsg1',
619
+ status: 'in_progress',
620
+ messageId: '0xmsg1',
621
+ origin: 1,
622
+ destination: 3,
623
+ amount: 100n,
624
+ sender: '0xuser1',
625
+ recipient: '0xuser2',
626
+ createdAt: Date.now(),
627
+ updatedAt: Date.now(),
628
+ });
629
+ explorerClient.getInflightUserTransfers.resolves([]);
630
+ mailboxStub.delivered.resolves(false);
631
+ const confirmedBlockTags = { chain2: 12345 };
632
+ await tracker.syncTransfers(confirmedBlockTags);
633
+ expect(mailboxStub.delivered.calledOnce).to.be.true;
634
+ });
635
+ });
636
+ });
637
+ //# sourceMappingURL=ActionTracker.test.js.map