web-ui: full history on by default; trim trailing zeros on amounts
Blockbook history is enabled by default (existing configs migrated once via a v2 marker; a later deliberate opt-out sticks). The in-panel opt-in prompt is replaced by a one-line note when it is off. All displayed DGB amounts go through one formatter: thousands separators, trailing zeros trimmed, never fewer than two decimals (3,811.32468574, 5.00, 0.0003345). Amount-field values keep full precision.
This commit is contained in:
parent
989a2696c9
commit
a965a09b40
9 changed files with 56 additions and 55 deletions
|
|
@ -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({
|
|||
</div>
|
||||
|
||||
{historyStatus === 'disabled' && (
|
||||
<div className="history-optin">
|
||||
<div className="history-optin-body">
|
||||
<strong>Want the full history?</strong>
|
||||
<p>
|
||||
Send + receive with dates comes from a Blockbook indexer. Enable it and the operator (<code>digibyte.atomicwallet.io</code>) will see the addresses this wallet queries.
|
||||
Your seed and keys never leave this browser.
|
||||
</p>
|
||||
</div>
|
||||
<button className="history-optin-btn" onClick={onEnableHistory}>
|
||||
Enable full history
|
||||
</button>
|
||||
<div className="field-help" style={{ padding: '0 4px 10px' }}>
|
||||
Full history is off (Settings → Connectivity → Blockbook).{' '}
|
||||
<button className="text-btn" style={{ padding: 0 }} onClick={onEnableHistory}>Turn it on</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<section className="assets">
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<PinConfirmModal
|
||||
title="Confirm broadcast"
|
||||
message={`Enter your session PIN to send ${(r.amountSats / 1e8).toFixed(8)} DGB.`}
|
||||
message={`Enter your session PIN to send ${fmtSats(r.amountSats)} DGB.`}
|
||||
onConfirm={() => { setPinPrompt(false); void doBroadcast(); }}
|
||||
onCancel={() => setPinPrompt(false)}
|
||||
/>
|
||||
|
|
@ -506,15 +507,15 @@ export function SendScreen({
|
|||
<span className="tx-k">To</span>
|
||||
<span className="tx-v"><code className="tx-mono">{r.recipient}</code></span>
|
||||
<span className="tx-k">Amount</span>
|
||||
<span className="tx-v send-review-amount">{(r.amountSats / 1e8).toFixed(8)} DGB</span>
|
||||
<span className="tx-v send-review-amount">{fmtSats(r.amountSats)} DGB</span>
|
||||
<span className="tx-k">Fee</span>
|
||||
<span className="tx-v">{(r.feeSats / 1e8).toFixed(8)} DGB</span>
|
||||
<span className="tx-v">{fmtSats(r.feeSats)} DGB</span>
|
||||
<span className="tx-k">Total out</span>
|
||||
<span className="tx-v">{((r.amountSats + r.feeSats) / 1e8).toFixed(8)} DGB</span>
|
||||
<span className="tx-v">{fmtSats(r.amountSats + r.feeSats)} DGB</span>
|
||||
<span className="tx-k">Change</span>
|
||||
<span className="tx-v">
|
||||
{status.changeAddress
|
||||
? <>{(r.change.reduce((s, c) => s + c.value, 0) / 1e8).toFixed(8)} DGB → <code className="tx-mono">{status.changeAddress}</code></>
|
||||
? <>{fmtSats(r.change.reduce((s, c) => s + c.value, 0))} DGB → <code className="tx-mono">{status.changeAddress}</code></>
|
||||
: 'none'}
|
||||
</span>
|
||||
<span className="tx-k">Inputs</span>
|
||||
|
|
@ -635,12 +636,12 @@ export function SendScreen({
|
|||
</div>
|
||||
</div>
|
||||
<div className="field-help">
|
||||
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)' : ''}.
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
22
packages/web-ui/src/format.ts
Normal file
22
packages/web-ui/src/format.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue