dgb-wallet/packages/electrum/scripts/scan-seed-demo.ts
Digibyte.X d50286a9a5 DigiByte.X Wallet — recovery-first web wallet monorepo
Packages: core (BIP39/32/44/49/84/86 derivation, WIF), keystore (PBKDF2 +
AES-GCM), psbt, p2p (BIP157/158 SPV daemon, Dandelion++ broadcast with
relay-echo verification), blockbook, rpc, electrum, net, bns, web-ui
(React/Vite), mobile (Expo). ops/ holds the nginx vhost, systemd unit and
push.sh deploy script for digibyte.x/web.
2026-09-20 20:16:52 +02:00

148 lines
5.3 KiB
TypeScript

// End-to-end recovery scan against real DigiByte mainnet via public
// Electrum-DGB servers. Takes a seed phrase, derives addresses at
// BIP44/49-modern/49-legacy/84/86, and reports live balance for each.
//
// This is the recovery-check-balance flow, real chain data, no local
// node required. Feed the resulting UTXOs into @dgb-wallet/psbt and
// you have a working end-to-end wallet.
import {
seedFromPhrase,
validateSeedPhrase,
rootFromSeed,
accountNode,
addressNode,
p2pkhAddress,
p2shP2wpkhAddress,
p2shP2wpkhAddressesBoth,
p2wpkhAddress,
p2trAddress,
} from '@dgb-wallet/core';
import { DEFAULT_SERVERS, connectAny, getBalances, getChainTip, listUnspent } from '../src/index.js';
const args = process.argv.slice(2);
const phrase =
args.length > 0
? args.join(' ').trim()
: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
if (!validateSeedPhrase(phrase)) {
console.error('Invalid BIP39 seed phrase.');
console.error('Usage: npm run demo -w @dgb-wallet/electrum -- "word1 word2 ... word12"');
console.error('(with no args, uses the public all-abandon BIP39 test vector)');
process.exit(1);
}
const ACCOUNTS = 1;
const ADDRESSES_PER_ACCOUNT = 5;
console.log('\n=== DigiByte Recovery Scan (real mainnet) ===\n');
console.log(`Seed: ${phrase.split(/\s+/).length}-word BIP39 (all-abandon test seed shown by default)`);
console.log(`Accounts: 0..${ACCOUNTS - 1}`);
console.log(`Depth: ${ADDRESSES_PER_ACCOUNT} receive addresses per account per path\n`);
console.log('Connecting to a public Electrum-DGB server...');
let client;
try {
client = await connectAny(DEFAULT_SERVERS, { timeoutMs: 10_000 });
} catch (e) {
console.error(`\nCould not reach any Electrum-DGB server:\n${(e as Error).message}\n`);
console.error('Servers are community-run and may be temporarily offline.');
console.error('Update DEFAULT_SERVERS in src/servers.ts if you know of live peers.');
process.exit(1);
}
console.log(` connected: ${client.server.host}:${client.server.port} (${client.server.protocol})\n`);
const tip = await getChainTip(client);
console.log(`Chain tip: height ${tip.height.toLocaleString()}\n`);
const seed = await seedFromPhrase(phrase);
const root = rootFromSeed(seed);
interface AddressJob {
path: string;
label: string;
address: string;
}
const jobs: AddressJob[] = [];
for (let a = 0; a < ACCOUNTS; a++) {
const bip44 = accountNode(root, 44, a);
const bip49 = accountNode(root, 49, a);
const bip84 = accountNode(root, 84, a);
const bip86 = accountNode(root, 86, a);
for (let i = 0; i < ADDRESSES_PER_ACCOUNT; i++) {
jobs.push({
path: `m/44'/20'/${a}'/0/${i}`,
label: 'BIP44',
address: p2pkhAddress(addressNode(bip44, 0, i)),
});
jobs.push({
path: `m/49'/20'/${a}'/0/${i}`,
label: 'BIP49-S',
address: p2shP2wpkhAddress(addressNode(bip49, 0, i)),
});
jobs.push({
path: `m/49'/20'/${a}'/0/${i}`,
label: 'BIP49-3',
address: p2shP2wpkhAddressesBoth(addressNode(bip49, 0, i)).legacy,
});
jobs.push({
path: `m/84'/20'/${a}'/0/${i}`,
label: 'BIP84',
address: p2wpkhAddress(addressNode(bip84, 0, i)),
});
jobs.push({
path: `m/86'/20'/${a}'/0/${i}`,
label: 'BIP86',
address: p2trAddress(addressNode(bip86, 0, i)),
});
}
}
console.log(`Querying ${jobs.length} addresses in parallel over one connection...\n`);
const balances = await getBalances(client, jobs.map((j) => j.address));
let totalConfirmed = 0;
let totalUnconfirmed = 0;
let anyWithFunds = false;
console.log('label path address confirmed unconfirmed');
console.log('─'.repeat(148));
for (const j of jobs) {
const b = balances.get(j.address)!;
totalConfirmed += b.confirmed;
totalUnconfirmed += b.unconfirmed;
const marker = b.confirmed > 0 || b.unconfirmed > 0 ? ' ★' : ' ';
if (b.confirmed > 0 || b.unconfirmed > 0) anyWithFunds = true;
console.log(
`${j.label.padEnd(9)} ${j.path.padEnd(24)} ${j.address.padEnd(66)} ${(b.confirmed / 1e8).toFixed(8).padStart(16)} DGB ${(b.unconfirmed / 1e8).toFixed(8).padStart(16)} DGB${marker}`,
);
}
console.log('─'.repeat(148));
console.log(` ${(totalConfirmed / 1e8).toFixed(8).padStart(16)} DGB ${(totalUnconfirmed / 1e8).toFixed(8).padStart(16)} DGB`);
console.log();
if (anyWithFunds) {
console.log('★ Addresses with funds — fetching UTXOs:\n');
for (const j of jobs) {
const b = balances.get(j.address)!;
if (b.confirmed === 0 && b.unconfirmed === 0) continue;
const utxos = await listUnspent(client, j.address);
for (const u of utxos) {
console.log(` ${j.address}`);
console.log(` ${u.txid}:${u.vout} value=${(u.value / 1e8).toFixed(8)} DGB height=${u.height}`);
}
}
console.log();
console.log('These UTXOs can be fed to @dgb-wallet/psbt to construct a spend.');
} else {
console.log('No funds at any scanned address (expected for the all-abandon test seed).');
console.log('Run with your own seed: npm run demo -w @dgb-wallet/electrum -- "your words here"');
console.log('(Reminder: never paste a real seed protecting funds anywhere but the wallet itself.)');
}
console.log();
client.close();