From 0a5d7ade2843c10b732ffab5885e7c4aa123e626 Mon Sep 17 00:00:00 2001 From: "Digibyte.X" Date: Fri, 2 Oct 2026 18:13:07 +0200 Subject: [PATCH] scan gap 200/100 (DigiScope parity); trim decimals only on the main balance; price feed on by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Receive-chain gap 200 and change-chain gap 100 everywhere a chain is walked: daemon /balance (new gapInternal, cap 200), Blockbook xpub queries, history ownership set, and the signing-key derivation in Send so anything the scan finds is spendable. Trailing-zero trimming now applies to the hero balance and assets row only; history, details and the send review keep eight decimals. The hero number no longer wraps — long balances step down in size. Price feed defaults on; existing configs flipped once via the v3 marker. --- packages/p2p/bin/dgb-daemon.ts | 9 ++++++-- packages/web-ui/src/ActivityList.tsx | 4 ++-- packages/web-ui/src/BalanceCard.tsx | 2 +- packages/web-ui/src/Dashboard.tsx | 4 ++-- packages/web-ui/src/ImportScreen.tsx | 4 ++-- packages/web-ui/src/PathSearchPanel.tsx | 4 ++-- packages/web-ui/src/SendScreen.tsx | 30 +++++++++++++------------ packages/web-ui/src/backend-config.ts | 9 +++++--- packages/web-ui/src/format.ts | 5 +++++ packages/web-ui/src/gap.ts | 7 ++++++ packages/web-ui/src/styles.css | 7 +++++- packages/web-ui/src/useWalletBalance.ts | 7 +++--- packages/web-ui/src/useWalletHistory.ts | 5 +++-- 13 files changed, 63 insertions(+), 34 deletions(-) create mode 100644 packages/web-ui/src/gap.ts diff --git a/packages/p2p/bin/dgb-daemon.ts b/packages/p2p/bin/dgb-daemon.ts index 66113c2..e934a10 100644 --- a/packages/p2p/bin/dgb-daemon.ts +++ b/packages/p2p/bin/dgb-daemon.ts @@ -473,6 +473,7 @@ const server = http.createServer(async (req, res) => { // are no xpub-derived children to enumerate. scripts?: string[]; gapLimit?: number; + gapInternal?: number; startHeight?: number; endHeight?: number; }; @@ -488,7 +489,11 @@ const server = http.createServer(async (req, res) => { res.end(JSON.stringify({ error: 'expected {xpub}, {descriptors:[...]}, or {scripts:[hex,…]}' })); return; } - const gapLimit = Math.min(body.gapLimit ?? 20, 100); + // Receive-chain gap (default 20, cap 200) and change-chain gap + // (defaults to the receive gap). 200/100 matches what other DGB + // recovery wallets probe, so a sparse old wallet is not missed. + const gapLimit = Math.min(body.gapLimit ?? 20, 200); + const gapInternal = Math.min(body.gapInternal ?? gapLimit, 200); const DEFAULT_WINDOW = 1000; const rawStart = body.startHeight ?? Math.max(1, chain.tipHeight - DEFAULT_WINDOW); @@ -520,7 +525,7 @@ const server = http.createServer(async (req, res) => { const account = bip32.fromBase58(d.xpub, digibyte); for (const change of [0, 1] as const) { const chainNode = account.derive(change); - for (let i = 0; i < gapLimit; i++) { + for (let i = 0; i < (change === 0 ? gapLimit : gapInternal); i++) { const node = chainNode.derive(i); const pubkey = Buffer.from(node.publicKey); const spk = spkForPurpose(d.purpose, pubkey); diff --git a/packages/web-ui/src/ActivityList.tsx b/packages/web-ui/src/ActivityList.tsx index b35a3cf..3ceea11 100644 --- a/packages/web-ui/src/ActivityList.tsx +++ b/packages/web-ui/src/ActivityList.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { fmtSats } from './format.js'; +import { fmtSats8 } from './format.js'; import type { DashboardUtxo } from './useWalletBalance.js'; import type { BlockbookHistoryTx } from '@dgb-wallet/blockbook'; import type { HistoryStatus } from './useWalletHistory.js'; @@ -336,7 +336,7 @@ function shortenAddr(a: string): string { return a.length <= 20 ? a : `${a.slice(0, 10)}…${a.slice(-6)}`; } -const formatSats = fmtSats; +const formatSats = fmtSats8; function formatTime(ms: number): string { const d = new Date(ms); diff --git a/packages/web-ui/src/BalanceCard.tsx b/packages/web-ui/src/BalanceCard.tsx index f25ba3a..edec753 100644 --- a/packages/web-ui/src/BalanceCard.tsx +++ b/packages/web-ui/src/BalanceCard.tsx @@ -134,7 +134,7 @@ export function BalanceCard({
- {formatted} + 11 ? ' long' : ''}${formatted.length > 15 ? ' longer' : ''}`}>{formatted} DGB
diff --git a/packages/web-ui/src/Dashboard.tsx b/packages/web-ui/src/Dashboard.tsx index de8b80b..335ca05 100644 --- a/packages/web-ui/src/Dashboard.tsx +++ b/packages/web-ui/src/Dashboard.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { fmtSats } from './format.js'; +import { fmtSats8 } from './format.js'; import { seedFromPhrase, rootFromSeed, accountNode, addressNode, addressesForPubkey, importWifWithAddresses, @@ -412,4 +412,4 @@ function AddressesPanel({ ); } -const formatSats = fmtSats; +const formatSats = fmtSats8; diff --git a/packages/web-ui/src/ImportScreen.tsx b/packages/web-ui/src/ImportScreen.tsx index 94000c2..d630a55 100644 --- a/packages/web-ui/src/ImportScreen.tsx +++ b/packages/web-ui/src/ImportScreen.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { fmtSats } from './format.js'; +import { fmtSats8 } from './format.js'; import { PasswordInput } from './PasswordInput.js'; import { validateSeedPhrase, @@ -560,5 +560,5 @@ export function ImportScreen(): JSX.Element { } function fmt(sat: bigint | null): string { - return sat === null ? '—' : fmtSats(sat); + return sat === null ? '—' : fmtSats8(sat); } diff --git a/packages/web-ui/src/PathSearchPanel.tsx b/packages/web-ui/src/PathSearchPanel.tsx index f651056..a8cb49f 100644 --- a/packages/web-ui/src/PathSearchPanel.tsx +++ b/packages/web-ui/src/PathSearchPanel.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { fmtSats } from './format.js'; +import { fmtSats8 } from './format.js'; import { searchPaths, searchSingle, PATH_CANDIDATES, candidateKey, type PathHit, type PathCandidate } from './path-search.js'; import { useActiveSeed } from './ActiveSeed.js'; import { @@ -270,5 +270,5 @@ export function PathSearchPanel(): JSX.Element | null { } function fmt(sat: bigint | null): string { - return sat === null ? '—' : fmtSats(sat); + return sat === null ? '—' : fmtSats8(sat); } diff --git a/packages/web-ui/src/SendScreen.tsx b/packages/web-ui/src/SendScreen.tsx index 94d1b21..75486d0 100644 --- a/packages/web-ui/src/SendScreen.tsx +++ b/packages/web-ui/src/SendScreen.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react'; -import { fmtSats } from './format.js'; +import { GAP_EXTERNAL, GAP_INTERNAL } from './gap.js'; +import { fmtSats8 } from './format.js'; import { PinConfirmModal } from './PinConfirmModal.js'; import { useSessionAuth } from './session-auth.js'; import { @@ -34,7 +35,8 @@ const ECPair = ECPairFactory(ecc); // ~220 vB → ~0.00033 DGB. const FEE_RATE_SAT_PER_VB = 150; const MIN_RELAY_SAT_PER_VB = 100; -const GAP = 20; +// Key derivation depth per chain — must cover what the scans look at (gap.ts). +const GAP_FOR = (change: 0 | 1): number => (change === 0 ? GAP_EXTERNAL : GAP_INTERNAL); const ALL_PURPOSES: Array<44 | 49 | 84 | 86> = [44, 49, 84, 86]; // The dashboard scan covers account 0 only, so every UTXO we can be // handed lives in account 0 — deriving further accounts is wasted work. @@ -259,7 +261,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 ${fmtSats(inputSats)}, need ${fmtSats(amtSats + need)} (incl. ${fmtSats(need)} fee)`); + throw new Error(`Insufficient funds: have ${fmtSats8(inputSats)}, need ${fmtSats8(amtSats + need)} (incl. ${fmtSats8(need)} fee)`); } changeAmt = 0; feeSats = inputSats - amtSats; @@ -493,7 +495,7 @@ export function SendScreen({ {pinPrompt && ( { setPinPrompt(false); void doBroadcast(); }} onCancel={() => setPinPrompt(false)} /> @@ -507,15 +509,15 @@ export function SendScreen({ To {r.recipient} Amount - {fmtSats(r.amountSats)} DGB + {fmtSats8(r.amountSats)} DGB Fee - {fmtSats(r.feeSats)} DGB + {fmtSats8(r.feeSats)} DGB Total out - {fmtSats(r.amountSats + r.feeSats)} DGB + {fmtSats8(r.amountSats + r.feeSats)} DGB Change {status.changeAddress - ? <>{fmtSats(r.change.reduce((s, c) => s + c.value, 0))} DGB → {status.changeAddress} + ? <>{fmtSats8(r.change.reduce((s, c) => s + c.value, 0))} DGB → {status.changeAddress} : 'none'} Inputs @@ -636,12 +638,12 @@ export function SendScreen({
- 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. + Network fee: {FEE_RATE_SAT_PER_VB} sat/vB (DigiByte relays nothing under {MIN_RELAY_SAT_PER_VB}), about {fmtSats8(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: {fmtSats(availableSats)} DGB + {' '}Spendable now: {fmtSats8(availableSats)} DGB {scanStatus === 'loading' ? ' (scan still running)' : ''}.
@@ -684,7 +686,7 @@ async function buildFromSeed( const purpose: Purpose = isSegwit ? 84 : 44; const entries = new Map(); for (const change of [0, 1] as const) { - for (let i = 0; i < GAP; i++) { + for (let i = 0; i < GAP_FOR(change); i++) { const n = addressNode(node, change, i); if (!n.privateKey) continue; registerKey(entries, purpose, Buffer.from(n.publicKey), Buffer.from(n.privateKey)); @@ -721,7 +723,7 @@ async function buildFromSeed( for (let a = 0; a < ACCOUNTS; a++) { const acct = accountNode(root, purpose, a); for (const change of [0, 1] as const) { - for (let i = 0; i < GAP; i++) { + for (let i = 0; i < GAP_FOR(change); i++) { const node = addressNode(acct, change, i); if (!node.privateKey) continue; const pubkey = Buffer.from(node.publicKey); @@ -739,7 +741,7 @@ async function buildFromSeed( const rootDgb = rootFromSeed(seed, undefined, HMAC_DIGIBYTE_SEED); for (const legacy of [root.deriveHardened(0), root.derivePath("m/44'/0'/0'"), rootDgb.deriveHardened(0)]) { for (const change of [0, 1] as const) { - for (let i = 0; i < GAP; i++) { + for (let i = 0; i < GAP_FOR(change); i++) { const node = addressNode(legacy, change, i); if (!node.privateKey) continue; registerKey(entries, 44, Buffer.from(node.publicKey), Buffer.from(node.privateKey)); @@ -760,7 +762,7 @@ async function buildFromSeed( const acct = extra.path === 'm' ? base : base.derivePath(extra.path); const purpose = scriptToPurpose[extra.script]; for (const change of [0, 1] as const) { - for (let i = 0; i < GAP; i++) { + for (let i = 0; i < GAP_FOR(change); i++) { const node = addressNode(acct, change, i); if (!node.privateKey) continue; registerKey(entries, purpose, Buffer.from(node.publicKey), Buffer.from(node.privateKey)); diff --git a/packages/web-ui/src/backend-config.ts b/packages/web-ui/src/backend-config.ts index ad39e15..04a5d5d 100644 --- a/packages/web-ui/src/backend-config.ts +++ b/packages/web-ui/src/backend-config.ts @@ -75,7 +75,7 @@ const STORAGE_KEY = 'dgb-wallet:backend-config'; const DEFAULT_CONFIG: BackendConfig = { p2p: { scan: true, broadcast: true }, blockbook: { scan: true, broadcast: false }, - display: { priceFeed: false, autoWidenScan: true, autoRefreshSec: 60 }, + display: { priceFeed: true, autoWidenScan: true, autoRefreshSec: 60 }, privacy: { freshAddresses: false, changePolicy: 'same' }, security: { requirePinOnSend: false }, }; @@ -93,10 +93,13 @@ function readStored(): BackendConfig { // deliberate later opt-out sticks. const blockbook = { ...DEFAULT_CONFIG.blockbook, ...(parsed.blockbook ?? {}) }; if ((parsed.v ?? 1) < 2) blockbook.scan = true; + // v3 (2026-10): the price feed became the default too — same one-time flip. + const display = { ...DEFAULT_CONFIG.display, ...(parsed.display ?? {}) }; + if ((parsed.v ?? 1) < 3) display.priceFeed = true; return { p2p: { ...DEFAULT_CONFIG.p2p, ...(parsed.p2p ?? {}) }, blockbook, - display: { ...DEFAULT_CONFIG.display, ...(parsed.display ?? {}) }, + display, privacy: { ...DEFAULT_CONFIG.privacy, ...(parsed.privacy ?? {}) }, security: { ...DEFAULT_CONFIG.security, ...(parsed.security ?? {}) }, }; @@ -106,7 +109,7 @@ function readStored(): BackendConfig { } function writeStored(config: BackendConfig): void { - try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...config, v: 2 })); } catch { /* private mode */ } + try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...config, v: 3 })); } 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 index 172e3bf..c448062 100644 --- a/packages/web-ui/src/format.ts +++ b/packages/web-ui/src/format.ts @@ -16,6 +16,11 @@ export function fmtSats(sats: bigint | number, min = MIN_DECIMALS): string { return `${neg ? '-' : ''}${int}${frac ? '.' + frac : ''}`; } +/** Full 8-decimal form (history, details, send review): never trimmed. */ +export function fmtSats8(sats: bigint | number): string { + return fmtSats(sats, 8); +} + /** 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); diff --git a/packages/web-ui/src/gap.ts b/packages/web-ui/src/gap.ts new file mode 100644 index 0000000..ff4dc5a --- /dev/null +++ b/packages/web-ui/src/gap.ts @@ -0,0 +1,7 @@ +// Address-chain gap limits used by every scan and by key derivation for +// signing. 200 receive / 100 change — the same depth the DigiScope +// Android wallet probes during its "universal restore" — so a wallet +// that handed out many addresses over its life is still found. Signing +// derives the same depth so anything the scan finds can be spent. +export const GAP_EXTERNAL = 200; +export const GAP_INTERNAL = 100; diff --git a/packages/web-ui/src/styles.css b/packages/web-ui/src/styles.css index aa66bc8..b7398be 100644 --- a/packages/web-ui/src/styles.css +++ b/packages/web-ui/src/styles.css @@ -1582,8 +1582,13 @@ button.primary:disabled { opacity: 0.4; cursor: not-allowed; } color: var(--text); letter-spacing: -0.02em; line-height: 1; - overflow-wrap: anywhere; + /* Never wrap the number: a 14-character balance used to break into + two lines. Long values step down in size instead. */ + white-space: nowrap; + overflow-wrap: normal; } +.hero-value.long { font-size: clamp(26px, 3.3vw, 50px); } +.hero-value.longer { font-size: clamp(22px, 2.6vw, 40px); } .hero-unit { font-family: var(--mono); font-size: clamp(14px, 2vw, 18px); diff --git a/packages/web-ui/src/useWalletBalance.ts b/packages/web-ui/src/useWalletBalance.ts index 9b5f7df..f2e7477 100644 --- a/packages/web-ui/src/useWalletBalance.ts +++ b/packages/web-ui/src/useWalletBalance.ts @@ -9,6 +9,7 @@ // already opted into a working indexer. import { useEffect, useRef, useState } from 'react'; +import { GAP_EXTERNAL, GAP_INTERNAL } from './gap.js'; import { walletIdHash, cacheKey, readCache, writeCache, type CachedScan } from './scan-cache.js'; import { seedFromPhrase, seedFromElectrumPhrase, rootFromSeed, accountNode, accountXpub as toXpub, @@ -497,7 +498,7 @@ async function scanSeedViaP2p( ? 1 : Math.max(1, health.chainHeight - win.days * BLOCKS_PER_DAY); const scan = await p2pScan({ - descriptors, gapLimit: 20, startHeight, endHeight: health.chainHeight, + descriptors, gapLimit: GAP_EXTERNAL, gapInternal: GAP_INTERNAL, startHeight, endHeight: health.chainHeight, }, { onProgress }); // Per-purpose UTXO count from the flat list const byPurpose = new Map(); @@ -541,8 +542,8 @@ async function scanSeedViaBlockbook( // we can distinguish "Blockbook says this key genuinely has 0" from // "Blockbook broke on this request". const [summaryRes, utxosRes] = await Promise.allSettled([ - bb.xpub(key, 20), - bb.xpubUtxos(key, 20), + bb.xpub(key, GAP_EXTERNAL), + bb.xpubUtxos(key, GAP_EXTERNAL), ]); const summary = summaryRes.status === 'fulfilled' ? summaryRes.value : null; const utxos = utxosRes.status === 'fulfilled' ? utxosRes.value : []; diff --git a/packages/web-ui/src/useWalletHistory.ts b/packages/web-ui/src/useWalletHistory.ts index 98f32c1..c142574 100644 --- a/packages/web-ui/src/useWalletHistory.ts +++ b/packages/web-ui/src/useWalletHistory.ts @@ -15,6 +15,7 @@ // point the local placeholder is dropped in favour of the real row. import { useEffect, useRef, useState } from 'react'; +import { GAP_EXTERNAL, GAP_INTERNAL } from './gap.js'; import { seedFromPhrase, rootFromSeed, accountNode, accountXpub as toXpub, HMAC_DIGIBYTE_SEED, addressNode, addressesForPubkey, importWifWithAddresses, @@ -178,7 +179,7 @@ export function useWalletHistory(): { const { key, supported } = keyForPurpose(xpub, purpose); if (!supported) return []; try { - const page = await bb.xpubTxs(key, { pageSize: 100 }); + const page = await bb.xpubTxs(key, { gap: GAP_EXTERNAL, pageSize: 100 }); return page.transactions; } catch (e) { fetchProblems.push(`${purpose === 44 ? 'legacy/BIP44' : `BIP${purpose}`}: ${(e as Error).message.replace(/ at \.\.\/blockbook\S*/, '').slice(0, 90)}`); @@ -261,7 +262,7 @@ export function useWalletHistory(): { // How many indexes per branch we derive locally to judge ownership. // Matches the gap limit the scanners use, plus headroom for change. -const OWN_SCAN = 40; +const OWN_SCAN = GAP_EXTERNAL; // OR the isOwn flags of two Blockbook copies of the same tx. function mergeOwnFlags(a: BlockbookHistoryTx, b: BlockbookHistoryTx): BlockbookHistoryTx {