@hyperlane-xyz/rebalancer 2.0.0 → 3.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 (209) hide show
  1. package/dist/bridges/LiFiBridge.d.ts +67 -0
  2. package/dist/bridges/LiFiBridge.d.ts.map +1 -0
  3. package/dist/bridges/LiFiBridge.js +386 -0
  4. package/dist/bridges/LiFiBridge.js.map +1 -0
  5. package/dist/config/RebalancerConfig.d.ts +7 -2
  6. package/dist/config/RebalancerConfig.d.ts.map +1 -1
  7. package/dist/config/RebalancerConfig.js +7 -4
  8. package/dist/config/RebalancerConfig.js.map +1 -1
  9. package/dist/config/RebalancerConfig.test.js +134 -1
  10. package/dist/config/RebalancerConfig.test.js.map +1 -1
  11. package/dist/config/types.d.ts +1016 -304
  12. package/dist/config/types.d.ts.map +1 -1
  13. package/dist/config/types.js +105 -10
  14. package/dist/config/types.js.map +1 -1
  15. package/dist/core/InventoryRebalancer.d.ts +190 -0
  16. package/dist/core/InventoryRebalancer.d.ts.map +1 -0
  17. package/dist/core/InventoryRebalancer.js +885 -0
  18. package/dist/core/InventoryRebalancer.js.map +1 -0
  19. package/dist/core/InventoryRebalancer.test.d.ts +2 -0
  20. package/dist/core/InventoryRebalancer.test.d.ts.map +1 -0
  21. package/dist/core/InventoryRebalancer.test.js +1351 -0
  22. package/dist/core/InventoryRebalancer.test.js.map +1 -0
  23. package/dist/core/Rebalancer.d.ts +11 -4
  24. package/dist/core/Rebalancer.d.ts.map +1 -1
  25. package/dist/core/Rebalancer.js +92 -9
  26. package/dist/core/Rebalancer.js.map +1 -1
  27. package/dist/core/Rebalancer.test.js +82 -49
  28. package/dist/core/Rebalancer.test.js.map +1 -1
  29. package/dist/core/RebalancerOrchestrator.d.ts +30 -9
  30. package/dist/core/RebalancerOrchestrator.d.ts.map +1 -1
  31. package/dist/core/RebalancerOrchestrator.js +79 -71
  32. package/dist/core/RebalancerOrchestrator.js.map +1 -1
  33. package/dist/core/RebalancerOrchestrator.test.d.ts +2 -0
  34. package/dist/core/RebalancerOrchestrator.test.d.ts.map +1 -0
  35. package/dist/core/RebalancerOrchestrator.test.js +714 -0
  36. package/dist/core/RebalancerOrchestrator.test.js.map +1 -0
  37. package/dist/core/RebalancerService.d.ts +7 -3
  38. package/dist/core/RebalancerService.d.ts.map +1 -1
  39. package/dist/core/RebalancerService.js +44 -24
  40. package/dist/core/RebalancerService.js.map +1 -1
  41. package/dist/core/RebalancerService.test.js +71 -109
  42. package/dist/core/RebalancerService.test.js.map +1 -1
  43. package/dist/e2e/collateral-deficit.e2e-test.js +1 -3
  44. package/dist/e2e/collateral-deficit.e2e-test.js.map +1 -1
  45. package/dist/e2e/composite.e2e-test.js.map +1 -1
  46. package/dist/e2e/harness/BridgeSetup.d.ts +6 -0
  47. package/dist/e2e/harness/BridgeSetup.d.ts.map +1 -1
  48. package/dist/e2e/harness/BridgeSetup.js +10 -1
  49. package/dist/e2e/harness/BridgeSetup.js.map +1 -1
  50. package/dist/e2e/harness/TestHelpers.d.ts.map +1 -1
  51. package/dist/e2e/harness/TestHelpers.js +1 -4
  52. package/dist/e2e/harness/TestHelpers.js.map +1 -1
  53. package/dist/e2e/harness/TestRebalancer.d.ts +1 -1
  54. package/dist/e2e/harness/TestRebalancer.d.ts.map +1 -1
  55. package/dist/e2e/harness/TestRebalancer.js +6 -7
  56. package/dist/e2e/harness/TestRebalancer.js.map +1 -1
  57. package/dist/e2e/minAmount.e2e-test.js +0 -1
  58. package/dist/e2e/minAmount.e2e-test.js.map +1 -1
  59. package/dist/e2e/weighted.e2e-test.js +0 -1
  60. package/dist/e2e/weighted.e2e-test.js.map +1 -1
  61. package/dist/factories/RebalancerContextFactory.d.ts +48 -6
  62. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
  63. package/dist/factories/RebalancerContextFactory.js +170 -17
  64. package/dist/factories/RebalancerContextFactory.js.map +1 -1
  65. package/dist/index.d.ts +5 -5
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +1 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/interfaces/IExternalBridge.d.ts +101 -0
  70. package/dist/interfaces/IExternalBridge.d.ts.map +1 -0
  71. package/dist/interfaces/IExternalBridge.js +2 -0
  72. package/dist/interfaces/IExternalBridge.js.map +1 -0
  73. package/dist/interfaces/IMonitor.d.ts +1 -0
  74. package/dist/interfaces/IMonitor.d.ts.map +1 -1
  75. package/dist/interfaces/IRebalancer.d.ts +25 -25
  76. package/dist/interfaces/IRebalancer.d.ts.map +1 -1
  77. package/dist/interfaces/IStrategy.d.ts +36 -3
  78. package/dist/interfaces/IStrategy.d.ts.map +1 -1
  79. package/dist/interfaces/IStrategy.js +12 -1
  80. package/dist/interfaces/IStrategy.js.map +1 -1
  81. package/dist/metrics/PriceGetter.js +1 -1
  82. package/dist/metrics/PriceGetter.js.map +1 -1
  83. package/dist/metrics/scripts/metrics.d.ts +3 -3
  84. package/dist/monitor/Monitor.d.ts +12 -2
  85. package/dist/monitor/Monitor.d.ts.map +1 -1
  86. package/dist/monitor/Monitor.js +46 -1
  87. package/dist/monitor/Monitor.js.map +1 -1
  88. package/dist/service.js +40 -17
  89. package/dist/service.js.map +1 -1
  90. package/dist/strategy/BaseStrategy.d.ts +12 -6
  91. package/dist/strategy/BaseStrategy.d.ts.map +1 -1
  92. package/dist/strategy/BaseStrategy.js +56 -21
  93. package/dist/strategy/BaseStrategy.js.map +1 -1
  94. package/dist/strategy/CollateralDeficitStrategy.d.ts +1 -1
  95. package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -1
  96. package/dist/strategy/CollateralDeficitStrategy.js +19 -11
  97. package/dist/strategy/CollateralDeficitStrategy.js.map +1 -1
  98. package/dist/strategy/CollateralDeficitStrategy.test.js +135 -2
  99. package/dist/strategy/CollateralDeficitStrategy.test.js.map +1 -1
  100. package/dist/strategy/CompositeStrategy.test.js +13 -0
  101. package/dist/strategy/CompositeStrategy.test.js.map +1 -1
  102. package/dist/strategy/MinAmountStrategy.test.js +4 -0
  103. package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
  104. package/dist/strategy/StrategyFactory.d.ts +2 -1
  105. package/dist/strategy/StrategyFactory.d.ts.map +1 -1
  106. package/dist/strategy/StrategyFactory.js +24 -8
  107. package/dist/strategy/StrategyFactory.js.map +1 -1
  108. package/dist/strategy/WeightedStrategy.test.js +6 -0
  109. package/dist/strategy/WeightedStrategy.test.js.map +1 -1
  110. package/dist/test/helpers.d.ts +8 -7
  111. package/dist/test/helpers.d.ts.map +1 -1
  112. package/dist/test/helpers.js +23 -5
  113. package/dist/test/helpers.js.map +1 -1
  114. package/dist/test/lifiMocks.d.ts +51 -0
  115. package/dist/test/lifiMocks.d.ts.map +1 -0
  116. package/dist/test/lifiMocks.js +130 -0
  117. package/dist/test/lifiMocks.js.map +1 -0
  118. package/dist/tracking/ActionTracker.d.ts +33 -1
  119. package/dist/tracking/ActionTracker.d.ts.map +1 -1
  120. package/dist/tracking/ActionTracker.js +193 -22
  121. package/dist/tracking/ActionTracker.js.map +1 -1
  122. package/dist/tracking/ActionTracker.test.js +107 -19
  123. package/dist/tracking/ActionTracker.test.js.map +1 -1
  124. package/dist/tracking/IActionTracker.d.ts +47 -3
  125. package/dist/tracking/IActionTracker.d.ts.map +1 -1
  126. package/dist/tracking/InflightContextAdapter.d.ts.map +1 -1
  127. package/dist/tracking/InflightContextAdapter.js +24 -7
  128. package/dist/tracking/InflightContextAdapter.js.map +1 -1
  129. package/dist/tracking/InflightContextAdapter.test.js +7 -4
  130. package/dist/tracking/InflightContextAdapter.test.js.map +1 -1
  131. package/dist/tracking/types.d.ts +31 -2
  132. package/dist/tracking/types.d.ts.map +1 -1
  133. package/dist/utils/ExplorerClient.d.ts +2 -1
  134. package/dist/utils/ExplorerClient.d.ts.map +1 -1
  135. package/dist/utils/ExplorerClient.js +13 -8
  136. package/dist/utils/ExplorerClient.js.map +1 -1
  137. package/dist/utils/bridgeUtils.d.ts +27 -4
  138. package/dist/utils/bridgeUtils.d.ts.map +1 -1
  139. package/dist/utils/bridgeUtils.js +38 -0
  140. package/dist/utils/bridgeUtils.js.map +1 -1
  141. package/dist/utils/bridgeUtils.test.js +9 -0
  142. package/dist/utils/bridgeUtils.test.js.map +1 -1
  143. package/dist/utils/gasEstimation.d.ts +65 -0
  144. package/dist/utils/gasEstimation.d.ts.map +1 -0
  145. package/dist/utils/gasEstimation.js +176 -0
  146. package/dist/utils/gasEstimation.js.map +1 -0
  147. package/dist/utils/tokenUtils.d.ts +9 -1
  148. package/dist/utils/tokenUtils.d.ts.map +1 -1
  149. package/dist/utils/tokenUtils.js +11 -0
  150. package/dist/utils/tokenUtils.js.map +1 -1
  151. package/package.json +9 -7
  152. package/src/bridges/LiFiBridge.ts +538 -0
  153. package/src/config/RebalancerConfig.test.ts +160 -0
  154. package/src/config/RebalancerConfig.ts +14 -3
  155. package/src/config/types.ts +136 -10
  156. package/src/core/InventoryRebalancer.test.ts +1684 -0
  157. package/src/core/InventoryRebalancer.ts +1255 -0
  158. package/src/core/Rebalancer.test.ts +84 -30
  159. package/src/core/Rebalancer.ts +144 -23
  160. package/src/core/RebalancerOrchestrator.test.ts +860 -0
  161. package/src/core/RebalancerOrchestrator.ts +146 -95
  162. package/src/core/RebalancerService.test.ts +80 -123
  163. package/src/core/RebalancerService.ts +67 -33
  164. package/src/e2e/collateral-deficit.e2e-test.ts +2 -4
  165. package/src/e2e/composite.e2e-test.ts +5 -5
  166. package/src/e2e/harness/BridgeSetup.ts +28 -1
  167. package/src/e2e/harness/TestHelpers.ts +1 -4
  168. package/src/e2e/harness/TestRebalancer.ts +7 -7
  169. package/src/e2e/minAmount.e2e-test.ts +1 -2
  170. package/src/e2e/weighted.e2e-test.ts +1 -2
  171. package/src/factories/RebalancerContextFactory.ts +293 -24
  172. package/src/index.ts +20 -5
  173. package/src/interfaces/IExternalBridge.ts +115 -0
  174. package/src/interfaces/IMonitor.ts +1 -0
  175. package/src/interfaces/IRebalancer.ts +45 -29
  176. package/src/interfaces/IStrategy.ts +50 -3
  177. package/src/metrics/PriceGetter.ts +1 -1
  178. package/src/monitor/Monitor.ts +81 -2
  179. package/src/service.ts +59 -18
  180. package/src/strategy/BaseStrategy.ts +77 -24
  181. package/src/strategy/CollateralDeficitStrategy.test.ts +181 -4
  182. package/src/strategy/CollateralDeficitStrategy.ts +42 -15
  183. package/src/strategy/CompositeStrategy.test.ts +13 -0
  184. package/src/strategy/MinAmountStrategy.test.ts +4 -0
  185. package/src/strategy/StrategyFactory.ts +33 -6
  186. package/src/strategy/WeightedStrategy.test.ts +6 -0
  187. package/src/test/helpers.ts +39 -14
  188. package/src/test/lifiMocks.ts +174 -0
  189. package/src/tracking/ActionTracker.test.ts +122 -19
  190. package/src/tracking/ActionTracker.ts +284 -24
  191. package/src/tracking/IActionTracker.ts +58 -3
  192. package/src/tracking/InflightContextAdapter.test.ts +7 -4
  193. package/src/tracking/InflightContextAdapter.ts +42 -9
  194. package/src/tracking/types.ts +43 -2
  195. package/src/utils/ExplorerClient.ts +23 -10
  196. package/src/utils/bridgeUtils.test.ts +9 -0
  197. package/src/utils/bridgeUtils.ts +75 -6
  198. package/src/utils/gasEstimation.ts +272 -0
  199. package/src/utils/tokenUtils.ts +12 -0
  200. package/dist/tracking/index.d.ts +0 -7
  201. package/dist/tracking/index.d.ts.map +0 -1
  202. package/dist/tracking/index.js +0 -6
  203. package/dist/tracking/index.js.map +0 -1
  204. package/dist/utils/index.d.ts +0 -5
  205. package/dist/utils/index.d.ts.map +0 -1
  206. package/dist/utils/index.js +0 -5
  207. package/dist/utils/index.js.map +0 -1
  208. package/src/tracking/index.ts +0 -36
  209. package/src/utils/index.ts +0 -4
