mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-01 14:29:01 -07:00
website_next: move hash logic to clients
This commit is contained in:
Generated
+3
-2
@@ -410,6 +410,7 @@ version = "0.3.6"
|
||||
dependencies = [
|
||||
"brk_cohort",
|
||||
"brk_types",
|
||||
"rapidhash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ureq",
|
||||
@@ -2458,9 +2459,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rapidhash"
|
||||
version = "4.4.1"
|
||||
version = "4.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59"
|
||||
checksum = "32b266a82f4aa99bb5c25e28d11cc44ace63d91adbcbcee4d323e2ae3d49ef37"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
@@ -71,6 +71,7 @@ owo-colors = "4.3.0"
|
||||
parking_lot = "0.12.5"
|
||||
pco = "1.0.2"
|
||||
rayon = "1.12.0"
|
||||
rapidhash = "4.4.2"
|
||||
rustc-hash = "2.1.2"
|
||||
schemars = { version = "1.2.1", features = ["indexmap2"] }
|
||||
serde = "1.0.228"
|
||||
|
||||
@@ -688,6 +688,160 @@ const _p = (prefix, acc) => acc ? `${{prefix}}_${{acc}}` : prefix;
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
output.push_str(r##"
|
||||
const _MASK_64 = 0xffffffffffffffffn;
|
||||
const _RAPIDHASH_SECRETS = /** @type {const} */ ([
|
||||
0x2d358dccaa6c78a5n,
|
||||
0x8bb84b93962eacc9n,
|
||||
0x4b33a62ed433d4a3n,
|
||||
0x4d5a2da51de1aa47n,
|
||||
0xa0761d6478bd642fn,
|
||||
0xe7037ed1a0b428dbn,
|
||||
0x90ed1765281c388cn,
|
||||
]);
|
||||
const _RAPIDHASH_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 ^ _RAPIDHASH_SECRETS[2], _RAPIDHASH_SECRETS[1]));
|
||||
}
|
||||
|
||||
/** @param {Uint8Array} bytes @param {number} offset */
|
||||
function _readU32(bytes, offset) {
|
||||
return (
|
||||
BigInt(bytes[offset]) |
|
||||
(BigInt(bytes[offset + 1]) << 8n) |
|
||||
(BigInt(bytes[offset + 2]) << 16n) |
|
||||
(BigInt(bytes[offset + 3]) << 24n)
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {Uint8Array} bytes @param {number} offset */
|
||||
function _readU64(bytes, offset) {
|
||||
return _readU32(bytes, offset) | (_readU32(bytes, offset + 4) << 32n);
|
||||
}
|
||||
|
||||
/** @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload */
|
||||
function _asUint8Array(payload) {
|
||||
if (payload instanceof Uint8Array) return payload;
|
||||
if (payload instanceof ArrayBuffer) return new Uint8Array(payload);
|
||||
if (ArrayBuffer.isView(payload)) return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
|
||||
if (Array.isArray(payload)) return new Uint8Array(payload);
|
||||
throw new Error("Expected address payload bytes");
|
||||
}
|
||||
|
||||
/** @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload */
|
||||
function _rapidHashV3(payload) {
|
||||
const bytes = _asUint8Array(payload);
|
||||
const length = bytes.length;
|
||||
if (length === 0) throw new Error("Expected a non-empty address payload");
|
||||
if (length > 65) throw new Error("Expected at most 65 address payload bytes");
|
||||
|
||||
let seed = _RAPIDHASH_SEED;
|
||||
let a = 0n;
|
||||
let b = 0n;
|
||||
let remainder;
|
||||
|
||||
if (length <= 16) {
|
||||
if (length >= 4) {
|
||||
seed ^= BigInt(length);
|
||||
if (length >= 8) {
|
||||
a ^= _readU64(bytes, 0);
|
||||
b ^= _readU64(bytes, length - 8);
|
||||
} else {
|
||||
a ^= _readU32(bytes, 0);
|
||||
b ^= _readU32(bytes, length - 4);
|
||||
}
|
||||
} else if (length > 0) {
|
||||
a ^= (BigInt(bytes[0]) << 45n) | BigInt(bytes[length - 1]);
|
||||
b ^= BigInt(bytes[length >> 1]);
|
||||
}
|
||||
remainder = BigInt(length);
|
||||
} else {
|
||||
seed = _rapidMix(_readU64(bytes, 0) ^ _RAPIDHASH_SECRETS[2], _readU64(bytes, 8) ^ seed);
|
||||
if (length > 32) {
|
||||
seed = _rapidMix(_readU64(bytes, 16) ^ _RAPIDHASH_SECRETS[2], _readU64(bytes, 24) ^ seed);
|
||||
if (length > 48) {
|
||||
seed = _rapidMix(_readU64(bytes, 32) ^ _RAPIDHASH_SECRETS[1], _readU64(bytes, 40) ^ seed);
|
||||
if (length > 64) {
|
||||
seed = _rapidMix(_readU64(bytes, 48) ^ _RAPIDHASH_SECRETS[1], _readU64(bytes, 56) ^ seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
remainder = BigInt(length);
|
||||
a ^= _readU64(bytes, length - 16) ^ remainder;
|
||||
b ^= _readU64(bytes, length - 8);
|
||||
}
|
||||
|
||||
a ^= _RAPIDHASH_SECRETS[1];
|
||||
b ^= seed;
|
||||
[a, b] = _rapidMum(a, b);
|
||||
return _rapidMix(a ^ 0xaaaaaaaaaaaaaaaan, b ^ _RAPIDHASH_SECRETS[1] ^ remainder);
|
||||
}
|
||||
|
||||
/** @param {number} nibbles */
|
||||
function _validateHashPrefixNibbles(nibbles) {
|
||||
if (!Number.isInteger(nibbles) || nibbles < 1 || nibbles > 16) {
|
||||
throw new Error("Expected hash-prefix length from 1 to 16 hex nibbles");
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {OutputType} addrType @returns {number[]} */
|
||||
function _addressPayloadLengths(addrType) {
|
||||
switch (addrType) {
|
||||
case "p2a": return [2];
|
||||
case "p2pk": return [33, 65];
|
||||
case "p2pkh":
|
||||
case "p2sh":
|
||||
case "v0_p2wpkh": return [20];
|
||||
case "v0_p2wsh":
|
||||
case "v1_p2tr": return [32];
|
||||
default:
|
||||
throw new Error(`Unsupported address type for address payload hash-prefix: ${addrType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OutputType} addrType
|
||||
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload
|
||||
*/
|
||||
function _validateAddressPayloadForType(addrType, payload) {
|
||||
const length = _asUint8Array(payload).length;
|
||||
const expected = _addressPayloadLengths(addrType);
|
||||
if (!expected.includes(length)) {
|
||||
throw new Error(`Expected ${addrType} address payload length ${expected.join(" or ")} bytes`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`.
|
||||
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload - Raw address payload bytes
|
||||
* @param {number} nibbles - Prefix length from 1 to 16 hex nibbles
|
||||
* @returns {string}
|
||||
*/
|
||||
function addressPayloadHashPrefix(payload, nibbles) {
|
||||
_validateHashPrefixNibbles(nibbles);
|
||||
return _rapidHashV3(payload).toString(16).padStart(16, "0").slice(0, nibbles);
|
||||
}
|
||||
|
||||
"##);
|
||||
}
|
||||
|
||||
/// Generate static constants for the BrkClient class.
|
||||
|
||||
@@ -111,6 +111,32 @@ pub fn generate_main_client(
|
||||
writeln!(output, " this.series = this._buildTree();").unwrap();
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
|
||||
output.push_str(r##" /**
|
||||
* Compute the RapidHash v3 hash-prefix for raw address payload bytes.
|
||||
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload
|
||||
* @param {number} nibbles
|
||||
* @returns {string}
|
||||
*/
|
||||
static addressPayloadHashPrefix(payload, nibbles) {
|
||||
return addressPayloadHashPrefix(payload, nibbles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch address hash-prefix matches from raw address payload bytes.
|
||||
* @param {OutputType} addrType
|
||||
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload - Raw payload bytes matching addrType length
|
||||
* @param {number} nibbles
|
||||
* @param {{ signal?: AbortSignal, onValue?: (value: AddrHashPrefixMatches) => void, cache?: boolean }} [options]
|
||||
* @returns {Promise<AddrHashPrefixMatches>}
|
||||
*/
|
||||
getAddressPayloadHashPrefixMatches(addrType, payload, nibbles, options = {}) {
|
||||
_validateAddressPayloadForType(addrType, payload);
|
||||
const prefix = addressPayloadHashPrefix(payload, nibbles);
|
||||
return this.getAddressHashPrefixMatches(addrType, prefix, options);
|
||||
}
|
||||
|
||||
"##);
|
||||
|
||||
writeln!(output, " /**").unwrap();
|
||||
writeln!(output, " * @private").unwrap();
|
||||
writeln!(output, " * @returns {{SeriesTree}}").unwrap();
|
||||
@@ -161,7 +187,11 @@ pub fn generate_main_client(
|
||||
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
writeln!(output, "export {{ BrkClient, BrkError }};").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"export {{ BrkClient, BrkError, addressPayloadHashPrefix }};"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@@ -84,6 +84,23 @@ pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
|
||||
.unwrap();
|
||||
writeln!(output, " return _date_to_index(index, d)").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
|
||||
output.push_str(r#" @staticmethod
|
||||
def address_payload_hash_prefix(payload: Union[bytes, bytearray, memoryview], nibbles: int) -> str:
|
||||
"""Compute the RapidHash v3 hash-prefix for raw address payload bytes."""
|
||||
return address_payload_hash_prefix(payload, nibbles)
|
||||
|
||||
def get_address_payload_hash_prefix_matches(
|
||||
self,
|
||||
addr_type: OutputType,
|
||||
payload: Union[bytes, bytearray, memoryview],
|
||||
nibbles: int,
|
||||
) -> AddrHashPrefixMatches:
|
||||
"""Fetch address hash-prefix matches from raw payload bytes matching addr_type length."""
|
||||
_validate_address_payload_for_type(addr_type, payload)
|
||||
return self.get_address_hash_prefix_matches(addr_type, address_payload_hash_prefix(payload, nibbles))
|
||||
|
||||
"#);
|
||||
// Generate API methods
|
||||
generate_api_methods(output, endpoints);
|
||||
}
|
||||
|
||||
@@ -147,6 +147,125 @@ def _p(prefix: str, acc: str) -> str:
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
output.push_str(r#"
|
||||
_MASK_64 = (1 << 64) - 1
|
||||
_RAPIDHASH_SECRETS = (
|
||||
0x2d358dccaa6c78a5,
|
||||
0x8bb84b93962eacc9,
|
||||
0x4b33a62ed433d4a3,
|
||||
0x4d5a2da51de1aa47,
|
||||
0xa0761d6478bd642f,
|
||||
0xe7037ed1a0b428db,
|
||||
0x90ed1765281c388c,
|
||||
)
|
||||
_RAPIDHASH_SEED = 0
|
||||
|
||||
|
||||
def _u64(value: int) -> int:
|
||||
return value & _MASK_64
|
||||
|
||||
|
||||
def _rapid_mix(left: int, right: int) -> int:
|
||||
result = _u64(left) * _u64(right)
|
||||
return _u64(result) ^ _u64(result >> 64)
|
||||
|
||||
|
||||
def _rapid_mum(left: int, right: int) -> Tuple[int, int]:
|
||||
result = _u64(left) * _u64(right)
|
||||
return _u64(result), _u64(result >> 64)
|
||||
|
||||
|
||||
def _rapid_hash_seed(seed: int) -> int:
|
||||
return _u64(seed ^ _rapid_mix(seed ^ _RAPIDHASH_SECRETS[2], _RAPIDHASH_SECRETS[1]))
|
||||
|
||||
|
||||
_RAPIDHASH_SEED = _rapid_hash_seed(0)
|
||||
|
||||
|
||||
def _read_u32(data: bytes, offset: int) -> int:
|
||||
return int.from_bytes(data[offset:offset + 4], "little")
|
||||
|
||||
|
||||
def _read_u64(data: bytes, offset: int) -> int:
|
||||
return int.from_bytes(data[offset:offset + 8], "little")
|
||||
|
||||
|
||||
def _rapid_hash_v3(payload: Union[bytes, bytearray, memoryview]) -> int:
|
||||
data = bytes(payload)
|
||||
length = len(data)
|
||||
if length == 0:
|
||||
raise ValueError("Expected a non-empty address payload")
|
||||
if length > 65:
|
||||
raise ValueError("Expected at most 65 address payload bytes")
|
||||
|
||||
seed = _RAPIDHASH_SEED
|
||||
a = 0
|
||||
b = 0
|
||||
|
||||
if length <= 16:
|
||||
if length >= 4:
|
||||
seed ^= length
|
||||
if length >= 8:
|
||||
a ^= _read_u64(data, 0)
|
||||
b ^= _read_u64(data, length - 8)
|
||||
else:
|
||||
a ^= _read_u32(data, 0)
|
||||
b ^= _read_u32(data, length - 4)
|
||||
elif length > 0:
|
||||
a ^= (data[0] << 45) | data[length - 1]
|
||||
b ^= data[length >> 1]
|
||||
remainder = length
|
||||
else:
|
||||
if length > 16:
|
||||
seed = _rapid_mix(_read_u64(data, 0) ^ _RAPIDHASH_SECRETS[2], _read_u64(data, 8) ^ seed)
|
||||
if length > 32:
|
||||
seed = _rapid_mix(_read_u64(data, 16) ^ _RAPIDHASH_SECRETS[2], _read_u64(data, 24) ^ seed)
|
||||
if length > 48:
|
||||
seed = _rapid_mix(_read_u64(data, 32) ^ _RAPIDHASH_SECRETS[1], _read_u64(data, 40) ^ seed)
|
||||
if length > 64:
|
||||
seed = _rapid_mix(_read_u64(data, 48) ^ _RAPIDHASH_SECRETS[1], _read_u64(data, 56) ^ seed)
|
||||
remainder = length
|
||||
a ^= _read_u64(data, length - 16) ^ remainder
|
||||
b ^= _read_u64(data, length - 8)
|
||||
|
||||
a ^= _RAPIDHASH_SECRETS[1]
|
||||
b ^= seed
|
||||
a, b = _rapid_mum(a, b)
|
||||
return _rapid_mix(a ^ 0xaaaaaaaaaaaaaaaa, b ^ _RAPIDHASH_SECRETS[1] ^ remainder)
|
||||
|
||||
|
||||
def _validate_hash_prefix_nibbles(nibbles: int) -> None:
|
||||
if isinstance(nibbles, bool) or not isinstance(nibbles, int) or nibbles < 1 or nibbles > 16:
|
||||
raise ValueError("Expected hash-prefix length from 1 to 16 hex nibbles")
|
||||
|
||||
|
||||
def _address_payload_lengths(addr_type: OutputType) -> Tuple[int, ...]:
|
||||
if addr_type == "p2a":
|
||||
return (2,)
|
||||
if addr_type == "p2pk":
|
||||
return (33, 65)
|
||||
if addr_type in ("p2pkh", "p2sh", "v0_p2wpkh"):
|
||||
return (20,)
|
||||
if addr_type in ("v0_p2wsh", "v1_p2tr"):
|
||||
return (32,)
|
||||
raise ValueError(f"Unsupported address type for address payload hash-prefix: {addr_type}")
|
||||
|
||||
|
||||
def _validate_address_payload_for_type(addr_type: OutputType, payload: Union[bytes, bytearray, memoryview]) -> None:
|
||||
length = len(bytes(payload))
|
||||
expected = _address_payload_lengths(addr_type)
|
||||
if length not in expected:
|
||||
joined = " or ".join(str(value) for value in expected)
|
||||
raise ValueError(f"Expected {addr_type} address payload length {joined} bytes")
|
||||
|
||||
|
||||
def address_payload_hash_prefix(payload: Union[bytes, bytearray, memoryview], nibbles: int) -> str:
|
||||
"""Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`."""
|
||||
_validate_hash_prefix_nibbles(nibbles)
|
||||
return f"{_rapid_hash_v3(payload):016x}"[:nibbles]
|
||||
|
||||
|
||||
"#);
|
||||
}
|
||||
|
||||
/// Generate the SeriesData and SeriesEndpoint classes
|
||||
|
||||
@@ -76,6 +76,36 @@ impl BrkClient {{
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
output.push_str(r#" /// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
|
||||
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {
|
||||
decode_address_payload(address)
|
||||
}
|
||||
|
||||
/// Compute the RapidHash v3 hash-prefix for raw address payload bytes.
|
||||
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {
|
||||
address_payload_hash_prefix(payload, nibbles)
|
||||
}
|
||||
|
||||
/// Decode a mainnet Bitcoin address and compute its hash prefix.
|
||||
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {
|
||||
address_hash_prefix(address, nibbles)
|
||||
}
|
||||
|
||||
/// Fetch address hash-prefix matches from raw payload bytes matching `addr_type` length.
|
||||
pub fn get_address_payload_hash_prefix_matches(&self, addr_type: OutputType, payload: &[u8], nibbles: usize) -> Result<AddrHashPrefixMatches> {
|
||||
validate_address_payload_for_type(addr_type, payload)?;
|
||||
let prefix = address_payload_hash_prefix(payload, nibbles)?;
|
||||
self.get_address_hash_prefix_matches(addr_type, &prefix)
|
||||
}
|
||||
|
||||
/// Fetch address hash-prefix matches for a mainnet Bitcoin address.
|
||||
pub fn get_address_hash_prefix_matches_for_address(&self, address: &str, nibbles: usize) -> Result<AddrHashPrefixMatches> {
|
||||
let hashed = address_hash_prefix(address, nibbles)?;
|
||||
self.get_address_hash_prefix_matches(hashed.addr_type, &hashed.prefix)
|
||||
}
|
||||
|
||||
"#);
|
||||
|
||||
generate_api_methods(output, endpoints);
|
||||
|
||||
writeln!(output, "}}").unwrap();
|
||||
@@ -118,6 +148,23 @@ fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
|
||||
"get_text"
|
||||
};
|
||||
|
||||
if endpoint.path == "/api/address/hash-prefix/{addr_type}/{prefix}" {
|
||||
writeln!(
|
||||
output,
|
||||
" let addr_type = address_payload_type_path(addr_type)?;"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.{}(&format!(\"{}\"{}))",
|
||||
fetch_method, path, index_arg
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(
|
||||
output,
|
||||
|
||||
@@ -11,7 +11,8 @@ use crate::{
|
||||
pub fn generate_imports(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"use std::sync::Arc;
|
||||
r#"use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::ops::{{Bound, RangeBounds}};
|
||||
use serde::de::DeserializeOwned;
|
||||
pub use brk_cohort::*;
|
||||
@@ -43,6 +44,97 @@ impl std::error::Error for BrkError {{}}
|
||||
/// Result type for BRK client operations.
|
||||
pub type Result<T> = std::result::Result<T, BrkError>;
|
||||
|
||||
/// BRK address type and raw payload bytes used by the hash-prefix index.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AddressPayload {{
|
||||
pub addr_type: OutputType,
|
||||
pub payload: Vec<u8>,
|
||||
}}
|
||||
|
||||
/// BRK address type and leading hex nibbles of the address-payload hash.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AddressHashPrefix {{
|
||||
pub addr_type: OutputType,
|
||||
pub prefix: String,
|
||||
}}
|
||||
|
||||
/// Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{{addr_type}}/{{prefix}}`.
|
||||
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {{
|
||||
if payload.is_empty() {{
|
||||
return Err(BrkError {{ message: "Expected a non-empty address payload".to_string() }});
|
||||
}}
|
||||
if payload.len() > 65 {{
|
||||
return Err(BrkError {{ message: "Expected at most 65 address payload bytes".to_string() }});
|
||||
}}
|
||||
if !(1..=16).contains(&nibbles) {{
|
||||
return Err(BrkError {{ message: "Expected hash-prefix length from 1 to 16 hex nibbles".to_string() }});
|
||||
}}
|
||||
Ok(format!("{{:016x}}", rapidhash::v3::rapidhash_v3(payload))[..nibbles].to_string())
|
||||
}}
|
||||
|
||||
fn validate_address_payload_for_type(addr_type: OutputType, payload: &[u8]) -> Result<()> {{
|
||||
let expected: &[usize] = match addr_type {{
|
||||
OutputType::P2A => &[2],
|
||||
OutputType::P2PK33 => &[33],
|
||||
OutputType::P2PK65 => &[65],
|
||||
OutputType::P2PKH | OutputType::P2SH | OutputType::P2WPKH => &[20],
|
||||
OutputType::P2WSH | OutputType::P2TR => &[32],
|
||||
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {{
|
||||
return Err(BrkError {{ message: format!("Unsupported address type for address payload hash-prefix: {{addr_type:?}}") }});
|
||||
}},
|
||||
}};
|
||||
let addr_type = address_payload_type_path(addr_type)?;
|
||||
|
||||
if !expected.contains(&payload.len()) {{
|
||||
let joined = expected
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" or ");
|
||||
return Err(BrkError {{ message: format!("Expected {{addr_type}} address payload length {{joined}} bytes") }});
|
||||
}}
|
||||
|
||||
Ok(())
|
||||
}}
|
||||
|
||||
fn address_payload_type_path(addr_type: OutputType) -> Result<&'static str> {{
|
||||
match addr_type {{
|
||||
OutputType::P2A => Ok("p2a"),
|
||||
OutputType::P2PK33 | OutputType::P2PK65 => Ok("p2pk"),
|
||||
OutputType::P2PKH => Ok("p2pkh"),
|
||||
OutputType::P2SH => Ok("p2sh"),
|
||||
OutputType::P2WPKH => Ok("v0_p2wpkh"),
|
||||
OutputType::P2WSH => Ok("v0_p2wsh"),
|
||||
OutputType::P2TR => Ok("v1_p2tr"),
|
||||
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {{
|
||||
Err(BrkError {{ message: format!("Unsupported address type for address payload hash-prefix: {{addr_type:?}}") }})
|
||||
}},
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
|
||||
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {{
|
||||
if address.is_empty() {{
|
||||
return Err(BrkError {{ message: "Expected an address string".to_string() }});
|
||||
}}
|
||||
let addr_bytes = AddrBytes::from_str(address).map_err(|e| BrkError {{ message: e.to_string() }})?;
|
||||
let addr_type = OutputType::from(&addr_bytes);
|
||||
|
||||
Ok(AddressPayload {{
|
||||
addr_type,
|
||||
payload: addr_bytes.as_slice().to_vec(),
|
||||
}})
|
||||
}}
|
||||
|
||||
/// Decode a mainnet Bitcoin address and compute its hash prefix.
|
||||
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {{
|
||||
let decoded = decode_address_payload(address)?;
|
||||
Ok(AddressHashPrefix {{
|
||||
addr_type: decoded.addr_type,
|
||||
prefix: address_payload_hash_prefix(&decoded.payload, nibbles)?,
|
||||
}})
|
||||
}}
|
||||
|
||||
/// Options for configuring the BRK client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrkClientOptions {{
|
||||
|
||||
@@ -13,6 +13,7 @@ exclude = ["examples/"]
|
||||
[dependencies]
|
||||
brk_cohort = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
rapidhash = { workspace = true }
|
||||
ureq = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#![allow(clippy::useless_format)]
|
||||
#![allow(clippy::unnecessary_to_owned)]
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::ops::{Bound, RangeBounds};
|
||||
use serde::de::DeserializeOwned;
|
||||
@@ -32,6 +33,97 @@ impl std::error::Error for BrkError {}
|
||||
/// Result type for BRK client operations.
|
||||
pub type Result<T> = std::result::Result<T, BrkError>;
|
||||
|
||||
/// BRK address type and raw payload bytes used by the hash-prefix index.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AddressPayload {
|
||||
pub addr_type: OutputType,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
/// BRK address type and leading hex nibbles of the address-payload hash.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AddressHashPrefix {
|
||||
pub addr_type: OutputType,
|
||||
pub prefix: String,
|
||||
}
|
||||
|
||||
/// Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`.
|
||||
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {
|
||||
if payload.is_empty() {
|
||||
return Err(BrkError { message: "Expected a non-empty address payload".to_string() });
|
||||
}
|
||||
if payload.len() > 65 {
|
||||
return Err(BrkError { message: "Expected at most 65 address payload bytes".to_string() });
|
||||
}
|
||||
if !(1..=16).contains(&nibbles) {
|
||||
return Err(BrkError { message: "Expected hash-prefix length from 1 to 16 hex nibbles".to_string() });
|
||||
}
|
||||
Ok(format!("{:016x}", rapidhash::v3::rapidhash_v3(payload))[..nibbles].to_string())
|
||||
}
|
||||
|
||||
fn validate_address_payload_for_type(addr_type: OutputType, payload: &[u8]) -> Result<()> {
|
||||
let expected: &[usize] = match addr_type {
|
||||
OutputType::P2A => &[2],
|
||||
OutputType::P2PK33 => &[33],
|
||||
OutputType::P2PK65 => &[65],
|
||||
OutputType::P2PKH | OutputType::P2SH | OutputType::P2WPKH => &[20],
|
||||
OutputType::P2WSH | OutputType::P2TR => &[32],
|
||||
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {
|
||||
return Err(BrkError { message: format!("Unsupported address type for address payload hash-prefix: {addr_type:?}") });
|
||||
},
|
||||
};
|
||||
let addr_type = address_payload_type_path(addr_type)?;
|
||||
|
||||
if !expected.contains(&payload.len()) {
|
||||
let joined = expected
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" or ");
|
||||
return Err(BrkError { message: format!("Expected {addr_type} address payload length {joined} bytes") });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn address_payload_type_path(addr_type: OutputType) -> Result<&'static str> {
|
||||
match addr_type {
|
||||
OutputType::P2A => Ok("p2a"),
|
||||
OutputType::P2PK33 | OutputType::P2PK65 => Ok("p2pk"),
|
||||
OutputType::P2PKH => Ok("p2pkh"),
|
||||
OutputType::P2SH => Ok("p2sh"),
|
||||
OutputType::P2WPKH => Ok("v0_p2wpkh"),
|
||||
OutputType::P2WSH => Ok("v0_p2wsh"),
|
||||
OutputType::P2TR => Ok("v1_p2tr"),
|
||||
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {
|
||||
Err(BrkError { message: format!("Unsupported address type for address payload hash-prefix: {addr_type:?}") })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
|
||||
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {
|
||||
if address.is_empty() {
|
||||
return Err(BrkError { message: "Expected an address string".to_string() });
|
||||
}
|
||||
let addr_bytes = AddrBytes::from_str(address).map_err(|e| BrkError { message: e.to_string() })?;
|
||||
let addr_type = OutputType::from(&addr_bytes);
|
||||
|
||||
Ok(AddressPayload {
|
||||
addr_type,
|
||||
payload: addr_bytes.as_slice().to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Decode a mainnet Bitcoin address and compute its hash prefix.
|
||||
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {
|
||||
let decoded = decode_address_payload(address)?;
|
||||
Ok(AddressHashPrefix {
|
||||
addr_type: decoded.addr_type,
|
||||
prefix: address_payload_hash_prefix(&decoded.payload, nibbles)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Options for configuring the BRK client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrkClientOptions {
|
||||
@@ -9538,7 +9630,7 @@ pub struct BrkClient {
|
||||
|
||||
impl BrkClient {
|
||||
/// Client version.
|
||||
pub const VERSION: &'static str = "v0.3.4";
|
||||
pub const VERSION: &'static str = "v0.3.6";
|
||||
|
||||
/// Create a new client with the given base URL.
|
||||
pub fn new(base_url: impl Into<String>) -> Self {
|
||||
@@ -9592,6 +9684,34 @@ impl BrkClient {
|
||||
))
|
||||
}
|
||||
|
||||
/// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
|
||||
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {
|
||||
decode_address_payload(address)
|
||||
}
|
||||
|
||||
/// Compute the RapidHash v3 hash-prefix for raw address payload bytes.
|
||||
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {
|
||||
address_payload_hash_prefix(payload, nibbles)
|
||||
}
|
||||
|
||||
/// Decode a mainnet Bitcoin address and compute its hash prefix.
|
||||
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {
|
||||
address_hash_prefix(address, nibbles)
|
||||
}
|
||||
|
||||
/// Fetch address hash-prefix matches from raw payload bytes matching `addr_type` length.
|
||||
pub fn get_address_payload_hash_prefix_matches(&self, addr_type: OutputType, payload: &[u8], nibbles: usize) -> Result<AddrHashPrefixMatches> {
|
||||
validate_address_payload_for_type(addr_type, payload)?;
|
||||
let prefix = address_payload_hash_prefix(payload, nibbles)?;
|
||||
self.get_address_hash_prefix_matches(addr_type, &prefix)
|
||||
}
|
||||
|
||||
/// Fetch address hash-prefix matches for a mainnet Bitcoin address.
|
||||
pub fn get_address_hash_prefix_matches_for_address(&self, address: &str, nibbles: usize) -> Result<AddrHashPrefixMatches> {
|
||||
let hashed = address_hash_prefix(address, nibbles)?;
|
||||
self.get_address_hash_prefix_matches(hashed.addr_type, &hashed.prefix)
|
||||
}
|
||||
|
||||
/// Health check
|
||||
///
|
||||
/// Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, see `/api/server/sync`.
|
||||
@@ -9868,10 +9988,11 @@ impl BrkClient {
|
||||
|
||||
/// Address hash-prefix matches
|
||||
///
|
||||
/// Find addresses by address type and address-payload hash prefix. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
/// Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
///
|
||||
/// Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`
|
||||
pub fn get_address_hash_prefix_matches(&self, addr_type: OutputType, prefix: &str) -> Result<AddrHashPrefixMatches> {
|
||||
let addr_type = address_payload_type_path(addr_type)?;
|
||||
self.base.get_json(&format!("/api/address/hash-prefix/{addr_type}/{prefix}"))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use brk_client::{
|
||||
BrkClient, OutputType, address_hash_prefix, address_payload_hash_prefix, decode_address_payload,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn address_payload_hash_prefix_vectors() {
|
||||
let vectors = [
|
||||
(vec![0x4e, 0x73], "58101afa51a1ecfd"),
|
||||
((0_u8..20).collect::<Vec<_>>(), "c3327ecb8ae1ff23"),
|
||||
((0_u8..32).collect::<Vec<_>>(), "c0186990f026b180"),
|
||||
((0_u8..65).collect::<Vec<_>>(), "0d4b77027ae7d700"),
|
||||
];
|
||||
|
||||
for (payload, expected) in vectors.iter() {
|
||||
assert_eq!(address_payload_hash_prefix(payload, 16).unwrap(), *expected);
|
||||
assert_eq!(
|
||||
BrkClient::address_payload_hash_prefix(payload, 8).unwrap(),
|
||||
&expected[..8]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn address_payload_hash_prefix_validation() {
|
||||
assert!(address_payload_hash_prefix(&[], 16).is_err());
|
||||
assert!(address_payload_hash_prefix(&[0; 66], 16).is_err());
|
||||
assert!(address_payload_hash_prefix(&[1, 2], 0).is_err());
|
||||
assert!(address_payload_hash_prefix(&[1, 2], 17).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn address_hash_prefix_uses_brk_address_parser() {
|
||||
let address = "1BoatSLRHtKNngkdXEeobR76b53LETtpyT";
|
||||
let decoded = decode_address_payload(address).unwrap();
|
||||
assert_eq!(decoded.addr_type, OutputType::P2PKH);
|
||||
assert_eq!(decoded.payload.len(), 20);
|
||||
|
||||
let hashed = address_hash_prefix(address, 8).unwrap();
|
||||
assert_eq!(hashed.addr_type, OutputType::P2PKH);
|
||||
assert_eq!(
|
||||
hashed.prefix,
|
||||
address_payload_hash_prefix(&decoded.payload, 8).unwrap()
|
||||
);
|
||||
}
|
||||
@@ -3,9 +3,7 @@ use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, Uri},
|
||||
};
|
||||
use brk_types::{
|
||||
AddrHashPrefixMatches, AddrStats, AddrValidation, Transaction, Utxo, Version,
|
||||
};
|
||||
use brk_types::{AddrHashPrefixMatches, AddrStats, AddrValidation, Transaction, Utxo, Version};
|
||||
|
||||
use crate::{
|
||||
AppState, CacheStrategy,
|
||||
@@ -43,7 +41,7 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
.id("get_address_hash_prefix_matches")
|
||||
.addrs_tag()
|
||||
.summary("Address hash-prefix matches")
|
||||
.description("Find addresses by address type and address-payload hash prefix. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.")
|
||||
.description("Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.")
|
||||
.json_response::<AddrHashPrefixMatches>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
|
||||
@@ -16,7 +16,7 @@ indexmap = { workspace = true }
|
||||
itoa = "1.0.18"
|
||||
jiff = { workspace = true }
|
||||
pco = { workspace = true }
|
||||
rapidhash = "4.4.1"
|
||||
rapidhash = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
ryu = "1.0.23"
|
||||
schemars = { workspace = true }
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
[](https://opensats.org/)
|
||||
[](https://discord.gg/WACpShCB7M)
|
||||
[](https://x.com/_nym21_)
|
||||
[](https://primal.net/p/nprofile1qqsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03sqxu2e6)
|
||||
|
||||
> "Shout out to Bitcoin Research Kit. [...] Couldn't recommend them highly enough."
|
||||
> — James Check (CheckOnChain), [What Bitcoin Did #1000](https://www.whatbitcoindid.com/episodes/wbd1000-checkmate)
|
||||
|
||||
@@ -233,6 +233,32 @@ Defined in: [Developer/brk/modules/brk-client/index.js:1894](https://github.com/
|
||||
|
||||
***
|
||||
|
||||
### addressPayloadHashPrefix()
|
||||
|
||||
> `static` **addressPayloadHashPrefix**(`payload`, `nibbles`): `string`
|
||||
|
||||
Compute the RapidHash v3 hash-prefix for raw address payload bytes.
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### payload
|
||||
|
||||
`Uint8Array` | `ArrayBuffer` | `ArrayBufferView` | `number`[]
|
||||
|
||||
Raw address payload bytes. Must be 1 to 65 bytes.
|
||||
|
||||
##### nibbles
|
||||
|
||||
`number`
|
||||
|
||||
Prefix length from 1 to 16 hex nibbles.
|
||||
|
||||
#### Returns
|
||||
|
||||
`string`
|
||||
|
||||
***
|
||||
|
||||
### dateToIndex()
|
||||
|
||||
> **dateToIndex**(`index`, `d`): `number`
|
||||
@@ -427,7 +453,7 @@ Defined in: [Developer/brk/modules/brk-client/index.js:11512](https://github.com
|
||||
|
||||
Address hash-prefix matches
|
||||
|
||||
Find addresses by address type and address-payload hash prefix. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
|
||||
Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`
|
||||
|
||||
@@ -461,6 +487,50 @@ Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`
|
||||
|
||||
***
|
||||
|
||||
### getAddressPayloadHashPrefixMatches()
|
||||
|
||||
> **getAddressPayloadHashPrefixMatches**(`addrType`, `payload`, `nibbles`, `options?`): `Promise`\<[`AddrHashPrefixMatches`](../interfaces/AddrHashPrefixMatches.md)\>
|
||||
|
||||
Fetch address hash-prefix matches from raw payload bytes matching `addrType` length.
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### addrType
|
||||
|
||||
[`OutputType`](../type-aliases/OutputType.md)
|
||||
|
||||
##### payload
|
||||
|
||||
`Uint8Array` | `ArrayBuffer` | `ArrayBufferView` | `number`[]
|
||||
|
||||
Raw payload bytes matching `addrType` length.
|
||||
|
||||
##### nibbles
|
||||
|
||||
`number`
|
||||
|
||||
Prefix length from 1 to 16 hex nibbles.
|
||||
|
||||
##### options?
|
||||
|
||||
###### cache?
|
||||
|
||||
`boolean`
|
||||
|
||||
###### onValue?
|
||||
|
||||
(`value`) => `void`
|
||||
|
||||
###### signal?
|
||||
|
||||
`AbortSignal`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<[`AddrHashPrefixMatches`](../interfaces/AddrHashPrefixMatches.md)\>
|
||||
|
||||
***
|
||||
|
||||
### getAddressMempoolTxs()
|
||||
|
||||
> **getAddressMempoolTxs**(`address`, `options?`): `Promise`\<[`Transaction`](../interfaces/Transaction.md)[]\>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
[**brk-client**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[brk-client](../globals.md) / addressPayloadHashPrefix
|
||||
|
||||
# Function: addressPayloadHashPrefix()
|
||||
|
||||
> **addressPayloadHashPrefix**(`payload`, `nibbles`): `string`
|
||||
|
||||
Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`.
|
||||
|
||||
## Parameters
|
||||
|
||||
### payload
|
||||
|
||||
`Uint8Array` | `ArrayBuffer` | `ArrayBufferView` | `number`[]
|
||||
|
||||
Raw address payload bytes. Must be 1 to 65 bytes.
|
||||
|
||||
### nibbles
|
||||
|
||||
`number`
|
||||
|
||||
Prefix length from 1 to 16 hex nibbles.
|
||||
|
||||
## Returns
|
||||
|
||||
`string`
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
- [BrkClient](classes/BrkClient.md)
|
||||
- [BrkError](classes/BrkError.md)
|
||||
|
||||
## Functions
|
||||
|
||||
- [addressPayloadHashPrefix](functions/addressPayloadHashPrefix.md)
|
||||
|
||||
## Interfaces
|
||||
|
||||
- [\_0sdM0M1M1sdM2M2sdM3sdP0P1P1sdP2P2sdP3sdSdZscorePattern](interfaces/0sdM0M1M1sdM2M2sdM3sdP0P1P1sdP2P2sdP3sdSdZscorePattern.md)
|
||||
|
||||
+179
-2
@@ -2132,6 +2132,159 @@ const _m = (acc, s) => s ? (acc ? `${acc}_${s}` : s) : acc;
|
||||
const _p = (prefix, acc) => acc ? `${prefix}_${acc}` : prefix;
|
||||
|
||||
|
||||
|
||||
const _MASK_64 = 0xffffffffffffffffn;
|
||||
const _RAPIDHASH_SECRETS = /** @type {const} */ ([
|
||||
0x2d358dccaa6c78a5n,
|
||||
0x8bb84b93962eacc9n,
|
||||
0x4b33a62ed433d4a3n,
|
||||
0x4d5a2da51de1aa47n,
|
||||
0xa0761d6478bd642fn,
|
||||
0xe7037ed1a0b428dbn,
|
||||
0x90ed1765281c388cn,
|
||||
]);
|
||||
const _RAPIDHASH_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 ^ _RAPIDHASH_SECRETS[2], _RAPIDHASH_SECRETS[1]));
|
||||
}
|
||||
|
||||
/** @param {Uint8Array} bytes @param {number} offset */
|
||||
function _readU32(bytes, offset) {
|
||||
return (
|
||||
BigInt(bytes[offset]) |
|
||||
(BigInt(bytes[offset + 1]) << 8n) |
|
||||
(BigInt(bytes[offset + 2]) << 16n) |
|
||||
(BigInt(bytes[offset + 3]) << 24n)
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {Uint8Array} bytes @param {number} offset */
|
||||
function _readU64(bytes, offset) {
|
||||
return _readU32(bytes, offset) | (_readU32(bytes, offset + 4) << 32n);
|
||||
}
|
||||
|
||||
/** @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload */
|
||||
function _asUint8Array(payload) {
|
||||
if (payload instanceof Uint8Array) return payload;
|
||||
if (payload instanceof ArrayBuffer) return new Uint8Array(payload);
|
||||
if (ArrayBuffer.isView(payload)) return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
|
||||
if (Array.isArray(payload)) return new Uint8Array(payload);
|
||||
throw new Error("Expected address payload bytes");
|
||||
}
|
||||
|
||||
/** @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload */
|
||||
function _rapidHashV3(payload) {
|
||||
const bytes = _asUint8Array(payload);
|
||||
const length = bytes.length;
|
||||
if (length === 0) throw new Error("Expected a non-empty address payload");
|
||||
if (length > 65) throw new Error("Expected at most 65 address payload bytes");
|
||||
|
||||
let seed = _RAPIDHASH_SEED;
|
||||
let a = 0n;
|
||||
let b = 0n;
|
||||
let remainder;
|
||||
|
||||
if (length <= 16) {
|
||||
if (length >= 4) {
|
||||
seed ^= BigInt(length);
|
||||
if (length >= 8) {
|
||||
a ^= _readU64(bytes, 0);
|
||||
b ^= _readU64(bytes, length - 8);
|
||||
} else {
|
||||
a ^= _readU32(bytes, 0);
|
||||
b ^= _readU32(bytes, length - 4);
|
||||
}
|
||||
} else if (length > 0) {
|
||||
a ^= (BigInt(bytes[0]) << 45n) | BigInt(bytes[length - 1]);
|
||||
b ^= BigInt(bytes[length >> 1]);
|
||||
}
|
||||
remainder = BigInt(length);
|
||||
} else {
|
||||
seed = _rapidMix(_readU64(bytes, 0) ^ _RAPIDHASH_SECRETS[2], _readU64(bytes, 8) ^ seed);
|
||||
if (length > 32) {
|
||||
seed = _rapidMix(_readU64(bytes, 16) ^ _RAPIDHASH_SECRETS[2], _readU64(bytes, 24) ^ seed);
|
||||
if (length > 48) {
|
||||
seed = _rapidMix(_readU64(bytes, 32) ^ _RAPIDHASH_SECRETS[1], _readU64(bytes, 40) ^ seed);
|
||||
if (length > 64) {
|
||||
seed = _rapidMix(_readU64(bytes, 48) ^ _RAPIDHASH_SECRETS[1], _readU64(bytes, 56) ^ seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
remainder = BigInt(length);
|
||||
a ^= _readU64(bytes, length - 16) ^ remainder;
|
||||
b ^= _readU64(bytes, length - 8);
|
||||
}
|
||||
|
||||
a ^= _RAPIDHASH_SECRETS[1];
|
||||
b ^= seed;
|
||||
[a, b] = _rapidMum(a, b);
|
||||
return _rapidMix(a ^ 0xaaaaaaaaaaaaaaaan, b ^ _RAPIDHASH_SECRETS[1] ^ remainder);
|
||||
}
|
||||
|
||||
/** @param {number} nibbles */
|
||||
function _validateHashPrefixNibbles(nibbles) {
|
||||
if (!Number.isInteger(nibbles) || nibbles < 1 || nibbles > 16) {
|
||||
throw new Error("Expected hash-prefix length from 1 to 16 hex nibbles");
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {OutputType} addrType @returns {number[]} */
|
||||
function _addressPayloadLengths(addrType) {
|
||||
switch (addrType) {
|
||||
case "p2a": return [2];
|
||||
case "p2pk": return [33, 65];
|
||||
case "p2pkh":
|
||||
case "p2sh":
|
||||
case "v0_p2wpkh": return [20];
|
||||
case "v0_p2wsh":
|
||||
case "v1_p2tr": return [32];
|
||||
default:
|
||||
throw new Error(`Unsupported address type for address payload hash-prefix: ${addrType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OutputType} addrType
|
||||
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload
|
||||
*/
|
||||
function _validateAddressPayloadForType(addrType, payload) {
|
||||
const length = _asUint8Array(payload).length;
|
||||
const expected = _addressPayloadLengths(addrType);
|
||||
if (!expected.includes(length)) {
|
||||
throw new Error(`Expected ${addrType} address payload length ${expected.join(" or ")} bytes`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`.
|
||||
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload - Raw address payload bytes
|
||||
* @param {number} nibbles - Prefix length from 1 to 16 hex nibbles
|
||||
* @returns {string}
|
||||
*/
|
||||
function addressPayloadHashPrefix(payload, nibbles) {
|
||||
_validateHashPrefixNibbles(nibbles);
|
||||
return _rapidHashV3(payload).toString(16).padStart(16, "0").slice(0, nibbles);
|
||||
}
|
||||
|
||||
// Index group constants and factory
|
||||
|
||||
const _i1 = /** @type {const} */ (["minute10", "minute30", "hour1", "hour4", "hour12", "day1", "day3", "week1", "month1", "month3", "month6", "year1", "year10", "halving", "epoch", "height"]);
|
||||
@@ -9091,6 +9244,30 @@ class BrkClient extends BrkClientBase {
|
||||
this.series = this._buildTree();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the RapidHash v3 hash-prefix for raw address payload bytes.
|
||||
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload
|
||||
* @param {number} nibbles
|
||||
* @returns {string}
|
||||
*/
|
||||
static addressPayloadHashPrefix(payload, nibbles) {
|
||||
return addressPayloadHashPrefix(payload, nibbles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch address hash-prefix matches from raw address payload bytes.
|
||||
* @param {OutputType} addrType
|
||||
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload - Raw payload bytes matching addrType length
|
||||
* @param {number} nibbles
|
||||
* @param {{ signal?: AbortSignal, onValue?: (value: AddrHashPrefixMatches) => void, cache?: boolean }} [options]
|
||||
* @returns {Promise<AddrHashPrefixMatches>}
|
||||
*/
|
||||
getAddressPayloadHashPrefixMatches(addrType, payload, nibbles, options = {}) {
|
||||
_validateAddressPayloadForType(addrType, payload);
|
||||
const prefix = addressPayloadHashPrefix(payload, nibbles);
|
||||
return this.getAddressHashPrefixMatches(addrType, prefix, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {SeriesTree}
|
||||
@@ -11500,7 +11677,7 @@ class BrkClient extends BrkClientBase {
|
||||
/**
|
||||
* Address hash-prefix matches
|
||||
*
|
||||
* Find addresses by address type and address-payload hash prefix. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
* Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
*
|
||||
* Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`
|
||||
*
|
||||
@@ -12766,4 +12943,4 @@ class BrkClient extends BrkClientBase {
|
||||
|
||||
}
|
||||
|
||||
export { BrkClient, BrkError };
|
||||
export { BrkClient, BrkError, addressPayloadHashPrefix };
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"scripts": {
|
||||
"test": "node tests/basic.js && node tests/tree.js",
|
||||
"test:basic": "node tests/basic.js",
|
||||
"test:hash-prefix": "node tests/hash_prefix.js",
|
||||
"test:tree": "node tests/tree.js"
|
||||
},
|
||||
"description": "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { BrkClient, addressPayloadHashPrefix } from "../index.js";
|
||||
|
||||
const vectors = [
|
||||
[Uint8Array.of(0x4e, 0x73), "58101afa51a1ecfd"],
|
||||
[Uint8Array.from({ length: 20 }, (_, i) => i), "c3327ecb8ae1ff23"],
|
||||
[Uint8Array.from({ length: 32 }, (_, i) => i), "c0186990f026b180"],
|
||||
[Uint8Array.from({ length: 65 }, (_, i) => i), "0d4b77027ae7d700"],
|
||||
];
|
||||
|
||||
for (const [payload, hash] of vectors) {
|
||||
assert.equal(addressPayloadHashPrefix(payload, 16), hash);
|
||||
assert.equal(BrkClient.addressPayloadHashPrefix(payload, 8), hash.slice(0, 8));
|
||||
}
|
||||
|
||||
assert.throws(() => addressPayloadHashPrefix(Uint8Array.of(), 16), /non-empty/);
|
||||
assert.throws(() => addressPayloadHashPrefix(new Uint8Array(66), 16), /at most 65/);
|
||||
assert.throws(() => addressPayloadHashPrefix(Uint8Array.of(1, 2), 0), /1 to 16/);
|
||||
|
||||
const client = new BrkClient("http://127.0.0.1:0");
|
||||
client.getAddressHashPrefixMatches = (addrType, prefix) => ({ addrType, prefix, truncated: false, addresses: [] });
|
||||
|
||||
assert.deepEqual(
|
||||
client.getAddressPayloadHashPrefixMatches("p2pkh", Uint8Array.from({ length: 20 }, (_, i) => i), 8),
|
||||
{ addrType: "p2pkh", prefix: "c3327ecb", truncated: false, addresses: [] },
|
||||
);
|
||||
assert.throws(
|
||||
() => client.getAddressPayloadHashPrefixMatches("p2pkh", Uint8Array.of(1, 2), 8),
|
||||
/p2pkh address payload length 20 bytes/,
|
||||
);
|
||||
assert.throws(
|
||||
() => client.getAddressPayloadHashPrefixMatches("op_return", Uint8Array.of(1, 2), 8),
|
||||
/Unsupported address type/,
|
||||
);
|
||||
@@ -24,6 +24,7 @@
|
||||
* [series\_endpoint](#brk_client.BrkClient.series_endpoint)
|
||||
* [index\_to\_date](#brk_client.BrkClient.index_to_date)
|
||||
* [date\_to\_index](#brk_client.BrkClient.date_to_index)
|
||||
* [address\_payload\_hash\_prefix](#brk_client.BrkClient.address_payload_hash_prefix)
|
||||
* [get\_health](#brk_client.BrkClient.get_health)
|
||||
* [get\_version](#brk_client.BrkClient.get_version)
|
||||
* [get\_sync\_status](#brk_client.BrkClient.get_sync_status)
|
||||
@@ -47,6 +48,7 @@
|
||||
* [get\_difficulty\_adjustment](#brk_client.BrkClient.get_difficulty_adjustment)
|
||||
* [get\_prices](#brk_client.BrkClient.get_prices)
|
||||
* [get\_historical\_price](#brk_client.BrkClient.get_historical_price)
|
||||
* [get\_address\_payload\_hash\_prefix\_matches](#brk_client.BrkClient.get_address_payload_hash_prefix_matches)
|
||||
* [get\_address\_hash\_prefix\_matches](#brk_client.BrkClient.get_address_hash_prefix_matches)
|
||||
* [get\_address](#brk_client.BrkClient.get_address)
|
||||
* [get\_address\_txs](#brk_client.BrkClient.get_address_txs)
|
||||
@@ -255,6 +257,17 @@ def date_to_index(index: Index, d: Union[date, datetime]) -> int
|
||||
|
||||
Convert a date/datetime to an index value for date-based indexes.
|
||||
|
||||
<a id="brk_client.BrkClient.address_payload_hash_prefix"></a>
|
||||
|
||||
#### address\_payload\_hash\_prefix
|
||||
|
||||
```python
|
||||
def address_payload_hash_prefix(
|
||||
payload: Union[bytes, bytearray, memoryview], nibbles: int) -> str
|
||||
```
|
||||
|
||||
Compute the RapidHash v3 hash-prefix for raw address payload bytes.
|
||||
|
||||
<a id="brk_client.BrkClient.get_health"></a>
|
||||
|
||||
#### get\_health
|
||||
@@ -607,6 +620,19 @@ Get historical BTC/USD price. Optionally specify a UNIX timestamp to get the pri
|
||||
|
||||
Endpoint: `GET /api/v1/historical-price`
|
||||
|
||||
<a id="brk_client.BrkClient.get_address_payload_hash_prefix_matches"></a>
|
||||
|
||||
#### get\_address\_payload\_hash\_prefix\_matches
|
||||
|
||||
```python
|
||||
def get_address_payload_hash_prefix_matches(
|
||||
addr_type: OutputType,
|
||||
payload: Union[bytes, bytearray, memoryview],
|
||||
nibbles: int) -> AddrHashPrefixMatches
|
||||
```
|
||||
|
||||
Fetch address hash-prefix matches from raw payload bytes matching `addr_type` length.
|
||||
|
||||
<a id="brk_client.BrkClient.get_address_hash_prefix_matches"></a>
|
||||
|
||||
#### get\_address\_hash\_prefix\_matches
|
||||
@@ -618,7 +644,7 @@ def get_address_hash_prefix_matches(addr_type: OutputType,
|
||||
|
||||
Address hash-prefix matches.
|
||||
|
||||
Find addresses by address type and address-payload hash prefix. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
|
||||
Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`
|
||||
|
||||
@@ -1769,4 +1795,3 @@ Compact OpenAPI specification.
|
||||
Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. Full spec available at `/openapi.json`.
|
||||
|
||||
Endpoint: `GET /api.json`
|
||||
|
||||
|
||||
@@ -1912,6 +1912,124 @@ def _p(prefix: str, acc: str) -> str:
|
||||
return f"{prefix}_{acc}" if acc else prefix
|
||||
|
||||
|
||||
|
||||
_MASK_64 = (1 << 64) - 1
|
||||
_RAPIDHASH_SECRETS = (
|
||||
0x2d358dccaa6c78a5,
|
||||
0x8bb84b93962eacc9,
|
||||
0x4b33a62ed433d4a3,
|
||||
0x4d5a2da51de1aa47,
|
||||
0xa0761d6478bd642f,
|
||||
0xe7037ed1a0b428db,
|
||||
0x90ed1765281c388c,
|
||||
)
|
||||
_RAPIDHASH_SEED = 0
|
||||
|
||||
|
||||
def _u64(value: int) -> int:
|
||||
return value & _MASK_64
|
||||
|
||||
|
||||
def _rapid_mix(left: int, right: int) -> int:
|
||||
result = _u64(left) * _u64(right)
|
||||
return _u64(result) ^ _u64(result >> 64)
|
||||
|
||||
|
||||
def _rapid_mum(left: int, right: int) -> Tuple[int, int]:
|
||||
result = _u64(left) * _u64(right)
|
||||
return _u64(result), _u64(result >> 64)
|
||||
|
||||
|
||||
def _rapid_hash_seed(seed: int) -> int:
|
||||
return _u64(seed ^ _rapid_mix(seed ^ _RAPIDHASH_SECRETS[2], _RAPIDHASH_SECRETS[1]))
|
||||
|
||||
|
||||
_RAPIDHASH_SEED = _rapid_hash_seed(0)
|
||||
|
||||
|
||||
def _read_u32(data: bytes, offset: int) -> int:
|
||||
return int.from_bytes(data[offset:offset + 4], "little")
|
||||
|
||||
|
||||
def _read_u64(data: bytes, offset: int) -> int:
|
||||
return int.from_bytes(data[offset:offset + 8], "little")
|
||||
|
||||
|
||||
def _rapid_hash_v3(payload: Union[bytes, bytearray, memoryview]) -> int:
|
||||
data = bytes(payload)
|
||||
length = len(data)
|
||||
if length == 0:
|
||||
raise ValueError("Expected a non-empty address payload")
|
||||
if length > 65:
|
||||
raise ValueError("Expected at most 65 address payload bytes")
|
||||
|
||||
seed = _RAPIDHASH_SEED
|
||||
a = 0
|
||||
b = 0
|
||||
|
||||
if length <= 16:
|
||||
if length >= 4:
|
||||
seed ^= length
|
||||
if length >= 8:
|
||||
a ^= _read_u64(data, 0)
|
||||
b ^= _read_u64(data, length - 8)
|
||||
else:
|
||||
a ^= _read_u32(data, 0)
|
||||
b ^= _read_u32(data, length - 4)
|
||||
elif length > 0:
|
||||
a ^= (data[0] << 45) | data[length - 1]
|
||||
b ^= data[length >> 1]
|
||||
remainder = length
|
||||
else:
|
||||
if length > 16:
|
||||
seed = _rapid_mix(_read_u64(data, 0) ^ _RAPIDHASH_SECRETS[2], _read_u64(data, 8) ^ seed)
|
||||
if length > 32:
|
||||
seed = _rapid_mix(_read_u64(data, 16) ^ _RAPIDHASH_SECRETS[2], _read_u64(data, 24) ^ seed)
|
||||
if length > 48:
|
||||
seed = _rapid_mix(_read_u64(data, 32) ^ _RAPIDHASH_SECRETS[1], _read_u64(data, 40) ^ seed)
|
||||
if length > 64:
|
||||
seed = _rapid_mix(_read_u64(data, 48) ^ _RAPIDHASH_SECRETS[1], _read_u64(data, 56) ^ seed)
|
||||
remainder = length
|
||||
a ^= _read_u64(data, length - 16) ^ remainder
|
||||
b ^= _read_u64(data, length - 8)
|
||||
|
||||
a ^= _RAPIDHASH_SECRETS[1]
|
||||
b ^= seed
|
||||
a, b = _rapid_mum(a, b)
|
||||
return _rapid_mix(a ^ 0xaaaaaaaaaaaaaaaa, b ^ _RAPIDHASH_SECRETS[1] ^ remainder)
|
||||
|
||||
|
||||
def _validate_hash_prefix_nibbles(nibbles: int) -> None:
|
||||
if isinstance(nibbles, bool) or not isinstance(nibbles, int) or nibbles < 1 or nibbles > 16:
|
||||
raise ValueError("Expected hash-prefix length from 1 to 16 hex nibbles")
|
||||
|
||||
|
||||
def _address_payload_lengths(addr_type: OutputType) -> Tuple[int, ...]:
|
||||
if addr_type == "p2a":
|
||||
return (2,)
|
||||
if addr_type == "p2pk":
|
||||
return (33, 65)
|
||||
if addr_type in ("p2pkh", "p2sh", "v0_p2wpkh"):
|
||||
return (20,)
|
||||
if addr_type in ("v0_p2wsh", "v1_p2tr"):
|
||||
return (32,)
|
||||
raise ValueError(f"Unsupported address type for address payload hash-prefix: {addr_type}")
|
||||
|
||||
|
||||
def _validate_address_payload_for_type(addr_type: OutputType, payload: Union[bytes, bytearray, memoryview]) -> None:
|
||||
length = len(bytes(payload))
|
||||
expected = _address_payload_lengths(addr_type)
|
||||
if length not in expected:
|
||||
joined = " or ".join(str(value) for value in expected)
|
||||
raise ValueError(f"Expected {addr_type} address payload length {joined} bytes")
|
||||
|
||||
|
||||
def address_payload_hash_prefix(payload: Union[bytes, bytearray, memoryview], nibbles: int) -> str:
|
||||
"""Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`."""
|
||||
_validate_hash_prefix_nibbles(nibbles)
|
||||
return f"{_rapid_hash_v3(payload):016x}"[:nibbles]
|
||||
|
||||
|
||||
# Date conversion constants
|
||||
_GENESIS = date(2009, 1, 3) # day1 0, week1 0
|
||||
_DAY_ONE = date(2009, 1, 9) # day1 1 (6 day gap after genesis)
|
||||
@@ -8201,6 +8319,21 @@ class BrkClient(BrkClientBase):
|
||||
"""Convert a date/datetime to an index value for date-based indexes."""
|
||||
return _date_to_index(index, d)
|
||||
|
||||
@staticmethod
|
||||
def address_payload_hash_prefix(payload: Union[bytes, bytearray, memoryview], nibbles: int) -> str:
|
||||
"""Compute the RapidHash v3 hash-prefix for raw address payload bytes."""
|
||||
return address_payload_hash_prefix(payload, nibbles)
|
||||
|
||||
def get_address_payload_hash_prefix_matches(
|
||||
self,
|
||||
addr_type: OutputType,
|
||||
payload: Union[bytes, bytearray, memoryview],
|
||||
nibbles: int,
|
||||
) -> AddrHashPrefixMatches:
|
||||
"""Fetch address hash-prefix matches from raw payload bytes matching addr_type length."""
|
||||
_validate_address_payload_for_type(addr_type, payload)
|
||||
return self.get_address_hash_prefix_matches(addr_type, address_payload_hash_prefix(payload, nibbles))
|
||||
|
||||
def get_health(self) -> Health:
|
||||
"""Health check.
|
||||
|
||||
@@ -8449,7 +8582,7 @@ class BrkClient(BrkClientBase):
|
||||
def get_address_hash_prefix_matches(self, addr_type: OutputType, prefix: str) -> AddrHashPrefixMatches:
|
||||
"""Address hash-prefix matches.
|
||||
|
||||
Find addresses by address type and address-payload hash prefix. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
|
||||
Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`"""
|
||||
return self.get_json(f'/api/address/hash-prefix/{addr_type}/{prefix}')
|
||||
@@ -9163,4 +9296,3 @@ class BrkClient(BrkClientBase):
|
||||
|
||||
Endpoint: `GET /api.json`"""
|
||||
return self.get_json('/api.json')
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkClient, address_payload_hash_prefix
|
||||
|
||||
|
||||
VECTORS = (
|
||||
(bytes([0x4E, 0x73]), "58101afa51a1ecfd"),
|
||||
(bytes(range(20)), "c3327ecb8ae1ff23"),
|
||||
(bytes(range(32)), "c0186990f026b180"),
|
||||
(bytes(range(65)), "0d4b77027ae7d700"),
|
||||
)
|
||||
|
||||
|
||||
def test_address_payload_hash_prefix_vectors():
|
||||
for payload, expected in VECTORS:
|
||||
assert address_payload_hash_prefix(payload, 16) == expected
|
||||
assert BrkClient.address_payload_hash_prefix(payload, 8) == expected[:8]
|
||||
|
||||
|
||||
def test_address_payload_hash_prefix_validation():
|
||||
with pytest.raises(ValueError, match="non-empty"):
|
||||
address_payload_hash_prefix(b"", 16)
|
||||
with pytest.raises(ValueError, match="at most 65"):
|
||||
address_payload_hash_prefix(bytes(range(66)), 16)
|
||||
with pytest.raises(ValueError, match="1 to 16"):
|
||||
address_payload_hash_prefix(b"\x01\x02", 0)
|
||||
|
||||
|
||||
def test_address_payload_hash_prefix_match_validation():
|
||||
client = BrkClient("http://127.0.0.1:0")
|
||||
client.get_address_hash_prefix_matches = lambda addr_type, prefix: {
|
||||
"addr_type": addr_type,
|
||||
"prefix": prefix,
|
||||
"truncated": False,
|
||||
"addresses": [],
|
||||
}
|
||||
|
||||
assert client.get_address_payload_hash_prefix_matches("p2pkh", bytes(range(20)), 8) == {
|
||||
"addr_type": "p2pkh",
|
||||
"prefix": "c3327ecb",
|
||||
"truncated": False,
|
||||
"addresses": [],
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="p2pkh address payload length 20 bytes"):
|
||||
client.get_address_payload_hash_prefix_matches("p2pkh", b"\x01\x02", 8)
|
||||
with pytest.raises(ValueError, match="Unsupported address type"):
|
||||
client.get_address_payload_hash_prefix_matches("op_return", b"\x01\x02", 8)
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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] : [];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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]);
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user