@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,16 +1,394 @@
1
- import { ethers } from 'ethers';
1
+ import { type PopulatedTransaction, ethers, type providers } from 'ethers';
2
+ import Sinon from 'sinon';
3
+
4
+ import {
5
+ type ChainMap,
6
+ type ChainMetadata,
7
+ type ChainName,
8
+ EvmMovableCollateralAdapter,
9
+ type InterchainGasQuote,
10
+ type MultiProvider,
11
+ type Token,
12
+ type TokenAmount,
13
+ type WarpCore,
14
+ } from '@hyperlane-xyz/sdk';
2
15
 
3
16
  import type { RebalancerConfig } from '../config/RebalancerConfig.js';
4
17
  import { RebalancerStrategyOptions } from '../config/types.js';
5
- import type { IRebalancer } from '../interfaces/IRebalancer.js';
6
- import type { RebalancingRoute } from '../interfaces/IStrategy.js';
18
+ import type {
19
+ IRebalancer,
20
+ PreparedTransaction,
21
+ RebalanceExecutionResult,
22
+ RebalanceRoute,
23
+ } from '../interfaces/IRebalancer.js';
24
+ import type { StrategyRoute } from '../interfaces/IStrategy.js';
25
+ import type { BridgeConfigWithOverride } from '../utils/index.js';
26
+
27
+ // === Mock Classes ===
7
28
 
