nostr-tools
Version:
Tools for making a Nostr client.
163 lines (162 loc) • 4.32 kB
JavaScript
// nip45.ts
import { sha256 } from "@noble/hashes/sha2.js";
import { bytesToHex } from "@noble/hashes/utils.js";
var M = 256;
var HLL_HEX_LENGTH = M * 2;
var utf8Encoder = new TextEncoder();
function getCountManyFilter(target, directive) {
switch (directive) {
case "reactions":
return { "#e": [target], kinds: [7] };
case "reposts":
return { "#e": [target], kinds: [6] };
case "quotes":
return { "#q": [target], kinds: [1, 1111] };
case "replies":
return { "#e": [target], kinds: [1] };
case "comments":
return { "#E": [target], kinds: [1111] };
case "followers":
return { "#p": [target], kinds: [3] };
}
}
function newHll() {
return new Uint8Array(M);
}
function hllDecode(hex) {
if (hex.length !== HLL_HEX_LENGTH || !/^[0-9a-fA-F]+$/.test(hex))
return void 0;
const registers = new Uint8Array(M);
for (let i = 0; i < M; i++) {
registers[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return registers;
}
function hllEncode(registers) {
if (registers.length !== M)
throw new Error(`invalid number of registers ${registers.length}`);
let hex = "";
for (let i = 0; i < M; i++) {
hex += registers[i].toString(16).padStart(2, "0");
}
return hex;
}
function computeOffset(filterFirstTagValue) {
let hex = filterFirstTagValue;
if (!isHex64(hex)) {
const parts = hex.split(":");
if (parts.length === 3 && isHex64(parts[1])) {
hex = parts[1];
} else {
hex = bytesToHex(sha256(utf8Encoder.encode(filterFirstTagValue)));
}
}
return parseInt(hex[32], 16) + 8;
}
function getFilterFirstTagValue(filter) {
for (const key in filter) {
if (key[0] !== "#")
continue;
const values = filter[key];
if (Array.isArray(values) && typeof values[0] === "string")
return values[0];
}
return void 0;
}
function feedPubkey(hll, pubkey, offset) {
if (offset < 0 || offset > 24)
throw new Error(`invalid offset ${offset}`);
if (!isHex64(pubkey))
throw new Error("pubkey must be 32-byte hex");
if (hll.length === 0)
hll = newHll();
if (hll.length !== M)
throw new Error(`invalid number of registers ${hll.length}`);
const ri = parseInt(pubkey.slice(offset * 2, offset * 2 + 2), 16);
const value = countLeadingZeroBitsAfterOffset(pubkey, offset) + 1;
if (value > hll[ri])
hll[ri] = value;
return hll;
}
function feedEvent(hll, event, offset) {
return feedPubkey(hll, event.pubkey, offset);
}
function mergeHll(target, source) {
if (target.length === 0)
target = newHll();
if (target.length !== M)
throw new Error(`invalid number of registers ${target.length}`);
if (source.length !== M)
throw new Error(`invalid number of registers ${source.length}`);
for (let i = 0; i < M; i++) {
if (source[i] > target[i])
target[i] = source[i];
}
return target;
}
function estimateCount(hll) {
if (hll.length === 0)
return 0;
if (hll.length !== M)
throw new Error(`invalid number of registers ${hll.length}`);
const v = countZeros(hll);
if (v !== 0) {
const lc = linearCounting(M, v);
if (lc <= 220)
return Math.floor(lc);
}
const estimate = calculateEstimate(hll);
if (estimate <= M * 3 && v !== 0)
return Math.floor(linearCounting(M, v));
return Math.floor(estimate);
}
function isHex64(value) {
return /^[0-9a-fA-F]{64}$/.test(value);
}
function countLeadingZeroBitsAfterOffset(pubkey, offset) {
let zeroBits = 0;
for (let i = offset + 1; i < offset + 8; i++) {
const byte = parseInt(pubkey.slice(i * 2, i * 2 + 2), 16);
if (byte === 0) {
zeroBits += 8;
continue;
}
let mask = 128;
while ((byte & mask) === 0) {
zeroBits++;
mask >>= 1;
}
break;
}
return zeroBits;
}
function countZeros(registers) {
let count = 0;
for (let i = 0; i < M; i++) {
if (registers[i] === 0)
count++;
}
return count;
}
function linearCounting(m, v) {
return m * Math.log(m / v);
}
function calculateEstimate(registers) {
let sum = 0;
for (let i = 0; i < M; i++) {
sum += 1 / 2 ** registers[i];
}
return 0.7182725932495458 * M * M / sum;
}
export {
computeOffset,
estimateCount,
feedEvent,
feedPubkey,
getCountManyFilter,
getFilterFirstTagValue,
hllDecode,
hllEncode,
mergeHll,
newHll
};