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 <noreply@anthropic.com>
This commit is contained in:
parent
f9ccd18f15
commit
f5388ecc2c
|
|
@ -492,7 +492,7 @@ class FolkWalletViewer extends HTMLElement {
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">Tokens</div>
|
<div class="stat-label">Tokens</div>
|
||||||
<div class="stat-value">${this.balances.filter((b) => parseFloat(b.fiatBalance || "0") > 0).length}</div>
|
<div class="stat-value">${this.balances.filter((b) => parseFloat(b.fiatBalance || "0") > 0 || BigInt(b.balance || "0") > 0n).length}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">Chains</div>
|
<div class="stat-label">Chains</div>
|
||||||
|
|
@ -511,8 +511,12 @@ class FolkWalletViewer extends HTMLElement {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${this.balances
|
${this.balances
|
||||||
.filter((b) => parseFloat(b.fiatBalance || "0") > 0.01)
|
.filter((b) => parseFloat(b.fiatBalance || "0") > 0.01 || BigInt(b.balance || "0") > 0n)
|
||||||
.sort((a, b) => parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0"))
|
.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) => `
|
.map((b) => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,18 @@ routes.get("/api/safe/:chainId/:address/balances", async (c) => {
|
||||||
const chainPrefix = getSafePrefix(chainId);
|
const chainPrefix = getSafePrefix(chainId);
|
||||||
if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400);
|
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);
|
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");
|
c.header("Cache-Control", "public, max-age=30");
|
||||||
return c.json(data);
|
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);
|
if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400);
|
||||||
|
|
||||||
const limit = c.req.query("limit") || "100";
|
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);
|
if (!res.ok) return c.json({ error: "Safe API error" }, res.status as any);
|
||||||
return c.json(await res.json());
|
return c.json(await res.json());
|
||||||
});
|
});
|
||||||
|
|
@ -45,7 +54,7 @@ routes.get("/api/safe/:chainId/:address/info", async (c) => {
|
||||||
const chainPrefix = getSafePrefix(chainId);
|
const chainPrefix = getSafePrefix(chainId);
|
||||||
if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400);
|
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);
|
if (!res.ok) return c.json({ error: "Safe API error" }, res.status as any);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
c.header("Cache-Control", "public, max-age=300");
|
c.header("Cache-Control", "public, max-age=300");
|
||||||
|
|
@ -62,7 +71,7 @@ routes.get("/api/safe/detect/:address", async (c) => {
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
chains.map(async ([chainId, info]) => {
|
chains.map(async ([chainId, info]) => {
|
||||||
try {
|
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),
|
signal: AbortSignal.timeout(5000),
|
||||||
});
|
});
|
||||||
if (res.ok) results.push({ chainId, name: info.name, prefix: info.prefix });
|
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)) });
|
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<string, { name: string; prefix: string }> = {
|
const CHAIN_MAP: Record<string, { name: string; prefix: string }> = {
|
||||||
"1": { name: "Ethereum", prefix: "mainnet" },
|
"1": { name: "Ethereum", prefix: "eth" },
|
||||||
"10": { name: "Optimism", prefix: "optimism" },
|
"10": { name: "Optimism", prefix: "oeth" },
|
||||||
"100": { name: "Gnosis", prefix: "gnosis-chain" },
|
"100": { name: "Gnosis", prefix: "gno" },
|
||||||
"137": { name: "Polygon", prefix: "polygon" },
|
"137": { name: "Polygon", prefix: "pol" },
|
||||||
"8453": { name: "Base", prefix: "base" },
|
"8453": { name: "Base", prefix: "base" },
|
||||||
"42161": { name: "Arbitrum", prefix: "arbitrum" },
|
"42161": { name: "Arbitrum", prefix: "arb1" },
|
||||||
"42220": { name: "Celo", prefix: "celo" },
|
"42220": { name: "Celo", prefix: "celo" },
|
||||||
"43114": { name: "Avalanche", prefix: "avalanche" },
|
"43114": { name: "Avalanche", prefix: "avax" },
|
||||||
"56": { name: "BSC", prefix: "bsc" },
|
"56": { name: "BSC", prefix: "bnb" },
|
||||||
"324": { name: "zkSync", prefix: "zksync" },
|
"324": { name: "zkSync", prefix: "zksync" },
|
||||||
"11155111": { name: "Sepolia", prefix: "sepolia" },
|
"11155111": { name: "Sepolia", prefix: "sep" },
|
||||||
"84532": { name: "Base Sepolia", prefix: "base-sepolia" },
|
"84532": { name: "Base Sepolia", prefix: "basesep" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const TESTNET_CHAIN_IDS = new Set(["11155111", "84532"]);
|
const TESTNET_CHAIN_IDS = new Set(["11155111", "84532"]);
|
||||||
|
|
@ -108,6 +117,10 @@ function getSafePrefix(chainId: string): string | null {
|
||||||
return CHAIN_MAP[chainId]?.prefix || 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 ──
|
// ── Public RPC endpoints for EOA balance lookups ──
|
||||||
const RPC_URLS: Record<string, string> = {
|
const RPC_URLS: Record<string, string> = {
|
||||||
"1": "https://eth.llamarpc.com",
|
"1": "https://eth.llamarpc.com",
|
||||||
|
|
@ -195,7 +208,7 @@ routes.post("/api/safe/:chainId/:address/propose", async (c) => {
|
||||||
|
|
||||||
// Submit proposal to Safe Transaction Service
|
// Submit proposal to Safe Transaction Service
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://safe-transaction-${chainPrefix}.safe.global/api/v1/safes/${address}/multisig-transactions/`,
|
`${safeApiBase(chainPrefix)}/safes/${address}/multisig-transactions/`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|
@ -246,7 +259,7 @@ routes.post("/api/safe/:chainId/:address/confirm", async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://safe-transaction-${chainPrefix}.safe.global/api/v1/multisig-transactions/${safeTxHash}/confirmations/`,
|
`${safeApiBase(chainPrefix)}/multisig-transactions/${safeTxHash}/confirmations/`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
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
|
// Fetch the transaction details from Safe Transaction Service
|
||||||
const txRes = await fetch(
|
const txRes = await fetch(
|
||||||
`https://safe-transaction-${chainPrefix}.safe.global/api/v1/multisig-transactions/${safeTxHash}/`,
|
`${safeApiBase(chainPrefix)}/multisig-transactions/${safeTxHash}/`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!txRes.ok) {
|
if (!txRes.ok) {
|
||||||
|
|
@ -409,7 +422,7 @@ routes.get("/", (c) => {
|
||||||
modules: getModuleInfoList(),
|
modules: getModuleInfoList(),
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
body: `<folk-wallet-viewer></folk-wallet-viewer>`,
|
body: `<folk-wallet-viewer></folk-wallet-viewer>`,
|
||||||
scripts: `<script type="module" src="/modules/rwallet/folk-wallet-viewer.js?v=2"></script>`,
|
scripts: `<script type="module" src="/modules/rwallet/folk-wallet-viewer.js?v=3"></script>`,
|
||||||
styles: `<link rel="stylesheet" href="/modules/rwallet/wallet.css">`,
|
styles: `<link rel="stylesheet" href="/modules/rwallet/wallet.css">`,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2640,9 +2640,9 @@ app.post('/encryptid/api/safe/verify', async (c) => {
|
||||||
|
|
||||||
const chain = chainId || 84532;
|
const chain = chainId || 84532;
|
||||||
const CHAIN_PREFIXES: Record<number, string> = {
|
const CHAIN_PREFIXES: Record<number, string> = {
|
||||||
1: 'mainnet', 10: 'optimism', 100: 'gnosis-chain', 137: 'polygon',
|
1: 'eth', 10: 'oeth', 100: 'gno', 137: 'pol',
|
||||||
8453: 'base', 42161: 'arbitrum', 42220: 'celo', 43114: 'avalanche',
|
8453: 'base', 42161: 'arb1', 42220: 'celo', 43114: 'avax',
|
||||||
56: 'bsc', 324: 'zksync', 11155111: 'sepolia', 84532: 'base-sepolia',
|
56: 'bnb', 324: 'zksync', 11155111: 'sep', 84532: 'basesep',
|
||||||
};
|
};
|
||||||
const prefix = CHAIN_PREFIXES[chain];
|
const prefix = CHAIN_PREFIXES[chain];
|
||||||
if (!prefix) {
|
if (!prefix) {
|
||||||
|
|
@ -2651,7 +2651,7 @@ app.post('/encryptid/api/safe/verify', async (c) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const safeRes = await fetch(
|
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) {
|
if (!safeRes.ok) {
|
||||||
return c.json({ isOwner: false, error: 'Safe not found' });
|
return c.json({ isOwner: false, error: 'Safe not found' });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue