@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
package/README.md ADDED
@@ -0,0 +1,582 @@
1
+ # Rebalancer Simulation Harness
2
+
3
+ A fast, real-time simulation framework for testing Hyperlane warp route rebalancers against synthetic transfer scenarios.
4
+
5
+ ## Purpose
6
+
7
+ This simulator helps answer questions like:
8
+
9
+ - Does the rebalancer respond correctly to liquidity imbalances?
10
+ - How quickly does the rebalancer restore balance after a traffic surge?
11
+ - What happens when bridge delays cause the rebalancer to over-correct?
12
+ - How do different rebalancer strategies compare on the same traffic pattern?
13
+
14
+ ## Architecture
15
+
16
+ ```
17
+ ┌─────────────────────────────────────────────────────────────────────┐
18
+ │ SimulationEngine │
19
+ │ Orchestrates scenario execution, rebalancer polling, KPI collection│
20
+ └─────────────────────────────────────────────────────────────────────┘
21
+ │ │ │
22
+ ▼ ▼ ▼
23
+ ┌───────────────┐ ┌────────────────┐ ┌─────────────────┐
24
+ │ Scenario │ │ Rebalancer │ │ BridgeMock │
25
+ │ Generator │ │ Runners │ │ Controller │
26
+ │ │ │ │ │ │
27
+ │ Creates │ │ SimpleRunner │ │ Simulates slow │
28
+ │ transfer │ │ (simplified) │ │ bridge delivery │
29
+ │ patterns │ │ Production │ │ with config- │
30
+ │ │ │ Rebalancer │ │ urable delays │
31
+ └───────────────┘ └────────────────┘ └─────────────────┘
32
+ │ │ │
33
+ └────────────────────┼────────────────────┘
34
+
35
+ ┌─────────────────────────────┐
36
+ │ Multi-Domain Deployment │
37
+ │ │
38
+ │ Single Anvil instance │
39
+ │ simulating N "chains" │
40
+ │ via different domain IDs │
41
+ │ │
42
+ │ Each domain has: │
43
+ │ - Mailbox (instant) │
44
+ │ - WarpToken + Collateral │
45
+ │ - Bridge (delayed) │
46
+ └─────────────────────────────┘
47
+ ```
48
+
49
+ ## Key Concepts
50
+
51
+ ### Warp Token Mechanics
52
+
53
+ Understanding collateral flow is critical:
54
+
55
+ ```
56
+ User sends FROM chain A TO chain B:
57
+ - Chain A: User deposits collateral → WarpToken GAINS collateral
58
+ - Chain B: Recipient withdraws → WarpToken LOSES collateral
59
+
60
+ This is counterintuitive! Transfers TO a chain DRAIN its liquidity.
61
+ ```
62
+
63
+ ### Two Message Paths
64
+
65
+ The simulator uses two different delivery mechanisms:
66
+
67
+ | Path | Mechanism | Delay | Use Case |
68
+ | -------------------- | ----------------------- | -------------------------- | ----------------------------------- |
69
+ | User transfers | MockMailbox | Configurable (default 0ms) | Simulates Hyperlane message passing |
70
+ | Rebalancer transfers | MockValueTransferBridge | Configurable (e.g., 500ms) | Simulates CCTP/bridge delays |
71
+
72
+ This separation is important because rebalancer transfers go through external bridges (CCTP, etc.) which have significant delays, while user transfers use Hyperlane's fast messaging.
73
+
74
+ ### Message Tracking
75
+
76
+ | Component | Description |
77
+ | ---------------- | ------------------------------------------------------------ |
78
+ | `MessageTracker` | Off-chain tracking of pending Hyperlane messages with delays |
79
+ | `KPICollector` | Collects transfer/rebalance metrics and generates final KPIs |
80
+
81
+ ### Rebalancer Runners
82
+
83
+ Two rebalancer implementations are available:
84
+
85
+ | Runner | Description | Use Case |
86
+ | ---------------------------- | ------------------------------------------------------ | ------------------------------- |
87
+ | `SimpleRunner` | Simplified rebalancer with weighted/minAmount strategy | Fast tests, baseline comparison |
88
+ | `ProductionRebalancerRunner` | Wraps actual `@hyperlane-xyz/rebalancer` CLI service | Production behavior validation |
89
+
90
+ ## Directory Structure
91
+
92
+ ```
93
+ typescript/rebalancer-sim/
94
+ ├── src/
95
+ │ ├── BridgeMockController.ts # Bridge delay simulation
96
+ │ ├── KPICollector.ts # Metrics collection
97
+ │ ├── MessageTracker.ts # Message tracking
98
+ │ ├── RebalancerSimulationHarness.ts # Main entry point
99
+ │ ├── ScenarioGenerator.ts # Create synthetic scenarios
100
+ │ ├── ScenarioLoader.ts # Load from JSON files
101
+ │ ├── SimulationDeployment.ts # Anvil + contract deployment
102
+ │ ├── SimulationEngine.ts # Simulation orchestration
103
+ │ ├── types.ts # Consolidated types
104
+ │ ├── index.ts # Explicit exports
105
+ │ ├── runners/ # Rebalancer implementations
106
+ │ │ ├── SimpleRunner.ts # Simplified for testing
107
+ │ │ ├── ProductionRebalancerRunner.ts # Wraps production service
108
+ │ │ └── SimulationRegistry.ts # IRegistry impl
109
+ │ └── visualizer/ # HTML timeline generation
110
+ │ └── HtmlTimelineGenerator.ts
111
+ ├── scenarios/ # Pre-generated scenario JSON files
112
+ ├── results/ # Test results (gitignored)
113
+ ├── scripts/
114
+ │ └── generate-scenarios.ts
115
+ └── test/
116
+ ├── scenarios/ # Unit tests for scenario generation
117
+ ├── utils/ # Test utilities (Anvil management)
118
+ └── integration/ # Full simulation tests
119
+ ```
120
+
121
+ ## Scenario File Format
122
+
123
+ Each scenario JSON is self-contained with metadata, transfers, and default configurations:
124
+
125
+ ```json
126
+ {
127
+ "name": "extreme-drain-chain1",
128
+ "description": "Tests rebalancer response when one chain is rapidly drained.",
129
+ "expectedBehavior": "95% of transfers go TO chain1, draining collateral...",
130
+ "duration": 10000,
131
+ "chains": ["chain1", "chain2", "chain3"],
132
+ "transfers": [...],
133
+ "defaultInitialCollateral": "100000000000000000000",
134
+ "defaultTiming": {
135
+ "bridgeDeliveryDelay": 500,
136
+ "rebalancerPollingFrequency": 1000,
137
+ "userTransferInterval": 100
138
+ },
139
+ "defaultBridgeConfig": {...},
140
+ "defaultStrategyConfig": {...},
141
+ "expectations": {
142
+ "minCompletionRate": 0.9,
143
+ "shouldTriggerRebalancing": true
144
+ }
145
+ }
146
+ ```
147
+
148
+ Tests can use the defaults from JSON or override them for specific test needs.
149
+
150
+ ## Running Simulations
151
+
152
+ ### 1. Generate Scenarios (one-time)
153
+
154
+ ```bash
155
+ pnpm generate-scenarios
156
+ ```
157
+
158
+ Creates JSON files in `scenarios/` with various traffic patterns.
159
+
160
+ ### 2. Run All Tests
161
+
162
+ ```bash
163
+ pnpm test
164
+ ```
165
+
166
+ Tests automatically detect if Anvil is available. If not installed, integration tests are skipped.
167
+
168
+ ### 3. Select Rebalancers
169
+
170
+ By default, tests run with both rebalancers. Use the `REBALANCERS` env var to select:
171
+
172
+ ```bash
173
+ # Run with simplified rebalancer only (faster)
174
+ REBALANCERS=simple pnpm test
175
+
176
+ # Run with production rebalancer only
177
+ REBALANCERS=production pnpm test
178
+
179
+ # Run with both (default) - compare behavior
180
+ REBALANCERS=simple,production pnpm test
181
+
182
+ # Compare on specific scenario (recommended for debugging)
183
+ REBALANCERS=simple,production pnpm test --grep "extreme-drain"
184
+ ```
185
+
186
+ ### 4. View Results
187
+
188
+ Test results are saved to `results/` directory (gitignored):
189
+
190
+ ```bash
191
+ # JSON results with KPIs
192
+ cat results/extreme-drain-chain1.json
193
+
194
+ # HTML timeline visualization
195
+ open results/extreme-drain-chain1-HyperlaneRebalancer.html
196
+ ```
197
+
198
+ **Note:** If Anvil is not installed, integration tests will be skipped. Install Foundry with:
199
+
200
+ ```bash
201
+ curl -L https://foundry.paradigm.xyz | bash && foundryup
202
+ ```
203
+
204
+ ## Visualization
205
+
206
+ The simulator generates interactive HTML timelines for each test run:
207
+
208
+ ```
209
+ Time →
210
+ ═══════════════════════════════════════════════════════════════════
211
+ chain1 │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ (balance curve)
212
+ │ ──▶ T1 ──▶ T3 ←── R1 (rebalance from chain2)
213
+ ───────┼───────────────────────────────────────────────────────────
214
+ chain2 │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
215
+ │ ──▶ T2 R1 ──▶
216
+ ═══════════════════════════════════════════════════════════════════
217
+ ```
218
+
219
+ Features:
220
+
221
+ - **Transfer bars**: Horizontal bars showing transfer start → delivery (length = latency)
222
+ - **Rebalance markers**: Arrows showing rebalancer actions with direction
223
+ - **Balance curves**: Per-chain collateral over time
224
+ - **Hover tooltips**: Details on transfers, amounts, timing
225
+ - **KPI summary**: Completion rate, latencies, rebalance count
226
+
227
+ ## Scenario Types
228
+
229
+ ### Predefined Scenarios (in `scenarios/`)
230
+
231
+ | Scenario | Description | Expected Behavior |
232
+ | -------------------------------- | ----------------------------------- | ------------------------- |
233
+ | `extreme-drain-chain1` | 95% of transfers TO chain1 | Heavy rebalancing needed |
234
+ | `extreme-accumulate-chain1` | 95% of transfers FROM chain1 | Heavy rebalancing needed |
235
+ | `large-unidirectional-to-chain1` | 5 large (20 token) transfers | Immediate imbalance |
236
+ | `whale-transfers` | 3 massive (30 token) transfers | Stress test response time |
237
+ | `balanced-bidirectional` | Uniform random traffic | Minimal rebalancing |
238
+ | `surge-to-chain1` | Traffic spike mid-scenario | Tests burst handling |
239
+ | `stress-high-volume` | 50 transfers, Poisson distribution | Load testing |
240
+ | `moderate-imbalance-chain1` | 70% of transfers to chain1 | Moderate rebalancing |
241
+ | `sustained-drain-chain3` | 30 transfers over 30s | Endurance test |
242
+ | `random-with-headroom` | Random traffic with extra liquidity | Tests steady-state |
243
+
244
+ ## Test Organization
245
+
246
+ ### Unit Tests (`test/scenarios/`)
247
+
248
+ Test the scenario generation logic without running simulations:
249
+
250
+ - Does `unidirectionalFlow()` create correct transfer patterns?
251
+ - Does `randomTraffic()` distribute across all chains?
252
+ - Does serialization preserve BigInt amounts?
253
+
254
+ ### Integration Tests (`test/integration/`)
255
+
256
+ Run full simulations on Anvil:
257
+
258
+ | Test File | Purpose |
259
+ | ------------------------- | ---------------------------------------------------- |
260
+ | `harness-setup.test.ts` | Verifies multi-domain deployment and harness setup |
261
+ | `full-simulation.test.ts` | Runs predefined scenarios, saves results |
262
+ | `inflight-guard.test.ts` | Demonstrates over-rebalancing without inflight guard |
263
+
264
+ ### Why `inflight-guard.test.ts` is Separate
265
+
266
+ This test demonstrates a specific bug/limitation rather than testing a scenario type:
267
+
268
+ **What it proves:** Without tracking pending (inflight) transfers, the rebalancer sends redundant transfers because each poll sees "stale" on-chain balances.
269
+
270
+ **How it differs:**
271
+
272
+ - Uses custom inline scenario with extreme timing (3s bridge delay vs 200ms polling)
273
+ - Asserts on specific failure behavior (expects over-rebalancing)
274
+ - Documents a bug that needs fixing, not a passing scenario
275
+
276
+ ## KPIs Collected
277
+
278
+ ```typescript
279
+ interface SimulationKPIs {
280
+ totalTransfers: number;
281
+ completedTransfers: number;
282
+ completionRate: number; // 0-1, should be >0.9 with working rebalancer
283
+
284
+ averageLatency: number; // ms
285
+ p50Latency: number;
286
+ p95Latency: number;
287
+ p99Latency: number;
288
+
289
+ totalRebalances: number;
290
+ rebalanceVolume: bigint; // Total tokens moved by rebalancer
291
+
292
+ perChainMetrics: Record<
293
+ string,
294
+ {
295
+ initialBalance: bigint;
296
+ finalBalance: bigint;
297
+ transfersIn: number;
298
+ transfersOut: number;
299
+ }
300
+ >;
301
+ }
302
+ ```
303
+
304
+ ## Current Limitations
305
+
306
+ 1. **No Inflight Guard**: Neither rebalancer implementation tracks pending transfers, causing over-rebalancing when bridge delays are long relative to polling frequency. The `inflight-guard.test.ts` demonstrates this.
307
+
308
+ 2. **Single Anvil**: All "chains" run on one Anvil instance. Real cross-chain timing differences aren't simulated.
309
+
310
+ 3. **Instant User Transfers**: User transfers via MockMailbox are instant. Real Hyperlane has ~15-30 second finality.
311
+
312
+ 4. **No Gas Costs**: Gas costs aren't simulated. KPIs include rebalance count but not actual cost.
313
+
314
+ 5. **Nonce Caching**: When running both rebalancers (`REBALANCERS=simple,production`), ethers v5 nonce caching can cause timeouts on the full test suite. Run specific scenarios for comparison.
315
+
316
+ 6. **Production ActionTracker**: The `ProductionRebalancerRunner` uses a mock `ActionTracker` that does not persist state. The real production rebalancer's ActionTracker depends on external services not available in simulation. A mock ActionTracker with full in-memory tracking is planned for a future PR.
317
+
318
+ ## Design Decisions
319
+
320
+ ### Single Anvil, Multiple Domains
321
+
322
+ All simulated "chains" run on a single Anvil instance with different domain IDs:
323
+
324
+ ```typescript
325
+ // All chains share one RPC but have unique domain IDs
326
+ const chainMetadata = {
327
+ chain1: { domainId: 1000, rpcUrls: [{ http: anvilRpc }] },
328
+ chain2: { domainId: 2000, rpcUrls: [{ http: anvilRpc }] },
329
+ chain3: { domainId: 3000, rpcUrls: [{ http: anvilRpc }] },
330
+ };
331
+
332
+ // Each domain has its own:
333
+ // - MockMailbox (for instant user transfers)
334
+ // - HypERC20Collateral (warp token with liquidity)
335
+ // - MockValueTransferBridge (for delayed rebalancer transfers)
336
+ ```
337
+
338
+ This approach enables fast, deterministic testing without multi-process coordination.
339
+
340
+ ### Fast Real-Time Execution
341
+
342
+ Simulations run in "compressed" real-time:
343
+
344
+ | Real World | Simulation Default |
345
+ | ------------- | ------------------ |
346
+ | 30s bridge | 500ms |
347
+ | 60s polling | 1000ms |
348
+ | 5min scenario | ~10s |
349
+
350
+ Configure via `SimulationTiming`:
351
+
352
+ ```typescript
353
+ interface SimulationTiming {
354
+ bridgeDeliveryDelay: number; // ms - bridge transfer time
355
+ rebalancerPollingFrequency: number; // ms - how often rebalancer checks
356
+ userTransferInterval: number; // ms - spacing between user transfers
357
+ }
358
+ ```
359
+
360
+ ### Observation Isolation
361
+
362
+ Rebalancers can ONLY observe state via:
363
+
364
+ - JSON-RPC balance queries (`eth_call` to ERC20.balanceOf)
365
+ - Event logs (`eth_getLogs`)
366
+ - View functions (ISM queries, router configs)
367
+
368
+ NOT allowed:
369
+
370
+ - Direct contract object access
371
+ - Simulation internal state
372
+ - Bridge controller pending queue
373
+
374
+ This ensures the simulation tests realistic rebalancer behavior.
375
+
376
+ ## Programmatic Usage
377
+
378
+ ### Basic Simulation
379
+
380
+ ```typescript
381
+ import {
382
+ RebalancerSimulationHarness,
383
+ ScenarioLoader,
384
+ SimpleRunner,
385
+ } from '@hyperlane-xyz/rebalancer-sim';
386
+
387
+ // Load scenario from JSON
388
+ const scenario = ScenarioLoader.loadScenario('balanced-bidirectional');
389
+
390
+ // Create and initialize harness (deploys contracts on anvil)
391
+ const harness = new RebalancerSimulationHarness({
392
+ anvilRpc: 'http://localhost:8545',
393
+ initialCollateralBalance: BigInt(scenario.defaultInitialCollateral),
394
+ });
395
+ await harness.initialize();
396
+
397
+ // Run simulation
398
+ const result = await harness.runSimulation(scenario, new SimpleRunner(), {
399
+ bridgeConfig: scenario.defaultBridgeConfig,
400
+ timing: scenario.defaultTiming,
401
+ strategyConfig: scenario.defaultStrategyConfig,
402
+ });
403
+
404
+ console.log(`Completion: ${result.kpis.completionRate * 100}%`);
405
+ console.log(`Avg Latency: ${result.kpis.averageLatency}ms`);
406
+ console.log(`Rebalances: ${result.kpis.totalRebalances}`);
407
+ ```
408
+
409
+ ### Compare Rebalancers
410
+
411
+ ```typescript
412
+ import {
413
+ ProductionRebalancerRunner,
414
+ SimpleRunner,
415
+ } from '@hyperlane-xyz/rebalancer-sim';
416
+
417
+ const rebalancers = [
418
+ new SimpleRunner(), // Simplified baseline
419
+ new ProductionRebalancerRunner(), // Production rebalancer service
420
+ ];
421
+
422
+ // compareRebalancers() handles state reset internally
423
+ const report = await harness.compareRebalancers(scenario, rebalancers, {
424
+ strategyConfig: scenario.defaultStrategyConfig,
425
+ });
426
+
427
+ for (const result of report.results) {
428
+ console.log(`${result.rebalancerName}: ${result.kpis.completionRate * 100}%`);
429
+ }
430
+ console.log(`Best latency: ${report.comparison.bestLatency}`);
431
+ ```
432
+
433
+ ### Generate Custom Scenarios
434
+
435
+ ```typescript
436
+ import { parseEther } from 'ethers/lib/utils';
437
+
438
+ import { ScenarioGenerator } from '@hyperlane-xyz/rebalancer-sim';
439
+
440
+ // Unidirectional flow (tests drain)
441
+ const drainScenario = ScenarioGenerator.unidirectionalFlow({
442
+ origin: 'chain1',
443
+ destination: 'chain2',
444
+ transferCount: 100,
445
+ duration: 10000,
446
+ amount: parseEther('1'),
447
+ });
448
+
449
+ // Random traffic across all chains
450
+ const randomScenario = ScenarioGenerator.randomTraffic({
451
+ chains: ['chain1', 'chain2', 'chain3'],
452
+ transferCount: 50,
453
+ duration: 5000,
454
+ amountRange: [parseEther('1'), parseEther('10')],
455
+ });
456
+
457
+ // Surge pattern (spike mid-scenario)
458
+ const surgeScenario = ScenarioGenerator.surgeScenario({
459
+ chains: ['chain1', 'chain2', 'chain3'],
460
+ baselineRate: 1, // 1 tx/s baseline
461
+ surgeMultiplier: 5, // 5x during surge
462
+ surgeStart: 3000,
463
+ surgeDuration: 2000,
464
+ totalDuration: 10000,
465
+ amountRange: [parseEther('1'), parseEther('5')],
466
+ });
467
+
468
+ // Balanced bidirectional traffic (equal in/out per chain)
469
+ const balancedScenario = ScenarioGenerator.balancedTraffic({
470
+ chains: ['chain1', 'chain2', 'chain3'],
471
+ pairCount: 10, // Creates 20 transfers (10 pairs of A→B, B→A)
472
+ duration: 5000,
473
+ amountRange: [parseEther('1'), parseEther('5')],
474
+ });
475
+ ```
476
+
477
+ ## Future Work
478
+
479
+ ### Phase 9: Backtesting with Real Warp Route History
480
+
481
+ **Goal:** Replay historical warp route traffic to backtest rebalancer strategies.
482
+
483
+ **Planned implementation:**
484
+
485
+ ```typescript
486
+ // Load historical transfers from explorer or indexer
487
+ const historicalTransfers = await fetchWarpRouteHistory({
488
+ warpRouteId: 'ETH/USDC-ethereum-arbitrum-optimism',
489
+ startDate: '2024-01-01',
490
+ endDate: '2024-03-01',
491
+ });
492
+
493
+ // Convert to scenario format
494
+ const scenario = ScenarioGenerator.fromHistoricalData(historicalTransfers);
495
+
496
+ // Run simulation with historical traffic
497
+ const result = await harness.runSimulation(scenario, rebalancer, config);
498
+ ```
499
+
500
+ **Benefits:**
501
+
502
+ - Validate strategies against real-world traffic patterns
503
+ - Identify edge cases that synthetic scenarios miss
504
+ - Compare how different strategies would have performed historically
505
+
506
+ ### Phase 10: Mock Explorer API for Inflight Guard
507
+
508
+ **Goal:** Enable testing of inflight guard functionality without real Explorer infrastructure.
509
+
510
+ The real rebalancer uses `WithInflightGuard` wrapper that queries Hyperlane Explorer API to track pending (inflight) transfers. This prevents over-rebalancing by accounting for transfers in the bridge pipeline.
511
+
512
+ **Planned implementation:**
513
+
514
+ ```typescript
515
+ // src/mocks/MockExplorerApi.ts
516
+ export class MockExplorerApi {
517
+ // Called by BridgeMockController when transfer initiated
518
+ registerPendingTransfer(transfer: {
519
+ messageId;
520
+ origin;
521
+ destination;
522
+ amount;
523
+ }): void;
524
+
525
+ // Called by BridgeMockController when transfer delivered
526
+ markDelivered(messageId: string): void;
527
+
528
+ // Called by rebalancer's inflight guard
529
+ async getInflightTransfers(origin, destination): Promise<InflightTransfer[]>;
530
+ }
531
+ ```
532
+
533
+ **Integration points:**
534
+
535
+ - BridgeMockController calls `registerPendingTransfer()` on bridge initiation
536
+ - BridgeMockController calls `markDelivered()` on bridge delivery
537
+ - RealRebalancerRunner injects mock API for inflight queries
538
+
539
+ **Expected outcome:** `inflight-guard.test.ts` should PASS (1-2 rebalances instead of 30+) once mock explorer is integrated.
540
+
541
+ ### Phase 11: Advanced Scenarios
542
+
543
+ **Bridge Failures and Latency Variance**
544
+
545
+ - Configure `failureRate > 0` in bridge config
546
+ - Test rebalancer recovery after partial failures
547
+ - Verify no stuck state after transient failures
548
+ - Asymmetric delays: `chain1→chain2: 500ms`, `chain2→chain1: 2000ms`
549
+ - Variable latency per route for heterogeneous bridge environments
550
+
551
+ **Rebalancer Restart**
552
+
553
+ - Stop rebalancer mid-scenario, restart
554
+ - Verify recovery and correct state resumption
555
+ - Test idempotency of rebalance operations
556
+
557
+ **Scoring Based on Rebalancing Cost**
558
+
559
+ - Mock gas prices per chain
560
+ - Track total gas cost in KPIs (already partially implemented)
561
+ - Add rebalancing cost as scoring metric:
562
+ ```typescript
563
+ const score =
564
+ completionRate * 0.5 +
565
+ (1 - normalizedLatency) * 0.3 +
566
+ (1 - normalizedCost) * 0.2;
567
+ ```
568
+ - Compare strategies by cost-efficiency ratio
569
+
570
+ ### Phase 12: Enhanced Visualization
571
+
572
+ **Real-time dashboard** (stretch goal):
573
+
574
+ - WebSocket updates during simulation
575
+ - Live balance curves
576
+ - Transfer animation
577
+
578
+ **Comparison views:**
579
+
580
+ - Side-by-side rebalancer comparison in single HTML
581
+ - Diff highlighting for KPI differences
582
+ - Strategy effectiveness scoring
@@ -0,0 +1,87 @@
1
+ import { ethers } from 'ethers';
2
+ import { EventEmitter } from 'events';
3
+ import type { BridgeMockConfig, DeployedDomain, PendingTransfer } from './types.js';
4
+ /**
5
+ * BridgeMockController manages simulated bridge transfers with configurable
6
+ * delays, failures, and fees. It intercepts SentTransferRemote events and
7
+ * schedules async delivery to simulate real bridge behavior.
8
+ */
9
+ export declare class BridgeMockController extends EventEmitter {
10
+ private readonly provider;
11
+ private readonly domains;
12
+ private readonly deployerKey;
13
+ private readonly bridgeConfig;
14
+ private pendingTransfers;
15
+ private completedTransfers;
16
+ private transferCounter;
17
+ private deliveryTimers;
18
+ private isRunning;
19
+ private eventListeners;
20
+ private txQueue;
21
+ private txProcessing;
22
+ constructor(provider: ethers.providers.JsonRpcProvider, domains: Record<string, DeployedDomain>, deployerKey: string, bridgeConfig?: BridgeMockConfig);
23
+ /**
24
+ * Queue a transaction to be executed serially (prevents nonce collisions)
25
+ */
26
+ private queueTransaction;
27
+ /**
28
+ * Process queued transactions one at a time
29
+ */
30
+ private processQueue;
31
+ /**
32
+ * Gets the bridge config for a specific route
33
+ */
34
+ private getRouteConfig;
35
+ /**
36
+ * Calculates delivery delay with jitter
37
+ */
38
+ private calculateDelay;
39
+ /**
40
+ * Start listening for bridge events
41
+ */
42
+ start(): Promise<void>;
43
+ /**
44
+ * Stop listening and cancel pending deliveries
45
+ */
46
+ stop(): Promise<void>;
47
+ /**
48
+ * Handle transfer initiated event
49
+ */
50
+ private onTransferInitiated;
51
+ /**
52
+ * Execute delivery of a pending transfer
53
+ */
54
+ private executeDelivery;
55
+ /**
56
+ * Simulate bridge delivery by burning from origin bridge and minting to destination.
57
+ * This maintains token conservation across the simulation.
58
+ * Uses transaction queue to prevent nonce collisions.
59
+ */
60
+ private simulateBridgeDelivery;
61
+ /**
62
+ * Manually trigger delivery for a pending transfer (for testing)
63
+ */
64
+ forceDelivery(transferId: string): Promise<void>;
65
+ /**
66
+ * Check if there are pending transfers
67
+ */
68
+ hasPendingTransfers(): boolean;
69
+ /**
70
+ * Get count of pending transfers
71
+ */
72
+ getPendingCount(): number;
73
+ /**
74
+ * Get all pending transfers
75
+ */
76
+ getPendingTransfers(): PendingTransfer[];
77
+ /**
78
+ * Get completed transfers
79
+ */
80
+ getCompletedTransfers(): PendingTransfer[];
81
+ /**
82
+ * Wait for all pending transfers to complete
83
+ * On timeout, marks remaining transfers as failed and clears them
84
+ */
85
+ waitForAllDeliveries(timeoutMs?: number): Promise<void>;
86
+ }
87
+ //# sourceMappingURL=BridgeMockController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BridgeMockController.d.ts","sourceRoot":"","sources":["../src/BridgeMockController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAStC,OAAO,KAAK,EAEV,gBAAgB,EAEhB,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAKpB;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;IAalD,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAf/B,OAAO,CAAC,gBAAgB,CAA2C;IACnE,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,cAAc,CAA0C;IAChE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAA2C;IAGjE,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,YAAY,CAAS;gBAGV,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,eAAe,EAC1C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,EACvC,WAAW,EAAE,MAAM,EACnB,YAAY,GAAE,gBAAqB;IAKtD;;OAEG;YACW,gBAAgB;IAc9B;;OAEG;YACW,YAAY;IAiB1B;;OAEG;IACH,OAAO,CAAC,cAAc;IAStB;;OAEG;IACH,OAAO,CAAC,cAAc;IAKtB;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA+B5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB3B;;OAEG;YACW,mBAAmB;IAiEjC;;OAEG;YACW,eAAe;IAwD7B;;;;OAIG;YACW,sBAAsB;IAgCpC;;OAEG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBtD;;OAEG;IACH,mBAAmB,IAAI,OAAO;IAI9B;;OAEG;IACH,eAAe,IAAI,MAAM;IAIzB;;OAEG;IACH,mBAAmB,IAAI,eAAe,EAAE;IAIxC;;OAEG;IACH,qBAAqB,IAAI,eAAe,EAAE;IAI1C;;;OAGG;IACG,oBAAoB,CAAC,SAAS,GAAE,MAAc,GAAG,OAAO,CAAC,IAAI,CAAC;CA2BrE"}