website_next: move hash logic to clients

This commit is contained in:
nym21
2026-07-01 17:04:29 +02:00
parent 5d83ee4d70
commit 153fcdf4e0
33 changed files with 1206 additions and 189 deletions
+3 -14
View File
@@ -1,24 +1,15 @@
import { rapidHashV3Prefix } from "./hash.js";
const MIN_PREFIX_NIBBLES = 4;
const MAX_PREFIX_NIBBLES = 16;
/**
* @typedef {import("../../modules/brk-client/index.js").AddrHashPrefixMatches} AddrHashPrefixMatches
* @typedef {import("../derive/index.js").AddressType} AddressType
* @typedef {import("../derive/index.js").GeneratedAddress} GeneratedAddress
*/
/**
* @typedef {Object} AddrHashPrefixMatches
* @property {AddressType} addrType
* @property {string} prefix
* @property {boolean} truncated
* @property {string[]} addresses
*/
/**
* @typedef {Object} AddressClient
* @property {(addrType: AddressType, prefix: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressHashPrefixMatches
* @property {(addrType: AddressType, payload: Uint8Array, nibbles: number, options?: { cache?: boolean }) => Promise<AddrHashPrefixMatches>} getAddressPayloadHashPrefixMatches
*/
/**
@@ -28,10 +19,8 @@ const MAX_PREFIX_NIBBLES = 16;
* @returns {Promise<AddrHashPrefixMatches>}
*/
async function fetchPrefixMatches(client, generated, nibbles) {
const prefix = rapidHashV3Prefix(generated.payload, nibbles);
return /** @type {AddrHashPrefixMatches} */ (
await client.getAddressHashPrefixMatches(generated.addrType, prefix, {
await client.getAddressPayloadHashPrefixMatches(generated.addrType, generated.payload, nibbles, {
cache: false,
})
);
-107
View File
@@ -1,107 +0,0 @@
const MASK_64 = 0xffffffffffffffffn;
const DEFAULT_SECRETS = /** @type {const} */ ([
0x2d358dccaa6c78a5n,
0x8bb84b93962eacc9n,
0x4b33a62ed433d4a3n,
0x4d5a2da51de1aa47n,
0xa0761d6478bd642fn,
0xe7037ed1a0b428dbn,
0x90ed1765281c388cn,
]);
const DEFAULT_SEED = rapidHashSeed(0n);
/**
* @param {bigint} value
*/
function u64(value) {
return value & MASK_64;
}
/**
* @param {bigint} left
* @param {bigint} right
*/
function rapidMix(left, right) {
const result = u64(left) * u64(right);
return u64(result) ^ u64(result >> 64n);
}
/**
* @param {bigint} left
* @param {bigint} right
* @returns {[bigint, bigint]}
*/
function rapidMum(left, right) {
const result = u64(left) * u64(right);
return [u64(result), u64(result >> 64n)];
}
/**
* @param {bigint} seed
*/
function rapidHashSeed(seed) {
return u64(seed ^ rapidMix(seed ^ DEFAULT_SECRETS[2], DEFAULT_SECRETS[1]));
}
/**
* @param {Uint8Array} bytes
* @param {number} offset
*/
function readU64(bytes, offset) {
return (
BigInt(bytes[offset]) |
(BigInt(bytes[offset + 1]) << 8n) |
(BigInt(bytes[offset + 2]) << 16n) |
(BigInt(bytes[offset + 3]) << 24n) |
(BigInt(bytes[offset + 4]) << 32n) |
(BigInt(bytes[offset + 5]) << 40n) |
(BigInt(bytes[offset + 6]) << 48n) |
(BigInt(bytes[offset + 7]) << 56n)
);
}
/**
* @param {Uint8Array} bytes
*/
function rapidHashV3(bytes) {
const length = bytes.length;
if (length <= 16) {
throw new Error("Expected more than 16 bytes");
}
if (length > 32) {
throw new Error("Expected at most 32 bytes");
}
let seed = rapidMix(
readU64(bytes, 0) ^ DEFAULT_SECRETS[2],
readU64(bytes, 8) ^ DEFAULT_SEED,
);
let a = readU64(bytes, length - 16) ^ BigInt(length);
let b = readU64(bytes, length - 8);
a ^= DEFAULT_SECRETS[1];
b ^= seed;
[a, b] = rapidMum(a, b);
return rapidMix(a ^ 0xaaaaaaaaaaaaaaaan, b ^ DEFAULT_SECRETS[1] ^ BigInt(length));
}
/**
* @param {Uint8Array} bytes
*/
function rapidHashV3Hex(bytes) {
return rapidHashV3(bytes).toString(16).padStart(16, "0");
}
/**
* @param {Uint8Array} bytes
* @param {number} nibbles
*/
export function rapidHashV3Prefix(bytes, nibbles) {
return rapidHashV3Hex(bytes).slice(0, nibbles);
}
+5 -8
View File
@@ -16,6 +16,7 @@ const LOOKUP_CONCURRENCY = 8;
/**
* @typedef {import("./stats.js").AddressStats} AddressStats
* @typedef {import("./bucket.js").AddrHashPrefixMatches} AddrHashPrefixMatches
*/
/**
@@ -37,8 +38,8 @@ const LOOKUP_CONCURRENCY = 8;
/**
* @typedef {Object} AddressClient
* @property {string} domain
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddress
* @property {(addrType: AddressType, prefix: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressHashPrefixMatches
* @property {(address: string, options?: { cache?: boolean }) => Promise<AddressStats>} getAddress
* @property {(addrType: AddressType, payload: Uint8Array, nibbles: number, options?: { cache?: boolean }) => Promise<AddrHashPrefixMatches>} getAddressPayloadHashPrefixMatches
*/
/**
@@ -115,9 +116,7 @@ async function fetchBucketMetadata(client, addresses, cache) {
if (!cache.has(address)) {
cache.set(
address,
client.getAddress(address, { cache: false }).then(
(stats) => /** @type {AddressStats} */ (stats),
),
client.getAddress(address, { cache: false }),
);
}
}
@@ -132,9 +131,7 @@ async function fetchBucketMetadata(client, addresses, cache) {
*/
async function fetchDirectWalletAddress(client, generated) {
try {
const stats = /** @type {AddressStats} */ (
await client.getAddress(generated.address, { cache: false })
);
const stats = await client.getAddress(generated.address, { cache: false });
const historyAddresses =
getAddressTxCount(stats) > 0 ? [generated.address] : [];
+1 -8
View File
@@ -8,17 +8,10 @@ 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").AddressClient} AddressClient
* @typedef {import("../lookup/index.js").WalletAddress} WalletAddress
*/
/**
* @typedef {Object} AddressClient
* @property {string} domain
* @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
+1 -8
View File
@@ -25,17 +25,10 @@ const descriptorBranches = /** @type {const} */ ([
/**
* @typedef {import("../derive/address.js").AddressScript} AddressScript
* @typedef {import("../derive/index.js").AddressType} AddressType
* @typedef {import("../lookup/index.js").AddressClient} AddressClient
* @typedef {import("./branch.js").WalletAddress} WalletAddress
*/
/**
* @typedef {Object} AddressClient
* @property {string} domain
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddress
* @property {(addrType: AddressType, prefix: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressHashPrefixMatches
*/
/**
* @typedef {WalletAddress & {
* branchId: WalletBranchId,
+5 -9
View File
@@ -5,7 +5,7 @@ import { addressScripts } from "../derive/script.js";
/**
* @typedef {import("../derive/address.js").AddressScript} AddressScript
* @typedef {import("../derive/index.js").AddressType} AddressType
* @typedef {import("../lookup/index.js").AddressClient} AddressClient
* @typedef {Awaited<ReturnType<typeof scanBranches>>["addresses"][number]} WalletAddress
* @typedef {Awaited<ReturnType<typeof scanBranches>>} ScriptScan
*/
@@ -18,11 +18,9 @@ import { addressScripts } from "../derive/script.js";
*/
/**
* @typedef {Object} WalletScanClient
* @property {string} domain
* @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 {AddressClient & {
* getLivePrice(options?: { cache?: boolean }): Promise<number>,
* }} WalletScanClient
*/
/**
@@ -135,9 +133,7 @@ export async function scanWalletAddresses({
const addresses = scans.flatMap((scan) => scan.addresses)
.sort(compareWalletAddresses);
const btcUsdPrice = /** @type {number} */ (
await client.getLivePrice({ cache: false })
);
const btcUsdPrice = await client.getLivePrice({ cache: false });
return {
addresses,
@@ -4,20 +4,21 @@ const HISTORY_CONCURRENCY = 4;
const MAX_SELECTED_ADDRESS_TXS = 100;
const historyByBucketKey =
/** @type {Map<string, Promise<Map<string, unknown[]>>>} */ (new Map());
/** @type {Map<string, Promise<Map<string, ApiTransaction[]>>>} */ (new Map());
/**
* @typedef {import("../../scan/index.js").WalletAddress} WalletAddress
* @typedef {import("./transaction.js").ApiTransaction} ApiTransaction
*/
/**
* @typedef {Object} AddressHistoryClient
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressTxs
* @property {(address: string, options?: { cache?: boolean }) => Promise<ApiTransaction[]>} getAddressTxs
*/
/**
* @typedef {Object} AddressHistory
* @property {unknown[]} transactions
* @property {ApiTransaction[]} transactions
*/
/**
@@ -30,16 +31,14 @@ function createBucketKey(addresses) {
/**
* @param {AddressHistoryClient} client
* @param {readonly string[]} addresses
* @returns {Promise<Map<string, unknown[]>>}
* @returns {Promise<Map<string, ApiTransaction[]>>}
*/
async function fetchBucketHistory(client, addresses) {
const entries = await mapConcurrent(
addresses,
HISTORY_CONCURRENCY,
async (address) => {
const transactions = /** @type {unknown[]} */ (
await client.getAddressTxs(address, { cache: false })
);
const transactions = await client.getAddressTxs(address, { cache: false });
return /** @type {const} */ ([address, transactions]);
},
+2 -1
View File
@@ -3,12 +3,13 @@ import { readWalletTransaction } from "./transaction.js";
/**
* @typedef {import("../../scan/index.js").WalletAddress} WalletAddress
* @typedef {import("./transaction.js").ApiTransaction} ApiTransaction
* @typedef {import("./transaction.js").WalletTransaction} WalletTransaction
*/
/**
* @typedef {Object} TransactionClient
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressTxs
* @property {(address: string, options?: { cache?: boolean }) => Promise<ApiTransaction[]>} getAddressTxs
*/
/**
@@ -1,5 +1,6 @@
/**
* @typedef {import("../../scan/index.js").WalletAddress} WalletAddress
* @typedef {Record<string, unknown>} ApiTransaction
*
* @typedef {Object} WalletTransactionAddress
* @property {WalletAddress} walletAddress
@@ -186,7 +187,7 @@ function getExternalOutputValue(transaction, walletAddressSet) {
}
/**
* @param {unknown} transaction
* @param {ApiTransaction} transaction
* @param {readonly WalletAddress[]} walletAddresses
* @returns {WalletTransaction}
*/
+17 -7
View File
@@ -3,20 +3,31 @@ import { mapConcurrent } from "../../concurrent.js";
const UTXO_CONCURRENCY = 4;
const utxosByBucketKey =
/** @type {Map<string, Promise<Map<string, unknown[]>>>} */ (new Map());
/** @type {Map<string, Promise<Map<string, ApiUtxo[]>>>} */ (new Map());
/**
* @typedef {import("../../scan/index.js").WalletAddress} WalletAddress
*/
/**
* @typedef {Object} ApiUtxoStatus
* @property {boolean} confirmed
*
* @typedef {Object} ApiUtxo
* @property {string} txid
* @property {number} vout
* @property {number} value
* @property {ApiUtxoStatus} status
*/
/**
* @typedef {Object} UtxoClient
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressUtxos
* @property {(address: string, options?: { cache?: boolean }) => Promise<ApiUtxo[]>} getAddressUtxos
*/
/**
* @typedef {Object} AddressUtxos
* @property {unknown[]} utxos
* @property {ApiUtxo[]} utxos
*/
/**
@@ -39,12 +50,11 @@ function createBucketKey(addresses) {
/**
* @param {UtxoClient} client
* @param {string} address
* @returns {Promise<ApiUtxo[]>}
*/
async function fetchAddressUtxos(client, address) {
try {
return /** @type {unknown[]} */ (
await client.getAddressUtxos(address, { cache: false })
);
return await client.getAddressUtxos(address, { cache: false });
} catch (error) {
if (isNotFound(error)) return [];
@@ -55,7 +65,7 @@ async function fetchAddressUtxos(client, address) {
/**
* @param {UtxoClient} client
* @param {readonly string[]} addresses
* @returns {Promise<Map<string, unknown[]>>}
* @returns {Promise<Map<string, ApiUtxo[]>>}
*/
async function fetchBucketUtxos(client, addresses) {
const entries = await mapConcurrent(