@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,1321 @@
1
+ import { toVisualizationData } from '../types.js';
2
+ const DEFAULT_OPTIONS = {
3
+ width: 1200,
4
+ rowHeight: 150,
5
+ showBalances: true,
6
+ showRebalances: true,
7
+ title: '',
8
+ };
9
+ /**
10
+ * Generate a standalone HTML timeline visualization
11
+ */
12
+ export function generateTimelineHtml(results, options = {}, config) {
13
+ const opts = { ...DEFAULT_OPTIONS, ...options };
14
+ const visualizations = results.map((r) => toVisualizationData(r, config));
15
+ const title = opts.title || `Simulation: ${visualizations[0]?.scenario || 'Unknown'}`;
16
+ // Serialize data for embedding (handle BigInt)
17
+ const serializedData = JSON.stringify(visualizations, (_, value) => (typeof value === 'bigint' ? value.toString() : value), 2);
18
+ return `<!DOCTYPE html>
19
+ <html lang="en">
20
+ <head>
21
+ <meta charset="UTF-8">
22
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
23
+ <title>${escapeHtml(title)}</title>
24
+ <style>
25
+ ${getStyles(opts)}
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div class="container">
30
+ <h1>${escapeHtml(title)}</h1>
31
+ <div id="config-panel"></div>
32
+ <div id="timeline-container"></div>
33
+ <div id="legend"></div>
34
+ <div id="details-panel"></div>
35
+ </div>
36
+
37
+ <script>
38
+ ${getScript(opts)}
39
+
40
+ // Embedded simulation data
41
+ const simulationData = ${serializedData};
42
+
43
+ // Render on load
44
+ document.addEventListener('DOMContentLoaded', () => {
45
+ renderVisualization(simulationData);
46
+ });
47
+ </script>
48
+ </body>
49
+ </html>`;
50
+ }
51
+ function escapeHtml(str) {
52
+ return str
53
+ .replace(/&/g, '&amp;')
54
+ .replace(/</g, '&lt;')
55
+ .replace(/>/g, '&gt;')
56
+ .replace(/"/g, '&quot;');
57
+ }
58
+ function getStyles(opts) {
59
+ return `
60
+ * {
61
+ box-sizing: border-box;
62
+ margin: 0;
63
+ padding: 0;
64
+ }
65
+
66
+ body {
67
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
68
+ background: #1a1a2e;
69
+ color: #e0e0e0;
70
+ padding: 20px;
71
+ line-height: 1.6;
72
+ }
73
+
74
+ .container {
75
+ max-width: ${opts.width + 150}px;
76
+ margin: 0 auto;
77
+ }
78
+
79
+ h1 {
80
+ margin-bottom: 20px;
81
+ color: #fff;
82
+ font-size: 1.5rem;
83
+ }
84
+
85
+ .rebalancer-section {
86
+ margin-bottom: 40px;
87
+ background: #252542;
88
+ border-radius: 8px;
89
+ padding: 20px;
90
+ }
91
+
92
+ .rebalancer-title {
93
+ font-size: 1.2rem;
94
+ margin-bottom: 15px;
95
+ color: #fff;
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 10px;
99
+ }
100
+
101
+ .rebalancer-badge {
102
+ font-size: 0.7rem;
103
+ padding: 3px 8px;
104
+ background: #4ecdc4;
105
+ color: #1a1a2e;
106
+ border-radius: 4px;
107
+ }
108
+
109
+ .kpi-row {
110
+ display: flex;
111
+ gap: 15px;
112
+ margin-bottom: 15px;
113
+ flex-wrap: wrap;
114
+ }
115
+
116
+ .kpi-card {
117
+ background: #1e1e30;
118
+ padding: 12px 16px;
119
+ border-radius: 6px;
120
+ min-width: 120px;
121
+ }
122
+
123
+ .kpi-card .label {
124
+ font-size: 0.75rem;
125
+ color: #888;
126
+ text-transform: uppercase;
127
+ }
128
+
129
+ .kpi-card .value {
130
+ font-size: 1.3rem;
131
+ font-weight: bold;
132
+ color: #4ecdc4;
133
+ }
134
+
135
+ .kpi-card.warning .value {
136
+ color: #f9c74f;
137
+ }
138
+
139
+ .timeline-wrapper {
140
+ position: relative;
141
+ margin-top: 20px;
142
+ }
143
+
144
+ .timeline-svg {
145
+ background: #1e1e30;
146
+ border-radius: 8px;
147
+ display: block;
148
+ }
149
+
150
+ .chain-label {
151
+ font-size: 12px;
152
+ fill: #aaa;
153
+ font-family: monospace;
154
+ font-weight: bold;
155
+ }
156
+
157
+ .balance-label {
158
+ font-size: 9px;
159
+ fill: #666;
160
+ font-family: monospace;
161
+ }
162
+
163
+ .time-axis-label {
164
+ font-size: 10px;
165
+ fill: #666;
166
+ }
167
+
168
+ .transfer-group {
169
+ cursor: pointer;
170
+ }
171
+
172
+ .transfer-group:hover .transfer-bar {
173
+ filter: brightness(1.2);
174
+ stroke: #fff;
175
+ stroke-width: 2;
176
+ }
177
+
178
+ .transfer-bar {
179
+ transition: filter 0.2s, stroke 0.2s;
180
+ }
181
+
182
+ .transfer-label {
183
+ font-size: 10px;
184
+ fill: #fff;
185
+ font-weight: bold;
186
+ pointer-events: none;
187
+ }
188
+
189
+ .transfer-time-label {
190
+ font-size: 8px;
191
+ fill: #888;
192
+ font-family: monospace;
193
+ }
194
+
195
+ .start-marker {
196
+ fill: #fff;
197
+ stroke: none;
198
+ }
199
+
200
+ .end-marker {
201
+ stroke-width: 2;
202
+ }
203
+
204
+ .rebalance-marker {
205
+ cursor: pointer;
206
+ }
207
+
208
+ .rebalance-marker:hover .rebalance-bar {
209
+ stroke: #fff;
210
+ stroke-width: 2;
211
+ }
212
+
213
+ .rebalance-bar {
214
+ transition: stroke 0.2s;
215
+ }
216
+
217
+ .rebalance-arrow {
218
+ stroke-width: 2;
219
+ stroke-dasharray: 4,2;
220
+ }
221
+
222
+ .balance-line {
223
+ fill: none;
224
+ stroke-width: 2;
225
+ opacity: 0.7;
226
+ }
227
+
228
+ .balance-hover-area {
229
+ fill: transparent;
230
+ stroke: transparent;
231
+ stroke-width: 20;
232
+ cursor: crosshair;
233
+ }
234
+
235
+ #config-panel {
236
+ display: flex;
237
+ gap: 30px;
238
+ margin-bottom: 20px;
239
+ padding: 15px;
240
+ background: #252542;
241
+ border-radius: 8px;
242
+ flex-wrap: wrap;
243
+ }
244
+
245
+ .config-section {
246
+ display: flex;
247
+ flex-direction: column;
248
+ gap: 6px;
249
+ }
250
+
251
+ .config-title {
252
+ font-size: 0.75rem;
253
+ color: #888;
254
+ text-transform: uppercase;
255
+ margin-bottom: 4px;
256
+ }
257
+
258
+ .config-item {
259
+ display: flex;
260
+ align-items: center;
261
+ gap: 8px;
262
+ font-size: 0.85rem;
263
+ }
264
+
265
+ .config-label {
266
+ color: #888;
267
+ min-width: 80px;
268
+ }
269
+
270
+ .config-value {
271
+ color: #fff;
272
+ font-family: monospace;
273
+ }
274
+
275
+ #legend {
276
+ display: flex;
277
+ gap: 25px;
278
+ margin-top: 20px;
279
+ flex-wrap: wrap;
280
+ padding: 15px;
281
+ background: #252542;
282
+ border-radius: 8px;
283
+ }
284
+
285
+ .legend-section {
286
+ display: flex;
287
+ flex-direction: column;
288
+ gap: 8px;
289
+ }
290
+
291
+ .legend-title {
292
+ font-size: 0.75rem;
293
+ color: #888;
294
+ text-transform: uppercase;
295
+ margin-bottom: 4px;
296
+ }
297
+
298
+ .legend-item {
299
+ display: flex;
300
+ align-items: center;
301
+ gap: 8px;
302
+ font-size: 0.85rem;
303
+ }
304
+
305
+ .legend-color {
306
+ width: 24px;
307
+ height: 12px;
308
+ border-radius: 2px;
309
+ }
310
+
311
+ .legend-line {
312
+ width: 24px;
313
+ height: 3px;
314
+ border-radius: 1px;
315
+ }
316
+
317
+ .legend-marker {
318
+ width: 10px;
319
+ height: 10px;
320
+ border-radius: 50%;
321
+ }
322
+
323
+ #details-panel {
324
+ margin-top: 20px;
325
+ padding: 15px;
326
+ background: #252542;
327
+ border-radius: 8px;
328
+ display: none;
329
+ }
330
+
331
+ #details-panel.visible {
332
+ display: block;
333
+ }
334
+
335
+ #details-panel h3 {
336
+ margin-bottom: 10px;
337
+ color: #fff;
338
+ }
339
+
340
+ #details-panel .detail-row {
341
+ display: flex;
342
+ gap: 10px;
343
+ margin: 5px 0;
344
+ }
345
+
346
+ #details-panel .detail-label {
347
+ color: #888;
348
+ min-width: 100px;
349
+ }
350
+
351
+ #details-panel .detail-value {
352
+ color: #fff;
353
+ font-family: monospace;
354
+ }
355
+
356
+ .tooltip {
357
+ position: absolute;
358
+ background: rgba(30, 30, 48, 0.95);
359
+ color: #fff;
360
+ padding: 10px 14px;
361
+ border-radius: 6px;
362
+ font-size: 12px;
363
+ pointer-events: none;
364
+ z-index: 1000;
365
+ max-width: 300px;
366
+ border: 1px solid #444;
367
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
368
+ }
369
+
370
+ .tooltip strong {
371
+ color: #4ecdc4;
372
+ }
373
+ `;
374
+ }
375
+ function getScript(opts) {
376
+ return `
377
+ const WIDTH = ${opts.width};
378
+ const ROW_HEIGHT = ${opts.rowHeight};
379
+ const SHOW_BALANCES = ${opts.showBalances};
380
+ const SHOW_REBALANCES = ${opts.showRebalances};
381
+ const MARGIN = { top: 50, right: 30, bottom: 40, left: 100 };
382
+
383
+ // Distinct colors for transfers (T1, T2, T3, etc.)
384
+ const TRANSFER_COLORS = [
385
+ '#00b4d8', // cyan
386
+ '#06d6a0', // green
387
+ '#ffd166', // yellow
388
+ '#ef476f', // pink
389
+ '#118ab2', // blue
390
+ '#073b4c', // dark blue
391
+ '#e76f51', // orange
392
+ '#2a9d8f', // teal
393
+ ];
394
+
395
+ // Colors for balance curves per chain
396
+ const CHAIN_COLORS = {
397
+ chain1: '#f9c74f', // yellow/gold
398
+ chain2: '#4ecdc4', // teal
399
+ chain3: '#f94144', // red
400
+ chain4: '#90be6d', // green
401
+ chain5: '#577590', // blue-gray
402
+ };
403
+
404
+ const REBALANCE_COLOR = '#9b59b6'; // purple
405
+
406
+ function renderVisualization(data) {
407
+ const container = document.getElementById('timeline-container');
408
+ const legend = document.getElementById('legend');
409
+ const configPanel = document.getElementById('config-panel');
410
+
411
+ // Render config panel (from first viz)
412
+ if (data[0]?.config) {
413
+ renderConfig(configPanel, data[0].config, data[0].chains);
414
+ }
415
+
416
+ // Render each rebalancer's results
417
+ data.forEach((viz, index) => {
418
+ const section = document.createElement('div');
419
+ section.className = 'rebalancer-section';
420
+
421
+ // Title
422
+ const titleDiv = document.createElement('div');
423
+ titleDiv.className = 'rebalancer-title';
424
+ titleDiv.innerHTML = '<span>' + viz.rebalancerName + '</span>' +
425
+ '<span class="rebalancer-badge">Rebalancer ' + (index + 1) + '</span>';
426
+ section.appendChild(titleDiv);
427
+
428
+ // KPIs
429
+ section.appendChild(renderKPIs(viz));
430
+
431
+ // Timeline SVG
432
+ section.appendChild(renderTimeline(viz, index));
433
+
434
+ container.appendChild(section);
435
+ });
436
+
437
+ // Legend
438
+ renderLegend(legend, data[0]);
439
+ }
440
+
441
+ function renderKPIs(viz) {
442
+ const kpis = viz.kpis;
443
+ const div = document.createElement('div');
444
+ div.className = 'kpi-row';
445
+
446
+ const completionClass = kpis.completionRate < 0.95 ? 'warning' : '';
447
+ const latencyClass = kpis.p95Latency > 1000 ? 'warning' : '';
448
+
449
+ div.innerHTML = \`
450
+ <div class="kpi-card \${completionClass}">
451
+ <div class="label">Completion</div>
452
+ <div class="value">\${(kpis.completionRate * 100).toFixed(1)}%</div>
453
+ </div>
454
+ <div class="kpi-card">
455
+ <div class="label">Transfers</div>
456
+ <div class="value">\${kpis.completedTransfers}/\${kpis.totalTransfers}</div>
457
+ </div>
458
+ <div class="kpi-card \${latencyClass}">
459
+ <div class="label">Avg Latency</div>
460
+ <div class="value">\${kpis.averageLatency.toFixed(0)}ms</div>
461
+ </div>
462
+ <div class="kpi-card">
463
+ <div class="label">P95 Latency</div>
464
+ <div class="value">\${kpis.p95Latency.toFixed(0)}ms</div>
465
+ </div>
466
+ <div class="kpi-card">
467
+ <div class="label">Rebalances</div>
468
+ <div class="value">\${kpis.totalRebalances}</div>
469
+ </div>
470
+ \`;
471
+ return div;
472
+ }
473
+
474
+ function renderTimeline(viz, vizIndex) {
475
+ const chains = viz.chains;
476
+ const height = MARGIN.top + chains.length * ROW_HEIGHT + MARGIN.bottom;
477
+ const innerWidth = WIDTH - MARGIN.left - MARGIN.right;
478
+
479
+ // Time scale
480
+ const timeExtent = [viz.startTime, viz.endTime];
481
+ const duration = timeExtent[1] - timeExtent[0];
482
+ const xScale = (t) => MARGIN.left + ((t - timeExtent[0]) / duration) * innerWidth;
483
+
484
+ // Create SVG
485
+ const wrapper = document.createElement('div');
486
+ wrapper.className = 'timeline-wrapper';
487
+
488
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
489
+ svg.setAttribute('class', 'timeline-svg');
490
+ svg.setAttribute('width', WIDTH);
491
+ svg.setAttribute('height', height);
492
+
493
+ // Defs for markers
494
+ const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
495
+ defs.innerHTML = \`
496
+ <marker id="rebalance-arrow-\${vizIndex}" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
497
+ <polygon points="0 0, 8 3, 0 6" fill="\${REBALANCE_COLOR}"/>
498
+ </marker>
499
+ \`;
500
+ svg.appendChild(defs);
501
+
502
+ // Background and chain rows
503
+ chains.forEach((chain, i) => {
504
+ const y = MARGIN.top + i * ROW_HEIGHT;
505
+
506
+ // Row background
507
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
508
+ rect.setAttribute('x', MARGIN.left);
509
+ rect.setAttribute('y', y);
510
+ rect.setAttribute('width', innerWidth);
511
+ rect.setAttribute('height', ROW_HEIGHT);
512
+ rect.setAttribute('fill', i % 2 === 0 ? '#1a1a2e' : '#1e1e35');
513
+ svg.appendChild(rect);
514
+
515
+ // Chain label
516
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
517
+ text.setAttribute('class', 'chain-label');
518
+ text.setAttribute('x', MARGIN.left - 15);
519
+ text.setAttribute('y', y + ROW_HEIGHT / 2 + 4);
520
+ text.setAttribute('text-anchor', 'end');
521
+ text.textContent = chain;
522
+ svg.appendChild(text);
523
+
524
+ // Horizontal line at center of row
525
+ const centerLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
526
+ centerLine.setAttribute('x1', MARGIN.left);
527
+ centerLine.setAttribute('y1', y + ROW_HEIGHT / 2);
528
+ centerLine.setAttribute('x2', MARGIN.left + innerWidth);
529
+ centerLine.setAttribute('y2', y + ROW_HEIGHT / 2);
530
+ centerLine.setAttribute('stroke', '#333');
531
+ centerLine.setAttribute('stroke-width', '1');
532
+ svg.appendChild(centerLine);
533
+ });
534
+
535
+ // Time axis
536
+ const tickCount = Math.min(10, Math.ceil(duration / 500));
537
+ for (let i = 0; i <= tickCount; i++) {
538
+ const t = timeExtent[0] + (i / tickCount) * duration;
539
+ const x = xScale(t);
540
+
541
+ // Vertical grid line
542
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
543
+ line.setAttribute('x1', x);
544
+ line.setAttribute('y1', MARGIN.top);
545
+ line.setAttribute('x2', x);
546
+ line.setAttribute('y2', height - MARGIN.bottom);
547
+ line.setAttribute('stroke', '#2a2a45');
548
+ line.setAttribute('stroke-width', '1');
549
+ svg.appendChild(line);
550
+
551
+ // Time label
552
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
553
+ text.setAttribute('class', 'time-axis-label');
554
+ text.setAttribute('x', x);
555
+ text.setAttribute('y', height - 15);
556
+ text.setAttribute('text-anchor', 'middle');
557
+ text.textContent = ((t - timeExtent[0]) / 1000).toFixed(1) + 's';
558
+ svg.appendChild(text);
559
+ }
560
+
561
+ // Balance curves (render first, behind transfers)
562
+ if (SHOW_BALANCES && viz.balanceTimeline.length > 0) {
563
+ renderBalanceCurves(svg, viz, xScale, chains);
564
+ }
565
+
566
+ // Group transfers by origin chain for vertical stacking
567
+ const transfersByChain = {};
568
+ chains.forEach(c => transfersByChain[c] = []);
569
+ viz.transfers.forEach((t, i) => {
570
+ t._index = i; // Store original index for coloring
571
+ if (transfersByChain[t.origin]) {
572
+ transfersByChain[t.origin].push(t);
573
+ }
574
+ });
575
+
576
+ // Render transfers with distinct colors and labels
577
+ chains.forEach((chain, chainIndex) => {
578
+ const chainY = MARGIN.top + chainIndex * ROW_HEIGHT;
579
+ const transfers = transfersByChain[chain] || [];
580
+ const barHeight = 16;
581
+ const barSpacing = 20;
582
+
583
+ // Calculate usable vertical space for transfer bars (leave room for balance curves and rebalances)
584
+ const minY = chainY + 20; // Top margin within row
585
+ const maxY = chainY + ROW_HEIGHT / 2 + 10; // Stop above center + rebalance area
586
+ const usableHeight = maxY - minY;
587
+ const maxBarsPerColumn = Math.max(1, Math.floor(usableHeight / barSpacing));
588
+
589
+ // Start from top of usable area
590
+ const startY = minY + barHeight / 2;
591
+
592
+ transfers.forEach((transfer, stackIndex) => {
593
+ const color = TRANSFER_COLORS[transfer._index % TRANSFER_COLORS.length];
594
+ // Wrap stackIndex to stay within row boundaries
595
+ const wrappedIndex = stackIndex % maxBarsPerColumn;
596
+ const y = startY + wrappedIndex * barSpacing;
597
+ const startX = xScale(transfer.startTime);
598
+ const endX = transfer.endTime ? xScale(transfer.endTime) : xScale(viz.endTime);
599
+ const width = Math.max(endX - startX, 20);
600
+
601
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
602
+ g.setAttribute('class', 'transfer-group');
603
+
604
+ // Transfer bar
605
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
606
+ rect.setAttribute('class', 'transfer-bar');
607
+ rect.setAttribute('x', startX);
608
+ rect.setAttribute('y', y - barHeight / 2);
609
+ rect.setAttribute('width', width);
610
+ rect.setAttribute('height', barHeight);
611
+ rect.setAttribute('rx', 3);
612
+ rect.setAttribute('fill', color);
613
+ if (transfer.status === 'failed') {
614
+ rect.setAttribute('fill', '#f94144');
615
+ rect.setAttribute('opacity', '0.7');
616
+ } else if (transfer.status === 'pending') {
617
+ rect.setAttribute('fill', color);
618
+ rect.setAttribute('opacity', '0.5');
619
+ rect.setAttribute('stroke', color);
620
+ rect.setAttribute('stroke-width', '2');
621
+ rect.setAttribute('stroke-dasharray', '4,2');
622
+ }
623
+ g.appendChild(rect);
624
+
625
+ // Start marker (circle)
626
+ const startCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
627
+ startCircle.setAttribute('class', 'start-marker');
628
+ startCircle.setAttribute('cx', startX);
629
+ startCircle.setAttribute('cy', y);
630
+ startCircle.setAttribute('r', 4);
631
+ startCircle.setAttribute('fill', '#fff');
632
+ g.appendChild(startCircle);
633
+
634
+ // End marker (diamond) if completed
635
+ if (transfer.status === 'completed' && transfer.endTime) {
636
+ const endMarker = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
637
+ endMarker.setAttribute('class', 'end-marker');
638
+ const ex = endX;
639
+ const ey = y;
640
+ const s = 5;
641
+ endMarker.setAttribute('points', \`\${ex},\${ey-s} \${ex+s},\${ey} \${ex},\${ey+s} \${ex-s},\${ey}\`);
642
+ endMarker.setAttribute('fill', color);
643
+ endMarker.setAttribute('stroke', '#fff');
644
+ g.appendChild(endMarker);
645
+ }
646
+
647
+ // Transfer ID label inside bar
648
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
649
+ label.setAttribute('class', 'transfer-label');
650
+ label.setAttribute('x', startX + 6);
651
+ label.setAttribute('y', y + 4);
652
+ label.textContent = 'T' + (transfer._index + 1);
653
+ g.appendChild(label);
654
+
655
+ // Latency label above bar
656
+ if (transfer.latency) {
657
+ const latencyLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
658
+ latencyLabel.setAttribute('class', 'transfer-time-label');
659
+ latencyLabel.setAttribute('x', startX + width / 2);
660
+ latencyLabel.setAttribute('y', y - barHeight / 2 - 3);
661
+ latencyLabel.setAttribute('text-anchor', 'middle');
662
+ latencyLabel.textContent = transfer.latency + 'ms';
663
+ g.appendChild(latencyLabel);
664
+ }
665
+
666
+ // Arrow to destination
667
+ if (transfer.status === 'completed' && transfer.endTime) {
668
+ const destChainIndex = chains.indexOf(transfer.destination);
669
+ if (destChainIndex !== -1 && destChainIndex !== chainIndex) {
670
+ const destY = MARGIN.top + destChainIndex * ROW_HEIGHT + ROW_HEIGHT / 2;
671
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'line');
672
+ arrow.setAttribute('x1', endX);
673
+ arrow.setAttribute('y1', y);
674
+ arrow.setAttribute('x2', endX);
675
+ arrow.setAttribute('y2', destY > y ? destY - 10 : destY + 10);
676
+ arrow.setAttribute('stroke', color);
677
+ arrow.setAttribute('stroke-width', '2');
678
+ arrow.setAttribute('stroke-dasharray', '4,3');
679
+ arrow.setAttribute('opacity', '0.6');
680
+ g.appendChild(arrow);
681
+
682
+ // Arrow head at destination
683
+ const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
684
+ const ay = destY > y ? destY - 10 : destY + 10;
685
+ const dir = destY > y ? 1 : -1;
686
+ arrowHead.setAttribute('points', \`\${endX-4},\${ay} \${endX+4},\${ay} \${endX},\${ay + dir * 8}\`);
687
+ arrowHead.setAttribute('fill', color);
688
+ arrowHead.setAttribute('opacity', '0.6');
689
+ g.appendChild(arrowHead);
690
+ }
691
+ }
692
+
693
+ // Event handlers
694
+ g.addEventListener('mouseenter', (e) => {
695
+ // Bring to front by moving to end of parent
696
+ g.parentNode.appendChild(g);
697
+ showTooltip(e, transfer, 'transfer');
698
+ });
699
+ g.addEventListener('mouseleave', hideTooltip);
700
+ g.addEventListener('click', () => showDetails(transfer, 'transfer'));
701
+
702
+ svg.appendChild(g);
703
+ });
704
+ });
705
+
706
+ // Rebalance bars (similar to transfers but with distinct styling)
707
+ if (SHOW_REBALANCES) {
708
+ // Group rebalances by origin chain for vertical stacking
709
+ const rebalancesByChain = {};
710
+ chains.forEach(c => rebalancesByChain[c] = []);
711
+ viz.rebalances.forEach((r, i) => {
712
+ r._index = i;
713
+ if (rebalancesByChain[r.origin]) {
714
+ rebalancesByChain[r.origin].push(r);
715
+ }
716
+ });
717
+
718
+ chains.forEach((chain, chainIndex) => {
719
+ const chainY = MARGIN.top + chainIndex * ROW_HEIGHT;
720
+ const rebalances = rebalancesByChain[chain] || [];
721
+ const barHeight = 12;
722
+ const barSpacing = 16;
723
+
724
+ // Calculate usable vertical space for rebalance bars (below center line)
725
+ const minY = chainY + ROW_HEIGHT / 2 + 15; // Start below center
726
+ const maxY = chainY + ROW_HEIGHT - 20; // Bottom margin within row
727
+ const usableHeight = maxY - minY;
728
+ const maxBarsPerColumn = Math.max(1, Math.floor(usableHeight / barSpacing));
729
+
730
+ // Start from top of rebalance area
731
+ const startY = minY + barHeight / 2;
732
+
733
+ rebalances.forEach((rebalance, stackIndex) => {
734
+ // Wrap stackIndex to stay within row boundaries
735
+ const wrappedIndex = stackIndex % maxBarsPerColumn;
736
+ const y = startY + wrappedIndex * barSpacing;
737
+ const startX = xScale(rebalance.startTime);
738
+ const endX = rebalance.endTime ? xScale(rebalance.endTime) : xScale(viz.endTime);
739
+ const width = Math.max(endX - startX, 20);
740
+
741
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
742
+ g.setAttribute('class', 'rebalance-marker');
743
+
744
+ // Rebalance bar
745
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
746
+ rect.setAttribute('class', 'rebalance-bar');
747
+ rect.setAttribute('x', startX);
748
+ rect.setAttribute('y', y - barHeight / 2);
749
+ rect.setAttribute('width', width);
750
+ rect.setAttribute('height', barHeight);
751
+ rect.setAttribute('rx', 2);
752
+ rect.setAttribute('fill', REBALANCE_COLOR);
753
+ if (rebalance.status === 'failed') {
754
+ rect.setAttribute('fill', '#f94144');
755
+ rect.setAttribute('opacity', '0.7');
756
+ } else if (rebalance.status === 'pending') {
757
+ rect.setAttribute('opacity', '0.5');
758
+ rect.setAttribute('stroke', REBALANCE_COLOR);
759
+ rect.setAttribute('stroke-width', '2');
760
+ rect.setAttribute('stroke-dasharray', '4,2');
761
+ }
762
+ g.appendChild(rect);
763
+
764
+ // Start marker (small circle)
765
+ const startCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
766
+ startCircle.setAttribute('cx', startX);
767
+ startCircle.setAttribute('cy', y);
768
+ startCircle.setAttribute('r', 3);
769
+ startCircle.setAttribute('fill', '#fff');
770
+ g.appendChild(startCircle);
771
+
772
+ // End marker (diamond) if completed
773
+ if (rebalance.status === 'completed' && rebalance.endTime) {
774
+ const endMarker = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
775
+ const ex = endX;
776
+ const ey = y;
777
+ const s = 4;
778
+ endMarker.setAttribute('points', \`\${ex},\${ey-s} \${ex+s},\${ey} \${ex},\${ey+s} \${ex-s},\${ey}\`);
779
+ endMarker.setAttribute('fill', REBALANCE_COLOR);
780
+ endMarker.setAttribute('stroke', '#fff');
781
+ g.appendChild(endMarker);
782
+ }
783
+
784
+ // R label inside bar
785
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
786
+ label.setAttribute('x', startX + 5);
787
+ label.setAttribute('y', y + 3);
788
+ label.setAttribute('fill', '#fff');
789
+ label.setAttribute('font-size', '8');
790
+ label.setAttribute('font-weight', 'bold');
791
+ label.textContent = 'R' + (rebalance._index + 1);
792
+ g.appendChild(label);
793
+
794
+ // Latency label above bar
795
+ if (rebalance.latency) {
796
+ const latencyLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
797
+ latencyLabel.setAttribute('class', 'transfer-time-label');
798
+ latencyLabel.setAttribute('x', startX + width / 2);
799
+ latencyLabel.setAttribute('y', y - barHeight / 2 - 3);
800
+ latencyLabel.setAttribute('text-anchor', 'middle');
801
+ latencyLabel.textContent = rebalance.latency + 'ms';
802
+ g.appendChild(latencyLabel);
803
+ }
804
+
805
+ // Arrow to destination chain
806
+ const destChainIndex = chains.indexOf(rebalance.destination);
807
+ if (destChainIndex !== -1 && destChainIndex !== chainIndex && rebalance.endTime) {
808
+ const destY = MARGIN.top + destChainIndex * ROW_HEIGHT + ROW_HEIGHT / 2 + 20;
809
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'line');
810
+ arrow.setAttribute('class', 'rebalance-arrow');
811
+ arrow.setAttribute('x1', endX);
812
+ arrow.setAttribute('y1', y);
813
+ arrow.setAttribute('x2', endX);
814
+ arrow.setAttribute('y2', destY > y ? destY - 10 : destY + 10);
815
+ arrow.setAttribute('stroke', REBALANCE_COLOR);
816
+ arrow.setAttribute('marker-end', 'url(#rebalance-arrow-' + vizIndex + ')');
817
+ g.appendChild(arrow);
818
+ }
819
+
820
+ g.addEventListener('mouseenter', (e) => {
821
+ // Bring to front by moving to end of parent
822
+ g.parentNode.appendChild(g);
823
+ showTooltip(e, rebalance, 'rebalance');
824
+ });
825
+ g.addEventListener('mouseleave', hideTooltip);
826
+ g.addEventListener('click', () => showDetails(rebalance, 'rebalance'));
827
+
828
+ svg.appendChild(g);
829
+ });
830
+ });
831
+ }
832
+
833
+ // Setup balance hover listeners after all elements are added
834
+ setupBalanceHoverListeners(svg, xScale, viz.startTime);
835
+
836
+ wrapper.appendChild(svg);
837
+ return wrapper;
838
+ }
839
+
840
+ function setupBalanceHoverListeners(svg, xScale, startTime) {
841
+ const hoverAreas = svg.querySelectorAll('.balance-hover-area');
842
+ hoverAreas.forEach(hoverPath => {
843
+ hoverPath.addEventListener('mousemove', (e) => {
844
+ const chain = hoverPath.getAttribute('data-chain');
845
+ const timeline = JSON.parse(hoverPath.getAttribute('data-timeline'));
846
+ const totalBalance = BigInt(hoverPath.getAttribute('data-total-balance'));
847
+ const simStartTime = parseInt(hoverPath.getAttribute('data-start-time'));
848
+
849
+ // Get mouse X position relative to SVG
850
+ const svgRect = svg.getBoundingClientRect();
851
+ const mouseX = e.clientX - svgRect.left;
852
+
853
+ // Find the timestamp at this X position (inverse of xScale)
854
+ const innerWidth = svg.clientWidth - MARGIN.left - MARGIN.right;
855
+ const duration = timeline[timeline.length - 1].timestamp - simStartTime;
856
+ const timestamp = simStartTime + ((mouseX - MARGIN.left) / innerWidth) * duration;
857
+
858
+ // Find the balance at this timestamp (step function - find last point before timestamp)
859
+ let balance = BigInt(timeline[0].balance);
860
+ for (let i = 0; i < timeline.length; i++) {
861
+ if (timeline[i].timestamp <= timestamp) {
862
+ balance = BigInt(timeline[i].balance);
863
+ } else {
864
+ break;
865
+ }
866
+ }
867
+
868
+ // Calculate percentage of total
869
+ const percentage = totalBalance > 0n
870
+ ? (Number(balance) / Number(totalBalance) * 100).toFixed(1)
871
+ : '0.0';
872
+
873
+ showTooltip(e, {
874
+ chain,
875
+ balance,
876
+ totalBalance,
877
+ timestamp,
878
+ startTime: simStartTime,
879
+ percentage
880
+ }, 'balance');
881
+ });
882
+
883
+ hoverPath.addEventListener('mouseleave', hideTooltip);
884
+ });
885
+ }
886
+
887
+ function renderBalanceCurves(svg, viz, xScale, chains) {
888
+ // Compute balance curve from transfer/rebalance events for instant visual feedback
889
+ // Transfer start: origin +amount (user deposits collateral)
890
+ // Transfer complete: destination -amount (collateral released to recipient)
891
+ // Rebalance start: origin +amount (rebalancer deposits)
892
+ // Rebalance complete: destination -amount (collateral released)
893
+
894
+ // Get initial balances from first snapshot
895
+ const initialSnapshot = viz.balanceTimeline[0];
896
+ if (!initialSnapshot) return;
897
+
898
+ // Build event-driven balance timeline
899
+ const balanceEvents = [];
900
+
901
+ // Process transfers - instant balance changes at start and end
902
+ viz.transfers.forEach(t => {
903
+ const amount = BigInt(t.amount);
904
+ // Transfer START: origin receives deposit (+amount)
905
+ balanceEvents.push({
906
+ timestamp: t.startTime,
907
+ chain: t.origin,
908
+ delta: amount,
909
+ });
910
+ // Transfer COMPLETE: destination releases to recipient (-amount)
911
+ if (t.endTime && t.status === 'completed') {
912
+ balanceEvents.push({
913
+ timestamp: t.endTime,
914
+ chain: t.destination,
915
+ delta: -amount,
916
+ });
917
+ }
918
+ });
919
+
920
+ // Process rebalances - INVERSE of transfers (moving liquidity)
921
+ viz.rebalances.forEach(r => {
922
+ const amount = BigInt(r.amount);
923
+ // Rebalance START: origin SENDS funds away (-amount)
924
+ balanceEvents.push({
925
+ timestamp: r.startTime,
926
+ chain: r.origin,
927
+ delta: -amount,
928
+ });
929
+ // Rebalance COMPLETE: destination RECEIVES funds (+amount)
930
+ if (r.endTime && r.status === 'completed') {
931
+ balanceEvents.push({
932
+ timestamp: r.endTime,
933
+ chain: r.destination,
934
+ delta: amount,
935
+ });
936
+ }
937
+ });
938
+
939
+ // Sort events by timestamp
940
+ balanceEvents.sort((a, b) => a.timestamp - b.timestamp);
941
+
942
+ // Build per-chain balance timelines
943
+ const chainBalances = {};
944
+ const chainTimelines = {};
945
+ chains.forEach(chain => {
946
+ chainBalances[chain] = BigInt(initialSnapshot.balances[chain] || '0');
947
+ chainTimelines[chain] = [{ timestamp: viz.startTime, balance: chainBalances[chain] }];
948
+ });
949
+
950
+ // Apply deltas to build timeline
951
+ balanceEvents.forEach(event => {
952
+ if (event.delta !== undefined) {
953
+ chainBalances[event.chain] += event.delta;
954
+ chainTimelines[event.chain].push({
955
+ timestamp: event.timestamp,
956
+ balance: chainBalances[event.chain],
957
+ });
958
+ }
959
+ });
960
+
961
+ // Add final state
962
+ chains.forEach(chain => {
963
+ chainTimelines[chain].push({
964
+ timestamp: viz.endTime,
965
+ balance: chainBalances[chain],
966
+ });
967
+ });
968
+
969
+ // Find global min/max for scaling
970
+ let minBalance = BigInt('999999999999999999999999999');
971
+ let maxBalance = 0n;
972
+ chains.forEach(chain => {
973
+ chainTimelines[chain].forEach(pt => {
974
+ if (pt.balance > maxBalance) maxBalance = pt.balance;
975
+ if (pt.balance < minBalance) minBalance = pt.balance;
976
+ });
977
+ });
978
+
979
+ if (maxBalance === 0n) return;
980
+ const balanceRange = maxBalance - minBalance || 1n;
981
+
982
+ chains.forEach((chain, chainIndex) => {
983
+ const chainY = MARGIN.top + chainIndex * ROW_HEIGHT;
984
+ const curveTop = chainY + 15;
985
+ const curveBottom = chainY + ROW_HEIGHT - 15;
986
+ const curveHeight = curveBottom - curveTop;
987
+ const color = CHAIN_COLORS[chain] || TRANSFER_COLORS[chainIndex % TRANSFER_COLORS.length];
988
+
989
+ // Build path data from event-driven timeline
990
+ const timeline = chainTimelines[chain];
991
+ const points = timeline.map(pt => {
992
+ const x = xScale(pt.timestamp);
993
+ const balance = pt.balance;
994
+ // Scale: high balance = top (low y), low balance = bottom (high y)
995
+ const normalizedY = balanceRange > 0n
996
+ ? Number((balance - minBalance) * BigInt(Math.floor(curveHeight * 100)) / balanceRange) / 100
997
+ : curveHeight / 2;
998
+ const y = curveBottom - normalizedY;
999
+ return { x, y, balance };
1000
+ });
1001
+
1002
+ // Line path (step function for clearer visualization)
1003
+ let pathD = 'M' + points[0].x + ',' + points[0].y;
1004
+ for (let i = 1; i < points.length; i++) {
1005
+ // Horizontal then vertical for step effect
1006
+ pathD += ' L' + points[i].x + ',' + points[i-1].y;
1007
+ pathD += ' L' + points[i].x + ',' + points[i].y;
1008
+ }
1009
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1010
+ path.setAttribute('class', 'balance-line');
1011
+ path.setAttribute('d', pathD);
1012
+ path.setAttribute('stroke', color);
1013
+ svg.appendChild(path);
1014
+
1015
+ // Invisible hover area for tooltips (wider stroke for easier hovering)
1016
+ const hoverPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1017
+ hoverPath.setAttribute('class', 'balance-hover-area');
1018
+ hoverPath.setAttribute('d', pathD);
1019
+ hoverPath.setAttribute('data-chain', chain);
1020
+ hoverPath.setAttribute('data-timeline', JSON.stringify(timeline.map(pt => ({
1021
+ timestamp: pt.timestamp,
1022
+ balance: pt.balance.toString()
1023
+ }))));
1024
+ hoverPath.setAttribute('data-total-balance', maxBalance.toString());
1025
+ hoverPath.setAttribute('data-start-time', viz.startTime.toString());
1026
+ svg.appendChild(hoverPath);
1027
+
1028
+ // Balance labels (start and end values)
1029
+ const startBal = points[0].balance;
1030
+ const endBal = points[points.length - 1].balance;
1031
+
1032
+ const startLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
1033
+ startLabel.setAttribute('class', 'balance-label');
1034
+ startLabel.setAttribute('x', points[0].x + 3);
1035
+ startLabel.setAttribute('y', points[0].y - 3);
1036
+ startLabel.textContent = formatBalanceShort(startBal);
1037
+ startLabel.setAttribute('fill', color);
1038
+ svg.appendChild(startLabel);
1039
+
1040
+ const endLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
1041
+ endLabel.setAttribute('class', 'balance-label');
1042
+ endLabel.setAttribute('x', points[points.length-1].x - 3);
1043
+ endLabel.setAttribute('y', points[points.length-1].y - 3);
1044
+ endLabel.setAttribute('text-anchor', 'end');
1045
+ endLabel.textContent = formatBalanceShort(endBal);
1046
+ endLabel.setAttribute('fill', color);
1047
+ svg.appendChild(endLabel);
1048
+ });
1049
+ }
1050
+
1051
+ function renderLegend(container, viz) {
1052
+ const transferCount = viz.transfers.length;
1053
+
1054
+ let transferItems = '';
1055
+ for (let i = 0; i < transferCount; i++) {
1056
+ const t = viz.transfers[i];
1057
+ const color = TRANSFER_COLORS[i % TRANSFER_COLORS.length];
1058
+ transferItems += \`
1059
+ <div class="legend-item">
1060
+ <div class="legend-color" style="background: \${color}"></div>
1061
+ <span>T\${i + 1}: \${t.origin} → \${t.destination} (\${formatBalanceShort(BigInt(t.amount))})</span>
1062
+ </div>
1063
+ \`;
1064
+ }
1065
+
1066
+ let chainItems = '';
1067
+ viz.chains.forEach(chain => {
1068
+ const color = CHAIN_COLORS[chain] || '#888';
1069
+ chainItems += \`
1070
+ <div class="legend-item">
1071
+ <div class="legend-line" style="background: \${color}"></div>
1072
+ <span>\${chain} collateral balance</span>
1073
+ </div>
1074
+ \`;
1075
+ });
1076
+
1077
+ container.innerHTML = \`
1078
+ <div class="legend-section">
1079
+ <div class="legend-title">Transfers</div>
1080
+ \${transferItems}
1081
+ </div>
1082
+ <div class="legend-section">
1083
+ <div class="legend-title">Markers</div>
1084
+ <div class="legend-item">
1085
+ <div class="legend-marker" style="background: #fff"></div>
1086
+ <span>Transfer start</span>
1087
+ </div>
1088
+ <div class="legend-item">
1089
+ <div class="legend-marker" style="background: #4ecdc4; transform: rotate(45deg)"></div>
1090
+ <span>Transfer delivered</span>
1091
+ </div>
1092
+ <div class="legend-item">
1093
+ <div class="legend-marker" style="background: \${REBALANCE_COLOR}"></div>
1094
+ <span>Rebalance (R)</span>
1095
+ </div>
1096
+ </div>
1097
+ <div class="legend-section">
1098
+ <div class="legend-title">Balance Curves</div>
1099
+ \${chainItems}
1100
+ </div>
1101
+ \`;
1102
+ }
1103
+
1104
+ function renderConfig(container, config, chains) {
1105
+ if (!config) {
1106
+ container.style.display = 'none';
1107
+ return;
1108
+ }
1109
+
1110
+ // Scenario metadata section
1111
+ let scenarioHtml = '';
1112
+ if (config.description) {
1113
+ scenarioHtml += \`
1114
+ <div class="config-item" style="flex-direction: column; align-items: flex-start;">
1115
+ <span class="config-label">Description:</span>
1116
+ <span class="config-value" style="white-space: normal; margin-top: 4px;">\${config.description}</span>
1117
+ </div>
1118
+ \`;
1119
+ }
1120
+ if (config.expectedBehavior) {
1121
+ scenarioHtml += \`
1122
+ <div class="config-item" style="flex-direction: column; align-items: flex-start; margin-top: 8px;">
1123
+ <span class="config-label">Expected Behavior:</span>
1124
+ <span class="config-value expected-behavior" style="white-space: pre-wrap; margin-top: 4px; font-size: 0.8rem; color: #aaa;">\${config.expectedBehavior}</span>
1125
+ </div>
1126
+ \`;
1127
+ }
1128
+ if (config.transferCount !== undefined || config.duration !== undefined) {
1129
+ scenarioHtml += \`
1130
+ <div class="config-item" style="margin-top: 8px;">
1131
+ \${config.transferCount !== undefined ? \`<span><b>\${config.transferCount}</b> transfers</span>\` : ''}
1132
+ \${config.duration !== undefined ? \`<span style="margin-left: 15px;"><b>\${(config.duration / 1000).toFixed(1)}s</b> duration</span>\` : ''}
1133
+ </div>
1134
+ \`;
1135
+ }
1136
+
1137
+ let targetHtml = '';
1138
+ if (config.targetWeights) {
1139
+ chains.forEach(chain => {
1140
+ const weight = config.targetWeights[chain] || 0;
1141
+ const tolerance = config.tolerances?.[chain] || 0;
1142
+ targetHtml += \`
1143
+ <div class="config-item">
1144
+ <span class="config-label">\${chain}:</span>
1145
+ <span class="config-value">\${weight}% ± \${tolerance}%</span>
1146
+ </div>
1147
+ \`;
1148
+ });
1149
+ }
1150
+
1151
+ let timingHtml = '';
1152
+ if (config.userTransferDelay !== undefined) {
1153
+ timingHtml += \`
1154
+ <div class="config-item">
1155
+ <span class="config-label">User xfer:</span>
1156
+ <span class="config-value">\${config.userTransferDelay}ms</span>
1157
+ </div>
1158
+ \`;
1159
+ }
1160
+ if (config.bridgeDeliveryDelay !== undefined) {
1161
+ timingHtml += \`
1162
+ <div class="config-item">
1163
+ <span class="config-label">Rebal bridge:</span>
1164
+ <span class="config-value">\${config.bridgeDeliveryDelay}ms</span>
1165
+ </div>
1166
+ \`;
1167
+ }
1168
+ if (config.rebalancerPollingFrequency !== undefined) {
1169
+ timingHtml += \`
1170
+ <div class="config-item">
1171
+ <span class="config-label">Rebal poll:</span>
1172
+ <span class="config-value">\${config.rebalancerPollingFrequency}ms</span>
1173
+ </div>
1174
+ \`;
1175
+ }
1176
+
1177
+ let initialHtml = '';
1178
+ if (config.initialCollateral) {
1179
+ chains.forEach(chain => {
1180
+ const initial = config.initialCollateral[chain];
1181
+ if (initial) {
1182
+ const formatted = formatBalanceShort(BigInt(initial));
1183
+ initialHtml += \`
1184
+ <div class="config-item">
1185
+ <span class="config-label">\${chain}:</span>
1186
+ <span class="config-value">\${formatted} tokens</span>
1187
+ </div>
1188
+ \`;
1189
+ }
1190
+ });
1191
+ }
1192
+
1193
+ container.innerHTML = \`
1194
+ \${scenarioHtml ? \`
1195
+ <div class="config-section scenario-section" style="flex-basis: 100%; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #3a3a5a;">
1196
+ <div class="config-title">Scenario</div>
1197
+ \${scenarioHtml}
1198
+ </div>
1199
+ \` : ''}
1200
+ \${targetHtml ? \`
1201
+ <div class="config-section">
1202
+ <div class="config-title">Target Weights</div>
1203
+ \${targetHtml}
1204
+ </div>
1205
+ \` : ''}
1206
+ \${timingHtml ? \`
1207
+ <div class="config-section">
1208
+ <div class="config-title">Timing</div>
1209
+ \${timingHtml}
1210
+ </div>
1211
+ \` : ''}
1212
+ \${initialHtml ? \`
1213
+ <div class="config-section">
1214
+ <div class="config-title">Initial Collateral</div>
1215
+ \${initialHtml}
1216
+ </div>
1217
+ \` : ''}
1218
+ \`;
1219
+ }
1220
+
1221
+ let tooltipEl = null;
1222
+
1223
+ function showTooltip(event, data, type) {
1224
+ if (!tooltipEl) {
1225
+ tooltipEl = document.createElement('div');
1226
+ tooltipEl.className = 'tooltip';
1227
+ document.body.appendChild(tooltipEl);
1228
+ }
1229
+
1230
+ let content = '';
1231
+ if (type === 'transfer') {
1232
+ const status = data.status === 'completed' ? '✓ Delivered' :
1233
+ data.status === 'failed' ? '✗ Failed' : '⏳ Pending';
1234
+ content = \`
1235
+ <strong>Transfer T\${data._index + 1}</strong><br>
1236
+ <b>Route:</b> \${data.origin} → \${data.destination}<br>
1237
+ <b>Amount:</b> \${formatAmount(data.amount)}<br>
1238
+ <b>Latency:</b> \${data.latency ? data.latency + 'ms' : 'N/A'}<br>
1239
+ <b>Status:</b> \${status}
1240
+ \`;
1241
+ } else if (type === 'rebalance') {
1242
+ const status = data.status === 'completed' ? '✓ Delivered' :
1243
+ data.status === 'failed' ? '✗ Failed' : '⏳ Pending';
1244
+ content = \`
1245
+ <strong>Rebalance R\${data._index + 1}</strong><br>
1246
+ <b>Route:</b> \${data.origin} → \${data.destination}<br>
1247
+ <b>Amount:</b> \${formatAmount(data.amount)}<br>
1248
+ <b>Latency:</b> \${data.latency ? data.latency + 'ms' : 'N/A'}<br>
1249
+ <b>Status:</b> \${status}
1250
+ \`;
1251
+ } else if (type === 'balance') {
1252
+ content = \`
1253
+ <strong>\${data.chain} Collateral</strong><br>
1254
+ <b>Balance:</b> \${formatBalanceShort(data.balance)} tokens<br>
1255
+ <b>Share:</b> \${data.percentage}% of total<br>
1256
+ <b>Time:</b> \${((data.timestamp - data.startTime) / 1000).toFixed(2)}s
1257
+ \`;
1258
+ }
1259
+
1260
+ tooltipEl.innerHTML = content;
1261
+ tooltipEl.style.left = (event.pageX + 15) + 'px';
1262
+ tooltipEl.style.top = (event.pageY + 15) + 'px';
1263
+ tooltipEl.style.display = 'block';
1264
+ }
1265
+
1266
+ function hideTooltip() {
1267
+ if (tooltipEl) {
1268
+ tooltipEl.style.display = 'none';
1269
+ }
1270
+ }
1271
+
1272
+ function showDetails(data, type) {
1273
+ const panel = document.getElementById('details-panel');
1274
+ panel.classList.add('visible');
1275
+
1276
+ let html = '';
1277
+ if (type === 'transfer') {
1278
+ html = \`
1279
+ <h3>Transfer T\${data._index + 1} Details</h3>
1280
+ <div class="detail-row"><span class="detail-label">ID:</span><span class="detail-value">\${data.id}</span></div>
1281
+ <div class="detail-row"><span class="detail-label">Route:</span><span class="detail-value">\${data.origin} → \${data.destination}</span></div>
1282
+ <div class="detail-row"><span class="detail-label">Amount:</span><span class="detail-value">\${formatAmount(data.amount)}</span></div>
1283
+ <div class="detail-row"><span class="detail-label">Start:</span><span class="detail-value">\${new Date(data.startTime).toISOString()}</span></div>
1284
+ <div class="detail-row"><span class="detail-label">End:</span><span class="detail-value">\${data.endTime ? new Date(data.endTime).toISOString() : 'N/A'}</span></div>
1285
+ <div class="detail-row"><span class="detail-label">Latency:</span><span class="detail-value">\${data.latency ? data.latency + 'ms' : 'N/A'}</span></div>
1286
+ <div class="detail-row"><span class="detail-label">Status:</span><span class="detail-value">\${data.status}</span></div>
1287
+ \`;
1288
+ } else {
1289
+ html = \`
1290
+ <h3>Rebalance R\${data._index + 1} Details</h3>
1291
+ <div class="detail-row"><span class="detail-label">ID:</span><span class="detail-value">\${data.id}</span></div>
1292
+ <div class="detail-row"><span class="detail-label">Route:</span><span class="detail-value">\${data.origin} → \${data.destination}</span></div>
1293
+ <div class="detail-row"><span class="detail-label">Amount:</span><span class="detail-value">\${formatAmount(data.amount)}</span></div>
1294
+ <div class="detail-row"><span class="detail-label">Start:</span><span class="detail-value">\${new Date(data.startTime).toISOString()}</span></div>
1295
+ <div class="detail-row"><span class="detail-label">End:</span><span class="detail-value">\${data.endTime ? new Date(data.endTime).toISOString() : 'N/A'}</span></div>
1296
+ <div class="detail-row"><span class="detail-label">Latency:</span><span class="detail-value">\${data.latency ? data.latency + 'ms' : 'N/A'}</span></div>
1297
+ <div class="detail-row"><span class="detail-label">Gas Cost:</span><span class="detail-value">\${formatAmount(data.gasCost)}</span></div>
1298
+ <div class="detail-row"><span class="detail-label">Status:</span><span class="detail-value">\${data.status}</span></div>
1299
+ \`;
1300
+ }
1301
+
1302
+ panel.innerHTML = html;
1303
+ }
1304
+
1305
+ function formatAmount(amount) {
1306
+ const val = BigInt(amount);
1307
+ const eth = Number(val) / 1e18;
1308
+ if (eth >= 1) return eth.toFixed(2) + ' tokens';
1309
+ if (eth >= 0.001) return (eth * 1000).toFixed(2) + ' mTokens';
1310
+ return val.toString() + ' wei';
1311
+ }
1312
+
1313
+ function formatBalanceShort(balance) {
1314
+ const eth = Number(balance) / 1e18;
1315
+ if (eth >= 1000) return (eth / 1000).toFixed(1) + 'k';
1316
+ if (eth >= 1) return eth.toFixed(0);
1317
+ return eth.toFixed(2);
1318
+ }
1319
+ `;
1320
+ }
1321
+ //# sourceMappingURL=HtmlTimelineGenerator.js.map