diff --git a/packages/web-ui/src/ActivityList.tsx b/packages/web-ui/src/ActivityList.tsx index dcb6e17..b35a3cf 100644 --- a/packages/web-ui/src/ActivityList.tsx +++ b/packages/web-ui/src/ActivityList.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { fmtSats } from './format.js'; import type { DashboardUtxo } from './useWalletBalance.js'; import type { BlockbookHistoryTx } from '@dgb-wallet/blockbook'; import type { HistoryStatus } from './useWalletHistory.js'; @@ -60,17 +61,9 @@ export function ActivityList({ {historyStatus === 'disabled' && ( -
-
- Want the full history? -

- Send + receive with dates comes from a Blockbook indexer. Enable it and the operator (digibyte.atomicwallet.io) will see the addresses this wallet queries. - Your seed and keys never leave this browser. -

-
- +
+ Full history is off (Settings → Connectivity → Blockbook).{' '} +
)} @@ -343,15 +336,7 @@ function shortenAddr(a: string): string { return a.length <= 20 ? a : `${a.slice(0, 10)}…${a.slice(-6)}`; } -function formatSats(sats: bigint): string { - // BigInt → 8-decimal DGB. Avoid Number rounding. - const neg = sats < 0n; - const abs = neg ? -sats : sats; - const s = abs.toString().padStart(9, '0'); - const int = s.slice(0, -8); - const frac = s.slice(-8); - return `${neg ? '-' : ''}${Number(int).toLocaleString()}.${frac}`; -} +const formatSats = fmtSats; function formatTime(ms: number): string { const d = new Date(ms); diff --git a/packages/web-ui/src/AssetsList.tsx b/packages/web-ui/src/AssetsList.tsx index 6650137..6800be8 100644 --- a/packages/web-ui/src/AssetsList.tsx +++ b/packages/web-ui/src/AssetsList.tsx @@ -1,4 +1,5 @@ import { DgbLogo } from './DgbLogo.js'; +import { fmtDgb } from './format.js'; // Assets section. Today the only asset is native DGB. When // DigiAssets (Layer-2 token) support lands, each token becomes @@ -13,8 +14,7 @@ export function AssetsList({ loading: boolean; }): JSX.Element { const dgb = totalSats / 1e8; - const [intPart, fracPart] = dgb.toFixed(8).split('.'); - const intFmt = Number(intPart).toLocaleString(); + const [intFmt, fracPart = '00'] = fmtDgb(dgb).split('.'); return (
diff --git a/packages/web-ui/src/BalanceCard.tsx b/packages/web-ui/src/BalanceCard.tsx index 5747ff6..f25ba3a 100644 --- a/packages/web-ui/src/BalanceCard.tsx +++ b/packages/web-ui/src/BalanceCard.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { fmtDgb } from './format.js'; import { DgbLogo } from './DgbLogo.js'; import { WINDOW_CHOICES, type WindowChoice, type ScanDiagnostic } from './useWalletBalance.js'; @@ -164,12 +165,7 @@ export function BalanceCard({ // 8-decimal fixed. Splits into integer + fractional so the CSS can // dim the low-order fraction (a common wallet UX to make small // balances feel less overwhelming). -function formatDgb(dgb: number): string { - const rounded = Math.round(dgb * 1e8) / 1e8; - const [intPart, fracPart = '00000000'] = rounded.toFixed(8).split('.'); - const intWithSep = Number(intPart).toLocaleString(undefined, { maximumFractionDigits: 0 }); - return `${intWithSep}.${fracPart}`; -} +const formatDgb = (dgb: number): string => fmtDgb(dgb); function formatAge(ms: number): string { if (ms < 60_000) return `${Math.floor(ms / 1000)}s`; diff --git a/packages/web-ui/src/Dashboard.tsx b/packages/web-ui/src/Dashboard.tsx index 9c1a7d9..de8b80b 100644 --- a/packages/web-ui/src/Dashboard.tsx +++ b/packages/web-ui/src/Dashboard.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { fmtSats } from './format.js'; import { seedFromPhrase, rootFromSeed, accountNode, addressNode, addressesForPubkey, importWifWithAddresses, @@ -411,12 +412,4 @@ function AddressesPanel({ ); } -function formatSats(sats: bigint): string { - if (sats === 0n) return '0'; - const neg = sats < 0n; - const abs = neg ? -sats : sats; - const s = abs.toString().padStart(9, '0'); - const int = s.slice(0, -8); - const frac = s.slice(-8).replace(/0+$/, ''); - return `${neg ? '-' : ''}${Number(int).toLocaleString()}${frac ? '.' + frac : ''}`; -} +const formatSats = fmtSats; diff --git a/packages/web-ui/src/ImportScreen.tsx b/packages/web-ui/src/ImportScreen.tsx index c381a8a..94000c2 100644 --- a/packages/web-ui/src/ImportScreen.tsx +++ b/packages/web-ui/src/ImportScreen.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; +import { fmtSats } from './format.js'; import { PasswordInput } from './PasswordInput.js'; import { validateSeedPhrase, @@ -559,7 +560,5 @@ export function ImportScreen(): JSX.Element { } function fmt(sat: bigint | null): string { - if (sat === null) return '—'; - const s = sat.toString().padStart(9, '0'); - return `${Number(s.slice(0, -8)).toLocaleString()}.${s.slice(-8)}`; + return sat === null ? '—' : fmtSats(sat); } diff --git a/packages/web-ui/src/PathSearchPanel.tsx b/packages/web-ui/src/PathSearchPanel.tsx index 664dfae..f651056 100644 --- a/packages/web-ui/src/PathSearchPanel.tsx +++ b/packages/web-ui/src/PathSearchPanel.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; +import { fmtSats } from './format.js'; import { searchPaths, searchSingle, PATH_CANDIDATES, candidateKey, type PathHit, type PathCandidate } from './path-search.js'; import { useActiveSeed } from './ActiveSeed.js'; import { @@ -269,7 +270,5 @@ export function PathSearchPanel(): JSX.Element | null { } function fmt(sat: bigint | null): string { - if (sat === null) return '—'; - const s = sat.toString().padStart(9, '0'); - return `${Number(s.slice(0, -8)).toLocaleString()}.${s.slice(-8)}`; + return sat === null ? '—' : fmtSats(sat); } diff --git a/packages/web-ui/src/SendScreen.tsx b/packages/web-ui/src/SendScreen.tsx index 9a7e0a1..94d1b21 100644 --- a/packages/web-ui/src/SendScreen.tsx +++ b/packages/web-ui/src/SendScreen.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react'; +import { fmtSats } from './format.js'; import { PinConfirmModal } from './PinConfirmModal.js'; import { useSessionAuth } from './session-auth.js'; import { @@ -258,7 +259,7 @@ export function SendScreen({ // is extra fee — bounded by DUST_SATS + one output's fee share. const need = feeNoChange(); if (inputSats < amtSats + need) { - throw new Error(`Insufficient funds: have ${(inputSats / 1e8).toFixed(8)}, need ${((amtSats + need) / 1e8).toFixed(8)} (incl. ${(need / 1e8).toFixed(8)} fee)`); + throw new Error(`Insufficient funds: have ${fmtSats(inputSats)}, need ${fmtSats(amtSats + need)} (incl. ${fmtSats(need)} fee)`); } changeAmt = 0; feeSats = inputSats - amtSats; @@ -492,7 +493,7 @@ export function SendScreen({ {pinPrompt && ( { setPinPrompt(false); void doBroadcast(); }} onCancel={() => setPinPrompt(false)} /> @@ -506,15 +507,15 @@ export function SendScreen({ To {r.recipient} Amount - {(r.amountSats / 1e8).toFixed(8)} DGB + {fmtSats(r.amountSats)} DGB Fee - {(r.feeSats / 1e8).toFixed(8)} DGB + {fmtSats(r.feeSats)} DGB Total out - {((r.amountSats + r.feeSats) / 1e8).toFixed(8)} DGB + {fmtSats(r.amountSats + r.feeSats)} DGB Change {status.changeAddress - ? <>{(r.change.reduce((s, c) => s + c.value, 0) / 1e8).toFixed(8)} DGB → {status.changeAddress} + ? <>{fmtSats(r.change.reduce((s, c) => s + c.value, 0))} DGB → {status.changeAddress} : 'none'} Inputs @@ -635,12 +636,12 @@ export function SendScreen({
- Network fee: {FEE_RATE_SAT_PER_VB} sat/vB (DigiByte relays nothing under {MIN_RELAY_SAT_PER_VB}), about {(feeFor(estimateVbytes(['p2wpkh'], ['dgb1q', 'dgb1q'])) / 1e8).toFixed(5)} DGB for a typical send. + Network fee: {FEE_RATE_SAT_PER_VB} sat/vB (DigiByte relays nothing under {MIN_RELAY_SAT_PER_VB}), about {fmtSats(feeFor(estimateVbytes(['p2wpkh'], ['dgb1q', 'dgb1q'])))} DGB for a typical send. {' '}Change goes to { backendConfig.privacy.changePolicy === 'same' ? 'the address the coins were spent from' : backendConfig.privacy.changePolicy === 'fresh' ? 'a fresh address on the same derivation path as the coins spent' : 'your BIP84 bech32 change branch'} (Settings → Security). - {' '}Spendable now: {(availableSats / 1e8).toFixed(8)} DGB + {' '}Spendable now: {fmtSats(availableSats)} DGB {scanStatus === 'loading' ? ' (scan still running)' : ''}.
diff --git a/packages/web-ui/src/backend-config.ts b/packages/web-ui/src/backend-config.ts index 2b59797..ad39e15 100644 --- a/packages/web-ui/src/backend-config.ts +++ b/packages/web-ui/src/backend-config.ts @@ -74,7 +74,7 @@ const STORAGE_KEY = 'dgb-wallet:backend-config'; const DEFAULT_CONFIG: BackendConfig = { p2p: { scan: true, broadcast: true }, - blockbook: { scan: false, broadcast: false }, + blockbook: { scan: true, broadcast: false }, display: { priceFeed: false, autoWidenScan: true, autoRefreshSec: 60 }, privacy: { freshAddresses: false, changePolicy: 'same' }, security: { requirePinOnSend: false }, @@ -84,12 +84,18 @@ function readStored(): BackendConfig { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return DEFAULT_CONFIG; - const parsed = JSON.parse(raw) as BackendConfig; + const parsed = JSON.parse(raw) as BackendConfig & { v?: number }; // Fill missing keys from defaults so old stored configs stay // valid when we add capabilities. + // v2 (2026-10): full history became the default. Configs saved before + // that carry blockbook.scan=false only because it was the old default, + // not because anyone chose it — flip it once, then record v2 so a + // deliberate later opt-out sticks. + const blockbook = { ...DEFAULT_CONFIG.blockbook, ...(parsed.blockbook ?? {}) }; + if ((parsed.v ?? 1) < 2) blockbook.scan = true; return { p2p: { ...DEFAULT_CONFIG.p2p, ...(parsed.p2p ?? {}) }, - blockbook: { ...DEFAULT_CONFIG.blockbook, ...(parsed.blockbook ?? {}) }, + blockbook, display: { ...DEFAULT_CONFIG.display, ...(parsed.display ?? {}) }, privacy: { ...DEFAULT_CONFIG.privacy, ...(parsed.privacy ?? {}) }, security: { ...DEFAULT_CONFIG.security, ...(parsed.security ?? {}) }, @@ -100,7 +106,7 @@ function readStored(): BackendConfig { } function writeStored(config: BackendConfig): void { - try { localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); } catch { /* private mode */ } + try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...config, v: 2 })); } catch { /* private mode */ } } // Context-backed store. Every consumer in the tree shares the same diff --git a/packages/web-ui/src/format.ts b/packages/web-ui/src/format.ts new file mode 100644 index 0000000..172e3bf --- /dev/null +++ b/packages/web-ui/src/format.ts @@ -0,0 +1,22 @@ +// One rule for every DGB amount the wallet displays: integer part with +// thousands separators, decimals trimmed of trailing zeros but never +// fewer than MIN_DECIMALS. So 3,811.32468574 stays as is, 5.00000000 +// becomes 5.00, and 0.00033450 becomes 0.0003345. Exact: works on +// satoshis as bigint, never through a float. +const MIN_DECIMALS = 2; + +export function fmtSats(sats: bigint | number, min = MIN_DECIMALS): string { + const v = typeof sats === 'bigint' ? sats : BigInt(Math.round(sats)); + const neg = v < 0n; + const abs = neg ? -v : v; + const s = abs.toString().padStart(9, '0'); + const int = Number(s.slice(0, -8)).toLocaleString(); + let frac = s.slice(-8).replace(/0+$/, ''); + if (frac.length < min) frac = frac.padEnd(min, '0'); + return `${neg ? '-' : ''}${int}${frac ? '.' + frac : ''}`; +} + +/** Same rule for a DGB-denominated float (converted to sats first). */ +export function fmtDgb(dgb: number, min = MIN_DECIMALS): string { + return fmtSats(BigInt(Math.round(dgb * 1e8)), min); +}