global: private xpub support part 2

This commit is contained in:
nym21
2026-06-17 11:25:42 +02:00
parent 0c7861071d
commit 43df9e098c
68 changed files with 1836 additions and 1653 deletions
+103
View File
@@ -0,0 +1,103 @@
import { fetchWalletAddresses } from "../lookup/index.js";
import { generateAddressesFromWalletSource } from "../derive/index.js";
export const GAP_LIMIT = 10;
const SCAN_BATCH_SIZE = GAP_LIMIT;
const MAX_SCANNED_ADDRESSES = 1_000;
/**
* @typedef {import("../derive/address.js").AddressScript} AddressScript
* @typedef {import("../derive/index.js").AddressType} AddressType
* @typedef {import("../lookup/index.js").WalletAddress} WalletAddress
*/
/**
* @typedef {Object} AddressClient
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddress
* @property {(addrType: AddressType, prefix: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressHashPrefixMatches
*/
/**
* @typedef {Object} ScanProgress
* @property {number} scannedCount
* @property {number} unusedInRow
*/
/**
* @typedef {Object} ScanOptions
* @property {AddressScript} script
* @property {readonly number[]} path
* @property {string} [branchId]
* @property {(progress: ScanProgress) => void} [onProgress]
*/
/**
* @typedef {Object} ScanResult
* @property {WalletAddress[]} addresses
* @property {number} scannedCount
* @property {number} gapLimit
* @property {boolean} maxed
*/
/**
* @param {WalletAddress} address
*/
function isUsedAddress(address) {
return address.received > 0 || address.sent > 0 || address.txCount > 0;
}
/**
* @param {AddressClient} client
* @param {string} source
* @param {ScanOptions} options
* @returns {Promise<ScanResult>}
*/
export async function scanBranch(client, source, options) {
const addresses = /** @type {WalletAddress[]} */ ([]);
let unusedInRow = 0;
let nextStart = 0;
while (
unusedInRow < GAP_LIMIT &&
addresses.length < MAX_SCANNED_ADDRESSES
) {
const count = Math.min(
SCAN_BATCH_SIZE,
GAP_LIMIT - unusedInRow,
MAX_SCANNED_ADDRESSES - addresses.length,
);
const generated = await generateAddressesFromWalletSource(source, {
start: nextStart,
count,
script: options.script,
path: options.path,
branchId: options.branchId,
});
const batch = /** @type {WalletAddress[]} */ (
await fetchWalletAddresses(client, generated)
);
for (const address of batch) {
addresses.push(address);
unusedInRow = isUsedAddress(address) ? 0 : unusedInRow + 1;
if (unusedInRow >= GAP_LIMIT) {
break;
}
}
nextStart += count;
options.onProgress?.({
scannedCount: addresses.length,
unusedInRow,
});
}
return {
addresses,
scannedCount: addresses.length,
gapLimit: GAP_LIMIT,
maxed: addresses.length >= MAX_SCANNED_ADDRESSES,
};
}
+164
View File
@@ -0,0 +1,164 @@
import {
scanBranch,
GAP_LIMIT,
} from "./branch.js";
import {
getOutputDescriptorBranchIds,
isOutputDescriptor,
} from "../derive/index.js";
const keyBranches = /** @type {const} */ ([
{ id: "receive", label: "Receive", path: [0] },
{ id: "change", label: "Change", path: [1] },
{ id: "direct", label: "Direct", path: [] },
]);
const descriptorBranches = /** @type {const} */ ([
{ id: "receive", label: "Receive", path: [] },
{ id: "change", label: "Change", path: [] },
]);
/**
* @typedef {(typeof keyBranches[number] | typeof descriptorBranches[number])} WalletBranch
* @typedef {WalletBranch["id"]} WalletBranchId
*/
/**
* @typedef {import("../derive/address.js").AddressScript} AddressScript
* @typedef {import("../derive/index.js").AddressType} AddressType
* @typedef {import("./branch.js").WalletAddress} WalletAddress
*/
/**
* @typedef {Object} AddressClient
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddress
* @property {(addrType: AddressType, prefix: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressHashPrefixMatches
*/
/**
* @typedef {WalletAddress & {
* branchId: WalletBranchId,
* branchLabel: string,
* }} ScannedAddress
*/
/**
* @typedef {Object} ScanProgress
* @property {WalletBranchId} branchId
* @property {string} branchLabel
* @property {number} scannedCount
* @property {number} unusedInRow
*/
/**
* @typedef {Object} ScanBranchesOptions
* @property {AddressScript} script
* @property {(progress: ScanProgress) => void} [onProgress]
*/
/**
* @typedef {Object} ScanBranchesResult
* @property {ScannedAddress[]} addresses
* @property {ScannedAddress | undefined} receiveAddress
* @property {number} gapLimit
* @property {boolean} maxed
*/
/**
* @param {WalletAddress} address
*/
function isUsedAddress(address) {
return address.received > 0 || address.sent > 0 || address.txCount > 0;
}
/**
* @param {ScannedAddress} a
* @param {ScannedAddress} b
*/
function compareWalletAddresses(a, b) {
if (a.typeIndex === undefined) return 1;
if (b.typeIndex === undefined) return -1;
return b.typeIndex - a.typeIndex || a.index - b.index;
}
/**
* @param {WalletAddress} address
* @param {WalletBranch} branch
* @returns {ScannedAddress}
*/
function addBranch(address, branch) {
return {
...address,
branchId: branch.id,
branchLabel: branch.label,
};
}
/**
* @param {string} source
*/
function getWalletBranches(source) {
if (!isOutputDescriptor(source)) return keyBranches;
const branchIds = new Set(getOutputDescriptorBranchIds(source));
const branches = descriptorBranches.filter((branch) => {
return branchIds.has(branch.id);
});
return branches.length ? branches : [descriptorBranches[0]];
}
/**
* @param {AddressClient} client
* @param {string} source
* @param {ScanBranchesOptions} options
* @returns {Promise<ScanBranchesResult>}
*/
export async function scanBranches(client, source, options) {
const addresses = /** @type {ScannedAddress[]} */ ([]);
const branches = getWalletBranches(source);
const receiveBranch =
branches.find((branch) => branch.id === "receive") ?? branches[0];
/** @type {ScannedAddress | undefined} */
let receiveAddress;
let maxed = false;
for (const branch of branches) {
const scan = await scanBranch(client, source, {
script: options.script,
path: branch.path,
branchId: branch.id,
onProgress(progress) {
options.onProgress?.({
branchId: branch.id,
branchLabel: branch.label,
scannedCount: progress.scannedCount,
unusedInRow: progress.unusedInRow,
});
},
});
for (const address of scan.addresses) {
const branchedAddress = addBranch(address, branch);
if (!isUsedAddress(address)) {
if (!receiveAddress && branch.id === receiveBranch.id) {
receiveAddress = branchedAddress;
}
continue;
}
addresses.push(branchedAddress);
}
maxed = maxed || scan.maxed;
}
return {
addresses: addresses.sort(compareWalletAddresses),
receiveAddress,
gapLimit: GAP_LIMIT,
maxed,
};
}
+58
View File
@@ -0,0 +1,58 @@
import { scanBranches } from "./branches.js";
/**
* @typedef {import("../derive/address.js").AddressScript} AddressScript
* @typedef {import("../derive/index.js").AddressType} AddressType
* @typedef {Awaited<ReturnType<typeof scanBranches>>["addresses"][number]} WalletAddress
*/
/**
* @typedef {Object} WalletScan
* @property {WalletAddress[]} addresses
* @property {WalletAddress | undefined} receiveAddress
* @property {number} btcUsdPrice
*/
/**
* @typedef {Object} WalletScanClient
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddress
* @property {(addrType: AddressType, prefix: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressHashPrefixMatches
* @property {(options?: { cache?: boolean }) => Promise<unknown>} getLivePrice
*/
/**
* @typedef {Object} WalletScanProgress
* @property {string} branchLabel
* @property {number} scannedCount
* @property {number} unusedInRow
*/
/**
* @param {Object} options
* @param {WalletScanClient} options.client
* @param {string} options.source
* @param {AddressScript} options.script
* @param {(progress: WalletScanProgress) => void} [options.onProgress]
* @returns {Promise<WalletScan>}
*/
export async function scanWalletAddresses({
client,
source,
script,
onProgress,
}) {
const scan = await scanBranches(client, source, {
script,
onProgress,
});
const addresses = /** @type {WalletAddress[]} */ (scan.addresses);
const btcUsdPrice = /** @type {number} */ (
await client.getLivePrice({ cache: false })
);
return {
addresses,
receiveAddress: scan.receiveAddress,
btcUsdPrice,
};
}