import { mapConcurrent } from "../concurrent.js"; import { getAddressReceived, getAddressSent, getAddressTxCount, } from "./stats.js"; import { findUsablePrefixBucket } from "./bucket.js"; import { isLocalClient } from "./local.js"; import { createAddressMetadata } from "./metadata.js"; const LOOKUP_CONCURRENCY = 8; /** * @typedef {import("../derive/index.js").AddressType} AddressType * @typedef {import("../derive/index.js").GeneratedAddress} GeneratedAddress */ /** * @typedef {import("./stats.js").AddressStats} AddressStats * @typedef {import("./bucket.js").AddrHashPrefixMatches} AddrHashPrefixMatches */ /** * @typedef {Object} WalletAddress * @property {number} index * @property {string} address * @property {GeneratedAddress["script"]} script * @property {GeneratedAddress["network"]} network * @property {AddressType} addrType * @property {number} balance * @property {number} received * @property {number} sent * @property {number} txCount * @property {number | undefined} typeIndex * @property {string[]} historyAddresses * @property {number} historyBucketSize */ /** * @typedef {Object} AddressClient * @property {string} domain * @property {(address: string, options?: { cache?: boolean }) => Promise} getAddress * @property {(addrType: AddressType, payload: Uint8Array, nibbles: number, options?: { cache?: boolean }) => Promise} getAddressPayloadHashPrefixMatches */ /** * @param {GeneratedAddress} generated * @param {number} historyBucketSize * @returns {WalletAddress} */ function createEmptyWalletAddress(generated, historyBucketSize = 0) { return { index: generated.index, address: generated.address, script: generated.script, network: generated.network, addrType: generated.addrType, balance: 0, received: 0, sent: 0, txCount: 0, typeIndex: undefined, historyAddresses: [], historyBucketSize, }; } /** * @param {GeneratedAddress} generated * @param {AddressStats} stats * @param {readonly string[]} historyAddresses * @param {number} historyBucketSize * @returns {WalletAddress} */ function createWalletAddress( generated, stats, historyAddresses, historyBucketSize, ) { const received = getAddressReceived(stats); const sent = getAddressSent(stats); return { index: generated.index, address: generated.address, script: generated.script, network: generated.network, addrType: generated.addrType, balance: received - sent, received, sent, txCount: getAddressTxCount(stats), typeIndex: stats.chainStats.typeIndex, historyAddresses: [...historyAddresses], historyBucketSize, }; } /** * @param {unknown} error */ function isNotFound(error) { return ( error instanceof Error && /** @type {{ status?: unknown }} */ (error).status === 404 ); } /** * @param {AddressClient} client * @param {GeneratedAddress} generated * @returns {Promise} */ async function fetchDirectWalletAddress(client, generated) { try { const stats = await client.getAddress(generated.address, { cache: false }); const historyAddresses = getAddressTxCount(stats) > 0 ? [generated.address] : []; return createWalletAddress(generated, stats, historyAddresses, 1); } catch (error) { if (isNotFound(error)) { return createEmptyWalletAddress(generated, 1); } throw error; } } /** * @param {AddressClient} client * @param {GeneratedAddress} generated * @param {ReturnType} metadata * @returns {Promise} */ async function fetchWalletAddress(client, generated, metadata) { const matches = await findUsablePrefixBucket(client, generated); if (!matches.addresses.includes(generated.address)) { return createEmptyWalletAddress(generated, matches.addresses.length); } await metadata.fetchAll(matches.addresses); const stats = await metadata.get(generated.address); const historyAddresses = []; for (const address of matches.addresses) { const bucketStats = await metadata.get(address); if (getAddressTxCount(bucketStats) > 0) { historyAddresses.push(address); } } return createWalletAddress( generated, stats, historyAddresses, matches.addresses.length, ); } /** * @param {AddressClient} client * @param {readonly GeneratedAddress[]} generated * @returns {Promise} */ export async function fetchWalletAddresses(client, generated) { if (isLocalClient(client)) { return mapConcurrent(generated, LOOKUP_CONCURRENCY, (address) => { return fetchDirectWalletAddress(client, address); }); } const metadata = createAddressMetadata(client); return mapConcurrent(generated, LOOKUP_CONCURRENCY, (address) => { return fetchWalletAddress(client, address, metadata); }); }