@hyperlane-xyz/rebalancer 0.1.0-beta.5a8bd28ab

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 (202) hide show
  1. package/README.md +178 -0
  2. package/dist/config/RebalancerConfig.d.ts +12 -0
  3. package/dist/config/RebalancerConfig.d.ts.map +1 -0
  4. package/dist/config/RebalancerConfig.js +29 -0
  5. package/dist/config/RebalancerConfig.js.map +1 -0
  6. package/dist/config/RebalancerConfig.test.d.ts +2 -0
  7. package/dist/config/RebalancerConfig.test.d.ts.map +1 -0
  8. package/dist/config/RebalancerConfig.test.js +325 -0
  9. package/dist/config/RebalancerConfig.test.js.map +1 -0
  10. package/dist/core/Rebalancer.d.ts +23 -0
  11. package/dist/core/Rebalancer.d.ts.map +1 -0
  12. package/dist/core/Rebalancer.js +290 -0
  13. package/dist/core/Rebalancer.js.map +1 -0
  14. package/dist/core/RebalancerService.d.ts +115 -0
  15. package/dist/core/RebalancerService.d.ts.map +1 -0
  16. package/dist/core/RebalancerService.js +227 -0
  17. package/dist/core/RebalancerService.js.map +1 -0
  18. package/dist/core/WithInflightGuard.d.ts +20 -0
  19. package/dist/core/WithInflightGuard.d.ts.map +1 -0
  20. package/dist/core/WithInflightGuard.js +47 -0
  21. package/dist/core/WithInflightGuard.js.map +1 -0
  22. package/dist/core/WithInflightGuard.test.d.ts +2 -0
  23. package/dist/core/WithInflightGuard.test.d.ts.map +1 -0
  24. package/dist/core/WithInflightGuard.test.js +64 -0
  25. package/dist/core/WithInflightGuard.test.js.map +1 -0
  26. package/dist/core/WithSemaphore.d.ts +22 -0
  27. package/dist/core/WithSemaphore.d.ts.map +1 -0
  28. package/dist/core/WithSemaphore.js +67 -0
  29. package/dist/core/WithSemaphore.js.map +1 -0
  30. package/dist/core/WithSemaphore.test.d.ts +2 -0
  31. package/dist/core/WithSemaphore.test.d.ts.map +1 -0
  32. package/dist/core/WithSemaphore.test.js +83 -0
  33. package/dist/core/WithSemaphore.test.js.map +1 -0
  34. package/dist/factories/RebalancerContextFactory.d.ts +41 -0
  35. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -0
  36. package/dist/factories/RebalancerContextFactory.js +115 -0
  37. package/dist/factories/RebalancerContextFactory.js.map +1 -0
  38. package/dist/index.d.ts +33 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +35 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/interfaces/IMetrics.d.ts +5 -0
  43. package/dist/interfaces/IMetrics.d.ts.map +1 -0
  44. package/dist/interfaces/IMetrics.js +2 -0
  45. package/dist/interfaces/IMetrics.js.map +1 -0
  46. package/dist/interfaces/IMonitor.d.ts +51 -0
  47. package/dist/interfaces/IMonitor.d.ts.map +1 -0
  48. package/dist/interfaces/IMonitor.js +14 -0
  49. package/dist/interfaces/IMonitor.js.map +1 -0
  50. package/dist/interfaces/IRebalancer.d.ts +15 -0
  51. package/dist/interfaces/IRebalancer.d.ts.map +1 -0
  52. package/dist/interfaces/IRebalancer.js +2 -0
  53. package/dist/interfaces/IRebalancer.js.map +1 -0
  54. package/dist/interfaces/IStrategy.d.ts +11 -0
  55. package/dist/interfaces/IStrategy.d.ts.map +1 -0
  56. package/dist/interfaces/IStrategy.js +2 -0
  57. package/dist/interfaces/IStrategy.js.map +1 -0
  58. package/dist/metrics/Metrics.d.ts +31 -0
  59. package/dist/metrics/Metrics.d.ts.map +1 -0
  60. package/dist/metrics/Metrics.js +302 -0
  61. package/dist/metrics/Metrics.js.map +1 -0
  62. package/dist/metrics/PriceGetter.d.ts +10 -0
  63. package/dist/metrics/PriceGetter.d.ts.map +1 -0
  64. package/dist/metrics/PriceGetter.js +41 -0
  65. package/dist/metrics/PriceGetter.js.map +1 -0
  66. package/dist/metrics/scripts/metrics.d.ts +14 -0
  67. package/dist/metrics/scripts/metrics.d.ts.map +1 -0
  68. package/dist/metrics/scripts/metrics.js +198 -0
  69. package/dist/metrics/scripts/metrics.js.map +1 -0
  70. package/dist/metrics/types.d.ts +24 -0
  71. package/dist/metrics/types.d.ts.map +1 -0
  72. package/dist/metrics/types.js +2 -0
  73. package/dist/metrics/types.js.map +1 -0
  74. package/dist/metrics/utils/metrics.d.ts +12 -0
  75. package/dist/metrics/utils/metrics.d.ts.map +1 -0
  76. package/dist/metrics/utils/metrics.js +28 -0
  77. package/dist/metrics/utils/metrics.js.map +1 -0
  78. package/dist/monitor/Monitor.d.ts +26 -0
  79. package/dist/monitor/Monitor.d.ts.map +1 -0
  80. package/dist/monitor/Monitor.js +116 -0
  81. package/dist/monitor/Monitor.js.map +1 -0
  82. package/dist/service.d.ts +3 -0
  83. package/dist/service.d.ts.map +1 -0
  84. package/dist/service.js +125 -0
  85. package/dist/service.js.map +1 -0
  86. package/dist/strategy/BaseStrategy.d.ts +34 -0
  87. package/dist/strategy/BaseStrategy.d.ts.map +1 -0
  88. package/dist/strategy/BaseStrategy.js +127 -0
  89. package/dist/strategy/BaseStrategy.js.map +1 -0
  90. package/dist/strategy/MinAmountStrategy.d.ts +27 -0
  91. package/dist/strategy/MinAmountStrategy.d.ts.map +1 -0
  92. package/dist/strategy/MinAmountStrategy.js +103 -0
  93. package/dist/strategy/MinAmountStrategy.js.map +1 -0
  94. package/dist/strategy/MinAmountStrategy.test.d.ts +2 -0
  95. package/dist/strategy/MinAmountStrategy.test.d.ts.map +1 -0
  96. package/dist/strategy/MinAmountStrategy.test.js +472 -0
  97. package/dist/strategy/MinAmountStrategy.test.js.map +1 -0
  98. package/dist/strategy/StrategyFactory.d.ts +16 -0
  99. package/dist/strategy/StrategyFactory.d.ts.map +1 -0
  100. package/dist/strategy/StrategyFactory.js +25 -0
  101. package/dist/strategy/StrategyFactory.js.map +1 -0
  102. package/dist/strategy/StrategyFactory.test.d.ts +2 -0
  103. package/dist/strategy/StrategyFactory.test.d.ts.map +1 -0
  104. package/dist/strategy/StrategyFactory.test.js +80 -0
  105. package/dist/strategy/StrategyFactory.test.js.map +1 -0
  106. package/dist/strategy/WeightedStrategy.d.ts +23 -0
  107. package/dist/strategy/WeightedStrategy.d.ts.map +1 -0
  108. package/dist/strategy/WeightedStrategy.js +61 -0
  109. package/dist/strategy/WeightedStrategy.js.map +1 -0
  110. package/dist/strategy/WeightedStrategy.test.d.ts +2 -0
  111. package/dist/strategy/WeightedStrategy.test.d.ts.map +1 -0
  112. package/dist/strategy/WeightedStrategy.test.js +307 -0
  113. package/dist/strategy/WeightedStrategy.test.js.map +1 -0
  114. package/dist/strategy/index.d.ts +5 -0
  115. package/dist/strategy/index.d.ts.map +1 -0
  116. package/dist/strategy/index.js +5 -0
  117. package/dist/strategy/index.js.map +1 -0
  118. package/dist/test/helpers.d.ts +8 -0
  119. package/dist/test/helpers.d.ts.map +1 -0
  120. package/dist/test/helpers.js +33 -0
  121. package/dist/test/helpers.js.map +1 -0
  122. package/dist/utils/ExplorerClient.d.ts +14 -0
  123. package/dist/utils/ExplorerClient.d.ts.map +1 -0
  124. package/dist/utils/ExplorerClient.js +82 -0
  125. package/dist/utils/ExplorerClient.js.map +1 -0
  126. package/dist/utils/balanceUtils.d.ts +13 -0
  127. package/dist/utils/balanceUtils.d.ts.map +1 -0
  128. package/dist/utils/balanceUtils.js +43 -0
  129. package/dist/utils/balanceUtils.js.map +1 -0
  130. package/dist/utils/balanceUtils.test.d.ts +2 -0
  131. package/dist/utils/balanceUtils.test.d.ts.map +1 -0
  132. package/dist/utils/balanceUtils.test.js +54 -0
  133. package/dist/utils/balanceUtils.test.js.map +1 -0
  134. package/dist/utils/bridgeUtils.d.ts +19 -0
  135. package/dist/utils/bridgeUtils.d.ts.map +1 -0
  136. package/dist/utils/bridgeUtils.js +20 -0
  137. package/dist/utils/bridgeUtils.js.map +1 -0
  138. package/dist/utils/bridgeUtils.test.d.ts +2 -0
  139. package/dist/utils/bridgeUtils.test.d.ts.map +1 -0
  140. package/dist/utils/bridgeUtils.test.js +77 -0
  141. package/dist/utils/bridgeUtils.test.js.map +1 -0
  142. package/dist/utils/errors.d.ts +4 -0
  143. package/dist/utils/errors.d.ts.map +1 -0
  144. package/dist/utils/errors.js +6 -0
  145. package/dist/utils/errors.js.map +1 -0
  146. package/dist/utils/files.d.ts +35 -0
  147. package/dist/utils/files.d.ts.map +1 -0
  148. package/dist/utils/files.js +190 -0
  149. package/dist/utils/files.js.map +1 -0
  150. package/dist/utils/generalUtils.d.ts +3 -0
  151. package/dist/utils/generalUtils.d.ts.map +1 -0
  152. package/dist/utils/generalUtils.js +9 -0
  153. package/dist/utils/generalUtils.js.map +1 -0
  154. package/dist/utils/index.d.ts +5 -0
  155. package/dist/utils/index.d.ts.map +1 -0
  156. package/dist/utils/index.js +5 -0
  157. package/dist/utils/index.js.map +1 -0
  158. package/dist/utils/tokenUtils.d.ts +14 -0
  159. package/dist/utils/tokenUtils.d.ts.map +1 -0
  160. package/dist/utils/tokenUtils.js +21 -0
  161. package/dist/utils/tokenUtils.js.map +1 -0
  162. package/package.json +70 -0
  163. package/src/config/RebalancerConfig.test.ts +388 -0
  164. package/src/config/RebalancerConfig.ts +39 -0
  165. package/src/core/Rebalancer.ts +471 -0
  166. package/src/core/RebalancerService.ts +333 -0
  167. package/src/core/WithInflightGuard.test.ts +131 -0
  168. package/src/core/WithInflightGuard.ts +67 -0
  169. package/src/core/WithSemaphore.test.ts +112 -0
  170. package/src/core/WithSemaphore.ts +92 -0
  171. package/src/factories/RebalancerContextFactory.ts +210 -0
  172. package/src/index.ts +68 -0
  173. package/src/interfaces/IMetrics.ts +5 -0
  174. package/src/interfaces/IMonitor.ts +63 -0
  175. package/src/interfaces/IRebalancer.ts +20 -0
  176. package/src/interfaces/IStrategy.ts +13 -0
  177. package/src/metrics/Metrics.ts +558 -0
  178. package/src/metrics/PriceGetter.ts +74 -0
  179. package/src/metrics/scripts/metrics.ts +298 -0
  180. package/src/metrics/types.ts +27 -0
  181. package/src/metrics/utils/metrics.ts +33 -0
  182. package/src/monitor/Monitor.ts +174 -0
  183. package/src/service.ts +154 -0
  184. package/src/strategy/BaseStrategy.ts +210 -0
  185. package/src/strategy/MinAmountStrategy.test.ts +625 -0
  186. package/src/strategy/MinAmountStrategy.ts +170 -0
  187. package/src/strategy/StrategyFactory.test.ts +109 -0
  188. package/src/strategy/StrategyFactory.ts +48 -0
  189. package/src/strategy/WeightedStrategy.test.ts +408 -0
  190. package/src/strategy/WeightedStrategy.ts +93 -0
  191. package/src/strategy/index.ts +4 -0
  192. package/src/test/helpers.ts +46 -0
  193. package/src/utils/ExplorerClient.ts +99 -0
  194. package/src/utils/balanceUtils.test.ts +74 -0
  195. package/src/utils/balanceUtils.ts +69 -0
  196. package/src/utils/bridgeUtils.test.ts +92 -0
  197. package/src/utils/bridgeUtils.ts +42 -0
  198. package/src/utils/errors.ts +5 -0
  199. package/src/utils/files.ts +276 -0
  200. package/src/utils/generalUtils.ts +13 -0
  201. package/src/utils/index.ts +4 -0
  202. package/src/utils/tokenUtils.ts +26 -0
