web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

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:
Mapps-lib-market/package.json | 4+---
Mapps-lib-pwa/package.json | 4----
Mapps-lib/.env.example | 3+--
Mapps-lib/package.json | 4----
Mapps-lib/src/lib/index.ts | 4----
Dapps-lib/src/lib/stores/ndk.ts | 18------------------
Dapps-lib/src/lib/types/ndk.ts | 4----
Mapps-lib/src/lib/utils/_env.ts | 8--------
Dapps-lib/src/lib/utils/nostr/lib.ts | 43-------------------------------------------
Dapps-lib/src/lib/utils/nostr/ndk.ts | 13-------------
Rutils-nostr/.gitignore -> apps-nostr/.gitignore | 0
Rutils-nostr/LICENSE -> apps-nostr/LICENSE | 0
Aapps-nostr/package.json | 46++++++++++++++++++++++++++++++++++++++++++++++
Aapps-nostr/src/index.ts | 2++
Aapps-nostr/src/session.ts | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps-nostr/src/stores.ts | 20++++++++++++++++++++
Aapps-nostr/tsconfig.cjs.json | 15+++++++++++++++
Rutils-nostr/tsconfig.esm.json -> apps-nostr/tsconfig.esm.json | 0
Rutils-nostr/tsconfig.json -> apps-nostr/tsconfig.json | 0
Mclient/package.json | 5++---
Mclient/src/cipher/web.ts | 16+++++++++++++---
Mclient/src/crypto/registry.ts | 60++++++++++++++++++++++++++++++++++--------------------------
Mclient/src/crypto/service.ts | 2++
Mclient/src/datastore/web.ts | 70+++++++++++++++++++++++++++++++++++++++++++++-------------------------
Aclient/src/idb/store.ts | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mclient/src/keystore/web-nostr.ts | 12++++++++----
Mclient/src/keystore/web.ts | 34++++++++++++++++++++++------------
Mclient/src/radroots/web.ts | 4++--
Mclient/src/sql/web.ts | 18+++++++++++++-----
Rutils-nostr/.gitignore -> nostr/.gitignore | 0
Rutils-nostr/LICENSE -> nostr/LICENSE | 0
Anostr/package.json | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/domain/trade/lib.ts | 44++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/domain/trade/listing/accept/lib.ts | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/domain/trade/listing/conveyance/lib.ts | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/domain/trade/listing/fulfillment/lib.ts | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/domain/trade/listing/invoice/lib.ts | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/domain/trade/listing/order/lib.ts | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/domain/trade/listing/payment/lib.ts | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/domain/trade/listing/receipt/lib.ts | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rutils-nostr/src/domain/trade/listing/tags.ts -> nostr/src/domain/trade/listing/tags.ts | 0
Anostr/src/domain/trade/tags.ts | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/comment/lib.ts | 21+++++++++++++++++++++
Anostr/src/events/comment/parse.ts | 22++++++++++++++++++++++
Anostr/src/events/comment/tags.ts | 37+++++++++++++++++++++++++++++++++++++
Anostr/src/events/follow/lib.ts | 21+++++++++++++++++++++
Anostr/src/events/follow/parse.ts | 22++++++++++++++++++++++
Anostr/src/events/follow/tags.ts | 11+++++++++++
Anostr/src/events/job/lib.ts | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/job/tags.ts | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/job/utils.ts | 43+++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/lib.ts | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/listing/lib.ts | 22++++++++++++++++++++++
Anostr/src/events/listing/parse.ts | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/listing/tags.ts | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/profile/lib.ts | 19+++++++++++++++++++
Anostr/src/events/profile/parse.ts | 22++++++++++++++++++++++
Anostr/src/events/reaction/lib.ts | 21+++++++++++++++++++++
Anostr/src/events/reaction/parse.ts | 22++++++++++++++++++++++
Anostr/src/events/reaction/tags.ts | 16++++++++++++++++
Anostr/src/events/subscription.ts | 33+++++++++++++++++++++++++++++++++
Anostr/src/index.ts | 38++++++++++++++++++++++++++++++++++++++
Anostr/src/keys.ts | 1+
Anostr/src/keys/lib.ts | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/kinds.ts | 8++++++++
Anostr/src/relay/lib.ts | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/relays.ts | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/repository.ts | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/schemas/lib.ts | 7+++++++
Anostr/src/signers.ts | 38++++++++++++++++++++++++++++++++++++++
Anostr/src/types.ts | 2++
Anostr/src/types/lib.ts | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/types/nostr.ts | 24++++++++++++++++++++++++
Anostr/src/utils/relays.ts | 22++++++++++++++++++++++
Anostr/src/utils/tags.ts | 11+++++++++++
Anostr/tsconfig.cjs.json | 15+++++++++++++++
Rutils-nostr/tsconfig.esm.json -> nostr/tsconfig.esm.json | 0
Rutils-nostr/tsconfig.json -> nostr/tsconfig.json | 0
Dutils-nostr/package.json | 47-----------------------------------------------
Dutils-nostr/src/domain/trade/lib.ts | 52----------------------------------------------------
Dutils-nostr/src/domain/trade/listing/accept/lib.ts | 62--------------------------------------------------------------
Dutils-nostr/src/domain/trade/listing/conveyance/lib.ts | 64----------------------------------------------------------------
Dutils-nostr/src/domain/trade/listing/fulfillment/lib.ts | 61-------------------------------------------------------------
Dutils-nostr/src/domain/trade/listing/invoice/lib.ts | 73-------------------------------------------------------------------------
Dutils-nostr/src/domain/trade/listing/order/lib.ts | 70----------------------------------------------------------------------
Dutils-nostr/src/domain/trade/listing/payment/lib.ts | 63---------------------------------------------------------------
Dutils-nostr/src/domain/trade/listing/receipt/lib.ts | 63---------------------------------------------------------------
Dutils-nostr/src/domain/trade/tags.ts | 88-------------------------------------------------------------------------------
Dutils-nostr/src/events/comment/lib.ts | 22----------------------
Dutils-nostr/src/events/comment/parse.ts | 20--------------------
Dutils-nostr/src/events/comment/tags.ts | 38--------------------------------------
Dutils-nostr/src/events/follow/lib.ts | 22----------------------
Dutils-nostr/src/events/follow/parse.ts | 19-------------------
Dutils-nostr/src/events/follow/tags.ts | 12------------
Dutils-nostr/src/events/job/lib.ts | 96-------------------------------------------------------------------------------
Dutils-nostr/src/events/job/tags.ts | 75---------------------------------------------------------------------------
Dutils-nostr/src/events/job/utils.ts | 34----------------------------------
Dutils-nostr/src/events/lib.ts | 88-------------------------------------------------------------------------------
Dutils-nostr/src/events/listing/lib.ts | 22----------------------
Dutils-nostr/src/events/listing/parse.ts | 190-------------------------------------------------------------------------------
Dutils-nostr/src/events/listing/tags.ts | 118-------------------------------------------------------------------------------
Dutils-nostr/src/events/profile/lib.ts | 20--------------------
Dutils-nostr/src/events/profile/parse.ts | 19-------------------
Dutils-nostr/src/events/reaction/lib.ts | 22----------------------
Dutils-nostr/src/events/reaction/parse.ts | 19-------------------
Dutils-nostr/src/events/reaction/tags.ts | 16----------------
Dutils-nostr/src/events/subscription.ts | 36------------------------------------
Dutils-nostr/src/index.ts | 38--------------------------------------
Dutils-nostr/src/keys/lib.ts | 95-------------------------------------------------------------------------------
Dutils-nostr/src/relay/lib.ts | 33---------------------------------
Dutils-nostr/src/schemas/lib.ts | 8--------
Dutils-nostr/src/types/lib.ts | 94-------------------------------------------------------------------------------
Dutils-nostr/src/types/ndk.ts | 10----------
Dutils-nostr/src/utils/ndk.ts | 14--------------
Dutils-nostr/src/utils/relays.ts | 22----------------------
Dutils-nostr/src/utils/tags.ts | 11-----------
Dutils-nostr/tsconfig.cjs.json | 15---------------
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"] -}