diff --git a/modules/rwallet/components/folk-wallet-viewer.ts b/modules/rwallet/components/folk-wallet-viewer.ts
index 83495c4..e4c11ad 100644
--- a/modules/rwallet/components/folk-wallet-viewer.ts
+++ b/modules/rwallet/components/folk-wallet-viewer.ts
@@ -42,6 +42,7 @@ class FolkWalletViewer extends HTMLElement {
private loading = false;
private error = "";
private isDemo = false;
+ private walletType: "safe" | "eoa" | "" = "";
constructor() {
super();
@@ -102,10 +103,13 @@ class FolkWalletViewer extends HTMLElement {
this.error = "";
this.detectedChains = [];
this.balances = [];
+ this.walletType = "";
this.render();
try {
const base = this.getApiBase();
+
+ // Try Safe detection first
const res = await fetch(`${base}/api/safe/detect/${this.address}`);
const data = await res.json();
@@ -114,15 +118,30 @@ class FolkWalletViewer extends HTMLElement {
color: CHAIN_COLORS[c.chainId] || "#888",
}));
- if (this.detectedChains.length === 0) {
- this.error = "No Safe wallets found for this address on any supported chain.";
- } else {
- // Auto-select first chain
+ if (this.detectedChains.length > 0) {
+ this.walletType = "safe";
this.selectedChain = this.detectedChains[0].chainId;
await this.loadBalances();
+ } else {
+ // Fall back to EOA detection (any wallet)
+ const eoaRes = await fetch(`${base}/api/eoa/detect/${this.address}`);
+ const eoaData = await eoaRes.json();
+
+ this.detectedChains = (eoaData.chains || []).map((c: any) => ({
+ ...c,
+ color: CHAIN_COLORS[c.chainId] || "#888",
+ }));
+
+ if (this.detectedChains.length > 0) {
+ this.walletType = "eoa";
+ this.selectedChain = this.detectedChains[0].chainId;
+ await this.loadBalances();
+ } else {
+ this.error = "No balances found for this address on any supported chain.";
+ }
}
} catch (e) {
- this.error = "Failed to detect chains. Check the address and try again.";
+ this.error = "Failed to detect wallet. Check the address and try again.";
}
this.loading = false;
@@ -134,7 +153,10 @@ class FolkWalletViewer extends HTMLElement {
if (!this.selectedChain) return;
try {
const base = this.getApiBase();
- const res = await fetch(`${base}/api/safe/${this.selectedChain}/${this.address}/balances`);
+ const apiPath = this.walletType === "safe"
+ ? `${base}/api/safe/${this.selectedChain}/${this.address}/balances`
+ : `${base}/api/eoa/${this.selectedChain}/${this.address}/balances`;
+ const res = await fetch(apiPath);
if (res.ok) {
this.balances = await res.json();
}
@@ -254,20 +276,21 @@ class FolkWalletViewer extends HTMLElement {
${!this.address && !this.loading ? `
-
Enter a Safe wallet address to visualize
-
Try: TEC Commons Fund
+
Enter any wallet address to visualize
+
Supports Safe multisigs and regular wallets (EOA)
+
Try: TEC Commons Fund (Safe)
` : ""}
${this.error ? `${this.esc(this.error)}
` : ""}
- ${this.loading ? 'Detecting Safe wallets across chains...
' : ""}
+ ${this.loading ? 'Detecting wallet across chains...
' : ""}
${!this.loading && this.detectedChains.length > 0 ? `
diff --git a/modules/rwallet/mod.ts b/modules/rwallet/mod.ts
index ef7654a..98fa63a 100644
--- a/modules/rwallet/mod.ts
+++ b/modules/rwallet/mod.ts
@@ -92,6 +92,48 @@ function getSafePrefix(chainId: string): string | null {
return CHAIN_MAP[chainId]?.prefix || null;
}
+// ── Public RPC endpoints for EOA balance lookups ──
+const RPC_URLS: Record
= {
+ "1": "https://eth.llamarpc.com",
+ "10": "https://mainnet.optimism.io",
+ "100": "https://rpc.gnosischain.com",
+ "137": "https://polygon-rpc.com",
+ "8453": "https://mainnet.base.org",
+ "42161": "https://arb1.arbitrum.io/rpc",
+ "42220": "https://forno.celo.org",
+ "43114": "https://api.avax.network/ext/bc/C/rpc",
+ "56": "https://bsc-dataseed.binance.org",
+ "324": "https://mainnet.era.zksync.io",
+ "11155111": "https://rpc.sepolia.org",
+ "84532": "https://sepolia.base.org",
+};
+
+const NATIVE_TOKENS: Record = {
+ "1": { name: "Ether", symbol: "ETH", decimals: 18 },
+ "10": { name: "Ether", symbol: "ETH", decimals: 18 },
+ "100": { name: "xDAI", symbol: "xDAI", decimals: 18 },
+ "137": { name: "MATIC", symbol: "MATIC", decimals: 18 },
+ "8453": { name: "Ether", symbol: "ETH", decimals: 18 },
+ "42161": { name: "Ether", symbol: "ETH", decimals: 18 },
+ "42220": { name: "CELO", symbol: "CELO", decimals: 18 },
+ "43114": { name: "AVAX", symbol: "AVAX", decimals: 18 },
+ "56": { name: "BNB", symbol: "BNB", decimals: 18 },
+ "324": { name: "Ether", symbol: "ETH", decimals: 18 },
+ "11155111": { name: "Sepolia ETH", symbol: "ETH", decimals: 18 },
+ "84532": { name: "Base Sepolia ETH", symbol: "ETH", decimals: 18 },
+};
+
+async function rpcCall(rpcUrl: string, method: string, params: any[]): Promise {
+ const res = await fetch(rpcUrl, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
+ signal: AbortSignal.timeout(5000),
+ });
+ const data = await res.json();
+ return data.result;
+}
+
// ── Safe Proposal / Confirm / Execute (EncryptID-authenticated) ──
// Helper: extract and verify JWT from request
@@ -271,6 +313,65 @@ routes.post("/api/safe/:chainId/:address/execute", async (c) => {
});
});
+// ── EOA (any wallet) balance endpoints ──
+
+// 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 results: Array<{ chainId: string; name: string; prefix: string; balance: string }> = [];
+
+ await Promise.allSettled(
+ Object.entries(CHAIN_MAP).map(async ([chainId, info]) => {
+ const rpcUrl = RPC_URLS[chainId];
+ if (!rpcUrl) return;
+ try {
+ const balHex = await rpcCall(rpcUrl, "eth_getBalance", [address, "latest"]);
+ if (balHex && balHex !== "0x0" && balHex !== "0x") {
+ results.push({ chainId, name: info.name, prefix: info.prefix, balance: balHex });
+ }
+ } catch {}
+ })
+ );
+
+ return c.json({ address, chains: results.sort((a, b) => a.name.localeCompare(b.name)) });
+});
+
+// Get native token balance for an EOA on a specific chain
+routes.get("/api/eoa/:chainId/:address/balances", async (c) => {
+ const chainId = c.req.param("chainId");
+ const address = c.req.param("address");
+ const rpcUrl = RPC_URLS[chainId];
+ if (!rpcUrl) return c.json({ error: "Unsupported chain" }, 400);
+
+ const nativeToken = NATIVE_TOKENS[chainId] || { name: "ETH", symbol: "ETH", decimals: 18 };
+ const balances: BalanceItem[] = [];
+
+ try {
+ const balHex = await rpcCall(rpcUrl, "eth_getBalance", [address, "latest"]);
+ const balWei = BigInt(balHex || "0x0");
+ if (balWei > 0n) {
+ balances.push({
+ tokenAddress: null,
+ token: nativeToken,
+ balance: balWei.toString(),
+ fiatBalance: "0",
+ fiatConversion: "0",
+ });
+ }
+ } catch {}
+
+ c.header("Cache-Control", "public, max-age=30");
+ return c.json(balances);
+});
+
+interface BalanceItem {
+ tokenAddress: string | null;
+ token: { name: string; symbol: string; decimals: number };
+ balance: string;
+ fiatBalance: string;
+ fiatConversion: string;
+}
+
// ── Page route ──
routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "demo";