@@ -1,5 +1,7 @@
1
1
  import type { Address, Domain } from '@hyperlane-xyz/utils';
2
2
 
3
+ import type { ExternalBridgeType } from '../config/types.js';
4
+
3
5
  import type { IStore } from './store/IStore.js';
4
6
 
5
7
  // === Base Interfaces ===
@@ -35,6 +37,26 @@ export type RebalanceIntentStatus =
35
37
  | 'failed';
36
38
  export type RebalanceActionStatus = 'in_progress' | 'complete' | 'failed';
37
39
 
40
+ // === Execution Types ===
41
+
42
+ /**
43
+ * Execution method for rebalancing:
44
+ * - `movable_collateral`: Uses MovableCollateralRouter.rebalance() on-chain
45
+ * - `inventory`: Uses external bridges + transferRemote
46
+ */
47
+ export type ExecutionMethod = 'movable_collateral' | 'inventory';
48
+
49
+ /**
50
+ * Type of rebalance action:
51
+ * - `rebalance_message`: Standard movable collateral rebalance (Hyperlane message)
52
+ * - `inventory_movement`: External bridge transfer (e.g., LiFi) to move inventory
53
+ * - `inventory_deposit`: transferRemote to deposit inventory as collateral
54
+ */
55
+ export type ActionType =
56
+ | 'rebalance_message'
57
+ | 'inventory_movement'
58
+ | 'inventory_deposit';
59
+
38
60
  // === Entity Types ===
