From f5388ecc2c935d691df9f2b536d54acf89011f79 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 9 Mar 2026 13:57:09 -0700 Subject: [PATCH] fix(rwallet): migrate to new Safe Global API (api.safe.global) Safe Global deprecated per-chain subdomains (safe-transaction-*.safe.global) in favour of api.safe.global/tx-service/{shortcode}. The old URLs now 308 redirect, and the /balances/usd/ endpoint no longer exists. - Update CHAIN_MAP prefixes to new shortcodes (eth, oeth, gno, etc.) - Switch all Safe API calls to new base URL - Use /balances/ instead of /balances/usd/ (fiat data no longer available) - Normalize balance response with native token info and placeholder fiat fields - Update client to show tokens with non-zero balance even without fiat data - Update encryptid/server.ts Safe verify endpoint to new API Co-Authored-By: Claude Opus 4.6 --- .../rwallet/components/folk-wallet-viewer.ts | 10 ++-- modules/rwallet/mod.ts | 51 ++++++++++++------- src/encryptid/server.ts | 8 +-- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/modules/rwallet/components/folk-wallet-viewer.ts b/modules/rwallet/components/folk-wallet-viewer.ts index a9b1a72..b18f406 100644 --- a/modules/rwallet/components/folk-wallet-viewer.ts +++ b/modules/rwallet/components/folk-wallet-viewer.ts @@ -492,7 +492,7 @@ class FolkWalletViewer extends HTMLElement {
Tokens
-
${this.balances.filter((b) => parseFloat(b.fiatBalance || "0") > 0).length}
+
${this.balances.filter((b) => parseFloat(b.fiatBalance || "0") > 0 || BigInt(b.balance || "0") > 0n).length}
Chains
@@ -511,8 +511,12 @@ class FolkWalletViewer extends HTMLElement { ${this.balances - .filter((b) => parseFloat(b.fiatBalance || "0") > 0.01) - .sort((a, b) => parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0")) + .filter((b) => parseFloat(b.fiatBalance || "0") > 0.01 || BigInt(b.balance || "0") > 0n) + .sort((a, b) => { + const fiatDiff = parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0"); + if (fiatDiff !== 0) return fiatDiff; + return Number(BigInt(b.balance || "0") - BigInt(a.balance || "0")); + }) .map((b) => ` diff --git a/modules/rwallet/mod.ts b/modules/rwallet/mod.ts index fbad47a..c797044 100644 --- a/modules/rwallet/mod.ts +++ b/modules/rwallet/mod.ts @@ -20,9 +20,18 @@ routes.get("/api/safe/:chainId/:address/balances", async (c) => { const chainPrefix = getSafePrefix(chainId); if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400); - const res = await fetch(`https://safe-transaction-${chainPrefix}.safe.global/api/v1/safes/${address}/balances/usd/?trusted=true&exclude_spam=true`); + const res = await fetch(`${safeApiBase(chainPrefix)}/safes/${address}/balances/?trusted=true&exclude_spam=true`); if (!res.ok) return c.json({ error: "Safe API error" }, res.status as any); - const data = await res.json(); + const raw = await res.json() as any[]; + const nativeToken = NATIVE_TOKENS[chainId] || { name: "ETH", symbol: "ETH", decimals: 18 }; + // Normalize: fill in native token info and ensure fiatBalance fields exist + const data = raw.map((item: any) => ({ + tokenAddress: item.tokenAddress, + token: item.token || nativeToken, + balance: item.balance || "0", + fiatBalance: item.fiatBalance || "0", + fiatConversion: item.fiatConversion || "0", + })); c.header("Cache-Control", "public, max-age=30"); return c.json(data); }); @@ -34,7 +43,7 @@ routes.get("/api/safe/:chainId/:address/transfers", async (c) => { if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400); const limit = c.req.query("limit") || "100"; - const res = await fetch(`https://safe-transaction-${chainPrefix}.safe.global/api/v1/safes/${address}/all-transactions/?limit=${limit}&executed=true`); + const res = await fetch(`${safeApiBase(chainPrefix)}/safes/${address}/all-transactions/?limit=${limit}&executed=true`); if (!res.ok) return c.json({ error: "Safe API error" }, res.status as any); return c.json(await res.json()); }); @@ -45,7 +54,7 @@ routes.get("/api/safe/:chainId/:address/info", async (c) => { const chainPrefix = getSafePrefix(chainId); if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400); - const res = await fetch(`https://safe-transaction-${chainPrefix}.safe.global/api/v1/safes/${address}/`); + const res = await fetch(`${safeApiBase(chainPrefix)}/safes/${address}/`); if (!res.ok) return c.json({ error: "Safe API error" }, res.status as any); const data = await res.json(); c.header("Cache-Control", "public, max-age=300"); @@ -62,7 +71,7 @@ routes.get("/api/safe/detect/:address", async (c) => { await Promise.allSettled( chains.map(async ([chainId, info]) => { try { - const res = await fetch(`https://safe-transaction-${info.prefix}.safe.global/api/v1/safes/${address}/`, { + const res = await fetch(`${safeApiBase(info.prefix)}/safes/${address}/`, { signal: AbortSignal.timeout(5000), }); if (res.ok) results.push({ chainId, name: info.name, prefix: info.prefix }); @@ -82,20 +91,20 @@ routes.get("/api/safe/detect/:address", async (c) => { return c.json({ address, chains: results.sort((a, b) => a.name.localeCompare(b.name)) }); }); -// ── Chain mapping ── +// ── Chain mapping (prefix = Safe TX Service shortcode) ── const CHAIN_MAP: Record = { - "1": { name: "Ethereum", prefix: "mainnet" }, - "10": { name: "Optimism", prefix: "optimism" }, - "100": { name: "Gnosis", prefix: "gnosis-chain" }, - "137": { name: "Polygon", prefix: "polygon" }, + "1": { name: "Ethereum", prefix: "eth" }, + "10": { name: "Optimism", prefix: "oeth" }, + "100": { name: "Gnosis", prefix: "gno" }, + "137": { name: "Polygon", prefix: "pol" }, "8453": { name: "Base", prefix: "base" }, - "42161": { name: "Arbitrum", prefix: "arbitrum" }, + "42161": { name: "Arbitrum", prefix: "arb1" }, "42220": { name: "Celo", prefix: "celo" }, - "43114": { name: "Avalanche", prefix: "avalanche" }, - "56": { name: "BSC", prefix: "bsc" }, + "43114": { name: "Avalanche", prefix: "avax" }, + "56": { name: "BSC", prefix: "bnb" }, "324": { name: "zkSync", prefix: "zksync" }, - "11155111": { name: "Sepolia", prefix: "sepolia" }, - "84532": { name: "Base Sepolia", prefix: "base-sepolia" }, + "11155111": { name: "Sepolia", prefix: "sep" }, + "84532": { name: "Base Sepolia", prefix: "basesep" }, }; const TESTNET_CHAIN_IDS = new Set(["11155111", "84532"]); @@ -108,6 +117,10 @@ function getSafePrefix(chainId: string): string | null { return CHAIN_MAP[chainId]?.prefix || null; } +function safeApiBase(prefix: string): string { + return `https://api.safe.global/tx-service/${prefix}/api/v1`; +} + // ── Public RPC endpoints for EOA balance lookups ── const RPC_URLS: Record = { "1": "https://eth.llamarpc.com", @@ -195,7 +208,7 @@ routes.post("/api/safe/:chainId/:address/propose", async (c) => { // Submit proposal to Safe Transaction Service const res = await fetch( - `https://safe-transaction-${chainPrefix}.safe.global/api/v1/safes/${address}/multisig-transactions/`, + `${safeApiBase(chainPrefix)}/safes/${address}/multisig-transactions/`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -246,7 +259,7 @@ routes.post("/api/safe/:chainId/:address/confirm", async (c) => { } const res = await fetch( - `https://safe-transaction-${chainPrefix}.safe.global/api/v1/multisig-transactions/${safeTxHash}/confirmations/`, + `${safeApiBase(chainPrefix)}/multisig-transactions/${safeTxHash}/confirmations/`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -294,7 +307,7 @@ routes.post("/api/safe/:chainId/:address/execute", async (c) => { // Fetch the transaction details from Safe Transaction Service const txRes = await fetch( - `https://safe-transaction-${chainPrefix}.safe.global/api/v1/multisig-transactions/${safeTxHash}/`, + `${safeApiBase(chainPrefix)}/multisig-transactions/${safeTxHash}/`, ); if (!txRes.ok) { @@ -409,7 +422,7 @@ routes.get("/", (c) => { modules: getModuleInfoList(), theme: "dark", body: ``, - scripts: ``, + scripts: ``, styles: ``, })); }); diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index f495586..93af80a 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -2640,9 +2640,9 @@ app.post('/encryptid/api/safe/verify', async (c) => { const chain = chainId || 84532; const CHAIN_PREFIXES: Record = { - 1: 'mainnet', 10: 'optimism', 100: 'gnosis-chain', 137: 'polygon', - 8453: 'base', 42161: 'arbitrum', 42220: 'celo', 43114: 'avalanche', - 56: 'bsc', 324: 'zksync', 11155111: 'sepolia', 84532: 'base-sepolia', + 1: 'eth', 10: 'oeth', 100: 'gno', 137: 'pol', + 8453: 'base', 42161: 'arb1', 42220: 'celo', 43114: 'avax', + 56: 'bnb', 324: 'zksync', 11155111: 'sep', 84532: 'basesep', }; const prefix = CHAIN_PREFIXES[chain]; if (!prefix) { @@ -2651,7 +2651,7 @@ app.post('/encryptid/api/safe/verify', async (c) => { try { const safeRes = await fetch( - `https://safe-transaction-${prefix}.safe.global/api/v1/safes/${safeAddress}/`, + `https://api.safe.global/tx-service/${prefix}/api/v1/safes/${safeAddress}/`, ); if (!safeRes.ok) { return c.json({ isOwner: false, error: 'Safe not found' });