@@ -0,0 +1,471 @@
1
+ import { PopulatedTransaction } from 'ethers';
2
+ import { Logger } from 'pino';
3
+
4
+ import {
5
+ type ChainMap,
6
+ type ChainMetadata,
7
+ EvmMovableCollateralAdapter,
8
+ InterchainGasQuote,
9
+ type MultiProvider,
10
+ type Token,
11
+ type WarpCore,
12
+ } from '@hyperlane-xyz/sdk';
13
+ import { eqAddress, toWei } from '@hyperlane-xyz/utils';
14
+
15
+ import type {
16
+ IRebalancer,
17
+ PreparedTransaction,
18
+ } from '../interfaces/IRebalancer.js';
19
+ import type { RebalancingRoute } from '../interfaces/IStrategy.js';
20
+ import { Metrics } from '../metrics/Metrics.js';
21
+ import {
22
+ type BridgeConfigWithOverride,
23
+ getBridgeConfig,
24
+ } from '../utils/index.js';
25
+
26
+ export class Rebalancer implements IRebalancer {
27
+ private readonly logger: Logger;
28
+ constructor(
29
+ private readonly bridges: ChainMap<BridgeConfigWithOverride>,
30
+ private readonly warpCore: WarpCore,
31
+ private readonly chainMetadata: ChainMap<ChainMetadata>,
32
+ private readonly tokensByChainName: ChainMap<Token>,
33
+ private readonly multiProvider: MultiProvider,
34
+ logger: Logger,
35
+ private readonly metrics?: Metrics,
36
+ ) {
37
+ this.logger = logger.child({ class: Rebalancer.name });
38
+ }
39
+
40
+ async rebalance(routes: RebalancingRoute[]): Promise<void> {
41
+ if (routes.length === 0) {
42
+ this.logger.info('No routes to execute, exiting');
43
+ return;
44
+ }
45
+
46
+ this.logger.info({ numberOfRoutes: routes.length }, 'Rebalance initiated');
47
+
48
+ const { preparedTransactions, preparationFailures } =
49
+ await this.prepareTransactions(routes);
50
+
51
+ let gasEstimationFailures = 0;
52
+ let transactionFailures = 0;
53
+ let successfulTransactions: PreparedTransaction[] = [];
54
+
55
+ if (preparedTransactions.length > 0) {
56
+ const filteredTransactions =
57
+ this.filterTransactions(preparedTransactions);
58
+ if (filteredTransactions.length > 0) {
59
+ ({
60
+ gasEstimationFailures,
61
+ transactionFailures,
62
+ successfulTransactions,
63
+ } = await this.executeTransactions(filteredTransactions));
64
+ }
65
+ }
66
+
67
+ if (
68
+ preparationFailures > 0 ||
69
+ gasEstimationFailures > 0 ||
70
+ transactionFailures > 0
71
+ ) {
72
+ this.logger.error(
73
+ {
74
+ preparationFailures,
75
+ gasEstimationFailures,
76
+ transactionFailures,
77
+ },
78
+ 'A rebalance stage failed.',
79
+ );
80
+ throw new Error('❌ Some rebalance transaction failed');
81
+ }
82
+
83
+ if (this.metrics && successfulTransactions.length > 0) {
84
+ for (const transaction of successfulTransactions) {
85
+ this.metrics.recordRebalanceAmount(
86
+ transaction.route,
87
+ transaction.originTokenAmount,
88
+ );
89
+ }
90
+ }
91
+
92
+ this.logger.info('✅ Rebalance successful');
93
+ return;
94
+ }
95
+
96
+ private async prepareTransactions(routes: RebalancingRoute[]): Promise<{
97
+ preparedTransactions: PreparedTransaction[];
98
+ preparationFailures: number;
99
+ }> {
100
+ this.logger.info(
101
+ { numRoutes: routes.length },
102
+ 'Preparing all rebalance transactions.',
103
+ );
104
+ const settledResults = await Promise.allSettled(
105
+ routes.map((route) => this.prepareTransaction(route)),
106
+ );
107
+
108
+ const preparedTransactions: PreparedTransaction[] = [];
109
+ for (const result of settledResults) {
110
+ if (result.status === 'fulfilled' && result.value) {
111
+ preparedTransactions.push(result.value);
112
+ }
113
+ }
114
+ const preparationFailures = routes.length - preparedTransactions.length;
115
+
116
+ return { preparedTransactions, preparationFailures };
117
+ }
118
+
119
+ private async prepareTransaction(
120
+ route: RebalancingRoute,
121
+ ): Promise<PreparedTransaction | null> {
122
+ const { origin, destination, amount } = route;
123
+
124
+ this.logger.info(
125
+ {
126
+ origin,
127
+ destination,
128
+ amount,
129
+ },
130
+ 'Preparing transaction for route',
131
+ );
132
+
133
+ // 1. Adapter and permissions validation
134
+ if (!(await this.validateRoute(route))) {
135
+ // Errors logged in validateRoute
136
+ return null;
137
+ }
138
+
139
+ const originToken = this.tokensByChainName[origin];
140
+ const destinationToken = this.tokensByChainName[destination];
141
+ const destinationChainMeta = this.chainMetadata[destination];
142
+
143
+ const originTokenAmount = originToken.amount(amount);
144
+ const decimalFormattedAmount =
145
+ originTokenAmount.getDecimalFormattedAmount();
146
+ const originHypAdapter = originToken.getHypAdapter(
147
+ this.warpCore.multiProvider,
148
+ ) as EvmMovableCollateralAdapter;
149
+ const { bridge, bridgeIsWarp } = getBridgeConfig(
150
+ this.bridges,
151
+ origin,
152
+ destination,
153
+ this.logger,
154
+ );
155
+
156
+ // 2. Get quotes
157
+ let quotes: InterchainGasQuote[];
158
+ try {
159
+ quotes = await originHypAdapter.getRebalanceQuotes(
160
+ bridge,
161
+ destinationChainMeta.domainId,
162
+ destinationToken.addressOrDenom,
163
+ amount,
164
+ bridgeIsWarp,
165
+ );
166
+ } catch (error) {
167
+ this.logger.error(
168
+ {
169
+ origin,
170
+ destination,
171
+ amount: decimalFormattedAmount,
172
+ tokenName: originToken.name,
173
+ error,
174
+ },
175
+ 'Failed to get quotes for route.',
176
+ );
177
+ return null;
178
+ }
179
+
180
+ // 3. Populate transaction
181
+ let populatedTx: PopulatedTransaction;
182
+ try {
183
+ populatedTx = await originHypAdapter.populateRebalanceTx(
184
+ destinationChainMeta.domainId,
185
+ amount,
186
+ bridge,
187
+ quotes,
188
+ );
189
+ } catch (error) {
190
+ this.logger.error(
191
+ {
192
+ origin,
193
+ destination,
194
+ amount: decimalFormattedAmount,
195
+ tokenName: originToken.name,
196
+ error,
197
+ },
198
+ 'Failed to populate transaction for route.',
199
+ );
200
+ return null;
201
+ }
202
+
203
+ return { populatedTx, route, originTokenAmount };
204
+ }
205
+
206
+ private async validateRoute(route: RebalancingRoute): Promise<boolean> {
207
+ const { origin, destination, amount } = route;
208
+ const originToken = this.tokensByChainName[origin];
209
+ const destinationToken = this.tokensByChainName[destination];
210
+ const destinationDomain = this.chainMetadata[destination];
211
+
212
+ if (!originToken) {
213
+ this.logger.error(
214
+ { origin, destination, amount },
215
+ 'Route validation failed: origin token not found.',
216
+ );
217
+ return false;
218
+ }
219
+
220
+ const originTokenAmount = originToken.amount(amount);
221
+ const decimalFormattedAmount =
222
+ originTokenAmount.getDecimalFormattedAmount();
223
+
224
+ if (!destinationToken) {
225
+ this.logger.error(
226
+ { origin, destination, amount: decimalFormattedAmount },
227
+ 'Route validation failed: destination token not found.',
228
+ );
229
+ return false;
230
+ }
231
+
232
+ if (!destinationDomain) {
233
+ this.logger.error(
234
+ { origin, destination, amount: decimalFormattedAmount },
235
+ 'Route validation failed: destination domain metadata not found.',
236
+ );
237
+ return false;
238
+ }
239
+
240
+ const originHypAdapter = originToken.getHypAdapter(
241
+ this.warpCore.multiProvider,
242
+ );
243
+ if (!(originHypAdapter instanceof EvmMovableCollateralAdapter)) {
244
+ this.logger.error(
245
+ {
246
+ origin,
247
+ destination,
248
+ amount: decimalFormattedAmount,
249
+ tokenName: originToken.name,
250
+ },
251
+ 'Route validation failed: Origin TokenAdapter is not an EvmHypCollateralAdapter.',
252
+ );
253
+ return false;
254
+ }
255
+
256
+ const signer = this.multiProvider.getSigner(origin);
257
+ const signerAddress = await signer.getAddress();
258
+ if (!(await originHypAdapter.isRebalancer(signerAddress))) {
259
+ this.logger.error(
260
+ {
261
+ origin,
262
+ destination,
263
+ amount: decimalFormattedAmount,
264
+ tokenName: originToken.name,
265
+ tokenAddress: originToken.addressOrDenom,
266
+ signerAddress,
267
+ },
268
+ 'Route validation failed: Signer is not a rebalancer.',
269
+ );
270
+ return false;
271
+ }
272
+
273
+ const allowedDestination = await originHypAdapter.getAllowedDestination(
274
+ destinationDomain.domainId,
275
+ );
276
+ if (!eqAddress(allowedDestination, destinationToken.addressOrDenom)) {
277
+ this.logger.error(
278
+ {
279
+ origin,
280
+ destination,
281
+ amount: decimalFormattedAmount,
282
+ tokenName: originToken.name,
283
+ tokenAddress: originToken.addressOrDenom,
284
+ destinationTokenAddress: destinationToken.addressOrDenom,
285
+ allowedDestinationTokenAddress: allowedDestination,
286
+ },
287
+ 'Route validation failed: Destination is not allowed.',
288
+ );
289
+ return false;
290
+ }
291
+
292
+ const { bridge } = getBridgeConfig(
293
+ this.bridges,
294
+ origin,
295
+ destination,
296
+ this.logger,
297
+ );
298
+ if (
299
+ !(await originHypAdapter.isBridgeAllowed(
300
+ destinationDomain.domainId,
301
+ bridge,
302
+ ))
303
+ ) {
304
+ this.logger.error(
305
+ {
306
+ origin,
307
+ destination,
308
+ amount: decimalFormattedAmount,
309
+ tokenName: originToken.name,
310
+ tokenAddress: originToken.addressOrDenom,
311
+ bridgeAddress: bridge,
312
+ },
313
+ 'Route validation failed: Bridge is not allowed.',
314
+ );
315
+ return false;
316
+ }
317
+
318
+ return true;
319
+ }
320
+
321
+ private async executeTransactions(
322
+ transactions: PreparedTransaction[],
323
+ ): Promise<{
324
+ gasEstimationFailures: number;
325
+ transactionFailures: number;
326
+ successfulTransactions: PreparedTransaction[];
327
+ }> {
328
+ this.logger.info(
329
+ { numTransactions: transactions.length },
330
+ 'Estimating gas for all prepared transactions.',
331
+ );
332
+
333
+ // 1. Estimate gas
334
+ const gasEstimateResults = await Promise.allSettled(
335
+ transactions.map(async (transaction) => {
336
+ await this.multiProvider.estimateGas(
337
+ transaction.route.origin,
338
+ transaction.populatedTx,
339
+ );
340
+ return transaction;
341
+ }),
342
+ );
343
+
344
+ // 2. Filter out failed transactions and log errors
345
+ const validTransactions: PreparedTransaction[] = [];
346
+ let gasEstimationFailures = 0;
347
+ gasEstimateResults.forEach((result, i) => {
348
+ if (result.status === 'fulfilled') {
349
+ validTransactions.push(result.value);
350
+ } else {
351
+ gasEstimationFailures++;
352
+ const failedTransaction = transactions[i];
353
+ this.logger.error(
354
+ {
355
+ origin: failedTransaction.route.origin,
356
+ destination: failedTransaction.route.destination,
357
+ amount:
358
+ failedTransaction.originTokenAmount.getDecimalFormattedAmount(),
359
+ tokenName: failedTransaction.originTokenAmount.token.name,
360
+ error: result.reason,
361
+ },
362
+ 'Gas estimation failed for route.',
363
+ );
364
+ }
365
+ });
366
+
367
+ if (validTransactions.length === 0) {
368
+ this.logger.info('No transactions to execute after gas estimation.');
369
+ return {
370
+ gasEstimationFailures,
371
+ transactionFailures: 0,
372
+ successfulTransactions: [],
373
+ };
374
+ }
375
+
376
+ // 2. Send transactions
377
+ this.logger.info(
378
+ { numTransactions: validTransactions.length },
379
+ 'Sending valid transactions.',
380
+ );
381
+ let transactionFailures = 0;
382
+ const successfulTransactions: PreparedTransaction[] = [];
383
+ for (const transaction of validTransactions) {
384
+ try {
385
+ const { origin, destination } = transaction.route;
386
+ const decimalFormattedAmount =
387
+ transaction.originTokenAmount.getDecimalFormattedAmount();
388
+ const tokenName = transaction.originTokenAmount.token.name;
389
+ this.logger.info(
390
+ {
391
+ origin,
392
+ destination,
393
+ amount: decimalFormattedAmount,
394
+ tokenName,
395
+ },
396
+ 'Sending transaction for route.',
397
+ );
398
+ const receipt = await this.multiProvider.sendTransaction(
399
+ origin,
400
+ transaction.populatedTx,
401
+ );
402
+ this.logger.info(
403
+ {
404
+ origin,
405
+ destination,
406
+ amount: decimalFormattedAmount,
407
+ tokenName,
408
+ txHash: receipt.transactionHash,
409
+ },
410
+ 'Transaction confirmed for route.',
411
+ );
412
+ successfulTransactions.push(transaction);
413
+ } catch (error) {
414
+ transactionFailures++;
415
+ this.logger.error(
416
+ {
417
+ origin: transaction.route.origin,
418
+ destination: transaction.route.destination,
419
+ amount: transaction.originTokenAmount.getDecimalFormattedAmount(),
420
+ tokenName: transaction.originTokenAmount.token.name,
421
+ error,
422
+ },
423
+ 'Transaction failed for route.',
424
+ );
425
+ }
426
+ }
427
+
428
+ return {
429
+ gasEstimationFailures,
430
+ transactionFailures,
431
+ successfulTransactions,
432
+ };
433
+ }
434
+
435
+ private filterTransactions(
436
+ transactions: PreparedTransaction[],
437
+ ): PreparedTransaction[] {
438
+ const filteredTransactions: PreparedTransaction[] = [];
439
+ for (const transaction of transactions) {
440
+ const { origin, destination, amount } = transaction.route;
441
+ const originToken = this.tokensByChainName[origin];
442
+ const decimalFormattedAmount =
443
+ transaction.originTokenAmount.getDecimalFormattedAmount();
444
+
445
+ // minimum amount check
446
+ const { bridgeMinAcceptedAmount } = getBridgeConfig(
447
+ this.bridges,
448
+ origin,
449
+ destination,
450
+ this.logger,
451
+ );
452
+ const minAccepted = BigInt(
453
+ toWei(bridgeMinAcceptedAmount, originToken.decimals),
454
+ );
455
+ if (minAccepted > amount) {
456
+ this.logger.info(
457
+ {
458
+ origin,
459
+ destination,
460
+ amount: decimalFormattedAmount,
461
+ tokenName: originToken.name,
462
+ },
463
+ 'Route skipped due to minimum threshold amount not met.',
464
+ );
465
+ continue;
466
+ }
467
+ filteredTransactions.push(transaction);
468
+ }
469
+ return filteredTransactions;
470
+ }
471
+ }