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.
148 lines
5.3 KiB
TypeScript
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();
|