Skip to content

Instantly share code, notes, and snippets.

@nambrot
Last active January 20, 2026 22:17
Show Gist options
  • Select an option

  • Save nambrot/a09469043d9e718ea0ab2b312fc3b0a0 to your computer and use it in GitHub Desktop.

Select an option

Save nambrot/a09469043d9e718ea0ab2b312fc3b0a0 to your computer and use it in GitHub Desktop.
StableSwap Collateral Flow Simulator - Option A Architecture
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>StableSwap Collateral Flow Simulator</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
background: #0d1117;
color: #c9d1d9;
padding: 20px;
min-height: 100vh;
}
h1 {
color: #58a6ff;
margin-bottom: 8px;
font-size: 1.5rem;
}
.subtitle {
color: #8b949e;
margin-bottom: 24px;
font-size: 0.9rem;
}
.container {
max-width: 1800px;
margin: 0 auto;
}
/* Global Stats */
.global-stats {
display: flex;
gap: 24px;
margin-bottom: 24px;
padding: 16px;
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
flex-wrap: wrap;
}
.stat {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 0.75rem;
color: #8b949e;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 1.25rem;
font-weight: 600;
color: #58a6ff;
}
.stat-value.success {
color: #3fb950;
}
.stat-value.warning {
color: #d29922;
}
.stat-value.error {
color: #f85149;
}
/* Architecture Diagram */
.architecture {
position: relative;
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
}
.arch-title {
font-size: 0.8rem;
color: #8b949e;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 20px;
text-align: center;
}
/* Triangle Layout Container */
.triangle-container {
position: relative;
width: 100%;
height: 750px;
}
/* Chain positioning for triangle */
.chain-column {
position: absolute;
width: 320px;
}
.chain-column.top {
top: 0;
left: 50%;
transform: translateX(-50%);
}
.chain-column.bottom-left {
bottom: 0;
left: 5%;
}
.chain-column.bottom-right {
bottom: 0;
right: 5%;
}
.chain-header {
text-align: center;
padding: 10px;
background: #0d1117;
border: 2px solid;
border-radius: 8px 8px 0 0;
border-bottom: none;
}
.chain-name {
font-weight: 700;
font-size: 1rem;
}
.chain-domain {
font-size: 0.65rem;
color: #8b949e;
}
.chain-body {
background: #0d1117;
border: 2px solid #30363d;
border-top: none;
border-radius: 0 0 8px 8px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
/* Adapters Container */
.adapters-container {
display: flex;
flex-direction: column;
gap: 6px;
}
.adapters-label {
font-size: 0.6rem;
color: #8b949e;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Adapter Box */
.adapter-box {
background: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 8px;
position: relative;
}
.adapter-box.highlight-deposit {
border-color: #58a6ff;
box-shadow: 0 0 10px rgba(88, 166, 255, 0.3);
}
.adapter-box.highlight-redeem {
border-color: #3fb950;
box-shadow: 0 0 10px rgba(63, 185, 80, 0.3);
}
.adapter-box.highlight-rebalance {
border-color: #f97316;
box-shadow: 0 0 10px rgba(249, 115, 22, 0.3);
}
.adapter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.adapter-token {
font-weight: 600;
font-size: 0.85rem;
color: #f0f6fc;
}
.adapter-label {
font-size: 0.5rem;
color: #8b949e;
background: #21262d;
padding: 2px 4px;
border-radius: 3px;
}
.adapter-stats {
font-size: 0.7rem;
}
.adapter-stat {
display: flex;
justify-content: space-between;
}
.adapter-stat-label {
color: #8b949e;
}
.adapter-stat-value {
font-family: monospace;
}
.adapter-stat-value.healthy {
color: #3fb950;
}
.adapter-stat-value.low {
color: #d29922;
}
.adapter-stat-value.depleted {
color: #f85149;
}
.adapter-stat-value.fee {
color: #d29922;
}
/* sUSD Route Box - Separate from adapters */
.susd-route-container {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #30363d;
}
.susd-route-label {
font-size: 0.6rem;
color: #a371f7;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.susd-box {
background: rgba(163, 113, 247, 0.15);
border: 2px solid rgba(163, 113, 247, 0.5);
border-radius: 6px;
padding: 10px;
}
.susd-box.highlight {
border-color: #a371f7;
box-shadow: 0 0 12px rgba(163, 113, 247, 0.5);
}
.susd-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.susd-token {
font-weight: 600;
color: #a371f7;
font-size: 0.9rem;
}
.susd-label {
font-size: 0.5rem;
color: #a371f7;
background: rgba(163, 113, 247, 0.25);
padding: 2px 4px;
border-radius: 3px;
}
.susd-supply {
font-family: monospace;
color: #a371f7;
font-size: 0.8rem;
text-align: right;
margin-top: 4px;
}
/* User boxes */
.user-box {
position: absolute;
background: rgba(236, 72, 153, 0.15);
border: 2px solid rgba(236, 72, 153, 0.6);
border-radius: 8px;
padding: 8px 12px;
text-align: center;
min-width: 80px;
box-shadow: 0 0 15px rgba(236, 72, 153, 0.3);
z-index: 10;
}
.user-box.sender {
border-color: #ec4899;
box-shadow: 0 0 15px rgba(236, 72, 153, 0.5);
}
.user-box.receiver {
border-color: #10b981;
background: rgba(16, 185, 129, 0.15);
box-shadow: 0 0 15px rgba(16, 185, 129, 0.5);
}
.user-icon {
font-size: 1.2rem;
margin-bottom: 2px;
}
.user-label {
font-size: 0.65rem;
color: #ec4899;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.user-box.receiver .user-label {
color: #10b981;
}
.user-amount {
font-family: monospace;
font-size: 0.75rem;
color: #f0f6fc;
margin-top: 2px;
}
/* SVG Connections */
.connections-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
/* Legend */
.legend {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #30363d;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.65rem;
color: #8b949e;
}
.legend-line {
width: 24px;
height: 3px;
border-radius: 2px;
}
.legend-line.deposit {
background: #58a6ff;
}
.legend-line.warp {
background: #a371f7;
}
.legend-line.redeem {
background: #3fb950;
}
.legend-line.rebalance {
background: repeating-linear-gradient(
90deg,
#f97316,
#f97316 4px,
transparent 4px,
transparent 8px
);
}
.legend-line.swap {
background: linear-gradient(90deg, #58a6ff, #3fb950);
}
/* Presets */
.presets {
margin-bottom: 24px;
}
.presets-label {
font-size: 0.8rem;
color: #8b949e;
margin-bottom: 8px;
}
.preset-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.preset-btn {
background: #21262d;
color: #c9d1d9;
border: 1px solid #30363d;
padding: 6px 12px;
font-size: 0.8rem;
border-radius: 4px;
cursor: pointer;
}
.preset-btn:hover {
background: #30363d;
}
.preset-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.preset-btn:disabled:hover {
background: #21262d;
}
/* Actions Panel */
.actions-panel {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
margin-bottom: 24px;
}
@media (max-width: 1200px) {
.actions-panel {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 800px) {
.actions-panel {
grid-template-columns: 1fr;
}
}
.action-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 16px;
}
.action-title {
font-weight: 600;
color: #f0f6fc;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.action-title .icon {
width: 20px;
height: 20px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
}
.action-card.transfer .icon {
background: linear-gradient(135deg, #58a6ff, #a371f7, #3fb950);
}
.action-card.rebalance .icon {
background: #f97316;
}
.action-card.swap .icon {
background: linear-gradient(135deg, #58a6ff, #3fb950);
}
.action-form {
display: grid;
gap: 10px;
}
.form-row {
display: grid;
grid-template-columns: 80px 1fr;
align-items: center;
gap: 8px;
}
.form-row.amount {
grid-template-columns: 80px 1fr 100px;
}
label {
font-size: 0.8rem;
color: #8b949e;
}
select,
input {
background: #0d1117;
border: 1px solid #30363d;
border-radius: 4px;
padding: 8px 12px;
color: #c9d1d9;
font-size: 0.85rem;
}
select:focus,
input:focus {
outline: none;
border-color: #58a6ff;
}
button {
background: #238636;
color: #fff;
border: none;
border-radius: 4px;
padding: 10px 16px;
font-weight: 500;
cursor: pointer;
}
button:hover {
background: #2ea043;
}
.fee-indicator {
font-size: 0.7rem;
color: #8b949e;
margin-top: 4px;
line-height: 1.4;
}
.flow-description {
font-size: 0.7rem;
color: #58a6ff;
background: rgba(88, 166, 255, 0.1);
padding: 8px;
border-radius: 4px;
margin-top: 8px;
line-height: 1.4;
}
.flow-description.rebalance {
color: #f97316;
background: rgba(249, 115, 22, 0.1);
}
/* Transaction Log */
.log-panel {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 16px;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.log-title {
font-weight: 600;
color: #f0f6fc;
}
.log-entries {
max-height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 0.75rem;
}
.log-entry {
padding: 6px 8px;
border-radius: 4px;
margin-bottom: 4px;
background: #0d1117;
}
.log-entry.success {
border-left: 3px solid #3fb950;
}
.log-entry.error {
border-left: 3px solid #f85149;
}
.log-entry.info {
border-left: 3px solid #58a6ff;
}
.log-entry.rebalance {
border-left: 3px solid #f97316;
}
.log-timestamp {
color: #8b949e;
margin-right: 8px;
}
.log-message {
color: #c9d1d9;
}
.log-details {
color: #8b949e;
font-size: 0.7rem;
margin-top: 2px;
}
/* Invariant Warning */
.invariant-warning {
background: #f8514922;
border: 1px solid #f85149;
border-radius: 8px;
padding: 12px;
margin-bottom: 24px;
display: none;
}
.invariant-warning.show {
display: block;
}
/* Arrow animations */
@keyframes flowForward {
from {
stroke-dashoffset: 16;
}
to {
stroke-dashoffset: 0;
}
}
.animated-path {
animation: flowForward 0.4s linear infinite;
}
</style>
</head>
<body>
<div class="container">
<h1>StableSwap Collateral Flow Simulator</h1>
<p class="subtitle">
Option A: Per-Adapter Architecture — Triangle topology with separated
sUSD route
</p>
<!-- Presets -->
<div class="presets">
<div class="presets-label">Load Scenario:</div>
<div class="preset-buttons">
<button
class="preset-btn"
style="background: #238636; border-color: #238636"
onclick="loadLiveData()"
id="liveDataBtn"
>
Load Live Data
</button>
<button class="preset-btn" onclick="loadPreset('balanced')">
Balanced Start
</button>
<button class="preset-btn" onclick="loadPreset('imbalanced')">
Imbalanced (Arb depleted)
</button>
<button class="preset-btn" onclick="loadPreset('appchain')">
Soneium Bootstrap
</button>
<button class="preset-btn" onclick="resetState()">Reset</button>
</div>
<div
id="liveDataStatus"
style="font-size: 0.75rem; color: #8b949e; margin-top: 8px"
></div>
</div>
<!-- Invariant Warning -->
<div class="invariant-warning" id="invariantWarning">
⚠️ <strong>Invariant Violated:</strong> Total Collateral ≠ Total sUSD
Supply.
</div>
<!-- Global Stats -->
<div class="global-stats">
<div class="stat">
<span class="stat-label">Total sUSD Supply</span>
<span class="stat-value" id="totalSUSD">0</span>
</div>
<div class="stat">
<span class="stat-label">Total Collateral</span>
<span class="stat-value" id="totalCollateral">0</span>
</div>
<div class="stat">
<span class="stat-label">Invariant</span>
<span class="stat-value success" id="invariantCheck">✓ Valid</span>
</div>
<div class="stat">
<span class="stat-label">Fees Collected</span>
<span class="stat-value" id="totalFees">0</span>
</div>
</div>
<!-- Architecture Diagram -->
<div class="architecture">
<div class="arch-title">
StableSwap Architecture — Collateral Adapters + sUSD Warp Route
</div>
<div class="triangle-container" id="triangleContainer">
<svg class="connections-svg" id="connectionsSvg"></svg>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-line deposit"></div>
<span>deposit() - adapter→sUSD</span>
</div>
<div class="legend-item">
<div class="legend-line warp"></div>
<span>transferRemote() - sUSD cross-chain</span>
</div>
<div class="legend-item">
<div class="legend-line redeem"></div>
<span>redeem() - sUSD→adapter</span>
</div>
<div class="legend-item">
<div class="legend-line rebalance"></div>
<span>rebalance() - adapter→adapter</span>
</div>
<div class="legend-item">
<div class="legend-line swap"></div>
<span>swapLocal() - same-chain swap</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #ec4899"></div>
<span>user sends tokens</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #10b981"></div>
<span>user receives tokens</span>
</div>
</div>
</div>
<!-- Actions Panel -->
<div class="actions-panel">
<!-- Cross-Chain Transfer -->
<div class="action-card transfer">
<div class="action-title">
<div class="icon">→</div>
Cross-Chain Transfer
</div>
<div class="action-form">
<div class="form-row">
<label>From:</label>
<select
id="transferFromChain"
onchange="updateTransferTokens(); updateFlowDescription();"
></select>
</div>
<div class="form-row">
<label>Input:</label>
<select
id="transferInputToken"
onchange="updateFlowDescription();"
></select>
</div>
<div class="form-row">
<label>To:</label>
<select
id="transferToChain"
onchange="updateTransferOutputTokens(); updateFlowDescription();"
></select>
</div>
<div class="form-row">
<label>Output:</label>
<select
id="transferOutputToken"
onchange="updateFlowDescription();"
></select>
</div>
<div class="form-row amount">
<label>Amount:</label>
<input
type="number"
id="transferAmount"
value="100"
min="0"
step="1"
/>
<button onclick="executeTransfer()">Transfer</button>
</div>
<div class="flow-description" id="transferFlowDesc"></div>
<div class="fee-indicator">
Fee: 5bps on deposit. Collateral increases on origin, decreases on
destination.
</div>
</div>
</div>
<!-- Rebalance -->
<div class="action-card rebalance">
<div class="action-title">
<div class="icon">⇄</div>
Rebalance Collateral
</div>
<div class="action-form">
<div class="form-row">
<label>From:</label>
<select
id="rebalanceFromChain"
onchange="updateRebalanceTokens(); updateRebalanceDesc();"
></select>
</div>
<div class="form-row">
<label>Token:</label>
<select
id="rebalanceToken"
onchange="updateRebalanceDesc();"
></select>
</div>
<div class="form-row">
<label>To:</label>
<select
id="rebalanceToChain"
onchange="updateRebalanceTokens(); updateRebalanceDesc();"
></select>
</div>
<div class="form-row amount">
<label>Amount:</label>
<input
type="number"
id="rebalanceAmount"
value="100"
min="0"
step="1"
/>
<button onclick="executeRebalance()">Rebalance</button>
</div>
<div
class="flow-description rebalance"
id="rebalanceFlowDesc"
></div>
<div class="fee-indicator">
Uses underlying warp route. sUSD supply unchanged. Same-token
only.
</div>
</div>
</div>
<!-- Same-Chain Swap -->
<div class="action-card swap">
<div class="action-title">
<div class="icon">↔</div>
Same-Chain Swap
</div>
<div class="action-form">
<div class="form-row">
<label>Chain:</label>
<select
id="swapChain"
onchange="updateSwapTokens(); updateSwapDesc();"
></select>
</div>
<div class="form-row">
<label>From:</label>
<select id="swapFromToken" onchange="updateSwapDesc();"></select>
</div>
<div class="form-row">
<label>To:</label>
<select id="swapToToken" onchange="updateSwapDesc();"></select>
</div>
<div class="form-row amount">
<label>Amount:</label>
<input
type="number"
id="swapAmount"
value="100"
min="0"
step="1"
/>
<button onclick="executeSwap()">Swap</button>
</div>
<div
class="flow-description"
style="background: rgba(63, 185, 80, 0.1); color: #3fb950"
id="swapFlowDesc"
></div>
<div class="fee-indicator">
Fee: 5bps on deposit. No cross-chain messaging. sUSD supply
unchanged.
</div>
</div>
</div>
</div>
<!-- Transaction Log -->
<div class="log-panel">
<div class="log-header">
<span class="log-title">Transaction Log</span>
<button
style="background: #30363d; padding: 6px 12px; font-size: 0.75rem"
onclick="clearLog()"
>
Clear
</button>
</div>
<div class="log-entries" id="logEntries"></div>
</div>
</div>
<script>
// ============================================
// CONFIG & STATE
// ============================================
const FEE_BPS = 5;
// RPC endpoints (public)
const RPC_ENDPOINTS = {
arbitrum: 'https://arb1.arbitrum.io/rpc',
base: 'https://mainnet.base.org',
soneium: 'https://rpc.soneium.org',
};
// Deployed contract addresses from warp UI config
const DEPLOYED_CONTRACTS = {
arbitrum: {
USDC: {
adapter: '0x94C62e7958738B65737a0Db8A5077def3AED84AA',
collateral: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
},
USDT: {
adapter: '0x6BEC839292A36372882Cb850E93FB5aC2A9BA4Af',
collateral: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
},
sUSD: '0x38E8720EBE02e7c5254F9De9F81440C7a770a9c6',
helper: '0x880024136413E048427F299FfA85cBbFFc1be2bF',
},
base: {
USDC: {
adapter: '0x73Ef899fDa87213e26501707ab585028BFB297c8',
collateral: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
},
USDT: {
adapter: '0x23c51024b19303F1315DbFFA055666aE9B7A0B2c',
collateral: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2',
},
sUSD: '0x38E8720EBE02e7c5254F9De9F81440C7a770a9c6',
helper: '0xD5d718EE1466A7a7f7e4C3B3bA54636707a919af',
},
soneium: {
USDSC: {
adapter: '0x867D428B8FbE196EA4e997e7980623E75ED219a7',
collateral: '0x3f99231dD03a9F0E7e3421c92B7b90fbe012985a',
},
sUSD: '0x38E8720EBE02e7c5254F9De9F81440C7a770a9c6',
helper: '0x73Ef899fDa87213e26501707ab585028BFB297c8',
},
};
// ERC20 balanceOf(address) selector
const BALANCE_OF_SELECTOR = '0x70a08231';
// ERC20 totalSupply() selector
const TOTAL_SUPPLY_SELECTOR = '0x18160ddd';
const CHAIN_CONFIG = {
base: {
name: 'Base',
domain: 8453,
tokens: ['USDC', 'USDT'],
color: '#0052FF',
position: 'bottom-left',
},
arbitrum: {
name: 'Arbitrum',
domain: 42161,
tokens: ['USDC', 'USDT'],
color: '#28A0F0',
position: 'bottom-right',
},
soneium: {
name: 'Soneium',
domain: 1868,
tokens: ['USDSC'],
color: '#9945FF',
position: 'top',
},
};
const CHAIN_ORDER = ['base', 'arbitrum', 'soneium'];
// ============================================
// LIVE DATA FETCHING
// ============================================
async function ethCall(rpcUrl, to, data) {
const response = await fetch(rpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_call',
params: [{ to, data }, 'latest'],
id: 1,
}),
});
const json = await response.json();
if (json.error) throw new Error(json.error.message);
return json.result;
}
function encodeBalanceOf(address) {
// balanceOf(address) - pad address to 32 bytes
const paddedAddress = address
.toLowerCase()
.replace('0x', '')
.padStart(64, '0');
return BALANCE_OF_SELECTOR + paddedAddress;
}
function decodeUint256(hex) {
// Convert hex to BigInt, then to number (safe for balances < 2^53)
if (!hex || hex === '0x') return 0;
return Number(BigInt(hex));
}
async function fetchCollateralBalance(chainId, token) {
const contracts = DEPLOYED_CONTRACTS[chainId];
if (!contracts || !contracts[token]) return 0;
const rpcUrl = RPC_ENDPOINTS[chainId];
const adapterAddress = contracts[token].adapter;
const collateralAddress = contracts[token].collateral;
try {
const data = encodeBalanceOf(adapterAddress);
const result = await ethCall(rpcUrl, collateralAddress, data);
// All stablecoins are 6 decimals
return decodeUint256(result) / 1e6;
} catch (err) {
console.error(`Error fetching ${token} balance on ${chainId}:`, err);
return 0;
}
}
async function fetchSUSDSupply(chainId) {
const contracts = DEPLOYED_CONTRACTS[chainId];
if (!contracts || !contracts.sUSD) return 0;
const rpcUrl = RPC_ENDPOINTS[chainId];
const sUSDAddress = contracts.sUSD;
try {
const result = await ethCall(
rpcUrl,
sUSDAddress,
TOTAL_SUPPLY_SELECTOR,
);
// sUSD is 6 decimals (same as underlying stablecoins)
return decodeUint256(result) / 1e6;
} catch (err) {
console.error(`Error fetching sUSD supply on ${chainId}:`, err);
return 0;
}
}
async function loadLiveData() {
const btn = document.getElementById('liveDataBtn');
const status = document.getElementById('liveDataStatus');
btn.disabled = true;
btn.textContent = 'Loading...';
status.textContent =
'Fetching live data from Arbitrum, Base, and Soneium...';
status.style.color = '#58a6ff';
initState();
try {
// Fetch all data in parallel
const fetchPromises = [];
for (const chainId of CHAIN_ORDER) {
const config = CHAIN_CONFIG[chainId];
for (const token of config.tokens) {
fetchPromises.push(
fetchCollateralBalance(chainId, token).then((balance) => ({
chainId,
token,
balance,
})),
);
}
// Fetch sUSD supply for this chain
fetchPromises.push(
fetchSUSDSupply(chainId).then((supply) => ({
chainId,
type: 'sUSD',
supply,
})),
);
}
const results = await Promise.all(fetchPromises);
// Process results
for (const result of results) {
if (result.type === 'sUSD') {
state.chains[result.chainId].sUSDSupply = result.supply;
} else {
state.chains[result.chainId].adapters[result.token].collateral =
result.balance;
}
}
// Calculate totals for status message
let totalCollateral = 0;
let totalSUSD = 0;
for (const chain of Object.values(state.chains)) {
totalSUSD += chain.sUSDSupply;
for (const adapter of Object.values(chain.adapters)) {
totalCollateral += adapter.collateral;
}
}
const timestamp = new Date().toLocaleTimeString();
status.innerHTML = `<span style="color: #3fb950;">✓ Live data loaded at ${timestamp}</span> — Total Collateral: ${totalCollateral.toFixed(2)} | Total sUSD: ${totalSUSD.toFixed(2)}`;
addLogEntry(
'info',
'Loaded live data from mainnet',
`Arbitrum, Base, Soneium at ${timestamp}`,
);
render();
} catch (err) {
console.error('Error loading live data:', err);
status.innerHTML = `<span style="color: #f85149;">✗ Error: ${err.message}</span>`;
addLogEntry('error', 'Failed to load live data', err.message);
} finally {
btn.disabled = false;
btn.textContent = 'Load Live Data';
}
}
// All possible rebalance routes (same token pairs across chains)
const REBALANCE_ROUTES = [
{ from: 'base', to: 'arbitrum', token: 'USDC' },
{ from: 'base', to: 'arbitrum', token: 'USDT' },
{ from: 'arbitrum', to: 'base', token: 'USDC' },
{ from: 'arbitrum', to: 'base', token: 'USDT' },
];
let state = {
chains: {},
accumulatedFees: {},
log: [],
activeFlow: null, // Only set when Transfer/Rebalance is clicked
};
function initState() {
state = { chains: {}, accumulatedFees: {}, log: [], activeFlow: null };
for (const [chainId, config] of Object.entries(CHAIN_CONFIG)) {
state.chains[chainId] = {
name: config.name,
domain: config.domain,
color: config.color,
position: config.position,
adapters: {},
sUSDSupply: 0,
fees: {},
};
for (const token of config.tokens) {
state.chains[chainId].adapters[token] = { collateral: 0 };
state.chains[chainId].fees[token] = 0;
state.accumulatedFees[token] = state.accumulatedFees[token] || 0;
}
}
}
// ============================================
// PRESETS
// ============================================
function loadPreset(preset) {
initState();
switch (preset) {
case 'balanced':
state.chains.base.adapters.USDC.collateral = 1000;
state.chains.base.adapters.USDT.collateral = 800;
state.chains.base.sUSDSupply = 1800;
state.chains.arbitrum.adapters.USDC.collateral = 1000;
state.chains.arbitrum.adapters.USDT.collateral = 800;
state.chains.arbitrum.sUSDSupply = 1800;
state.chains.soneium.adapters.USDSC.collateral = 400;
state.chains.soneium.sUSDSupply = 400;
addLogEntry('info', 'Loaded balanced scenario');
break;
case 'imbalanced':
state.chains.base.adapters.USDC.collateral = 2000;
state.chains.base.adapters.USDT.collateral = 1500;
state.chains.base.sUSDSupply = 3500;
state.chains.arbitrum.adapters.USDC.collateral = 50;
state.chains.arbitrum.adapters.USDT.collateral = 50;
state.chains.arbitrum.sUSDSupply = 100;
state.chains.soneium.adapters.USDSC.collateral = 400;
state.chains.soneium.sUSDSupply = 400;
addLogEntry(
'info',
'Loaded imbalanced scenario',
'Arbitrum depleted. Transfer will fail, then rebalance.',
);
break;
case 'appchain':
state.chains.base.adapters.USDC.collateral = 1500;
state.chains.base.adapters.USDT.collateral = 1000;
state.chains.base.sUSDSupply = 2500;
state.chains.arbitrum.adapters.USDC.collateral = 1000;
state.chains.arbitrum.adapters.USDT.collateral = 500;
state.chains.arbitrum.sUSDSupply = 1500;
state.chains.soneium.adapters.USDSC.collateral = 0;
state.chains.soneium.sUSDSupply = 0;
addLogEntry(
'info',
'Loaded Soneium bootstrap',
'Soneium has no collateral yet.',
);
break;
}
render();
}
function resetState() {
initState();
addLogEntry('info', 'State reset');
render();
}
// ============================================
// OPERATIONS
// ============================================
function executeTransfer() {
const fromChain = document.getElementById('transferFromChain').value;
const toChain = document.getElementById('transferToChain').value;
const inputToken = document.getElementById('transferInputToken').value;
const outputToken = document.getElementById(
'transferOutputToken',
).value;
const amount = parseFloat(
document.getElementById('transferAmount').value,
);
if (!amount || amount <= 0) {
addLogEntry('error', 'Invalid amount');
return;
}
const fee = (amount * FEE_BPS) / 10000;
const amountAfterFee = amount - fee;
const destAdapter = state.chains[toChain]?.adapters[outputToken];
if (!destAdapter) {
addLogEntry(
'error',
`${outputToken} not available on ${state.chains[toChain].name}`,
);
return;
}
if (destAdapter.collateral < amountAfterFee) {
addLogEntry(
'error',
`Transfer FAILED: Insufficient ${outputToken} on ${state.chains[toChain].name}`,
`Need ${amountAfterFee.toFixed(2)}, have ${destAdapter.collateral.toFixed(2)}`,
);
state.activeFlow = {
type: 'transfer',
from: fromChain,
to: toChain,
inputToken,
outputToken,
amount,
amountAfterFee,
fee,
failed: true,
};
render();
return;
}
// Execute
state.chains[fromChain].adapters[inputToken].collateral +=
amountAfterFee;
state.chains[fromChain].fees[inputToken] += fee;
state.accumulatedFees[inputToken] += fee;
state.chains[toChain].adapters[outputToken].collateral -=
amountAfterFee;
state.activeFlow = {
type: 'transfer',
from: fromChain,
to: toChain,
inputToken,
outputToken,
amount,
amountAfterFee,
fee,
failed: false,
};
addLogEntry(
'success',
`Transfer: ${amount} ${inputToken} (${state.chains[fromChain].name}) → ${amountAfterFee.toFixed(2)} ${outputToken} (${state.chains[toChain].name})`,
`Fee: ${fee.toFixed(4)} ${inputToken}`,
);
render();
}
function executeRebalance() {
const fromChain = document.getElementById('rebalanceFromChain').value;
const toChain = document.getElementById('rebalanceToChain').value;
const token = document.getElementById('rebalanceToken').value;
const amount = parseFloat(
document.getElementById('rebalanceAmount').value,
);
if (!amount || amount <= 0) {
addLogEntry('error', 'Invalid amount');
return;
}
if (!token) {
addLogEntry('error', 'No common token between chains');
return;
}
const sourceAdapter = state.chains[fromChain]?.adapters[token];
if (!sourceAdapter || sourceAdapter.collateral < amount) {
addLogEntry(
'error',
`Rebalance FAILED: Insufficient ${token} on ${state.chains[fromChain].name}`,
`Need ${amount}, have ${(sourceAdapter?.collateral || 0).toFixed(2)}`,
);
return;
}
// Execute
state.chains[fromChain].adapters[token].collateral -= amount;
state.chains[toChain].adapters[token].collateral += amount;
state.activeFlow = {
type: 'rebalance',
from: fromChain,
to: toChain,
token,
amount,
};
addLogEntry(
'rebalance',
`Rebalance: ${amount} ${token} from ${state.chains[fromChain].name} → ${state.chains[toChain].name}`,
`Via underlying ${token} warp route`,
);
render();
}
function executeSwap() {
const chain = document.getElementById('swapChain').value;
const fromToken = document.getElementById('swapFromToken').value;
const toToken = document.getElementById('swapToToken').value;
const amount = parseFloat(document.getElementById('swapAmount').value);
if (!amount || amount <= 0) {
addLogEntry('error', 'Invalid amount');
return;
}
if (fromToken === toToken) {
addLogEntry('error', 'Cannot swap same token');
return;
}
const fee = (amount * FEE_BPS) / 10000;
const amountAfterFee = amount - fee;
const destAdapter = state.chains[chain]?.adapters[toToken];
if (!destAdapter) {
addLogEntry(
'error',
`${toToken} not available on ${state.chains[chain].name}`,
);
return;
}
if (destAdapter.collateral < amountAfterFee) {
addLogEntry(
'error',
`Swap FAILED: Insufficient ${toToken} on ${state.chains[chain].name}`,
`Need ${amountAfterFee.toFixed(2)}, have ${destAdapter.collateral.toFixed(2)}`,
);
state.activeFlow = {
type: 'swap',
chain,
fromToken,
toToken,
amount,
amountAfterFee,
fee,
failed: true,
};
render();
return;
}
// Execute: deposit fromToken, redeem toToken (same chain, no sUSD movement)
state.chains[chain].adapters[fromToken].collateral += amountAfterFee;
state.chains[chain].fees[fromToken] += fee;
state.accumulatedFees[fromToken] += fee;
state.chains[chain].adapters[toToken].collateral -= amountAfterFee;
state.activeFlow = {
type: 'swap',
chain,
fromToken,
toToken,
amount,
amountAfterFee,
fee,
failed: false,
};
addLogEntry(
'success',
`Swap: ${amount} ${fromToken} → ${amountAfterFee.toFixed(2)} ${toToken} on ${state.chains[chain].name}`,
`Fee: ${fee.toFixed(4)} ${fromToken}. No cross-chain messaging.`,
);
render();
}
// ============================================
// RENDERING
// ============================================
function render() {
renderDiagram();
renderConnections();
renderGlobalStats();
renderDropdowns();
updateFlowDescription();
updateRebalanceDesc();
updateSwapDesc();
}
function renderDiagram() {
const container = document.getElementById('triangleContainer');
const svg = document.getElementById('connectionsSvg');
// Clear everything except SVG
Array.from(container.children).forEach((child) => {
if (child !== svg) container.removeChild(child);
});
const flow = state.activeFlow;
for (const chainId of CHAIN_ORDER) {
const chain = state.chains[chainId];
const col = document.createElement('div');
col.className = `chain-column ${chain.position}`;
col.id = `chain-col-${chainId}`;
// Determine highlights
const isTransferFrom =
flow?.type === 'transfer' && flow.from === chainId;
const isTransferTo = flow?.type === 'transfer' && flow.to === chainId;
const isRebalanceFrom =
flow?.type === 'rebalance' && flow.from === chainId;
const isRebalanceTo =
flow?.type === 'rebalance' && flow.to === chainId;
col.innerHTML = `
<div class="chain-header" style="border-color: ${chain.color}">
<div class="chain-name" style="color: ${chain.color}">${chain.name}</div>
<div class="chain-domain">Domain: ${chain.domain}</div>
</div>
<div class="chain-body">
<div class="adapters-container">
<div class="adapters-label">Collateral Adapters</div>
${Object.entries(chain.adapters)
.map(([token, adapter]) => {
const fee = chain.fees[token] || 0;
const healthClass = getHealthClass(
adapter.collateral,
chain.sUSDSupply,
);
let highlightClass = '';
if (flow?.type === 'transfer' && !flow.failed) {
if (flow.from === chainId && flow.inputToken === token)
highlightClass = 'highlight-deposit';
if (flow.to === chainId && flow.outputToken === token)
highlightClass = 'highlight-redeem';
}
if (flow?.type === 'rebalance' && flow.token === token) {
if (flow.from === chainId || flow.to === chainId)
highlightClass = 'highlight-rebalance';
}
if (
flow?.type === 'swap' &&
!flow.failed &&
flow.chain === chainId
) {
if (flow.fromToken === token)
highlightClass = 'highlight-deposit';
if (flow.toToken === token)
highlightClass = 'highlight-redeem';
}
return `
<div class="adapter-box ${highlightClass}" id="adapter-${chainId}-${token}">
<div class="adapter-header">
<span class="adapter-token">${token}</span>
<span class="adapter-label">CollateralAdapter</span>
</div>
<div class="adapter-stats">
<div class="adapter-stat">
<span class="adapter-stat-label">Collateral:</span>
<span class="adapter-stat-value ${healthClass}">${adapter.collateral.toFixed(2)}</span>
</div>
${
fee > 0
? `
<div class="adapter-stat">
<span class="adapter-stat-label">Fees:</span>
<span class="adapter-stat-value fee">${fee.toFixed(4)}</span>
</div>
`
: ''
}
</div>
</div>
`;
})
.join('')}
</div>
<div class="susd-route-container">
<div class="susd-route-label">sUSD Warp Route</div>
<div class="susd-box ${(flow?.type === 'transfer' && !flow.failed && (isTransferFrom || isTransferTo)) || (flow?.type === 'swap' && !flow.failed && flow.chain === chainId) ? 'highlight' : ''}"
id="susd-${chainId}">
<div class="susd-header">
<span class="susd-token">sUSD</span>
<span class="susd-label">HypERC20WithMinters</span>
</div>
<div class="susd-supply">Supply: ${chain.sUSDSupply.toFixed(2)}</div>
</div>
</div>
</div>
`;
container.appendChild(col);
}
}
function renderConnections() {
const svg = document.getElementById('connectionsSvg');
const container = document.getElementById('triangleContainer');
requestAnimationFrame(() => {
const containerRect = container.getBoundingClientRect();
svg.setAttribute('width', containerRect.width);
svg.setAttribute('height', containerRect.height);
svg.setAttribute(
'viewBox',
`0 0 ${containerRect.width} ${containerRect.height}`,
);
let paths = '';
// Arrow markers
paths += `
<defs>
<marker id="arrowGrey" markerWidth="6" markerHeight="5" refX="5" refY="2.5" orient="auto">
<polygon points="0 0, 6 2.5, 0 5" fill="#484f58"/>
</marker>
<marker id="arrowBlue" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#58a6ff"/>
</marker>
<marker id="arrowPurple" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#a371f7"/>
</marker>
<marker id="arrowGreen" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#3fb950"/>
</marker>
<marker id="arrowOrange" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#f97316"/>
</marker>
<marker id="arrowPink" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#ec4899"/>
</marker>
<marker id="arrowTeal" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#10b981"/>
</marker>
</defs>
`;
// Helper functions
const getElementRect = (id) => {
const el = document.getElementById(id);
if (!el) return null;
const rect = el.getBoundingClientRect();
return {
x: rect.left - containerRect.left + rect.width / 2,
y: rect.top - containerRect.top + rect.height / 2,
top: rect.top - containerRect.top,
bottom: rect.bottom - containerRect.top,
left: rect.left - containerRect.left,
right: rect.right - containerRect.left,
width: rect.width,
height: rect.height,
};
};
const flow = state.activeFlow;
// ========================================
// 1. Draw deposit/redeem edges: adapter ↔ sUSD (within each chain)
// ========================================
for (const chainId of CHAIN_ORDER) {
const chain = state.chains[chainId];
const susd = getElementRect(`susd-${chainId}`);
for (const token of Object.keys(chain.adapters)) {
const adapter = getElementRect(`adapter-${chainId}-${token}`);
if (!adapter || !susd) continue;
// Is this edge part of active flow?
const isActiveDeposit =
(flow?.type === 'transfer' &&
!flow.failed &&
flow.from === chainId &&
flow.inputToken === token) ||
(flow?.type === 'swap' &&
!flow.failed &&
flow.chain === chainId &&
flow.fromToken === token);
const isActiveRedeem =
(flow?.type === 'transfer' &&
!flow.failed &&
flow.to === chainId &&
flow.outputToken === token) ||
(flow?.type === 'swap' &&
!flow.failed &&
flow.chain === chainId &&
flow.toToken === token);
// Deposit edge: adapter → sUSD (on the left side)
const depStart = { x: adapter.left + 15, y: adapter.bottom };
const depEnd = { x: susd.left + 15, y: susd.top };
const depCtrlX = depStart.x - 25;
const depCtrlY = (depStart.y + depEnd.y) / 2;
if (isActiveDeposit) {
paths += `<path d="M ${depStart.x} ${depStart.y} Q ${depCtrlX} ${depCtrlY} ${depEnd.x} ${depEnd.y}"
fill="none" stroke="#58a6ff" stroke-width="2.5" class="animated-path"
marker-end="url(#arrowBlue)"/>`;
paths += createEdgeLabel(
depCtrlX - 25,
depCtrlY,
'deposit',
`+${flow.amountAfterFee.toFixed(1)}`,
'#58a6ff',
false,
);
} else {
paths += `<path d="M ${depStart.x} ${depStart.y} Q ${depCtrlX} ${depCtrlY} ${depEnd.x} ${depEnd.y}"
fill="none" stroke="#484f58" stroke-width="1.5" opacity="0.4"
marker-end="url(#arrowGrey)"/>`;
paths += createEdgeLabel(
depCtrlX - 20,
depCtrlY,
'deposit',
token,
'#484f58',
true,
);
}
// Redeem edge: sUSD → adapter (on the right side)
const redStart = { x: susd.right - 15, y: susd.top };
const redEnd = { x: adapter.right - 15, y: adapter.bottom };
const redCtrlX = redEnd.x + 25;
const redCtrlY = (redStart.y + redEnd.y) / 2;
if (isActiveRedeem) {
paths += `<path d="M ${redStart.x} ${redStart.y} Q ${redCtrlX} ${redCtrlY} ${redEnd.x} ${redEnd.y}"
fill="none" stroke="#3fb950" stroke-width="2.5" class="animated-path"
marker-end="url(#arrowGreen)"/>`;
paths += createEdgeLabel(
redCtrlX + 25,
redCtrlY,
'redeem',
`-${flow.amountAfterFee.toFixed(1)}`,
'#3fb950',
false,
);
} else {
paths += `<path d="M ${redStart.x} ${redStart.y} Q ${redCtrlX} ${redCtrlY} ${redEnd.x} ${redEnd.y}"
fill="none" stroke="#484f58" stroke-width="1.5" opacity="0.4"
marker-end="url(#arrowGrey)"/>`;
paths += createEdgeLabel(
redCtrlX + 20,
redCtrlY,
'redeem',
token,
'#484f58',
true,
);
}
}
}
// ========================================
// 2. Draw sUSD transferRemote edges (between sUSD boxes)
// ========================================
const chainPairs = [
['base', 'arbitrum'],
['base', 'soneium'],
['arbitrum', 'soneium'],
];
for (const [c1, c2] of chainPairs) {
const susd1 = getElementRect(`susd-${c1}`);
const susd2 = getElementRect(`susd-${c2}`);
if (!susd1 || !susd2) continue;
const midX = (susd1.x + susd2.x) / 2;
const midY = (susd1.y + susd2.y) / 2;
const dx = susd2.x - susd1.x;
const dy = susd2.y - susd1.y;
const len = Math.sqrt(dx * dx + dy * dy);
const offset = 50;
const perpX = (-dy / len) * offset;
const perpY = (dx / len) * offset;
// Direction 1: c1 → c2
const isActive1 =
flow?.type === 'transfer' &&
!flow.failed &&
flow.from === c1 &&
flow.to === c2;
const ctrlX1 = midX + perpX;
const ctrlY1 = midY + perpY;
if (isActive1) {
paths += `<path d="M ${susd1.x} ${susd1.y} Q ${ctrlX1} ${ctrlY1} ${susd2.x} ${susd2.y}"
fill="none" stroke="#a371f7" stroke-width="2.5" stroke-dasharray="6,3" class="animated-path"
marker-end="url(#arrowPurple)"/>`;
paths += createEdgeLabel(
ctrlX1,
ctrlY1 - 12,
'transferRemote',
`${flow.amountAfterFee.toFixed(1)} sUSD`,
'#a371f7',
false,
);
} else {
paths += `<path d="M ${susd1.x} ${susd1.y} Q ${ctrlX1} ${ctrlY1} ${susd2.x} ${susd2.y}"
fill="none" stroke="#484f58" stroke-width="1.5" opacity="0.35"
marker-end="url(#arrowGrey)"/>`;
paths += createEdgeLabel(
ctrlX1,
ctrlY1 - 8,
'transferRemote',
`${state.chains[c1].name}→${state.chains[c2].name}`,
'#484f58',
true,
);
}
// Direction 2: c2 → c1
const isActive2 =
flow?.type === 'transfer' &&
!flow.failed &&
flow.from === c2 &&
flow.to === c1;
const ctrlX2 = midX - perpX;
const ctrlY2 = midY - perpY;
if (isActive2) {
paths += `<path d="M ${susd2.x} ${susd2.y} Q ${ctrlX2} ${ctrlY2} ${susd1.x} ${susd1.y}"
fill="none" stroke="#a371f7" stroke-width="2.5" stroke-dasharray="6,3" class="animated-path"
marker-end="url(#arrowPurple)"/>`;
paths += createEdgeLabel(
ctrlX2,
ctrlY2 + 12,
'transferRemote',
`${flow.amountAfterFee.toFixed(1)} sUSD`,
'#a371f7',
false,
);
} else {
paths += `<path d="M ${susd2.x} ${susd2.y} Q ${ctrlX2} ${ctrlY2} ${susd1.x} ${susd1.y}"
fill="none" stroke="#484f58" stroke-width="1.5" opacity="0.35"
marker-end="url(#arrowGrey)"/>`;
paths += createEdgeLabel(
ctrlX2,
ctrlY2 + 8,
'transferRemote',
`${state.chains[c2].name}→${state.chains[c1].name}`,
'#484f58',
true,
);
}
}
// ========================================
// 3. Draw rebalance edges (adapter → adapter, same token)
// ========================================
for (const route of REBALANCE_ROUTES) {
const from = getElementRect(`adapter-${route.from}-${route.token}`);
const to = getElementRect(`adapter-${route.to}-${route.token}`);
if (!from || !to) continue;
const isActive =
flow?.type === 'rebalance' &&
flow.from === route.from &&
flow.to === route.to &&
flow.token === route.token;
const midX = (from.x + to.x) / 2;
const midY = (from.y + to.y) / 2 - 20;
if (isActive) {
paths += `<path d="M ${from.right} ${from.y} Q ${midX} ${midY} ${to.left} ${to.y}"
fill="none" stroke="#f97316" stroke-width="2.5" stroke-dasharray="6,3" class="animated-path"
marker-end="url(#arrowOrange)"/>`;
paths += createEdgeLabel(
midX,
midY - 10,
'rebalance',
`${flow.amount} ${route.token}`,
'#f97316',
false,
);
} else {
paths += `<path d="M ${from.right} ${from.y} Q ${midX} ${midY} ${to.left} ${to.y}"
fill="none" stroke="#484f58" stroke-width="1.5" stroke-dasharray="4,3" opacity="0.3"
marker-end="url(#arrowGrey)"/>`;
paths += createEdgeLabel(
midX,
midY - 6,
'rebalance',
route.token,
'#484f58',
true,
);
}
}
// ========================================
// 4. Draw user boxes and edges for transfers/swaps
// ========================================
if (flow?.type === 'transfer' && !flow.failed) {
const fromAdapter = getElementRect(
`adapter-${flow.from}-${flow.inputToken}`,
);
const toAdapter = getElementRect(
`adapter-${flow.to}-${flow.outputToken}`,
);
if (fromAdapter && toAdapter) {
// Sender user box (positioned to the left of origin adapter)
const senderX = fromAdapter.left - 100;
const senderY = fromAdapter.y - 30;
const senderBoxWidth = 85;
const senderBoxHeight = 55;
paths += `
<rect x="${senderX}" y="${senderY}" width="${senderBoxWidth}" height="${senderBoxHeight}"
rx="6" fill="rgba(236, 72, 153, 0.15)" stroke="#ec4899" stroke-width="2"/>
<text x="${senderX + senderBoxWidth / 2}" y="${senderY + 18}" fill="#ec4899" font-size="14" text-anchor="middle">👤</text>
<text x="${senderX + senderBoxWidth / 2}" y="${senderY + 32}" fill="#ec4899" font-size="8" text-anchor="middle" font-weight="bold">SENDER</text>
<text x="${senderX + senderBoxWidth / 2}" y="${senderY + 45}" fill="#f0f6fc" font-size="9" text-anchor="middle" font-family="monospace">-${flow.amount} ${flow.inputToken}</text>
`;
// Edge from sender to adapter
const senderEdgeStartX = senderX + senderBoxWidth;
const senderEdgeStartY = senderY + senderBoxHeight / 2;
const senderEdgeEndX = fromAdapter.left;
const senderEdgeEndY = fromAdapter.y;
paths += `<path d="M ${senderEdgeStartX} ${senderEdgeStartY} L ${senderEdgeEndX} ${senderEdgeEndY}"
fill="none" stroke="#ec4899" stroke-width="2.5" class="animated-path"
marker-end="url(#arrowPink)"/>`;
// Receiver user box (positioned to the right of destination adapter)
const receiverX = toAdapter.right + 15;
const receiverY = toAdapter.y - 30;
const receiverBoxWidth = 85;
const receiverBoxHeight = 55;
paths += `
<rect x="${receiverX}" y="${receiverY}" width="${receiverBoxWidth}" height="${receiverBoxHeight}"
rx="6" fill="rgba(16, 185, 129, 0.15)" stroke="#10b981" stroke-width="2"/>
<text x="${receiverX + receiverBoxWidth / 2}" y="${receiverY + 18}" fill="#10b981" font-size="14" text-anchor="middle">👤</text>
<text x="${receiverX + receiverBoxWidth / 2}" y="${receiverY + 32}" fill="#10b981" font-size="8" text-anchor="middle" font-weight="bold">RECEIVER</text>
<text x="${receiverX + receiverBoxWidth / 2}" y="${receiverY + 45}" fill="#f0f6fc" font-size="9" text-anchor="middle" font-family="monospace">+${flow.amountAfterFee.toFixed(2)} ${flow.outputToken}</text>
`;
// Edge from adapter to receiver
const receiverEdgeStartX = toAdapter.right;
const receiverEdgeStartY = toAdapter.y;
const receiverEdgeEndX = receiverX;
const receiverEdgeEndY = receiverY + receiverBoxHeight / 2;
paths += `<path d="M ${receiverEdgeStartX} ${receiverEdgeStartY} L ${receiverEdgeEndX} ${receiverEdgeEndY}"
fill="none" stroke="#10b981" stroke-width="2.5" class="animated-path"
marker-end="url(#arrowTeal)"/>`;
}
}
// User boxes for same-chain swap
if (flow?.type === 'swap' && !flow.failed) {
const fromAdapter = getElementRect(
`adapter-${flow.chain}-${flow.fromToken}`,
);
const toAdapter = getElementRect(
`adapter-${flow.chain}-${flow.toToken}`,
);
if (fromAdapter && toAdapter) {
// Sender user box (positioned to the left of input adapter)
const senderX = fromAdapter.left - 100;
const senderY = fromAdapter.y - 30;
const senderBoxWidth = 85;
const senderBoxHeight = 55;
paths += `
<rect x="${senderX}" y="${senderY}" width="${senderBoxWidth}" height="${senderBoxHeight}"
rx="6" fill="rgba(236, 72, 153, 0.15)" stroke="#ec4899" stroke-width="2"/>
<text x="${senderX + senderBoxWidth / 2}" y="${senderY + 18}" fill="#ec4899" font-size="14" text-anchor="middle">👤</text>
<text x="${senderX + senderBoxWidth / 2}" y="${senderY + 32}" fill="#ec4899" font-size="8" text-anchor="middle" font-weight="bold">USER IN</text>
<text x="${senderX + senderBoxWidth / 2}" y="${senderY + 45}" fill="#f0f6fc" font-size="9" text-anchor="middle" font-family="monospace">-${flow.amount} ${flow.fromToken}</text>
`;
// Edge from sender to adapter
const senderEdgeStartX = senderX + senderBoxWidth;
const senderEdgeStartY = senderY + senderBoxHeight / 2;
const senderEdgeEndX = fromAdapter.left;
const senderEdgeEndY = fromAdapter.y;
paths += `<path d="M ${senderEdgeStartX} ${senderEdgeStartY} L ${senderEdgeEndX} ${senderEdgeEndY}"
fill="none" stroke="#ec4899" stroke-width="2.5" class="animated-path"
marker-end="url(#arrowPink)"/>`;
// Receiver user box (positioned to the right of output adapter)
const receiverX = toAdapter.right + 15;
const receiverY = toAdapter.y - 30;
const receiverBoxWidth = 85;
const receiverBoxHeight = 55;
paths += `
<rect x="${receiverX}" y="${receiverY}" width="${receiverBoxWidth}" height="${receiverBoxHeight}"
rx="6" fill="rgba(16, 185, 129, 0.15)" stroke="#10b981" stroke-width="2"/>
<text x="${receiverX + receiverBoxWidth / 2}" y="${receiverY + 18}" fill="#10b981" font-size="14" text-anchor="middle">👤</text>
<text x="${receiverX + receiverBoxWidth / 2}" y="${receiverY + 32}" fill="#10b981" font-size="8" text-anchor="middle" font-weight="bold">USER OUT</text>
<text x="${receiverX + receiverBoxWidth / 2}" y="${receiverY + 45}" fill="#f0f6fc" font-size="9" text-anchor="middle" font-family="monospace">+${flow.amountAfterFee.toFixed(2)} ${flow.toToken}</text>
`;
// Edge from adapter to receiver
const receiverEdgeStartX = toAdapter.right;
const receiverEdgeStartY = toAdapter.y;
const receiverEdgeEndX = receiverX;
const receiverEdgeEndY = receiverY + receiverBoxHeight / 2;
paths += `<path d="M ${receiverEdgeStartX} ${receiverEdgeStartY} L ${receiverEdgeEndX} ${receiverEdgeEndY}"
fill="none" stroke="#10b981" stroke-width="2.5" class="animated-path"
marker-end="url(#arrowTeal)"/>`;
}
}
// ========================================
// 5. Show failure indicator
// ========================================
if (flow?.type === 'transfer' && flow.failed) {
const toAdapter = getElementRect(
`adapter-${flow.to}-${flow.outputToken}`,
);
if (toAdapter) {
paths += `<rect x="${toAdapter.x - 70}" y="${toAdapter.top - 30}" width="140" height="22" rx="4" fill="#f85149" opacity="0.9"/>`;
paths += `<text x="${toAdapter.x}" y="${toAdapter.top - 15}" fill="#fff" font-size="10" text-anchor="middle" font-weight="bold">✗ Insufficient collateral</text>`;
}
}
if (flow?.type === 'swap' && flow.failed) {
const toAdapter = getElementRect(
`adapter-${flow.chain}-${flow.toToken}`,
);
if (toAdapter) {
paths += `<rect x="${toAdapter.x - 70}" y="${toAdapter.top - 30}" width="140" height="22" rx="4" fill="#f85149" opacity="0.9"/>`;
paths += `<text x="${toAdapter.x}" y="${toAdapter.top - 15}" fill="#fff" font-size="10" text-anchor="middle" font-weight="bold">✗ Insufficient collateral</text>`;
}
}
svg.innerHTML = paths;
});
}
function createEdgeLabel(x, y, action, amount, color, small) {
const fontSize = small ? 6 : 7;
const amountSize = small ? 7 : 8;
const padding = small ? 2 : 3;
const actionWidth = action.length * (small ? 4 : 5);
const amountWidth = amount.length * (small ? 4.5 : 5.5);
const textWidth = Math.max(actionWidth, amountWidth) + padding * 2;
const textHeight = small ? 18 : 22;
const opacity = small ? 0.7 : 0.95;
return `
<rect x="${x - textWidth / 2}" y="${y - textHeight / 2}" width="${textWidth}" height="${textHeight}"
rx="3" fill="#0d1117" stroke="${color}" stroke-width="1" opacity="${opacity}"/>
<text x="${x}" y="${y - 2}" fill="${color}" font-size="${fontSize}" text-anchor="middle" font-weight="bold" font-family="monospace">${action}</text>
<text x="${x}" y="${y + (small ? 6 : 7)}" fill="${color}" font-size="${amountSize}" text-anchor="middle" font-family="monospace">${amount}</text>
`;
}
function getHealthClass(collateral, sUSDSupply) {
if (sUSDSupply === 0) return collateral > 0 ? 'healthy' : '';
const ratio = collateral / sUSDSupply;
if (ratio < 0.1) return 'depleted';
if (ratio < 0.3) return 'low';
return 'healthy';
}
function renderGlobalStats() {
let totalSUSD = 0,
totalCollateral = 0;
for (const chain of Object.values(state.chains)) {
totalSUSD += chain.sUSDSupply;
for (const adapter of Object.values(chain.adapters)) {
totalCollateral += adapter.collateral;
}
}
const totalFees = Object.values(state.accumulatedFees).reduce(
(a, b) => a + b,
0,
);
document.getElementById('totalSUSD').textContent = totalSUSD.toFixed(2);
document.getElementById('totalCollateral').textContent =
totalCollateral.toFixed(2);
document.getElementById('totalFees').textContent = totalFees.toFixed(4);
const invariantEl = document.getElementById('invariantCheck');
const warningEl = document.getElementById('invariantWarning');
const diff = Math.abs(totalSUSD - totalCollateral);
if (diff < 0.01) {
invariantEl.textContent = '✓ Valid';
invariantEl.className = 'stat-value success';
warningEl.classList.remove('show');
} else {
invariantEl.textContent = '✗ VIOLATED';
invariantEl.className = 'stat-value error';
warningEl.classList.add('show');
}
}
function renderDropdowns() {
const chains = Object.keys(state.chains);
[
'transferFromChain',
'transferToChain',
'rebalanceFromChain',
'rebalanceToChain',
'swapChain',
].forEach((id, i) => {
const select = document.getElementById(id);
const current = select.value;
select.innerHTML = chains
.map((c) => `<option value="${c}">${state.chains[c].name}</option>`)
.join('');
select.value = current || chains[i % 2];
});
if (
!document.getElementById('transferToChain').value ||
document.getElementById('transferToChain').value ===
document.getElementById('transferFromChain').value
) {
document.getElementById('transferToChain').value = chains[1];
}
if (
!document.getElementById('rebalanceToChain').value ||
document.getElementById('rebalanceToChain').value ===
document.getElementById('rebalanceFromChain').value
) {
document.getElementById('rebalanceToChain').value = chains[1];
}
updateTransferTokens();
updateTransferOutputTokens();
updateRebalanceTokens();
updateSwapTokens();
}
function updateTransferTokens() {
const chain = document.getElementById('transferFromChain').value;
const tokens = Object.keys(state.chains[chain].adapters);
document.getElementById('transferInputToken').innerHTML = tokens
.map((t) => `<option value="${t}">${t}</option>`)
.join('');
}
function updateTransferOutputTokens() {
const chain = document.getElementById('transferToChain').value;
const tokens = Object.keys(state.chains[chain].adapters);
document.getElementById('transferOutputToken').innerHTML = tokens
.map((t) => `<option value="${t}">${t}</option>`)
.join('');
}
function updateRebalanceTokens() {
const from = document.getElementById('rebalanceFromChain').value;
const to = document.getElementById('rebalanceToChain').value;
const fromTokens = new Set(Object.keys(state.chains[from].adapters));
const toTokens = new Set(Object.keys(state.chains[to].adapters));
const common = [...fromTokens].filter((t) => toTokens.has(t));
const select = document.getElementById('rebalanceToken');
select.innerHTML =
common.length > 0
? common.map((t) => `<option value="${t}">${t}</option>`).join('')
: '<option value="">No common tokens</option>';
}
function updateFlowDescription() {
const from = document.getElementById('transferFromChain').value;
const to = document.getElementById('transferToChain').value;
const input = document.getElementById('transferInputToken').value;
const output = document.getElementById('transferOutputToken').value;
document.getElementById('transferFlowDesc').innerHTML =
`<span style="color:#58a6ff">① deposit</span> ${input} →
<span style="color:#a371f7">② transferRemote</span> sUSD →
<span style="color:#3fb950">③ redeem</span> ${output}`;
}
function updateRebalanceDesc() {
const from = document.getElementById('rebalanceFromChain').value;
const to = document.getElementById('rebalanceToChain').value;
const token = document.getElementById('rebalanceToken').value;
if (!token) {
document.getElementById('rebalanceFlowDesc').innerHTML =
'<em>No common token to rebalance</em>';
return;
}
document.getElementById('rebalanceFlowDesc').innerHTML =
`${token} Adapter (${state.chains[from]?.name}) → <span style="color:#f97316">underlying ${token} warp</span> → ${token} Adapter (${state.chains[to]?.name})`;
}
function updateSwapTokens() {
const chain = document.getElementById('swapChain').value;
const tokens = Object.keys(state.chains[chain]?.adapters || {});
const fromSelect = document.getElementById('swapFromToken');
const toSelect = document.getElementById('swapToToken');
const currentFrom = fromSelect.value;
const currentTo = toSelect.value;
fromSelect.innerHTML = tokens
.map((t) => `<option value="${t}">${t}</option>`)
.join('');
toSelect.innerHTML = tokens
.map((t) => `<option value="${t}">${t}</option>`)
.join('');
// Try to keep current selections or pick different defaults
if (tokens.includes(currentFrom)) {
fromSelect.value = currentFrom;
}
if (tokens.includes(currentTo) && currentTo !== fromSelect.value) {
toSelect.value = currentTo;
} else if (tokens.length > 1) {
toSelect.value =
tokens.find((t) => t !== fromSelect.value) || tokens[1];
}
}
function updateSwapDesc() {
const chain = document.getElementById('swapChain').value;
const fromToken = document.getElementById('swapFromToken').value;
const toToken = document.getElementById('swapToToken').value;
const chainName = state.chains[chain]?.name || chain;
if (!fromToken || !toToken) {
document.getElementById('swapFlowDesc').innerHTML =
'<em>Select tokens to swap</em>';
return;
}
if (fromToken === toToken) {
document.getElementById('swapFlowDesc').innerHTML =
'<em style="color:#f85149">Cannot swap same token</em>';
return;
}
document.getElementById('swapFlowDesc').innerHTML =
`<span style="color:#58a6ff">① deposit</span> ${fromToken} → mint sUSD → <span style="color:#3fb950">② redeem</span> ${toToken} (on ${chainName})`;
}
// ============================================
// LOGGING
// ============================================
function addLogEntry(type, message, details) {
state.log.unshift({
type,
message,
details,
timestamp: new Date().toLocaleTimeString(),
});
renderLog();
}
function clearLog() {
state.log = [];
renderLog();
}
function renderLog() {
document.getElementById('logEntries').innerHTML = state.log
.map(
(e) => `
<div class="log-entry ${e.type}">
<span class="log-timestamp">${e.timestamp}</span>
<span class="log-message">${e.message}</span>
${e.details ? `<div class="log-details">${e.details}</div>` : ''}
</div>
`,
)
.join('');
}
// ============================================
// INIT
// ============================================
initState();
loadPreset('balanced');
window.addEventListener('resize', renderConnections);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment