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)} DGBFee
- {(r.feeSats / 1e8).toFixed(8)} DGB
+ {fmtSats(r.feeSats)} DGBTotal out
- {((r.amountSats + r.feeSats) / 1e8).toFixed(8)} DGB
+ {fmtSats(r.amountSats + r.feeSats)} DGBChange
{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);
+}