@hyperlane-xyz/rebalancer-sim 0.1.1

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 (83) hide show
  1. package/LICENSE.md +195 -0
  2. package/README.md +582 -0
  3. package/dist/BridgeMockController.d.ts +87 -0
  4. package/dist/BridgeMockController.d.ts.map +1 -0
  5. package/dist/BridgeMockController.js +300 -0
  6. package/dist/BridgeMockController.js.map +1 -0
  7. package/dist/KPICollector.d.ts +81 -0
  8. package/dist/KPICollector.d.ts.map +1 -0
  9. package/dist/KPICollector.js +239 -0
  10. package/dist/KPICollector.js.map +1 -0
  11. package/dist/MessageTracker.d.ts +82 -0
  12. package/dist/MessageTracker.d.ts.map +1 -0
  13. package/dist/MessageTracker.js +213 -0
  14. package/dist/MessageTracker.js.map +1 -0
  15. package/dist/RebalancerSimulationHarness.d.ts +72 -0
  16. package/dist/RebalancerSimulationHarness.d.ts.map +1 -0
  17. package/dist/RebalancerSimulationHarness.js +217 -0
  18. package/dist/RebalancerSimulationHarness.js.map +1 -0
  19. package/dist/ScenarioGenerator.d.ts +50 -0
  20. package/dist/ScenarioGenerator.d.ts.map +1 -0
  21. package/dist/ScenarioGenerator.js +326 -0
  22. package/dist/ScenarioGenerator.js.map +1 -0
  23. package/dist/ScenarioLoader.d.ts +18 -0
  24. package/dist/ScenarioLoader.d.ts.map +1 -0
  25. package/dist/ScenarioLoader.js +59 -0
  26. package/dist/ScenarioLoader.js.map +1 -0
  27. package/dist/SimulationDeployment.d.ts +20 -0
  28. package/dist/SimulationDeployment.d.ts.map +1 -0
  29. package/dist/SimulationDeployment.js +170 -0
  30. package/dist/SimulationDeployment.js.map +1 -0
  31. package/dist/SimulationEngine.d.ts +58 -0
  32. package/dist/SimulationEngine.d.ts.map +1 -0
  33. package/dist/SimulationEngine.js +302 -0
  34. package/dist/SimulationEngine.js.map +1 -0
  35. package/dist/index.d.ts +22 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +26 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/runners/NoOpRebalancer.d.ts +17 -0
  40. package/dist/runners/NoOpRebalancer.d.ts.map +1 -0
  41. package/dist/runners/NoOpRebalancer.js +28 -0
  42. package/dist/runners/NoOpRebalancer.js.map +1 -0
  43. package/dist/runners/ProductionRebalancerRunner.d.ts +22 -0
  44. package/dist/runners/ProductionRebalancerRunner.d.ts.map +1 -0
  45. package/dist/runners/ProductionRebalancerRunner.js +219 -0
  46. package/dist/runners/ProductionRebalancerRunner.js.map +1 -0
  47. package/dist/runners/SimpleRunner.d.ts +31 -0
  48. package/dist/runners/SimpleRunner.d.ts.map +1 -0
  49. package/dist/runners/SimpleRunner.js +286 -0
  50. package/dist/runners/SimpleRunner.js.map +1 -0
  51. package/dist/runners/SimulationRegistry.d.ts +46 -0
  52. package/dist/runners/SimulationRegistry.d.ts.map +1 -0
  53. package/dist/runners/SimulationRegistry.js +156 -0
  54. package/dist/runners/SimulationRegistry.js.map +1 -0
  55. package/dist/types.d.ts +637 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +158 -0
  58. package/dist/types.js.map +1 -0
  59. package/dist/visualizer/HtmlTimelineGenerator.d.ts +6 -0
  60. package/dist/visualizer/HtmlTimelineGenerator.d.ts.map +1 -0
  61. package/dist/visualizer/HtmlTimelineGenerator.js +1321 -0
  62. package/dist/visualizer/HtmlTimelineGenerator.js.map +1 -0
  63. package/dist/visualizer/index.d.ts +4 -0
  64. package/dist/visualizer/index.d.ts.map +1 -0
  65. package/dist/visualizer/index.js +3 -0
  66. package/dist/visualizer/index.js.map +1 -0
  67. package/package.json +62 -0
  68. package/src/BridgeMockController.ts +404 -0
  69. package/src/KPICollector.ts +304 -0
  70. package/src/MessageTracker.ts +312 -0
  71. package/src/RebalancerSimulationHarness.ts +325 -0
  72. package/src/ScenarioGenerator.ts +433 -0
  73. package/src/ScenarioLoader.ts +73 -0
  74. package/src/SimulationDeployment.ts +265 -0
  75. package/src/SimulationEngine.ts +432 -0
  76. package/src/index.ts +101 -0
  77. package/src/runners/NoOpRebalancer.ts +40 -0
  78. package/src/runners/ProductionRebalancerRunner.ts +289 -0
  79. package/src/runners/SimpleRunner.ts +382 -0
  80. package/src/runners/SimulationRegistry.ts +215 -0
  81. package/src/types.ts +878 -0
  82. package/src/visualizer/HtmlTimelineGenerator.ts +1341 -0
  83. package/src/visualizer/index.ts +7 -0
