121 lines
3.3 KiB
TypeScript
121 lines
3.3 KiB
TypeScript
import * as utils from "@noble/curves/abstract/utils";
|
|
import { bech32 } from "@scure/base";
|
|
import { HexKey } from "./nostr";
|
|
|
|
export const enum NostrPrefix {
|
|
PublicKey = "npub",
|
|
PrivateKey = "nsec",
|
|
Note = "note",
|
|
|
|
// TLV prefixes
|
|
Profile = "nprofile",
|
|
Event = "nevent",
|
|
Relay = "nrelay",
|
|
Address = "naddr",
|
|
}
|
|
|
|
export enum TLVEntryType {
|
|
Special = 0,
|
|
Relay = 1,
|
|
Author = 2,
|
|
Kind = 3,
|
|
}
|
|
|
|
export interface TLVEntry {
|
|
type: TLVEntryType;
|
|
length: number;
|
|
value: string | HexKey | number;
|
|
}
|
|
|
|
export function encodeTLV(prefix: NostrPrefix, id: string, relays?: string[], kind?: number, author?: string) {
|
|
const enc = new TextEncoder();
|
|
const buf = prefix === NostrPrefix.Address ? enc.encode(id) : utils.hexToBytes(id);
|
|
|
|
const tl0 = [0, buf.length, ...buf];
|
|
const tl1 =
|
|
relays
|
|
?.map(a => {
|
|
const data = enc.encode(a);
|
|
return [1, data.length, ...data];
|
|
})
|
|
.flat() ?? [];
|
|
|
|
const tl2 = author ? [2, 32, ...utils.hexToBytes(author)] : [];
|
|
const tl3 = kind ? [3, 4, ...new Uint8Array(new Uint32Array([kind]).buffer).reverse()] : [];
|
|
|
|
return bech32.encode(prefix, bech32.toWords(new Uint8Array([...tl0, ...tl1, ...tl2, ...tl3])), 1_000);
|
|
}
|
|
|
|
export function encodeTLVEntries(prefix: NostrPrefix, ...entries: Array<TLVEntry>) {
|
|
const enc = new TextEncoder();
|
|
const buffers: Array<number> = [];
|
|
|
|
for (const v of entries) {
|
|
switch (v.type) {
|
|
case TLVEntryType.Special: {
|
|
const buf =
|
|
prefix === NostrPrefix.Address ? enc.encode(v.value as string) : utils.hexToBytes(v.value as string);
|
|
buffers.push(0, buf.length, ...buf);
|
|
break;
|
|
}
|
|
case TLVEntryType.Relay: {
|
|
const data = enc.encode(v.value as string);
|
|
buffers.push(1, data.length, ...data);
|
|
break;
|
|
}
|
|
case TLVEntryType.Author: {
|
|
if ((v.value as string).length !== 64) throw new Error("Author must be 32 bytes");
|
|
buffers.push(2, 32, ...utils.hexToBytes(v.value as string));
|
|
break;
|
|
}
|
|
case TLVEntryType.Kind: {
|
|
if (typeof v.value !== "number") throw new Error("Kind must be a number");
|
|
buffers.push(3, 4, ...new Uint8Array(new Uint32Array([v.value as number]).buffer).reverse());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return bech32.encode(prefix, bech32.toWords(new Uint8Array(buffers)), 1_000);
|
|
}
|
|
|
|
export function decodeTLV(str: string) {
|
|
const decoded = bech32.decode(str, 1_000);
|
|
const data = bech32.fromWords(decoded.words);
|
|
|
|
const entries: TLVEntry[] = [];
|
|
let x = 0;
|
|
while (x < data.length) {
|
|
const t = data[x];
|
|
const l = data[x + 1];
|
|
const v = data.slice(x + 2, x + 2 + l);
|
|
entries.push({
|
|
type: t,
|
|
length: l,
|
|
value: decodeTLVEntry(t, decoded.prefix, new Uint8Array(v)),
|
|
});
|
|
x += 2 + l;
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function decodeTLVEntry(type: TLVEntryType, prefix: string, data: Uint8Array) {
|
|
switch (type) {
|
|
case TLVEntryType.Special: {
|
|
if (prefix === NostrPrefix.Address) {
|
|
return new TextDecoder("ASCII").decode(data);
|
|
} else {
|
|
return utils.bytesToHex(data);
|
|
}
|
|
}
|
|
case TLVEntryType.Author: {
|
|
return utils.bytesToHex(data);
|
|
}
|
|
case TLVEntryType.Kind: {
|
|
return new Uint32Array(new Uint8Array(data.reverse()).buffer)[0];
|
|
}
|
|
case TLVEntryType.Relay: {
|
|
return new TextDecoder("ASCII").decode(data);
|
|
}
|
|
}
|
|
}
|