39
61
 
40
62
  export interface Transfer extends TrackedActionBase {
@@ -46,17 +68,22 @@ export interface Transfer extends TrackedActionBase {
46
68
 
47
69
  export interface RebalanceIntent extends TrackedActionBase {
48
70
  status: RebalanceIntentStatus;
49
- fulfilledAmount: bigint;
50
71
  bridge?: Address; // Optional - bridge contract used (missing for recovered intents)
51
72
  priority?: number; // Optional - missing for recovered intents
52
73
  strategyType?: string; // Optional - missing for recovered intents
74
+ executionMethod?: ExecutionMethod; // Optional - defaults to movable_collateral
75
+ externalBridge?: ExternalBridgeType; // Optional - external bridge type (e.g., LiFi)
53
76
  }
54
77
 
55
78
  export interface RebalanceAction extends TrackedActionBase {
56
79
  status: RebalanceActionStatus;
80
+ type: ActionType; // Type of action (rebalance_message, inventory_movement, inventory_deposit)
57
81
  intentId: string; // Links to parent RebalanceIntent
58
- messageId: string; // Hyperlane message ID
82
+ messageId?: string; // Hyperlane message ID (required for rebalance_message, inventory_deposit)
59
83
  txHash?: string; // Origin transaction hash
84
+ // Fields for inventory_movement (external bridge)
85
+ externalBridgeTransferId?: string; // External bridge transfer ID (e.g., LiFi transfer ID)
86
+ externalBridgeId?: ExternalBridgeType; // External bridge identifier (e.g., 'lifi')
60
87
  }
61
88
 
62
89
  // === Type Aliases for Stores ===
@@ -70,3 +97,17 @@ export type IRebalanceActionStore = IStore<
70
97
  RebalanceAction,
71
98
  RebalanceActionStatus
72
99
  >;
100
+
101
+ // === Derived Types ===
102
+
103
+ /**
104
+ * Represents an inventory intent that has been partially fulfilled
105
+ * and can continue execution. Values are derived from action states.
106
+ */
107
+ export interface PartialInventoryIntent {
108
+ intent: RebalanceIntent;
109
+ /** Sum of complete inventory_deposit action amounts */
110
+ completedAmount: bigint;
111
+ /** Amount remaining to fulfill: intent.amount - completedAmount - inflightAmount */
112
+ remaining: bigint;
113
+ }
@@ -9,14 +9,15 @@ export type InflightRebalanceQueryParams = {
9
9
 
10
10
  export type UserTransferQueryParams = {
11
11
  routersByDomain: Record<number, string>; // Domain ID → router address (derive routers and domains from this)
12
- excludeTxSender: string; // Rebalancer address to exclude
12
+ excludeTxSenders: string[]; // Addresses to exclude (rebalancer + inventory signer)
13
13
  limit?: number;
14
14
  };
15
15
 
16
16
  export type RebalanceActionQueryParams = {
17
17
  bridges: string[]; // Bridge contract addresses
18
18
  routersByDomain: Record<number, string>; // Domain ID → router address (derive routers and domains from this)
19
- rebalancerAddress: string; // Only include rebalancer's txs
19
+ rebalancerAddress: string; // Include rebalancer's txs
20
+ inventorySignerAddress?: string; // Optional: also include inventory signer's txs (for inventory_deposit actions)
20
21
  limit?: number;
21
22
  };
22
23
 
@@ -191,7 +192,7 @@ export class ExplorerClient implements IExplorerClient {
191
192
  params: UserTransferQueryParams,
192
193
  logger: Logger,
193
194
  ): Promise<ExplorerMessage[]> {
194
- const { routersByDomain, excludeTxSender, limit = 100 } = params;
195
+ const { routersByDomain, excludeTxSenders, limit = 100 } = params;
195
196
 
196
197
  // Derive routers and domains from routersByDomain
197
198
  const routers = Object.values(routersByDomain);
@@ -202,7 +203,7 @@ export class ExplorerClient implements IExplorerClient {
202
203
  recipients: routers.map((a) => this.toBytea(a)),
203
204
  originDomains: domains,
204
205
  destDomains: domains,
205
- excludeTxSender: this.toBytea(excludeTxSender),
206
+ excludeTxSenders: excludeTxSenders.map((a) => this.toBytea(a)),
206
207
  limit,
207
208
  };
208
209
 
@@ -214,7 +215,7 @@ export class ExplorerClient implements IExplorerClient {
214
215
  $recipients: [bytea!],
215
216
  $originDomains: [Int!],
216
217
  $destDomains: [Int!],
217
- $excludeTxSender: bytea!,
218
+ $excludeTxSenders: [bytea!],
218
219
  $limit: Int = 100
219
220
  ) {
220
221
  message_view(
@@ -225,7 +226,7 @@ export class ExplorerClient implements IExplorerClient {
225
226
  { recipient: { _in: $recipients } },
226
227
  { origin_domain_id: { _in: $originDomains } },
227
228
  { destination_domain_id: { _in: $destDomains } },
228
- { origin_tx_sender: { _neq: $excludeTxSender } }
229
+ { origin_tx_sender: { _nin: $excludeTxSenders } }
229
230
  ]
230
231
  }
231
232
  order_by: { origin_tx_id: desc }
@@ -284,19 +285,31 @@ export class ExplorerClient implements IExplorerClient {
284
285
  params: RebalanceActionQueryParams,
285
286
  logger: Logger,
286
287
  ): Promise<ExplorerMessage[]> {
287
- const { bridges, routersByDomain, rebalancerAddress, limit = 100 } = params;
288
+ const {
289
+ bridges,
290
+ routersByDomain,
291
+ rebalancerAddress,
292
+ inventorySignerAddress,
293
+ limit = 100,
294
+ } = params;
288
295
 
289
296
  // Derive routers and domains from routersByDomain
290
297
  const routers = Object.values(routersByDomain);
291
298
  const domains = Object.keys(routersByDomain).map(Number);
292
299
 
300
+ // Build list of tx senders to include (rebalancer + optional inventory signer)
301
+ const txSenders = [this.toBytea(rebalancerAddress)];
302
+ if (inventorySignerAddress) {
303
+ txSenders.push(this.toBytea(inventorySignerAddress));
304
+ }
305
+
293
306
  const variables = {
294
307
  senders: bridges.map((a) => this.toBytea(a)),
295
308
  recipients: bridges.map((a) => this.toBytea(a)),
296
309
  originTxRecipients: routers.map((a) => this.toBytea(a)),
297
310
  originDomains: domains,
298
311
  destDomains: domains,
299
- txSender: this.toBytea(rebalancerAddress),
312
+ txSenders,
300
313
  limit,
301
314
  };
302
315
 
@@ -309,7 +322,7 @@ export class ExplorerClient implements IExplorerClient {
309
322
  $originTxRecipients: [bytea!],
310
323
  $originDomains: [Int!],
311
324
  $destDomains: [Int!],
312
- $txSender: bytea!,
325
+ $txSenders: [bytea!],
313
326
  $limit: Int = 100
314
327
  ) {
315
328
  message_view(
@@ -321,7 +334,7 @@ export class ExplorerClient implements IExplorerClient {
321
334
  { origin_tx_recipient: { _in: $originTxRecipients } },
322
335
  { origin_domain_id: { _in: $originDomains } },
323
336
  { destination_domain_id: { _in: $destDomains } },
324
- { origin_tx_sender: { _eq: $txSender } }
337
+ { origin_tx_sender: { _in: $txSenders } }
325
338
  ]
326
339
  }
327
340
  order_by: { origin_tx_id: desc }
@@ -11,10 +11,12 @@ describe('bridgeConfig', () => {
11
11
  it('should return the base bridge config when no overrides exist', () => {
12
12
  const bridges: ChainMap<BridgeConfigWithOverride> = {
13
13
  chain1: {
14
+ executionType: 'movableCollateral',
14
15
  bridge: '0x1234567890123456789012345678901234567890',
15
16
  bridgeMinAcceptedAmount: 1000,
16
17
  },
17
18
  chain2: {
19
+ executionType: 'movableCollateral',
18
20
  bridge: '0x0987654321098765432109876543210987654321',
19
21
  bridgeMinAcceptedAmount: 2000,
20
22
  },
@@ -23,6 +25,7 @@ describe('bridgeConfig', () => {
23
25
  const result = getBridgeConfig(bridges, 'chain1', 'chain2');
24
26
 
25
27
  expect(result).to.deep.equal({
28
+ executionType: 'movableCollateral',
26
29
  bridge: '0x1234567890123456789012345678901234567890',
27
30
  bridgeMinAcceptedAmount: 1000,
28
31
  });
@@ -31,6 +34,7 @@ describe('bridgeConfig', () => {
31
34
  it('should merge base config with overrides when they exist', () => {
32
35
  const bridges: ChainMap<BridgeConfigWithOverride> = {
33
36
  chain1: {
37
+ executionType: 'movableCollateral',
34
38
  bridge: '0x1234567890123456789012345678901234567890',
35
39
  bridgeMinAcceptedAmount: 1000,
36
40
  override: {
@@ -40,6 +44,7 @@ describe('bridgeConfig', () => {
40
44
  },
41
45
  },
42
46
  chain2: {
47
+ executionType: 'movableCollateral',
43
48
  bridge: '0x0987654321098765432109876543210987654321',
44
49
  bridgeMinAcceptedAmount: 2000,
45
50
  },
@@ -48,6 +53,7 @@ describe('bridgeConfig', () => {
48
53
  const result = getBridgeConfig(bridges, 'chain1', 'chain2');
49
54
 
50
55
  expect(result).to.deep.equal({
56
+ executionType: 'movableCollateral',
51
57
  bridge: '0x1234567890123456789012345678901234567890',
52
58
  bridgeMinAcceptedAmount: 5000,
53
59
  });
@@ -56,6 +62,7 @@ describe('bridgeConfig', () => {
56
62
  it('should handle overrides that change the bridge address', () => {
57
63
  const bridges: ChainMap<BridgeConfigWithOverride> = {
58
64
  chain1: {
65
+ executionType: 'movableCollateral',
59
66
  bridge: '0x1234567890123456789012345678901234567890',
60
67
  bridgeMinAcceptedAmount: 1000,
61
68
  override: {
@@ -65,6 +72,7 @@ describe('bridgeConfig', () => {
65
72
  },
66
73
  },
67
74
  chain2: {
75
+ executionType: 'movableCollateral',
68
76
  bridge: '0x0987654321098765432109876543210987654321',
69
77
  bridgeMinAcceptedAmount: 2000,
70
78
  },
@@ -73,6 +81,7 @@ describe('bridgeConfig', () => {
73
81
  const result = getBridgeConfig(bridges, 'chain1', 'chain2');
74
82
 
75
83
  expect(result).to.deep.equal({
84
+ executionType: 'movableCollateral',
76
85
  bridge: '0xABCDEF0123456789ABCDEF0123456789ABCDEF01',
77
86
  bridgeMinAcceptedAmount: 1000,
78
87
  });
@@ -1,12 +1,41 @@
1
1
  import type { ChainMap, ChainName } from '@hyperlane-xyz/sdk';
2
+ import type { Address } from '@hyperlane-xyz/utils';
2
3
 
3
- export type BridgeConfigWithOverride = BridgeConfig & {
4
- override?: ChainMap<Partial<BridgeConfig>>;
4
+ import type { ExternalBridgeType } from '../config/types.js';
5
+ import type { StrategyRoute } from '../interfaces/IStrategy.js';
6
+
7
+ type BaseBridgeConfig = {
8
+ bridgeMinAcceptedAmount?: string | number;
9
+ };
10
+
11
+ export type MovableCollateralBridgeConfig = BaseBridgeConfig & {
12
+ executionType: 'movableCollateral';
13
+ bridge: Address;
5
14
  };
6
15
 
7
- export type BridgeConfig = {
8
- bridge: string;
9
- bridgeMinAcceptedAmount: string | number;
16
+ export type InventoryBridgeConfig = BaseBridgeConfig & {
17
+ executionType: 'inventory';
18
+ externalBridge: ExternalBridgeType;
19
+ };
20
+
21
+ export type BridgeConfig =
22
+ | MovableCollateralBridgeConfig
23
+ | InventoryBridgeConfig;
24
+
25
+ export function isMovableCollateralConfig(
26
+ config: BridgeConfig,
27
+ ): config is MovableCollateralBridgeConfig {
28
+ return config.executionType === 'movableCollateral';
29
+ }
30
+
31
+ export function isInventoryConfig(
32
+ config: BridgeConfig,
33
+ ): config is InventoryBridgeConfig {
34
+ return config.executionType === 'inventory';
35
+ }
36
+
37
+ export type BridgeConfigWithOverride = BridgeConfig & {
38
+ override?: ChainMap<Partial<BridgeConfig>>;
10
39
  };
11
40
 
12
41
  /**
@@ -28,5 +57,45 @@ export function getBridgeConfig(
28
57
  const { override: _, ...baseConfig } = fromConfig;
29
58
 
30
59
  // Return a new object with the base config and any overrides
31
- return { ...baseConfig, ...routeSpecificOverrides };
60
+ return { ...baseConfig, ...routeSpecificOverrides } as BridgeConfig;
61
+ }
62
+
63
+ /**
64
+ * Creates a StrategyRoute from a BridgeConfig with exhaustive type checking
65
+ * @param bridgeConfig The bridge configuration
66
+ * @param origin The origin chain
67
+ * @param destination The destination chain
68
+ * @param amount The amount to transfer
69
+ * @returns A StrategyRoute with the appropriate execution type
70
+ */
71
+ export function createStrategyRoute(
72
+ bridgeConfig: BridgeConfig,
73
+ origin: ChainName,
74
+ destination: ChainName,
75
+ amount: bigint,
76
+ ): StrategyRoute {
77
+ switch (bridgeConfig.executionType) {
78
+ case 'inventory':
79
+ return {
80
+ origin,
81
+ destination,
82
+ amount,
83
+ executionType: 'inventory',
84
+ externalBridge: bridgeConfig.externalBridge,
85
+ };
86
+ case 'movableCollateral':
87
+ return {
88
+ origin,
89
+ destination,
90
+ amount,
91
+ executionType: 'movableCollateral',
92
+ bridge: bridgeConfig.bridge,
93
+ };
94
+ default: {
95
+ const _exhaustive: never = bridgeConfig;
96
+ throw new Error(
97
+ `Unknown execution type: ${(_exhaustive as BridgeConfig).executionType}`,
98
+ );
99
+ }
100
+ }
32
101
  }
@@ -0,0 +1,272 @@
1
+ import { BigNumber } from 'ethers';
2
+ import type { Logger } from 'pino';
3
+
4
+ import {
5
+ type AnnotatedEV5Transaction,
6
+ type ChainName,
7
+ type InterchainGasQuote,
8
+ type MultiProvider,
9
+ type Token,
10
+ TokenStandard,
11
+ } from '@hyperlane-xyz/sdk';
12
+ import { addBufferToGasLimit, isZeroishAddress } from '@hyperlane-xyz/utils';
13
+
14
+ /**
15
+ * Fallback gas limit for transferRemote when eth_estimateGas fails.
16
+ * Conservative estimate for cross-chain token transfers.
17
+ */
18
+ export const FALLBACK_TRANSFER_REMOTE_GAS_LIMIT = 300_000n;
19
+
20
+ /**
21
+ * Cost multiplier for minimum viable transfer.
22
+ * A transfer must be worth at least this multiple of its cost to be worthwhile.
23
+ */
24
+ export const MIN_VIABLE_COST_MULTIPLIER = 2n;
25
+
26
+ /**
27
+ * Transfer cost estimate for native token transfers.
28
+ * Contains all cost components needed for transfer decisions.
29
+ */
30
+ export interface TransferCostEstimate {
31
+ /** IGP cost for the Hyperlane message */
32
+ igpCost: bigint;
33
+ /** Estimated gas cost for the transferRemote transaction (with buffer) */
34
+ gasCost: bigint;
35
+ /** Token fee cost (native denom only) */
36
+ tokenFeeCost: bigint;
37
+ /** Total cost = igpCost + gasCost + tokenFeeCost */
38
+ totalCost: bigint;
39
+ /** Maximum transferable amount after reserving costs (availableInventory - totalCost) */
40
+ maxTransferable: bigint;
41
+ /** Minimum viable transfer (totalCost * MIN_VIABLE_COST_MULTIPLIER) */
42
+ minViableTransfer: bigint;
43
+ /** Gas quote from adapter (for passing to executeTransferRemote) */
44
+ gasQuote?: InterchainGasQuote;
45
+ }
46
+
47
+ /**
48
+ * Estimate gas for a transferRemote transaction using eth_estimateGas.
49
+ * Falls back to conservative estimate if estimation fails.
50
+ *
51
+ * @param originChain - Chain where transferRemote will be called
52
+ * @param destinationChain - Chain where the Hyperlane message goes
53
+ * @param amount - Amount to transfer
54
+ * @param multiProvider - MultiProvider for chain access
55
+ * @param warpCoreMultiProvider - MultiProvider from WarpCore for adapter access
56
+ * @param getTokenForChain - Function to get token for a chain
57
+ * @param inventorySigner - Address of the inventory signer
58
+ * @param logger - Logger instance
59
+ * @returns Estimated gas limit for the transaction
60
+ */
61
+ export async function estimateTransferRemoteGas(
62
+ originChain: ChainName,
63
+ destinationChain: ChainName,
64
+ amount: bigint,
65
+ multiProvider: MultiProvider,
66
+ warpCoreMultiProvider: any,
67
+ getTokenForChain: (chain: ChainName) => Token | undefined,
68
+ inventorySigner: string,
69
+ logger: Logger,
70
+ ): Promise<bigint> {
71
+ const originToken = getTokenForChain(originChain);
72
+ if (!originToken) {
73
+ logger.warn(
74
+ { originChain },
75
+ 'No token found for origin chain, using fallback gas limit',
76
+ );
77
+ return FALLBACK_TRANSFER_REMOTE_GAS_LIMIT;
78
+ }
79
+
80
+ try {
81
+ const destinationDomain = multiProvider.getDomainId(destinationChain);
82
+ const adapter = originToken.getHypAdapter(warpCoreMultiProvider);
83
+
84
+ // Quote the IGP gas first (needed for the full transaction)
85
+ const gasQuote = await adapter.quoteTransferRemoteGas({
86
+ destination: destinationDomain,
87
+ sender: inventorySigner,
88
+ recipient: inventorySigner,
89
+ amount,
90
+ });
91
+
92
+ // Populate with minimal amount for gas estimation
93
+ // Gas cost is independent of transfer size (just a require check in _transferFromSender),
94
+ // and using minimal amount prevents eth_estimateGas from failing when account balance < requested amount
95
+ // Note: getHypAdapter returns IHypTokenAdapter<unknown> for protocol-agnostic support.
96
+ // For EVM chains (which inventory rebalancing uses), the actual type is AnnotatedEV5Transaction.
97
+ const populatedTx = (await adapter.populateTransferRemoteTx({
98
+ destination: destinationDomain,
99
+ recipient: inventorySigner,
100
+ weiAmountOrId: 1n,
101
+ interchainGas: gasQuote,
102
+ })) as AnnotatedEV5Transaction;
103
+
104
+ // Estimate gas using the provider
105
+ const provider = multiProvider.getProvider(originChain);
106
+ const gasEstimate = await provider.estimateGas({
107
+ to: populatedTx.to,
108
+ data: populatedTx.data,
109
+ value: populatedTx.value,
110
+ from: inventorySigner,
111
+ });
112
+
113
+ const estimatedGas = BigInt(gasEstimate.toString());
114
+
115
+ logger.debug(
116
+ {
117
+ originChain,
118
+ destinationChain,
119
+ amount: amount.toString(),
120
+ estimatedGas: estimatedGas.toString(),
121
+ },
122
+ 'Estimated transferRemote gas via eth_estimateGas',
123
+ );
124
+
125
+ return estimatedGas;
126
+ } catch (error) {
127
+ logger.warn(
128
+ {
129
+ originChain,
130
+ destinationChain,
131
+ error: (error as Error).message,
132
+ fallbackGas: FALLBACK_TRANSFER_REMOTE_GAS_LIMIT.toString(),
133
+ },
134
+ 'Gas estimation failed, using fallback gas limit',
135
+ );
136
+ return FALLBACK_TRANSFER_REMOTE_GAS_LIMIT;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Calculate all transfer costs for a transferRemote operation.
142
+ * Consolidates IGP costs, gas costs, and derived values (max transferable, min viable).
143
+ *
144
+ * @param originChain - Chain to transfer from (where transferRemote is called)
145
+ * @param destinationChain - Chain to transfer to (Hyperlane message destination)
146
+ * @param availableInventory - Available token balance on origin chain
147
+ * @param requestedAmount - Requested transfer amount
148
+ * @param multiProvider - MultiProvider for chain access
149
+ * @param warpCoreMultiProvider - MultiProvider from WarpCore for adapter access
150
+ * @param getTokenForChain - Function to get token for a chain
151
+ * @param inventorySigner - Address of the inventory signer
152
+ * @param isNativeTokenStandard - Function to check if token standard is native
153
+ * @param logger - Logger instance
154
+ * @returns Cost estimate with all components and derived values
155
+ */
156
+ export async function calculateTransferCosts(
157
+ originChain: ChainName,
158
+ destinationChain: ChainName,
159
+ availableInventory: bigint,
160
+ requestedAmount: bigint,
161
+ multiProvider: MultiProvider,
162
+ warpCoreMultiProvider: any,
163
+ getTokenForChain: (chain: ChainName) => Token | undefined,
164
+ inventorySigner: string,
165
+ isNativeTokenStandard: (standard: TokenStandard) => boolean,
166
+ logger: Logger,
167
+ ): Promise<TransferCostEstimate> {
168
+ const originToken = getTokenForChain(originChain);
169
+ if (!originToken) {
170
+ throw new Error(`No token found for origin chain: ${originChain}`);
171
+ }
172
+
173
+ const destinationDomain = multiProvider.getDomainId(destinationChain);
174
+ const adapter = originToken.getHypAdapter(warpCoreMultiProvider);
175
+
176
+ // Always quote IGP for the gas quote (needed for populateTransferRemoteTx)
177
+ const gasQuote = await adapter.quoteTransferRemoteGas({
178
+ destination: destinationDomain,
179
+ sender: inventorySigner,
180
+ recipient: inventorySigner,
181
+ amount: requestedAmount,
182
+ });
183
+
184
+ // For non-native tokens, no cost reservation needed from token balance
185
+ if (!isNativeTokenStandard(originToken.standard)) {
186
+ return {
187
+ igpCost: 0n,
188
+ gasCost: 0n,
189
+ tokenFeeCost: 0n,
190
+ totalCost: 0n,
191
+ maxTransferable:
192
+ availableInventory < requestedAmount
193
+ ? availableInventory
194
+ : requestedAmount,
195
+ minViableTransfer: 0n,
196
+ gasQuote,
197
+ };
198
+ }
199
+
200
+ // For native tokens, calculate costs
201
+ const igpCost = gasQuote.igpQuote.amount;
202
+
203
+ // Extract token fee cost (native denom only)
204
+ const tokenFeeCost =
205
+ !gasQuote.tokenFeeQuote?.addressOrDenom ||
206
+ isZeroishAddress(gasQuote.tokenFeeQuote.addressOrDenom)
207
+ ? (gasQuote.tokenFeeQuote?.amount ?? 0n)
208
+ : 0n;
209
+
210
+ // Estimate gas with buffer
211
+ const estimatedGasLimit = await estimateTransferRemoteGas(
212
+ originChain,
213
+ destinationChain,
214
+ requestedAmount,
215
+ multiProvider,
216
+ warpCoreMultiProvider,
217
+ getTokenForChain,
218
+ inventorySigner,
219
+ logger,
220
+ );
221
+ const bufferedGasLimit = addBufferToGasLimit(
222
+ BigNumber.from(estimatedGasLimit.toString()),
223
+ );
224
+
225
+ // Get gas price and calculate cost
226
+ const provider = multiProvider.getProvider(originChain);
227
+ const feeData = await provider.getFeeData();
228
+ const gasPrice = feeData.maxFeePerGas ?? feeData.gasPrice ?? 0n;
229
+ const gasCost = bufferedGasLimit.toBigInt() * BigInt(gasPrice.toString());
230
+
231
+ const totalCost = igpCost + gasCost + tokenFeeCost;
232
+
233
+ // Calculate derived values
234
+ let maxTransferable: bigint;
235
+ if (availableInventory <= totalCost) {
236
+ maxTransferable = 0n;
237
+ } else {
238
+ const maxAfterReservation = availableInventory - totalCost;
239
+ maxTransferable =
240
+ maxAfterReservation < requestedAmount
241
+ ? maxAfterReservation
242
+ : requestedAmount;
243
+ }
244
+
245
+ const minViableTransfer = totalCost * MIN_VIABLE_COST_MULTIPLIER;
246
+
247
+ logger.debug(
248
+ {
249
+ originChain,
250
+ destinationChain,
251
+ availableInventory: availableInventory.toString(),
252
+ requestedAmount: requestedAmount.toString(),
253
+ igpCost: igpCost.toString(),
254
+ gasCost: gasCost.toString(),
255
+ tokenFeeCost: tokenFeeCost.toString(),
256
+ totalCost: totalCost.toString(),
257
+ maxTransferable: maxTransferable.toString(),
258
+ minViableTransfer: minViableTransfer.toString(),
259
+ },
260
+ 'Calculated transfer costs for native token',
261
+ );
262
+
263
+ return {
264
+ igpCost,
265
+ gasCost,
266
+ tokenFeeCost,
267
+ totalCost,
268
+ maxTransferable,
269
+ minViableTransfer,
270
+ gasQuote,
271
+ };
272
+ }
@@ -5,6 +5,18 @@ const REBALANCEABLE_TOKEN_COLLATERALIZED_STANDARDS = new Set<TokenStandard>([
5
5
  TokenStandard.EvmHypNative,
6
6
  ]);
7
7
 
8
+ /**
9
+ * Check if a token's balance is the same as native gas balance.
10
+ * For these tokens, we must reserve funds for IGP when calculating max transferable.
11
+ *
12
+ * @param standard - The token standard to check.
13
+ * @returns `true` if the token is a native token standard, `false` otherwise.
14
+ */
15
+ export function isNativeTokenStandard(standard: TokenStandard): boolean {
16
+ // EvmHypNative covers all native token types including scaled variants
17
+ return standard === TokenStandard.EvmHypNative;
18
+ }
19
+
8
20
  /**
9
21
  * @dev This function exists because the rebalancer currently only supports a subset of collateralized token standards
10
22
  * (see `REBALANCEABLE_TOKEN_COLLATERALIZED_STANDARDS` vs. all possible `TOKEN_COLLATERALIZED_STANDARDS`).
@@ -1,7 +0,0 @@
1
- export type { IStore } from './store/index.js';
2
- export { InMemoryStore } from './store/index.js';
3
- export type { Identifiable, CrossChainAction, Timestamped, TrackedActionBase, TransferStatus, RebalanceIntentStatus, RebalanceActionStatus, Transfer, RebalanceIntent, RebalanceAction, ITransferStore, IRebalanceIntentStore, IRebalanceActionStore, } from './types.js';
4
- export { ActionTracker, type ActionTrackerConfig } from './ActionTracker.js';
5
- export type { IActionTracker, CreateRebalanceIntentParams, CreateRebalanceActionParams, } from './IActionTracker.js';
6
- export { InflightContextAdapter } from './InflightContextAdapter.js';
7
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tracking/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGjD,YAAY,EAEV,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,iBAAiB,EAEjB,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EAErB,QAAQ,EACR,eAAe,EACf,eAAe,EAEf,cAAc,EACd,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE7E,YAAY,EACV,cAAc,EACd,2BAA2B,EAC3B,2BAA2B,GAC5B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC"}
@@ -1,6 +0,0 @@
1
- export { InMemoryStore } from './store/index.js';
2
- // Export ActionTracker components
3
- export { ActionTracker } from './ActionTracker.js';
4
- // Export InflightContextAdapter
5
- export { InflightContextAdapter } from './InflightContextAdapter.js';
6
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tracking/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAuBjD,kCAAkC;AAClC,OAAO,EAAE,aAAa,EAA4B,MAAM,oBAAoB,CAAC;AAQ7E,gCAAgC;AAChC,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC"}
@@ -1,5 +0,0 @@
1
- export * from './balanceUtils.js';
2
- export * from './blockTag.js';
3
- export * from './bridgeUtils.js';
4
- export * from './tokenUtils.js';
5
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC"}
@@ -1,5 +0,0 @@
1
- export * from './balanceUtils.js';
2
- export * from './blockTag.js';
3
- export * from './bridgeUtils.js';
4
- export * from './tokenUtils.js';
5
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC"}