@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,647 @@
1
+ import type { Logger } from 'pino';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ import type { HyperlaneCore } from '@hyperlane-xyz/sdk';
5
+ import type { Address, Domain } from '@hyperlane-xyz/utils';
6
+ import { parseWarpRouteMessage } from '@hyperlane-xyz/utils';
7
+
8
+ import type {
9
+ ConfirmedBlockTag,
10
+ ConfirmedBlockTags,
11
+ } from '../interfaces/IMonitor.js';
12
+ import type {
13
+ ExplorerClient,
14
+ ExplorerMessage,
15
+ } from '../utils/ExplorerClient.js';
16
+
17
+ import type {
18
+ CreateRebalanceActionParams,
19
+ CreateRebalanceIntentParams,
20
+ IActionTracker,
21
+ } from './IActionTracker.js';
22
+ import type {
23
+ IRebalanceActionStore,
24
+ IRebalanceIntentStore,
25
+ ITransferStore,
26
+ RebalanceAction,
27
+ RebalanceIntent,
28
+ Transfer,
29
+ } from './types.js';
30
+
31
+ export interface ActionTrackerConfig {
32
+ routersByDomain: Record<number, string>; // Domain ID → router address (source of truth for routers and domains)
33
+ bridges: Address[]; // Bridge contract addresses for rebalance action queries
34
+ rebalancerAddress: Address;
35
+ }
36
+
37
+ /**
38
+ * ActionTracker implementation managing the lifecycle of tracked entities.
39
+ */
40
+ export class ActionTracker implements IActionTracker {
41
+ constructor(
42
+ private readonly transferStore: ITransferStore,
43
+ private readonly rebalanceIntentStore: IRebalanceIntentStore,
44
+ private readonly rebalanceActionStore: IRebalanceActionStore,
45
+ private readonly explorerClient: ExplorerClient,
46
+ private readonly core: HyperlaneCore,
47
+ private readonly config: ActionTrackerConfig,
48
+ private readonly logger: Logger,
49
+ ) {}
50
+
51
+ // === Lifecycle ===
52
+
53
+ async initialize(): Promise<void> {
54
+ this.logger.info('ActionTracker initializing');
55
+
56
+ // Log config for debugging
57
+ this.logger.debug(
58
+ {
59
+ routersByDomain: this.config.routersByDomain,
60
+ bridges: this.config.bridges,
61
+ rebalancerAddress: this.config.rebalancerAddress,
62
+ },
63
+ 'ActionTracker config',
64
+ );
65
+
66
+ // 1. Startup recovery: query Explorer for inflight rebalance messages
67
+ const inflightMessages =
68
+ await this.explorerClient.getInflightRebalanceActions(
69
+ {
70
+ bridges: this.config.bridges,
71
+ routersByDomain: this.config.routersByDomain,
72
+ rebalancerAddress: this.config.rebalancerAddress,
73
+ },
74
+ this.logger,
75
+ );
76
+
77
+ this.logger.info(
78
+ { count: inflightMessages.length },
79
+ 'Found inflight rebalance messages during startup',
80
+ );
81
+
82
+ // 2. For each message, create synthetic intent + action
83
+ for (const msg of inflightMessages) {
84
+ await this.recoverAction(msg);
85
+ }
86
+
87
+ // 3. Sync all stores
88
+ await this.syncTransfers();
89
+ await this.syncRebalanceIntents();
90
+ await this.syncRebalanceActions();
91
+
92
+ // Log store contents for debugging
93
+ await this.logStoreContents();
94
+
95
+ this.logger.info('ActionTracker initialized');
96
+ }
97
+
98
+ // === Sync Operations ===
99
+
100
+ async syncTransfers(confirmedBlockTags?: ConfirmedBlockTags): Promise<void> {
101
+ this.logger.debug('Syncing transfers');
102
+
103
+ const inflightMessages = await this.explorerClient.getInflightUserTransfers(
104
+ {
105
+ routersByDomain: this.config.routersByDomain,
106
+ excludeTxSender: this.config.rebalancerAddress,
107
+ },
108
+ this.logger,
109
+ );
110
+
111
+ this.logger.debug(
112
+ { count: inflightMessages.length },
113
+ 'Received inflight user transfers from Explorer',
114
+ );
115
+
116
+ let newTransfers = 0;
117
+ let completedTransfers = 0;
118
+
119
+ for (const msg of inflightMessages) {
120
+ const transfer = await this.transferStore.get(msg.msg_id);
121
+
122
+ if (!transfer) {
123
+ this.logger.debug(
124
+ {
125
+ msgId: msg.msg_id,
126
+ origin: msg.origin_domain_id,
127
+ destination: msg.destination_domain_id,
128
+ sender: msg.sender,
129
+ recipient: msg.recipient,
130
+ messageBodyLength: msg.message_body?.length,
131
+ messageBodyPreview: msg.message_body?.substring(0, 66),
132
+ },
133
+ 'Processing new transfer message',
134
+ );
135
+
136
+ try {
137
+ const { amount } = parseWarpRouteMessage(msg.message_body);
138
+ const newTransfer: Transfer = {
139
+ id: msg.msg_id,
140
+ status: 'in_progress',
141
+ messageId: msg.msg_id,
142
+ origin: msg.origin_domain_id,
143
+ destination: msg.destination_domain_id,
144
+ sender: msg.sender,
145
+ recipient: msg.recipient,
146
+ amount,
147
+ createdAt: Date.now(),
148
+ updatedAt: Date.now(),
149
+ };
150
+ await this.transferStore.save(newTransfer);
151
+ newTransfers++;
152
+ this.logger.debug(
153
+ { id: newTransfer.id, amount: amount.toString() },
154
+ 'Created new transfer',
155
+ );
156
+ } catch (error) {
157
+ this.logger.warn(
158
+ {
159
+ msgId: msg.msg_id,
160
+ messageBody: msg.message_body,
161
+ messageBodyLength: msg.message_body?.length,
162
+ origin: msg.origin_domain_id,
163
+ destination: msg.destination_domain_id,
164
+ error: error instanceof Error ? error.message : String(error),
165
+ },
166
+ 'Failed to parse message body, skipping transfer',
167
+ );
168
+ }
169
+ }
170
+ }
171
+
172
+ const existingTransfers = await this.getInProgressTransfers();
173
+ for (const transfer of existingTransfers) {
174
+ const chainName = this.core.multiProvider.getChainName(
175
+ transfer.destination,
176
+ );
177
+ const blockTag = confirmedBlockTags?.[chainName];
178
+
179
+ const delivered = await this.isMessageDelivered(
180
+ transfer.messageId,
181
+ transfer.destination,
182
+ blockTag,
183
+ );
184
+
185
+ if (delivered) {
186
+ await this.transferStore.update(transfer.id, { status: 'complete' });
187
+ completedTransfers++;
188
+ this.logger.debug({ id: transfer.id }, 'Transfer completed');
189
+ }
190
+ }
191
+
192
+ const inProgressCount = (await this.getInProgressTransfers()).length;
193
+ this.logger.info(
194
+ {
195
+ newTransfers,
196
+ completed: completedTransfers,
197
+ inProgress: inProgressCount,
198
+ },
199
+ 'Transfers synced',
200
+ );
201
+ }
202
+
203
+ async syncRebalanceIntents(): Promise<void> {
204
+ this.logger.debug('Syncing rebalance intents');
205
+
206
+ // Check in_progress intents for completion
207
+ const inProgressIntents =
208
+ await this.rebalanceIntentStore.getByStatus('in_progress');
209
+ for (const intent of inProgressIntents) {
210
+ if (intent.fulfilledAmount >= intent.amount) {
211
+ await this.rebalanceIntentStore.update(intent.id, {
212
+ status: 'complete',
213
+ });
214
+ this.logger.debug({ id: intent.id }, 'RebalanceIntent completed');
215
+ }
216
+ }
217
+
218
+ this.logger.debug('Rebalance intents synced');
219
+ }
220
+
221
+ async syncRebalanceActions(
222
+ confirmedBlockTags?: ConfirmedBlockTags,
223
+ ): Promise<void> {
224
+ this.logger.debug('Syncing rebalance actions');
225
+
226
+ let discoveredActions = 0;
227
+ let completedActions = 0;
228
+
229
+ const inflightMessages =
230
+ await this.explorerClient.getInflightRebalanceActions(
231
+ {
232
+ bridges: this.config.bridges,
233
+ routersByDomain: this.config.routersByDomain,
234
+ rebalancerAddress: this.config.rebalancerAddress,
235
+ },
236
+ this.logger,
237
+ );
238
+
239
+ this.logger.debug(
240
+ { count: inflightMessages.length },
241
+ 'Found inflight rebalance actions from Explorer',
242
+ );
243
+
244
+ const allActions = await this.rebalanceActionStore.getAll();
245
+
246
+ for (const msg of inflightMessages) {
247
+ const existingAction = allActions.find((a) => a.messageId === msg.msg_id);
248
+
249
+ if (!existingAction) {
250
+ this.logger.info(
251
+ {
252
+ msgId: msg.msg_id,
253
+ origin: msg.origin_domain_id,
254
+ destination: msg.destination_domain_id,
255
+ },
256
+ 'Discovered new rebalance action, recovering...',
257
+ );
258
+ await this.recoverAction(msg);
259
+ discoveredActions++;
260
+ }
261
+ }
262
+
263
+ const inProgressActions =
264
+ await this.rebalanceActionStore.getByStatus('in_progress');
265
+ for (const action of inProgressActions) {
266
+ const chainName = this.core.multiProvider.getChainName(
267
+ action.destination,
268
+ );
269
+ const blockTag = confirmedBlockTags?.[chainName];
270
+
271
+ const delivered = await this.isMessageDelivered(
272
+ action.messageId,
273
+ action.destination,
274
+ blockTag,
275
+ );
276
+
277
+ if (delivered) {
278
+ await this.completeRebalanceAction(action.id);
279
+ completedActions++;
280
+ this.logger.debug({ id: action.id }, 'RebalanceAction completed');
281
+ }
282
+ }
283
+
284
+ const inProgressCount = (
285
+ await this.rebalanceActionStore.getByStatus('in_progress')
286
+ ).length;
287
+ this.logger.info(
288
+ {
289
+ discovered: discoveredActions,
290
+ completed: completedActions,
291
+ inProgress: inProgressCount,
292
+ },
293
+ 'Actions synced',
294
+ );
295
+ }
296
+
297
+ // === Transfer Queries ===
298
+
299
+ async getInProgressTransfers(): Promise<Transfer[]> {
300
+ return this.transferStore.getByStatus('in_progress');
301
+ }
302
+
303
+ async getTransfersByDestination(destination: Domain): Promise<Transfer[]> {
304
+ return this.transferStore.getByDestination(destination);
305
+ }
306
+
307
+ // === RebalanceIntent Queries ===
308
+
309
+ async getActiveRebalanceIntents(): Promise<RebalanceIntent[]> {
310
+ // Only return in_progress intents - their origin tx is confirmed
311
+ // so simulation only needs to add to destination (origin already deducted on-chain)
312
+ return this.rebalanceIntentStore.getByStatus('in_progress');
313
+ }
314
+
315
+ async getRebalanceIntentsByDestination(
316
+ destination: Domain,
317
+ ): Promise<RebalanceIntent[]> {
318
+ return this.rebalanceIntentStore.getByDestination(destination);
319
+ }
320
+
321
+ // === RebalanceIntent Management ===
322
+
323
+ async createRebalanceIntent(
324
+ params: CreateRebalanceIntentParams,
325
+ ): Promise<RebalanceIntent> {
326
+ const intent: RebalanceIntent = {
327
+ id: uuidv4(),
328
+ status: 'not_started',
329
+ origin: params.origin,
330
+ destination: params.destination,
331
+ amount: params.amount,
332
+ fulfilledAmount: 0n,
333
+ bridge: params.bridge,
334
+ priority: params.priority,
335
+ strategyType: params.strategyType,
336
+ createdAt: Date.now(),
337
+ updatedAt: Date.now(),
338
+ };
339
+
340
+ await this.rebalanceIntentStore.save(intent);
341
+ this.logger.debug(
342
+ { id: intent.id, origin: intent.origin, destination: intent.destination },
343
+ 'Created RebalanceIntent',
344
+ );
345
+
346
+ return intent;
347
+ }
348
+
349
+ async completeRebalanceIntent(id: string): Promise<void> {
350
+ await this.rebalanceIntentStore.update(id, { status: 'complete' });
351
+ this.logger.info({ id }, 'Intent completed');
352
+ }
353
+
354
+ async cancelRebalanceIntent(id: string): Promise<void> {
355
+ await this.rebalanceIntentStore.update(id, { status: 'cancelled' });
356
+ this.logger.debug({ id }, 'Cancelled RebalanceIntent');
357
+ }
358
+
359
+ async failRebalanceIntent(id: string): Promise<void> {
360
+ await this.rebalanceIntentStore.update(id, { status: 'failed' });
361
+ this.logger.info({ id }, 'Intent failed');
362
+ }
363
+
364
+ // === RebalanceAction Management ===
365
+
366
+ async createRebalanceAction(
367
+ params: CreateRebalanceActionParams,
368
+ ): Promise<RebalanceAction> {
369
+ const action: RebalanceAction = {
370
+ id: uuidv4(),
371
+ status: 'in_progress',
372
+ intentId: params.intentId,
373
+ messageId: params.messageId,
374
+ txHash: params.txHash,
375
+ origin: params.origin,
376
+ destination: params.destination,
377
+ amount: params.amount,
378
+ createdAt: Date.now(),
379
+ updatedAt: Date.now(),
380
+ };
381
+
382
+ await this.rebalanceActionStore.save(action);
383
+
384
+ // Transition parent intent from not_started to in_progress
385
+ const intent = await this.rebalanceIntentStore.get(params.intentId);
386
+ if (intent && intent.status === 'not_started') {
387
+ await this.rebalanceIntentStore.update(intent.id, {
388
+ status: 'in_progress',
389
+ });
390
+ this.logger.debug(
391
+ { intentId: intent.id },
392
+ 'Transitioned RebalanceIntent to in_progress',
393
+ );
394
+ }
395
+
396
+ this.logger.debug(
397
+ { id: action.id, intentId: action.intentId },
398
+ 'Created RebalanceAction',
399
+ );
400
+
401
+ return action;
402
+ }
403
+
404
+ async completeRebalanceAction(id: string): Promise<void> {
405
+ const action = await this.rebalanceActionStore.get(id);
406
+ if (!action) {
407
+ throw new Error(`RebalanceAction ${id} not found`);
408
+ }
409
+
410
+ await this.rebalanceActionStore.update(id, { status: 'complete' });
411
+
412
+ // Update parent intent's fulfilledAmount
413
+ const intent = await this.rebalanceIntentStore.get(action.intentId);
414
+ if (intent) {
415
+ const newFulfilledAmount = intent.fulfilledAmount + action.amount;
416
+ const updates: Partial<RebalanceIntent> = {
417
+ fulfilledAmount: newFulfilledAmount,
418
+ };
419
+
420
+ // Check if intent is now complete
421
+ if (newFulfilledAmount >= intent.amount) {
422
+ updates.status = 'complete';
423
+ this.logger.debug(
424
+ { intentId: intent.id },
425
+ 'RebalanceIntent fully fulfilled',
426
+ );
427
+ }
428
+
429
+ await this.rebalanceIntentStore.update(intent.id, updates);
430
+ }
431
+
432
+ this.logger.info({ id, intentId: action.intentId }, 'Action completed');
433
+ }
434
+
435
+ async failRebalanceAction(id: string): Promise<void> {
436
+ await this.rebalanceActionStore.update(id, { status: 'failed' });
437
+ this.logger.info({ id }, 'Action failed');
438
+ }
439
+
440
+ // === Debug Helpers ===
441
+
442
+ /**
443
+ * Log the contents of all stores.
444
+ * Logs each item separately for full visibility (avoids [Object] truncation).
445
+ */
446
+ async logStoreContents(): Promise<void> {
447
+ const transfers = await this.transferStore.getAll();
448
+ const intents = await this.rebalanceIntentStore.getAll();
449
+ const actions = await this.rebalanceActionStore.getAll();
450
+
451
+ const activeIntents = intents.filter((i) =>
452
+ ['not_started', 'in_progress'].includes(i.status),
453
+ );
454
+ const inProgressTransfers = transfers.filter(
455
+ (t) => t.status === 'in_progress',
456
+ );
457
+ const inProgressActions = actions.filter((a) => a.status === 'in_progress');
458
+
459
+ // Log summary
460
+ this.logger.info(
461
+ {
462
+ transfers: inProgressTransfers.length,
463
+ intents: activeIntents.length,
464
+ actions: inProgressActions.length,
465
+ },
466
+ 'Store summary',
467
+ );
468
+
469
+ // Log each transfer separately
470
+ for (const t of inProgressTransfers) {
471
+ this.logger.info(
472
+ {
473
+ type: 'transfer',
474
+ origin: t.origin,
475
+ destination: t.destination,
476
+ amount: t.amount.toString(),
477
+ messageId: t.messageId,
478
+ },
479
+ 'In-progress transfer',
480
+ );
481
+ }
482
+
483
+ // Log each intent separately
484
+ for (const i of activeIntents) {
485
+ this.logger.info(
486
+ {
487
+ type: 'intent',
488
+ id: i.id,
489
+ origin: i.origin,
490
+ destination: i.destination,
491
+ amount: i.amount.toString(),
492
+ status: i.status,
493
+ bridge: i.bridge,
494
+ },
495
+ 'Active intent',
496
+ );
497
+ }
498
+
499
+ // Log each action separately
500
+ for (const a of inProgressActions) {
501
+ this.logger.info(
502
+ {
503
+ type: 'action',
504
+ id: a.id,
505
+ origin: a.origin,
506
+ destination: a.destination,
507
+ amount: a.amount.toString(),
508
+ messageId: a.messageId,
509
+ intentId: a.intentId,
510
+ },
511
+ 'In-progress action',
512
+ );
513
+ }
514
+ }
515
+
516
+ // === Private Helpers ===
517
+
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
+ }
528
+
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;
538
+ }
539
+ }
540
+
541
+ private async isMessageDelivered(
542
+ messageId: string,
543
+ destination: Domain,
544
+ providedBlockTag?: ConfirmedBlockTag,
545
+ ): Promise<boolean> {
546
+ try {
547
+ 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 });
553
+
554
+ this.logger.debug(
555
+ { messageId, destination: chainName, blockTag, delivered },
556
+ 'Checked message delivery at confirmed block',
557
+ );
558
+
559
+ return delivered;
560
+ } catch (error) {
561
+ this.logger.warn(
562
+ { messageId, destination, error },
563
+ 'Failed to check message delivery status',
564
+ );
565
+ return false;
566
+ }
567
+ }
568
+
569
+ private async recoverAction(msg: ExplorerMessage): Promise<void> {
570
+ // Check if action already exists
571
+ const existing = await this.rebalanceActionStore.get(msg.msg_id);
572
+ if (existing) {
573
+ this.logger.debug({ id: msg.msg_id }, 'Action already exists, skipping');
574
+ return;
575
+ }
576
+
577
+ this.logger.debug(
578
+ {
579
+ msgId: msg.msg_id,
580
+ origin: msg.origin_domain_id,
581
+ destination: msg.destination_domain_id,
582
+ sender: msg.sender,
583
+ recipient: msg.recipient,
584
+ txHash: msg.origin_tx_hash,
585
+ messageBodyLength: msg.message_body?.length,
586
+ messageBodyPreview: msg.message_body?.substring(0, 66),
587
+ },
588
+ 'Recovering rebalance action',
589
+ );
590
+
591
+ try {
592
+ // Create synthetic intent
593
+ const { amount } = parseWarpRouteMessage(msg.message_body);
594
+ const intent: RebalanceIntent = {
595
+ id: uuidv4(),
596
+ status: 'in_progress',
597
+ origin: msg.origin_domain_id,
598
+ destination: msg.destination_domain_id,
599
+ amount,
600
+ fulfilledAmount: 0n,
601
+ priority: undefined,
602
+ strategyType: undefined,
603
+ createdAt: Date.now(),
604
+ updatedAt: Date.now(),
605
+ };
606
+
607
+ await this.rebalanceIntentStore.save(intent);
608
+ this.logger.debug(
609
+ { id: intent.id, amount: amount.toString() },
610
+ 'Created synthetic RebalanceIntent',
611
+ );
612
+
613
+ // Create action
614
+ const action: RebalanceAction = {
615
+ id: msg.msg_id,
616
+ status: 'in_progress',
617
+ intentId: intent.id,
618
+ messageId: msg.msg_id,
619
+ txHash: msg.origin_tx_hash,
620
+ origin: msg.origin_domain_id,
621
+ destination: msg.destination_domain_id,
622
+ amount,
623
+ createdAt: Date.now(),
624
+ updatedAt: Date.now(),
625
+ };
626
+
627
+ await this.rebalanceActionStore.save(action);
628
+ this.logger.debug(
629
+ { id: action.id, intentId: action.intentId, amount: amount.toString() },
630
+ 'Recovered RebalanceAction',
631
+ );
632
+ } catch (error) {
633
+ this.logger.warn(
634
+ {
635
+ msgId: msg.msg_id,
636
+ messageBody: msg.message_body,
637
+ messageBodyLength: msg.message_body?.length,
638
+ origin: msg.origin_domain_id,
639
+ destination: msg.destination_domain_id,
640
+ txHash: msg.origin_tx_hash,
641
+ error: error instanceof Error ? error.message : String(error),
642
+ },
643
+ 'Failed to parse message body during recovery, skipping action',
644
+ );
645
+ }
646
+ }
647
+ }