Wallet Funds
+-- ETH | -- USDC
+diff --git a/README.md b/README.md index 8166e19..8607d05 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Top-level control surface: 6. Pull-first updates feed + support ticket action 7. Identity assurance visibility (`none` / `crypto_direct_unattested` / `sponsored_unattested` / `onramp_attested`) 8. Explicit operator-visible mode toggles (`Human mode` / `Auto mode`) synced to governance `operation_mode` +9. Wallet utility actions (`Refresh balances`, `Copy address`) with native + USDC balance visibility Advanced integration controls (collapsible): diff --git a/app/app.js b/app/app.js index 2fc745e..4b3f2d1 100644 --- a/app/app.js +++ b/app/app.js @@ -12,6 +12,8 @@ const state = { walletSessionToken: "", walletSessionExpiresAt: "", walletSessionRefreshInFlight: null, + walletBalanceNative: "", + walletBalanceUSDC: "", }; function nowISO() { @@ -77,6 +79,12 @@ function sessionSummary() { return `active (exp ${state.walletSessionExpiresAt})`; } +function walletBalanceSummary() { + const native = state.walletBalanceNative || "-- ETH"; + const usdc = state.walletBalanceUSDC || "-- USDC"; + return `${native} | ${usdc}`; +} + function clearWalletSession(reason, payload = {}) { if (!state.walletSessionToken && !state.walletSessionExpiresAt) { return; @@ -184,6 +192,41 @@ function utf8ToHex(value) { .join("")}`; } +function normalizeHexQuantity(value) { + const raw = String(value || "").trim().toLowerCase(); + if (!raw.startsWith("0x")) { + return null; + } + return raw; +} + +function formatUnitsHex(hexValue, decimals, precision = 6) { + const hex = normalizeHexQuantity(hexValue); + if (!hex) { + return null; + } + let bigint; + try { + bigint = BigInt(hex); + } catch { + return null; + } + const unit = 10n ** BigInt(Math.max(0, Number(decimals) || 0)); + const whole = bigint / unit; + const fraction = bigint % unit; + if (fraction === 0n) { + return whole.toString(); + } + const padded = fraction.toString().padStart(Number(decimals), "0"); + const trimmed = padded.slice(0, Math.max(1, precision)).replace(/0+$/, ""); + return trimmed ? `${whole.toString()}.${trimmed}` : whole.toString(); +} + +function encodeAddressWord(address) { + const normalized = normalizedAddress(address).replace(/^0x/, ""); + return normalized.padStart(64, "0"); +} + function logLine(label, payload) { const log = $("log"); const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`; @@ -216,6 +259,7 @@ function refreshActionLocks(statusPayload) { function refreshOverview(statusPayload) { const currentWallet = wallet(); setSummary("summaryWallet", currentWallet || "not connected"); + setSummary("summaryFunds", walletBalanceSummary()); setSummary("summarySession", sessionSummary()); refreshModeUI(); if (statusPayload && typeof statusPayload === "object") { @@ -453,9 +497,74 @@ async function onConnectWallet() { provider_chain_id: providerChainID, }); } + try { + await onRefreshBalances(); + } catch (err) { + logLine("wallet balance refresh warning", { error: String(err) }); + } refreshOverview(); } +async function onRefreshBalances() { + const address = normalizedAddress(requireWallet()); + const provider = await requireProvider(); + const nativeHex = await provider.request({ + method: "eth_getBalance", + params: [address, "latest"], + }); + const nativeDisplay = formatUnitsHex(nativeHex, 18, 6); + if (nativeDisplay) { + state.walletBalanceNative = `${nativeDisplay} ETH`; + } + + const usdcToken = normalizedAddress($("usdcTokenAddress")?.value || ""); + if (usdcToken && /^0x[a-f0-9]{40}$/.test(usdcToken)) { + const balanceOfSelector = "0x70a08231"; + const decimalsSelector = "0x313ce567"; + const balanceCallData = `${balanceOfSelector}${encodeAddressWord(address)}`; + const usdcHex = await provider.request({ + method: "eth_call", + params: [{ to: usdcToken, data: balanceCallData }, "latest"], + }); + let decimals = 6; + try { + const decimalsHex = await provider.request({ + method: "eth_call", + params: [{ to: usdcToken, data: decimalsSelector }, "latest"], + }); + const parsed = Number(BigInt(String(decimalsHex || "0x6"))); + if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 36) { + decimals = parsed; + } + } catch { + // keep default 6 when token decimals call is unavailable + } + const usdcDisplay = formatUnitsHex(usdcHex, decimals, 2); + if (usdcDisplay) { + state.walletBalanceUSDC = `${usdcDisplay} USDC`; + } + } else if (usdcToken) { + state.walletBalanceUSDC = "invalid token"; + } + + refreshOverview(); + logLine("wallet balances refreshed", { + wallet: address, + native: state.walletBalanceNative || "", + usdc: state.walletBalanceUSDC || "", + }); +} + +async function onCopyWallet() { + const address = requireWallet(); + if (!navigator?.clipboard?.writeText) { + throw new Error("clipboard API unavailable"); + } + await navigator.clipboard.writeText(address); + setFlowStatus("wallet address copied"); + logLine("wallet copied", { wallet: address }); +} + async function onSignIntent() { if (!state.lastIntent) { throw new Error("create intent before signing"); @@ -1119,6 +1228,8 @@ bind("btnQuickConnect", onQuickConnect); bind("btnQuickActivate", onQuickActivate); bind("btnQuickRefresh", onQuickRefresh); bind("btnQuickInstallStatus", onQuickInstallStatus); +bind("btnRefreshBalances", onRefreshBalances); +bind("btnCopyWallet", onCopyWallet); bind("btnModeHuman", onModeHuman); bind("btnModeAuto", onModeAuto); bind("btnConnectWallet", onConnectWallet); diff --git a/app/index.html b/app/index.html index f5a1474..11bbed3 100644 --- a/app/index.html +++ b/app/index.html @@ -20,6 +20,8 @@ + + @@ -33,6 +35,10 @@
none
+-- ETH | -- USDC
+unknown
@@ -96,6 +102,10 @@ Chain ID +