recover an imported wallet's coins with an nbxplorer utxo scan
registering an xpub only indexes it from that moment on, so an imported seed with history read as a confident zero: every call succeeded, the coins were simply absent. scantxoutset walks the node's current utxo set directly and finds them regardless of when the account was registered. runs all four script variants sequentially — the funds could be on any one — and surfaces progress through the existing SyncState channel so the balance says "scanning" rather than nothing. auto-fires on an imported mnemonic only; a generated seed has no history to look for. recovers spendable coins, not spent history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
// about which addresses are the wallet's, which is the kind of bug that loses coins rather than failing.
|
||||
|
||||
import type { SpendableUtxo } from './psbt';
|
||||
import type { AddressType, FeeEstimates, OnchainTx } from './types';
|
||||
import type { AddressType, FeeEstimates, OnchainTx, RescanState } from './types';
|
||||
|
||||
/** BIP44 chain index: 0 is the receive chain, 1 the internal (change) chain. */
|
||||
export type ChainIndex = 0 | 1;
|
||||
@@ -69,6 +69,15 @@ export type ScanContext = {
|
||||
derive: (type: AddressType, chain: ChainIndex, index: number) => AddressEntry;
|
||||
};
|
||||
|
||||
/**
|
||||
* A rescan in flight.
|
||||
*
|
||||
* `state` is the SAME object the source keeps and mutates as the scan progresses, so a caller that holds
|
||||
* it sees the counters move without asking again. `done` is how the wallet knows to re-read the chain:
|
||||
* a rescan that found coins has changed nothing until the snapshot behind it is rebuilt.
|
||||
*/
|
||||
export type RescanHandle = { state: RescanState; done: Promise<RescanState> };
|
||||
|
||||
/** One coherent read of the chain — balances, coins and history as of the same moment. */
|
||||
export type ScanResult = {
|
||||
tipHeight: number;
|
||||
@@ -103,6 +112,19 @@ export interface WalletChainSource {
|
||||
|
||||
/** Read the whole wallet off the chain. */
|
||||
scan(ctx: ScanContext): Promise<ScanResult>;
|
||||
|
||||
/**
|
||||
* Search the chain for this wallet's coins from scratch, rather than from whenever the upstream
|
||||
* started watching it. Returns immediately — a rescan takes minutes, and the caller is an HTTP route.
|
||||
*
|
||||
* Optional, and Esplora does not implement it — it has nothing to rescan, because a gap-limit walk
|
||||
* already asks about every address every time. This exists for an upstream that *indexes*, where a
|
||||
* newly registered account starts empty and stays empty until told to go and look.
|
||||
*/
|
||||
startRescan?(accounts: readonly ScanAccount[]): RescanHandle;
|
||||
|
||||
/** The rescan in flight, or the last one's outcome. Null when this source has never run one. */
|
||||
rescanState?(): RescanState | null;
|
||||
}
|
||||
|
||||
/** Bounded-concurrency map that preserves input order. Address-level sources fan out wide. */
|
||||
|
||||
Reference in New Issue
Block a user