mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-23 00:48:11 -07:00
global: private xpub support part 2
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user