feat(rwallet): rich landing UI, testnet toggle, and wallet type badges
Replaces the bare address-only empty state with a full dashboard landing: hero branding, supported chain chips, feature cards, clickable example wallets (TEC, Gitcoin, Vitalik), and animated loading spinner. Adds testnet toggle (off by default) that filters Sepolia/Base Sepolia from detection endpoints via ?testnets=true query param. Shows Safe Multisig / EOA Wallet badge when a wallet is loaded. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
be8982f160
commit
bfb3a588ed
|
|
@ -31,8 +31,22 @@ const CHAIN_COLORS: Record<string, string> = {
|
|||
"43114": "#e84142",
|
||||
"56": "#f3ba2f",
|
||||
"324": "#8c8dfc",
|
||||
"11155111": "#f59e0b",
|
||||
"84532": "#f59e0b",
|
||||
};
|
||||
|
||||
const CHAIN_NAMES: Record<string, string> = {
|
||||
"1": "Ethereum", "10": "Optimism", "100": "Gnosis", "137": "Polygon",
|
||||
"8453": "Base", "42161": "Arbitrum", "42220": "Celo", "43114": "Avalanche",
|
||||
"56": "BSC", "324": "zkSync",
|
||||
};
|
||||
|
||||
const EXAMPLE_WALLETS = [
|
||||
{ name: "TEC Commons Fund", address: "0x29567BdBcC92aCF37AC6B56B69180857bB69f7D1", type: "Safe" },
|
||||
{ name: "Gitcoin Treasury", address: "0xde21F729137C5Af1b01d73aF1dC21eFfa2B8a0d6", type: "Safe" },
|
||||
{ name: "Vitalik.eth", address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", type: "EOA" },
|
||||
];
|
||||
|
||||
class FolkWalletViewer extends HTMLElement {
|
||||
private shadow: ShadowRoot;
|
||||
private address = "";
|
||||
|
|
@ -43,6 +57,7 @@ class FolkWalletViewer extends HTMLElement {
|
|||
private error = "";
|
||||
private isDemo = false;
|
||||
private walletType: "safe" | "eoa" | "" = "";
|
||||
private includeTestnets = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -108,9 +123,10 @@ class FolkWalletViewer extends HTMLElement {
|
|||
|
||||
try {
|
||||
const base = this.getApiBase();
|
||||
const tn = this.includeTestnets ? "?testnets=true" : "";
|
||||
|
||||
// Try Safe detection first
|
||||
const res = await fetch(`${base}/api/safe/detect/${this.address}`);
|
||||
const res = await fetch(`${base}/api/safe/detect/${this.address}${tn}`);
|
||||
const data = await res.json();
|
||||
|
||||
this.detectedChains = (data.chains || []).map((c: any) => ({
|
||||
|
|
@ -124,7 +140,7 @@ class FolkWalletViewer extends HTMLElement {
|
|||
await this.loadBalances();
|
||||
} else {
|
||||
// Fall back to EOA detection (any wallet)
|
||||
const eoaRes = await fetch(`${base}/api/eoa/detect/${this.address}`);
|
||||
const eoaRes = await fetch(`${base}/api/eoa/detect/${this.address}${tn}`);
|
||||
const eoaData = await eoaRes.json();
|
||||
|
||||
this.detectedChains = (eoaData.chains || []).map((c: any) => ({
|
||||
|
|
@ -211,27 +227,120 @@ class FolkWalletViewer extends HTMLElement {
|
|||
this.render();
|
||||
}
|
||||
|
||||
private render() {
|
||||
const totalUSD = this.balances.reduce((sum, b) => sum + parseFloat(b.fiatBalance || "0"), 0);
|
||||
private hasData(): boolean {
|
||||
return this.detectedChains.length > 0;
|
||||
}
|
||||
|
||||
this.shadow.innerHTML = `
|
||||
private renderStyles(): string {
|
||||
return `
|
||||
<style>
|
||||
:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: #e0e0e0; }
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.address-bar { display: flex; gap: 8px; margin-bottom: 24px; }
|
||||
.address-bar input {
|
||||
flex: 1; padding: 10px 14px; border-radius: 8px;
|
||||
border: 1px solid #444; background: #2a2a3e; color: #e0e0e0;
|
||||
font-family: monospace; font-size: 13px;
|
||||
/* ── Hero ── */
|
||||
.hero {
|
||||
text-align: center; padding: 32px 16px 24px;
|
||||
border-bottom: 1px solid #2a2a3e; margin-bottom: 24px;
|
||||
}
|
||||
.address-bar input:focus { border-color: #00d4ff; outline: none; }
|
||||
.address-bar button {
|
||||
padding: 10px 20px; border-radius: 8px; border: none;
|
||||
background: #00d4ff; color: #000; font-weight: 600; cursor: pointer;
|
||||
.hero-title {
|
||||
font-size: 28px; font-weight: 700; margin: 0 0 6px;
|
||||
background: linear-gradient(135deg, #00d4ff, #4ade80);
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.address-bar button:hover { background: #00b8d9; }
|
||||
.hero-subtitle { font-size: 14px; color: #888; margin: 0 0 20px; }
|
||||
|
||||
/* ── Address bar ── */
|
||||
.address-bar { display: flex; gap: 8px; margin-bottom: 12px; max-width: 640px; margin-left: auto; margin-right: auto; }
|
||||
.address-bar input {
|
||||
flex: 1; padding: 12px 16px; border-radius: 10px;
|
||||
border: 1px solid #444; background: #1e1e2e; color: #e0e0e0;
|
||||
font-family: monospace; font-size: 14px;
|
||||
}
|
||||
.address-bar input:focus { border-color: #00d4ff; outline: none; box-shadow: 0 0 0 2px rgba(0,212,255,0.15); }
|
||||
.address-bar button {
|
||||
padding: 12px 24px; border-radius: 10px; border: none;
|
||||
background: linear-gradient(135deg, #00d4ff, #00b8d9); color: #000;
|
||||
font-weight: 600; cursor: pointer; font-size: 14px; transition: opacity 0.2s;
|
||||
}
|
||||
.address-bar button:hover { opacity: 0.9; }
|
||||
|
||||
/* ── Controls row ── */
|
||||
.controls-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
max-width: 640px; margin: 0 auto 24px; flex-wrap: wrap; gap: 8px;
|
||||
}
|
||||
.testnet-toggle {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
cursor: pointer; user-select: none; font-size: 13px; color: #888;
|
||||
}
|
||||
.testnet-toggle:hover { color: #bbb; }
|
||||
.toggle-track {
|
||||
width: 34px; height: 18px; border-radius: 9px; background: #333;
|
||||
position: relative; transition: background 0.2s;
|
||||
}
|
||||
.testnet-toggle.active .toggle-track { background: #f59e0b; }
|
||||
.toggle-thumb {
|
||||
width: 14px; height: 14px; border-radius: 50%; background: #666;
|
||||
position: absolute; top: 2px; left: 2px; transition: all 0.2s;
|
||||
}
|
||||
.testnet-toggle.active .toggle-thumb { left: 18px; background: #fff; }
|
||||
.wallet-badge {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
.wallet-badge.safe { background: rgba(74,222,128,0.12); color: #4ade80; border: 1px solid rgba(74,222,128,0.25); }
|
||||
.wallet-badge.eoa { background: rgba(0,212,255,0.12); color: #00d4ff; border: 1px solid rgba(0,212,255,0.25); }
|
||||
|
||||
/* ── Supported chains ── */
|
||||
.supported-chains {
|
||||
display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
|
||||
margin-bottom: 28px; max-width: 640px; margin-left: auto; margin-right: auto;
|
||||
}
|
||||
.supported-chain {
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
padding: 4px 10px; border-radius: 6px; background: #1e1e2e;
|
||||
border: 1px solid #2a2a3e; font-size: 12px; color: #999;
|
||||
}
|
||||
.supported-chain .dot { width: 6px; height: 6px; border-radius: 50%; }
|
||||
|
||||
/* ── Feature cards ── */
|
||||
.features {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px; margin-bottom: 28px;
|
||||
}
|
||||
.feature-card {
|
||||
background: #1e1e2e; border: 1px solid #2a2a3e; border-radius: 12px;
|
||||
padding: 20px; text-align: center; transition: border-color 0.2s;
|
||||
}
|
||||
.feature-card:hover { border-color: #444; }
|
||||
.feature-icon { font-size: 28px; margin-bottom: 10px; }
|
||||
.feature-card h3 { font-size: 14px; font-weight: 600; margin: 0 0 6px; color: #e0e0e0; }
|
||||
.feature-card p { font-size: 12px; color: #888; margin: 0; line-height: 1.5; }
|
||||
|
||||
/* ── Example wallets ── */
|
||||
.examples {
|
||||
max-width: 640px; margin: 0 auto 20px;
|
||||
}
|
||||
.examples-label { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
|
||||
.example-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.example-item {
|
||||
display: flex; align-items: center; gap: 10px; padding: 10px 14px;
|
||||
background: #1e1e2e; border: 1px solid #2a2a3e; border-radius: 10px;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.example-item:hover { border-color: #00d4ff; background: #252540; }
|
||||
.example-name { font-size: 13px; font-weight: 500; color: #e0e0e0; }
|
||||
.example-addr { font-size: 11px; color: #666; font-family: monospace; }
|
||||
.example-type {
|
||||
margin-left: auto; font-size: 10px; font-weight: 600;
|
||||
padding: 2px 8px; border-radius: 4px; text-transform: uppercase;
|
||||
}
|
||||
.example-type.safe { background: rgba(74,222,128,0.1); color: #4ade80; }
|
||||
.example-type.eoa { background: rgba(0,212,255,0.1); color: #00d4ff; }
|
||||
|
||||
/* ── Dashboard: chains ── */
|
||||
.chains { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }
|
||||
.chain-btn {
|
||||
padding: 6px 14px; border-radius: 8px; border: 2px solid #333;
|
||||
|
|
@ -242,6 +351,7 @@ class FolkWalletViewer extends HTMLElement {
|
|||
.chain-btn.active { border-color: var(--chain-color); background: rgba(255,255,255,0.05); }
|
||||
.chain-dot { width: 8px; height: 8px; border-radius: 50%; }
|
||||
|
||||
/* ── Dashboard: stats ── */
|
||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; }
|
||||
.stat-card {
|
||||
background: #1e1e2e; border: 1px solid #333; border-radius: 10px;
|
||||
|
|
@ -250,6 +360,7 @@ class FolkWalletViewer extends HTMLElement {
|
|||
.stat-label { font-size: 11px; color: #888; text-transform: uppercase; margin-bottom: 6px; }
|
||||
.stat-value { font-size: 20px; font-weight: 700; color: #00d4ff; }
|
||||
|
||||
/* ── Dashboard: balance table ── */
|
||||
.balance-table { width: 100%; border-collapse: collapse; }
|
||||
.balance-table th {
|
||||
text-align: left; padding: 10px 8px; border-bottom: 2px solid #333;
|
||||
|
|
@ -262,95 +373,205 @@ class FolkWalletViewer extends HTMLElement {
|
|||
.amount-cell { text-align: right; font-family: monospace; }
|
||||
.fiat { color: #4ade80; }
|
||||
|
||||
/* ── States ── */
|
||||
.empty { text-align: center; color: #666; padding: 40px; }
|
||||
.loading { text-align: center; color: #888; padding: 40px; }
|
||||
.error { text-align: center; color: #ef5350; padding: 20px; }
|
||||
.demo-link { color: #00d4ff; cursor: pointer; text-decoration: underline; }
|
||||
.loading .spinner {
|
||||
display: inline-block; width: 20px; height: 20px;
|
||||
border: 2px solid #333; border-top-color: #00d4ff; border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite; margin-right: 8px; vertical-align: middle;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.error {
|
||||
text-align: center; color: #ef5350; padding: 16px;
|
||||
background: rgba(239,83,80,0.08); border: 1px solid rgba(239,83,80,0.2);
|
||||
border-radius: 10px; margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-title { font-size: 22px; }
|
||||
.balance-table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
.address-bar { flex-wrap: wrap; }
|
||||
.address-bar input { min-width: 0; }
|
||||
.chains { flex-wrap: wrap; }
|
||||
.features { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
</style>
|
||||
@media (max-width: 480px) {
|
||||
.features { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
|
||||
private renderHero(): string {
|
||||
if (this.hasData()) return "";
|
||||
return `
|
||||
<div class="hero">
|
||||
<div class="hero-title">rWallet</div>
|
||||
<div class="hero-subtitle">Multichain treasury visualization — Safe multisigs and EOA wallets</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderSupportedChains(): string {
|
||||
if (this.hasData() || this.loading) return "";
|
||||
return `
|
||||
<div class="supported-chains">
|
||||
${Object.entries(CHAIN_NAMES).map(([id, name]) => `
|
||||
<div class="supported-chain">
|
||||
<div class="dot" style="background: ${CHAIN_COLORS[id]}"></div>
|
||||
${name}
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderFeatures(): string {
|
||||
if (this.hasData() || this.loading) return "";
|
||||
return `
|
||||
<div class="features">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⛓</div>
|
||||
<h3>Safe Multisig</h3>
|
||||
<p>Visualize Gnosis Safe balances, signers, and thresholds across all chains.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔗</div>
|
||||
<h3>Any Wallet (EOA)</h3>
|
||||
<p>Paste any 0x address — works for regular wallets too, not just Safes.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🌐</div>
|
||||
<h3>10+ Chains</h3>
|
||||
<p>Ethereum, Base, Polygon, Gnosis, Arbitrum, Optimism, and more in one view.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔒</div>
|
||||
<h3>No Custody Risk</h3>
|
||||
<p>Read-only. rWallet never holds keys or moves funds — just visualizes.</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderExamples(): string {
|
||||
if (this.hasData() || this.loading) return "";
|
||||
return `
|
||||
<div class="examples">
|
||||
<div class="examples-label">Try an example</div>
|
||||
<div class="example-list">
|
||||
${EXAMPLE_WALLETS.map((w) => `
|
||||
<div class="example-item" data-address="${w.address}">
|
||||
<div>
|
||||
<div class="example-name">${w.name}</div>
|
||||
<div class="example-addr">${w.address.slice(0, 6)}...${w.address.slice(-4)}</div>
|
||||
</div>
|
||||
<div class="example-type ${w.type.toLowerCase()}">${w.type}</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderDashboard(): string {
|
||||
if (!this.hasData()) return "";
|
||||
const totalUSD = this.balances.reduce((sum, b) => sum + parseFloat(b.fiatBalance || "0"), 0);
|
||||
|
||||
return `
|
||||
<div class="chains">
|
||||
${this.detectedChains.map((ch) => `
|
||||
<div class="chain-btn ${this.selectedChain === ch.chainId ? "active" : ""}"
|
||||
data-chain="${ch.chainId}" style="--chain-color: ${ch.color}">
|
||||
<div class="chain-dot" style="background: ${ch.color}"></div>
|
||||
${ch.name}
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Value</div>
|
||||
<div class="stat-value">${this.formatUSD(String(totalUSD))}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Tokens</div>
|
||||
<div class="stat-value">${this.balances.filter((b) => parseFloat(b.fiatBalance || "0") > 0).length}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Chains</div>
|
||||
<div class="stat-value">${this.detectedChains.length}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Address</div>
|
||||
<div class="stat-value" style="font-size:13px;font-family:monospace">${this.shortenAddress(this.address)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.balances.length > 0 ? `
|
||||
<table class="balance-table">
|
||||
<thead>
|
||||
<tr><th>Token</th><th class="amount-cell">Balance</th><th class="amount-cell">USD Value</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.balances
|
||||
.filter((b) => parseFloat(b.fiatBalance || "0") > 0.01)
|
||||
.sort((a, b) => parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0"))
|
||||
.map((b) => `
|
||||
<tr>
|
||||
<td>
|
||||
<span class="token-symbol">${b.token?.symbol || "ETH"}</span>
|
||||
<span class="token-name">${b.token?.name || "Ether"}</span>
|
||||
</td>
|
||||
<td class="amount-cell">${this.formatBalance(b.balance, b.token?.decimals || 18)}</td>
|
||||
<td class="amount-cell fiat">${this.formatUSD(b.fiatBalance)}</td>
|
||||
</tr>
|
||||
`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
` : '<div class="empty">No token balances found on this chain.</div>'}`;
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.shadow.innerHTML = `
|
||||
${this.renderStyles()}
|
||||
|
||||
${this.renderHero()}
|
||||
|
||||
<form class="address-bar" id="address-form">
|
||||
<input id="address-input" type="text" placeholder="Enter wallet address (0x...)"
|
||||
<input id="address-input" type="text" placeholder="Enter any wallet or Safe address (0x...)"
|
||||
value="${this.address}" spellcheck="false">
|
||||
<button type="submit">Load</button>
|
||||
</form>
|
||||
|
||||
${!this.address && !this.loading ? `
|
||||
<div class="empty">
|
||||
<p style="font-size:16px;margin-bottom:8px">Enter any wallet address to visualize</p>
|
||||
<p>Supports Safe multisigs and regular wallets (EOA)</p>
|
||||
<p style="margin-top:8px">Try: <span class="demo-link" data-address="0x29567BdBcC92aCF37AC6B56B69180857bB69f7D1">TEC Commons Fund (Safe)</span></p>
|
||||
<div class="controls-row">
|
||||
<div class="testnet-toggle ${this.includeTestnets ? "active" : ""}" id="testnet-toggle">
|
||||
<div class="toggle-track"><div class="toggle-thumb"></div></div>
|
||||
<span>Include testnets</span>
|
||||
</div>
|
||||
` : ""}
|
||||
${this.walletType ? `
|
||||
<div class="wallet-badge ${this.walletType}">
|
||||
${this.walletType === "safe" ? "⛓ Safe Multisig" : "👤 EOA Wallet"}
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
|
||||
${this.renderSupportedChains()}
|
||||
|
||||
${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
|
||||
${this.loading ? '<div class="loading">Detecting wallet across chains...</div>' : ""}
|
||||
${this.loading ? '<div class="loading"><span class="spinner"></span> Detecting wallet across chains...</div>' : ""}
|
||||
|
||||
${!this.loading && this.detectedChains.length > 0 ? `
|
||||
<div class="chains">
|
||||
${this.detectedChains.map((ch) => `
|
||||
<div class="chain-btn ${this.selectedChain === ch.chainId ? "active" : ""}"
|
||||
data-chain="${ch.chainId}" style="--chain-color: ${ch.color}">
|
||||
<div class="chain-dot" style="background: ${ch.color}"></div>
|
||||
${ch.name}
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Value</div>
|
||||
<div class="stat-value">${this.formatUSD(String(totalUSD))}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Tokens</div>
|
||||
<div class="stat-value">${this.balances.filter((b) => parseFloat(b.fiatBalance || "0") > 0).length}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Chains</div>
|
||||
<div class="stat-value">${this.detectedChains.length}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Address</div>
|
||||
<div class="stat-value" style="font-size:13px;font-family:monospace">${this.shortenAddress(this.address)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.balances.length > 0 ? `
|
||||
<table class="balance-table">
|
||||
<thead>
|
||||
<tr><th>Token</th><th class="amount-cell">Balance</th><th class="amount-cell">USD Value</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.balances
|
||||
.filter((b) => parseFloat(b.fiatBalance || "0") > 0.01)
|
||||
.sort((a, b) => parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0"))
|
||||
.map((b) => `
|
||||
<tr>
|
||||
<td>
|
||||
<span class="token-symbol">${b.token?.symbol || "ETH"}</span>
|
||||
<span class="token-name">${b.token?.name || "Ether"}</span>
|
||||
</td>
|
||||
<td class="amount-cell">${this.formatBalance(b.balance, b.token?.decimals || 18)}</td>
|
||||
<td class="amount-cell fiat">${this.formatUSD(b.fiatBalance)}</td>
|
||||
</tr>
|
||||
`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
` : '<div class="empty">No balances found on this chain.</div>'}
|
||||
` : ""}
|
||||
${this.renderFeatures()}
|
||||
${this.renderExamples()}
|
||||
${this.renderDashboard()}
|
||||
`;
|
||||
|
||||
// Event listeners
|
||||
const form = this.shadow.querySelector("#address-form");
|
||||
form?.addEventListener("submit", (e) => this.handleSubmit(e));
|
||||
|
||||
this.shadow.querySelector("#testnet-toggle")?.addEventListener("click", () => {
|
||||
this.includeTestnets = !this.includeTestnets;
|
||||
if (this.address) this.detectChains();
|
||||
else this.render();
|
||||
});
|
||||
|
||||
this.shadow.querySelectorAll(".chain-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const chainId = (btn as HTMLElement).dataset.chain!;
|
||||
|
|
@ -358,12 +579,15 @@ class FolkWalletViewer extends HTMLElement {
|
|||
});
|
||||
});
|
||||
|
||||
this.shadow.querySelectorAll(".demo-link").forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
const addr = (link as HTMLElement).dataset.address!;
|
||||
this.shadow.querySelectorAll(".example-item").forEach((item) => {
|
||||
item.addEventListener("click", () => {
|
||||
const addr = (item as HTMLElement).dataset.address!;
|
||||
const input = this.shadow.querySelector("#address-input") as HTMLInputElement;
|
||||
if (input) input.value = addr;
|
||||
this.address = addr;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("address", addr);
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
this.detectChains();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ routes.get("/api/safe/:chainId/:address/info", async (c) => {
|
|||
// Detect which chains have a Safe for this address
|
||||
routes.get("/api/safe/detect/:address", async (c) => {
|
||||
const address = c.req.param("address");
|
||||
const chains = Object.entries(CHAIN_MAP);
|
||||
const includeTestnets = c.req.query("testnets") === "true";
|
||||
const chains = getChains(includeTestnets);
|
||||
const results: Array<{ chainId: string; name: string; prefix: string }> = [];
|
||||
|
||||
await Promise.allSettled(
|
||||
|
|
@ -88,6 +89,12 @@ const CHAIN_MAP: Record<string, { name: string; prefix: string }> = {
|
|||
"84532": { name: "Base Sepolia", prefix: "base-sepolia" },
|
||||
};
|
||||
|
||||
const TESTNET_CHAIN_IDS = new Set(["11155111", "84532"]);
|
||||
|
||||
function getChains(includeTestnets: boolean): [string, { name: string; prefix: string }][] {
|
||||
return Object.entries(CHAIN_MAP).filter(([id]) => includeTestnets || !TESTNET_CHAIN_IDS.has(id));
|
||||
}
|
||||
|
||||
function getSafePrefix(chainId: string): string | null {
|
||||
return CHAIN_MAP[chainId]?.prefix || null;
|
||||
}
|
||||
|
|
@ -318,10 +325,11 @@ routes.post("/api/safe/:chainId/:address/execute", async (c) => {
|
|||
// Detect which chains have a non-zero native balance for any address
|
||||
routes.get("/api/eoa/detect/:address", async (c) => {
|
||||
const address = c.req.param("address");
|
||||
const includeTestnets = c.req.query("testnets") === "true";
|
||||
const results: Array<{ chainId: string; name: string; prefix: string; balance: string }> = [];
|
||||
|
||||
await Promise.allSettled(
|
||||
Object.entries(CHAIN_MAP).map(async ([chainId, info]) => {
|
||||
getChains(includeTestnets).map(async ([chainId, info]) => {
|
||||
const rpcUrl = RPC_URLS[chainId];
|
||||
if (!rpcUrl) return;
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in New Issue