8
29
  export class MockRebalancer implements IRebalancer {
9
- rebalance(_routes: RebalancingRoute[]): Promise<void> {
10
- return Promise.resolve();
30
+ rebalance(_routes: RebalanceRoute[]): Promise<RebalanceExecutionResult[]> {
31
+ return Promise.resolve([]);
11
32
  }
12
33
  }
13
34
 
35
+ // === Test Data Builders ===
36
+
37
+ export function buildTestRoute(
38
+ overrides: Partial<StrategyRoute> = {},
39
+ ): StrategyRoute {
40
+ return {
41
+ origin: 'ethereum',
42
+ destination: 'arbitrum',
43
+ amount: ethers.utils.parseEther('100').toBigInt(),
44
+ bridge: TEST_ADDRESSES.bridge,
45
+ ...overrides,
46
+ };
47
+ }
48
+
49
+ export function buildTestRebalanceRoute(
50
+ overrides: Partial<RebalanceRoute> = {},
51
+ ): RebalanceRoute {
52
+ return {
53
+ intentId: overrides.intentId ?? `test-route-${Date.now()}`,
54
+ origin: 'ethereum',
55
+ destination: 'arbitrum',
56
+ amount: ethers.utils.parseEther('100').toBigInt(),
57
+ bridge: TEST_ADDRESSES.bridge,
58
+ ...overrides,
59
+ };
60
+ }
61
+
62
+ export function buildTestResult(
63
+ overrides: Partial<RebalanceExecutionResult> = {},
64
+ ): RebalanceExecutionResult {
65
+ const route = overrides.route ?? buildTestRebalanceRoute();
66
+ return {
67
+ route,
68
+ success: true,
69
+ messageId:
70
+ '0x1111111111111111111111111111111111111111111111111111111111111111',
71
+ txHash:
72
+ '0x2222222222222222222222222222222222222222222222222222222222222222',
73
+ ...overrides,
74
+ };
75
+ }
76
+
77
+ export function buildTestPreparedTransaction(
78
+ overrides: Partial<PreparedTransaction> = {},
79
+ ): PreparedTransaction {
80
+ const route = overrides.route ?? buildTestRebalanceRoute();
81
+ return {
82
+ populatedTx: {
83
+ to: TEST_ADDRESSES.token,
84
+ data: '0x',
85
+ value: ethers.BigNumber.from(0),
86
+ } as PopulatedTransaction,
87
+ route,
88
+ originTokenAmount: createMockTokenAmount(route.amount),
89
+ ...overrides,
90
+ };
91
+ }
92
+
93
+ // === Mock Factories ===
94
+
95
+ export function createMockTokenAmount(amount: bigint): TokenAmount {
96
+ return {
97
+ amount,
98
+ token: {
99
+ name: 'TestToken',
100
+ symbol: 'TEST',
101
+ decimals: 18,
102
+ addressOrDenom: TEST_ADDRESSES.token,
103
+ },
104
+ getDecimalFormattedAmount: () => ethers.utils.formatEther(amount),
105
+ } as unknown as TokenAmount;
106
+ }
107
+
108
+ export interface MockAdapterConfig {
109
+ isRebalancer?: boolean;
110
+ allowedDestination?: string;
111
+ isBridgeAllowed?: boolean;
112
+ quotes?: InterchainGasQuote[];
113
+ populatedTx?: PopulatedTransaction;
114
+ throwOnQuotes?: Error;
115
+ throwOnPopulate?: Error;
116
+ }
117
+
118
+ export function createMockAdapter(config: MockAdapterConfig = {}) {
119
+ const {
120
+ isRebalancer = true,
121
+ allowedDestination = TEST_ADDRESSES.arbitrum,
122
+ isBridgeAllowed = true,
123
+ quotes = [{ igpQuote: { amount: BigInt(1000000) } }],
124
+ populatedTx = {
125
+ to: TEST_ADDRESSES.token,
126
+ data: '0x',
127
+ value: ethers.BigNumber.from(0),
128
+ },
129
+ throwOnQuotes,
130
+ throwOnPopulate,
131
+ } = config;
132
+
133
+ const adapter = {
134
+ isRebalancer: Sinon.stub().resolves(isRebalancer),
135
+ getAllowedDestination: Sinon.stub().resolves(allowedDestination),
136
+ isBridgeAllowed: Sinon.stub().resolves(isBridgeAllowed),
137
+ getRebalanceQuotes: throwOnQuotes
138
+ ? Sinon.stub().rejects(throwOnQuotes)
139
+ : Sinon.stub().resolves(quotes),
140
+ populateRebalanceTx: throwOnPopulate
141
+ ? Sinon.stub().rejects(throwOnPopulate)
142
+ : Sinon.stub().resolves(populatedTx),
143
+ };
144
+
145
+ Object.setPrototypeOf(adapter, EvmMovableCollateralAdapter.prototype);
146
+ return adapter;
147
+ }
148
+
149
+ export interface MockTokenConfig {
150
+ name?: string;
151
+ decimals?: number;
152
+ addressOrDenom?: string;
153
+ adapter?: ReturnType<typeof createMockAdapter>;
154
+ }
155
+
156
+ export function createMockToken(config: MockTokenConfig = {}) {
157
+ const {
158
+ name = 'TestToken',
159
+ decimals = 18,
160
+ addressOrDenom = TEST_ADDRESSES.token,
161
+ adapter = createMockAdapter(),
162
+ } = config;
163
+
164
+ const token = {
165
+ name,
166
+ decimals,
167
+ addressOrDenom,
168
+ amount: (amt: bigint) => createMockTokenAmount(amt),
169
+ getHypAdapter: Sinon.stub().returns(adapter),
170
+ };
171
+
172
+ return { token, adapter };
173
+ }
174
+
175
+ export interface MockMultiProviderConfig {
176
+ chainMetadata?: ChainMap<Partial<ChainMetadata>>;
177
+ signerAddress?: string;
178
+ sendTransactionReceipt?: providers.TransactionReceipt;
179
+ throwOnSendTransaction?: Error;
180
+ throwOnEstimateGas?: Error;
181
+ providerWaitForTransaction?: providers.TransactionReceipt;
182
+ providerGetBlock?: providers.Block | null;
183
+ providerGetTransactionReceipt?: providers.TransactionReceipt | null;
184
+ }
185
+
186
+ export function createMockMultiProvider(config: MockMultiProviderConfig = {}) {
187
+ const {
188
+ chainMetadata = {},
189
+ signerAddress = TEST_ADDRESSES.signer,
190
+ sendTransactionReceipt = {
191
+ transactionHash:
192
+ '0x1111111111111111111111111111111111111111111111111111111111111111',
193
+ blockNumber: 100,
194
+ status: 1,
195
+ } as providers.TransactionReceipt,
196
+ throwOnSendTransaction,
197
+ throwOnEstimateGas,
198
+ providerWaitForTransaction = sendTransactionReceipt,
199
+ providerGetBlock = { number: 150 } as providers.Block,
200
+ providerGetTransactionReceipt = sendTransactionReceipt,
201
+ } = config;
202
+
203
+ const mockProvider = {
204
+ waitForTransaction: Sinon.stub().resolves(providerWaitForTransaction),
205
+ getBlock: Sinon.stub().resolves(providerGetBlock),
206
+ getTransactionReceipt: Sinon.stub().resolves(providerGetTransactionReceipt),
207
+ };
208
+
209
+ const mockSigner = {
210
+ getAddress: Sinon.stub().resolves(signerAddress),
211
+ sendTransaction: throwOnSendTransaction
212
+ ? Sinon.stub().rejects(throwOnSendTransaction)
213
+ : Sinon.stub().resolves({
214
+ hash: sendTransactionReceipt.transactionHash,
215
+ wait: Sinon.stub().resolves(sendTransactionReceipt),
216
+ }),
217
+ };
218
+
219
+ const defaultChainMetadata: ChainMap<Partial<ChainMetadata>> = {
220
+ ethereum: { domainId: 1, blocks: { confirmations: 32, reorgPeriod: 32 } },
221
+ arbitrum: { domainId: 42161, blocks: { confirmations: 0, reorgPeriod: 0 } },
222
+ };
223
+
224
+ const mergedMetadata = { ...defaultChainMetadata, ...chainMetadata };
225
+
226
+ return {
227
+ getChainMetadata: Sinon.stub().callsFake(
228
+ (chain: ChainName) => mergedMetadata[chain] ?? {},
229
+ ),
230
+ getProvider: Sinon.stub().returns(mockProvider),
231
+ getSigner: Sinon.stub().returns(mockSigner),
232
+ estimateGas: throwOnEstimateGas
233
+ ? Sinon.stub().rejects(throwOnEstimateGas)
234
+ : Sinon.stub().resolves(ethers.BigNumber.from(100000)),
235
+ sendTransaction: throwOnSendTransaction
236
+ ? Sinon.stub().rejects(throwOnSendTransaction)
237
+ : Sinon.stub().resolves(sendTransactionReceipt),
238
+ getDomainId: Sinon.stub().callsFake(
239
+ (chain: ChainName) => mergedMetadata[chain]?.domainId ?? 0,
240
+ ),
241
+ _mockProvider: mockProvider,
242
+ _mockSigner: mockSigner,
243
+ } as unknown as MultiProvider & {
244
+ _mockProvider: typeof mockProvider;
245
+ _mockSigner: typeof mockSigner;
246
+ };
247
+ }
248
+
249
+ export function createMockWarpCore(multiProvider: MultiProvider) {
250
+ return {
251
+ multiProvider,
252
+ } as unknown as WarpCore;
253
+ }
254
+
255
+ // Valid EVM test addresses (40 hex chars after 0x)
256
+ export const TEST_ADDRESSES: Record<string, string> = {
257
+ ethereum: '0x1111111111111111111111111111111111111111',
258
+ arbitrum: '0x2222222222222222222222222222222222222222',
259
+ optimism: '0x3333333333333333333333333333333333333333',
260
+ polygon: '0x4444444444444444444444444444444444444444',
261
+ bridge: '0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',
262
+ signer: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
263
+ token: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
264
+ };
265
+
266
+ export function getTestAddress(key: string): string {
267
+ return TEST_ADDRESSES[key] ?? `0x${key.padStart(40, '0').slice(-40)}`;
268
+ }
269
+
270
+ export function buildTestBridges(
271
+ chains: ChainName[] = ['ethereum', 'arbitrum'],
272
+ ): ChainMap<BridgeConfigWithOverride> {
273
+ return chains.reduce((acc, chain) => {
274
+ acc[chain] = {
275
+ bridge: TEST_ADDRESSES.bridge,
276
+ bridgeMinAcceptedAmount: 0,
277
+ };
278
+ return acc;
279
+ }, {} as ChainMap<BridgeConfigWithOverride>);
280
+ }
281
+
282
+ /**
283
+ * Convert a chain config map (with bridge addresses) to a BridgeConfigWithOverride map.
284
+ * Useful for tests that define bridge addresses in the strategy config.
285
+ */
286
+ export function extractBridgeConfigs(
287
+ chainConfig: Record<
288
+ string,
289
+ { bridge: string; bridgeMinAcceptedAmount?: number | string }
290
+ >,
291
+ ): ChainMap<BridgeConfigWithOverride> {
292
+ return Object.entries(chainConfig).reduce((acc, [chain, config]) => {
293
+ acc[chain] = {
294
+ bridge: config.bridge,
295
+ bridgeMinAcceptedAmount: config.bridgeMinAcceptedAmount ?? 0,
296
+ };
297
+ return acc;
298
+ }, {} as ChainMap<BridgeConfigWithOverride>);
299
+ }
300
+
301
+ export function buildTestChainMetadata(
302
+ chains: ChainName[] = ['ethereum', 'arbitrum'],
303
+ ): ChainMap<ChainMetadata> {
304
+ const domainIds: Record<string, number> = {
305
+ ethereum: 1,
306
+ arbitrum: 42161,
307
+ optimism: 10,
308
+ polygon: 137,
309
+ };
310
+
311
+ return chains.reduce((acc, chain) => {
312
+ acc[chain] = {
313
+ name: chain,
314
+ chainId: domainIds[chain] ?? 1,
315
+ domainId: domainIds[chain] ?? 1,
316
+ protocol: 'ethereum' as any,
317
+ rpcUrls: [{ http: 'http://localhost:8545' }],
318
+ blocks: { reorgPeriod: chain === 'polygon' ? 'finalized' : 32 },
319
+ } as ChainMetadata;
320
+ return acc;
321
+ }, {} as ChainMap<ChainMetadata>);
322
+ }
323
+
324
+ export interface RebalancerTestContext {
325
+ multiProvider: ReturnType<typeof createMockMultiProvider>;
326
+ warpCore: WarpCore;
327
+ bridges: ChainMap<BridgeConfigWithOverride>;
328
+ chainMetadata: ChainMap<ChainMetadata>;
329
+ tokensByChainName: ChainMap<Token>;
330
+ adapters: ChainMap<ReturnType<typeof createMockAdapter>>;
331
+ }
332
+
333
+ export function createRebalancerTestContext(
334
+ chains: ChainName[] = ['ethereum', 'arbitrum'],
335
+ adapterConfigs: ChainMap<MockAdapterConfig> = {},
336
+ ): RebalancerTestContext {
337
+ const multiProvider = createMockMultiProvider();
338
+ const warpCore = createMockWarpCore(
339
+ multiProvider as unknown as MultiProvider,
340
+ );
341
+ const bridges = buildTestBridges(chains);
342
+ const chainMetadata = buildTestChainMetadata(chains);
343
+
344
+ const adapters: ChainMap<ReturnType<typeof createMockAdapter>> = {};
345
+ const tokensByChainName: ChainMap<Token> = {};
346
+
347
+ for (const chain of chains) {
348
+ const adapterConfig = adapterConfigs[chain] ?? {};
349
+ const tokenAddress = getTestAddress(chain);
350
+ const { token, adapter } = createMockToken({
351
+ name: `${chain}Token`,
352
+ addressOrDenom: tokenAddress,
353
+ adapter: createMockAdapter(adapterConfig),
354
+ });
355
+ adapters[chain] = adapter;
356
+ tokensByChainName[chain] = token as unknown as Token;
357
+ }
358
+
359
+ for (const originChain of chains) {
360
+ const adapterConfig = adapterConfigs[originChain] ?? {};
361
+ if (adapterConfig.allowedDestination === undefined) {
362
+ const destAddressMap: Record<number, string> = {};
363
+ for (const destChain of chains) {
364
+ if (originChain !== destChain) {
365
+ destAddressMap[chainMetadata[destChain].domainId] =
366
+ getTestAddress(destChain);
367
+ }
368
+ }
369
+ adapters[originChain].getAllowedDestination.callsFake(
370
+ (domainId: number) => {
371
+ return Promise.resolve(
372
+ destAddressMap[domainId] ??
373
+ '0x0000000000000000000000000000000000000000',
374
+ );
375
+ },
376
+ );
377
+ }
378
+ }
379
+
380
+ return {
381
+ multiProvider,
382
+ warpCore,
383
+ bridges,
384
+ chainMetadata,
385
+ tokensByChainName,
386
+ adapters,
387
+ };
388
+ }
389
+
390
+ // === Config Builders ===
391
+
14
392
  export function buildTestConfig(
15
393
  overrides: Partial<RebalancerConfig> = {},
16
394
  chains: string[] = ['chain1'],
@@ -30,16 +408,42 @@ export function buildTestConfig(
30
408
  {} as Record<string, any>,
31
409
  );
32
410
 
411
+ // Build the default strategy config
412
+ const defaultStrategyConfig = {
413
+ rebalanceStrategy: RebalancerStrategyOptions.Weighted,
414
+ chains: baseChains,
415
+ };
416
+
417
+ // If overrides has strategyConfig as an array, use it directly
418
+ // Otherwise, wrap single strategy in an array
419
+ let strategyConfig;
420
+ if (overrides.strategyConfig) {
421
+ if (Array.isArray(overrides.strategyConfig)) {
422
+ strategyConfig = overrides.strategyConfig;
423
+ } else {
424
+ // Single strategy override - use it directly wrapped in array
425
+ // If chains is explicitly provided, use it (don't merge with baseChains)
426
+ const singleConfig = overrides.strategyConfig as any;
427
+ strategyConfig = [
428
+ {
429
+ ...singleConfig,
430
+ chains:
431
+ singleConfig.chains !== undefined
432
+ ? singleConfig.chains
433
+ : baseChains,
434
+ },
435
+ ];
436
+ }
437
+ } else {
438
+ strategyConfig = [defaultStrategyConfig];
439
+ }
440
+
441
+ // Destructure to exclude strategyConfig from overrides spread
442
+ const { strategyConfig: _, ...restOverrides } = overrides;
443
+
33
444
  return {
34
445
  warpRouteId: 'test-route',
35
- strategyConfig: {
36
- rebalanceStrategy: RebalancerStrategyOptions.Weighted,
37
- chains: {
38
- ...baseChains,
39
- ...(overrides.strategyConfig?.chains ?? {}),
40
- },
41
- ...overrides.strategyConfig,
42
- },
43
- ...overrides,
446
+ ...restOverrides,
447
+ strategyConfig,
44
448
  } as any as RebalancerConfig;
45
449
  }