commit 998b84da27aad5a0179086630e5232e67d5caac6
parent 6fb816c4b3c21e8f99eb425e8be47b69099a7cb8
Author: triesap <triesap@radroots.dev>
Date: Wed, 24 Dec 2025 20:41:53 +0000
nostr: replace NDK integration with @radroots/nostr packages
- Remove @nostr-dev-kit/ndk deps and delete apps-lib NDK stores/types/utils
- Rename utils-nostr to @radroots/nostr and add @radroots/apps-nostr session/stores module
- Update client imports to @radroots/nostr and align public env vars for Nostr client config
- Add idb_store_ensure and async store init across cipher/keystore/datastore/crypto registry/sql
Diffstat:
117 files changed, 2479 insertions(+), 2054 deletions(-)
diff --git a/apps-lib-market/package.json b/apps-lib-market/package.json
@@ -50,11 +50,10 @@
},
"dependencies": {
"@radroots/apps-lib": "workspace:*",
- "@radroots/utils-nostr": "workspace:*",
"@radroots/utils": "workspace:*",
"@radroots/core-bindings": "workspace:*",
"@radroots/events-bindings": "workspace:*",
"@radroots/events-indexed-bindings": "workspace:*",
"@radroots/trade-bindings": "workspace:*"
}
-}
-\ No newline at end of file
+}
diff --git a/apps-lib-pwa/package.json b/apps-lib-pwa/package.json
@@ -85,10 +85,6 @@
"@radroots/tangle-schema-bindings": "workspace:*",
"@radroots/themes": "workspace:*",
"@radroots/utils": "workspace:*",
- "@radroots/utils-nostr": "workspace:*",
- "@nostr-dev-kit/ndk": "2.14.33",
- "@nostr-dev-kit/ndk-cache-dexie": "2.6.34",
- "@nostr-dev-kit/ndk-svelte": "2.4.38",
"@sveltekit-i18n/base": "^1.3.7",
"@sveltekit-i18n/parser-icu": "^1.0.8",
"luxon": "^3.5.0",
diff --git a/apps-lib/.env.example b/apps-lib/.env.example
@@ -1,4 +1,3 @@
VITE_PUBLIC_KEYVAL_NAME=
-VITE_PUBLIC_NDK_CACHE=
-VITE_PUBLIC_NDK_CLIENT=
+VITE_PUBLIC_NOSTR_CLIENT=
VITE_PUBLIC_RADROOTS_RELAY=
diff --git a/apps-lib/package.json b/apps-lib/package.json
@@ -52,11 +52,7 @@
"@radroots/geo": "workspace:*",
"@radroots/locales": "workspace:*",
"@radroots/utils": "workspace:*",
- "@radroots/utils-nostr": "workspace:*",
"@radroots/themes": "workspace:*",
- "@nostr-dev-kit/ndk": "2.14.33",
- "@nostr-dev-kit/ndk-cache-dexie": "2.6.34",
- "@nostr-dev-kit/ndk-svelte": "2.4.38",
"@sveltekit-i18n/base": "^1.3.7",
"@sveltekit-i18n/parser-icu": "^1.0.8",
"luxon": "^3.5.0",
diff --git a/apps-lib/src/lib/index.ts b/apps-lib/src/lib/index.ts
@@ -1,12 +1,10 @@
export * from "./index.js";
export * from "./stores/app.js";
export * from "./stores/carousel.js";
-export * from "./stores/ndk.js";
export * from "./stores/theme.js";
export * from "./styles/glyphs.js";
export * from "./types/components.js";
export * from "./types/lib.js";
-export * from "./types/ndk.js";
export * from "./types/ui.js";
export * from "./utils/app/carousel.js";
export * from "./utils/app/lib.js";
@@ -16,8 +14,6 @@ export * from "./utils/geo.js";
export * from "./utils/i18n.js";
export * from "./utils/keyval/idb.js";
export * from "./utils/keyval/lib.js";
-export * from "./utils/nostr/lib.js";
-export * from "./utils/nostr/ndk.js";
export { default as Fade } from "./components/fade.svelte";
export { default as Flex } from "./components/flex.svelte";
export { default as Glyph } from "./components/glyph.svelte";
diff --git a/apps-lib/src/lib/stores/ndk.ts b/apps-lib/src/lib/stores/ndk.ts
@@ -1,18 +0,0 @@
-import { _env_lib } from "$lib/utils/_env";
-import { type NDKCacheAdapter, type NDKUser } from "@nostr-dev-kit/ndk";
-import NDKCacheAdapterDexie from "@nostr-dev-kit/ndk-cache-dexie";
-import NDKSvelte from "@nostr-dev-kit/ndk-svelte";
-import { writable } from "svelte/store";
-
-let cache_adapter: NDKCacheAdapter | undefined;
-if (typeof window !== `undefined`) cache_adapter = new NDKCacheAdapterDexie({ dbName: _env_lib.NDK_CACHE });
-
-let cache_adapter_global: NDKCacheAdapter | undefined;
-if (typeof window !== `undefined`) cache_adapter_global = new NDKCacheAdapterDexie({ dbName: `${_env_lib.NDK_CACHE}-global` });
-
-const ndk_i = new NDKSvelte({ cacheAdapter: cache_adapter, clientName: _env_lib.NDK_CLIENT, explicitRelayUrls: [_env_lib.RADROOTS_RELAY], autoConnectUserRelays: true, autoFetchUserMutelist: true });
-export const ndk = writable<NDKSvelte>(ndk_i);
-export const ndk_user = writable<NDKUser>();
-
-const ndk_global_i = new NDKSvelte({ cacheAdapter: cache_adapter_global, clientName: _env_lib.NDK_CLIENT, autoConnectUserRelays: true, autoFetchUserMutelist: true });
-export const ndk_global = writable<NDKSvelte>(ndk_global_i);
diff --git a/apps-lib/src/lib/types/ndk.ts b/apps-lib/src/lib/types/ndk.ts
@@ -1,3 +0,0 @@
-import type { NDKEvent } from "@nostr-dev-kit/ndk";
-
-export type LibNdkEvent = NDKEvent;
-\ No newline at end of file
diff --git a/apps-lib/src/lib/utils/_env.ts b/apps-lib/src/lib/utils/_env.ts
@@ -1,12 +1,6 @@
const KEYVAL_NAME = import.meta.env.VITE_PUBLIC_KEYVAL_NAME;
if (!KEYVAL_NAME || typeof KEYVAL_NAME !== 'string') throw new Error('Missing env var: VITE_PUBLIC_KEYVAL_NAME');
-const NDK_CACHE = import.meta.env.VITE_PUBLIC_NDK_CACHE;
-if (!NDK_CACHE || typeof NDK_CACHE !== 'string') throw new Error('Missing env var: VITE_PUBLIC_NDK_CACHE');
-
-const NDK_CLIENT = import.meta.env.VITE_PUBLIC_NDK_CLIENT;
-if (!NDK_CLIENT || typeof NDK_CLIENT !== 'string') throw new Error('Missing env var: VITE_PUBLIC_NDK_CLIENT');
-
const RADROOTS_RELAY = import.meta.env.VITE_PUBLIC_RADROOTS_RELAY;
if (!RADROOTS_RELAY || typeof RADROOTS_RELAY !== 'string') throw new Error('Missing env var: VITE_PUBLIC_RADROOTS_RELAY');
@@ -15,7 +9,5 @@ const PROD = import.meta.env.MODE === 'production';
export const _env_lib = {
PROD,
KEYVAL_NAME,
- NDK_CACHE,
- NDK_CLIENT,
RADROOTS_RELAY,
} as const;
diff --git a/apps-lib/src/lib/utils/nostr/lib.ts b/apps-lib/src/lib/utils/nostr/lib.ts
@@ -1,43 +0,0 @@
-import { get_store } from "$lib";
-import { ndk, ndk_user } from "$lib/stores/ndk";
-import { NDKNip07Signer, NDKPrivateKeySigner, NDKUser } from "@nostr-dev-kit/ndk";
-
-export type NostrLoginOptionNip05 = {
- nip07: true;
-};
-
-export type NostrLoginOptionNostrKey = {
- nostr_key: Uint8Array | string;
-};
-
-export type NostrLoginOptions =
- | NostrLoginOptionNip05
- | NostrLoginOptionNostrKey
-
-
-export const nostr_logout = async (): Promise<void> => {
- const ndk_val = get_store(ndk);
- ndk_val.activeUser = undefined;
- document.cookie = "radroots_npub=";
- console.log(`logged out (nostr)`)
-
-};
-
-export const nostr_login = async (opts: NostrLoginOptions): Promise<void> => {
- const ndk_val = get_store(ndk);
- let user: NDKUser | null = null;
- if (`nip07` in opts) {
- const signer = new NDKNip07Signer();
- ndk_val.signer = signer;
- user = await ndk_val.signer.user();
- user.ndk = ndk_val;
- } else if (`nostr_key` in opts) {
- const signer = new NDKPrivateKeySigner(opts.nostr_key);
- ndk_val.signer = signer;
- user = await ndk_val.signer.user();
- user.ndk = ndk_val;
- }
- if (!user) return;
- ndk_user.set(user);
- ndk_val.activeUser = user;
-};
diff --git a/apps-lib/src/lib/utils/nostr/ndk.ts b/apps-lib/src/lib/utils/nostr/ndk.ts
@@ -1,12 +0,0 @@
-import { NDKPrivateKeySigner, type NDKUser } from "@nostr-dev-kit/ndk";
-import NDKSvelte from "@nostr-dev-kit/ndk-svelte";
-import { throw_err } from "@radroots/utils";
-
-export const ndk_init = async (ndk: NDKSvelte, secret_key: string): Promise<NDKUser> => {
- const signer = new NDKPrivateKeySigner(secret_key);
- ndk.signer = signer;
- const user = await signer.user();
- if (!user) throw_err("*-user");
- user.ndk = ndk;
- return user;
-};
-\ No newline at end of file
diff --git a/utils-nostr/.gitignore b/apps-nostr/.gitignore
diff --git a/utils-nostr/LICENSE b/apps-nostr/LICENSE
diff --git a/apps-nostr/package.json b/apps-nostr/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@radroots/apps-nostr",
+ "version": "0.0.1",
+ "private": true,
+ "license": "GPLv3",
+ "type": "module",
+ "main": "./dist/cjs/index.js",
+ "module": "./dist/esm/index.js",
+ "types": "./dist/types/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/types/index.d.ts",
+ "import": "./dist/esm/index.js",
+ "require": "./dist/cjs/index.js"
+ }
+ },
+ "scripts": {
+ "build:esm": "tsc -p tsconfig.esm.json",
+ "build:cjs": "tsc -p tsconfig.cjs.json",
+ "build": "npm run clean && npm run build:esm && npm run build:cjs",
+ "prebuild": "npm run clean",
+ "clean": "rimraf dist",
+ "dev": "npm run watch",
+ "watch": "tsc -w"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "peerDependencies": {
+ "svelte": "^5.0.0"
+ },
+ "devDependencies": {
+ "@radroots/tsconfig": "workspace:*",
+ "@types/node": "^22.13.1",
+ "rimraf": "^6.0.1",
+ "svelte": "^5.0.0",
+ "typescript": "5.8.3"
+ },
+ "dependencies": {
+ "@radroots/nostr": "workspace:*",
+ "@welshman/app": "workspace:*",
+ "@welshman/net": "workspace:*",
+ "@welshman/signer": "workspace:*",
+ "@welshman/store": "workspace:*"
+ }
+}
diff --git a/apps-nostr/src/index.ts b/apps-nostr/src/index.ts
@@ -0,0 +1,2 @@
+export * from "./session.js";
+export * from "./stores.js";
diff --git a/apps-nostr/src/session.ts b/apps-nostr/src/session.ts
@@ -0,0 +1,53 @@
+import { nostr_public_key_from_secret, nostr_signer_nip07_get } from "@radroots/nostr";
+import {
+ dropSession,
+ loginWithNip01,
+ loginWithNip07,
+ loginWithNip46,
+ loginWithNip55,
+ loginWithPubkey,
+ pubkey,
+} from "@welshman/app";
+
+export type NostrLoginNip46Options = {
+ pubkey: string;
+ client_secret: string;
+ signer_pubkey: string;
+ relays: string[];
+};
+
+export type NostrLoginNip55Options = {
+ pubkey: string;
+ signer: string;
+};
+
+export const nostr_login_nip01 = (secret_key: string): string => {
+ loginWithNip01(secret_key);
+ return nostr_public_key_from_secret(secret_key);
+};
+
+export const nostr_login_nip07 = async (): Promise<string | undefined> => {
+ const nip07 = nostr_signer_nip07_get();
+ if (!nip07) return undefined;
+ const pubkey_val = nip07.getPublicKey();
+ if (!pubkey_val) return undefined;
+ loginWithNip07(pubkey_val);
+ return pubkey_val;
+};
+
+export const nostr_login_nip46 = (opts: NostrLoginNip46Options): void => {
+ loginWithNip46(opts.pubkey, opts.client_secret, opts.signer_pubkey, opts.relays);
+};
+
+export const nostr_login_nip55 = (opts: NostrLoginNip55Options): void => {
+ loginWithNip55(opts.pubkey, opts.signer);
+};
+
+export const nostr_login_pubkey = (pubkey_val: string): void => {
+ loginWithPubkey(pubkey_val);
+};
+
+export const nostr_logout = (): void => {
+ const pubkey_val = pubkey.get();
+ if (pubkey_val) dropSession(pubkey_val);
+};
diff --git a/apps-nostr/src/stores.ts b/apps-nostr/src/stores.ts
@@ -0,0 +1,20 @@
+import type { NostrUser } from "@radroots/nostr";
+import type { Session, SignerLogEntry } from "@welshman/app";
+import { pubkey, repository, session, sessions, signer, signerLog, tracker } from "@welshman/app";
+import type { Repository, Tracker } from "@welshman/net";
+import type { ISigner } from "@welshman/signer";
+import type { ReadableWithGetter, WritableWithGetter } from "@welshman/store";
+import { derived, type Readable } from "svelte/store";
+
+export const nostr_pubkey: WritableWithGetter<string | undefined> = pubkey;
+export const nostr_sessions: WritableWithGetter<Record<string, Session>> = sessions;
+export const nostr_session: ReadableWithGetter<Session | undefined> = session;
+export const nostr_signer: ReadableWithGetter<ISigner | undefined> = signer;
+export const nostr_signer_log: WritableWithGetter<SignerLogEntry[]> = signerLog;
+export const nostr_repository: Repository = repository;
+export const nostr_tracker: Tracker = tracker;
+
+export const nostr_user: Readable<NostrUser | undefined> = derived(
+ pubkey,
+ pubkey_val => (pubkey_val ? { pubkey: pubkey_val } : undefined),
+);
diff --git a/apps-nostr/tsconfig.cjs.json b/apps-nostr/tsconfig.cjs.json
@@ -0,0 +1,15 @@
+{
+ "extends": "@radroots/tsconfig/tsconfig.esm.json",
+ "compilerOptions": {
+ "module": "CommonJS",
+ "moduleResolution": "Node",
+ "rootDir": "./src",
+ "outDir": "dist/cjs",
+ "declaration": false,
+ "declarationMap": false,
+ "emitDeclarationOnly": false,
+ "tsBuildInfoFile": "node_modules/.cache/tsc.apps-nostr.cjs.tsbuildinfo"
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/utils-nostr/tsconfig.esm.json b/apps-nostr/tsconfig.esm.json
diff --git a/utils-nostr/tsconfig.json b/apps-nostr/tsconfig.json
diff --git a/client/package.json b/client/package.json
@@ -82,7 +82,7 @@
"@radroots/tangle-sql-wasm": "workspace:*",
"@radroots/types-bindings": "workspace:*",
"@radroots/utils": "workspace:*",
- "@radroots/utils-nostr": "workspace:*",
+ "@radroots/nostr": "workspace:*",
"idb": "^8.0.3",
"idb-keyval": "^6.2.1",
"sql.js": "1.13.0"
@@ -97,4 +97,4 @@
"publishConfig": {
"access": "public"
}
-}
-\ No newline at end of file
+}
diff --git a/client/src/cipher/web.ts b/client/src/cipher/web.ts
@@ -5,6 +5,7 @@ import { crypto_registry_clear_key_entry, crypto_registry_clear_store_index, cry
import { WebCryptoService } from "../crypto/service.js";
import type { LegacyKeyConfig } from "../crypto/types.js";
import { IDB_CONFIG_CIPHER_AES_GCM } from "../idb/config.js";
+import { idb_store_ensure } from "../idb/store.js";
import { cl_cipher_error } from "./error.js";
import type { IClientCipher } from "./types.js";
@@ -25,10 +26,11 @@ export class WebAesGcmCipher implements IWebAesGcmCipher {
private readonly key_name: string;
private readonly algorithm_name: string;
private readonly iv_length: number;
- private readonly legacy_store: UseStore;
+ private legacy_store: UseStore | null;
private readonly store_id: string;
private readonly crypto: WebCryptoService;
private readonly legacy_key_config: LegacyKeyConfig;
+ private store_ready: Promise<void> | null = null;
constructor(config?: WebAesGcmCipherConfig) {
const idb_config = config?.idb_config ?? {};
@@ -43,7 +45,7 @@ export class WebAesGcmCipher implements IWebAesGcmCipher {
if (typeof indexedDB === "undefined") throw new Error(cl_cipher_error.idb_undefined);
if (!globalThis.crypto || !globalThis.crypto.subtle) throw new Error(cl_cipher_error.crypto_undefined);
- this.legacy_store = createStore(this.db_name, this.store_name);
+ this.legacy_store = null;
this.store_id = this.key_name;
this.crypto = new WebCryptoService();
this.legacy_key_config = {
@@ -69,13 +71,21 @@ export class WebAesGcmCipher implements IWebAesGcmCipher {
};
}
+ private async get_store(): Promise<UseStore> {
+ if (!this.store_ready) this.store_ready = idb_store_ensure(this.db_name, this.store_name);
+ await this.store_ready;
+ if (!this.legacy_store) this.legacy_store = createStore(this.db_name, this.store_name);
+ return this.legacy_store;
+ }
+
public async reset(): Promise<void> {
+ const store = await this.get_store();
const index = await crypto_registry_get_store_index(this.store_id);
if (index) {
await crypto_registry_clear_store_index(this.store_id);
for (const key_id of index.key_ids) await crypto_registry_clear_key_entry(key_id);
}
- await idb_del(this.key_name, this.legacy_store);
+ await idb_del(this.key_name, store);
}
public async encrypt(data: Uint8Array): Promise<Uint8Array> {
diff --git a/client/src/crypto/registry.ts b/client/src/crypto/registry.ts
@@ -1,18 +1,26 @@
-import { createStore, del as idb_del, get as idb_get, keys as idb_keys, set as idb_set } from "idb-keyval";
+import { createStore, del as idb_del, get as idb_get, keys as idb_keys, set as idb_set, type UseStore } from "idb-keyval";
import type { IdbClientConfig } from "@radroots/utils";
import { IDB_CONFIG_CRYPTO_REGISTRY } from "../idb/config.js";
+import { idb_store_ensure } from "../idb/store.js";
import { cl_crypto_error } from "./error.js";
import type { CryptoKeyEntry, CryptoRegistryExport, CryptoStoreIndex } from "./types.js";
const CRYPTO_IDB_CONFIG: IdbClientConfig = IDB_CONFIG_CRYPTO_REGISTRY;
-const CRYPTO_STORE = createStore(CRYPTO_IDB_CONFIG.database, CRYPTO_IDB_CONFIG.store);
+let crypto_store: UseStore | null = null;
const STORE_INDEX_PREFIX = "store:";
const KEY_ENTRY_PREFIX = "key:";
const DEVICE_MATERIAL_KEY = "device:material";
-const ensure_idb = (): void => {
+const ensure_idb = async (): Promise<void> => {
if (typeof indexedDB === "undefined") throw new Error(cl_crypto_error.idb_undefined);
+ await idb_store_ensure(CRYPTO_IDB_CONFIG.database, CRYPTO_IDB_CONFIG.store);
+};
+
+const get_crypto_store = async (): Promise<UseStore> => {
+ await ensure_idb();
+ if (!crypto_store) crypto_store = createStore(CRYPTO_IDB_CONFIG.database, CRYPTO_IDB_CONFIG.store);
+ return crypto_store;
};
const store_index_key = (store_id: string): string => `${STORE_INDEX_PREFIX}${store_id}`;
@@ -48,38 +56,38 @@ const is_crypto_key_entry = (value: unknown): value is CryptoKeyEntry => {
};
export const crypto_registry_get_store_index = async (store_id: string): Promise<CryptoStoreIndex | null> => {
- ensure_idb();
- const record = await idb_get(store_index_key(store_id), CRYPTO_STORE);
+ const store = await get_crypto_store();
+ const record = await idb_get(store_index_key(store_id), store);
if (!record) return null;
if (!is_crypto_store_index(record)) throw new Error(cl_crypto_error.registry_failure);
return record;
};
export const crypto_registry_set_store_index = async (index: CryptoStoreIndex): Promise<void> => {
- ensure_idb();
- await idb_set(store_index_key(index.store_id), index, CRYPTO_STORE);
+ const store = await get_crypto_store();
+ await idb_set(store_index_key(index.store_id), index, store);
};
export const crypto_registry_get_key_entry = async (key_id: string): Promise<CryptoKeyEntry | null> => {
- ensure_idb();
- const record = await idb_get(key_entry_key(key_id), CRYPTO_STORE);
+ const store = await get_crypto_store();
+ const record = await idb_get(key_entry_key(key_id), store);
if (!record) return null;
if (!is_crypto_key_entry(record)) throw new Error(cl_crypto_error.registry_failure);
return record;
};
export const crypto_registry_set_key_entry = async (entry: CryptoKeyEntry): Promise<void> => {
- ensure_idb();
- await idb_set(key_entry_key(entry.key_id), entry, CRYPTO_STORE);
+ const store = await get_crypto_store();
+ await idb_set(key_entry_key(entry.key_id), entry, store);
};
export const crypto_registry_list_store_indices = async (): Promise<CryptoStoreIndex[]> => {
- ensure_idb();
- const keys = await idb_keys(CRYPTO_STORE);
+ const store = await get_crypto_store();
+ const keys = await idb_keys(store);
const store_keys = keys.filter((key): key is string => typeof key === "string" && key.startsWith(STORE_INDEX_PREFIX));
const out: CryptoStoreIndex[] = [];
for (const key of store_keys) {
- const record = await idb_get(key, CRYPTO_STORE);
+ const record = await idb_get(key, store);
if (!record) continue;
if (!is_crypto_store_index(record)) throw new Error(cl_crypto_error.registry_failure);
out.push(record);
@@ -88,12 +96,12 @@ export const crypto_registry_list_store_indices = async (): Promise<CryptoStoreI
};
export const crypto_registry_list_key_entries = async (): Promise<CryptoKeyEntry[]> => {
- ensure_idb();
- const keys = await idb_keys(CRYPTO_STORE);
+ const store = await get_crypto_store();
+ const keys = await idb_keys(store);
const entry_keys = keys.filter((key): key is string => typeof key === "string" && key.startsWith(KEY_ENTRY_PREFIX));
const out: CryptoKeyEntry[] = [];
for (const key of entry_keys) {
- const record = await idb_get(key, CRYPTO_STORE);
+ const record = await idb_get(key, store);
if (!record) continue;
if (!is_crypto_key_entry(record)) throw new Error(cl_crypto_error.registry_failure);
out.push(record);
@@ -108,14 +116,14 @@ export const crypto_registry_export = async (): Promise<CryptoRegistryExport> =>
};
export const crypto_registry_import = async (registry: CryptoRegistryExport): Promise<void> => {
- ensure_idb();
+ await get_crypto_store();
for (const store of registry.stores) await crypto_registry_set_store_index(store);
for (const entry of registry.keys) await crypto_registry_set_key_entry(entry);
};
export const crypto_registry_get_device_material = async (): Promise<Uint8Array | null> => {
- ensure_idb();
- const record = await idb_get(DEVICE_MATERIAL_KEY, CRYPTO_STORE);
+ const store = await get_crypto_store();
+ const record = await idb_get(DEVICE_MATERIAL_KEY, store);
if (!record) return null;
if (record instanceof Uint8Array) return record;
if (record instanceof ArrayBuffer) return new Uint8Array(record);
@@ -124,16 +132,16 @@ export const crypto_registry_get_device_material = async (): Promise<Uint8Array
};
export const crypto_registry_set_device_material = async (material: Uint8Array): Promise<void> => {
- ensure_idb();
- await idb_set(DEVICE_MATERIAL_KEY, material, CRYPTO_STORE);
+ const store = await get_crypto_store();
+ await idb_set(DEVICE_MATERIAL_KEY, material, store);
};
export const crypto_registry_clear_store_index = async (store_id: string): Promise<void> => {
- ensure_idb();
- await idb_del(store_index_key(store_id), CRYPTO_STORE);
+ const store = await get_crypto_store();
+ await idb_del(store_index_key(store_id), store);
};
export const crypto_registry_clear_key_entry = async (key_id: string): Promise<void> => {
- ensure_idb();
- await idb_del(key_entry_key(key_id), CRYPTO_STORE);
+ const store = await get_crypto_store();
+ await idb_del(key_entry_key(key_id), store);
};
diff --git a/client/src/crypto/service.ts b/client/src/crypto/service.ts
@@ -1,5 +1,6 @@
import { createStore, get as idb_get } from "idb-keyval";
import { as_array_buffer } from "@radroots/utils";
+import { idb_store_ensure } from "../idb/store.js";
import { cl_crypto_error } from "./error.js";
import { crypto_envelope_decode, crypto_envelope_encode } from "./envelope.js";
import { crypto_kdf_derive_kek, crypto_kdf_iterations_default, crypto_kdf_salt_create } from "./kdf.js";
@@ -267,6 +268,7 @@ export class WebCryptoService implements IWebCryptoService {
private async load_legacy_key(legacy: LegacyKeyConfig): Promise<CryptoKey | null> {
if (typeof indexedDB === "undefined") return null;
+ await idb_store_ensure(legacy.idb_config.database, legacy.idb_config.store);
const legacy_store = createStore(legacy.idb_config.database, legacy.idb_config.store);
const stored = await idb_get(legacy.key_name, legacy_store);
if (!stored) return null;
diff --git a/client/src/datastore/web.ts b/client/src/datastore/web.ts
@@ -4,6 +4,7 @@ import type { BackupDatastorePayload } from "../backup/types.js";
import { WebCryptoService } from "../crypto/service.js";
import { crypto_registry_clear_key_entry, crypto_registry_clear_store_index, crypto_registry_get_store_index } from "../crypto/registry.js";
import { IDB_CONFIG_DATASTORE } from "../idb/config.js";
+import { idb_store_ensure } from "../idb/store.js";
import { cl_datastore_error } from "./error.js";
import type {
IClientDatastore,
@@ -32,6 +33,7 @@ export class WebDatastore<
private db_name: string;
private store_name: string;
private store: UseStore | null = null;
+ private store_ready: Promise<void> | null = null;
private _key_map: Tk;
private _key_param_map: Tp;
private _key_obj_map: TkO;
@@ -53,11 +55,11 @@ export class WebDatastore<
});
}
- private get_store(): UseStore {
- if (!this.store) {
- if (typeof indexedDB === "undefined") throw new Error(cl_datastore_error.idb_undefined);
- this.store = createStore(this.db_name, this.store_name);
- }
+ private async get_store(): Promise<UseStore> {
+ if (typeof indexedDB === "undefined") throw new Error(cl_datastore_error.idb_undefined);
+ if (!this.store_ready) this.store_ready = idb_store_ensure(this.db_name, this.store_name);
+ await this.store_ready;
+ if (!this.store) this.store = createStore(this.db_name, this.store_name);
return this.store;
}
@@ -71,13 +73,17 @@ export class WebDatastore<
private async decrypt_value(store_key: string, stored: unknown): Promise<ResolveError<ResultObj<string>>> {
if (typeof stored === "string") {
const encrypted = await this.crypto.encrypt(this.store_id, text_enc(stored));
- await idb_set(store_key, encrypted, this.get_store());
+ const store = await this.get_store();
+ await idb_set(store_key, encrypted, store);
return { result: stored };
}
const bytes = this.as_bytes(stored);
if (!bytes) return err_msg(cl_datastore_error.no_result);
const outcome = await this.crypto.decrypt_record(this.store_id, bytes);
- if (outcome.reencrypted) await idb_set(store_key, outcome.reencrypted, this.get_store());
+ if (outcome.reencrypted) {
+ const store = await this.get_store();
+ await idb_set(store_key, outcome.reencrypted, store);
+ }
return { result: text_dec(outcome.plaintext) };
}
@@ -94,7 +100,7 @@ export class WebDatastore<
public async init(): Promise<ResolveError<void>> {
try {
- this.get_store();
+ await this.get_store();
} catch (e) {
return handle_err(e);
}
@@ -103,7 +109,8 @@ export class WebDatastore<
public async set(key: keyof Tk, value: string): Promise<ResolveError<ResultObj<string>>> {
try {
const encrypted = await this.crypto.encrypt(this.store_id, text_enc(value));
- await idb_set(this._key_map[key], encrypted, this.get_store());
+ const store = await this.get_store();
+ await idb_set(this._key_map[key], encrypted, store);
return { result: value };
} catch (e) {
return handle_err(e);
@@ -113,7 +120,8 @@ export class WebDatastore<
public async get(key: keyof Tk): Promise<ResolveError<ResultObj<string>>> {
try {
const store_key = this._key_map[key];
- const value = await idb_get(store_key, this.get_store());
+ const store = await this.get_store();
+ const value = await idb_get(store_key, store);
if (!value) return err_msg(cl_datastore_error.no_result);
return await this.decrypt_value(store_key, value);
} catch (e) {
@@ -123,7 +131,8 @@ export class WebDatastore<
public async del(key: keyof Tk): Promise<IClientDatastoreDelResolve> {
try {
- await idb_del(this._key_map[key], this.get_store());
+ const store = await this.get_store();
+ await idb_del(this._key_map[key], store);
return { result: key.toString() };
} catch (e) {
return handle_err(e);
@@ -134,7 +143,8 @@ export class WebDatastore<
try {
const serialized = JSON.stringify(value);
const encrypted = await this.crypto.encrypt(this.store_id, text_enc(serialized));
- await idb_set(this._key_obj_map[key], encrypted, this.get_store());
+ const store = await this.get_store();
+ await idb_set(this._key_obj_map[key], encrypted, store);
return { result: value };
} catch (e) {
return handle_err(e);
@@ -143,9 +153,10 @@ export class WebDatastore<
public async update_obj<T extends Record<string, unknown>>(key: keyof TkO, value: Partial<T>): Promise<ResolveError<ResultObj<T>>> {
try {
+ const store = await this.get_store();
const k = this._key_obj_map[key];
const obj_curr: Record<string, unknown> = {};
- const curr = await idb_get(k, this.get_store());
+ const curr = await idb_get(k, store);
if (curr) {
const decrypted = await this.decrypt_value(k, curr);
if ("err" in decrypted) return decrypted;
@@ -155,7 +166,7 @@ export class WebDatastore<
const obj: T = { ...obj_curr, ...value } as T;
const serialized = JSON.stringify(obj);
const encrypted = await this.crypto.encrypt(this.store_id, text_enc(serialized));
- await idb_set(k, encrypted, this.get_store());
+ await idb_set(k, encrypted, store);
return { result: obj };
} catch (e) {
return handle_err(e);
@@ -165,7 +176,8 @@ export class WebDatastore<
public async get_obj<T>(key: keyof TkO): Promise<ResolveError<ResultObj<T>>> {
try {
const store_key = this._key_obj_map[key];
- const value = await idb_get(store_key, this.get_store());
+ const store = await this.get_store();
+ const value = await idb_get(store_key, store);
if (!value) return err_msg(cl_datastore_error.no_result);
const decrypted = await this.decrypt_value(store_key, value);
if ("err" in decrypted) return decrypted;
@@ -177,7 +189,8 @@ export class WebDatastore<
public async del_obj(key: keyof TkO): Promise<ResolveError<ResultObj<string>>> {
try {
- await idb_del(this._key_obj_map[key], this.get_store());
+ const store = await this.get_store();
+ await idb_del(this._key_obj_map[key], store);
return { result: key.toString() };
} catch (e) {
return handle_err(e);
@@ -192,7 +205,8 @@ export class WebDatastore<
try {
const store_key = this._key_param_map[key](key_param);
const encrypted = await this.crypto.encrypt(this.store_id, text_enc(value));
- await idb_set(store_key, encrypted, this.get_store());
+ const store = await this.get_store();
+ await idb_set(store_key, encrypted, store);
return { result: value };
} catch (e) {
return handle_err(e);
@@ -205,7 +219,8 @@ export class WebDatastore<
): Promise<ResolveError<ResultObj<string>>> {
try {
const store_key = this._key_param_map[key](key_param);
- const value = await idb_get(store_key, this.get_store());
+ const store = await this.get_store();
+ const value = await idb_get(store_key, store);
if (!value) return err_msg(cl_datastore_error.no_result);
return await this.decrypt_value(store_key, value);
} catch (e) {
@@ -215,10 +230,11 @@ export class WebDatastore<
public async del_pref(key_prefix: string): Promise<IClientDatastoreDelPrefResolve> {
try {
- const all_keys = await idb_keys(this.get_store());
+ const store = await this.get_store();
+ const all_keys = await idb_keys(store);
const filtered_keys = all_keys.filter((k): k is string => (typeof k === "string" && k.startsWith(key_prefix)));
for (const key of filtered_keys) {
- await idb_del(key, this.get_store());
+ await idb_del(key, store);
}
return { results: filtered_keys };
} catch (e) {
@@ -228,7 +244,8 @@ export class WebDatastore<
public async keys(): Promise<ResolveError<ResultsList<string>>> {
try {
- const all_keys = await idb_keys(this.get_store());
+ const store = await this.get_store();
+ const all_keys = await idb_keys(store);
return { results: all_keys.filter((k): k is string => typeof k === "string") };
} catch (e) {
return handle_err(e);
@@ -237,11 +254,12 @@ export class WebDatastore<
public async export_backup(): Promise<ResolveError<BackupDatastorePayload>> {
try {
- const all_keys = await idb_keys(this.get_store());
+ const store = await this.get_store();
+ const all_keys = await idb_keys(store);
const entries: BackupDatastorePayload["entries"] = [];
for (const key of all_keys) {
if (typeof key !== "string") continue;
- const value = await idb_get(key, this.get_store());
+ const value = await idb_get(key, store);
if (!value) continue;
const decrypted = await this.decrypt_value(key, value);
if ("err" in decrypted) return decrypted;
@@ -255,9 +273,10 @@ export class WebDatastore<
public async import_backup(payload: BackupDatastorePayload): Promise<ResolveError<void>> {
try {
+ const store = await this.get_store();
for (const entry of payload.entries) {
const encrypted = await this.crypto.encrypt(this.store_id, text_enc(entry.value));
- await idb_set(entry.key, encrypted, this.get_store());
+ await idb_set(entry.key, encrypted, store);
}
return;
} catch (e) {
@@ -267,7 +286,8 @@ export class WebDatastore<
public async reset(): Promise<ResolveError<ResultPass>> {
try {
- await idb_clear(this.get_store());
+ const store = await this.get_store();
+ await idb_clear(store);
const index = await crypto_registry_get_store_index(this.store_id);
if (index) {
await crypto_registry_clear_store_index(this.store_id);
diff --git a/client/src/idb/store.ts b/client/src/idb/store.ts
@@ -0,0 +1,71 @@
+import {
+ IDB_STORE_CIPHER_AES_GCM,
+ IDB_STORE_CIPHER_SQL,
+ IDB_STORE_CIPHER_SUFFIX,
+ IDB_STORE_CRYPTO_REGISTRY,
+ IDB_STORE_DATASTORE,
+ IDB_STORE_KEYSTORE,
+ IDB_STORE_KEYSTORE_NOSTR,
+ IDB_STORE_TANGLE,
+ RADROOTS_IDB_DATABASE
+} from "./config.js";
+
+const RADROOTS_IDB_STORES = [
+ IDB_STORE_DATASTORE,
+ IDB_STORE_KEYSTORE,
+ IDB_STORE_KEYSTORE_NOSTR,
+ IDB_STORE_CRYPTO_REGISTRY,
+ IDB_STORE_CIPHER_AES_GCM,
+ IDB_STORE_CIPHER_SQL,
+ IDB_STORE_TANGLE,
+ `${IDB_STORE_KEYSTORE}${IDB_STORE_CIPHER_SUFFIX}`,
+ `${IDB_STORE_KEYSTORE_NOSTR}${IDB_STORE_CIPHER_SUFFIX}`
+];
+
+const idb_missing_stores = (db: IDBDatabase, stores: string[]): string[] =>
+ stores.filter((store) => !db.objectStoreNames.contains(store));
+
+const idb_open = (database: string, version?: number, stores?: string[]): Promise<IDBDatabase> =>
+ new Promise((resolve, reject) => {
+ const request = indexedDB.open(database, version);
+ request.onupgradeneeded = () => {
+ if (!stores || stores.length === 0) return;
+ const db = request.result;
+ for (const store of stores) {
+ if (!db.objectStoreNames.contains(store)) db.createObjectStore(store);
+ }
+ };
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => {
+ if (request.error) reject(request.error);
+ else reject(new Error("idb_open_failed"));
+ };
+ });
+
+export const idb_store_ensure = async (database: string, store: string): Promise<void> => {
+ if (typeof indexedDB === "undefined") return;
+ const target_stores = database === RADROOTS_IDB_DATABASE
+ ? Array.from(new Set([...RADROOTS_IDB_STORES, store]))
+ : [store];
+ let attempt = 0;
+ while (attempt < 5) {
+ attempt++;
+ const db = await idb_open(database);
+ const missing = idb_missing_stores(db, target_stores);
+ const version = db.version;
+ if (missing.length === 0) {
+ db.close();
+ return;
+ }
+ db.close();
+ try {
+ const upgraded = await idb_open(database, version + 1, missing);
+ const still_missing = idb_missing_stores(upgraded, target_stores);
+ upgraded.close();
+ if (still_missing.length === 0) return;
+ } catch (e) {
+ if (e instanceof DOMException && e.name === "VersionError") continue;
+ throw e;
+ }
+ }
+};
diff --git a/client/src/keystore/web-nostr.ts b/client/src/keystore/web-nostr.ts
@@ -9,7 +9,11 @@ import {
type ResultSecretKey,
type ResultsList
} from '@radroots/utils';
-import { lib_nostr_key_generate, lib_nostr_public_key, lib_nostr_secret_key_validate } from '@radroots/utils-nostr';
+import {
+ nostr_key_generate,
+ nostr_public_key_from_secret,
+ nostr_secret_key_validate
+} from "@radroots/nostr";
import { cl_keystore_error } from "./error.js";
import type { IClientKeystoreNostr } from './types.js';
import { IDB_CONFIG_KEYSTORE_NOSTR } from "../idb/config.js";
@@ -33,9 +37,9 @@ export class WebKeystoreNostr implements IWebKeystoreNostr {
}
private async add_secret_key(secret_key_raw: string): Promise<ResolveError<ResultObj<string>>> {
- const secret_key = lib_nostr_secret_key_validate(secret_key_raw);
+ const secret_key = nostr_secret_key_validate(secret_key_raw);
if (!secret_key) throw new Error(cl_keystore_error.nostr_invalid_secret_key);
- const public_key = lib_nostr_public_key(secret_key);
+ const public_key = nostr_public_key_from_secret(secret_key);
return await this._keystore.add(public_key, secret_key);
}
@@ -45,7 +49,7 @@ export class WebKeystoreNostr implements IWebKeystoreNostr {
public async generate(): Promise<ResolveError<ResultPublicKey>> {
try {
- const secret_key = lib_nostr_key_generate();
+ const secret_key = nostr_key_generate();
const resolve = await this.add_secret_key(secret_key);
if ("err" in resolve) return resolve;
return { public_key: resolve.result };
diff --git a/client/src/keystore/web.ts b/client/src/keystore/web.ts
@@ -15,6 +15,7 @@ import { WebCryptoService } from "../crypto/service.js";
import type { LegacyKeyConfig } from "../crypto/types.js";
import { crypto_registry_clear_key_entry, crypto_registry_clear_store_index, crypto_registry_get_store_index } from "../crypto/registry.js";
import { IDB_CONFIG_KEYSTORE, IDB_STORE_CIPHER_SUFFIX } from "../idb/config.js";
+import { idb_store_ensure } from "../idb/store.js";
import { cl_keystore_error } from "./error.js";
import type { IClientKeystore, IClientKeystoreValue } from "./types.js";
@@ -36,6 +37,7 @@ export interface IWebKeystore extends IClientKeystore {
export class WebKeystore implements IWebKeystore {
private config: IdbClientConfig;
private store: UseStore | null;
+ private store_ready: Promise<void> | null;
private crypto: WebCryptoService;
private store_id: string;
private legacy_key_config: LegacyKeyConfig;
@@ -46,6 +48,7 @@ export class WebKeystore implements IWebKeystore {
store: config?.store ?? IDB_CONFIG_KEYSTORE.store
};
this.store = null;
+ this.store_ready = null;
this.store_id = `keystore:${this.config.database}:${this.config.store}`;
this.crypto = new WebCryptoService();
const legacy_store = `${this.config.store}${IDB_STORE_CIPHER_SUFFIX}`;
@@ -67,11 +70,11 @@ export class WebKeystore implements IWebKeystore {
});
}
- private get_store(): UseStore {
- if (!this.store) {
- if (typeof indexedDB === "undefined") throw new Error(cl_keystore_error.idb_undefined);
- this.store = createStore(this.config.database, this.config.store);
- }
+ private async get_store(): Promise<UseStore> {
+ if (typeof indexedDB === "undefined") throw new Error(cl_keystore_error.idb_undefined);
+ if (!this.store_ready) this.store_ready = idb_store_ensure(this.config.database, this.config.store);
+ await this.store_ready;
+ if (!this.store) this.store = createStore(this.config.database, this.config.store);
return this.store;
}
@@ -97,7 +100,8 @@ export class WebKeystore implements IWebKeystore {
try {
const bytes = text_enc(value);
const cipher_bytes = await this.crypto.encrypt(this.store_id, bytes);
- await idb_set(key, cipher_bytes, this.get_store());
+ const store = await this.get_store();
+ await idb_set(key, cipher_bytes, store);
return { result: key };
} catch (e) {
return handle_err(e);
@@ -106,7 +110,8 @@ export class WebKeystore implements IWebKeystore {
public async remove(key: string): Promise<ResolveError<ResultObj<string>>> {
try {
- await idb_del(key, this.get_store());
+ const store = await this.get_store();
+ await idb_del(key, store);
return { result: key };
} catch (e) {
return handle_err(e);
@@ -116,11 +121,12 @@ export class WebKeystore implements IWebKeystore {
public async read(key?: string | null): Promise<ResolveError<ResultObj<IClientKeystoreValue>>> {
try {
if (!key) return err_msg(cl_keystore_error.missing_key);
- const cipher_value = await idb_get(key, this.get_store());
+ const store = await this.get_store();
+ const cipher_value = await idb_get(key, store);
const cipher_bytes = this.as_bytes(cipher_value);
if (!cipher_bytes) return err_msg(cl_keystore_error.corrupt_data);
const outcome = await this.crypto.decrypt_record(this.store_id, cipher_bytes);
- if (outcome.reencrypted) await idb_set(key, outcome.reencrypted, this.get_store());
+ if (outcome.reencrypted) await idb_set(key, outcome.reencrypted, store);
const plain = text_dec(outcome.plaintext);
return { result: plain };
} catch (e) {
@@ -130,7 +136,8 @@ export class WebKeystore implements IWebKeystore {
public async keys(): Promise<ResolveError<ResultsList<string>>> {
try {
- const all_keys = await idb_keys(this.get_store());
+ const store = await this.get_store();
+ const all_keys = await idb_keys(store);
return { results: all_keys.filter((k): k is string => typeof k === "string") };
} catch (e) {
return handle_err(e);
@@ -139,7 +146,8 @@ export class WebKeystore implements IWebKeystore {
public async export_backup(): Promise<ResolveError<BackupKeystorePayload>> {
try {
- const all_keys = await idb_keys(this.get_store());
+ const store = await this.get_store();
+ const all_keys = await idb_keys(store);
const entries: BackupKeystorePayload["entries"] = [];
for (const key of all_keys) {
if (typeof key !== "string") continue;
@@ -156,6 +164,7 @@ export class WebKeystore implements IWebKeystore {
public async import_backup(payload: BackupKeystorePayload): Promise<ResolveError<void>> {
try {
+ await this.get_store();
for (const entry of payload.entries) {
const res = await this.add(entry.key, entry.value);
if ("err" in res) return res;
@@ -168,7 +177,8 @@ export class WebKeystore implements IWebKeystore {
public async reset(): Promise<ResolveError<ResultPass>> {
try {
- await idb_clear(this.get_store());
+ const store = await this.get_store();
+ await idb_clear(store);
const index = await crypto_registry_get_store_index(this.store_id);
if (index) {
await crypto_registry_clear_store_index(this.store_id);
diff --git a/client/src/radroots/web.ts b/client/src/radroots/web.ts
@@ -1,6 +1,6 @@
import { err_msg, schema_media_resource } from '@radroots/utils';
import { type IHttpResponse, is_err_response, is_error_response, is_pass_response, WebHttp } from '@radroots/http';
-import { lib_nostr_event_sign_attest } from '@radroots/utils-nostr';
+import { nostr_event_sign_attest } from "@radroots/nostr";
import { cl_radroots_error } from "./error.js";
import type {
IClientRadroots,
@@ -41,7 +41,7 @@ export class WebClientRadroots implements IWebClientRadroots {
}
private create_x_nostr_event(secret_key: string): string {
- return JSON.stringify(lib_nostr_event_sign_attest(secret_key));
+ return JSON.stringify(nostr_event_sign_attest(secret_key));
}
public async accounts_request(opts: IClientRadrootsAccountsRequest): Promise<IClientRadrootsAccountsRequestResolve> {
diff --git a/client/src/sql/web.ts b/client/src/sql/web.ts
@@ -7,6 +7,7 @@ import type { BackupSqlPayload } from "../backup/types.js";
import { WebCryptoService } from "../crypto/service.js";
import type { LegacyKeyConfig } from "../crypto/types.js";
import { IDB_CONFIG_CIPHER_SQL } from "../idb/config.js";
+import { idb_store_ensure } from "../idb/store.js";
import type { IClientSqlEncryptedStore, IWebSqlEngine, SqlJsExecOutcome, SqlJsParams, SqlJsResultRow, WebSqlEngineConfig } from "./types.js";
const DEFAULT_SQL_CIPHER_CONFIG: IdbClientConfig = IDB_CONFIG_CIPHER_SQL;
@@ -22,12 +23,14 @@ class WebSqlEngineEncryptedStore implements IWebSqlEngineEncryptedStore {
private readonly db_name: string;
private readonly store_name: string;
private store: UseStore | null;
+ private store_ready: Promise<void> | null;
constructor(config: WebSqlEngineConfig) {
this.store_key = config.store_key;
this.db_name = config.idb_config.database;
this.store_name = config.idb_config.store;
this.store = null;
+ this.store_ready = null;
this.store_id = `sql:${this.store_key}`;
this.crypto = new WebCryptoService();
const legacy_config: LegacyKeyConfig = {
@@ -54,30 +57,35 @@ class WebSqlEngineEncryptedStore implements IWebSqlEngineEncryptedStore {
return null;
}
- private get_store(): UseStore {
+ private async get_store(): Promise<UseStore> {
+ if (!this.store_ready) this.store_ready = idb_store_ensure(this.db_name, this.store_name);
+ await this.store_ready;
if (!this.store) this.store = createStore(this.db_name, this.store_name);
return this.store;
}
async load(): Promise<Uint8Array | null> {
if (typeof indexedDB === "undefined") return null;
- const data = await idb_get(this.store_key, this.get_store());
+ const store = await this.get_store();
+ const data = await idb_get(this.store_key, store);
const bytes = this.as_bytes(data);
if (!bytes) return null;
const outcome = await this.crypto.decrypt_record(this.store_id, bytes);
- if (outcome.reencrypted) await idb_set(this.store_key, outcome.reencrypted, this.get_store());
+ if (outcome.reencrypted) await idb_set(this.store_key, outcome.reencrypted, store);
return outcome.plaintext;
}
async save(bytes: Uint8Array): Promise<void> {
if (typeof indexedDB === "undefined") return;
const enc = await this.crypto.encrypt(this.store_id, bytes);
- await idb_set(this.store_key, enc, this.get_store());
+ const store = await this.get_store();
+ await idb_set(this.store_key, enc, store);
}
async remove(): Promise<void> {
if (typeof indexedDB === "undefined") return;
- await idb_del(this.store_key, this.get_store());
+ const store = await this.get_store();
+ await idb_del(this.store_key, store);
}
}
diff --git a/utils-nostr/.gitignore b/nostr/.gitignore
diff --git a/utils-nostr/LICENSE b/nostr/LICENSE
diff --git a/nostr/package.json b/nostr/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@radroots/nostr",
+ "version": "0.0.1",
+ "private": true,
+ "license": "GPLv3",
+ "type": "module",
+ "main": "./dist/cjs/index.js",
+ "module": "./dist/esm/index.js",
+ "types": "./dist/types/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/types/index.d.ts",
+ "import": "./dist/esm/index.js",
+ "require": "./dist/cjs/index.js"
+ }
+ },
+ "scripts": {
+ "build:esm": "tsc -p tsconfig.esm.json",
+ "build:cjs": "tsc -p tsconfig.cjs.json",
+ "build": "npm run clean && npm run build:esm && npm run build:cjs",
+ "prebuild": "npm run clean",
+ "clean": "rimraf dist",
+ "dev": "npm run watch",
+ "watch": "tsc -w"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "devDependencies": {
+ "@radroots/tsconfig": "workspace:*",
+ "@types/node": "^22.13.1",
+ "rimraf": "^6.0.1",
+ "typescript": "5.8.3"
+ },
+ "dependencies": {
+ "@noble/curves": "^1.6.0",
+ "@noble/hashes": "^1.4.0",
+ "@radroots/core-bindings": "workspace:*",
+ "@radroots/events-bindings": "workspace:*",
+ "@radroots/events-codec-wasm": "workspace:*",
+ "@radroots/trade-bindings": "workspace:*",
+ "@radroots/utils": "workspace:*",
+ "@welshman/net": "workspace:*",
+ "@welshman/signer": "workspace:*",
+ "@welshman/util": "workspace:*",
+ "nostr-geotags": "^0.7.2",
+ "nostr-tools": "^2.10.4",
+ "zod": "^4.2.1"
+ }
+}
+\ No newline at end of file
diff --git a/nostr/src/domain/trade/lib.ts b/nostr/src/domain/trade/lib.ts
@@ -0,0 +1,44 @@
+import {
+ KIND_TRADE_LISTING_ACCEPT_REQ,
+ KIND_TRADE_LISTING_ACCEPT_RES,
+ KIND_TRADE_LISTING_CANCEL_REQ,
+ KIND_TRADE_LISTING_CANCEL_RES,
+ KIND_TRADE_LISTING_CONVEYANCE_REQ,
+ KIND_TRADE_LISTING_CONVEYANCE_RES,
+ KIND_TRADE_LISTING_FULFILL_REQ,
+ KIND_TRADE_LISTING_FULFILL_RES,
+ KIND_TRADE_LISTING_INVOICE_REQ,
+ KIND_TRADE_LISTING_INVOICE_RES,
+ KIND_TRADE_LISTING_ORDER_REQ,
+ KIND_TRADE_LISTING_ORDER_RES,
+ KIND_TRADE_LISTING_PAYMENT_REQ,
+ KIND_TRADE_LISTING_PAYMENT_RES,
+ KIND_TRADE_LISTING_RECEIPT_REQ,
+ KIND_TRADE_LISTING_RECEIPT_RES,
+ KIND_TRADE_LISTING_REFUND_REQ,
+ KIND_TRADE_LISTING_REFUND_RES,
+} from "@radroots/trade-bindings";
+
+export const REQUEST_KINDS: Record<string, number> = {
+ order: KIND_TRADE_LISTING_ORDER_REQ,
+ accept: KIND_TRADE_LISTING_ACCEPT_REQ,
+ conveyance: KIND_TRADE_LISTING_CONVEYANCE_REQ,
+ invoice: KIND_TRADE_LISTING_INVOICE_REQ,
+ payment: KIND_TRADE_LISTING_PAYMENT_REQ,
+ fulfillment: KIND_TRADE_LISTING_FULFILL_REQ,
+ receipt: KIND_TRADE_LISTING_RECEIPT_REQ,
+ cancel: KIND_TRADE_LISTING_CANCEL_REQ,
+ refund: KIND_TRADE_LISTING_REFUND_REQ,
+};
+
+export const RESULT_KINDS: Record<string, number> = {
+ order: KIND_TRADE_LISTING_ORDER_RES,
+ accept: KIND_TRADE_LISTING_ACCEPT_RES,
+ conveyance: KIND_TRADE_LISTING_CONVEYANCE_RES,
+ invoice: KIND_TRADE_LISTING_INVOICE_RES,
+ payment: KIND_TRADE_LISTING_PAYMENT_RES,
+ fulfillment: KIND_TRADE_LISTING_FULFILL_RES,
+ receipt: KIND_TRADE_LISTING_RECEIPT_RES,
+ cancel: KIND_TRADE_LISTING_CANCEL_RES,
+ refund: KIND_TRADE_LISTING_REFUND_RES,
+};
diff --git a/nostr/src/domain/trade/listing/accept/lib.ts b/nostr/src/domain/trade/listing/accept/lib.ts
@@ -0,0 +1,59 @@
+import { RadrootsJobInput } from "@radroots/events-bindings";
+import {
+ KIND_TRADE_LISTING_ACCEPT_REQ,
+ KIND_TRADE_LISTING_ACCEPT_RES,
+ MARKER_LISTING,
+ MARKER_ORDER_RESULT,
+ TradeListingAcceptRequest,
+ TradeListingAcceptResult,
+} from "@radroots/trade-bindings";
+import { nostr_event_create } from "../../../../events/lib.js";
+import type { NostrEventFigure, NostrSignedEvent } from "../../../../types/nostr.js";
+import {
+ build_request_tags,
+ build_result_tags,
+ CommonRequestOpts,
+ CommonResultOpts,
+ make_event_input,
+} from "../../tags.js";
+import { tags_trade_listing_chain } from "../tags.js";
+
+export const nostr_event_trade_listing_accept_request = async (
+ opts: NostrEventFigure<{ data: TradeListingAcceptRequest; options?: CommonRequestOpts }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data, options } = opts;
+
+ const inputs: RadrootsJobInput[] = [
+ make_event_input(data.order_result_event_id, MARKER_ORDER_RESULT),
+ make_event_input(data.listing_event_id, MARKER_LISTING),
+ ];
+
+ const tags = build_request_tags(KIND_TRADE_LISTING_ACCEPT_REQ, inputs, options);
+
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_ACCEPT_REQ, content: "", tags },
+ });
+};
+
+export const nostr_event_trade_listing_accept_result = async (
+ opts: NostrEventFigure<{
+ request_event_id: string;
+ content: TradeListingAcceptResult | string;
+ options?: CommonResultOpts & { chain?: { e_root: string; d?: string; e_prev?: string } };
+ }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { request_event_id, content, options } = opts;
+
+ const base_tags = build_result_tags(KIND_TRADE_LISTING_ACCEPT_RES, request_event_id, options);
+
+ const tags = options?.chain
+ ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
+ : base_tags;
+
+ const content_body = typeof content === "string" ? content : JSON.stringify(content);
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_ACCEPT_RES, content: content_body, tags },
+ });
+};
diff --git a/nostr/src/domain/trade/listing/conveyance/lib.ts b/nostr/src/domain/trade/listing/conveyance/lib.ts
@@ -0,0 +1,60 @@
+import { RadrootsJobInput } from "@radroots/events-bindings";
+import {
+ KIND_TRADE_LISTING_CONVEYANCE_REQ,
+ KIND_TRADE_LISTING_CONVEYANCE_RES,
+ MARKER_ACCEPT_RESULT,
+ MARKER_PAYLOAD,
+ TradeListingConveyanceRequest,
+ TradeListingConveyanceResult,
+} from "@radroots/trade-bindings";
+import { nostr_event_create } from "../../../../events/lib.js";
+import type { NostrEventFigure, NostrSignedEvent } from "../../../../types/nostr.js";
+import {
+ build_request_tags,
+ build_result_tags,
+ CommonRequestOpts,
+ CommonResultOpts,
+ make_event_input,
+ make_text_input,
+} from "../../tags.js";
+import { tags_trade_listing_chain } from "../tags.js";
+
+export const nostr_event_trade_listing_conveyance_request = async (
+ opts: NostrEventFigure<{ data: TradeListingConveyanceRequest; options?: CommonRequestOpts }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data, options } = opts;
+
+ const inputs: RadrootsJobInput[] = [
+ make_event_input(data.accept_result_event_id, MARKER_ACCEPT_RESULT),
+ make_text_input({ method: data.method }, MARKER_PAYLOAD),
+ ];
+
+ const tags = build_request_tags(KIND_TRADE_LISTING_CONVEYANCE_REQ, inputs, options);
+
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_CONVEYANCE_REQ, content: "", tags },
+ });
+};
+
+export const nostr_event_trade_listing_conveyance_result = async (
+ opts: NostrEventFigure<{
+ request_event_id: string;
+ content: TradeListingConveyanceResult | string;
+ options?: CommonResultOpts & { chain?: { e_root: string; d?: string; e_prev?: string } };
+ }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { request_event_id, content, options } = opts;
+
+ const base_tags = build_result_tags(KIND_TRADE_LISTING_CONVEYANCE_RES, request_event_id, options);
+
+ const tags = options?.chain
+ ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
+ : base_tags;
+
+ const content_body = typeof content === "string" ? content : JSON.stringify(content);
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_CONVEYANCE_RES, content: content_body, tags },
+ });
+};
diff --git a/nostr/src/domain/trade/listing/fulfillment/lib.ts b/nostr/src/domain/trade/listing/fulfillment/lib.ts
@@ -0,0 +1,57 @@
+import { RadrootsJobInput } from "@radroots/events-bindings";
+import {
+ KIND_TRADE_LISTING_FULFILL_REQ,
+ KIND_TRADE_LISTING_FULFILL_RES,
+ MARKER_PAYMENT_RESULT,
+ TradeListingFulfillmentRequest,
+ TradeListingFulfillmentResult,
+} from "@radroots/trade-bindings";
+import { nostr_event_create } from "../../../../events/lib.js";
+import type { NostrEventFigure, NostrSignedEvent } from "../../../../types/nostr.js";
+import {
+ build_request_tags,
+ build_result_tags,
+ CommonRequestOpts,
+ CommonResultOpts,
+ make_event_input,
+} from "../../tags.js";
+import { tags_trade_listing_chain } from "../tags.js";
+
+export const nostr_event_trade_listing_fulfillment_request = async (
+ opts: NostrEventFigure<{ data: TradeListingFulfillmentRequest; options?: CommonRequestOpts }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data, options } = opts;
+
+ const inputs: RadrootsJobInput[] = [
+ make_event_input(data.payment_result_event_id, MARKER_PAYMENT_RESULT),
+ ];
+
+ const tags = build_request_tags(KIND_TRADE_LISTING_FULFILL_REQ, inputs, options);
+
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_FULFILL_REQ, content: "", tags },
+ });
+};
+
+export const nostr_event_trade_listing_fulfillment_result = async (
+ opts: NostrEventFigure<{
+ request_event_id: string;
+ content: TradeListingFulfillmentResult | string;
+ options?: CommonResultOpts & { chain?: { e_root: string; d?: string; e_prev?: string } };
+ }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { request_event_id, content, options } = opts;
+
+ const base_tags = build_result_tags(KIND_TRADE_LISTING_FULFILL_RES, request_event_id, options);
+
+ const tags = options?.chain
+ ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
+ : base_tags;
+
+ const content_body = typeof content === "string" ? content : JSON.stringify(content);
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_FULFILL_RES, content: content_body, tags },
+ });
+};
diff --git a/nostr/src/domain/trade/listing/invoice/lib.ts b/nostr/src/domain/trade/listing/invoice/lib.ts
@@ -0,0 +1,71 @@
+import { RadrootsJobInput } from "@radroots/events-bindings";
+import {
+ KIND_TRADE_LISTING_INVOICE_REQ,
+ KIND_TRADE_LISTING_INVOICE_RES,
+ MARKER_ACCEPT_RESULT,
+ TradeListingInvoiceRequest,
+ TradeListingInvoiceResult,
+} from "@radroots/trade-bindings";
+import { nostr_event_create } from "../../../../events/lib.js";
+import type { NostrEventFigure, NostrSignedEvent } from "../../../../types/nostr.js";
+import {
+ build_request_tags,
+ build_result_tags,
+ CommonRequestOpts,
+ CommonResultOpts,
+ make_event_input,
+} from "../../tags.js";
+import { tags_trade_listing_chain } from "../tags.js";
+
+export const nostr_event_trade_listing_invoice_request = async (
+ opts: NostrEventFigure<{ data: TradeListingInvoiceRequest; options?: CommonRequestOpts }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data, options } = opts;
+
+ const inputs: RadrootsJobInput[] = [
+ make_event_input(data.accept_result_event_id, MARKER_ACCEPT_RESULT),
+ ];
+
+ const tags = build_request_tags(KIND_TRADE_LISTING_INVOICE_REQ, inputs, options);
+
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_INVOICE_REQ, content: "", tags },
+ });
+};
+
+export const nostr_event_trade_listing_invoice_result = async (
+ opts: NostrEventFigure<{
+ request_event_id: string;
+ content: TradeListingInvoiceResult | string;
+ options?: Omit<CommonResultOpts, "payment_sat" | "payment_bolt11"> & {
+ chain?: { e_root: string; d?: string; e_prev?: string };
+ };
+ }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { request_event_id, content, options } = opts;
+
+ const parsed = typeof content === "string" ? undefined : content;
+
+ const base_tags = build_result_tags(
+ KIND_TRADE_LISTING_INVOICE_RES,
+ request_event_id,
+ options,
+ parsed
+ ? {
+ payment_sat: parsed.total_sat,
+ payment_bolt11: parsed.bolt11 ?? undefined,
+ }
+ : undefined,
+ );
+
+ const tags = options?.chain
+ ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
+ : base_tags;
+
+ const content_body = typeof content === "string" ? content : JSON.stringify(content);
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_INVOICE_RES, content: content_body, tags },
+ });
+};
diff --git a/nostr/src/domain/trade/listing/order/lib.ts b/nostr/src/domain/trade/listing/order/lib.ts
@@ -0,0 +1,60 @@
+import { RadrootsJobInput } from "@radroots/events-bindings";
+import {
+ KIND_TRADE_LISTING_ORDER_REQ,
+ KIND_TRADE_LISTING_ORDER_RES,
+ MARKER_LISTING,
+ MARKER_PAYLOAD,
+ TradeListingOrderRequest,
+ TradeListingOrderResult,
+} from "@radroots/trade-bindings";
+import { nostr_event_create } from "../../../../events/lib.js";
+import type { NostrEventFigure, NostrSignedEvent } from "../../../../types/nostr.js";
+import {
+ build_request_tags,
+ build_result_tags,
+ CommonRequestOpts,
+ CommonResultOpts,
+ make_event_input,
+ make_text_input,
+} from "../../tags.js";
+import { tags_trade_listing_chain } from "../tags.js";
+
+export const nostr_event_trade_listing_order_request = async (
+ opts: NostrEventFigure<{ data: TradeListingOrderRequest; options?: CommonRequestOpts }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data, options } = opts;
+
+ const inputs: RadrootsJobInput[] = [
+ make_event_input(data.event.id, MARKER_LISTING, data.event.relays ?? undefined),
+ make_text_input(data.payload, MARKER_PAYLOAD),
+ ];
+
+ const tags = build_request_tags(KIND_TRADE_LISTING_ORDER_REQ, inputs, options);
+
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_ORDER_REQ, content: "", tags },
+ });
+};
+
+export const nostr_event_trade_listing_order_result = async (
+ opts: NostrEventFigure<{
+ request_event_id: string;
+ content: TradeListingOrderResult | string;
+ options?: CommonResultOpts & { chain?: { e_root: string; d?: string; e_prev?: string } };
+ }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { request_event_id, content, options } = opts;
+
+ const base_tags = build_result_tags(KIND_TRADE_LISTING_ORDER_RES, request_event_id, options);
+
+ const tags = options?.chain
+ ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
+ : base_tags;
+
+ const content_body = typeof content === "string" ? content : JSON.stringify(content);
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_ORDER_RES, content: content_body, tags },
+ });
+};
diff --git a/nostr/src/domain/trade/listing/payment/lib.ts b/nostr/src/domain/trade/listing/payment/lib.ts
@@ -0,0 +1,60 @@
+import { RadrootsJobInput } from "@radroots/events-bindings";
+import {
+ KIND_TRADE_LISTING_PAYMENT_REQ,
+ KIND_TRADE_LISTING_PAYMENT_RES,
+ MARKER_INVOICE_RESULT,
+ MARKER_PROOF,
+ TradeListingPaymentProofRequest,
+ TradeListingPaymentResult,
+} from "@radroots/trade-bindings";
+import { nostr_event_create } from "../../../../events/lib.js";
+import type { NostrEventFigure, NostrSignedEvent } from "../../../../types/nostr.js";
+import {
+ build_request_tags,
+ build_result_tags,
+ CommonRequestOpts,
+ CommonResultOpts,
+ make_event_input,
+ make_text_input,
+} from "../../tags.js";
+import { tags_trade_listing_chain } from "../tags.js";
+
+export const nostr_event_trade_listing_payment_request = async (
+ opts: NostrEventFigure<{ data: TradeListingPaymentProofRequest; options?: CommonRequestOpts }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data, options } = opts;
+
+ const inputs: RadrootsJobInput[] = [
+ make_event_input(data.invoice_result_event_id, MARKER_INVOICE_RESULT),
+ make_text_input(data.proof, MARKER_PROOF),
+ ];
+
+ const tags = build_request_tags(KIND_TRADE_LISTING_PAYMENT_REQ, inputs, options);
+
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_PAYMENT_REQ, content: "", tags },
+ });
+};
+
+export const nostr_event_trade_listing_payment_result = async (
+ opts: NostrEventFigure<{
+ request_event_id: string;
+ content: TradeListingPaymentResult | string;
+ options?: CommonResultOpts & { chain?: { e_root: string; d?: string; e_prev?: string } };
+ }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { request_event_id, content, options } = opts;
+
+ const base_tags = build_result_tags(KIND_TRADE_LISTING_PAYMENT_RES, request_event_id, options);
+
+ const tags = options?.chain
+ ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
+ : base_tags;
+
+ const content_body = typeof content === "string" ? content : JSON.stringify(content);
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_PAYMENT_RES, content: content_body, tags },
+ });
+};
diff --git a/nostr/src/domain/trade/listing/receipt/lib.ts b/nostr/src/domain/trade/listing/receipt/lib.ts
@@ -0,0 +1,60 @@
+import { RadrootsJobInput } from "@radroots/events-bindings";
+import {
+ KIND_TRADE_LISTING_RECEIPT_REQ,
+ KIND_TRADE_LISTING_RECEIPT_RES,
+ MARKER_FULFILLMENT_RESULT,
+ MARKER_PAYLOAD,
+ TradeListingReceiptRequest,
+ TradeListingReceiptResult,
+} from "@radroots/trade-bindings";
+import { nostr_event_create } from "../../../../events/lib.js";
+import type { NostrEventFigure, NostrSignedEvent } from "../../../../types/nostr.js";
+import {
+ build_request_tags,
+ build_result_tags,
+ CommonRequestOpts,
+ CommonResultOpts,
+ make_event_input,
+ make_text_input,
+} from "../../tags.js";
+import { tags_trade_listing_chain } from "../tags.js";
+
+export const nostr_event_trade_listing_receipt_request = async (
+ opts: NostrEventFigure<{ data: TradeListingReceiptRequest; options?: CommonRequestOpts }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data, options } = opts;
+
+ const inputs: RadrootsJobInput[] = [
+ make_event_input(data.fulfillment_result_event_id, MARKER_FULFILLMENT_RESULT),
+ ...(data.note ? [make_text_input({ note: data.note }, MARKER_PAYLOAD)] : []),
+ ];
+
+ const tags = build_request_tags(KIND_TRADE_LISTING_RECEIPT_REQ, inputs, options);
+
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_RECEIPT_REQ, content: "", tags },
+ });
+};
+
+export const nostr_event_trade_listing_receipt_result = async (
+ opts: NostrEventFigure<{
+ request_event_id: string;
+ content: TradeListingReceiptResult | string;
+ options?: CommonResultOpts & { chain?: { e_root: string; d?: string; e_prev?: string } };
+ }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { request_event_id, content, options } = opts;
+
+ const base_tags = build_result_tags(KIND_TRADE_LISTING_RECEIPT_RES, request_event_id, options);
+
+ const tags = options?.chain
+ ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
+ : base_tags;
+
+ const content_body = typeof content === "string" ? content : JSON.stringify(content);
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_TRADE_LISTING_RECEIPT_RES, content: content_body, tags },
+ });
+};
diff --git a/utils-nostr/src/domain/trade/listing/tags.ts b/nostr/src/domain/trade/listing/tags.ts
diff --git a/nostr/src/domain/trade/tags.ts b/nostr/src/domain/trade/tags.ts
@@ -0,0 +1,88 @@
+import { RadrootsJobInput } from "@radroots/events-bindings";
+import { tags_job_request, tags_job_result } from "../../events/job/tags.js";
+
+export type CommonRequestOpts = {
+ output?: string;
+ bid_sat?: number;
+ relays?: string[];
+ providers?: string[];
+ topics?: string[];
+ encrypted?: boolean;
+ params?: Array<{ key: string; value: string }>;
+};
+
+export type CommonResultOpts = {
+ request_relay_hint?: string;
+ request_json?: string;
+ customer_pubkey?: string;
+ payment_sat?: number;
+ payment_bolt11?: string;
+ encrypted?: boolean;
+ include_inputs?: string[];
+ chain?: { e_root: string; d?: string; e_prev?: string };
+};
+
+export const make_event_input = (
+ id: string,
+ marker: string,
+ relay?: string,
+): RadrootsJobInput => ({
+ data: id,
+ input_type: "event",
+ ...(relay ? { relay } : {}),
+ marker,
+});
+
+export const make_text_input = (
+ payload: unknown,
+ marker: string,
+): RadrootsJobInput => ({
+ data: typeof payload === "string" ? payload : JSON.stringify(payload),
+ input_type: "text",
+ marker,
+});
+
+export const build_request_tags = (
+ kind: number,
+ inputs: RadrootsJobInput[],
+ opts?: CommonRequestOpts,
+) =>
+ tags_job_request({
+ kind,
+ inputs,
+ output: opts?.output,
+ params: opts?.params ?? [],
+ bid_sat: opts?.bid_sat,
+ relays: opts?.relays ?? [],
+ providers: opts?.providers ?? [],
+ topics: opts?.topics ?? [],
+ encrypted: !!opts?.encrypted,
+ });
+
+export const build_result_tags = (
+ kind: number,
+ request_event_id: string,
+ opts?: CommonResultOpts,
+ extra?: {
+ inputs?: RadrootsJobInput[];
+ payment_sat?: number;
+ payment_bolt11?: string;
+ },
+) =>
+ tags_job_result({
+ kind,
+ request_event: {
+ id: request_event_id,
+ ...(opts?.request_relay_hint ? { relays: opts.request_relay_hint } : {}),
+ },
+ request_json: opts?.request_json,
+ inputs: !opts?.encrypted && extra?.inputs?.length ? extra.inputs : [],
+ customer_pubkey: opts?.customer_pubkey,
+ payment:
+ extra?.payment_sat !== undefined
+ ? { amount_sat: extra.payment_sat, bolt11: extra.payment_bolt11 }
+ : opts?.payment_sat !== undefined
+ ? { amount_sat: opts.payment_sat, bolt11: opts.payment_bolt11 }
+ : undefined,
+ encrypted: !!opts?.encrypted,
+ });
diff --git a/nostr/src/events/comment/lib.ts b/nostr/src/events/comment/lib.ts
@@ -0,0 +1,21 @@
+import type { RadrootsComment } from "@radroots/events-bindings";
+import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js";
+import { nostr_event_create } from "../lib.js";
+import { tags_comment } from "./tags.js";
+
+export const KIND_RADROOTS_COMMENT = 1111;
+export type KindRadrootsComment = typeof KIND_RADROOTS_COMMENT;
+
+export const nostr_event_comment = async (
+ opts: NostrEventFigure<{ data: RadrootsComment }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data } = opts;
+ return nostr_event_create({
+ ...opts,
+ basis: {
+ kind: KIND_RADROOTS_COMMENT,
+ content: data.content,
+ tags: tags_comment(data),
+ },
+ });
+};
diff --git a/nostr/src/events/comment/parse.ts b/nostr/src/events/comment/parse.ts
@@ -0,0 +1,22 @@
+import type { RadrootsComment } from "@radroots/events-bindings";
+import { radroots_comment_schema } from "@radroots/events-bindings";
+import type { NostrEvent } from "../../types/nostr.js";
+import { parse_nostr_event_basis } from "../lib.js";
+import type { NostrEventBasis } from "../subscription.js";
+import { KIND_RADROOTS_COMMENT, type KindRadrootsComment } from "./lib.js";
+
+export type RadrootsCommentNostrEvent = NostrEventBasis<KindRadrootsComment> & { comment: RadrootsComment };
+
+export const parse_nostr_comment_event = (
+ event: NostrEvent,
+): RadrootsCommentNostrEvent | undefined => {
+ const ev = parse_nostr_event_basis(event, KIND_RADROOTS_COMMENT);
+ if (!ev) return undefined;
+ try {
+ const parsed = JSON.parse(event.content);
+ const comment = radroots_comment_schema.parse(parsed);
+ return { ...ev, comment };
+ } catch {
+ return undefined;
+ }
+};
diff --git a/nostr/src/events/comment/tags.ts b/nostr/src/events/comment/tags.ts
@@ -0,0 +1,37 @@
+import { RadrootsComment } from "@radroots/events-bindings";
+import { NostrEventTags } from "../../types/lib.js";
+
+export const tags_comment = (opts: RadrootsComment): NostrEventTags => {
+ const { root: root_event, parent: parent_event } = opts;
+
+ const root = {
+ kind: root_event.kind.toString(),
+ author: root_event.author,
+ id: root_event.id,
+ d_tag: root_event.d_tag,
+ relays: root_event.relays || [],
+ };
+
+ const parent = parent_event && parent_event.id
+ ? {
+ kind: parent_event.kind.toString(),
+ author: parent_event.author,
+ id: parent_event.id,
+ d_tag: parent_event.d_tag,
+ relays: parent_event.relays || [],
+ }
+ : root;
+
+ const tags: NostrEventTags = [
+ ["E", root.id, ...root.relays],
+ ["P", root.author],
+ ["K", root.kind],
+ ...(root.d_tag ? [["A", `${root.kind}:${root.author}:${root.d_tag}`, ...root.relays]] : []),
+ ["e", parent.id, ...parent.relays],
+ ["p", parent.author],
+ ["k", parent.kind],
+ ...(parent.d_tag ? [["a", `${parent.kind}:${parent.author}:${parent.d_tag}`, ...parent.relays]] : []),
+ ];
+
+ return tags;
+};
diff --git a/nostr/src/events/follow/lib.ts b/nostr/src/events/follow/lib.ts
@@ -0,0 +1,21 @@
+import type { RadrootsFollow } from "@radroots/events-bindings";
+import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js";
+import { nostr_event_create } from "../lib.js";
+import { tags_follow_list } from "./tags.js";
+
+export const KIND_RADROOTS_FOLLOW = 3;
+export type KindRadrootsFollow = typeof KIND_RADROOTS_FOLLOW;
+
+export const nostr_event_follows = async (
+ opts: NostrEventFigure<{ data: RadrootsFollow }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data } = opts;
+ return nostr_event_create({
+ ...opts,
+ basis: {
+ kind: KIND_RADROOTS_FOLLOW,
+ content: "",
+ tags: tags_follow_list(data.list),
+ },
+ });
+};
diff --git a/nostr/src/events/follow/parse.ts b/nostr/src/events/follow/parse.ts
@@ -0,0 +1,22 @@
+import type { RadrootsFollow } from "@radroots/events-bindings";
+import { radroots_follow_schema } from "@radroots/events-bindings";
+import type { NostrEvent } from "../../types/nostr.js";
+import { parse_nostr_event_basis } from "../lib.js";
+import type { NostrEventBasis } from "../subscription.js";
+import { KIND_RADROOTS_FOLLOW, type KindRadrootsFollow } from "./lib.js";
+
+export type RadrootsFollowNostrEvent = NostrEventBasis<KindRadrootsFollow> & { follow: RadrootsFollow };
+
+export const parse_nostr_follow_event = (
+ event: NostrEvent,
+): RadrootsFollowNostrEvent | undefined => {
+ const ev = parse_nostr_event_basis(event, KIND_RADROOTS_FOLLOW);
+ if (!ev) return undefined;
+ try {
+ const parsed = JSON.parse(event.content);
+ const follow = radroots_follow_schema.parse(parsed);
+ return { ...ev, follow };
+ } catch {
+ return undefined;
+ }
+};
diff --git a/nostr/src/events/follow/tags.ts b/nostr/src/events/follow/tags.ts
@@ -0,0 +1,11 @@
+import { RadrootsFollowProfile } from "@radroots/events-bindings";
+import { NostrEventTags } from "../../types/lib.js";
+
+export const tags_follow_list = (list: RadrootsFollowProfile[]): NostrEventTags => {
+ return list.map(({ public_key, relay_url, contact_name }) => {
+ const entry = ["p", public_key];
+ if (relay_url) entry.push(relay_url);
+ if (contact_name) entry.push(contact_name);
+ return entry;
+ });
+};
diff --git a/nostr/src/events/job/lib.ts b/nostr/src/events/job/lib.ts
@@ -0,0 +1,100 @@
+import {
+ JobFeedbackStatus,
+ KIND_JOB_FEEDBACK,
+ RadrootsJobFeedback,
+ RadrootsJobRequest,
+ RadrootsJobResult,
+} from "@radroots/events-bindings";
+import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js";
+import { nostr_event_create } from "../lib.js";
+import { tags_job_feedback, tags_job_request, tags_job_result } from "./tags.js";
+
+export const nostr_event_job_request = async (
+ opts: NostrEventFigure<{ data: RadrootsJobRequest }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data } = opts;
+ return nostr_event_create({
+ ...opts,
+ basis: {
+ kind: data.kind,
+ content: "",
+ tags: tags_job_request(data),
+ },
+ });
+};
+
+export const nostr_event_job_result = async (
+ opts: NostrEventFigure<{ data: RadrootsJobResult }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data } = opts;
+ return nostr_event_create({
+ ...opts,
+ basis: {
+ kind: data.kind,
+ content: data.content || "",
+ tags: tags_job_result(data),
+ },
+ });
+};
+
+export const nostr_event_job_feedback = async (
+ opts: NostrEventFigure<{ data: RadrootsJobFeedback }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data } = opts;
+ return nostr_event_create({
+ ...opts,
+ basis: {
+ kind: data.kind,
+ content: data.content || "",
+ tags: tags_job_feedback(data),
+ },
+ });
+};
+
+export const nostr_event_job_feedback_todo = async (
+ opts: NostrEventFigure<{
+ request_event_id: string;
+ status:
+ | JobFeedbackStatus
+ | "payment-required"
+ | "processing"
+ | "error"
+ | "success"
+ | "partial";
+ content?: string;
+ options?: {
+ request_relay_hint?: string;
+ extra_info?: string;
+ customer_pubkey?: string;
+ amount_sat?: number;
+ bolt11?: string;
+ encrypted?: boolean;
+ };
+ }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { request_event_id, status, content, options } = opts;
+
+ const fb: RadrootsJobFeedback = {
+ kind: KIND_JOB_FEEDBACK,
+ status: status as JobFeedbackStatus,
+ extra_info: options?.extra_info,
+ request_event: {
+ id: request_event_id,
+ ...(options?.request_relay_hint ? { relays: options.request_relay_hint } : {}),
+ },
+ customer_pubkey: options?.customer_pubkey,
+ payment:
+ options?.amount_sat !== undefined
+ ? { amount_sat: options.amount_sat, bolt11: options?.bolt11 }
+ : undefined,
+ content,
+ encrypted: !!options?.encrypted,
+ };
+
+ const tags = tags_job_feedback(fb);
+
+ return nostr_event_create({
+ ...opts,
+ basis: { kind: KIND_JOB_FEEDBACK, content: content ?? "", tags },
+ });
+};
diff --git a/nostr/src/events/job/tags.ts b/nostr/src/events/job/tags.ts
@@ -0,0 +1,81 @@
+import {
+ RadrootsJobFeedback,
+ RadrootsJobInput,
+ RadrootsJobRequest,
+ RadrootsJobResult,
+} from "@radroots/events-bindings";
+import { NostrEventTag, NostrEventTags } from "../../types/lib.js";
+
+export const tag_job_input = (input: RadrootsJobInput): NostrEventTag => {
+ const tag: NostrEventTag = ["i", input.data, input.input_type];
+ if (input.relay) tag.push(input.relay);
+ if (input.marker) tag.push(input.marker);
+ return tag;
+};
+
+export const tag_job_output = (mime: string): NostrEventTag => ["output", mime];
+
+export const tag_job_param = (key: string, value: string): NostrEventTag => ["param", key, value];
+
+export const tag_job_bid = (sat: number): NostrEventTag => ["bid", String(sat)];
+
+export const tags_job_relays = (relays: string[]): NostrEventTags =>
+ relays.map(r => ["relays", r]);
+
+export const tags_job_providers = (pubkeys: string[]): NostrEventTags =>
+ pubkeys.map(p => ["p", p]);
+
+export const tags_job_topics = (topics: string[]): NostrEventTags =>
+ topics.map(t => ["t", t]);
+
+export const tag_job_amount = (msat: number, bolt11?: string | null): NostrEventTag =>
+ bolt11 ? ["amount", String(msat), bolt11] : ["amount", String(msat)];
+
+export const tag_job_encrypted = (): NostrEventTag => ["encrypted"];
+
+export const tags_job_request = (opts: RadrootsJobRequest): NostrEventTags => {
+ const tags: NostrEventTags = [];
+ for (const input of opts.inputs) tags.push(tag_job_input(input));
+ if (opts.output) tags.push(tag_job_output(opts.output));
+ if (opts.params) for (const p of opts.params) tags.push(tag_job_param(p.key, p.value));
+ if (typeof opts.bid_sat === "number") tags.push(tag_job_bid(opts.bid_sat));
+ if (opts.relays?.length) tags.push(...tags_job_relays(opts.relays));
+ if (opts.providers?.length) tags.push(...tags_job_providers(opts.providers));
+ if (opts.topics?.length) tags.push(...tags_job_topics(opts.topics));
+ if (opts.encrypted) tags.push(tag_job_encrypted());
+ return tags;
+};
+
+export const tags_job_result = (opts: RadrootsJobResult): NostrEventTags => {
+ const tags: NostrEventTags = [];
+ const event_tag: NostrEventTag = ["e", opts.request_event.id];
+ if (opts.request_event.relays) event_tag.push(opts.request_event.relays);
+ tags.push(event_tag);
+ if (opts.request_json) tags.push(["request", opts.request_json]);
+ if (!opts.encrypted && opts.inputs?.length)
+ for (const input of opts.inputs) tags.push(tag_job_input(input));
+ if (opts.customer_pubkey) tags.push(["p", opts.customer_pubkey]);
+ if (opts.payment?.amount_sat !== undefined) {
+ const msat = Math.round(Number(opts.payment.amount_sat) * 1000);
+ tags.push(tag_job_amount(msat, opts.payment.bolt11));
+ }
+ if (opts.encrypted) tags.push(tag_job_encrypted());
+ return tags;
+};
+
+export const tags_job_feedback = (opts: RadrootsJobFeedback): NostrEventTags => {
+ const tags: NostrEventTags = [];
+ const status_tag: NostrEventTag = ["status", String(opts.status)];
+ if (opts.extra_info) status_tag.push(opts.extra_info);
+ tags.push(status_tag);
+ if (opts.payment?.amount_sat !== undefined) {
+ const msat = Math.round(Number(opts.payment.amount_sat) * 1000);
+ tags.push(tag_job_amount(msat, opts.payment.bolt11));
+ }
+ const event_tag: NostrEventTag = ["e", opts.request_event.id];
+ if (opts.request_event.relays) event_tag.push(opts.request_event.relays);
+ tags.push(event_tag);
+ if (opts.customer_pubkey) tags.push(["p", opts.customer_pubkey]);
+ if (opts.encrypted) tags.push(tag_job_encrypted());
+ return tags;
+};
diff --git a/nostr/src/events/job/utils.ts b/nostr/src/events/job/utils.ts
@@ -0,0 +1,43 @@
+import { JobInputType, KIND_JOB_FEEDBACK } from "@radroots/events-bindings";
+import { TradeListingStage } from "@radroots/trade-bindings";
+import { REQUEST_KINDS, RESULT_KINDS } from "../../domain/trade/lib.js";
+import type { NostrEventTags } from "../../types/lib.js";
+
+const TRADE_LISTING_STAGE_KEYS: TradeListingStage["kind"][] = [
+ "order",
+ "accept",
+ "conveyance",
+ "invoice",
+ "payment",
+ "fulfillment",
+ "receipt",
+ "cancel",
+ "refund",
+];
+
+export function get_job_input_data_for_marker(
+ tags: NostrEventTags,
+ marker: string,
+ input_type: JobInputType = "event",
+): string | undefined {
+ for (const t of tags) {
+ if (t[0] !== "i") continue;
+ if (t[2] !== input_type) continue;
+ const tag_marker = t.length >= 5 ? t[4] : t.length >= 4 ? t[3] : undefined;
+ if (tag_marker === marker) return t[1];
+ }
+ return undefined;
+}
+
+export function get_trade_listing_stage_from_event_kind(
+ kind: number,
+): TradeListingStage | undefined {
+ for (const key of TRADE_LISTING_STAGE_KEYS) {
+ if (REQUEST_KINDS[key] === kind) return { kind: key };
+ }
+ for (const key of TRADE_LISTING_STAGE_KEYS) {
+ if (RESULT_KINDS[key] === kind) return { kind: key };
+ }
+ if (kind === KIND_JOB_FEEDBACK) return { kind: "order" };
+ return undefined;
+}
diff --git a/nostr/src/events/lib.ts b/nostr/src/events/lib.ts
@@ -0,0 +1,107 @@
+import { schnorr } from "@noble/curves/secp256k1";
+import { hexToBytes } from "@noble/hashes/utils";
+import { makeEvent } from "@welshman/util";
+import { time_now_ms, time_now_s, uuidv4 } from "@radroots/utils";
+import {
+ finalizeEvent,
+ getEventHash,
+ nip19,
+ type NostrEvent as NostrToolsEvent,
+} from "nostr-tools";
+import { NostrEventSign, NostrEventTags, NostrNeventEncode } from "../types/lib.js";
+import type { NostrEvent, NostrEventFigure, NostrSignedEvent } from "../types/nostr.js";
+import { nostr_tag_client } from "../utils/tags.js";
+import type { NostrEventBasis } from "./subscription.js";
+
+export const get_event_tag = (tags: NostrEventTags, key: string): string =>
+ tags.find(t => t[0] === key)?.[1] ?? "";
+
+export const get_event_tags = (tags: NostrEventTags, key: string): NostrEventTags =>
+ tags.filter(t => t[0] === key);
+
+export const parse_nostr_event_basis = <T extends number>(
+ event: NostrEvent,
+ kind: T,
+): NostrEventBasis<T> | undefined => {
+ if (!event || typeof event.created_at !== "number" || event.kind !== kind) return undefined;
+ return {
+ id: event.id,
+ published_at: event.created_at,
+ author: event.pubkey,
+ kind: event.kind as T,
+ };
+};
+
+export const 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 nostr_event_sign = (opts: NostrEventSign): NostrToolsEvent => {
+ return finalizeEvent(opts.event, hexToBytes(opts.secret_key));
+};
+
+export const nostr_event_sign_attest = (secret_key: string): NostrToolsEvent => {
+ return nostr_event_sign({
+ secret_key,
+ event: {
+ kind: 1,
+ created_at: time_now_s(),
+ tags: [],
+ content: uuidv4(),
+ },
+ });
+};
+
+export const nostr_event_verify_serialized = async (
+ event_serialized: string,
+): Promise<{ public_key: string } | undefined> => {
+ try {
+ const event = JSON.parse(event_serialized) as NostrToolsEvent;
+ 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 nostr_nevent_encode = (opts: NostrNeventEncode): string => {
+ return nip19.neventEncode(opts);
+};
+
+export const nostr_event_create = async (
+ opts: NostrEventFigure<{
+ basis: {
+ kind: number;
+ content: string;
+ tags?: NostrEventTags;
+ };
+ }>,
+): Promise<NostrSignedEvent | undefined> => {
+ try {
+ const time_now = time_now_ms();
+ const published_at = opts.date_published
+ ? Math.floor(opts.date_published.getTime() / 1000).toString()
+ : time_now.toString();
+ const tags: NostrEventTags = [["published_at", published_at]];
+ if (opts.basis.tags?.length) tags.push(...opts.basis.tags);
+ if (opts.client) {
+ const d_tag = tags.find(tag => tag[0] === "d")?.[1]
+ ?? tags.find(tag => tag[0] === "d_tag")?.[1];
+ tags.push(nostr_tag_client(opts.client, d_tag));
+ }
+ const ev = makeEvent(opts.basis.kind, {
+ content: opts.basis.content,
+ tags,
+ created_at: time_now,
+ });
+ return await opts.signer.sign(ev);
+ } catch {
+ return undefined;
+ }
+};
diff --git a/nostr/src/events/listing/lib.ts b/nostr/src/events/listing/lib.ts
@@ -0,0 +1,22 @@
+import type { RadrootsListing } from "@radroots/events-bindings";
+import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js";
+import { nostr_event_create } from "../lib.js";
+import { tags_listing } from "./tags.js";
+
+export const KIND_RADROOTS_LISTING = 30402;
+export type KindRadrootsListing = typeof KIND_RADROOTS_LISTING;
+
+export const nostr_event_classified = async (
+ opts: NostrEventFigure<{ data: RadrootsListing }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data } = opts;
+ const tags = await tags_listing(data);
+ return nostr_event_create({
+ ...opts,
+ basis: {
+ kind: KIND_RADROOTS_LISTING,
+ content: "",
+ tags,
+ },
+ });
+};
diff --git a/nostr/src/events/listing/parse.ts b/nostr/src/events/listing/parse.ts
@@ -0,0 +1,242 @@
+import type {
+ RadrootsCoreMoney,
+ RadrootsCoreQuantity,
+ RadrootsCoreQuantityPrice,
+} from "@radroots/core-bindings";
+import {
+ type RadrootsListing,
+ type RadrootsListingDiscount,
+ type RadrootsListingImage,
+ type RadrootsListingLocation,
+ type RadrootsListingQuantity,
+ radroots_listing_discount_schema,
+ radroots_listing_image_schema,
+ radroots_listing_location_schema,
+ radroots_listing_price_schema,
+ radroots_listing_product_schema,
+ radroots_listing_quantity_schema,
+ radroots_listing_schema,
+} from "@radroots/events-bindings";
+import type { NostrEvent } from "../../types/nostr.js";
+import { get_event_tag, get_event_tags, parse_nostr_event_basis } from "../lib.js";
+import type { NostrEventBasis } from "../subscription.js";
+import { KIND_RADROOTS_LISTING, type KindRadrootsListing } from "./lib.js";
+
+export type RadrootsListingNostrEvent = NostrEventBasis<KindRadrootsListing> & { listing: RadrootsListing };
+
+type CoreUnit = RadrootsCoreQuantity["unit"];
+type CoreCurrency = RadrootsCoreMoney["currency"];
+
+type ListingLocationDraft = {
+ primary?: string;
+ city?: string;
+ region?: string;
+ country?: string;
+ geohash?: string;
+ lat?: number;
+ lng?: number;
+};
+
+type ListingImageDraft = {
+ url: string;
+ size?: {
+ w: number;
+ h: number;
+ };
+};
+
+const to_number = (value?: string): number | undefined => {
+ if (value === undefined) return undefined;
+ const num = Number(value);
+ return Number.isFinite(num) ? num : undefined;
+};
+
+const clean_string = (value?: string | null) => value?.trim() || undefined;
+
+const parse_currency_code = (code?: string): CoreCurrency | undefined => {
+ const cleaned = clean_string(code);
+ if (!cleaned || cleaned.length < 3) return undefined;
+ const upper = cleaned.toUpperCase();
+ return [upper.charCodeAt(0), upper.charCodeAt(1), upper.charCodeAt(2)] as CoreCurrency;
+};
+
+const parse_unit_code = (unit?: string): CoreUnit | undefined => {
+ switch ((unit ?? "").trim().toLowerCase()) {
+ case "each":
+ case "ea":
+ case "count":
+ return "Each" as CoreUnit;
+ case "kg":
+ case "kilogram":
+ case "kilograms":
+ return "MassKg" as CoreUnit;
+ case "g":
+ case "gram":
+ case "grams":
+ return "MassG" as CoreUnit;
+ case "oz":
+ case "ounce":
+ case "ounces":
+ return "MassOz" as CoreUnit;
+ case "lb":
+ case "pound":
+ case "pounds":
+ return "MassLb" as CoreUnit;
+ case "l":
+ case "liter":
+ case "litre":
+ case "liters":
+ case "litres":
+ return "VolumeL" as CoreUnit;
+ case "ml":
+ case "milliliter":
+ case "millilitre":
+ case "milliliters":
+ case "millilitres":
+ return "VolumeMl" as CoreUnit;
+ default:
+ return undefined;
+ }
+};
+
+const parse_quantity_tag = (tag: string[]): RadrootsListingQuantity | undefined => {
+ if (tag.length < 3) return undefined;
+ const amount = to_number(tag[1]);
+ const unit = parse_unit_code(tag[2]);
+ if (amount === undefined || !unit) return undefined;
+ const label = clean_string(tag[3]);
+ const count = to_number(tag[4]);
+ const value: RadrootsCoreQuantity = label ? { amount, unit, label } : { amount, unit };
+ return radroots_listing_quantity_schema.parse({ value, label, count });
+};
+
+const parse_price_tag = (tag: string[]): RadrootsCoreQuantityPrice | undefined => {
+ if (tag.length < 5) return undefined;
+ const amount = to_number(tag[1]);
+ const currency = parse_currency_code(tag[2]);
+ const quantity_amount = to_number(tag[3]);
+ const quantity_unit = parse_unit_code(tag[4]);
+ if (amount === undefined || !currency || quantity_amount === undefined || !quantity_unit) return undefined;
+ const label = clean_string(tag[5]);
+ const money: RadrootsCoreMoney = { amount, currency };
+ const quantity: RadrootsCoreQuantity = label
+ ? { amount: quantity_amount, unit: quantity_unit, label }
+ : { amount: quantity_amount, unit: quantity_unit };
+ return radroots_listing_price_schema.parse({ amount: money, quantity });
+};
+
+const parse_discount_tag = (tag: string[]): RadrootsListingDiscount | undefined => {
+ const prefix = "price-discount-";
+ if (!tag[0]?.startsWith(prefix) || tag.length < 2) return undefined;
+ const kind = tag[0].slice(prefix.length) as RadrootsListingDiscount["kind"];
+ try {
+ const amount = JSON.parse(tag[1]);
+ return radroots_listing_discount_schema.parse({ kind, amount });
+ } catch {
+ return undefined;
+ }
+};
+
+const parse_image_tag = (tag: string[]): RadrootsListingImage | undefined => {
+ if (tag[0] !== "image" || !tag[1]) return undefined;
+ const image: ListingImageDraft = { url: tag[1] };
+ if (tag[2]) {
+ const [w, h] = tag[2].split("x").map(v => Number(v));
+ if (Number.isFinite(w) && Number.isFinite(h)) image.size = { w, h };
+ }
+ return radroots_listing_image_schema.parse(image);
+};
+
+const is_listing_quantity = (value: RadrootsListingQuantity | undefined): value is RadrootsListingQuantity =>
+ Boolean(value);
+
+const is_listing_price = (value: RadrootsCoreQuantityPrice | undefined): value is RadrootsCoreQuantityPrice =>
+ Boolean(value);
+
+const is_listing_discount = (
+ value: RadrootsListingDiscount | undefined,
+): value is RadrootsListingDiscount => Boolean(value);
+
+const is_listing_image = (value: RadrootsListingImage | undefined): value is RadrootsListingImage =>
+ Boolean(value);
+
+export const parse_nostr_listing_event = (
+ event: NostrEvent,
+): RadrootsListingNostrEvent | undefined => {
+ const ev = parse_nostr_event_basis(event, KIND_RADROOTS_LISTING);
+ if (!ev) return undefined;
+ try {
+ const tags = event.tags;
+
+ const d_tag = get_event_tag(tags, "d");
+
+ const product_raw = {
+ key: get_event_tag(tags, "key"),
+ title: get_event_tag(tags, "title"),
+ category: get_event_tag(tags, "category"),
+ summary: get_event_tag(tags, "summary"),
+ process: get_event_tag(tags, "process"),
+ lot: get_event_tag(tags, "lot"),
+ location: get_event_tag(tags, "location"),
+ profile: get_event_tag(tags, "profile"),
+ year: get_event_tag(tags, "year"),
+ };
+
+ const product = radroots_listing_product_schema.parse(product_raw);
+
+ const quantities = get_event_tags(tags, "quantity")
+ .map(parse_quantity_tag)
+ .filter(is_listing_quantity);
+
+ const prices = get_event_tags(tags, "price")
+ .map(parse_price_tag)
+ .filter(is_listing_price);
+
+ const discounts = tags
+ .filter(t => t[0]?.startsWith("price-discount-"))
+ .map(parse_discount_tag)
+ .filter(is_listing_discount);
+
+ const images = get_event_tags(tags, "image")
+ .map(parse_image_tag)
+ .filter(is_listing_image);
+
+ const location_parts = get_event_tags(tags, "location")[0]?.slice(1) ?? [];
+
+ const location_raw: ListingLocationDraft = {};
+ if (location_parts[0]) location_raw.primary = location_parts[0];
+ if (location_parts[1]) location_raw.city = location_parts[1];
+ if (location_parts[2]) location_raw.region = location_parts[2];
+ if (location_parts[3]) location_raw.country = location_parts[3];
+
+ if (location_raw.primary) {
+ const geohash = get_event_tags(tags, "g")[0]?.[1];
+ if (geohash) location_raw.geohash = geohash;
+
+ for (const loc_tag of get_event_tags(tags, "l")) {
+ if (loc_tag.length < 3) continue;
+ const coord = Number(loc_tag[1]);
+ if (!Number.isFinite(coord)) continue;
+ if (loc_tag[2] === "dd.lat") location_raw.lat = coord;
+ if (loc_tag[2] === "dd.lon") location_raw.lng = coord;
+ }
+ }
+
+ const location = location_raw.primary
+ ? radroots_listing_location_schema.parse(location_raw as RadrootsListingLocation)
+ : undefined;
+
+ const listing = radroots_listing_schema.parse({
+ d_tag,
+ product,
+ quantities,
+ prices,
+ discounts: discounts.length ? discounts : undefined,
+ location,
+ images: images.length ? images : undefined,
+ });
+ return { ...ev, listing };
+ } catch {
+ return undefined;
+ }
+};
diff --git a/nostr/src/events/listing/tags.ts b/nostr/src/events/listing/tags.ts
@@ -0,0 +1,140 @@
+import type { RadrootsCoreQuantityPrice } from "@radroots/core-bindings";
+import {
+ RADROOTS_LISTING_PRODUCT_TAG_KEYS,
+ type RadrootsListing,
+ type RadrootsListingDiscount,
+ type RadrootsListingImage,
+ type RadrootsListingLocation,
+ type RadrootsListingQuantity,
+} from "@radroots/events-bindings";
+import nostr_geotags, { type InputData as NostrGeotagsInputData } from "nostr-geotags";
+import { NostrEventTag, NostrEventTagLocation, NostrEventTags } from "../../types/lib.js";
+
+type CoreUnit = RadrootsListingQuantity["value"]["unit"];
+type CoreCurrency = RadrootsCoreQuantityPrice["amount"]["currency"];
+
+const currency_to_code = (currency: CoreCurrency): string => {
+ if (Array.isArray(currency) && currency.length >= 3) {
+ const [a, b, c] = currency;
+ return String.fromCharCode(Number(a), Number(b), Number(c));
+ }
+ return String(currency);
+};
+
+const unit_to_code = (unit: CoreUnit): string => {
+ switch (unit) {
+ case "Each": return "each";
+ case "MassKg": return "kg";
+ case "MassG": return "g";
+ case "MassOz": return "oz";
+ case "MassLb": return "lb";
+ case "VolumeL": return "l";
+ case "VolumeMl": return "ml";
+ default: return String(unit).toLowerCase();
+ }
+};
+
+const clean_label = (value?: string | null) => value?.trim() || undefined;
+
+const normalize_listing_location = (
+ location?: RadrootsListingLocation | null,
+): NostrEventTagLocation | undefined => {
+ if (!location?.primary) return undefined;
+ const { primary, city, region, country, lat, lng } = location;
+ return {
+ primary,
+ city: city ?? undefined,
+ region: region ?? undefined,
+ country: country ?? undefined,
+ lat: typeof lat === "number" ? lat : undefined,
+ lng: typeof lng === "number" ? lng : undefined,
+ };
+};
+
+const normalize_image_size = (size: RadrootsListingImage["size"]) =>
+ size && typeof size?.w === "number" && typeof size?.h === "number" ? size : undefined;
+
+export const tag_listing_quantity = (opts: RadrootsListingQuantity): NostrEventTag => {
+ const tag: NostrEventTag = ["quantity", String(opts.value.amount), unit_to_code(opts.value.unit)];
+ const label = clean_label(opts.label ?? opts.value.label);
+ if (label) tag.push(label);
+ if (opts.count !== undefined && opts.count !== null) tag.push(String(opts.count));
+ return tag;
+};
+
+export const tag_listing_price = (price: RadrootsCoreQuantityPrice): NostrEventTag => {
+ const tag: NostrEventTag = [
+ "price",
+ String(price.amount.amount),
+ currency_to_code(price.amount.currency).toLowerCase(),
+ String(price.quantity.amount),
+ unit_to_code(price.quantity.unit),
+ ];
+ const label = clean_label(price.quantity.label);
+ if (label) tag.push(label);
+ return tag;
+};
+
+export const tag_listing_price_discount = (discount: RadrootsListingDiscount): NostrEventTag => {
+ const tag: NostrEventTag = [`price-discount-${discount.kind}`];
+ tag.push(JSON.stringify(discount.amount));
+ return tag;
+};
+
+export const tag_listing_location = (opts: NostrEventTagLocation): NostrEventTag => {
+ if (!opts.primary) return [];
+ const tag: NostrEventTag = ["location", opts.primary];
+ if (opts.city) tag.push(opts.city);
+ if (opts.region) tag.push(opts.region);
+ if (opts.country) tag.push(opts.country);
+ return tag;
+};
+
+export const tags_listing_location_geotags = (
+ opts: NostrEventTagLocation,
+ geohash?: string | null,
+): NostrEventTags => {
+ const { lat, lng: lon } = opts;
+ const input: NostrGeotagsInputData = {
+ lat,
+ lon,
+ geohash: clean_label(geohash),
+ };
+ return nostr_geotags(input, {
+ geohash: true,
+ gps: true,
+ iso31661: false,
+ iso31662: false,
+ city: false,
+ });
+};
+
+export const tag_listing_image = (opts: RadrootsListingImage): NostrEventTag => {
+ const tag: NostrEventTag = ["image", opts.url];
+ const size = normalize_image_size(opts.size);
+ if (size) tag.push(`${size.w}x${size.h}`);
+ return tag;
+};
+
+export const tags_listing = (opts: RadrootsListing): NostrEventTags => {
+ const { d_tag, product, quantities, prices } = opts;
+ const tags: NostrEventTags = [["d", d_tag]];
+ for (const key of RADROOTS_LISTING_PRODUCT_TAG_KEYS) {
+ const value = product[key];
+ if (value) tags.push([key, String(value)]);
+ }
+ for (const quantity of quantities) {
+ tags.push(tag_listing_quantity(quantity));
+ }
+ for (const price of prices) {
+ tags.push(tag_listing_price(price));
+ }
+ if (opts.discounts?.length) for (const discount of opts.discounts) if (discount) tags.push(tag_listing_price_discount(discount));
+ const location = normalize_listing_location(opts.location);
+ if (location) {
+ tags.push(tag_listing_location(location));
+ tags.push(...tags_listing_location_geotags(location, opts.location?.geohash));
+ }
+ if (opts.images) for (const image_tags of opts.images) if (image_tags) tags.push(tag_listing_image(image_tags));
+ return tags;
+};
+\ No newline at end of file
diff --git a/nostr/src/events/profile/lib.ts b/nostr/src/events/profile/lib.ts
@@ -0,0 +1,19 @@
+import type { RadrootsProfile } from "@radroots/events-bindings";
+import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js";
+import { nostr_event_create } from "../lib.js";
+
+export const KIND_RADROOTS_PROFILE = 0;
+export type KindRadrootsProfile = typeof KIND_RADROOTS_PROFILE;
+
+export const nostr_event_profile = async (
+ opts: NostrEventFigure<{ data: RadrootsProfile }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data } = opts;
+ return nostr_event_create({
+ ...opts,
+ basis: {
+ kind: KIND_RADROOTS_PROFILE,
+ content: JSON.stringify(data),
+ },
+ });
+};
diff --git a/nostr/src/events/profile/parse.ts b/nostr/src/events/profile/parse.ts
@@ -0,0 +1,22 @@
+import type { RadrootsProfile } from "@radroots/events-bindings";
+import { radroots_profile_schema } from "@radroots/events-bindings";
+import type { NostrEvent } from "../../types/nostr.js";
+import { parse_nostr_event_basis } from "../lib.js";
+import type { NostrEventBasis } from "../subscription.js";
+import { KIND_RADROOTS_PROFILE, type KindRadrootsProfile } from "./lib.js";
+
+export type RadrootsProfileNostrEvent = NostrEventBasis<KindRadrootsProfile> & { profile: RadrootsProfile };
+
+export const parse_nostr_profile_event = (
+ event: NostrEvent,
+): RadrootsProfileNostrEvent | undefined => {
+ const ev = parse_nostr_event_basis(event, KIND_RADROOTS_PROFILE);
+ if (!ev) return undefined;
+ try {
+ const parsed = JSON.parse(event.content);
+ const profile = radroots_profile_schema.parse(parsed);
+ return { ...ev, profile };
+ } catch {
+ return undefined;
+ }
+};
diff --git a/nostr/src/events/reaction/lib.ts b/nostr/src/events/reaction/lib.ts
@@ -0,0 +1,21 @@
+import type { RadrootsReaction } from "@radroots/events-bindings";
+import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js";
+import { nostr_event_create } from "../lib.js";
+import { tags_reaction } from "./tags.js";
+
+export const KIND_RADROOTS_REACTION = 7;
+export type KindRadrootsReaction = typeof KIND_RADROOTS_REACTION;
+
+export const nostr_event_reaction = async (
+ opts: NostrEventFigure<{ data: RadrootsReaction }>,
+): Promise<NostrSignedEvent | undefined> => {
+ const { data } = opts;
+ return nostr_event_create({
+ ...opts,
+ basis: {
+ kind: KIND_RADROOTS_REACTION,
+ content: data.content,
+ tags: tags_reaction(data),
+ },
+ });
+};
diff --git a/nostr/src/events/reaction/parse.ts b/nostr/src/events/reaction/parse.ts
@@ -0,0 +1,22 @@
+import type { RadrootsReaction } from "@radroots/events-bindings";
+import { radroots_reaction_schema } from "@radroots/events-bindings";
+import type { NostrEvent } from "../../types/nostr.js";
+import { parse_nostr_event_basis } from "../lib.js";
+import type { NostrEventBasis } from "../subscription.js";
+import { KIND_RADROOTS_REACTION, type KindRadrootsReaction } from "./lib.js";
+
+export type RadrootsReactionNostrEvent = NostrEventBasis<KindRadrootsReaction> & { reaction: RadrootsReaction };
+
+export const parse_nostr_reaction_event = (
+ event: NostrEvent,
+): RadrootsReactionNostrEvent | undefined => {
+ const ev = parse_nostr_event_basis(event, KIND_RADROOTS_REACTION);
+ if (!ev) return undefined;
+ try {
+ const parsed = JSON.parse(event.content);
+ const reaction = radroots_reaction_schema.parse(parsed);
+ return { ...ev, reaction };
+ } catch {
+ return undefined;
+ }
+};
diff --git a/nostr/src/events/reaction/tags.ts b/nostr/src/events/reaction/tags.ts
@@ -0,0 +1,16 @@
+import { RadrootsReaction } from "@radroots/events-bindings";
+import { NostrEventTags } from "../../types/lib.js";
+
+export const tags_reaction = (opts: RadrootsReaction): NostrEventTags => {
+ const { root } = opts;
+ const ref_kind = root.kind.toString();
+ const ref_author = root.author;
+ const relays = root.relays ?? [];
+ const tags: NostrEventTags = [
+ ["e", root.id, ...relays],
+ ["p", ref_author],
+ ["k", ref_kind],
+ ];
+ if (root.d_tag) tags.push(["a", `${ref_kind}:${ref_author}:${root.d_tag}`, ...relays]);
+ return tags;
+};
diff --git a/nostr/src/events/subscription.ts b/nostr/src/events/subscription.ts
@@ -0,0 +1,33 @@
+import type { NostrEvent } from "../types/nostr.js";
+import { parse_nostr_comment_event, RadrootsCommentNostrEvent } from "./comment/parse.js";
+import { parse_nostr_follow_event, RadrootsFollowNostrEvent } from "./follow/parse.js";
+import { parse_nostr_listing_event, RadrootsListingNostrEvent } from "./listing/parse.js";
+import { parse_nostr_profile_event, RadrootsProfileNostrEvent } from "./profile/parse.js";
+import { parse_nostr_reaction_event, RadrootsReactionNostrEvent } from "./reaction/parse.js";
+
+export type NostrEventBasis<T extends number> = {
+ id: string;
+ published_at: number;
+ author: string;
+ kind: T;
+};
+
+export type NostrEventPayload =
+ | RadrootsProfileNostrEvent
+ | RadrootsListingNostrEvent
+ | RadrootsCommentNostrEvent
+ | RadrootsReactionNostrEvent
+ | RadrootsFollowNostrEvent;
+
+export const nostr_event_on = (event: NostrEvent): NostrEventPayload | undefined => {
+ if (!event || typeof event.kind !== "number") return undefined;
+
+ switch (event.kind) {
+ case 0: return parse_nostr_profile_event(event);
+ case 30402: return parse_nostr_listing_event(event);
+ case 1111: return parse_nostr_comment_event(event);
+ case 7: return parse_nostr_reaction_event(event);
+ case 3: return parse_nostr_follow_event(event);
+ default: return undefined;
+ }
+};
diff --git a/nostr/src/index.ts b/nostr/src/index.ts
@@ -0,0 +1,38 @@
+export * from "./domain/trade/lib.js";
+export * from "./domain/trade/listing/accept/lib.js";
+export * from "./domain/trade/listing/conveyance/lib.js";
+export * from "./domain/trade/listing/fulfillment/lib.js";
+export * from "./domain/trade/listing/invoice/lib.js";
+export * from "./domain/trade/listing/order/lib.js";
+export * from "./domain/trade/listing/payment/lib.js";
+export * from "./domain/trade/listing/receipt/lib.js";
+export * from "./domain/trade/listing/tags.js";
+export * from "./domain/trade/tags.js";
+export * from "./events/comment/lib.js";
+export * from "./events/comment/parse.js";
+export * from "./events/comment/tags.js";
+export * from "./events/follow/lib.js";
+export * from "./events/follow/parse.js";
+export * from "./events/follow/tags.js";
+export * from "./events/job/lib.js";
+export * from "./events/job/tags.js";
+export * from "./events/job/utils.js";
+export * from "./events/lib.js";
+export * from "./events/listing/lib.js";
+export * from "./events/listing/parse.js";
+export * from "./events/listing/tags.js";
+export * from "./events/profile/lib.js";
+export * from "./events/profile/parse.js";
+export * from "./events/reaction/lib.js";
+export * from "./events/reaction/parse.js";
+export * from "./events/reaction/tags.js";
+export * from "./events/subscription.js";
+export * from "./keys.js";
+export * from "./kinds.js";
+export * from "./relays.js";
+export * from "./repository.js";
+export * from "./schemas/lib.js";
+export * from "./signers.js";
+export * from "./types.js";
+export * from "./utils/relays.js";
+export * from "./utils/tags.js";
diff --git a/nostr/src/keys.ts b/nostr/src/keys.ts
@@ -0,0 +1 @@
+export * from "./keys/lib.js";
diff --git a/nostr/src/keys/lib.ts b/nostr/src/keys/lib.ts
@@ -0,0 +1,100 @@
+import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
+import {
+ generateSecretKey,
+ getPublicKey,
+ nip19,
+} from "nostr-tools";
+
+export const REGEX_NOSTR_KEY = /^[a-f0-9]{64}$/;
+
+export const nostr_key_bytes_from_hex = (hex: string): Uint8Array => {
+ return hexToBytes(hex);
+};
+
+export const nostr_key_hex_from_bytes = (bytes: Uint8Array): string => {
+ return bytesToHex(bytes);
+};
+
+export const nostr_key_generate = (): string => {
+ const bytes = generateSecretKey();
+ return nostr_key_hex_from_bytes(bytes);
+};
+
+export const nostr_npub_encode = (public_key_hex?: string): string | undefined => {
+ try {
+ if (!public_key_hex) return undefined;
+ const npub = nip19.npubEncode(public_key_hex);
+ return npub;
+ } catch {
+ return undefined;
+ }
+};
+
+export const nostr_npub_decode = (npub?: string): string | undefined => {
+ try {
+ if (!npub) return undefined;
+ const { type, data } = nip19.decode(npub);
+ if (type === "npub" && data) return data;
+ } catch {
+ return undefined;
+ }
+};
+
+export const nostr_nsec_encode = (secret_key_hex?: string): string | undefined => {
+ try {
+ if (!secret_key_hex) return undefined;
+ const bytes = nostr_key_bytes_from_hex(secret_key_hex);
+ return nip19.nsecEncode(bytes);
+ } catch {
+ return undefined;
+ }
+};
+
+export const nostr_nsec_decode = (nsec?: string): string | undefined => {
+ try {
+ if (!nsec) return undefined;
+ const decode = nip19.decode(nsec);
+ if (decode && decode.type === "nsec" && decode.data) return bytesToHex(decode.data);
+ return undefined;
+ } catch {
+ return undefined;
+ }
+};
+
+export const nostr_nprofile_encode = (
+ public_key_hex: string,
+ relays: string[],
+): string | undefined => {
+ try {
+ if (!public_key_hex || !relays.length) return undefined;
+ const nprofile = nip19.nprofileEncode({ pubkey: public_key_hex, relays });
+ return nprofile;
+ } catch {
+ return undefined;
+ }
+};
+
+export const nostr_nprofile_decode = (
+ nprofile?: string,
+): nip19.ProfilePointer | undefined => {
+ try {
+ if (!nprofile) return undefined;
+ const { type, data } = nip19.decode(nprofile);
+ if (type === "nprofile" && data) return data;
+ } catch {
+ return undefined;
+ }
+};
+
+export const nostr_public_key_from_secret = (secret_key_hex: string): string => {
+ const bytes = nostr_key_bytes_from_hex(secret_key_hex);
+ return getPublicKey(bytes);
+};
+
+export const nostr_secret_key_validate = (secret_key: string): string | undefined => {
+ const trimmed = secret_key.trim();
+ if (REGEX_NOSTR_KEY.test(trimmed)) return trimmed;
+ const decoded = nostr_nsec_decode(trimmed);
+ if (decoded) return decoded;
+ return undefined;
+};
diff --git a/nostr/src/kinds.ts b/nostr/src/kinds.ts
@@ -0,0 +1,8 @@
+export {
+ CLASSIFIED,
+ COMMENT,
+ FOLLOWS,
+ NOTE,
+ PROFILE,
+ REACTION,
+} from "@welshman/util";
diff --git a/nostr/src/relay/lib.ts b/nostr/src/relay/lib.ts
@@ -0,0 +1,75 @@
+import {
+ NostrRelayInformationDocument,
+ NostrRelayInformationDocumentFields,
+} from "../types/lib.js";
+
+type NostrRelayInformationDocumentInput = Record<string, unknown> | string | null | undefined;
+
+const is_record = (value: unknown): value is Record<string, unknown> => {
+ if (!value) return false;
+ if (typeof value !== "object") return false;
+ if (Array.isArray(value)) return false;
+ return true;
+};
+
+const parse_supported_nips = (value: unknown): number[] | undefined => {
+ if (!Array.isArray(value)) return undefined;
+ const nips = value.filter((nip): nip is number => typeof nip === "number");
+ if (nips.length !== value.length) return undefined;
+ return nips;
+};
+
+const parse_limitation = (
+ limitation: Record<string, unknown> | undefined,
+): Pick<NostrRelayInformationDocument, "limitation_payment_required" | "limitation_restricted_writes"> => {
+ const payment_required =
+ limitation && typeof limitation.payment_required === "string"
+ ? limitation.payment_required
+ : undefined;
+ const restricted_writes =
+ limitation && typeof limitation.restricted_writes === "boolean"
+ ? limitation.restricted_writes
+ : undefined;
+ return {
+ limitation_payment_required: payment_required,
+ limitation_restricted_writes: restricted_writes,
+ };
+};
+
+export const nostr_relay_information_document_parse = (
+ data: NostrRelayInformationDocumentInput,
+): NostrRelayInformationDocument | undefined => {
+ const obj = typeof data === "string" ? JSON.parse(data) : data;
+ if (!is_record(obj)) return undefined;
+
+ const limitation = is_record(obj.limitation) ? obj.limitation : undefined;
+ const parsed_limitation = parse_limitation(limitation);
+
+ return {
+ id: typeof obj.id === "string" ? obj.id : undefined,
+ name: typeof obj.name === "string" ? obj.name : undefined,
+ description: typeof obj.description === "string" ? obj.description : undefined,
+ pubkey: typeof obj.pubkey === "string" ? obj.pubkey : undefined,
+ contact: typeof obj.contact === "string" ? obj.contact : undefined,
+ supported_nips: parse_supported_nips(obj.supported_nips),
+ software: typeof obj.software === "string" ? obj.software : undefined,
+ version: typeof obj.version === "string" ? obj.version : undefined,
+ limitation_payment_required: parsed_limitation.limitation_payment_required,
+ limitation_restricted_writes: parsed_limitation.limitation_restricted_writes,
+ };
+};
+
+export const nostr_relay_information_document_build = (
+ data: NostrRelayInformationDocumentInput,
+): NostrRelayInformationDocumentFields | undefined => {
+ const doc = nostr_relay_information_document_parse(data);
+ if (!doc) return undefined;
+ const result: Partial<NostrRelayInformationDocumentFields> = {};
+ for (const [key, value] of Object.entries(doc)) {
+ if (typeof value === "boolean") result[key as keyof NostrRelayInformationDocument] = value ? "1" : "0";
+ else if (Array.isArray(value)) result[key as keyof NostrRelayInformationDocument] = value.join(", ");
+ else if (value === null || value === undefined) result[key as keyof NostrRelayInformationDocument] = "";
+ else result[key as keyof NostrRelayInformationDocument] = String(value);
+ }
+ return result as NostrRelayInformationDocumentFields;
+};
diff --git a/nostr/src/relays.ts b/nostr/src/relays.ts
@@ -0,0 +1,65 @@
+import {
+ makeLoader as make_loader,
+ publish as publish_events,
+ request as request_events,
+ type AdapterContext,
+ type LoadOptions,
+ type PublishOptions,
+ type PublishResultsByRelay,
+ type RequestOptions,
+} from "@welshman/net";
+import type { TrustedEvent } from "@welshman/util";
+import type { NostrContext } from "./types/nostr.js";
+
+const build_adapter_context = (context?: NostrContext): AdapterContext | undefined => {
+ if (!context) return undefined;
+ return {
+ pool: context.pool,
+ repository: context.repository,
+ };
+};
+
+export type NostrRequestOptions = Omit<RequestOptions, "context"> & {
+ context?: NostrContext;
+};
+
+export type NostrPublishOptions = Omit<PublishOptions, "context"> & {
+ context?: NostrContext;
+};
+
+export type NostrLoadOptions = LoadOptions & {
+ context?: NostrContext;
+};
+
+export const nostr_request = async (opts: NostrRequestOptions): Promise<TrustedEvent[]> => {
+ const { context, ...rest } = opts;
+ const adapter_context = build_adapter_context(context);
+ return request_events({ ...rest, context: adapter_context });
+};
+
+export const nostr_load = async (opts: NostrLoadOptions): Promise<TrustedEvent[]> => {
+ const { context, ...rest } = opts;
+ const adapter_context = build_adapter_context(context);
+ const loader = make_loader({ delay: 200, timeout: 3000, threshold: 0.5, context: adapter_context });
+ return loader(rest);
+};
+
+export const nostr_publish = async (opts: NostrPublishOptions): Promise<PublishResultsByRelay> => {
+ const { context, ...rest } = opts;
+ const adapter_context = build_adapter_context(context);
+ return publish_events({ ...rest, context: adapter_context });
+};
+
+export const nostr_relays_open = (context: NostrContext, relays: string[]): void => {
+ for (const relay of relays) context.pool.get(relay).open();
+};
+
+export const nostr_relays_close = (context: NostrContext, relays: string[]): void => {
+ for (const relay of relays) context.pool.remove(relay);
+};
+
+export const nostr_relays_clear = (context: NostrContext): void => {
+ context.pool.clear();
+};
+
+export * from "./relay/lib.js";
diff --git a/nostr/src/repository.ts b/nostr/src/repository.ts
@@ -0,0 +1,53 @@
+import { Pool, Repository, Tracker } from "@welshman/net";
+import type { TrustedEvent } from "@welshman/util";
+import type { NostrContext } from "./types/nostr.js";
+
+export type NostrContextOptions = {
+ pool?: Pool;
+ repository?: Repository;
+ tracker?: Tracker;
+};
+
+export const nostr_pool_create = (): Pool => {
+ return new Pool();
+};
+
+export const nostr_pool_get = (): Pool => {
+ return Pool.get();
+};
+
+export const nostr_repository_create = (): Repository => {
+ return new Repository();
+};
+
+export const nostr_repository_get = (): Repository => {
+ return Repository.get();
+};
+
+export const nostr_tracker_create = (): Tracker => {
+ return new Tracker();
+};
+
+export const nostr_context_create = (opts?: NostrContextOptions): NostrContext => {
+ return {
+ pool: opts?.pool ?? nostr_pool_create(),
+ repository: opts?.repository ?? nostr_repository_create(),
+ tracker: opts?.tracker ?? nostr_tracker_create(),
+ };
+};
+
+export const nostr_context_default = (): NostrContext => {
+ return {
+ pool: nostr_pool_get(),
+ repository: nostr_repository_get(),
+ tracker: nostr_tracker_create(),
+ };
+};
+
+export const nostr_repository_dump = (repository: Repository): TrustedEvent[] => {
+ return repository.dump();
+};
+
+export const nostr_repository_load = (repository: Repository, events: TrustedEvent[]): void => {
+ repository.load(events);
+};
diff --git a/nostr/src/schemas/lib.ts b/nostr/src/schemas/lib.ts
@@ -0,0 +1,7 @@
+import { z } from "zod";
+
+export const nostr_tag_client_schema = z.object({
+ name: z.string(),
+ pubkey: z.string(),
+ relay: z.string()
+});
diff --git a/nostr/src/signers.ts b/nostr/src/signers.ts
@@ -0,0 +1,38 @@
+import { getNip07, Nip01Signer, Nip07Signer, Nip46Broker, Nip46Signer, Nip55Signer } from "@welshman/signer";
+import type { NostrSigner } from "./types/nostr.js";
+
+export type NostrSignerNip46Options = {
+ client_secret: string;
+ signer_pubkey: string;
+ relays: string[];
+};
+
+export type NostrSignerNip55Options = {
+ signer: string;
+ pubkey: string;
+};
+
+export const nostr_signer_nip01_create = (secret_key: string): NostrSigner => {
+ return new Nip01Signer(secret_key);
+};
+
+export const nostr_signer_nip07_create = (): NostrSigner => {
+ return new Nip07Signer();
+};
+
+export const nostr_signer_nip07_get = () => {
+ return getNip07();
+};
+
+export const nostr_signer_nip46_create = (opts: NostrSignerNip46Options): NostrSigner => {
+ const broker = new Nip46Broker({
+ clientSecret: opts.client_secret,
+ signerPubkey: opts.signer_pubkey,
+ relays: opts.relays,
+ });
+ return new Nip46Signer(broker);
+};
+
+export const nostr_signer_nip55_create = (opts: NostrSignerNip55Options): NostrSigner => {
+ return new Nip55Signer(opts.signer, opts.pubkey);
+};
diff --git a/nostr/src/types.ts b/nostr/src/types.ts
@@ -0,0 +1,2 @@
+export * from "./types/lib.js";
+export * from "./types/nostr.js";
diff --git a/nostr/src/types/lib.ts b/nostr/src/types/lib.ts
@@ -0,0 +1,57 @@
+import { type EventTemplate } from "nostr-tools";
+import { z } from "zod";
+import { nostr_tag_client_schema } from "../schemas/lib.js";
+
+export type NostrTagClient = z.infer<typeof nostr_tag_client_schema>;
+export type NostrEventTag = string[];
+export type NostrEventTags = NostrEventTag[];
+
+export type NostrEventTagClient = {
+ name: string;
+ pubkey: string;
+ relay: string;
+};
+
+export type NostrEventTagLocation = {
+ primary: string;
+ city?: string;
+ region?: string;
+ country?: string;
+ lat?: number;
+ lng?: number;
+};
+
+export type NostrEventTagImage = {
+ url: string;
+ size?: {
+ w: number;
+ h: number;
+ };
+};
+
+export type NostrRelayInformationDocument = {
+ id?: string;
+ name?: string;
+ description?: string;
+ pubkey?: string;
+ contact?: string;
+ supported_nips?: number[];
+ software?: string;
+ version?: string;
+ limitation_payment_required?: string;
+ limitation_restricted_writes?: boolean;
+}
+
+export type NostrRelayInformationDocumentFields = { [K in keyof NostrRelayInformationDocument]: string; };
+
+export type NostrNeventEncode = {
+ id: string;
+ relays: string[];
+ author: string;
+ kind: number;
+};
+
+export type NostrEventSign = {
+ secret_key: string;
+ event: EventTemplate;
+};
diff --git a/nostr/src/types/nostr.ts b/nostr/src/types/nostr.ts
@@ -0,0 +1,24 @@
+import type { Pool, Repository, Tracker } from "@welshman/net";
+import type { ISigner } from "@welshman/signer";
+import type { SignedEvent, TrustedEvent } from "@welshman/util";
+import type { NostrEventTagClient } from "./lib.js";
+
+export type NostrEvent = TrustedEvent;
+export type NostrSignedEvent = SignedEvent;
+export type NostrSigner = ISigner;
+
+export type NostrContext = {
+ pool: Pool;
+ repository: Repository;
+ tracker: Tracker;
+};
+
+export type NostrEventFigure<T extends object> = {
+ signer: NostrSigner;
+ date_published?: Date;
+ client?: NostrEventTagClient;
+} & T;
+
+export type NostrUser = {
+ pubkey: string;
+};
diff --git a/nostr/src/utils/relays.ts b/nostr/src/utils/relays.ts
@@ -0,0 +1,22 @@
+import type { RadrootsRelayDocument } from "@radroots/events-bindings";
+
+const NOSTR_RELAY_FORM_FIELD_RECORD: Record<keyof RadrootsRelayDocument, true> = {
+ name: true,
+ description: true,
+ pubkey: true,
+ contact: true,
+ supported_nips: true,
+ software: true,
+ version: true
+};
+
+const is_nostr_relay_form_field = (value: string): value is keyof RadrootsRelayDocument => {
+ return value in NOSTR_RELAY_FORM_FIELD_RECORD;
+};
+
+export const nostr_relay_parse_form_keys = (value: string): keyof RadrootsRelayDocument | "" => {
+ if (is_nostr_relay_form_field(value)) {
+ return value;
+ }
+ return "";
+};
diff --git a/nostr/src/utils/tags.ts b/nostr/src/utils/tags.ts
@@ -0,0 +1,11 @@
+import { NostrEventTag, NostrEventTagClient } from "../types/lib.js";
+
+export const TAG_E = "e";
+export const TAG_I = "i";
+
+export const nostr_tag_client = (opts: NostrEventTagClient, d_tag?: string): NostrEventTag => {
+ const tag = ["client", opts.name];
+ if (d_tag) tag.push(`31990:${opts.pubkey}:${d_tag}`);
+ tag.push(opts.relay);
+ return tag;
+};
diff --git a/nostr/tsconfig.cjs.json b/nostr/tsconfig.cjs.json
@@ -0,0 +1,15 @@
+{
+ "extends": "@radroots/tsconfig/tsconfig.esm.json",
+ "compilerOptions": {
+ "module": "CommonJS",
+ "moduleResolution": "Node",
+ "rootDir": "./src",
+ "outDir": "dist/cjs",
+ "declaration": false,
+ "declarationMap": false,
+ "emitDeclarationOnly": false,
+ "tsBuildInfoFile": "node_modules/.cache/tsc.nostr.cjs.tsbuildinfo"
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/utils-nostr/tsconfig.esm.json b/nostr/tsconfig.esm.json
diff --git a/utils-nostr/tsconfig.json b/nostr/tsconfig.json
diff --git a/utils-nostr/package.json b/utils-nostr/package.json
@@ -1,47 +0,0 @@
-{
- "name": "@radroots/utils-nostr",
- "version": "0.0.1",
- "private": true,
- "license": "GPLv3",
- "type": "module",
- "main": "./dist/cjs/index.js",
- "module": "./dist/esm/index.js",
- "types": "./dist/types/index.d.ts",
- "exports": {
- ".": {
- "types": "./dist/types/index.d.ts",
- "import": "./dist/esm/index.js",
- "require": "./dist/cjs/index.js"
- }
- },
- "scripts": {
- "build:esm": "tsc -p tsconfig.esm.json",
- "build:cjs": "tsc -p tsconfig.cjs.json",
- "build": "npm run clean && npm run build:esm && npm run build:cjs",
- "prebuild": "npm run clean",
- "clean": "rimraf dist",
- "dev": "npm run watch",
- "watch": "tsc -w"
- },
- "publishConfig": {
- "access": "public"
- },
- "devDependencies": {
- "@radroots/tsconfig": "workspace:*",
- "@types/node": "^22.13.1",
- "rimraf": "^6.0.1",
- "typescript": "5.8.3"
- },
- "dependencies": {
- "@noble/curves": "^1.6.0",
- "@noble/hashes": "^1.4.0",
- "@nostr-dev-kit/ndk": "2.14.33",
- "@radroots/core-bindings": "workspace:*",
- "@radroots/events-bindings": "workspace:*",
- "@radroots/trade-bindings": "workspace:*",
- "@radroots/utils": "workspace:*",
- "nostr-geotags": "^0.7.2",
- "nostr-tools": "^2.10.4",
- "zod": "^4.2.1"
- }
-}
diff --git a/utils-nostr/src/domain/trade/lib.ts b/utils-nostr/src/domain/trade/lib.ts
@@ -1,52 +0,0 @@
-import { KIND_TRADE_LISTING_ACCEPT_REQ, KIND_TRADE_LISTING_ACCEPT_RES, KIND_TRADE_LISTING_CANCEL_REQ, KIND_TRADE_LISTING_CANCEL_RES, KIND_TRADE_LISTING_CONVEYANCE_REQ, KIND_TRADE_LISTING_CONVEYANCE_RES, KIND_TRADE_LISTING_FULFILL_REQ, KIND_TRADE_LISTING_FULFILL_RES, KIND_TRADE_LISTING_INVOICE_REQ, KIND_TRADE_LISTING_INVOICE_RES, KIND_TRADE_LISTING_ORDER_REQ, KIND_TRADE_LISTING_ORDER_RES, KIND_TRADE_LISTING_PAYMENT_REQ, KIND_TRADE_LISTING_PAYMENT_RES, KIND_TRADE_LISTING_RECEIPT_REQ, KIND_TRADE_LISTING_RECEIPT_RES, KIND_TRADE_LISTING_REFUND_REQ, KIND_TRADE_LISTING_REFUND_RES } from "@radroots/trade-bindings";
-import type { TradeListingStage } from "@radroots/trade-bindings";
-
-export type TradeListingStageKind = TradeListingStage["kind"];
-
-export const TRADE_LISTING_STAGE = {
- Order: "order",
- Accept: "accept",
- Conveyance: "conveyance",
- Invoice: "invoice",
- Payment: "payment",
- Fulfillment: "fulfillment",
- Receipt: "receipt",
- Cancel: "cancel",
- Refund: "refund",
-} as const satisfies Record<string, TradeListingStageKind>;
-
-export const TRADE_LISTING_STAGE_KINDS = [
- TRADE_LISTING_STAGE.Order,
- TRADE_LISTING_STAGE.Accept,
- TRADE_LISTING_STAGE.Conveyance,
- TRADE_LISTING_STAGE.Invoice,
- TRADE_LISTING_STAGE.Payment,
- TRADE_LISTING_STAGE.Fulfillment,
- TRADE_LISTING_STAGE.Receipt,
- TRADE_LISTING_STAGE.Cancel,
- TRADE_LISTING_STAGE.Refund,
-] as const satisfies readonly TradeListingStageKind[];
-
-export const REQUEST_KINDS: Record<TradeListingStageKind, number> = {
- order: KIND_TRADE_LISTING_ORDER_REQ,
- accept: KIND_TRADE_LISTING_ACCEPT_REQ,
- conveyance: KIND_TRADE_LISTING_CONVEYANCE_REQ,
- invoice: KIND_TRADE_LISTING_INVOICE_REQ,
- payment: KIND_TRADE_LISTING_PAYMENT_REQ,
- fulfillment: KIND_TRADE_LISTING_FULFILL_REQ,
- receipt: KIND_TRADE_LISTING_RECEIPT_REQ,
- cancel: KIND_TRADE_LISTING_CANCEL_REQ,
- refund: KIND_TRADE_LISTING_REFUND_REQ,
-};
-
-export const RESULT_KINDS: Record<TradeListingStageKind, number> = {
- order: KIND_TRADE_LISTING_ORDER_RES,
- accept: KIND_TRADE_LISTING_ACCEPT_RES,
- conveyance: KIND_TRADE_LISTING_CONVEYANCE_RES,
- invoice: KIND_TRADE_LISTING_INVOICE_RES,
- payment: KIND_TRADE_LISTING_PAYMENT_RES,
- fulfillment: KIND_TRADE_LISTING_FULFILL_RES,
- receipt: KIND_TRADE_LISTING_RECEIPT_RES,
- cancel: KIND_TRADE_LISTING_CANCEL_RES,
- refund: KIND_TRADE_LISTING_REFUND_RES,
-};
diff --git a/utils-nostr/src/domain/trade/listing/accept/lib.ts b/utils-nostr/src/domain/trade/listing/accept/lib.ts
@@ -1,62 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { RadrootsJobInput } from "@radroots/events-bindings";
-import { KIND_TRADE_LISTING_ACCEPT_REQ, KIND_TRADE_LISTING_ACCEPT_RES, MARKER_LISTING, MARKER_ORDER_RESULT, TradeListingAcceptRequest, TradeListingAcceptResult } from "@radroots/trade-bindings";
-import { ndk_event } from "../../../../events/lib.js";
-import { NDKEventFigure } from "../../../../types/ndk.js";
-import {
- build_request_tags,
- build_result_tags,
- CommonRequestOpts,
- CommonResultOpts,
- make_event_input
-} from "../../tags.js";
-import { tags_trade_listing_chain } from "../tags.js";
-
-export const ndk_event_trade_listing_accept_request = async (
- opts: NDKEventFigure<{ data: TradeListingAcceptRequest; options?: CommonRequestOpts }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data, options } = opts;
-
- const inputs: RadrootsJobInput[] = [
- make_event_input(data.order_result_event_id, MARKER_ORDER_RESULT),
- make_event_input(data.listing_event_id, MARKER_LISTING),
- ];
-
- const tags = build_request_tags(KIND_TRADE_LISTING_ACCEPT_REQ, inputs, options);
-
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_ACCEPT_REQ, content: "", tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
-
-export const ndk_event_trade_listing_accept_result = async (
- opts: NDKEventFigure<{
- request_event_id: string;
- content: TradeListingAcceptResult | string;
- options?: CommonResultOpts;
- }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, request_event_id, content, options } = opts;
-
- const base_tags = build_result_tags(
- KIND_TRADE_LISTING_ACCEPT_RES,
- request_event_id,
- options
- );
- const tags = options?.chain
- ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
- : base_tags;
-
- const content_body = typeof content === "string" ? content : JSON.stringify(content);
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_ACCEPT_RES, content: content_body, tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
diff --git a/utils-nostr/src/domain/trade/listing/conveyance/lib.ts b/utils-nostr/src/domain/trade/listing/conveyance/lib.ts
@@ -1,63 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { RadrootsJobInput } from "@radroots/events-bindings";
-import { KIND_TRADE_LISTING_CONVEYANCE_REQ, KIND_TRADE_LISTING_CONVEYANCE_RES, MARKER_ACCEPT_RESULT, MARKER_PAYLOAD, TradeListingConveyanceRequest, TradeListingConveyanceResult } from "@radroots/trade-bindings";
-import { ndk_event } from "../../../../events/lib.js";
-import { NDKEventFigure } from "../../../../types/ndk.js";
-import {
- build_request_tags,
- build_result_tags,
- CommonRequestOpts,
- CommonResultOpts,
- make_event_input,
- make_text_input,
-} from "../../tags.js";
-import { tags_trade_listing_chain } from "../tags.js";
-
-export const ndk_event_trade_listing_conveyance_request = async (
- opts: NDKEventFigure<{ data: TradeListingConveyanceRequest; options?: CommonRequestOpts }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data, options } = opts;
-
- const inputs: RadrootsJobInput[] = [
- make_event_input(data.accept_result_event_id, MARKER_ACCEPT_RESULT),
- make_text_input({ method: data.method }, MARKER_PAYLOAD),
- ];
-
- const tags = build_request_tags(KIND_TRADE_LISTING_CONVEYANCE_REQ, inputs, options);
-
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_CONVEYANCE_REQ, content: "", tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
-
-export const ndk_event_trade_listing_conveyance_result = async (
- opts: NDKEventFigure<{
- request_event_id: string;
- content: TradeListingConveyanceResult | string;
- options?: CommonResultOpts;
- }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, request_event_id, content, options } = opts;
-
- const base_tags = build_result_tags(
- KIND_TRADE_LISTING_CONVEYANCE_RES,
- request_event_id,
- options
- );
- const tags = options?.chain
- ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
- : base_tags;
-
- const content_body = typeof content === "string" ? content : JSON.stringify(content);
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_CONVEYANCE_RES, content: content_body, tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/domain/trade/listing/fulfillment/lib.ts b/utils-nostr/src/domain/trade/listing/fulfillment/lib.ts
@@ -1,61 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { RadrootsJobInput } from "@radroots/events-bindings";
-import { KIND_TRADE_LISTING_FULFILL_REQ, KIND_TRADE_LISTING_FULFILL_RES, MARKER_PAYMENT_RESULT, TradeListingFulfillmentRequest, TradeListingFulfillmentResult } from "@radroots/trade-bindings";
-import { ndk_event } from "../../../../events/lib.js";
-import { NDKEventFigure } from "../../../../types/ndk.js";
-import {
- build_request_tags,
- build_result_tags,
- CommonRequestOpts,
- CommonResultOpts,
- make_event_input
-} from "../../tags.js";
-import { tags_trade_listing_chain } from "../tags.js";
-
-export const ndk_event_trade_listing_fulfillment_request = async (
- opts: NDKEventFigure<{ data: TradeListingFulfillmentRequest; options?: CommonRequestOpts }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data, options } = opts;
-
- const inputs: RadrootsJobInput[] = [
- make_event_input(data.payment_result_event_id, MARKER_PAYMENT_RESULT),
- ];
-
- const tags = build_request_tags(KIND_TRADE_LISTING_FULFILL_REQ, inputs, options);
-
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_FULFILL_REQ, content: "", tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
-
-export const ndk_event_trade_listing_fulfillment_result = async (
- opts: NDKEventFigure<{
- request_event_id: string;
- content: TradeListingFulfillmentResult | string;
- options?: CommonResultOpts;
- }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, request_event_id, content, options } = opts;
-
- const base_tags = build_result_tags(
- KIND_TRADE_LISTING_FULFILL_RES,
- request_event_id,
- options
- );
- const tags = options?.chain
- ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
- : base_tags;
-
- const content_body = typeof content === "string" ? content : JSON.stringify(content);
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_FULFILL_RES, content: content_body, tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
diff --git a/utils-nostr/src/domain/trade/listing/invoice/lib.ts b/utils-nostr/src/domain/trade/listing/invoice/lib.ts
@@ -1,73 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { RadrootsJobInput } from "@radroots/events-bindings";
-import { KIND_TRADE_LISTING_INVOICE_REQ, KIND_TRADE_LISTING_INVOICE_RES, MARKER_ACCEPT_RESULT, TradeListingInvoiceRequest, TradeListingInvoiceResult } from "@radroots/trade-bindings";
-import { ndk_event } from "../../../../events/lib.js";
-import { NDKEventFigure } from "../../../../types/ndk.js";
-import {
- build_request_tags,
- build_result_tags,
- CommonRequestOpts,
- CommonResultOpts,
- make_event_input
-} from "../../tags.js";
-import { tags_trade_listing_chain } from "../tags.js";
-
-export const ndk_event_trade_listing_invoice_request = async (
- opts: NDKEventFigure<{ data: TradeListingInvoiceRequest; options?: CommonRequestOpts }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data, options } = opts;
-
- const inputs: RadrootsJobInput[] = [
- make_event_input(data.accept_result_event_id, MARKER_ACCEPT_RESULT),
- ];
-
- const tags = build_request_tags(KIND_TRADE_LISTING_INVOICE_REQ, inputs, options);
-
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_INVOICE_REQ, content: "", tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
-
-export const ndk_event_trade_listing_invoice_result = async (
- opts: NDKEventFigure<{
- request_event_id: string;
- content: TradeListingInvoiceResult | string;
- options?: Omit<CommonResultOpts, "payment_sat" | "payment_bolt11"> & {
- chain?: { e_root: string; d?: string; e_prev?: string };
- };
- }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, request_event_id, content, options } = opts;
-
- const parsed: TradeListingInvoiceResult | undefined =
- typeof content === "string" ? undefined : (content as TradeListingInvoiceResult);
-
- const base_tags = build_result_tags(
- KIND_TRADE_LISTING_INVOICE_RES,
- request_event_id,
- options,
- parsed
- ? {
- payment_sat: parsed.total_sat,
- payment_bolt11: parsed.bolt11 ?? undefined,
- }
- : undefined
- );
-
- const tags = options?.chain
- ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
- : base_tags;
-
- const content_body = typeof content === "string" ? content : JSON.stringify(content);
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_INVOICE_RES, content: content_body, tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
diff --git a/utils-nostr/src/domain/trade/listing/order/lib.ts b/utils-nostr/src/domain/trade/listing/order/lib.ts
@@ -1,70 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { RadrootsJobInput } from "@radroots/events-bindings";
-import { KIND_TRADE_LISTING_ORDER_REQ, KIND_TRADE_LISTING_ORDER_RES, MARKER_LISTING, MARKER_PAYLOAD, TradeListingOrderResult, type TradeListingOrderRequest } from "@radroots/trade-bindings";
-import { ndk_event } from "../../../../events/lib.js";
-import { NDKEventFigure } from "../../../../types/ndk.js";
-import {
- build_request_tags,
- build_result_tags,
- CommonRequestOpts,
- CommonResultOpts,
- make_event_input,
- make_text_input,
-} from "../../tags.js";
-import { tags_trade_listing_chain } from "../tags.js";
-
-export const ndk_event_trade_listing_order_request = async (
- opts: NDKEventFigure<{ data: TradeListingOrderRequest; options?: CommonRequestOpts }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data, options } = opts;
-
- const inputs: RadrootsJobInput[] = [
- make_event_input(data.event.id, MARKER_LISTING, data.event.relays ?? undefined),
- make_text_input(data.payload, MARKER_PAYLOAD),
- ];
-
- const tags = build_request_tags(KIND_TRADE_LISTING_ORDER_REQ, inputs, options);
-
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_ORDER_REQ, content: "", tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
-
-export const ndk_event_trade_listing_order_result = async (
- opts: NDKEventFigure<{
- request_event_id: string;
- content: TradeListingOrderResult | string;
- options?: CommonResultOpts;
- }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, request_event_id, content, options } = opts;
-
- const include_inputs =
- options?.include_inputs && !options.encrypted
- ? options.include_inputs.map(s => make_text_input(s, MARKER_PAYLOAD))
- : [];
-
- const base_tags = build_result_tags(
- KIND_TRADE_LISTING_ORDER_RES,
- request_event_id,
- options,
- { inputs: include_inputs }
- );
-
- const tags = options?.chain
- ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
- : base_tags;
-
- const content_body = typeof content === "string" ? content : JSON.stringify(content);
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_ORDER_RES, content: content_body, tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
diff --git a/utils-nostr/src/domain/trade/listing/payment/lib.ts b/utils-nostr/src/domain/trade/listing/payment/lib.ts
@@ -1,63 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { RadrootsJobInput } from "@radroots/events-bindings";
-import { KIND_TRADE_LISTING_PAYMENT_REQ, KIND_TRADE_LISTING_PAYMENT_RES, MARKER_INVOICE_RESULT, MARKER_PROOF, TradeListingPaymentProofRequest, TradeListingPaymentResult } from "@radroots/trade-bindings";
-import { ndk_event } from "../../../../events/lib.js";
-import { NDKEventFigure } from "../../../../types/ndk.js";
-import {
- build_request_tags,
- build_result_tags,
- CommonRequestOpts,
- CommonResultOpts,
- make_event_input,
- make_text_input,
-} from "../../tags.js";
-import { tags_trade_listing_chain } from "../tags.js";
-
-export const ndk_event_trade_listing_payment_request = async (
- opts: NDKEventFigure<{ data: TradeListingPaymentProofRequest; options?: CommonRequestOpts }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data, options } = opts;
-
- const inputs: RadrootsJobInput[] = [
- make_event_input(data.invoice_result_event_id, MARKER_INVOICE_RESULT),
- make_text_input(data.proof, MARKER_PROOF),
- ];
-
- const tags = build_request_tags(KIND_TRADE_LISTING_PAYMENT_REQ, inputs, options);
-
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_PAYMENT_REQ, content: "", tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
-
-export const ndk_event_trade_listing_payment_result = async (
- opts: NDKEventFigure<{
- request_event_id: string;
- content: TradeListingPaymentResult | string;
- options?: CommonResultOpts;
- }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, request_event_id, content, options } = opts;
-
- const base_tags = build_result_tags(
- KIND_TRADE_LISTING_PAYMENT_RES,
- request_event_id,
- options
- );
- const tags = options?.chain
- ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
- : base_tags;
-
- const content_body = typeof content === "string" ? content : JSON.stringify(content);
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_PAYMENT_RES, content: content_body, tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
diff --git a/utils-nostr/src/domain/trade/listing/receipt/lib.ts b/utils-nostr/src/domain/trade/listing/receipt/lib.ts
@@ -1,63 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { RadrootsJobInput } from "@radroots/events-bindings";
-import { KIND_TRADE_LISTING_RECEIPT_REQ, KIND_TRADE_LISTING_RECEIPT_RES, MARKER_FULFILLMENT_RESULT, MARKER_PAYLOAD, TradeListingReceiptRequest, TradeListingReceiptResult } from "@radroots/trade-bindings";
-import { ndk_event } from "../../../../events/lib.js";
-import { NDKEventFigure } from "../../../../types/ndk.js";
-import {
- build_request_tags,
- build_result_tags,
- CommonRequestOpts,
- CommonResultOpts,
- make_event_input,
- make_text_input,
-} from "../../tags.js";
-import { tags_trade_listing_chain } from "../tags.js";
-
-export const ndk_event_trade_listing_receipt_request = async (
- opts: NDKEventFigure<{ data: TradeListingReceiptRequest; options?: CommonRequestOpts }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data, options } = opts;
-
- const inputs: RadrootsJobInput[] = [
- make_event_input(data.fulfillment_result_event_id, MARKER_FULFILLMENT_RESULT),
- ...(data.note ? [make_text_input({ note: data.note }, MARKER_PAYLOAD)] : []),
- ];
-
- const tags = build_request_tags(KIND_TRADE_LISTING_RECEIPT_REQ, inputs, options);
-
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_RECEIPT_REQ, content: "", tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
-
-export const ndk_event_trade_listing_receipt_result = async (
- opts: NDKEventFigure<{
- request_event_id: string;
- content: TradeListingReceiptResult | string;
- options?: CommonResultOpts;
- }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, request_event_id, content, options } = opts;
-
- const base_tags = build_result_tags(
- KIND_TRADE_LISTING_RECEIPT_RES,
- request_event_id,
- options
- );
- const tags = options?.chain
- ? [...base_tags, ...tags_trade_listing_chain(options.chain)]
- : base_tags;
-
- const content_body = typeof content === "string" ? content : JSON.stringify(content);
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_TRADE_LISTING_RECEIPT_RES, content: content_body, tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
diff --git a/utils-nostr/src/domain/trade/tags.ts b/utils-nostr/src/domain/trade/tags.ts
@@ -1,88 +0,0 @@
-import { RadrootsJobInput } from "@radroots/events-bindings";
-import { tags_job_request, tags_job_result } from "../../events/job/tags.js";
-
-export type CommonRequestOpts = {
- output?: string;
- bid_sat?: number;
- relays?: string[];
- providers?: string[];
- topics?: string[];
- encrypted?: boolean;
- params?: Array<{ key: string; value: string }>;
-};
-
-export type CommonResultOpts = {
- request_relay_hint?: string;
- request_json?: string;
- customer_pubkey?: string;
- payment_sat?: number;
- payment_bolt11?: string;
- encrypted?: boolean;
- include_inputs?: string[];
- chain?: { e_root: string; d?: string; e_prev?: string };
-};
-
-export const make_event_input = (
- id: string,
- marker: string,
- relay?: string
-): RadrootsJobInput => ({
- data: id,
- input_type: "event",
- ...(relay ? { relay } : {}),
- marker,
-});
-
-export const make_text_input = (
- payload: unknown,
- marker: string
-): RadrootsJobInput => ({
- data: typeof payload === "string" ? payload : JSON.stringify(payload),
- input_type: "text",
- marker,
-});
-
-export const build_request_tags = (
- kind: number,
- inputs: RadrootsJobInput[],
- opts?: CommonRequestOpts
-) =>
- tags_job_request({
- kind,
- inputs,
- output: opts?.output,
- params: opts?.params ?? [],
- bid_sat: opts?.bid_sat,
- relays: opts?.relays ?? [],
- providers: opts?.providers ?? [],
- topics: opts?.topics ?? [],
- encrypted: !!opts?.encrypted,
- });
-
-export const build_result_tags = (
- kind: number,
- request_event_id: string,
- opts?: CommonResultOpts,
- extra?: {
- inputs?: RadrootsJobInput[];
- payment_sat?: number;
- payment_bolt11?: string;
- }
-) =>
- tags_job_result({
- kind,
- request_event: {
- id: request_event_id,
- ...(opts?.request_relay_hint ? { relays: opts.request_relay_hint } : {}),
- },
- request_json: opts?.request_json,
- inputs: !opts?.encrypted && extra?.inputs?.length ? extra.inputs : [],
- customer_pubkey: opts?.customer_pubkey,
- payment:
- extra?.payment_sat !== undefined
- ? { amount_sat: extra.payment_sat, bolt11: extra.payment_bolt11 }
- : opts?.payment_sat !== undefined
- ? { amount_sat: opts.payment_sat, bolt11: opts.payment_bolt11 }
- : undefined,
- encrypted: !!opts?.encrypted,
- });
diff --git a/utils-nostr/src/events/comment/lib.ts b/utils-nostr/src/events/comment/lib.ts
@@ -1,21 +0,0 @@
-import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
-import { RadrootsComment } from "@radroots/events-bindings";
-import { NDKEventFigure } from "../../types/ndk.js";
-import { ndk_event } from "../lib.js";
-import { tags_comment } from "./tags.js";
-
-export const KIND_RADROOTS_COMMENT = 1111;
-export type KindRadrootsComment = typeof KIND_RADROOTS_COMMENT;
-
-export const ndk_event_comment = async (opts: NDKEventFigure<{ data: RadrootsComment; }>): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data } = opts;
- return await ndk_event({
- ndk,
- ndk_user,
- basis: {
- kind: NDKKind.GenericReply,
- content: data.content,
- tags: tags_comment(data)
- },
- });
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/events/comment/parse.ts b/utils-nostr/src/events/comment/parse.ts
@@ -1,20 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { radroots_comment_schema, RadrootsComment } from "@radroots/events-bindings";
-import { parse_nostr_event_basis } from "../lib.js";
-import { NdkEventBasis } from "../subscription.js";
-import { KIND_RADROOTS_COMMENT, type KindRadrootsComment } from "./lib.js";
-
-export type RadrootsCommentNostrEvent = NdkEventBasis<KindRadrootsComment> & { comment: RadrootsComment; }
-
-export const parse_nostr_comment_event = (event: NDKEvent): RadrootsCommentNostrEvent | undefined => {
- const ev = parse_nostr_event_basis(event, KIND_RADROOTS_COMMENT);
- if (!ev) return undefined;
- try {
- const parsed = JSON.parse(event.content);
- const comment = radroots_comment_schema.parse(parsed);
- return { ...ev, comment };
- } catch {
- return undefined;
- }
-};
-
diff --git a/utils-nostr/src/events/comment/tags.ts b/utils-nostr/src/events/comment/tags.ts
@@ -1,37 +0,0 @@
-import { RadrootsComment } from "@radroots/events-bindings";
-import { NostrEventTags } from "../../types/lib.js";
-
-export const tags_comment = (opts: RadrootsComment): NostrEventTags => {
- const { root: root_event, parent: parent_event } = opts;
-
- const root = {
- kind: root_event.kind.toString(),
- author: root_event.author,
- id: root_event.id,
- d_tag: root_event.d_tag,
- relays: root_event.relays || [],
- };
-
- const parent = (parent_event && parent_event.id)
- ? {
- kind: parent_event.kind.toString(),
- author: parent_event.author,
- id: parent_event.id,
- d_tag: parent_event.d_tag,
- relays: parent_event.relays || [],
- }
- : root;
-
- const tags: NostrEventTags = [
- ["E", root.id, ...root.relays],
- ["P", root.author],
- ["K", root.kind],
- ...(root.d_tag ? [["A", `${root.kind}:${root.author}:${root.d_tag}`, ...root.relays]] : []),
- ["e", parent.id, ...parent.relays],
- ["p", parent.author],
- ["k", parent.kind],
- ...(parent.d_tag ? [["a", `${parent.kind}:${parent.author}:${parent.d_tag}`, ...parent.relays]] : []),
- ];
-
- return tags;
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/events/follow/lib.ts b/utils-nostr/src/events/follow/lib.ts
@@ -1,21 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { RadrootsFollow } from "@radroots/events-bindings";
-import { NDKEventFigure } from "../../types/ndk.js";
-import { ndk_event } from "../lib.js";
-import { tags_follow_list } from "./tags.js";
-
-export const KIND_RADROOTS_FOLLOW = 3;
-export type KindRadrootsFollow = typeof KIND_RADROOTS_FOLLOW;
-
-export const ndk_event_follows = async (opts: NDKEventFigure<{ data: RadrootsFollow; }>): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data } = opts;
- return await ndk_event({
- ndk,
- ndk_user,
- basis: {
- kind: 3,
- content: ``,
- tags: tags_follow_list(data.list),
- },
- });
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/events/follow/parse.ts b/utils-nostr/src/events/follow/parse.ts
@@ -1,19 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { RadrootsFollow, radroots_follow_schema } from "@radroots/events-bindings";
-import { parse_nostr_event_basis } from "../lib.js";
-import { NdkEventBasis } from "../subscription.js";
-import { KIND_RADROOTS_FOLLOW, type KindRadrootsFollow } from "./lib.js";
-
-export type RadrootsFollowNostrEvent = NdkEventBasis<KindRadrootsFollow> & { follow: RadrootsFollow; }
-
-export const parse_nostr_follow_event = (event: NDKEvent): RadrootsFollowNostrEvent | undefined => {
- const ev = parse_nostr_event_basis(event, KIND_RADROOTS_FOLLOW);
- if (!ev) return undefined;
- try {
- const parsed = JSON.parse(event.content);
- const follow = radroots_follow_schema.parse(parsed);
- return { ...ev, follow };
- } catch {
- return undefined;
- }
-};
diff --git a/utils-nostr/src/events/follow/tags.ts b/utils-nostr/src/events/follow/tags.ts
@@ -1,11 +0,0 @@
-import { RadrootsFollowProfile } from "@radroots/events-bindings";
-import { NostrEventTags } from "../../types/lib.js";
-
-export const tags_follow_list = (list: RadrootsFollowProfile[]): NostrEventTags => {
- return list.map(({ public_key, relay_url, contact_name }) => {
- const entry = [`p`, public_key];
- if (relay_url) entry.push(relay_url);
- if (contact_name) entry.push(contact_name);
- return entry;
- });
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/events/job/lib.ts b/utils-nostr/src/events/job/lib.ts
@@ -1,95 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { JobFeedbackStatus, KIND_JOB_FEEDBACK, RadrootsJobFeedback, RadrootsJobRequest, RadrootsJobResult } from "@radroots/events-bindings";
-import { NDKEventFigure } from "../../types/ndk.js";
-import { ndk_event } from "../lib.js";
-import { tags_job_feedback, tags_job_request, tags_job_result } from "./tags.js";
-
-export const ndk_event_job_request = async (opts: NDKEventFigure<{ data: RadrootsJobRequest; }>): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data } = opts;
- return await ndk_event({
- ndk,
- ndk_user,
- basis: {
- kind: data.kind,
- content: "",
- tags: tags_job_request(data),
- },
- });
-};
-
-export const ndk_event_job_result = async (opts: NDKEventFigure<{ data: RadrootsJobResult; }>): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data } = opts;
- return await ndk_event({
- ndk,
- ndk_user,
- basis: {
- kind: data.kind,
- content: data.content || "",
- tags: tags_job_result(data),
- },
- });
-};
-
-export const ndk_event_job_feedback = async (opts: NDKEventFigure<{ data: RadrootsJobFeedback; }>): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data } = opts;
- return await ndk_event({
- ndk,
- ndk_user,
- basis: {
- kind: data.kind,
- content: data.content || "",
- tags: tags_job_feedback(data),
- },
- });
-};
-
-export const ndk_event_job_feedback_todo = async (
- opts: NDKEventFigure<{
- request_event_id: string;
- status:
- | JobFeedbackStatus
- | "payment-required"
- | "processing"
- | "error"
- | "success"
- | "partial";
- content?: string;
- options?: {
- request_relay_hint?: string;
- extra_info?: string;
- customer_pubkey?: string;
- amount_sat?: number;
- bolt11?: string;
- encrypted?: boolean;
- };
- }>
-): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, request_event_id, status, content, options } = opts;
-
- const fb: RadrootsJobFeedback = {
- kind: KIND_JOB_FEEDBACK,
- status: status as JobFeedbackStatus,
- extra_info: options?.extra_info,
- request_event: {
- id: request_event_id,
- ...(options?.request_relay_hint ? { relays: options.request_relay_hint } : {}),
- },
- customer_pubkey: options?.customer_pubkey,
- payment:
- options?.amount_sat !== undefined
- ? { amount_sat: options.amount_sat, bolt11: options?.bolt11 }
- : undefined,
- content,
- encrypted: !!options?.encrypted,
- };
-
- const tags = tags_job_feedback(fb);
-
- return await ndk_event({
- ndk,
- ndk_user,
- basis: { kind: KIND_JOB_FEEDBACK, content: content ?? "", tags },
- client: opts.client,
- date_published: opts.date_published,
- });
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/events/job/tags.ts b/utils-nostr/src/events/job/tags.ts
@@ -1,75 +0,0 @@
-import { RadrootsJobFeedback, RadrootsJobInput, RadrootsJobRequest, RadrootsJobResult } from "@radroots/events-bindings";
-import { NostrEventTag, NostrEventTags } from "../../types/lib.js";
-
-export const tag_job_input = (input: RadrootsJobInput): NostrEventTag => {
- const t: NostrEventTag = ["i", input.data, input.input_type];
- if (input.relay) t.push(input.relay);
- if (input.marker) t.push(input.marker);
- return t;
-};
-
-export const tag_job_output = (mime: string): NostrEventTag => ["output", mime];
-
-export const tag_job_param = (key: string, value: string): NostrEventTag => ["param", key, value];
-
-export const tag_job_bid = (sat: number): NostrEventTag => ["bid", String(sat)];
-
-export const tags_job_relays = (relays: string[]): NostrEventTags =>
- relays.map(r => ["relays", r]);
-
-export const tags_job_providers = (pubkeys: string[]): NostrEventTags =>
- pubkeys.map(p => ["p", p]);
-
-export const tags_job_topics = (topics: string[]): NostrEventTags =>
- topics.map(t => ["t", t]);
-
-export const tag_job_amount = (msat: number, bolt11?: string | null): NostrEventTag =>
- bolt11 ? ["amount", String(msat), bolt11] : ["amount", String(msat)];
-
-export const tag_job_encrypted = (): NostrEventTag => ["encrypted"];
-
-export const tags_job_request = (opts: RadrootsJobRequest): NostrEventTags => {
- const tags: NostrEventTags = [];
- for (const input of opts.inputs) tags.push(tag_job_input(input));
- if (opts.output) tags.push(tag_job_output(opts.output));
- if (opts.params) for (const p of opts.params) tags.push(tag_job_param(p.key, p.value));
- if (typeof opts.bid_sat === "number") tags.push(tag_job_bid(opts.bid_sat));
- if (opts.relays?.length) tags.push(...tags_job_relays(opts.relays));
- if (opts.providers?.length) tags.push(...tags_job_providers(opts.providers));
- if (opts.topics?.length) tags.push(...tags_job_topics(opts.topics));
- if (opts.encrypted) tags.push(tag_job_encrypted());
- return tags;
-};
-
-export const tags_job_result = (opts: RadrootsJobResult): NostrEventTags => {
- const tags: NostrEventTags = [];
- const event_tag: NostrEventTag = ["e", opts.request_event.id];
- if (opts.request_event.relays) event_tag.push(opts.request_event.relays);
- tags.push(event_tag);
- if (opts.request_json) tags.push(["request", opts.request_json]);
- if (!opts.encrypted && opts.inputs?.length) for (const input of opts.inputs) tags.push(tag_job_input(input));
- if (opts.customer_pubkey) tags.push(["p", opts.customer_pubkey]);
- if (opts.payment?.amount_sat !== undefined) {
- const msat = Math.round(Number(opts.payment.amount_sat) * 1000);
- tags.push(tag_job_amount(msat, opts.payment.bolt11));
- }
- if (opts.encrypted) tags.push(tag_job_encrypted());
- return tags;
-};
-
-export const tags_job_feedback = (opts: RadrootsJobFeedback): NostrEventTags => {
- const tags: NostrEventTags = [];
- const status_tag: NostrEventTag = ["status", String(opts.status)];
- if (opts.extra_info) status_tag.push(opts.extra_info);
- tags.push(status_tag);
- if (opts.payment?.amount_sat !== undefined) {
- const msat = Math.round(Number(opts.payment.amount_sat) * 1000);
- tags.push(tag_job_amount(msat, opts.payment.bolt11));
- }
- const event_tag: NostrEventTag = ["e", opts.request_event.id];
- if (opts.request_event.relays) event_tag.push(opts.request_event.relays);
- tags.push(event_tag);
- if (opts.customer_pubkey) tags.push(["p", opts.customer_pubkey]);
- if (opts.encrypted) tags.push(tag_job_encrypted());
- return tags;
-};
diff --git a/utils-nostr/src/events/job/utils.ts b/utils-nostr/src/events/job/utils.ts
@@ -1,34 +0,0 @@
-import { JobInputType, KIND_JOB_FEEDBACK } from "@radroots/events-bindings";
-import {
- REQUEST_KINDS,
- RESULT_KINDS,
- TRADE_LISTING_STAGE,
- TRADE_LISTING_STAGE_KINDS,
-} from "../../domain/trade/lib.js";
-import type { TradeListingStageKind } from "../../domain/trade/lib.js";
-import type { NostrEventTags } from "../../types/lib.js";
-
-export function get_job_input_data_for_marker(
- tags: NostrEventTags,
- marker: string,
- input_type: JobInputType = "event"
-): string | undefined {
- for (const t of tags) {
- if (t[0] !== "i") continue;
- if (t[2] !== input_type) continue;
- const tag_marker = t.length >= 5 ? t[4] : t.length >= 4 ? t[3] : undefined;
- if (tag_marker === marker) return t[1];
- }
- return undefined;
-}
-
-export function get_trade_listing_stage_from_event_kind(
- kind: number
-): TradeListingStageKind | undefined {
- for (const stage_kind of TRADE_LISTING_STAGE_KINDS) {
- if (REQUEST_KINDS[stage_kind] === kind) return stage_kind;
- if (RESULT_KINDS[stage_kind] === kind) return stage_kind;
- }
- if (kind === KIND_JOB_FEEDBACK) return TRADE_LISTING_STAGE.Order;
- return undefined;
-}
diff --git a/utils-nostr/src/events/lib.ts b/utils-nostr/src/events/lib.ts
@@ -1,88 +0,0 @@
-import { schnorr } from "@noble/curves/secp256k1";
-import { hexToBytes } from "@noble/hashes/utils";
-import { NDKEvent, NDKTag } from "@nostr-dev-kit/ndk";
-import { time_now_ms, time_now_s, uuidv4 } from "@radroots/utils";
-import { finalizeEvent, getEventHash, nip19, type NostrEvent } from "nostr-tools";
-import { ILibNostrEventSign, ILibNostrNeventEncode, NostrEventTags } from "../types/lib.js";
-import { NDKEventFigure } from "../types/ndk.js";
-import { tag_client } from "../utils/tags.js";
-import { NdkEventBasis } from "./subscription.js";
-
-export const get_event_tag = (tags: NDKTag[], key: string): string => tags.find(t => t[0] === key)?.[1] ?? '';
-export const get_event_tags = (tags: NDKTag[], key: string): NDKTag[] => tags.filter(t => t[0] === key);
-
-export const parse_nostr_event_basis = <T extends number>(event: NDKEvent, kind: T): NdkEventBasis<T> | undefined => {
- if (!event || typeof event.created_at !== 'number' || event.kind !== kind) return undefined;
- return { id: event.id, published_at: event.created_at, author: event.pubkey, kind: event.kind as T };
-};
-
-export const lib_nostr_event_verify = (event: NostrEvent): 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: ILibNostrEventSign): NostrEvent => {
- return finalizeEvent(opts.event, hexToBytes(opts.secret_key))
-};
-
-export const lib_nostr_event_sign_attest = (secret_key: string): NostrEvent => {
- return lib_nostr_event_sign({
- secret_key,
- event: {
- kind: 1,
- created_at: time_now_s(),
- 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: ILibNostrNeventEncode): string => {
- return nip19.neventEncode(opts);
-};
-
-export const ndk_event = async (opts: NDKEventFigure<{
- basis: {
- kind: number;
- content: string;
- tags?: NostrEventTags;
- }
-}>): Promise<NDKEvent | undefined> => {
- try {
- const { ndk: ndk, ndk_user: ndk_user, basis } = opts;
- const time_now = time_now_ms();
- const published_at = opts.date_published ? Math.floor(opts.date_published.getTime() / 1000).toString()
- : time_now.toString()
- const tags: NostrEventTags = [
- ['published_at', published_at],
- ];
- if (basis.tags?.length) tags.push(...basis.tags);
- if (opts.client) tags.push(tag_client(opts.client, tags.find(i => i[0] === `d_tag`)?.[1] || undefined));
- const ev = new NDKEvent(ndk, {
- kind: basis.kind,
- pubkey: ndk_user.pubkey,
- content: basis.content,
- created_at: time_now,
- tags
- });
- return ev;
- } catch (e) {
- console.log(`(error) ndk_event `, e);
- };
-};
diff --git a/utils-nostr/src/events/listing/lib.ts b/utils-nostr/src/events/listing/lib.ts
@@ -1,21 +0,0 @@
-import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
-import { type RadrootsListing } from "@radroots/events-bindings";
-import { NDKEventFigure } from "../../types/ndk.js";
-import { ndk_event } from "../lib.js";
-import { tags_listing } from "./tags.js";
-
-export const KIND_RADROOTS_LISTING = 30402;
-export type KindRadrootsListing = typeof KIND_RADROOTS_LISTING;
-
-export const ndk_event_classified = async (opts: NDKEventFigure<{ data: RadrootsListing; }>): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data } = opts;
- return await ndk_event({
- ndk,
- ndk_user,
- basis: {
- kind: NDKKind.Classified,
- content: ``,
- tags: tags_listing(data),
- },
- });
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/events/listing/parse.ts b/utils-nostr/src/events/listing/parse.ts
@@ -1,190 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import type { RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice } from "@radroots/core-bindings";
-import { type RadrootsListing, type RadrootsListingDiscount, radroots_listing_discount_schema, radroots_listing_image_schema, radroots_listing_location_schema, radroots_listing_price_schema, radroots_listing_product_schema, radroots_listing_quantity_schema, radroots_listing_schema } from "@radroots/events-bindings";
-import { get_event_tag, get_event_tags, parse_nostr_event_basis } from "../lib.js";
-import { NdkEventBasis } from "../subscription.js";
-import { KIND_RADROOTS_LISTING, type KindRadrootsListing } from "./lib.js";
-
-export type RadrootsListingNostrEvent = NdkEventBasis<KindRadrootsListing> & { listing: RadrootsListing; };
-
-type CoreUnit = RadrootsCoreQuantity["unit"];
-type CoreCurrency = RadrootsCoreMoney["currency"];
-
-const to_number = (value?: string): number | undefined => {
- if (value === undefined) return undefined;
- const num = Number(value);
- return Number.isFinite(num) ? num : undefined;
-};
-
-const clean_string = (value?: string | null) => value?.trim() || undefined;
-
-const parse_currency_code = (code?: string): CoreCurrency | undefined => {
- const cleaned = clean_string(code);
- if (!cleaned || cleaned.length < 3) return undefined;
- const upper = cleaned.toUpperCase();
- return [upper.charCodeAt(0), upper.charCodeAt(1), upper.charCodeAt(2)] as CoreCurrency;
-};
-
-const parse_unit_code = (unit?: string): CoreUnit | undefined => {
- switch ((unit ?? "").trim().toLowerCase()) {
- case "each":
- case "ea":
- case "count":
- return "Each" as CoreUnit;
- case "kg":
- case "kilogram":
- case "kilograms":
- return "MassKg" as CoreUnit;
- case "g":
- case "gram":
- case "grams":
- return "MassG" as CoreUnit;
- case "oz":
- case "ounce":
- case "ounces":
- return "MassOz" as CoreUnit;
- case "lb":
- case "pound":
- case "pounds":
- return "MassLb" as CoreUnit;
- case "l":
- case "liter":
- case "litre":
- case "liters":
- case "litres":
- return "VolumeL" as CoreUnit;
- case "ml":
- case "milliliter":
- case "millilitre":
- case "milliliters":
- case "millilitres":
- return "VolumeMl" as CoreUnit;
- default:
- return undefined;
- }
-};
-
-const parse_quantity_tag = (tag: string[]) => {
- if (tag.length < 3) return undefined;
- const amount = to_number(tag[1]);
- const unit = parse_unit_code(tag[2]);
- if (amount === undefined || !unit) return undefined;
- const label = clean_string(tag[3]);
- const count = to_number(tag[4]);
- const value: RadrootsCoreQuantity = label ? { amount, unit, label } : { amount, unit };
- return radroots_listing_quantity_schema.parse({ value, label, count });
-};
-
-const parse_price_tag = (tag: string[]) => {
- if (tag.length < 5) return undefined;
- const amount = to_number(tag[1]);
- const currency = parse_currency_code(tag[2]);
- const quantity_amount = to_number(tag[3]);
- const quantity_unit = parse_unit_code(tag[4]);
- if (amount === undefined || !currency || quantity_amount === undefined || !quantity_unit) return undefined;
- const label = clean_string(tag[5]);
- const money: RadrootsCoreMoney = { amount, currency };
- const quantity: RadrootsCoreQuantity = label ? { amount: quantity_amount, unit: quantity_unit, label } : { amount: quantity_amount, unit: quantity_unit };
- return radroots_listing_price_schema.parse({ amount: money, quantity });
-};
-
-const parse_discount_tag = (tag: string[]) => {
- const prefix = "price-discount-";
- if (!tag[0]?.startsWith(prefix) || tag.length < 2) return undefined;
- const kind = tag[0].slice(prefix.length) as RadrootsListingDiscount["kind"];
- try {
- const amount = JSON.parse(tag[1]);
- return radroots_listing_discount_schema.parse({ kind, amount });
- } catch {
- return undefined;
- }
-};
-
-const parse_image_tag = (tag: string[]) => {
- if (tag[0] !== "image" || !tag[1]) return undefined;
- const image: any = { url: tag[1] };
- if (tag[2]) {
- const [w, h] = tag[2].split("x").map(v => Number(v));
- if (Number.isFinite(w) && Number.isFinite(h)) image.size = { w, h };
- }
- return radroots_listing_image_schema.parse(image);
-};
-
-export const parse_nostr_listing_event = (event: NDKEvent): RadrootsListingNostrEvent | undefined => {
- const ev = parse_nostr_event_basis(event, KIND_RADROOTS_LISTING);
- if (!ev) return undefined;
- try {
- const tags = event.tags;
-
- const d_tag = get_event_tag(tags, "d");
-
- const product_raw = {
- key: get_event_tag(tags, "key"),
- title: get_event_tag(tags, "title"),
- category: get_event_tag(tags, "category"),
- summary: get_event_tag(tags, "summary"),
- process: get_event_tag(tags, "process"),
- lot: get_event_tag(tags, "lot"),
- location: get_event_tag(tags, "location"),
- profile: get_event_tag(tags, "profile"),
- year: get_event_tag(tags, "year")
- };
-
- const product = radroots_listing_product_schema.parse(product_raw);
-
- const quantities = get_event_tags(tags, "quantity")
- .map(parse_quantity_tag)
- .filter(Boolean) as RadrootsListing["quantities"];
-
- const prices = get_event_tags(tags, "price")
- .map(parse_price_tag)
- .filter(Boolean) as RadrootsCoreQuantityPrice[];
-
- const discounts = tags
- .filter(t => t[0]?.startsWith("price-discount-"))
- .map(parse_discount_tag)
- .filter(Boolean);
-
- const images = get_event_tags(tags, "image")
- .map(parse_image_tag)
- .filter(Boolean);
-
- const location_parts = get_event_tags(tags, "location")[0]?.slice(1) ?? [];
-
- const location_raw: any = {};
- if (location_parts[0]) location_raw.primary = location_parts[0];
- if (location_parts[1]) location_raw.city = location_parts[1];
- if (location_parts[2]) location_raw.region = location_parts[2];
- if (location_parts[3]) location_raw.country = location_parts[3];
-
- if (location_raw.primary) {
- const geohash = get_event_tags(tags, "g")[0]?.[1];
- if (geohash) location_raw.geohash = geohash;
-
- for (const loc_tag of get_event_tags(tags, "l")) {
- if (loc_tag.length < 3) continue;
- const coord = Number(loc_tag[1]);
- if (!Number.isFinite(coord)) continue;
- if (loc_tag[2] === "dd.lat") location_raw.lat = coord;
- if (loc_tag[2] === "dd.lon") location_raw.lng = coord;
- }
- }
-
- const location = location_raw.primary
- ? radroots_listing_location_schema.parse(location_raw)
- : undefined;
-
- const listing = radroots_listing_schema.parse({
- d_tag,
- product,
- quantities,
- prices,
- discounts: discounts.length ? discounts : undefined,
- location,
- images: images.length ? images : undefined
- });
- return { ...ev, listing };
- } catch {
- return undefined;
- }
-};
diff --git a/utils-nostr/src/events/listing/tags.ts b/utils-nostr/src/events/listing/tags.ts
@@ -1,118 +0,0 @@
-import type { RadrootsCoreQuantityPrice } from "@radroots/core-bindings";
-import type { RadrootsListing, RadrootsListingDiscount, RadrootsListingImage, RadrootsListingLocation, RadrootsListingQuantity } from "@radroots/events-bindings";
-import ngeotags, { type InputData as NostrGeotagsInputData } from "nostr-geotags";
-import { NostrEventTag, NostrEventTagLocation, NostrEventTags } from "../../types/lib.js";
-
-type CoreUnit = RadrootsListingQuantity["value"]["unit"];
-type CoreCurrency = RadrootsCoreQuantityPrice["amount"]["currency"];
-
-const currency_to_code = (currency: CoreCurrency): string => {
- if (Array.isArray(currency) && currency.length >= 3) {
- const [a, b, c] = currency;
- return String.fromCharCode(Number(a), Number(b), Number(c));
- }
- return String(currency);
-};
-
-const unit_to_code = (unit: CoreUnit): string => {
- switch (unit) {
- case "Each": return "each";
- case "MassKg": return "kg";
- case "MassG": return "g";
- case "MassOz": return "oz";
- case "MassLb": return "lb";
- case "VolumeL": return "l";
- case "VolumeMl": return "ml";
- default: return String(unit).toLowerCase();
- }
-};
-
-const clean_label = (value?: string | null) => value?.trim() || undefined;
-
-const normalize_listing_location = (location?: RadrootsListingLocation | null): NostrEventTagLocation | undefined => {
- if (!location?.primary) return undefined;
- const { primary, city, region, country, lat, lng } = location;
- return {
- primary,
- city: city ?? undefined,
- region: region ?? undefined,
- country: country ?? undefined,
- lat: typeof lat === "number" ? lat : undefined,
- lng: typeof lng === "number" ? lng : undefined,
- };
-};
-
-const normalize_image_size = (size: RadrootsListingImage["size"]) =>
- size && typeof size?.w === "number" && typeof size?.h === "number" ? size : undefined;
-
-export const tag_listing_quantity = (opts: RadrootsListingQuantity): NostrEventTag => {
- const tag: NostrEventTag = ["quantity", String(opts.value.amount), unit_to_code(opts.value.unit)];
- const label = clean_label(opts.label ?? opts.value.label);
- if (label) tag.push(label);
- if (opts.count !== undefined && opts.count !== null) tag.push(String(opts.count));
- return tag;
-};
-
-export const tag_listing_price = (price: RadrootsCoreQuantityPrice): NostrEventTag => {
- const tag: NostrEventTag = [
- "price",
- String(price.amount.amount),
- currency_to_code(price.amount.currency).toLowerCase(),
- String(price.quantity.amount),
- unit_to_code(price.quantity.unit),
- ];
- const label = clean_label(price.quantity.label);
- if (label) tag.push(label);
- return tag;
-};
-
-export const tag_listing_price_discount = (discount: RadrootsListingDiscount): NostrEventTag => {
- const tag: NostrEventTag = [`price-discount-${discount.kind}`];
- tag.push(JSON.stringify(discount.amount));
- return tag;
-};
-
-export const tag_listing_location = (opts: NostrEventTagLocation): NostrEventTag => {
- if (!opts.primary) return [];
- const tag: NostrEventTag = ["location", opts.primary];
- if (opts.city) tag.push(opts.city);
- if (opts.region) tag.push(opts.region);
- if (opts.country) tag.push(opts.country);
- return tag;
-};
-
-export const tags_listing_location_geotags = (opts: NostrEventTagLocation): NostrEventTags => {
- const { lat, lng: lon, city, region, country } = opts;
- const country_raw = country || ``;
- const country_code = country_raw && country_raw?.length <= 3 ? country_raw : undefined;
- const country_name = country_raw && country_raw?.length > 3 ? country_raw : undefined;
- const input: NostrGeotagsInputData = { lat, lon, city, regionName: region, countryCode: country_code, countryName: country_name };
- return ngeotags(input, { geohash: true, gps: true, city: true, iso31662: true });
-};
-
-export const tag_listing_image = (opts: RadrootsListingImage): NostrEventTag => {
- const tag: NostrEventTag = ["image", opts.url];
- const size = normalize_image_size(opts.size);
- if (size) tag.push(`${size.w}x${size.h}`);
- return tag;
-};
-
-export const tags_listing = (opts: RadrootsListing): NostrEventTags => {
- const { d_tag, product, quantities, prices } = opts;
- const tags: NostrEventTags = [["d", d_tag]];
- for (const [k, v] of Object.entries(product)) if (v) tags.push([k, String(v)]);
- for (const quantity of quantities) {
- tags.push(tag_listing_quantity(quantity));
- }
- for (const price of prices) {
- tags.push(tag_listing_price(price));
- }
- if (opts.discounts?.length) for (const discount of opts.discounts) if (discount) tags.push(tag_listing_price_discount(discount));
- const location = normalize_listing_location(opts.location);
- if (location) {
- tags.push(tag_listing_location(location));
- tags.push(...tags_listing_location_geotags(location));
- }
- if (opts.images) for (const image_tags of opts.images) if (image_tags) tags.push(tag_listing_image(image_tags));
- return tags;
-};
diff --git a/utils-nostr/src/events/profile/lib.ts b/utils-nostr/src/events/profile/lib.ts
@@ -1,19 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { type RadrootsProfile } from "@radroots/events-bindings";
-import { NDKEventFigure } from "../../types/ndk.js";
-import { ndk_event } from "../lib.js";
-
-export const KIND_RADROOTS_PROFILE = 0;
-export type KindRadrootsProfile = typeof KIND_RADROOTS_PROFILE;
-
-export const ndk_event_profile = async (opts: NDKEventFigure<{ data: RadrootsProfile }>): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data } = opts;
- return await ndk_event({
- ndk,
- ndk_user,
- basis: {
- kind: 0,
- content: JSON.stringify(data),
- },
- });
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/events/profile/parse.ts b/utils-nostr/src/events/profile/parse.ts
@@ -1,19 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { type RadrootsProfile, radroots_profile_schema } from "@radroots/events-bindings";
-import { parse_nostr_event_basis } from "../lib.js";
-import { NdkEventBasis } from "../subscription.js";
-import { KIND_RADROOTS_PROFILE, type KindRadrootsProfile } from "./lib.js";
-
-export type RadrootsProfileNostrEvent = NdkEventBasis<KindRadrootsProfile> & { profile: RadrootsProfile; }
-
-export const parse_nostr_profile_event = (event: NDKEvent): RadrootsProfileNostrEvent | undefined => {
- const ev = parse_nostr_event_basis(event, KIND_RADROOTS_PROFILE);
- if (!ev) return undefined;
- try {
- const parsed = JSON.parse(event.content);
- const profile = radroots_profile_schema.parse(parsed);
- return { ...ev, profile };
- } catch {
- return undefined;
- }
-};
diff --git a/utils-nostr/src/events/reaction/lib.ts b/utils-nostr/src/events/reaction/lib.ts
@@ -1,21 +0,0 @@
-import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
-import { RadrootsReaction } from "@radroots/events-bindings";
-import { NDKEventFigure } from "../../types/ndk.js";
-import { ndk_event } from "../lib.js";
-import { tags_reaction } from "./tags.js";
-
-export const KIND_RADROOTS_REACTION = 7;
-export type KindRadrootsReaction = typeof KIND_RADROOTS_REACTION;
-
-export const ndk_event_reaction = async (opts: NDKEventFigure<{ data: RadrootsReaction; }>): Promise<NDKEvent | undefined> => {
- const { ndk, ndk_user, data } = opts;
- return await ndk_event({
- ndk,
- ndk_user,
- basis: {
- kind: NDKKind.Reaction,
- content: data.content,
- tags: tags_reaction(data)
- },
- });
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/events/reaction/parse.ts b/utils-nostr/src/events/reaction/parse.ts
@@ -1,19 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { RadrootsReaction, radroots_reaction_schema } from "@radroots/events-bindings";
-import { parse_nostr_event_basis } from "../lib.js";
-import { NdkEventBasis } from "../subscription.js";
-import { KIND_RADROOTS_REACTION, type KindRadrootsReaction } from "./lib.js";
-
-export type RadrootsReactionNostrEvent = NdkEventBasis<KindRadrootsReaction> & { reaction: RadrootsReaction; }
-
-export const parse_nostr_reaction_event = (event: NDKEvent): RadrootsReactionNostrEvent | undefined => {
- const ev = parse_nostr_event_basis(event, KIND_RADROOTS_REACTION);
- if (!ev) return undefined;
- try {
- const parsed = JSON.parse(event.content);
- const reaction = radroots_reaction_schema.parse(parsed);
- return { ...ev, reaction };
- } catch {
- return undefined;
- }
-};
diff --git a/utils-nostr/src/events/reaction/tags.ts b/utils-nostr/src/events/reaction/tags.ts
@@ -1,15 +0,0 @@
-import { RadrootsReaction } from "@radroots/events-bindings";
-import { NostrEventTags } from "../../types/lib.js";
-
-export const tags_reaction = (opts: RadrootsReaction): NostrEventTags => {
- const { root } = opts;
- const ref_kind = root.kind.toString();
- const ref_author = root.author;
- const tags: NostrEventTags = [
- [`e`, root.id, ...root.relays || ``],
- [`p`, ref_author],
- [`k`, ref_kind],
- ];
- if (root.d_tag) tags.push([`a`, `${ref_kind}:${ref_author}:${root.d_tag}`, ...root.relays || ``])
- return tags;
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/events/subscription.ts b/utils-nostr/src/events/subscription.ts
@@ -1,36 +0,0 @@
-import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { parse_nostr_comment_event, RadrootsCommentNostrEvent } from "./comment/parse.js";
-import { parse_nostr_follow_event, RadrootsFollowNostrEvent } from "./follow/parse.js";
-import { parse_nostr_listing_event, RadrootsListingNostrEvent } from "./listing/parse.js";
-import { parse_nostr_profile_event, RadrootsProfileNostrEvent } from "./profile/parse.js";
-import { parse_nostr_reaction_event, RadrootsReactionNostrEvent } from "./reaction/parse.js";
-
-export type NdkEventBasis<T extends number> = {
- id: string;
- published_at: number;
- author: string;
- kind: T;
-};
-
-export type NdkEventPayload =
- | RadrootsProfileNostrEvent
- | RadrootsListingNostrEvent
- | RadrootsCommentNostrEvent
- | RadrootsReactionNostrEvent
- | RadrootsFollowNostrEvent
-
-export const on_ndk_event = (event: NDKEvent): NdkEventPayload | undefined => {
- if (!event || typeof event.kind !== 'number') return undefined;
-
- switch (event.kind) {
- case 0: return parse_nostr_profile_event(event);
- case 30402: return parse_nostr_listing_event(event);
- case 1111: return parse_nostr_comment_event(event);
- case 7: return parse_nostr_reaction_event(event);
- case 3: return parse_nostr_follow_event(event);
-
- default: return undefined;
- }
-};
-
-
diff --git a/utils-nostr/src/index.ts b/utils-nostr/src/index.ts
@@ -1,38 +0,0 @@
-export * from "./domain/trade/lib.js"
-export * from "./domain/trade/listing/accept/lib.js"
-export * from "./domain/trade/listing/conveyance/lib.js"
-export * from "./domain/trade/listing/fulfillment/lib.js"
-export * from "./domain/trade/listing/invoice/lib.js"
-export * from "./domain/trade/listing/order/lib.js"
-export * from "./domain/trade/listing/payment/lib.js"
-export * from "./domain/trade/listing/receipt/lib.js"
-export * from "./domain/trade/listing/tags.js"
-export * from "./domain/trade/tags.js"
-export * from "./events/comment/lib.js"
-export * from "./events/comment/parse.js"
-export * from "./events/comment/tags.js"
-export * from "./events/follow/lib.js"
-export * from "./events/follow/parse.js"
-export * from "./events/follow/tags.js"
-export * from "./events/job/lib.js"
-export * from "./events/job/tags.js"
-export * from "./events/job/utils.js"
-export * from "./events/lib.js"
-export * from "./events/listing/lib.js"
-export * from "./events/listing/parse.js"
-export * from "./events/listing/tags.js"
-export * from "./events/profile/lib.js"
-export * from "./events/profile/parse.js"
-export * from "./events/reaction/lib.js"
-export * from "./events/reaction/parse.js"
-export * from "./events/reaction/tags.js"
-export * from "./events/subscription.js"
-export * from "./keys/lib.js"
-export * from "./relay/lib.js"
-export * from "./schemas/lib.js"
-export * from "./types/lib.js"
-export * from "./types/ndk.js"
-export * from "./utils/ndk.js"
-export * from "./utils/relays.js"
-export * from "./utils/tags.js"
-
diff --git a/utils-nostr/src/keys/lib.ts b/utils-nostr/src/keys/lib.ts
@@ -1,94 +0,0 @@
-import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
-import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
-import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools";
-
-export const regex_nostr_key = /^[a-f0-9]{64}$/;
-
-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_npub_encode = (public_key_hex?: string): string | undefined => {
- try {
- if (!public_key_hex) return undefined;
- const npub = nip19.npubEncode(public_key_hex)
- return npub;
- } catch {
- return undefined;
- }
-};
-
-export const lib_nostr_npub_decode = (npub?: string): string | undefined => {
- try {
- if (!npub) return undefined;
- const { type, data } = nip19.decode(npub);
- if (type === `npub` && data) return data;
- } catch {
- return undefined;
- }
-};
-
-export const lib_nostr_nsec_encode = (secret_key_hex?: string): string | undefined => {
- try {
- if (!secret_key_hex) return undefined;
- const bytes = lib_nostr_get_key_bytes(secret_key_hex);
- return nip19.nsecEncode(bytes);
- } catch {
- return undefined;
- }
-};
-
-export const lib_nostr_nsec_decode = (nsec?: string): string | undefined => {
- try {
- if (!nsec) return undefined;
- const decode = nip19.decode(nsec);
- if (decode && decode.type === `nsec` && decode.data) return bytesToHex(decode.data);
- return undefined;
- } catch {
- return undefined;
- }
-};
-
-export const lib_nostr_nprofile_encode = (public_key_hex: string, relays: string[]): string | undefined => {
- try {
- if (!public_key_hex || !relays.length) return undefined;
- const nprofile = nip19.nprofileEncode({ pubkey: public_key_hex, relays })
- return nprofile;
- } catch {
- return undefined;
- }
-
-};
-
-export const lib_nostr_nprofile_decode = (nprofile?: string): nip19.ProfilePointer | undefined => {
- try {
- if (!nprofile) return undefined;
- const { type, data } = nip19.decode(nprofile);
- if (type === `nprofile` && data) return data;
- } catch {
- 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 => {
- try {
- const signer = new NDKPrivateKeySigner(secret_key);
- return signer.privateKey;
- } catch {
- return undefined;
- }
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/relay/lib.ts b/utils-nostr/src/relay/lib.ts
@@ -1,32 +0,0 @@
-import { NostrRelayInformationDocument, NostrRelayInformationDocumentFields } from "../types/lib.js";
-
-export const lib_nostr_relay_parse_information_document = (data: any): NostrRelayInformationDocument | undefined => {
- const obj = typeof data === `string` ? JSON.parse(data) : data;
- return {
- id: typeof obj.id === 'string' ? obj.id : undefined,
- name: typeof obj.name === 'string' ? obj.name : undefined,
- description: typeof obj.description === 'string' ? obj.description : undefined,
- pubkey: typeof obj.pubkey === 'string' ? obj.pubkey : undefined,
- contact: typeof obj.contact === 'string' ? obj.contact : undefined,
- supported_nips: Array.isArray(obj.supported_nips) && obj.supported_nips.every((nip: any) => typeof nip === 'number')
- ? obj.supported_nips
- : undefined,
- software: typeof obj.software === 'string' ? obj.software : undefined,
- version: typeof obj.version === 'string' ? obj.version : undefined,
- limitation_payment_required: obj.limitation && typeof obj.limitation === 'object' && typeof obj.limitation.payment_required === 'string' ? obj.limitation.payment_required : undefined,
- limitation_restricted_writes: obj.limitation && typeof obj.limitation === 'object' && typeof obj.limitation.restricted_writes === 'boolean' ? obj.limitation.restricted_writes : undefined,
- };
-};
-
-export const lib_nostr_relay_build_information_document = (data: any): NostrRelayInformationDocumentFields | undefined => {
- const doc = lib_nostr_relay_parse_information_document(data);
- if (!doc) return;
- const result: Partial<NostrRelayInformationDocumentFields> = {};
- Object.entries(doc).forEach(([key, value]) => {
- if (typeof value === 'boolean') result[key as keyof NostrRelayInformationDocument] = value ? '1' : '0';
- else if (Array.isArray(value)) result[key as keyof NostrRelayInformationDocument] = value.join(', ');
- else if (value === null || value === undefined) result[key as keyof NostrRelayInformationDocument] = '';
- else result[key as keyof NostrRelayInformationDocument] = String(value);
- });
- return result;
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/schemas/lib.ts b/utils-nostr/src/schemas/lib.ts
@@ -1,7 +0,0 @@
-import { z } from 'zod';
-
-export const nostr_tag_client_schema = z.object({
- name: z.string(),
- pubkey: z.string(),
- relay: z.string()
-});
-\ No newline at end of file
diff --git a/utils-nostr/src/types/lib.ts b/utils-nostr/src/types/lib.ts
@@ -1,94 +0,0 @@
-import { type EventTemplate } from "nostr-tools";
-import { z } from 'zod';
-import { nostr_tag_client_schema } from "../schemas/lib.js";
-
-export type NostrTagClient = z.infer<typeof nostr_tag_client_schema>;
-export type NostrEventTag = string[];
-export type NostrEventTags = NostrEventTag[];
-
-export type NostrEventTagClient = {
- name: string;
- pubkey: string;
- relay: string;
-};
-
-/*
-export type NostrEventTagPriceDiscount = (
- {
- quantity: {
- ref_quantity: string;
- threshold: string;
- value: string;
- currency: string;
- }
- } |
- {
- mass: {
- unit: string;
- threshold: string;
- threshold_unit: string;
- value: string;
- currency: string;
- }
- } |
- {
- subtotal: {
- threshold: string;
- currency: string;
- value: string;
- measure: string;
- }
- } |
- {
- total: {
- total_min: string;
- value: string;
- measure: string;
- }
- }
-);
-*/
-
-export type NostrEventTagLocation = {
- primary: string;
- city?: string;
- region?: string;
- country?: string;
- lat?: number;
- lng?: number;
-};
-
-export type NostrEventTagImage = {
- url: string;
- size?: {
- w: number;
- h: number;
- };
-};
-
-export type NostrRelayInformationDocument = {
- id?: string;
- name?: string;
- description?: string;
- pubkey?: string;
- contact?: string;
- supported_nips?: number[];
- software?: string;
- version?: string;
- limitation_payment_required?: string;
- limitation_restricted_writes?: boolean;
-}
-
-export type NostrRelayInformationDocumentFields = { [K in keyof NostrRelayInformationDocument]: string; };
-
-export type ILibNostrNeventEncode = {
- id: string;
- relays: string[];
- author: string;
- kind: number;
-};
-
-export type ILibNostrEventSign = {
- secret_key: string;
- event: EventTemplate;
-};
diff --git a/utils-nostr/src/types/ndk.ts b/utils-nostr/src/types/ndk.ts
@@ -1,10 +0,0 @@
-import NDK, { NDKUser } from "@nostr-dev-kit/ndk";
-import { type NostrEventTagClient } from "./lib.js";
-
-export type NDKEventFigure<T extends object> = {
- ndk: NDK;
- ndk_user: NDKUser;
- date_published?: Date;
- client?: NostrEventTagClient;
-} & T;
-
diff --git a/utils-nostr/src/utils/ndk.ts b/utils-nostr/src/utils/ndk.ts
@@ -1,13 +0,0 @@
-import NDK, { NDKCacheAdapter, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
-
-export const create_ndk = (explicitRelayUrls: string[], cacheAdapter?: NDKCacheAdapter): NDK => {
- return new NDK({
- explicitRelayUrls,
- enableOutboxModel: false,
- cacheAdapter
- });
-};
-
-export const create_ndk_signer = (secret_key: string): NDKPrivateKeySigner => {
- return new NDKPrivateKeySigner(secret_key);
-};
-\ No newline at end of file
diff --git a/utils-nostr/src/utils/relays.ts b/utils-nostr/src/utils/relays.ts
@@ -1,22 +0,0 @@
-import type { RadrootsRelayDocument } from "@radroots/events-bindings";
-
-const nostr_relay_form_field_record: Record<keyof RadrootsRelayDocument, true> = {
- name: true,
- description: true,
- pubkey: true,
- contact: true,
- supported_nips: true,
- software: true,
- version: true
-};
-
-const is_nostr_relay_form_field = (value: string): value is keyof RadrootsRelayDocument => {
- return value in nostr_relay_form_field_record;
-};
-
-export const nostr_relay_parse_form_keys = (value: string): keyof RadrootsRelayDocument | "" => {
- if (is_nostr_relay_form_field(value)) {
- return value;
- }
- return "";
-};
diff --git a/utils-nostr/src/utils/tags.ts b/utils-nostr/src/utils/tags.ts
@@ -1,11 +0,0 @@
-import { NostrEventTag, NostrEventTagClient } from "../types/lib.js";
-
-export const TAG_E = 'e';
-export const TAG_I = 'i';
-
-export const tag_client = (opts: NostrEventTagClient, d_tag?: string): NostrEventTag => {
- const tag = [`client`, opts.name];
- if (d_tag) tag.push(`31990:${opts.pubkey}:${d_tag}`);
- tag.push(opts.relay);
- return tag;
-};
diff --git a/utils-nostr/tsconfig.cjs.json b/utils-nostr/tsconfig.cjs.json
@@ -1,15 +0,0 @@
-{
- "extends": "@radroots/tsconfig/tsconfig.esm.json",
- "compilerOptions": {
- "module": "CommonJS",
- "moduleResolution": "Node",
- "rootDir": "./src",
- "outDir": "dist/cjs",
- "declaration": false,
- "declarationMap": false,
- "emitDeclarationOnly": false,
- "tsBuildInfoFile": "node_modules/.cache/tsc.utils-nostr.cjs.tsbuildinfo"
- },
- "include": ["src"],
- "exclude": ["node_modules", "dist"]
-}