@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,13 +1,21 @@
1
1
  import { type Logger } from 'pino';
2
2
 
3
- import type { ChainName } from '@hyperlane-xyz/sdk';
3
+ import type { ChainMap, ChainName, Token } from '@hyperlane-xyz/sdk';
4
+ import { toWei } from '@hyperlane-xyz/utils';
4
5
 
5
6
  import type {
6
7
  IStrategy,
8
+ InflightContext,
7
9
  RawBalances,
8
- RebalancingRoute,
10
+ Route,
11
+ StrategyRoute,
9
12
  } from '../interfaces/IStrategy.js';
10
13
  import { type Metrics } from '../metrics/Metrics.js';
14
+ import {
15
+ type BridgeConfig,
16
+ type BridgeConfigWithOverride,
17
+ getBridgeConfig,
18
+ } from '../utils/bridgeUtils.js';
11
19
 
12
20
  export type Delta = { chain: ChainName; amount: bigint };
13
21
 
@@ -15,41 +23,81 @@ export type Delta = { chain: ChainName; amount: bigint };
15
23
  * Base abstract class for rebalancing strategies
16
24
  */
17
25
  export abstract class BaseStrategy implements IStrategy {
26
+ abstract readonly name: string;
18
27
  protected readonly chains: ChainName[];
19
28
  protected readonly metrics?: Metrics;
20
29
  protected readonly logger: Logger;
30
+ protected readonly bridgeConfigs: ChainMap<BridgeConfigWithOverride>;
31
+ protected readonly tokensByChainName?: ChainMap<Token>;
21
32
 
22
- constructor(chains: ChainName[], logger: Logger, metrics?: Metrics) {
33
+ constructor(
34
+ chains: ChainName[],
35
+ logger: Logger,
36
+ bridgeConfigs: ChainMap<BridgeConfigWithOverride>,
37
+ metrics?: Metrics,
38
+ tokensByChainName?: ChainMap<Token>,
39
+ ) {
23
40
  // Rebalancing makes sense only with more than one chain.
24
41
  if (chains.length < 2) {
25
42
  throw new Error('At least two chains must be configured');
26
43
  }
27
44
  this.chains = chains;
28
45
  this.logger = logger;
46
+ this.bridgeConfigs = bridgeConfigs;
29
47
  this.metrics = metrics;
48
+ this.tokensByChainName = tokensByChainName;
49
+ }
50
+
51
+ protected getBridgeConfigForRoute(
52
+ origin: ChainName,
53
+ destination: ChainName,
54
+ ): BridgeConfig {
55
+ return getBridgeConfig(this.bridgeConfigs, origin, destination);
30
56
  }
31
57
 
32
58
  /**
33
59
  * Main method to get rebalancing routes
34
60
  */
35
- getRebalancingRoutes(rawBalances: RawBalances): RebalancingRoute[] {
36
- this.logger.info(
37
- {
38
- context: this.constructor.name,
39
- rawBalances,
40
- },
41
- 'Input rawBalances',
42
- );
61
+ getRebalancingRoutes(
62
+ rawBalances: RawBalances,
63
+ inflightContext?: InflightContext,
64
+ ): StrategyRoute[] {
65
+ const pendingRebalances = inflightContext?.pendingRebalances ?? [];
66
+ const pendingTransfers = inflightContext?.pendingTransfers ?? [];
67
+ const proposedRebalances = inflightContext?.proposedRebalances ?? [];
68
+
43
69
  this.logger.info(
44
70
  {
45
- context: this.constructor.name,
71
+ strategy: this.name,
72
+ balances: Object.entries(rawBalances).map(([c, b]) => ({
73
+ chain: c,
74
+ balance: b.toString(),
75
+ })),
76
+ pendingRebalances: pendingRebalances.length,
77
+ pendingTransfers: pendingTransfers.length,
78
+ proposedRebalances: proposedRebalances.length,
46
79
  },
47
- 'Calculating rebalancing routes',
80
+ 'Strategy evaluating',
48
81
  );
49
82
  this.validateRawBalances(rawBalances);
50
83
 
84
+ // Store original balances for filtering step
85
+ const actualBalances = rawBalances;
86
+
87
+ // Step 1: Reserve collateral for pending user transfers
88
+ // This prevents draining collateral needed for incoming user transfers
89
+ const effectiveBalances = this.reserveCollateral(
90
+ rawBalances,
91
+ pendingTransfers,
92
+ );
93
+
51
94
  // Get balances categorized by surplus and deficit
52
- const { surpluses, deficits } = this.getCategorizedBalances(rawBalances);
95
+ // Pass pending and proposed rebalances so strategy can account for them
96
+ const { surpluses, deficits } = this.getCategorizedBalances(
97
+ effectiveBalances,
98
+ pendingRebalances,
99
+ proposedRebalances,
100
+ );
53
101
 
54
102
  this.logger.debug(
55
103
  {
@@ -126,7 +174,7 @@ export abstract class BaseStrategy implements IStrategy {
126
174
  surpluses.sort((a, b) => (a.amount > b.amount ? -1 : 1));
127
175
  deficits.sort((a, b) => (a.amount > b.amount ? -1 : 1));
128
176
 
129
- const routes: RebalancingRoute[] = [];
177
+ const routes: StrategyRoute[] = [];
130
178
 
131
179
  // Transfer from surplus to deficit until all deficits are balanced.
132
180
  while (deficits.length > 0 && surpluses.length > 0) {
@@ -137,24 +185,34 @@ export abstract class BaseStrategy implements IStrategy {
137
185
  const transferAmount =
138
186
  surplus.amount > deficit.amount ? deficit.amount : surplus.amount;
139
187
 
140
- // Creates the balancing route
141
- routes.push({
142
- origin: surplus.chain,
143
- destination: deficit.chain,
144
- amount: transferAmount,
145
- });
188
+ // Skip zero-amount routes (can occur after scaling when surpluses < deficits)
189
+ if (transferAmount > 0n) {
190
+ // Get bridge config for this route (with destination-specific overrides)
191
+ const bridgeConfig = this.getBridgeConfigForRoute(
192
+ surplus.chain,
193
+ deficit.chain,
194
+ );
195
+
196
+ // Creates the balancing route
197
+ routes.push({
198
+ origin: surplus.chain,
199
+ destination: deficit.chain,
200
+ amount: transferAmount,
201
+ bridge: bridgeConfig.bridge,
202
+ });
203
+ }
146
204
 
147
205
  // Decreases the amounts for the following iterations
148
206
  deficit.amount -= transferAmount;
149
207
  surplus.amount -= transferAmount;
150
208
 
151
- // Removes the deficit if it is fully balanced
152
- if (!deficit.amount) {
209
+ // Removes the deficit if it is fully balanced (including scaled-to-zero)
210
+ if (deficit.amount <= 0n) {
153
211
  deficits.shift();
154
212
  }
155
213
 
156
214
  // Removes the surplus if it has been drained
157
- if (!surplus.amount) {
215
+ if (surplus.amount <= 0n) {
158
216
  surpluses.shift();
159
217
  }
160
218
  }
@@ -173,14 +231,40 @@ export abstract class BaseStrategy implements IStrategy {
173
231
  },
174
232
  'Found rebalancing routes',
175
233
  );
176
- return routes;
234
+
235
+ const filteredRoutes = this.filterRoutes(routes, actualBalances);
236
+
237
+ this.logger.debug(
238
+ {
239
+ context: this.constructor.name,
240
+ filteredRoutesCount: filteredRoutes.length,
241
+ droppedCount: routes.length - filteredRoutes.length,
242
+ },
243
+ 'Filtered rebalancing routes',
244
+ );
245
+
246
+ // Record metrics for each intent that passed filtering
247
+ for (const route of filteredRoutes) {
248
+ this.metrics?.recordIntentCreated(route, this.name);
249
+ }
250
+
251
+ return filteredRoutes;
177
252
  }
178
253
 
179
254
  /**
180
255
  * Abstract method to get balances categorized by surplus and deficit
181
256
  * Each specific strategy should implement its own logic
257
+ *
258
+ * @param balances - Effective balances (after collateral reservation)
259
+ * @param pendingRebalances - In-flight rebalances (origin tx confirmed, balance already deducted)
260
+ * @param proposedRebalances - Routes from earlier strategies in same cycle (not yet executed)
261
+ * @returns Categorized surpluses and deficits as Delta arrays
182
262
  */
183
- protected abstract getCategorizedBalances(rawBalances: RawBalances): {
263
+ protected abstract getCategorizedBalances(
264
+ balances: RawBalances,
265
+ pendingRebalances?: Route[],
266
+ proposedRebalances?: StrategyRoute[],
267
+ ): {
184
268
  surpluses: Delta[];
185
269
  deficits: Delta[];
186
270
  };
@@ -207,4 +291,210 @@ export abstract class BaseStrategy implements IStrategy {
207
291
  }
208
292
  }
209
293
  }
294
+
295
+ /**
296
+ * Reserve collateral for pending user transfers.
297
+ * Subtracts pending transfer amounts from destination balances.
298
+ * This ensures we don't drain collateral needed for incoming transfers.
299
+ *
300
+ * @param rawBalances - Current on-chain balances
301
+ * @param pendingTransfers - Transfers that will need collateral on destination
302
+ * @returns Balances with reserved amounts subtracted
303
+ */
304
+ protected reserveCollateral(
305
+ rawBalances: RawBalances,
306
+ pendingTransfers: Route[],
307
+ ): RawBalances {
308
+ if (pendingTransfers.length === 0) {
309
+ return rawBalances;
310
+ }
311
+
312
+ const reserved = { ...rawBalances };
313
+
314
+ for (const transfer of pendingTransfers) {
315
+ const destBalance = reserved[transfer.destination] ?? 0n;
316
+ // Reserve the transfer amount from destination
317
+ // Allow negative values to indicate collateral deficits
318
+ reserved[transfer.destination] = destBalance - transfer.amount;
319
+
320
+ this.logger.debug(
321
+ {
322
+ context: this.constructor.name,
323
+ destination: transfer.destination,
324
+ amount: transfer.amount.toString(),
325
+ newBalance: reserved[transfer.destination].toString(),
326
+ },
327
+ 'Reserved collateral for pending transfer',
328
+ );
329
+ }
330
+
331
+ this.logger.info(
332
+ {
333
+ reservations: pendingTransfers.map((t) => ({
334
+ destination: t.destination,
335
+ amount: t.amount.toString(),
336
+ })),
337
+ },
338
+ 'Collateral reserved for pending transfers',
339
+ );
340
+
341
+ return reserved;
342
+ }
343
+
344
+ /**
345
+ * Simulate pending rebalances by adding to destination balances.
346
+ *
347
+ * Only adds to destination - does NOT subtract from origin because:
348
+ * - pendingRebalances only contains in_progress intents (origin tx confirmed)
349
+ * - Origin balance is already deducted on-chain
350
+ *
351
+ * @param rawBalances - Current balances (may already have collateral reserved)
352
+ * @param pendingRebalances - In-flight rebalance operations (in_progress only)
353
+ * @returns Simulated future balances after rebalances complete
354
+ */
355
+ protected simulatePendingRebalances(
356
+ rawBalances: RawBalances,
357
+ pendingRebalances: Route[],
358
+ ): RawBalances {
359
+ if (pendingRebalances.length === 0) {
360
+ return rawBalances;
361
+ }
362
+
363
+ const simulated = { ...rawBalances };
364
+
365
+ for (const rebalance of pendingRebalances) {
366
+ // Only add to destination - origin is already deducted on-chain
367
+ // (pendingRebalances only contains in_progress intents with confirmed origin tx)
368
+ simulated[rebalance.destination] =
369
+ (simulated[rebalance.destination] ?? 0n) + rebalance.amount;
370
+
371
+ this.logger.debug(
372
+ {
373
+ context: this.constructor.name,
374
+ destination: rebalance.destination,
375
+ amount: rebalance.amount.toString(),
376
+ },
377
+ 'Simulated pending rebalance (destination increase)',
378
+ );
379
+ }
380
+
381
+ this.logger.info(
382
+ {
383
+ simulations: pendingRebalances.map((r) => ({
384
+ from: r.origin,
385
+ to: r.destination,
386
+ amount: r.amount.toString(),
387
+ })),
388
+ },
389
+ 'Simulated pending rebalances',
390
+ );
391
+
392
+ return simulated;
393
+ }
394
+
395
+ /**
396
+ * Simulate proposed rebalances by subtracting from origin AND adding to destination.
397
+ *
398
+ * Unlike pendingRebalances, proposedRebalances are routes from earlier strategies
399
+ * in the same cycle that haven't been executed yet. Therefore:
400
+ * - Origin balance has NOT been deducted on-chain
401
+ * - We must simulate both sides to maintain accurate total balance
402
+ *
403
+ * @param rawBalances - Current balances (may already have pending rebalances simulated)
404
+ * @param proposedRebalances - Routes from earlier strategies (not yet executed)
405
+ * @returns Simulated balances after proposed rebalances complete
406
+ */
407
+ protected simulateProposedRebalances(
408
+ rawBalances: RawBalances,
409
+ proposedRebalances: Route[],
410
+ ): RawBalances {
411
+ if (proposedRebalances.length === 0) {
412
+ return rawBalances;
413
+ }
414
+
415
+ const simulated = { ...rawBalances };
416
+
417
+ for (const rebalance of proposedRebalances) {
418
+ // Subtract from origin (not yet deducted on-chain)
419
+ simulated[rebalance.origin] =
420
+ (simulated[rebalance.origin] ?? 0n) - rebalance.amount;
421
+
422
+ // Add to destination
423
+ simulated[rebalance.destination] =
424
+ (simulated[rebalance.destination] ?? 0n) + rebalance.amount;
425
+
426
+ this.logger.debug(
427
+ {
428
+ context: this.constructor.name,
429
+ origin: rebalance.origin,
430
+ destination: rebalance.destination,
431
+ amount: rebalance.amount.toString(),
432
+ },
433
+ 'Simulated proposed rebalance (origin decrease, destination increase)',
434
+ );
435
+ }
436
+
437
+ this.logger.info(
438
+ {
439
+ simulations: proposedRebalances.map((r) => ({
440
+ from: r.origin,
441
+ to: r.destination,
442
+ amount: r.amount.toString(),
443
+ })),
444
+ },
445
+ 'Simulated proposed rebalances',
446
+ );
447
+
448
+ return simulated;
449
+ }
450
+
451
+ protected filterRoutes(
452
+ routes: StrategyRoute[],
453
+ actualBalances: RawBalances,
454
+ ): StrategyRoute[] {
455
+ return routes.filter((route) => {
456
+ const balance = actualBalances[route.origin] ?? 0n;
457
+ if (balance < route.amount) {
458
+ this.logger.warn(
459
+ {
460
+ context: this.constructor.name,
461
+ origin: route.origin,
462
+ destination: route.destination,
463
+ required: route.amount.toString(),
464
+ available: balance.toString(),
465
+ },
466
+ 'Dropping route due to insufficient balance',
467
+ );
468
+ return false;
469
+ }
470
+
471
+ if (this.tokensByChainName) {
472
+ const token = this.tokensByChainName[route.origin];
473
+ if (token) {
474
+ const bridgeConfig = this.getBridgeConfigForRoute(
475
+ route.origin,
476
+ route.destination,
477
+ );
478
+ const minAmount = BigInt(
479
+ toWei(bridgeConfig.bridgeMinAcceptedAmount, token.decimals),
480
+ );
481
+ if (route.amount < minAmount) {
482
+ this.logger.info(
483
+ {
484
+ context: this.constructor.name,
485
+ origin: route.origin,
486
+ destination: route.destination,
487
+ amount: route.amount.toString(),
488
+ minAmount: minAmount.toString(),
489
+ },
490
+ 'Dropping route below bridgeMinAcceptedAmount',
491
+ );
492
+ return false;
493
+ }
494
+ }
495
+ }
496
+
497
+ return true;
498
+ });
499
+ }
210
500
  }