@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.
- package/LICENSE.md +195 -0
- package/README.md +582 -0
- package/dist/BridgeMockController.d.ts +87 -0
- package/dist/BridgeMockController.d.ts.map +1 -0
- package/dist/BridgeMockController.js +300 -0
- package/dist/BridgeMockController.js.map +1 -0
- package/dist/KPICollector.d.ts +81 -0
- package/dist/KPICollector.d.ts.map +1 -0
- package/dist/KPICollector.js +239 -0
- package/dist/KPICollector.js.map +1 -0
- package/dist/MessageTracker.d.ts +82 -0
- package/dist/MessageTracker.d.ts.map +1 -0
- package/dist/MessageTracker.js +213 -0
- package/dist/MessageTracker.js.map +1 -0
- package/dist/RebalancerSimulationHarness.d.ts +72 -0
- package/dist/RebalancerSimulationHarness.d.ts.map +1 -0
- package/dist/RebalancerSimulationHarness.js +217 -0
- package/dist/RebalancerSimulationHarness.js.map +1 -0
- package/dist/ScenarioGenerator.d.ts +50 -0
- package/dist/ScenarioGenerator.d.ts.map +1 -0
- package/dist/ScenarioGenerator.js +326 -0
- package/dist/ScenarioGenerator.js.map +1 -0
- package/dist/ScenarioLoader.d.ts +18 -0
- package/dist/ScenarioLoader.d.ts.map +1 -0
- package/dist/ScenarioLoader.js +59 -0
- package/dist/ScenarioLoader.js.map +1 -0
- package/dist/SimulationDeployment.d.ts +20 -0
- package/dist/SimulationDeployment.d.ts.map +1 -0
- package/dist/SimulationDeployment.js +170 -0
- package/dist/SimulationDeployment.js.map +1 -0
- package/dist/SimulationEngine.d.ts +58 -0
- package/dist/SimulationEngine.d.ts.map +1 -0
- package/dist/SimulationEngine.js +302 -0
- package/dist/SimulationEngine.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/runners/NoOpRebalancer.d.ts +17 -0
- package/dist/runners/NoOpRebalancer.d.ts.map +1 -0
- package/dist/runners/NoOpRebalancer.js +28 -0
- package/dist/runners/NoOpRebalancer.js.map +1 -0
- package/dist/runners/ProductionRebalancerRunner.d.ts +22 -0
- package/dist/runners/ProductionRebalancerRunner.d.ts.map +1 -0
- package/dist/runners/ProductionRebalancerRunner.js +219 -0
- package/dist/runners/ProductionRebalancerRunner.js.map +1 -0
- package/dist/runners/SimpleRunner.d.ts +31 -0
- package/dist/runners/SimpleRunner.d.ts.map +1 -0
- package/dist/runners/SimpleRunner.js +286 -0
- package/dist/runners/SimpleRunner.js.map +1 -0
- package/dist/runners/SimulationRegistry.d.ts +46 -0
- package/dist/runners/SimulationRegistry.d.ts.map +1 -0
- package/dist/runners/SimulationRegistry.js +156 -0
- package/dist/runners/SimulationRegistry.js.map +1 -0
- package/dist/types.d.ts +637 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +158 -0
- package/dist/types.js.map +1 -0
- package/dist/visualizer/HtmlTimelineGenerator.d.ts +6 -0
- package/dist/visualizer/HtmlTimelineGenerator.d.ts.map +1 -0
- package/dist/visualizer/HtmlTimelineGenerator.js +1321 -0
- package/dist/visualizer/HtmlTimelineGenerator.js.map +1 -0
- package/dist/visualizer/index.d.ts +4 -0
- package/dist/visualizer/index.d.ts.map +1 -0
- package/dist/visualizer/index.js +3 -0
- package/dist/visualizer/index.js.map +1 -0
- package/package.json +62 -0
- package/src/BridgeMockController.ts +404 -0
- package/src/KPICollector.ts +304 -0
- package/src/MessageTracker.ts +312 -0
- package/src/RebalancerSimulationHarness.ts +325 -0
- package/src/ScenarioGenerator.ts +433 -0
- package/src/ScenarioLoader.ts +73 -0
- package/src/SimulationDeployment.ts +265 -0
- package/src/SimulationEngine.ts +432 -0
- package/src/index.ts +101 -0
- package/src/runners/NoOpRebalancer.ts +40 -0
- package/src/runners/ProductionRebalancerRunner.ts +289 -0
- package/src/runners/SimpleRunner.ts +382 -0
- package/src/runners/SimulationRegistry.ts +215 -0
- package/src/types.ts +878 -0
- package/src/visualizer/HtmlTimelineGenerator.ts +1341 -0
- 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
|
+
}
|