@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
@@ -16,18 +16,24 @@ import {
16
16
  import type { RebalancerConfig } from '../config/RebalancerConfig.js';
17
17
  import { RebalancerStrategyOptions } from '../config/types.js';
18
18
  import type {
19
+ ExecutionResult,
19
20
  IRebalancer,
21
+ MovableCollateralExecutionResult,
20
22
  PreparedTransaction,
21
- RebalanceExecutionResult,
22
- RebalanceRoute,
23
+ RebalancerType,
23
24
  } from '../interfaces/IRebalancer.js';
24
- import type { StrategyRoute } from '../interfaces/IStrategy.js';
25
- import type { BridgeConfigWithOverride } from '../utils/index.js';
25
+ import type {
26
+ MovableCollateralRoute,
27
+ StrategyRoute,
28
+ } from '../interfaces/IStrategy.js';
29
+ import type { BridgeConfigWithOverride } from '../utils/bridgeUtils.js';
26
30
 
27
31
  // === Mock Classes ===
28
32
 
29
33
  export class MockRebalancer implements IRebalancer {
30
- rebalance(_routes: RebalanceRoute[]): Promise<RebalanceExecutionResult[]> {
34
+ readonly rebalancerType: RebalancerType = 'movableCollateral';
35
+
36
+ rebalance(_routes: MovableCollateralRoute[]): Promise<ExecutionResult[]> {
31
37
  return Promise.resolve([]);
32
38
  }
33
39
  }
@@ -36,33 +42,45 @@ export class MockRebalancer implements IRebalancer {
36
42
 
37
43
  export function buildTestRoute(
38
44
  overrides: Partial<StrategyRoute> = {},
45
+ executionType: 'movableCollateral' | 'inventory' = 'movableCollateral',
39
46
  ): StrategyRoute {
47
+ if (executionType === 'inventory') {
48
+ return {
49
+ origin: 'ethereum',
50
+ destination: 'arbitrum',
51
+ amount: ethers.utils.parseEther('100').toBigInt(),
52
+ executionType: 'inventory',
53
+ externalBridge: 'lifi',
54
+ ...overrides,
55
+ } as StrategyRoute;
56
+ }
40
57
  return {
41
58
  origin: 'ethereum',
42
59
  destination: 'arbitrum',
43
60
  amount: ethers.utils.parseEther('100').toBigInt(),
61
+ executionType: 'movableCollateral',
44
62
  bridge: TEST_ADDRESSES.bridge,
45
63
  ...overrides,
46
- };
64
+ } as StrategyRoute;
47
65
  }
48
66
 
49
- export function buildTestRebalanceRoute(
50
- overrides: Partial<RebalanceRoute> = {},
51
- ): RebalanceRoute {
67
+ export function buildTestMovableCollateralRoute(
68
+ overrides: Partial<MovableCollateralRoute> = {},
69
+ ): MovableCollateralRoute {
52
70
  return {
53
- intentId: overrides.intentId ?? `test-route-${Date.now()}`,
54
71
  origin: 'ethereum',
55
72
  destination: 'arbitrum',
56
73
  amount: ethers.utils.parseEther('100').toBigInt(),
74
+ executionType: 'movableCollateral',
57
75
  bridge: TEST_ADDRESSES.bridge,
58
76
  ...overrides,
59
77
  };
60
78
  }
61
79
 
62
80
  export function buildTestResult(
63
- overrides: Partial<RebalanceExecutionResult> = {},
64
- ): RebalanceExecutionResult {
65
- const route = overrides.route ?? buildTestRebalanceRoute();
81
+ overrides: Partial<MovableCollateralExecutionResult> = {},
82
+ ): MovableCollateralExecutionResult {
83
+ const route = overrides.route ?? buildTestMovableCollateralRoute();
66
84
  return {
67
85
  route,
68
86
  success: true,
@@ -77,7 +95,12 @@ export function buildTestResult(
77
95
  export function buildTestPreparedTransaction(
78
96
  overrides: Partial<PreparedTransaction> = {},
79
97
  ): PreparedTransaction {
80
- const route = overrides.route ?? buildTestRebalanceRoute();
98
+ const route =
99
+ overrides.route ??
100
+ ({
101
+ ...buildTestMovableCollateralRoute(),
102
+ intentId: 'test-intent',
103
+ } as MovableCollateralRoute & { intentId: string });
81
104
  return {
82
105
  populatedTx: {
83
106
  to: TEST_ADDRESSES.token,
@@ -272,6 +295,7 @@ export function buildTestBridges(
272
295
  ): ChainMap<BridgeConfigWithOverride> {
273
296
  return chains.reduce((acc, chain) => {
274
297
  acc[chain] = {
298
+ executionType: 'movableCollateral',
275
299
  bridge: TEST_ADDRESSES.bridge,
276
300
  bridgeMinAcceptedAmount: 0,
277
301
  };
@@ -291,6 +315,7 @@ export function extractBridgeConfigs(
291
315
  ): ChainMap<BridgeConfigWithOverride> {
292
316
  return Object.entries(chainConfig).reduce((acc, [chain, config]) => {
293
317
  acc[chain] = {
318
+ executionType: 'movableCollateral',
294
319
  bridge: config.bridge,
295
320
  bridgeMinAcceptedAmount: config.bridgeMinAcceptedAmount ?? 0,
296
321
  };
@@ -0,0 +1,174 @@
1
+ import Sinon, { type SinonStub } from 'sinon';
2
+
3
+ import type {
4
+ BridgeQuote,
5
+ BridgeTransferStatus,
6
+ } from '../interfaces/IExternalBridge.js';
7
+
8
+ /**
9
+ * Create mock functions for LiFi SDK.
10
+ */
11
+ export function createLiFiSdkMocks() {
12
+ return {
13
+ createConfig: Sinon.stub(),
14
+ getQuote: Sinon.stub(),
15
+ executeRoute: Sinon.stub(),
16
+ getStatus: Sinon.stub(),
17
+ convertQuoteToRoute: Sinon.stub().callsFake((quote: unknown) => {
18
+ const q = quote as Record<string, unknown> & {
19
+ action?: { fromChainId?: number; toChainId?: number };
20
+ };
21
+ return {
22
+ ...q,
23
+ fromChainId: q.action?.fromChainId ?? 42161,
24
+ toChainId: q.action?.toChainId ?? 1399811149,
25
+ steps: [],
26
+ };
27
+ }),
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Configure a mock getQuote to return a successful quote.
33
+ */
34
+ export function mockSuccessfulQuote(
35
+ stub: SinonStub,
36
+ overrides?: Partial<{
37
+ id: string;
38
+ tool: string;
39
+ fromAmount: string;
40
+ toAmount: string;
41
+ toAmountMin: string;
42
+ executionDuration: number;
43
+ fromChainId: number;
44
+ toChainId: number;
45
+ }>,
46
+ ) {
47
+ const fromChainId = overrides?.fromChainId ?? 42161;
48
+ const toChainId = overrides?.toChainId ?? 1399811149;
49
+ const fromAmount = overrides?.fromAmount ?? '10000000000';
50
+ const toAmount = overrides?.toAmount ?? '9950000000';
51
+ const toAmountMin = overrides?.toAmountMin ?? '9900000000';
52
+
53
+ stub.resolves({
54
+ id: overrides?.id ?? 'quote-123',
55
+ tool: overrides?.tool ?? 'across',
56
+ action: {
57
+ fromAmount,
58
+ fromChainId,
59
+ toChainId,
60
+ },
61
+ estimate: {
62
+ toAmount,
63
+ toAmountMin,
64
+ executionDuration: overrides?.executionDuration ?? 300,
65
+ },
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Configure a mock executeRoute to return a successful execution.
71
+ */
72
+ export function mockSuccessfulExecution(stub: SinonStub, txHash: string) {
73
+ stub.resolves({
74
+ steps: [
75
+ {
76
+ execution: {
77
+ process: [{ txHash }],
78
+ },
79
+ },
80
+ ],
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Configure a mock getStatus to return a specific status.
86
+ */
87
+ export function mockLiFiStatus(
88
+ stub: SinonStub,
89
+ status: 'DONE' | 'PENDING' | 'FAILED' | 'NOT_FOUND',
90
+ overrides?: Partial<{
91
+ receivingTxHash: string;
92
+ amount: string;
93
+ substatus: string;
94
+ }>,
95
+ ) {
96
+ const responses: Record<string, unknown> = {
97
+ DONE: {
98
+ status: 'DONE',
99
+ receiving: {
100
+ txHash: overrides?.receivingTxHash ?? '0xReceivingTxHash',
101
+ amount: overrides?.amount ?? '9950000000',
102
+ },
103
+ },
104
+ PENDING: {
105
+ status: 'PENDING',
106
+ substatus: overrides?.substatus ?? 'WAIT_SOURCE_CONFIRMATIONS',
107
+ },
108
+ FAILED: {
109
+ status: 'FAILED',
110
+ substatus: overrides?.substatus ?? 'BRIDGE_CALL_FAILED',
111
+ },
112
+ NOT_FOUND: {
113
+ status: 'NOT_FOUND',
114
+ },
115
+ };
116
+
117
+ stub.resolves(responses[status]);
118
+ }
119
+
120
+ /**
121
+ * Create a mock BridgeQuote for testing.
122
+ */
123
+ export function createMockBridgeQuote(
124
+ overrides?: Partial<BridgeQuote>,
125
+ ): BridgeQuote {
126
+ return {
127
+ id: 'quote-123',
128
+ tool: 'across',
129
+ fromAmount: 10000000000n,
130
+ toAmount: 9950000000n,
131
+ toAmountMin: 9900000000n,
132
+ executionDuration: 300,
133
+ gasCosts: 50000000n, // Default mock gas costs
134
+ feeCosts: 0n, // Default mock fee costs
135
+ route: {
136
+ action: { fromChainId: 42161, toChainId: 1399811149 },
137
+ },
138
+ ...overrides,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Create a mock BridgeTransferStatus for testing.
144
+ */
145
+ export function createMockBridgeStatus(
146
+ status: 'pending' | 'complete' | 'failed' | 'not_found',
147
+ overrides?: Partial<{
148
+ substatus: string;
149
+ receivingTxHash: string;
150
+ receivedAmount: bigint;
151
+ error: string;
152
+ }>,
153
+ ): BridgeTransferStatus {
154
+ switch (status) {
155
+ case 'pending':
156
+ return {
157
+ status: 'pending',
158
+ substatus: overrides?.substatus,
159
+ };
160
+ case 'complete':
161
+ return {
162
+ status: 'complete',
163
+ receivingTxHash: overrides?.receivingTxHash ?? '0xReceivingTxHash',
164
+ receivedAmount: overrides?.receivedAmount ?? 9950000000n,
165
+ };
166
+ case 'failed':
167
+ return {
168
+ status: 'failed',
169
+ error: overrides?.error,
170
+ };
171
+ case 'not_found':
172
+ return { status: 'not_found' };
173
+ }
174
+ }
@@ -148,6 +148,7 @@ describe('ActionTracker', () => {
148
148
  // Pre-create action
149
149
  await rebalanceActionStore.save({
150
150
  id: '0xmsg1',
151
+ type: 'rebalance_message',
151
152
  status: 'in_progress',
152
153
  intentId: 'existing-intent',
153
154
  messageId: '0xmsg1',
@@ -269,18 +270,32 @@ describe('ActionTracker', () => {
269
270
 
270
271
  describe('syncRebalanceIntents', () => {
271
272
  it('should mark intents as complete when fully fulfilled', async () => {
273
+ // Intent derives completion from action states, so we need a complete action
272
274
  const intent: RebalanceIntent = {
273
275
  id: 'intent-1',
274
276
  status: 'in_progress',
275
277
  origin: 1,
276
278
  destination: 2,
277
279
  amount: 100n,
278
- fulfilledAmount: 100n,
280
+ createdAt: Date.now(),
281
+ updatedAt: Date.now(),
282
+ };
283
+
284
+ const action: RebalanceAction = {
285
+ id: 'action-1',
286
+ type: 'rebalance_message',
287
+ status: 'complete',
288
+ intentId: 'intent-1',
289
+ messageId: '0xmsg1',
290
+ origin: 1,
291
+ destination: 2,
292
+ amount: 100n,
279
293
  createdAt: Date.now(),
280
294
  updatedAt: Date.now(),
281
295
  };
282
296
 
283
297
  await rebalanceIntentStore.save(intent);
298
+ await rebalanceActionStore.save(action);
284
299
 
285
300
  await tracker.syncRebalanceIntents();
286
301
 
@@ -289,18 +304,32 @@ describe('ActionTracker', () => {
289
304
  });
290
305
 
291
306
  it('should not mark intents as complete if not fully fulfilled', async () => {
307
+ // Intent with only partial completion via actions
292
308
  const intent: RebalanceIntent = {
293
309
  id: 'intent-1',
294
310
  status: 'in_progress',
295
311
  origin: 1,
296
312
  destination: 2,
297
313
  amount: 100n,
298
- fulfilledAmount: 50n,
314
+ createdAt: Date.now(),
315
+ updatedAt: Date.now(),
316
+ };
317
+
318
+ const action: RebalanceAction = {
319
+ id: 'action-1',
320
+ type: 'rebalance_message',
321
+ status: 'complete',
322
+ intentId: 'intent-1',
323
+ messageId: '0xmsg1',
324
+ origin: 1,
325
+ destination: 2,
326
+ amount: 50n, // Only partial
299
327
  createdAt: Date.now(),
300
328
  updatedAt: Date.now(),
301
329
  };
302
330
 
303
331
  await rebalanceIntentStore.save(intent);
332
+ await rebalanceActionStore.save(action);
304
333
 
305
334
  await tracker.syncRebalanceIntents();
306
335
 
@@ -317,13 +346,13 @@ describe('ActionTracker', () => {
317
346
  origin: 1,
318
347
  destination: 2,
319
348
  amount: 100n,
320
- fulfilledAmount: 0n,
321
349
  createdAt: Date.now(),
322
350
  updatedAt: Date.now(),
323
351
  };
324
352
 
325
353
  const action: RebalanceAction = {
326
354
  id: 'action-1',
355
+ type: 'rebalance_message',
327
356
  status: 'in_progress',
328
357
  intentId: 'intent-1',
329
358
  messageId: '0xmsg1',
@@ -345,15 +374,15 @@ describe('ActionTracker', () => {
345
374
  const updatedAction = await rebalanceActionStore.get('action-1');
346
375
  expect(updatedAction?.status).to.equal('complete');
347
376
 
348
- // Intent should be updated and complete
377
+ // Intent should be complete (derived from completed action amounts)
349
378
  const updatedIntent = await rebalanceIntentStore.get('intent-1');
350
- expect(updatedIntent?.fulfilledAmount).to.equal(100n);
351
379
  expect(updatedIntent?.status).to.equal('complete');
352
380
  });
353
381
 
354
382
  it('should not mark actions as complete if not delivered', async () => {
355
383
  const action: RebalanceAction = {
356
384
  id: 'action-1',
385
+ type: 'rebalance_message',
357
386
  status: 'in_progress',
358
387
  intentId: 'intent-1',
359
388
  messageId: '0xmsg1',
@@ -417,7 +446,6 @@ describe('ActionTracker', () => {
417
446
  origin: 1,
418
447
  destination: 2,
419
448
  amount: 100n,
420
- fulfilledAmount: 0n,
421
449
  createdAt: Date.now(),
422
450
  updatedAt: Date.now(),
423
451
  });
@@ -428,7 +456,6 @@ describe('ActionTracker', () => {
428
456
  origin: 2,
429
457
  destination: 3,
430
458
  amount: 200n,
431
- fulfilledAmount: 50n,
432
459
  createdAt: Date.now(),
433
460
  updatedAt: Date.now(),
434
461
  });
@@ -439,7 +466,6 @@ describe('ActionTracker', () => {
439
466
  origin: 3,
440
467
  destination: 1,
441
468
  amount: 300n,
442
- fulfilledAmount: 300n,
443
469
  createdAt: Date.now(),
444
470
  updatedAt: Date.now(),
445
471
  });
@@ -452,6 +478,85 @@ describe('ActionTracker', () => {
452
478
  });
453
479
  });
454
480
 
481
+ describe('getPartiallyFulfilledInventoryIntents', () => {
482
+ it('returns not_started inventory intents', async () => {
483
+ // Create a not_started inventory intent (simulates failed execution before any action created)
484
+ await rebalanceIntentStore.save({
485
+ id: 'stuck-intent',
486
+ status: 'not_started',
487
+ origin: 1,
488
+ destination: 2,
489
+ amount: 1000000000000000000n, // 1 ETH
490
+ executionMethod: 'inventory',
491
+ createdAt: Date.now(),
492
+ updatedAt: Date.now(),
493
+ });
494
+
495
+ // Should be returned even though status is 'not_started'
496
+ const partialIntents =
497
+ await tracker.getPartiallyFulfilledInventoryIntents();
498
+
499
+ expect(partialIntents).to.have.lengthOf(1);
500
+ expect(partialIntents[0].intent.id).to.equal('stuck-intent');
501
+ expect(partialIntents[0].completedAmount).to.equal(0n);
502
+ expect(partialIntents[0].remaining).to.equal(1000000000000000000n);
503
+ });
504
+
505
+ it('returns in_progress inventory intents with partial completion', async () => {
506
+ // Create an in_progress inventory intent with a completed action
507
+ await rebalanceIntentStore.save({
508
+ id: 'partial-intent',
509
+ status: 'in_progress',
510
+ origin: 1,
511
+ destination: 2,
512
+ amount: 1000000000000000000n, // 1 ETH
513
+ executionMethod: 'inventory',
514
+ createdAt: Date.now(),
515
+ updatedAt: Date.now(),
516
+ });
517
+
518
+ // Create a completed inventory_deposit action for partial amount
519
+ await rebalanceActionStore.save({
520
+ id: 'action-1',
521
+ type: 'inventory_deposit',
522
+ status: 'complete',
523
+ intentId: 'partial-intent',
524
+ origin: 1,
525
+ destination: 2,
526
+ amount: 400000000000000000n, // 0.4 ETH completed
527
+ createdAt: Date.now(),
528
+ updatedAt: Date.now(),
529
+ });
530
+
531
+ const partialIntents =
532
+ await tracker.getPartiallyFulfilledInventoryIntents();
533
+
534
+ expect(partialIntents).to.have.lengthOf(1);
535
+ expect(partialIntents[0].intent.id).to.equal('partial-intent');
536
+ expect(partialIntents[0].completedAmount).to.equal(400000000000000000n);
537
+ expect(partialIntents[0].remaining).to.equal(600000000000000000n); // 0.6 ETH remaining
538
+ });
539
+
540
+ it('does not return non-inventory intents', async () => {
541
+ // Create a not_started intent without executionMethod: 'inventory'
542
+ await rebalanceIntentStore.save({
543
+ id: 'non-inventory-intent',
544
+ status: 'not_started',
545
+ origin: 1,
546
+ destination: 2,
547
+ amount: 1000000000000000000n,
548
+ // executionMethod is undefined - not an inventory intent
549
+ createdAt: Date.now(),
550
+ updatedAt: Date.now(),
551
+ });
552
+
553
+ const partialIntents =
554
+ await tracker.getPartiallyFulfilledInventoryIntents();
555
+
556
+ expect(partialIntents).to.have.lengthOf(0);
557
+ });
558
+ });
559
+
455
560
  describe('createRebalanceIntent', () => {
456
561
  it('should create a new intent with status not_started', async () => {
457
562
  const result = await tracker.createRebalanceIntent({
@@ -466,7 +571,6 @@ describe('ActionTracker', () => {
466
571
  expect(result.origin).to.equal(1);
467
572
  expect(result.destination).to.equal(2);
468
573
  expect(result.amount).to.equal(100n);
469
- expect(result.fulfilledAmount).to.equal(0n);
470
574
  expect(result.priority).to.equal(1);
471
575
  expect(result.strategyType).to.equal('MinAmountStrategy');
472
576
 
@@ -483,7 +587,6 @@ describe('ActionTracker', () => {
483
587
  origin: 1,
484
588
  destination: 2,
485
589
  amount: 100n,
486
- fulfilledAmount: 0n,
487
590
  createdAt: Date.now(),
488
591
  updatedAt: Date.now(),
489
592
  };
@@ -491,6 +594,7 @@ describe('ActionTracker', () => {
491
594
  await rebalanceIntentStore.save(intent);
492
595
 
493
596
  const result = await tracker.createRebalanceAction({
597
+ type: 'rebalance_message',
494
598
  intentId: 'intent-1',
495
599
  origin: 1,
496
600
  destination: 2,
@@ -514,7 +618,6 @@ describe('ActionTracker', () => {
514
618
  origin: 1,
515
619
  destination: 2,
516
620
  amount: 100n,
517
- fulfilledAmount: 50n,
518
621
  createdAt: Date.now(),
519
622
  updatedAt: Date.now(),
520
623
  };
@@ -522,6 +625,7 @@ describe('ActionTracker', () => {
522
625
  await rebalanceIntentStore.save(intent);
523
626
 
524
627
  await tracker.createRebalanceAction({
628
+ type: 'rebalance_message',
525
629
  intentId: 'intent-1',
526
630
  origin: 1,
527
631
  destination: 2,
@@ -532,25 +636,24 @@ describe('ActionTracker', () => {
532
636
 
533
637
  const updatedIntent = await rebalanceIntentStore.get('intent-1');
534
638
  expect(updatedIntent?.status).to.equal('in_progress');
535
- expect(updatedIntent?.fulfilledAmount).to.equal(50n); // Should not change
536
639
  });
537
640
  });
538
641
 
539
642
  describe('completeRebalanceAction', () => {
540
- it('should mark action as complete and update parent intent fulfilledAmount', async () => {
643
+ it('should mark action as complete and mark parent intent complete if fully fulfilled', async () => {
541
644
  const intent: RebalanceIntent = {
542
645
  id: 'intent-1',
543
646
  status: 'in_progress',
544
647
  origin: 1,
545
648
  destination: 2,
546
649
  amount: 100n,
547
- fulfilledAmount: 0n,
548
650
  createdAt: Date.now(),
549
651
  updatedAt: Date.now(),
550
652
  };
551
653
 
552
654
  const action: RebalanceAction = {
553
655
  id: 'action-1',
656
+ type: 'rebalance_message',
554
657
  status: 'in_progress',
555
658
  intentId: 'intent-1',
556
659
  messageId: '0xmsg1',
@@ -569,8 +672,8 @@ describe('ActionTracker', () => {
569
672
  const updatedAction = await rebalanceActionStore.get('action-1');
570
673
  expect(updatedAction?.status).to.equal('complete');
571
674
 
675
+ // Intent should be complete (derived from completed action amounts)
572
676
  const updatedIntent = await rebalanceIntentStore.get('intent-1');
573
- expect(updatedIntent?.fulfilledAmount).to.equal(100n);
574
677
  expect(updatedIntent?.status).to.equal('complete');
575
678
  });
576
679
 
@@ -589,7 +692,6 @@ describe('ActionTracker', () => {
589
692
  origin: 1,
590
693
  destination: 2,
591
694
  amount: 100n,
592
- fulfilledAmount: 0n,
593
695
  createdAt: Date.now(),
594
696
  updatedAt: Date.now(),
595
697
  };
@@ -607,6 +709,7 @@ describe('ActionTracker', () => {
607
709
  it('should mark action as failed', async () => {
608
710
  const action: RebalanceAction = {
609
711
  id: 'action-1',
712
+ type: 'rebalance_message',
610
713
  status: 'in_progress',
611
714
  intentId: 'intent-1',
612
715
  messageId: '0xmsg1',
@@ -653,7 +756,7 @@ describe('ActionTracker', () => {
653
756
 
654
757
  const params = call.args[0];
655
758
  expect(params.routersByDomain).to.deep.equal(config.routersByDomain);
656
- expect(params.excludeTxSender).to.equal(config.rebalancerAddress);
759
+ expect(params.excludeTxSenders).to.deep.equal([config.rebalancerAddress]);
657
760
  });
658
761
  });
659
762
 
@@ -692,13 +795,13 @@ describe('ActionTracker', () => {
692
795
  origin: 1,
693
796
  destination: 2,
694
797
  amount: 100n,
695
- fulfilledAmount: 0n,
696
798
  createdAt: Date.now(),
697
799
  updatedAt: Date.now(),
698
800
  };
699
801
 
700
802
  const action: RebalanceAction = {
701
803
  id: 'action-1',
804
+ type: 'rebalance_message',
702
805
  status: 'in_progress',
703
806
  intentId: 'intent-1',
704
807
  messageId: '0xmsg1',
@@ -806,7 +909,6 @@ describe('ActionTracker', () => {
806
909
  origin: 1,
807
910
  destination: 2,
808
911
  amount: 100n,
809
- fulfilledAmount: 0n,
810
912
  createdAt: Date.now(),
811
913
  updatedAt: Date.now(),
812
914
  };
@@ -814,6 +916,7 @@ describe('ActionTracker', () => {
814
916
  const action: RebalanceAction = {
815
917
  id: 'action-1',
816
918
  status: 'in_progress',
919
+ type: 'rebalance_message',
817
920
  intentId: 'intent-1',
818
921
  messageId: '0xmsg1',
819
922
  origin: 1,