commit fe2572f9ecc0810137b50d366a536e6664a12330
parent 1dd4db47e6b3629a88862112b1f50aa6a7348ce6
Author: triesap <137732411+triesap@users.noreply.github.com>
Date: Tue, 4 Mar 2025 07:47:13 +0000
utils-nostr: migrate utils from `@radroots/util`, add minor edits
Diffstat:
12 files changed, 513 insertions(+), 3 deletions(-)
diff --git a/utils-nostr/.gitignore b/utils-nostr/.gitignore
@@ -44,3 +44,4 @@ vite.config.ts.timestamp-*
notes*.txt
notes*.md
git-diff.txt
+justfile
+\ No newline at end of file
diff --git a/utils-nostr/package.json b/utils-nostr/package.json
@@ -18,6 +18,7 @@
"watch": "tsc -w"
},
"devDependencies": {
+ "@types/uuid": "^10.0.0",
"tsup": "^6.2.3",
"typescript": "^5.3.3"
},
@@ -28,6 +29,8 @@
"@noble/curves": "^1.6.0",
"@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "^2.11.0",
- "nostr-tools": "^2.10.4"
+ "nostr-geotags": "^0.7.1",
+ "nostr-tools": "^2.10.4",
+ "uuid": "^10.0.0"
}
}
\ No newline at end of file
diff --git a/utils-nostr/src/index.ts b/utils-nostr/src/index.ts
@@ -1 +1,9 @@
-export const rad = `roots`
-\ No newline at end of file
+export * from "./lib/events"
+export * from "./lib/keys"
+export * from "./lib/ndk"
+export * from "./lib/types"
+export * from "./services/events/lib"
+export * from "./services/events/types"
+export * from "./services/keys/lib"
+export * from "./services/keys/types"
+export * from "./util"
diff --git a/utils-nostr/src/lib/events.ts b/utils-nostr/src/lib/events.ts
@@ -0,0 +1,45 @@
+import { INostrEventServiceNeventEncode, uuidv4, type INostrEventEventSign } from "$root";
+import { schnorr } from "@noble/curves/secp256k1";
+import { hexToBytes } from "@noble/hashes/utils";
+import { finalizeEvent, getEventHash, nip19, type NostrEvent as NostrToolsEvent } from "nostr-tools";
+
+export const lib_nostr_event_verify = (event: NostrToolsEvent): boolean => {
+ const hash = getEventHash(event);
+ if (hash !== event.id) return false
+ const valid = schnorr.verify(event.sig, hash, event.pubkey);
+ return valid;
+};
+
+export const lib_nostr_event_sign = (opts: INostrEventEventSign): NostrToolsEvent => {
+ return finalizeEvent(opts.event, hexToBytes(opts.secret_key))
+};
+
+export const lib_nostr_event_sign_attest = (secret_key: string): NostrToolsEvent => {
+ return lib_nostr_event_sign({
+ secret_key,
+ event: {
+ kind: 1,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [],
+ content: uuidv4(),
+ },
+ });
+};
+
+
+export const lib_nostr_event_verify_serialized = async (event_serialized: string): Promise<{ public_key: string } | undefined> => {
+ try {
+ const event = JSON.parse(event_serialized);
+ const hash = getEventHash(event);
+ if (hash !== event.id) return undefined;
+ const valid = schnorr.verify(event.sig, hash, event.pubkey);
+ if (valid) return { public_key: String(event.pubkey) };
+ return undefined;
+ } catch {
+ return undefined;
+ }
+};
+
+export const lib_nostr_nevent_encode = (opts: INostrEventServiceNeventEncode): string => {
+ return nip19.neventEncode(opts);
+};
+\ No newline at end of file
diff --git a/utils-nostr/src/lib/keys.ts b/utils-nostr/src/lib/keys.ts
@@ -0,0 +1,42 @@
+import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
+import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools";
+
+export const lib_nostr_get_key_bytes = (hex: string): Uint8Array => {
+ return hexToBytes(hex);
+};
+
+export const lib_nostr_get_key_hex = (bytes: Uint8Array): string => {
+ return bytesToHex(bytes);
+};
+
+export const lib_nostr_key_generate = (): string => {
+ const bytes = generateSecretKey();
+ return lib_nostr_get_key_hex(bytes);
+};
+
+export const lib_nostr_nsec_encode = (secret_key_hex?: string): string | undefined => {
+ if (!secret_key_hex) return undefined;
+ const bytes = lib_nostr_get_key_bytes(secret_key_hex);
+ return nip19.nsecEncode(bytes);
+};
+
+export const lib_nostr_nsec_decode = (nsec?: string): string | undefined => {
+ if (!nsec) return undefined;
+ const decode = nip19.decode(nsec);
+ if (decode && decode.type === `nsec` && decode.data) return bytesToHex(decode.data);
+ return undefined;
+};
+
+
+export const lib_nostr_public_key = (secret_key_hex: string): string => {
+ const bytes = lib_nostr_get_key_bytes(secret_key_hex);
+ return getPublicKey(bytes);
+};
+
+export const lib_nostr_secret_key_validate = (secret_key: string): string | undefined => {
+ const is_valid_hex = lib_nostr_public_key(secret_key);
+ if (is_valid_hex) return secret_key;
+ const is_valid_nsec = lib_nostr_nsec_decode(secret_key);
+ if (is_valid_nsec) return is_valid_nsec;
+ return undefined;
+};
+\ No newline at end of file
diff --git a/utils-nostr/src/lib/ndk.ts b/utils-nostr/src/lib/ndk.ts
@@ -0,0 +1,74 @@
+import { time_now_ms, type NostrMetadataTmp } from '$root';
+import NDK, { NDKEvent, NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
+
+export const ndk_init = async (opts: {
+ $ndk: NDK;
+ secret_key: string;
+}): Promise<NDKUser | undefined> => {
+ try {
+ const { $ndk: ndk, secret_key } = opts;
+ const signer = new NDKPrivateKeySigner(secret_key);
+ ndk.signer = signer;
+
+ const user = await signer.user();
+ if (user) {
+ user.ndk = ndk;
+ return user;
+ }
+ } catch (e) {
+ console.log(`(error) ndk_init `, e);
+ };
+};
+
+export const ndk_event_metadata = async (opts: {
+ $ndk: NDK;
+ $ndk_user: NDKUser;
+ metadata: NostrMetadataTmp
+}): Promise<NDKEvent | undefined> => {
+ try {
+ const { $ndk, $ndk_user } = opts;
+ const ev = await ndk_event({
+ $ndk,
+ $ndk_user,
+ basis: {
+ kind: 0,
+ content: JSON.stringify(opts.metadata),
+ },
+ });
+ return ev;
+ } catch (e) {
+ console.log(`(error) ndk_event_metadata `, e);
+ }
+};
+
+export const ndk_event = async (opts: {
+ $ndk: NDK;
+ $ndk_user: NDKUser;
+ basis: {
+ kind: number;
+ content: string;
+ tags?: string[][];
+ }
+}): Promise<NDKEvent | undefined> => {
+ try {
+ const { $ndk: ndk, $ndk_user: ndk_user, basis } = opts;
+ const time_now = time_now_ms();
+
+ const tags: string[][] = [
+ ['published_at', time_now.toString()],
+ ];
+
+ if (basis.tags && basis.tags?.length > 0) for (const tag of basis.tags) tags.push(tag);
+
+ const event: NDKEvent = new NDKEvent(ndk, {
+ kind: basis.kind,
+ pubkey: ndk_user.pubkey,
+ content: basis.content,
+ created_at: time_now,
+ tags
+ });
+ return event;
+ } catch (e) {
+ console.log(`(error) ndk_event `, e);
+ };
+};
diff --git a/utils-nostr/src/lib/types.ts b/utils-nostr/src/lib/types.ts
@@ -0,0 +1,19 @@
+import { type EventTemplate as NostrToolsEventTemplate } from "nostr-tools";
+
+export type NostrMetadataTmp = {
+ name?: string;
+ display_name?: string;
+ about?: string;
+ website?: string;
+ picture?: string;
+ banner?: string;
+ nip05?: string;
+ lud06?: string;
+ lud16?: string;
+ bot?: boolean;
+};
+
+export type INostrEventEventSign = {
+ secret_key: string;
+ event: NostrToolsEventTemplate;
+}
+\ No newline at end of file
diff --git a/utils-nostr/src/services/events/lib.ts b/utils-nostr/src/services/events/lib.ts
@@ -0,0 +1,96 @@
+import { INostrEventEventSign, INostrEventService, INostrEventServiceFormatTagsBasisNip99, INostrEventServiceNeventEncode, lib_nostr_event_sign, lib_nostr_event_sign_attest, lib_nostr_event_verify, lib_nostr_event_verify_serialized, lib_nostr_nevent_encode, NostrEventTagClient, NostrEventTagLocation, NostrEventTagMediaUpload, NostrEventTagPrice, NostrEventTagQuantity } from "$root";
+import { type NDKEvent } from "@nostr-dev-kit/ndk";
+import ngeotags, { type GeoTags as NostrGeotagsGeotags, type InputData as NostrGeotagsInputData } from "nostr-geotags";
+import { type NostrEvent as NostrToolsEvent } from "nostr-tools";
+
+export class NostrEventService implements INostrEventService {
+ public first_tag_value = (event: NDKEvent, tag_name: string): string => {
+ const tag = event.getMatchingTags(tag_name)[0];
+ return tag ? tag[1] : "";
+ }
+
+ private fmt_tag_price = (opts: NostrEventTagPrice): string[] => {
+ const tag = [`price`, opts.amt, opts.currency, opts.qty_amt, opts.qty_unit];
+ return tag;
+ };
+
+
+ private fmt_tag_quantity = (opts: NostrEventTagQuantity): string[] => {
+ const tag = [`quantity`, opts.amt, opts.unit];
+ if (opts.label) tag.push(opts.label);
+ return tag;
+ };
+
+ private fmt_tag_location = (opts: NostrEventTagLocation): string[] => {
+ const tag = [`location`];
+ if (opts.city) tag.push(opts.city);
+ if (opts.region_code && !isNaN(parseInt(opts.region_code))) tag.push(opts.region_code);
+ else if (opts.region) tag.push(opts.region); //@todo
+ if (opts.country_code) tag.push(opts.country_code);
+ return tag;
+ };
+
+ private fmt_tag_image = (opts: NostrEventTagMediaUpload): string[] => {
+ const tag = [`image`, opts.url];
+ if (opts.size) tag.push(`${opts.size.w}x${opts.size.h}`)
+ return tag;
+ };
+
+ private fmt_tag_client = (opts: NostrEventTagClient, d_tag?: string): string[] => {
+ const tag = [`client`, opts.name];
+ if (d_tag) tag.push(`31990:${opts.pubkey}:${d_tag}`);
+ tag.push(opts.relay);
+ return tag;
+ };
+
+ private fmt_tag_geotags = (opts: NostrEventTagLocation): NostrGeotagsGeotags[] => {
+ const data: NostrGeotagsInputData = {
+ lat: opts.lat,
+ lon: opts.lng,
+ city: opts.city,
+ regionName: opts.region,
+ countryName: opts.country,
+ countryCode: opts.country_code
+ };
+ return ngeotags(data, {
+ geohash: true,
+ gps: true,
+ city: true,
+ iso31662: true,
+ });
+ };
+
+ public fmt_tags_basis_nip99 = (opts: INostrEventServiceFormatTagsBasisNip99): string[][] => {
+ const { d_tag, listing, quantity, price, location } = opts;
+ const tags: string[][] = [[`d`, d_tag]];
+ if (opts.client) tags.push(this.fmt_tag_client(opts.client, opts.d_tag));
+ for (const [k, v] of Object.entries(listing)) if (v) tags.push([k, v]);
+ tags.push(this.fmt_tag_quantity(quantity));
+ tags.push(this.fmt_tag_price(price));
+ tags.push(this.fmt_tag_location(location));
+ if (opts.images) for (const image of opts.images) tags.push(this.fmt_tag_image(image));
+ tags.push(...this.fmt_tag_geotags(location));
+ return tags;
+ };
+
+ public nostr_event_sign = (opts: INostrEventEventSign): NostrToolsEvent => {
+ return lib_nostr_event_sign(opts);
+ };
+
+ public nostr_event_sign_attest = (secret_key: string): NostrToolsEvent => {
+ return lib_nostr_event_sign_attest(secret_key);
+ };
+
+ public nostr_event_verify = (event: NostrToolsEvent): boolean => {
+ return lib_nostr_event_verify(event);
+ };
+
+ public nostr_event_verify_serialized = (event_serialized: string): boolean => {
+ const result = lib_nostr_event_verify_serialized(event_serialized);
+ return !!result;
+ };
+
+ public nevent_encode = (opts: INostrEventServiceNeventEncode): string => {
+ return lib_nostr_nevent_encode(opts);
+ };
+}
+\ No newline at end of file
diff --git a/utils-nostr/src/services/events/types.ts b/utils-nostr/src/services/events/types.ts
@@ -0,0 +1,80 @@
+import type { INostrEventEventSign } from "$root";
+import { NDKEvent } from "@nostr-dev-kit/ndk";
+import { type NostrEvent as NostrToolsEvent } from "nostr-tools";
+
+export type INostrEventServiceFormatTagsBasisNip99 = {
+ d_tag: string;
+ listing: NostrEventTagListing;
+ quantity: NostrEventTagQuantity;
+ price: NostrEventTagPrice;
+ location: NostrEventTagLocation;
+ images?: NostrEventTagMediaUpload[];
+ client?: NostrEventTagClient;
+};
+
+export type INostrEventServiceNeventEncode = {
+ id: string;
+ relays: string[];
+ author: string;
+ kind: number;
+};
+
+export type INostrEventService = {
+ first_tag_value(event: NDKEvent, tag_name: string): string;
+ fmt_tags_basis_nip99: (opts: INostrEventServiceFormatTagsBasisNip99) => string[][];
+ nostr_event_sign: (opts: INostrEventEventSign) => NostrToolsEvent;
+ nostr_event_sign_attest: (secret_key: string) => NostrToolsEvent;
+ nostr_event_verify_serialized: (event_serialized: string) => boolean;
+ nostr_event_verify: (event: NostrToolsEvent) => boolean;
+ nevent_encode: (opts: INostrEventServiceNeventEncode) => string;
+};
+
+export type NostrEventTagListing = {
+ key: string;
+ title: string;
+ category: string;
+ summary?: string;
+ process?: string;
+ lot?: string;
+ location?: string;
+ profile?: string;
+ year?: string;
+};
+
+export type NostrEventTagPrice = {
+ amt: string;
+ currency: string;
+ qty_amt: string;
+ qty_unit: string;
+};
+
+export type NostrEventTagQuantity = {
+ amt: string;
+ unit: string;
+ label?: string;
+};
+
+export type NostrEventTagLocation = {
+ city?: string;
+ region?: string;
+ region_code?: string;
+ country?: string;
+ country_code?: string;
+ lat: number;
+ lng: number;
+ geohash: string;
+};
+
+export type NostrEventTagMediaUpload = {
+ url: string;
+ size?: {
+ w: number;
+ h: number;
+ };
+};
+
+export type NostrEventTagClient = {
+ name: string;
+ pubkey: string;
+ relay: string;
+};
diff --git a/utils-nostr/src/services/keys/lib.ts b/utils-nostr/src/services/keys/lib.ts
@@ -0,0 +1,121 @@
+import { type INostrKeyService, lib_nostr_get_key_bytes, lib_nostr_key_generate, lib_nostr_nsec_decode, lib_nostr_nsec_encode } from '$root';
+import { getPublicKey, nip19 } from 'nostr-tools';
+
+export class NostrKeyService implements INostrKeyService {
+ /**
+ *
+ * @returns nostr secret key hex
+ */
+ public generate_key(): string {
+ return lib_nostr_key_generate();
+ };
+
+
+ /**
+ *
+ * @returns nostr public key hex from secret key
+ */
+ public public_key(secret_key_hex: string | undefined): string {
+ try {
+ if (!secret_key_hex) return ``;
+ const bytes = lib_nostr_get_key_bytes(secret_key_hex);
+ const hex = getPublicKey(bytes)
+ return hex;
+ } catch (e) {
+ return ``
+ }
+ }
+
+ /**
+ *
+ * @returns nostr secret key to public key hex
+ */
+ public publickey_decode(secret_key_hex?: string): string | undefined {
+ try {
+ if (secret_key_hex) {
+ return getPublicKey(lib_nostr_get_key_bytes(secret_key_hex))
+ }
+ return undefined;
+ } catch (e) {
+ return undefined;
+ }
+ }
+
+ /**
+ *
+ * @returns nostr public key npub
+ */
+ public npub(public_key_hex: string | undefined, fallback_to_hex?: boolean): string {
+ if (!public_key_hex) return ``;
+ const npub = nip19.npubEncode(public_key_hex);
+ return npub ? npub : fallback_to_hex ? public_key_hex : ``;
+ }
+
+ /**
+ *
+ * @returns public key hex from npub
+ */
+ public npub_decode(npub: string): string {
+ const decode = nip19.decode(npub);
+ console.log(`decode `, decode)
+ if (decode && decode.type === `npub` && decode.data) return decode.data
+ return ``;
+ }
+
+ /**
+ *
+ * @returns nostr secret key nsec
+ */
+ public nsec(secret_key_hex?: string): string | undefined {
+ return lib_nostr_nsec_encode(secret_key_hex);
+ }
+
+ /**
+ *
+ * @returns nostr secret key hex from nsec
+ */
+ public nsec_decode(nsec: string): string | undefined {
+ return lib_nostr_nsec_decode(nsec);
+ }
+
+ /**
+ *
+ * @returns
+ */
+ public nevent(event_pointer: nip19.EventPointer, relays: string[]): string {
+ return nip19.neventEncode(event_pointer)
+ }
+
+ /**
+ *
+ * @returns nostr public key nprofile
+ */
+ public nprofile(public_key_hex: string, relays: string[]): string {
+ if (!public_key_hex || !relays.length) return ``;
+ return nip19.nprofileEncode({ pubkey: public_key_hex, relays })
+ }
+
+ /**
+ *
+ * @returns nostr public key nprofile
+ */
+ public nprofile_decode(nprofile: string): [string, string[]] | undefined {
+ if (!nprofile) return undefined;
+ const decode = nip19.decode(nprofile);
+ if (decode && decode.type === `nprofile` && decode.data && decode.data.pubkey && decode.data.relays) return [decode.data.pubkey, decode.data.relays]
+ return undefined;
+ }
+
+ /**
+ *
+ * @returns
+ */
+ public secretkey_to_publickey(nsec_or_hex: string): string | undefined {
+ if (nsec_or_hex.startsWith(`nsec1`)) {
+ return this.nsec_decode(nsec_or_hex);
+ } else if (nsec_or_hex.length === 64) {
+ return this.publickey_decode(nsec_or_hex)
+ }
+ return undefined;
+ }
+};
diff --git a/utils-nostr/src/services/keys/types.ts b/utils-nostr/src/services/keys/types.ts
@@ -0,0 +1,11 @@
+export type INostrKeyService = {
+ generate_key(): string;
+ public_key(secret_key_hex: string | undefined): string;
+ npub(public_key_hex: string | undefined): string;
+ npub_decode(npub: string): string;
+ nsec(secret_key_hex: string | undefined): string | undefined;
+ nsec_decode(nsec: string): string | undefined;
+ nprofile(public_key_hex: string, relays: string[]): string;
+ nprofile_decode(nprofile: string): [string, string[]] | undefined;
+ secretkey_to_publickey(nsec_or_hex: string): string | undefined;
+};
diff --git a/utils-nostr/src/util.ts b/utils-nostr/src/util.ts
@@ -0,0 +1,5 @@
+import { v4 } from "uuid";
+
+export const time_now_ms = (): number => Math.floor(new Date().getTime() / 1000);
+
+export const uuidv4 = (): string => v4();
+\ No newline at end of file