diff --git a/modules/rwallet/components/folk-wallet-viewer.ts b/modules/rwallet/components/folk-wallet-viewer.ts index e4c11ad..a9b1a72 100644 --- a/modules/rwallet/components/folk-wallet-viewer.ts +++ b/modules/rwallet/components/folk-wallet-viewer.ts @@ -31,8 +31,22 @@ const CHAIN_COLORS: Record = { "43114": "#e84142", "56": "#f3ba2f", "324": "#8c8dfc", + "11155111": "#f59e0b", + "84532": "#f59e0b", }; +const CHAIN_NAMES: Record = { + "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 ` + @media (max-width: 480px) { + .features { grid-template-columns: 1fr; } + } + `; + } + + private renderHero(): string { + if (this.hasData()) return ""; + return ` +
+
rWallet
+
Multichain treasury visualization — Safe multisigs and EOA wallets
+
`; + } + + private renderSupportedChains(): string { + if (this.hasData() || this.loading) return ""; + return ` +
+ ${Object.entries(CHAIN_NAMES).map(([id, name]) => ` +
+
+ ${name} +
+ `).join("")} +
`; + } + + private renderFeatures(): string { + if (this.hasData() || this.loading) return ""; + return ` +
+
+
+

Safe Multisig

+

Visualize Gnosis Safe balances, signers, and thresholds across all chains.

+
+
+
🔗
+

Any Wallet (EOA)

+

Paste any 0x address — works for regular wallets too, not just Safes.

+
+
+
🌐
+

10+ Chains

+

Ethereum, Base, Polygon, Gnosis, Arbitrum, Optimism, and more in one view.

+
+
+
🔒
+

No Custody Risk

+

Read-only. rWallet never holds keys or moves funds — just visualizes.

+
+
`; + } + + private renderExamples(): string { + if (this.hasData() || this.loading) return ""; + return ` +
+
Try an example
+
+ ${EXAMPLE_WALLETS.map((w) => ` +
+
+
${w.name}
+
${w.address.slice(0, 6)}...${w.address.slice(-4)}
+
+
${w.type}
+
+ `).join("")} +
+
`; + } + + private renderDashboard(): string { + if (!this.hasData()) return ""; + const totalUSD = this.balances.reduce((sum, b) => sum + parseFloat(b.fiatBalance || "0"), 0); + + return ` +
+ ${this.detectedChains.map((ch) => ` +
+
+ ${ch.name} +
+ `).join("")} +
+ +
+
+
Total Value
+
${this.formatUSD(String(totalUSD))}
+
+
+
Tokens
+
${this.balances.filter((b) => parseFloat(b.fiatBalance || "0") > 0).length}
+
+
+
Chains
+
${this.detectedChains.length}
+
+
+
Address
+
${this.shortenAddress(this.address)}
+
+
+ + ${this.balances.length > 0 ? ` + + + + + + ${this.balances + .filter((b) => parseFloat(b.fiatBalance || "0") > 0.01) + .sort((a, b) => parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0")) + .map((b) => ` + + + + + + `).join("")} + +
TokenBalanceUSD Value
+ ${b.token?.symbol || "ETH"} + ${b.token?.name || "Ether"} + ${this.formatBalance(b.balance, b.token?.decimals || 18)}${this.formatUSD(b.fiatBalance)}
+ ` : '
No token balances found on this chain.
'}`; + } + + private render() { + this.shadow.innerHTML = ` + ${this.renderStyles()} + + ${this.renderHero()}
-
- ${!this.address && !this.loading ? ` -
-

Enter any wallet address to visualize

-

Supports Safe multisigs and regular wallets (EOA)

-

Try: TEC Commons Fund (Safe)

+
+
+
+ Include testnets
- ` : ""} + ${this.walletType ? ` +
+ ${this.walletType === "safe" ? "⛓ Safe Multisig" : "👤 EOA Wallet"} +
+ ` : ""} +
+ + ${this.renderSupportedChains()} ${this.error ? `
${this.esc(this.error)}
` : ""} - ${this.loading ? '
Detecting wallet across chains...
' : ""} + ${this.loading ? '
Detecting wallet across chains...
' : ""} - ${!this.loading && this.detectedChains.length > 0 ? ` -
- ${this.detectedChains.map((ch) => ` -
-
- ${ch.name} -
- `).join("")} -
- -
-
-
Total Value
-
${this.formatUSD(String(totalUSD))}
-
-
-
Tokens
-
${this.balances.filter((b) => parseFloat(b.fiatBalance || "0") > 0).length}
-
-
-
Chains
-
${this.detectedChains.length}
-
-
-
Address
-
${this.shortenAddress(this.address)}
-
-
- - ${this.balances.length > 0 ? ` - - - - - - ${this.balances - .filter((b) => parseFloat(b.fiatBalance || "0") > 0.01) - .sort((a, b) => parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0")) - .map((b) => ` - - - - - - `).join("")} - -
TokenBalanceUSD Value
- ${b.token?.symbol || "ETH"} - ${b.token?.name || "Ether"} - ${this.formatBalance(b.balance, b.token?.decimals || 18)}${this.formatUSD(b.fiatBalance)}
- ` : '
No balances found on this chain.
'} - ` : ""} + ${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(); }); }); diff --git a/modules/rwallet/mod.ts b/modules/rwallet/mod.ts index 98fa63a..b27bbbd 100644 --- a/modules/rwallet/mod.ts +++ b/modules/rwallet/mod.ts @@ -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 = { "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 {