@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
@@ -1,37 +1,31 @@
1
- import { type PopulatedTransaction } from 'ethers';
1
+ import { type PopulatedTransaction, type providers } from 'ethers';
2
2
  import { type Logger } from 'pino';
3
3
 
4
4
  import {
5
5
  type ChainMap,
6
6
  type ChainMetadata,
7
+ type ChainName,
8
+ type EthJsonRpcBlockParameterTag,
7
9
  EvmMovableCollateralAdapter,
10
+ HyperlaneCore,
8
11
  type InterchainGasQuote,
9
12
  type MultiProvider,
10
13
  type Token,
11
14
  type WarpCore,
12
15
  } from '@hyperlane-xyz/sdk';
13
- import {
14
- eqAddress,
15
- isNullish,
16
- mapAllSettled,
17
- toWei,
18
- } from '@hyperlane-xyz/utils';
16
+ import { eqAddress, isNullish, mapAllSettled } from '@hyperlane-xyz/utils';
19
17
 
20
18
  import type {
21
19
  IRebalancer,
22
20
  PreparedTransaction,
21
+ RebalanceExecutionResult,
22
+ RebalanceRoute,
23
23
  } from '../interfaces/IRebalancer.js';
24
- import type { RebalancingRoute } from '../interfaces/IStrategy.js';
25
24
  import { type Metrics } from '../metrics/Metrics.js';
26
- import {
27
- type BridgeConfigWithOverride,
28
- getBridgeConfig,
29
- } from '../utils/index.js';
30
25
 