@@ -0,0 +1,382 @@
1
+ import { ethers } from 'ethers';
2
+ import { EventEmitter } from 'events';
3
+ import { pino } from 'pino';
4
+
5
+ import {
6
+ ERC20Test__factory,
7
+ HypERC20Collateral__factory,
8
+ } from '@hyperlane-xyz/core';
9
+
10
+ import type {
11
+ DeployedDomain,
12
+ IRebalancerRunner,
13
+ RebalancerSimConfig,
14
+ } from '../types.js';
15
+
16
+ // Track the current SimpleRunner instance for cleanup
17
+ let currentSimpleRunner: SimpleRunner | null = null;
18
+ let currentSimpleProvider: ethers.providers.JsonRpcProvider | null = null;
19
+
20
+ /**
21
+ * Global cleanup function - call between test runs to ensure clean state
22
+ */
23
+ export async function cleanupSimpleRunner(): Promise<void> {
24
+ if (currentSimpleRunner) {
25
+ const runner = currentSimpleRunner;
26
+ currentSimpleRunner = null;
27
+ try {
28
+ await runner.stop();
29
+ } catch {
30
+ // Ignore errors
31
+ }
32
+ }
33
+
34
+ if (currentSimpleProvider) {
35
+ currentSimpleProvider.removeAllListeners();
36
+ currentSimpleProvider = null;
37
+ }
38
+
39
+ // Small delay to allow any async cleanup to complete
40
+ await new Promise((resolve) => setTimeout(resolve, 50));
41
+ }
42
+
43
+ /**
44
+ * SimpleRunner is a simplified rebalancer implementation for simulation testing.
45
+ * It monitors balances and triggers rebalances when imbalances exceed thresholds.
46
+ */
47
+ export class SimpleRunner extends EventEmitter implements IRebalancerRunner {
48
+ readonly name = 'SimpleRebalancer';
49
+
50
+ private config?: RebalancerSimConfig;
51
+ private logger = pino({ level: 'warn' });
52
+ private running = false;
53
+ private activeOperations = 0;
54
+ private pollingTimer?: NodeJS.Timeout;
55
+ private provider?: ethers.providers.JsonRpcProvider;
56
+ private deployer?: ethers.Wallet;
57
+
58
+ async initialize(config: RebalancerSimConfig): Promise<void> {
59
+ // Cleanup any previously running instance
60
+ await cleanupSimpleRunner();
61
+
62
+ this.config = config;
63
+ this.provider = new ethers.providers.JsonRpcProvider(
64
+ config.deployment.anvilRpc,
65
+ );
66
+ // Set fast polling interval for tx.wait() - ethers defaults to 4000ms
67
+ this.provider.pollingInterval = 100;
68
+ // Disable automatic polling to reduce RPC contention in simulation
69
+ this.provider.polling = false;
70
+ // Track for cleanup
71
+ currentSimpleProvider = this.provider;
72
+
73
+ // Use separate rebalancer key to avoid nonce conflicts with transfer execution
74
+ this.deployer = new ethers.Wallet(
75
+ config.deployment.rebalancerKey,
76
+ this.provider,
77
+ );
78
+ }
79
+
80
+ async start(): Promise<void> {
81
+ if (!this.config) {
82
+ throw new Error('Rebalancer not initialized');
83
+ }
84
+
85
+ if (this.running) {
86
+ return;
87
+ }
88
+
89
+ this.running = true;
90
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
91
+ currentSimpleRunner = this;
92
+ this.logger.info('Starting rebalancer daemon');
93
+
94
+ // Start polling loop
95
+ this.scheduleNextPoll();
96
+ }
97
+
98
+ private scheduleNextPoll(): void {
99
+ if (!this.running || !this.config) return;
100
+
101
+ this.pollingTimer = setTimeout(async () => {
102
+ await this.pollAndRebalance();
103
+ this.scheduleNextPoll();
104
+ }, this.config.pollingFrequency);
105
+ }
106
+
107
+ private async pollAndRebalance(): Promise<void> {
108
+ if (!this.config || !this.provider || !this.deployer) return;
109
+
110
+ try {
111
+ this.activeOperations++;
112
+
113
+ // Get current balances
114
+ const balances: Record<string, bigint> = {};
115
+ const domains = this.config.deployment.domains;
116
+
117
+ for (const [chainName, domain] of Object.entries(domains)) {
118
+ const token = ERC20Test__factory.connect(
119
+ domain.collateralToken,
120
+ this.provider,
121
+ );
122
+ const balance = await token.balanceOf(domain.warpToken);
123
+ balances[chainName] = balance.toBigInt();
124
+ }
125
+
126
+ // Calculate total and target balances per strategy
127
+ const { strategyConfig } = this.config;
128
+ if (strategyConfig.type === 'weighted') {
129
+ await this.executeWeightedRebalance(balances, domains);
130
+ } else if (strategyConfig.type === 'minAmount') {
131
+ await this.executeMinAmountRebalance(balances, domains);
132
+ }
133
+ } catch (error) {
134
+ this.logger.error({ error }, 'Error during rebalance poll');
135
+ } finally {
136
+ this.activeOperations--;
137
+ }
138
+ }
139
+
140
+ private async executeWeightedRebalance(
141
+ balances: Record<string, bigint>,
142
+ domains: Record<string, DeployedDomain>,
143
+ ): Promise<void> {
144
+ if (!this.config || !this.deployer) return;
145
+
146
+ const { strategyConfig } = this.config;
147
+ const chainNames = Object.keys(balances);
148
+
149
+ // Calculate total balance
150
+ let totalBalance = BigInt(0);
151
+ for (const balance of Object.values(balances)) {
152
+ totalBalance += balance;
153
+ }
154
+
155
+ if (totalBalance === BigInt(0)) return;
156
+
157
+ // Calculate weight sum
158
+ let totalWeight = 0;
159
+ for (const chainName of chainNames) {
160
+ const chainConfig = strategyConfig.chains[chainName];
161
+ const weight = chainConfig?.weighted?.weight
162
+ ? parseFloat(chainConfig.weighted.weight)
163
+ : 1 / chainNames.length;
164
+ totalWeight += weight;
165
+ }
166
+
167
+ // Find chains with excess and deficit
168
+ const excess: { chain: string; amount: bigint }[] = [];
169
+ const deficit: { chain: string; amount: bigint }[] = [];
170
+
171
+ for (const chainName of chainNames) {
172
+ const chainConfig = strategyConfig.chains[chainName];
173
+ const weight = chainConfig?.weighted?.weight
174
+ ? parseFloat(chainConfig.weighted.weight)
175
+ : 1 / chainNames.length;
176
+ const tolerance = chainConfig?.weighted?.tolerance
177
+ ? parseFloat(chainConfig.weighted.tolerance)
178
+ : 0.1;
179
+
180
+ const targetBalance =
181
+ (totalBalance * BigInt(Math.floor(weight * 10000))) /
182
+ BigInt(Math.floor(totalWeight * 10000));
183
+ const currentBalance = balances[chainName];
184
+
185
+ const minBalance =
186
+ (targetBalance * BigInt(Math.floor((1 - tolerance) * 10000))) /
187
+ BigInt(10000);
188
+ const maxBalance =
189
+ (targetBalance * BigInt(Math.floor((1 + tolerance) * 10000))) /
190
+ BigInt(10000);
191
+
192
+ if (currentBalance > maxBalance) {
193
+ excess.push({
194
+ chain: chainName,
195
+ amount: currentBalance - targetBalance,
196
+ });
197
+ } else if (currentBalance < minBalance) {
198
+ deficit.push({
199
+ chain: chainName,
200
+ amount: targetBalance - currentBalance,
201
+ });
202
+ }
203
+ }
204
+
205
+ // Execute rebalances - track remaining amounts to avoid over-rebalancing
206
+ const remainingExcess = new Map(excess.map((e) => [e.chain, e.amount]));
207
+ const remainingDeficit = new Map(deficit.map((d) => [d.chain, d.amount]));
208
+
209
+ for (const { chain: fromChain } of excess) {
210
+ for (const { chain: toChain } of deficit) {
211
+ const currentExcess = remainingExcess.get(fromChain) ?? BigInt(0);
212
+ const currentDeficit = remainingDeficit.get(toChain) ?? BigInt(0);
213
+ if (currentExcess <= BigInt(0) || currentDeficit <= BigInt(0)) continue;
214
+
215
+ const rebalanceAmount =
216
+ currentExcess < currentDeficit ? currentExcess : currentDeficit;
217
+ if (rebalanceAmount > BigInt(0)) {
218
+ await this.executeRebalance(
219
+ fromChain,
220
+ toChain,
221
+ rebalanceAmount,
222
+ domains,
223
+ );
224
+ remainingExcess.set(fromChain, currentExcess - rebalanceAmount);
225
+ remainingDeficit.set(toChain, currentDeficit - rebalanceAmount);
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ private async executeMinAmountRebalance(
232
+ balances: Record<string, bigint>,
233
+ domains: Record<string, DeployedDomain>,
234
+ ): Promise<void> {
235
+ if (!this.config) return;
236
+
237
+ const { strategyConfig } = this.config;
238
+
239
+ // Find chains below minimum
240
+ const belowMin: { chain: string; deficit: bigint; target: bigint }[] = [];
241
+ const aboveTarget: { chain: string; excess: bigint }[] = [];
242
+
243
+ for (const [chainName, balance] of Object.entries(balances)) {
244
+ const chainConfig = strategyConfig.chains[chainName];
245
+ if (!chainConfig?.minAmount) continue;
246
+
247
+ const min = BigInt(chainConfig.minAmount.min);
248
+ const target = BigInt(chainConfig.minAmount.target);
249
+
250
+ if (balance < min) {
251
+ belowMin.push({ chain: chainName, deficit: target - balance, target });
252
+ } else if (balance > target * BigInt(2)) {
253
+ aboveTarget.push({ chain: chainName, excess: balance - target });
254
+ }
255
+ }
256
+
257
+ // Rebalance from excess to deficit - track remaining amounts to avoid over-rebalancing
258
+ const remainingDeficit = new Map(belowMin.map((d) => [d.chain, d.deficit]));
259
+ const remainingExcess = new Map(
260
+ aboveTarget.map((e) => [e.chain, e.excess]),
261
+ );
262
+
263
+ for (const { chain: toChain } of belowMin) {
264
+ for (const { chain: fromChain } of aboveTarget) {
265
+ const currentDeficit = remainingDeficit.get(toChain) ?? BigInt(0);
266
+ const currentExcess = remainingExcess.get(fromChain) ?? BigInt(0);
267
+ if (currentDeficit <= BigInt(0) || currentExcess <= BigInt(0)) continue;
268
+
269
+ const amount =
270
+ currentDeficit < currentExcess ? currentDeficit : currentExcess;
271
+ if (amount > BigInt(0)) {
272
+ await this.executeRebalance(fromChain, toChain, amount, domains);
273
+ remainingDeficit.set(toChain, currentDeficit - amount);
274
+ remainingExcess.set(fromChain, currentExcess - amount);
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ private async executeRebalance(
281
+ fromChain: string,
282
+ toChain: string,
283
+ amount: bigint,
284
+ domains: Record<string, DeployedDomain>,
285
+ ): Promise<void> {
286
+ if (!this.deployer) return;
287
+
288
+ try {
289
+ const fromDomain = domains[fromChain];
290
+ const toDomain = domains[toChain];
291
+
292
+ this.logger.info(
293
+ { fromChain, toChain, amount: amount.toString() },
294
+ 'Executing rebalance',
295
+ );
296
+
297
+ const warpToken = HypERC20Collateral__factory.connect(
298
+ fromDomain.warpToken,
299
+ this.deployer,
300
+ );
301
+
302
+ // Use the bridge to rebalance
303
+ // Call rebalance through the warp token
304
+ const tx = await warpToken.rebalance(
305
+ toDomain.domainId,
306
+ amount,
307
+ fromDomain.bridge,
308
+ );
309
+ await tx.wait();
310
+
311
+ this.emit('rebalance', {
312
+ type: 'rebalance_completed',
313
+ timestamp: Date.now(),
314
+ origin: fromChain,
315
+ destination: toChain,
316
+ amount,
317
+ });
318
+
319
+ this.logger.info(
320
+ { fromChain, toChain, amount: amount.toString(), txHash: tx.hash },
321
+ 'Rebalance completed',
322
+ );
323
+ } catch (error) {
324
+ this.logger.error({ error, fromChain, toChain }, 'Rebalance failed');
325
+ this.emit('rebalance', {
326
+ type: 'rebalance_failed',
327
+ timestamp: Date.now(),
328
+ origin: fromChain,
329
+ destination: toChain,
330
+ error: String(error),
331
+ });
332
+ }
333
+ }
334
+
335
+ async stop(): Promise<void> {
336
+ if (!this.running) {
337
+ return;
338
+ }
339
+
340
+ this.running = false;
341
+
342
+ if (this.pollingTimer) {
343
+ clearTimeout(this.pollingTimer);
344
+ this.pollingTimer = undefined;
345
+ }
346
+
347
+ // Clear global reference
348
+ if (currentSimpleRunner === this) {
349
+ currentSimpleRunner = null;
350
+ }
351
+
352
+ // Clean up provider
353
+ if (this.provider) {
354
+ this.provider.removeAllListeners();
355
+ if (currentSimpleProvider === this.provider) {
356
+ currentSimpleProvider = null;
357
+ }
358
+ this.provider = undefined;
359
+ }
360
+
361
+ this.deployer = undefined;
362
+ this.config = undefined;
363
+ this.removeAllListeners();
364
+
365
+ this.logger.info('Rebalancer stopped');
366
+ }
367
+
368
+ isActive(): boolean {
369
+ return this.running && this.activeOperations > 0;
370
+ }
371
+
372
+ async waitForIdle(timeoutMs: number = 10000): Promise<void> {
373
+ const startTime = Date.now();
374
+
375
+ while (this.isActive()) {
376
+ if (Date.now() - startTime > timeoutMs) {
377
+ throw new Error('Timeout waiting for rebalancer to become idle');
378
+ }
379
+ await new Promise((resolve) => setTimeout(resolve, 100));
380
+ }
381
+ }
382
+ }
@@ -0,0 +1,215 @@
1
+ import type {
2
+ ChainFiles,
3
+ IRegistry,
4
+ RegistryContent,
5
+ RegistryType,
6
+ UpdateChainParams,
7
+ WarpRouteConfigMap,
8
+ WarpRouteFilterParams,
9
+ } from '@hyperlane-xyz/registry';
10
+ import {
11
+ type ChainMetadata,
12
+ type ChainName,
13
+ TokenStandard,
14
+ type WarpCoreConfig,
15
+ type WarpRouteDeployConfig,
16
+ } from '@hyperlane-xyz/sdk';
17
+ import { ProtocolType } from '@hyperlane-xyz/utils';
18
+
19
+ import type { MultiDomainDeploymentResult } from '../types.js';
20
+
21
+ /**
22
+ * A mock registry that provides chain metadata and warp route config
23
+ * for the simulation environment.
24
+ */
25
+ export class SimulationRegistry implements IRegistry {
26
+ readonly type: RegistryType = 'partial' as RegistryType;
27
+ readonly uri: string = 'simulation://local';
28
+ private readonly warpRouteId = 'SIM/simulation';
29
+ private readonly chainMetadata: Record<string, ChainMetadata>;
30
+ private readonly warpCoreConfig: WarpCoreConfig;
31
+
32
+ constructor(private readonly deployment: MultiDomainDeploymentResult) {
33
+ // Build chain metadata
34
+ this.chainMetadata = this.buildChainMetadata();
35
+ // Build warp core config
36
+ this.warpCoreConfig = this.buildWarpCoreConfig();
37
+ }
38
+
39
+ private buildChainMetadata(): Record<string, ChainMetadata> {
40
+ const metadata: Record<string, ChainMetadata> = {};
41
+
42
+ for (const [chainName, domain] of Object.entries(this.deployment.domains)) {
43
+ metadata[chainName] = {
44
+ name: chainName,
45
+ chainId: 31337, // Anvil's actual chainId (not domainId)
46
+ domainId: domain.domainId,
47
+ protocol: ProtocolType.Ethereum,
48
+ rpcUrls: [{ http: this.deployment.anvilRpc }],
49
+ nativeToken: {
50
+ name: 'Ether',
51
+ symbol: 'ETH',
52
+ decimals: 18,
53
+ },
54
+ blocks: {
55
+ confirmations: 0,
56
+ estimateBlockTime: 1,
57
+ reorgPeriod: 0, // Disable historical block queries in simulation
58
+ },
59
+ };
60
+ }
61
+
62
+ return metadata;
63
+ }
64
+
65
+ private buildWarpCoreConfig(): WarpCoreConfig {
66
+ const tokens: WarpCoreConfig['tokens'] = [];
67
+
68
+ for (const [chainName, domain] of Object.entries(this.deployment.domains)) {
69
+ tokens.push({
70
+ chainName,
71
+ standard: TokenStandard.EvmHypCollateral,
72
+ decimals: 18,
73
+ symbol: 'SIM',
74
+ name: 'Simulation Token',
75
+ addressOrDenom: domain.warpToken,
76
+ collateralAddressOrDenom: domain.collateralToken,
77
+ connections: Object.entries(this.deployment.domains)
78
+ .filter(([name]) => name !== chainName)
79
+ .map(([name, d]) => ({
80
+ token: `ethereum|${name}|${d.warpToken}`,
81
+ })),
82
+ });
83
+ }
84
+
85
+ return { tokens };
86
+ }
87
+
88
+ // IRegistry implementation
89
+
90
+ getUri(itemPath?: string): string {
91
+ return itemPath ? `${this.uri}/${itemPath}` : this.uri;
92
+ }
93
+
94
+ async listRegistryContent(): Promise<RegistryContent> {
95
+ const chains: Record<string, ChainFiles> = {};
96
+ for (const chainName of Object.keys(this.deployment.domains)) {
97
+ chains[chainName] = {
98
+ metadata: `chains/${chainName}/metadata.yaml`,
99
+ addresses: `chains/${chainName}/addresses.yaml`,
100
+ };
101
+ }
102
+ return {
103
+ chains,
104
+ deployments: {
105
+ warpRoutes: {
106
+ [this.warpRouteId]:
107
+ `deployments/warp_routes/${this.warpRouteId}.yaml`,
108
+ },
109
+ warpDeployConfig: {},
110
+ },
111
+ };
112
+ }
113
+
114
+ async getChains(): Promise<ChainName[]> {
115
+ return Object.keys(this.deployment.domains);
116
+ }
117
+
118
+ async getMetadata(): Promise<Record<ChainName, ChainMetadata>> {
119
+ return this.chainMetadata;
120
+ }
121
+
122
+ async getChainMetadata(chainName: ChainName): Promise<ChainMetadata | null> {
123
+ return this.chainMetadata[chainName] || null;
124
+ }
125
+
126
+ async getAddresses(): Promise<Record<ChainName, Record<string, string>>> {
127
+ const addresses: Record<string, Record<string, string>> = {};
128
+
129
+ for (const [chainName, domain] of Object.entries(this.deployment.domains)) {
130
+ addresses[chainName] = {
131
+ mailbox: domain.mailbox,
132
+ warpToken: domain.warpToken,
133
+ bridge: domain.bridge,
134
+ };
135
+ }
136
+
137
+ return addresses;
138
+ }
139
+
140
+ async getChainAddresses(
141
+ chainName: ChainName,
142
+ ): Promise<Record<string, string> | null> {
143
+ const addresses = await this.getAddresses();
144
+ return addresses[chainName] || null;
145
+ }
146
+
147
+ async getWarpRoute(routeId: string): Promise<WarpCoreConfig | null> {
148
+ if (routeId === this.warpRouteId) {
149
+ return this.warpCoreConfig;
150
+ }
151
+ return null;
152
+ }
153
+
154
+ async getWarpRoutes(
155
+ _filter?: WarpRouteFilterParams,
156
+ ): Promise<WarpRouteConfigMap> {
157
+ return {
158
+ [this.warpRouteId]: this.warpCoreConfig,
159
+ };
160
+ }
161
+
162
+ async getWarpDeployConfig(
163
+ _routeId: string,
164
+ ): Promise<WarpRouteDeployConfig | null> {
165
+ // Not needed for simulation
166
+ return null;
167
+ }
168
+
169
+ async getWarpDeployConfigs(
170
+ _filter?: WarpRouteFilterParams,
171
+ ): Promise<Record<string, WarpRouteDeployConfig>> {
172
+ // Not needed for simulation
173
+ return {};
174
+ }
175
+
176
+ async getChainLogoUri(_chainName: ChainName): Promise<string | null> {
177
+ // Not needed for simulation
178
+ return null;
179
+ }
180
+
181
+ async addWarpRoute(
182
+ _config: WarpCoreConfig,
183
+ _options?: { symbol?: string } | { warpRouteId?: string },
184
+ ): Promise<void> {
185
+ throw new Error('Not supported in simulation');
186
+ }
187
+
188
+ async addWarpRouteConfig(
189
+ _config: WarpRouteDeployConfig,
190
+ _options: { symbol?: string } | { warpRouteId?: string },
191
+ ): Promise<void> {
192
+ throw new Error('Not supported in simulation');
193
+ }
194
+
195
+ // Methods not needed for simulation
196
+ async addChain(_chain: UpdateChainParams): Promise<void> {
197
+ throw new Error('Not supported in simulation');
198
+ }
199
+
200
+ async updateChain(_chain: UpdateChainParams): Promise<void> {
201
+ throw new Error('Not supported in simulation');
202
+ }
203
+
204
+ async removeChain(_chain: ChainName): Promise<void> {
205
+ throw new Error('Not supported in simulation');
206
+ }
207
+
208
+ merge(_otherRegistry: IRegistry): IRegistry {
209
+ throw new Error('Not supported in simulation');
210
+ }
211
+
212
+ getWarpRouteId(): string {
213
+ return this.warpRouteId;
214
+ }
215
+ }