31
26
  export class Rebalancer implements IRebalancer {
32
27
  private readonly logger: Logger;
33
28
  constructor(
34
- private readonly bridges: ChainMap<BridgeConfigWithOverride>,
35
29
  private readonly warpCore: WarpCore,
36
30
  private readonly chainMetadata: ChainMap<ChainMetadata>,
37
31
  private readonly tokensByChainName: ChainMap<Token>,
@@ -42,65 +36,58 @@ export class Rebalancer implements IRebalancer {
42
36
  this.logger = logger.child({ class: Rebalancer.name });
43
37
  }
44
38
 
45
- async rebalance(routes: RebalancingRoute[]): Promise<void> {
39
+ async rebalance(
40
+ routes: RebalanceRoute[],
41
+ ): Promise<RebalanceExecutionResult[]> {
46
42
  if (routes.length === 0) {
47
43
  this.logger.info('No routes to execute, exiting');
48
- return;
44
+ return [];
49
45
  }
50
46
 
51
47
  this.logger.info({ numberOfRoutes: routes.length }, 'Rebalance initiated');
52
48
 
53
- const { preparedTransactions, preparationFailures } =
49
+ const { preparedTransactions, preparationFailureResults } =
54
50
  await this.prepareTransactions(routes);
55
51
 
56
- let gasEstimationFailures = 0;
57
- let transactionFailures = 0;
58
- let successfulTransactions: PreparedTransaction[] = [];
52
+ let executionResults: RebalanceExecutionResult[] = [];
59
53
 
60
54
  if (preparedTransactions.length > 0) {
61
- const filteredTransactions =
62
- this.filterTransactions(preparedTransactions);
63
- if (filteredTransactions.length > 0) {
64
- ({
65
- gasEstimationFailures,
66
- transactionFailures,
67
- successfulTransactions,
68
- } = await this.executeTransactions(filteredTransactions));
55
+ executionResults = await this.executeTransactions(preparedTransactions);
56
+ }
57
+
58
+ // Combine preparation failures with execution results
59
+ const allResults = [...preparationFailureResults, ...executionResults];
60
+
61
+ // Record metrics for successful transactions
62
+ const successfulResults = allResults.filter((r) => r.success);
63
+ if (this.metrics && successfulResults.length > 0) {
64
+ for (const result of successfulResults) {
65
+ const token = this.tokensByChainName[result.route.origin];
66
+ if (token) {
67
+ this.metrics.recordRebalanceAmount(
68
+ result.route,
69
+ token.amount(result.route.amount),
70
+ );
71
+ }
69
72
  }
70
73
  }
71
74
 
72
- if (
73
- preparationFailures > 0 ||
74
- gasEstimationFailures > 0 ||
75
- transactionFailures > 0
76
- ) {
75
+ const failures = allResults.filter((r) => !r.success);
76
+ if (failures.length > 0) {
77
77
  this.logger.error(
78
- {
79
- preparationFailures,
80
- gasEstimationFailures,
81
- transactionFailures,
82
- },
83
- 'A rebalance stage failed.',
78
+ { failureCount: failures.length, totalRoutes: routes.length },
79
+ 'Some rebalance operations failed.',
84
80
  );
85
- throw new Error('❌ Some rebalance transaction failed');
86
- }
87
-
88
- if (this.metrics && successfulTransactions.length > 0) {
89
- for (const transaction of successfulTransactions) {
90
- this.metrics.recordRebalanceAmount(
91
- transaction.route,
92
- transaction.originTokenAmount,
93
- );
94
- }
81
+ } else {
82
+ this.logger.info('✅ Rebalance successful');
95
83
  }
96
84
 
97
- this.logger.info('✅ Rebalance successful');
98
- return;
85
+ return allResults;
99
86
  }
100
87
 
101
- private async prepareTransactions(routes: RebalancingRoute[]): Promise<{
88
+ private async prepareTransactions(routes: RebalanceRoute[]): Promise<{
102
89
  preparedTransactions: PreparedTransaction[];
103
- preparationFailures: number;
90
+ preparationFailureResults: RebalanceExecutionResult[];
104
91
  }> {
105
92
  this.logger.info(
106
93
  { numRoutes: routes.length },
@@ -116,15 +103,32 @@ export class Rebalancer implements IRebalancer {
116
103
  const preparedTransactions = Array.from(fulfilled.values()).filter(
117
104
  (tx): tx is PreparedTransaction => !isNullish(tx),
118
105
  );
119
- // Count rejections + null results as failures
120
- const preparationFailures =
121
- rejected.size + (fulfilled.size - preparedTransactions.length);
122
106
 
123
- return { preparedTransactions, preparationFailures };
107
+ // Create failure results for tracking
108
+ const preparationFailureResults: RebalanceExecutionResult[] = [];
109
+ for (const [i, error] of rejected) {
110
+ preparationFailureResults.push({
111
+ route: routes[i],
112
+ success: false,
113
+ error: String(error),
114
+ });
115
+ }
116
+ // Also track null results (validation failures)
117
+ Array.from(fulfilled.entries()).forEach(([i, tx]) => {
118
+ if (isNullish(tx)) {
119
+ preparationFailureResults.push({
120
+ route: routes[i],
121
+ success: false,
122
+ error: 'Preparation returned null',
123
+ });
124
+ }
125
+ });
126
+
127
+ return { preparedTransactions, preparationFailureResults };
124
128
  }
125
129
 
126
130
  private async prepareTransaction(
127
- route: RebalancingRoute,
131
+ route: RebalanceRoute,
128
132
  ): Promise<PreparedTransaction | null> {
129
133
  const { origin, destination, amount } = route;
130
134
 
@@ -153,12 +157,8 @@ export class Rebalancer implements IRebalancer {
153
157
  const originHypAdapter = originToken.getHypAdapter(
154
158
  this.warpCore.multiProvider,
155
159
  ) as EvmMovableCollateralAdapter;
156
- const { bridge, bridgeIsWarp } = getBridgeConfig(
157
- this.bridges,
158
- origin,
159
- destination,
160
- this.logger,
161
- );
160
+
161
+ const { bridge } = route;
162
162
 
163
163
  // 2. Get quotes
164
164
  let quotes: InterchainGasQuote[];
@@ -168,7 +168,6 @@ export class Rebalancer implements IRebalancer {
168
168
  destinationChainMeta.domainId,
169
169
  destinationToken.addressOrDenom,
170
170
  amount,
171
- bridgeIsWarp,
172
171
  );
173
172
  } catch (error) {
174
173
  this.logger.error(
@@ -210,7 +209,7 @@ export class Rebalancer implements IRebalancer {
210
209
  return { populatedTx, route, originTokenAmount };
211
210
  }
212
211
 
213
- private async validateRoute(route: RebalancingRoute): Promise<boolean> {
212
+ private async validateRoute(route: RebalanceRoute): Promise<boolean> {
214
213
  const { origin, destination, amount } = route;
215
214
  const originToken = this.tokensByChainName[origin];
216
215
  const destinationToken = this.tokensByChainName[destination];
@@ -296,12 +295,8 @@ export class Rebalancer implements IRebalancer {
296
295
  return false;
297
296
  }
298
297
 
299
- const { bridge } = getBridgeConfig(
300
- this.bridges,
301
- origin,
302
- destination,
303
- this.logger,
304
- );
298
+ const { bridge } = route;
299
+
305
300
  if (
306
301
  !(await originHypAdapter.isBridgeAllowed(
307
302
  destinationDomain.domainId,
@@ -327,149 +322,244 @@ export class Rebalancer implements IRebalancer {
327
322
 
328
323
  private async executeTransactions(
329
324
  transactions: PreparedTransaction[],
330
- ): Promise<{
331
- gasEstimationFailures: number;
332
- transactionFailures: number;
333
- successfulTransactions: PreparedTransaction[];
334
- }> {
325
+ ): Promise<RebalanceExecutionResult[]> {
335
326
  this.logger.info(
336
327
  { numTransactions: transactions.length },
337
328
  'Estimating gas for all prepared transactions.',
338
329
  );
339
330
 
340
- // 1. Estimate gas
341
- const { fulfilled, rejected } = await mapAllSettled(
342
- transactions,
343
- async (transaction) => {
331
+ const results: RebalanceExecutionResult[] = [];
332
+
333
+ // 1. Estimate gas for rebalance transactions
334
+ const gasEstimateResults = await Promise.allSettled(
335
+ transactions.map(async (transaction) => {
344
336
  await this.multiProvider.estimateGas(
345
337
  transaction.route.origin,
346
338
  transaction.populatedTx,
347
339
  );
348
340
  return transaction;
349
- },
350
- (_, i) => i,
341
+ }),
351
342
  );
352
343
 
353
- // 2. Filter out failed transactions and log errors
354
- const validTransactions = Array.from(fulfilled.values());
355
- const gasEstimationFailures = rejected.size;
356
- for (const [i, error] of rejected) {
357
- const failedTransaction = transactions[i];
358
- this.logger.error(
359
- {
360
- origin: failedTransaction.route.origin,
361
- destination: failedTransaction.route.destination,
362
- amount:
363
- failedTransaction.originTokenAmount.getDecimalFormattedAmount(),
364
- tokenName: failedTransaction.originTokenAmount.token.name,
365
- error,
366
- },
367
- 'Gas estimation failed for route.',
368
- );
369
- }
344
+ // 2. Filter out failed transactions and track failures
345
+ const validTransactions: PreparedTransaction[] = [];
346
+ gasEstimateResults.forEach((result, i) => {
347
+ if (result.status === 'fulfilled') {
348
+ validTransactions.push(result.value);
349
+ } else {
350
+ const failedTransaction = transactions[i];
351
+ this.logger.error(
352
+ {
353
+ origin: failedTransaction.route.origin,
354
+ destination: failedTransaction.route.destination,
355
+ amount:
356
+ failedTransaction.originTokenAmount.getDecimalFormattedAmount(),
357
+ tokenName: failedTransaction.originTokenAmount.token.name,
358
+ error: result.reason,
359
+ },
360
+ 'Gas estimation failed for route.',
361
+ );
362
+ results.push({
363
+ route: failedTransaction.route,
364
+ success: false,
365
+ error: `Gas estimation failed: ${String(result.reason)}`,
366
+ });
367
+ }
368
+ });
370
369
 
371
370
  if (validTransactions.length === 0) {
372
371
  this.logger.info('No transactions to execute after gas estimation.');
373
- return {
374
- gasEstimationFailures,
375
- transactionFailures: 0,
376
- successfulTransactions: [],
377
- };
372
+ return results;
378
373
  }
379
374
 
380
- // 2. Send transactions
375
+ // 3. Group transactions by origin chain
376
+ const txsByOrigin = new Map<ChainName, PreparedTransaction[]>();
377
+ for (const tx of validTransactions) {
378
+ const origin = tx.route.origin;
379
+ if (!txsByOrigin.has(origin)) {
380
+ txsByOrigin.set(origin, []);
381
+ }
382
+ txsByOrigin.get(origin)!.push(tx);
383
+ }
384
+
385
+ // 4. Send transactions - parallel across chains, sequential within each chain
381
386
  this.logger.info(
382
- { numTransactions: validTransactions.length },
383
- 'Sending valid transactions.',
387
+ {
388
+ numChains: txsByOrigin.size,
389
+ numTransactions: validTransactions.length,
390
+ },
391
+ 'Sending transactions (parallel across chains, sequential within chain).',
392
+ );
393
+
394
+ const chainSendResults = await Promise.allSettled(
395
+ Array.from(txsByOrigin.entries()).map(([origin, txs]) =>
396
+ this.sendTransactionsForChain(origin, txs),
397
+ ),
384
398
  );
385
- let transactionFailures = 0;
386
- const successfulTransactions: PreparedTransaction[] = [];
387
- for (const transaction of validTransactions) {
399
+
400
+ // 5. Collect successful sends and record send failures
401
+ const successfulSends: Array<{
402
+ transaction: PreparedTransaction;
403
+ receipt: providers.TransactionReceipt;
404
+ }> = [];
405
+
406
+ chainSendResults.forEach((chainResult) => {
407
+ if (chainResult.status === 'fulfilled') {
408
+ for (const txResult of chainResult.value) {
409
+ if ('receipt' in txResult) {
410
+ successfulSends.push(txResult);
411
+ } else {
412
+ results.push({
413
+ route: txResult.transaction.route,
414
+ success: false,
415
+ error: `Transaction send failed: ${txResult.error}`,
416
+ });
417
+ this.metrics?.recordActionAttempt(
418
+ txResult.transaction.route,
419
+ false,
420
+ );
421
+ }
422
+ }
423
+ } else {
424
+ // This shouldn't happen since sendTransactionsForChain catches errors internally,
425
+ // but handle it just in case
426
+ this.logger.error(
427
+ { error: chainResult.reason },
428
+ 'Unexpected error during chain transaction sending.',
429
+ );
430
+ }
431
+ });
432
+
433
+ // 6. Build results from confirmed receipts
434
+ for (const { transaction, receipt } of successfulSends) {
435
+ const result = this.buildResult(transaction, receipt);
436
+ results.push(result);
437
+ this.metrics?.recordActionAttempt(result.route, result.success);
438
+ }
439
+
440
+ return results;
441
+ }
442
+
443
+ // === Parallel Transaction Sending Methods ===
444
+
445
+ /**
446
+ * Send all transactions for a single origin chain sequentially.
447
+ * Sequential sending is required to avoid nonce contention when using the same signing key.
448
+ */
449
+ private async sendTransactionsForChain(
450
+ origin: ChainName,
451
+ transactions: PreparedTransaction[],
452
+ ): Promise<
453
+ Array<
454
+ | {
455
+ transaction: PreparedTransaction;
456
+ receipt: providers.TransactionReceipt;
457
+ }
458
+ | { transaction: PreparedTransaction; error: string }
459
+ >
460
+ > {
461
+ const results: Array<
462
+ | {
463
+ transaction: PreparedTransaction;
464
+ receipt: providers.TransactionReceipt;
465
+ }
466
+ | { transaction: PreparedTransaction; error: string }
467
+ > = [];
468
+
469
+ // Send sequentially to avoid nonce contention
470
+ for (const transaction of transactions) {
388
471
  try {
389
- const { origin, destination } = transaction.route;
390
472
  const decimalFormattedAmount =
391
473
  transaction.originTokenAmount.getDecimalFormattedAmount();
392
474
  const tokenName = transaction.originTokenAmount.token.name;
475
+
476
+ const reorgPeriod = this.getReorgPeriod(origin);
477
+
393
478
  this.logger.info(
394
479
  {
395
480
  origin,
396
- destination,
481
+ destination: transaction.route.destination,
397
482
  amount: decimalFormattedAmount,
398
483
  tokenName,
484
+ reorgPeriod,
399
485
  },
400
- 'Sending transaction for route.',
486
+ 'Sending rebalance transaction and waiting for reorgPeriod confirmations.',
401
487
  );
488
+
402
489
  const receipt = await this.multiProvider.sendTransaction(
403
490
  origin,
404
491
  transaction.populatedTx,
492
+ {
493
+ waitConfirmations: reorgPeriod as
494
+ | number
495
+ | EthJsonRpcBlockParameterTag,
496
+ },
405
497
  );
498
+
406
499
  this.logger.info(
407
500
  {
408
501
  origin,
409
- destination,
502
+ destination: transaction.route.destination,
410
503
  amount: decimalFormattedAmount,
411
504
  tokenName,
412
505
  txHash: receipt.transactionHash,
413
506
  },
414
- 'Transaction confirmed for route.',
507
+ 'Rebalance transaction confirmed at reorgPeriod depth.',
415
508
  );
416
- successfulTransactions.push(transaction);
509
+
510
+ results.push({ transaction, receipt });
417
511
  } catch (error) {
418
- transactionFailures++;
419
512
  this.logger.error(
420
513
  {
421
- origin: transaction.route.origin,
514
+ origin,
422
515
  destination: transaction.route.destination,
423
516
  amount: transaction.originTokenAmount.getDecimalFormattedAmount(),
424
517
  tokenName: transaction.originTokenAmount.token.name,
425
518
  error,
426
519
  },
427
- 'Transaction failed for route.',
520
+ 'Transaction send failed for route.',
428
521
  );
522
+ results.push({ transaction, error: String(error) });
429
523
  }
430
524
  }
431
525
 
432
- return {
433
- gasEstimationFailures,
434
- transactionFailures,
435
- successfulTransactions,
436
- };
526
+ return results;
437
527
  }
438
528
 
439
- private filterTransactions(
440
- transactions: PreparedTransaction[],
441
- ): PreparedTransaction[] {
442
- const filteredTransactions: PreparedTransaction[] = [];
443
- for (const transaction of transactions) {
444
- const { origin, destination, amount } = transaction.route;
445
- const originToken = this.tokensByChainName[origin];
446
- const decimalFormattedAmount =
447
- transaction.originTokenAmount.getDecimalFormattedAmount();
448
-
449
- // minimum amount check
450
- const { bridgeMinAcceptedAmount } = getBridgeConfig(
451
- this.bridges,
452
- origin,
453
- destination,
454
- this.logger,
455
- );
456
- const minAccepted = BigInt(
457
- toWei(bridgeMinAcceptedAmount, originToken.decimals),
529
+ /**
530
+ * Build the execution result from a confirmed transaction receipt.
531
+ * Receipt is already confirmed at reorgPeriod depth from sendTransaction.
532
+ */
533
+ private buildResult(
534
+ transaction: PreparedTransaction,
535
+ receipt: providers.TransactionReceipt,
536
+ ): RebalanceExecutionResult {
537
+ const { origin, destination } = transaction.route;
538
+ const dispatchedMessages = HyperlaneCore.getDispatchedMessages(receipt);
539
+
540
+ if (dispatchedMessages.length === 0) {
541
+ this.logger.error(
542
+ { origin, destination, txHash: receipt.transactionHash },
543
+ 'No Dispatch event found in confirmed rebalance receipt',
458
544
  );
459
- if (minAccepted > amount) {
460
- this.logger.info(
461
- {
462
- origin,
463
- destination,
464
- amount: decimalFormattedAmount,
465
- tokenName: originToken.name,
466
- },
467
- 'Route skipped due to minimum threshold amount not met.',
468
- );
469
- continue;
470
- }
471
- filteredTransactions.push(transaction);
545
+ return {
546
+ route: transaction.route,
547
+ success: false,
548
+ error: `Transaction confirmed but no Dispatch event found`,
549
+ txHash: receipt.transactionHash,
550
+ };
472
551
  }
473
- return filteredTransactions;
552
+
553
+ return {
554
+ route: transaction.route,
555
+ success: true,
556
+ messageId: dispatchedMessages[0].id,
557
+ txHash: receipt.transactionHash,
558
+ };
559
+ }
560
+
561
+ private getReorgPeriod(chainName: string): number | string {
562
+ const metadata = this.multiProvider.getChainMetadata(chainName);
563
+ return metadata.blocks?.reorgPeriod ?? 32;
474
564
  }
475
565
  }