commit 5caf0ab3f2ecae377661a26d7f6ff4f27809b4bf parent 1a654e8ccbb736c2e80f8ad09e91f13a49c0026f Author: triesap <triesap@radroots.dev> Date: Sat, 3 Jan 2026 22:18:45 +0000 deps: migrate to radroots nostr packages - Bump workspace rust-version to 1.88.0 - Replace NDK deps with @radroots/apps-nostr and @radroots/nostr - Propagate FetchJsonResult through indexed listing/profile loaders - Update indexer listing/profile parsing for bins, refs, and availability tags Diffstat:
22 files changed, 496 insertions(+), 235 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock @@ -28,6 +28,17 @@ dependencies = [ ] [[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] name = "ahash" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -530,7 +541,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", "typenum", ] @@ -662,10 +672,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -729,6 +737,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] name = "hex-conservative" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -900,9 +914,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", ] [[package]] @@ -1037,10 +1048,9 @@ dependencies = [ [[package]] name = "nostr" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30e6dcb36d88017587b0b5578d1ed3398afe8e4f45fdb910e48b8675aaf6f68" +version = "0.44.1" dependencies = [ + "aes", "base64 0.22.1", "bech32", "bip39", @@ -1048,8 +1058,10 @@ dependencies = [ "cbc", "chacha20", "chacha20poly1305", - "getrandom 0.2.16", + "hex", "instant", + "once_cell", + "rand", "scrypt", "secp256k1", "serde", @@ -1181,7 +1193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1359,19 +1371,29 @@ dependencies = [ ] [[package]] -name = "radroots-market-indexer" +name = "radroots-nostr" +version = "0.1.0" +dependencies = [ + "nostr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "radroots-radroots-indexer" version = "0.1.0" dependencies = [ "anyhow", "bincode", "clap", "config", - "nostr", "once_cell", "radroots-core", "radroots-events", "radroots-events-codec", "radroots-events-indexed", + "radroots-nostr", "regex", "rusqlite", "serde", @@ -1388,23 +1410,22 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "libc", "rand_chacha", - "rand_core", + "rand_core 0.9.3", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", ] [[package]] @@ -1417,6 +1438,15 @@ dependencies = [ ] [[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1599,7 +1629,6 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "rand", "secp256k1-sys", "serde", ] @@ -2253,16 +2282,6 @@ dependencies = [ ] [[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -7,7 +7,7 @@ resolver = "2" [workspace.package] version = "0.1.0" edition = "2024" -rust-version = "1.86.0" +rust-version = "1.88.0" license = "AGPL-3.0" [workspace.dependencies] @@ -15,6 +15,7 @@ radroots-core = { path = "../crates/core" } radroots-events = { path = "../crates/events" } radroots-events-codec = { path = "../crates/events-codec" } radroots-events-indexed = { path = "../crates/events-indexed" } +radroots-nostr = { path = "../crates/nostr" } anyhow = { version = "1" } clap = { version = "4", features = ["derive"] } diff --git a/app/.env.example b/app/.env.example @@ -1,6 +1,5 @@ VITE_PUBLIC_RADROOTS_MARKET_RELAY_URL= VITE_PUBLIC_RADROOTS_MARKET_INDEXES_URL= VITE_PUBLIC_IDB_NAME= -VITE_PUBLIC_NDK_CACHE_NAME= -VITE_PUBLIC_NDK_CLIENT_NAME= +VITE_PUBLIC_NOSTR_CLIENT_NAME= PORT= \ No newline at end of file diff --git a/app/package.json b/app/package.json @@ -29,13 +29,13 @@ "vite": "7.0.6" }, "dependencies": { - "@radroots/apps-lib": "*", - "@radroots/apps-lib-market": "*", - "@radroots/utils-nostr": "*", - "@radroots/core-bindings": "*", - "@radroots/events-bindings": "*", - "@radroots/events-indexed-bindings": "*", - "@radroots/trade-bindings": "*", - "@nostr-dev-kit/ndk": "2.14.33" + "@radroots/apps-lib": "workspace:*", + "@radroots/apps-lib-market": "workspace:*", + "@radroots/apps-nostr": "workspace:*", + "@radroots/core-bindings": "workspace:*", + "@radroots/events-bindings": "workspace:*", + "@radroots/events-indexed-bindings": "workspace:*", + "@radroots/nostr": "workspace:*", + "@radroots/trade-bindings": "workspace:*" } } \ No newline at end of file diff --git a/app/src/lib/utils/_env.ts b/app/src/lib/utils/_env.ts @@ -1,22 +1,16 @@ const RADROOTS_MARKET_RELAY_URL = import.meta.env.VITE_PUBLIC_RADROOTS_MARKET_RELAY_URL; const RADROOTS_MARKET_INDEXES_URL = import.meta.env.VITE_PUBLIC_RADROOTS_MARKET_INDEXES_URL; const IDB_NAME = import.meta.env.VITE_PUBLIC_IDB_NAME; -const NDK_CACHE_NAME = import.meta.env.VITE_PUBLIC_NDK_CACHE_NAME; -const NDK_CLIENT_NAME = import.meta.env.VITE_PUBLIC_NDK_CLIENT_NAME; // Only validate in browser context, not during build/analysis if (typeof window !== 'undefined') { if (!RADROOTS_MARKET_RELAY_URL || typeof RADROOTS_MARKET_RELAY_URL !== 'string') throw new Error('Missing env var: VITE_PUBLIC_RADROOTS_MARKET_RELAY_URL'); if (!RADROOTS_MARKET_INDEXES_URL || typeof RADROOTS_MARKET_INDEXES_URL !== 'string') throw new Error('Missing env var: VITE_PUBLIC_RADROOTS_MARKET_INDEXES_URL'); if (!IDB_NAME || typeof IDB_NAME !== 'string') throw new Error('Missing env var: VITE_PUBLIC_IDB_NAME'); - if (!NDK_CACHE_NAME || typeof NDK_CACHE_NAME !== 'string') throw new Error('Missing env var: VITE_PUBLIC_NDK_CACHE_NAME'); - if (!NDK_CLIENT_NAME || typeof NDK_CLIENT_NAME !== 'string') throw new Error('Missing env var: VITE_PUBLIC_NDK_CLIENT_NAME'); } export const _env = { IDB_NAME, - NDK_CACHE_NAME, - NDK_CLIENT_NAME, RADROOTS_MARKET_INDEXES_URL, RADROOTS_MARKET_RELAY_URL, -} as const; -\ No newline at end of file +} as const; diff --git a/app/src/lib/utils/listing/index.ts b/app/src/lib/utils/listing/index.ts @@ -1,5 +1,5 @@ import { _env } from "$lib/utils/_env"; -import { type HttpFetch, fetch_json } from "@radroots/apps-lib"; +import { type FetchJsonResult, type HttpFetch, fetch_json } from "@radroots/apps-lib"; import type { RadrootsListingEventMetadata } from "@radroots/events-bindings"; import type { RadrootsEventsIndexedManifest } from "@radroots/events-indexed-bindings"; @@ -15,7 +15,7 @@ export type ListingIndexedData = { export async function fetch_listing_indexes( fetch_fn: HttpFetch, kind: ListingRoutesKind -): Promise<string[]> { +): Promise<FetchJsonResult<string[]>> { const url = `${idx_url}/events/30402/${kind}/indexes.json`; return fetch_json<string[]>(fetch_fn, url); } @@ -24,16 +24,19 @@ export async function load_listing_indexed( fetch_fn: HttpFetch, kind: ListingRoutesKind, key: string -): Promise<ListingIndexedData> { +): Promise<FetchJsonResult<ListingIndexedData>> { const manifest_url = `${idx_url}/events/30402/${kind}/${key}/manifest.json`; - const manifest = await fetch_json<RadrootsEventsIndexedManifest>(fetch_fn, manifest_url); + const manifest_res = await fetch_json<RadrootsEventsIndexedManifest>(fetch_fn, manifest_url); + if (!manifest_res.ok) return manifest_res; let events: RadrootsListingEventMetadata[] = []; - if (manifest.shards.length > 0) { - const shard = manifest.shards[0]; + if (manifest_res.data.shards.length > 0) { + const shard = manifest_res.data.shards[0]; const shard_url = `${idx_url}/events/30402/${kind}/${key}/${shard.file}?v=${shard.sha256}`; - events = await fetch_json<RadrootsListingEventMetadata[]>(fetch_fn, shard_url); + const events_res = await fetch_json<RadrootsListingEventMetadata[]>(fetch_fn, shard_url); + if (!events_res.ok) return events_res; + events = events_res.data; } - return { manifest, events }; + return { ok: true, data: { manifest: manifest_res.data, events } }; } diff --git a/app/src/lib/utils/profile/index.ts b/app/src/lib/utils/profile/index.ts @@ -1,5 +1,5 @@ import { _env } from "$lib/utils/_env"; -import { type HttpFetch, fetch_json } from "@radroots/apps-lib"; +import { type FetchJsonResult, type HttpFetch, fetch_json } from "@radroots/apps-lib"; import type { PageLoadProfileData } from "@radroots/apps-lib-market"; import type { RadrootsCommentEventMetadata, @@ -7,7 +7,7 @@ import type { RadrootsProfileEventMetadata } from "@radroots/events-bindings"; import type { RadrootsEventsIndexedManifest as radroots_events_indexed_manifest } from "@radroots/events-indexed-bindings"; -import { lib_nostr_npub_encode } from "@radroots/utils-nostr"; +import { nostr_npub_encode } from "@radroots/nostr"; type ProfileRoutesKind = "author" | "npub" | "nip05"; @@ -25,19 +25,23 @@ async function fetch_listings( fetch_fn: HttpFetch, kind: ProfileRoutesKind, key: string -): Promise<RadrootsListingEventMetadata[]> { - const manifest = await fetch_json<radroots_events_indexed_manifest>( +): Promise<FetchJsonResult<RadrootsListingEventMetadata[]>> { + const manifest_res = await fetch_json<radroots_events_indexed_manifest>( fetch_fn, `${idx_url}/events/30402/${kind}/${encodeURIComponent(key)}/manifest.json` ); - if (!manifest.shards.length) return []; + if (!manifest_res.ok) return manifest_res; - const shard = manifest.shards[0]; + if (!manifest_res.data.shards.length) return { ok: true, data: [] }; + + const shard = manifest_res.data.shards[0]; const shard_url = `${idx_url}/events/30402/${kind}/${encodeURIComponent( key )}/${shard.file}?v=${shard.sha256}`; - return fetch_json<RadrootsListingEventMetadata[]>(fetch_fn, shard_url); + const events_res = await fetch_json<RadrootsListingEventMetadata[]>(fetch_fn, shard_url); + if (!events_res.ok) return events_res; + return { ok: true, data: events_res.data }; } async function fetch_comments_for_roots( @@ -51,15 +55,11 @@ async function fetch_comments_for_roots( const url = `${idx_url}/events/1111/root/${encodeURIComponent( id )}/metadata.json`; - try { - const metas = await fetch_json<RadrootsCommentEventMetadata[]>( - fetch_fn, - url - ); - return [id, metas]; - } catch { - return [id, [] as RadrootsCommentEventMetadata[]]; - } + const metas_res = await fetch_json<RadrootsCommentEventMetadata[]>( + fetch_fn, + url + ); + return [id, metas_res.ok ? metas_res.data : []]; }) ); @@ -75,26 +75,32 @@ export async function load_profile_indexed( fetch_fn: HttpFetch, kind: ProfileRoutesKind, key: string -): Promise<PageLoadProfileDataWithComments> { - const profile = await fetch_json<RadrootsProfileEventMetadata>( +): Promise<FetchJsonResult<PageLoadProfileDataWithComments>> { + const profile_res = await fetch_json<RadrootsProfileEventMetadata>( fetch_fn, `${idx_url}/events/0/${kind}/${encodeURIComponent(key)}/metadata.json` ); + if (!profile_res.ok) return profile_res; - const listings = await fetch_listings(fetch_fn, kind, key); - const listing_ids = listings.map((m) => m.id.toLowerCase()); + const listings_res = await fetch_listings(fetch_fn, kind, key); + if (!listings_res.ok) return listings_res; + + const listing_ids = listings_res.data.map((m) => m.id.toLowerCase()); const listing_comments = await fetch_comments_for_roots(fetch_fn, listing_ids); - const public_key = profile.author; - const npub = lib_nostr_npub_encode(public_key); + const public_key = profile_res.data.author; + const npub = nostr_npub_encode(public_key); return { - public_key, - npub, - events: { - profile, - listings, - listing_comments + ok: true, + data: { + public_key, + npub, + events: { + profile: profile_res.data, + listings: listings_res.data, + listing_comments + } } }; } diff --git a/app/src/params/public_key.ts b/app/src/params/public_key.ts @@ -1,6 +1,6 @@ -import { regex_nostr_key } from "@radroots/utils-nostr"; +import { REGEX_NOSTR_KEY } from "@radroots/nostr"; import type { ParamMatcher } from '@sveltejs/kit'; export const match: ParamMatcher = (value: string): boolean => { - return regex_nostr_key.test(value); -}; -\ No newline at end of file + return REGEX_NOSTR_KEY.test(value); +}; diff --git a/app/src/routes/(market)/(listing)/[0=country]/+page.ts b/app/src/routes/(market)/(listing)/[0=country]/+page.ts @@ -2,6 +2,7 @@ import { _env } from "$lib/utils/_env"; import { load_listing_indexed } from "$lib/utils/listing"; import type { RadrootsListingEventMetadata } from "@radroots/events-bindings"; import type { RadrootsEventsIndexedManifest } from "@radroots/events-indexed-bindings"; +import { error } from "@sveltejs/kit"; import type { EntryGenerator, PageLoad } from "./$types"; const { RADROOTS_MARKET_INDEXES_URL: idx_url } = _env; @@ -21,11 +22,12 @@ type PageLoadData = { export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => { const { 0: country } = params; const indexed = await load_listing_indexed(fetch, "country", country); + if (!indexed.ok) throw error(indexed.status ?? 500, indexed.message); return { country, - manifest: indexed.manifest, - events: indexed.events, + manifest: indexed.data.manifest, + events: indexed.data.events, }; }; diff --git a/app/src/routes/(market)/(profile)/[0=nip05]/+page.ts b/app/src/routes/(market)/(profile)/[0=nip05]/+page.ts @@ -1,5 +1,6 @@ import { _env } from "$lib/utils/_env"; import { load_profile_indexed } from "$lib/utils/profile"; +import { error } from "@sveltejs/kit"; import type { EntryGenerator, PageLoad } from "./$types"; const { RADROOTS_MARKET_INDEXES_URL: idx_url } = _env; @@ -12,7 +13,9 @@ export const entries: EntryGenerator = async () => { export const load: PageLoad = async ({ fetch, params }) => { const { 0: nip05 } = params; - return load_profile_indexed(fetch, "nip05", nip05); + const result = await load_profile_indexed(fetch, "nip05", nip05); + if (!result.ok) throw error(result.status ?? 500, result.message); + return result.data; }; export const prerender = true; diff --git a/app/src/routes/(market)/(profile)/profile/[...query]/+page.ts b/app/src/routes/(market)/(profile)/profile/[...query]/+page.ts @@ -1,4 +1,4 @@ -import { regex_nostr_key } from "@radroots/utils-nostr"; +import { REGEX_NOSTR_KEY } from "@radroots/nostr"; import { error } from "@sveltejs/kit"; import type { PageLoad } from "./$types"; @@ -8,7 +8,7 @@ export const load: PageLoad = async ({ params }) => { let message = ``; if (query.startsWith(`npub`)) { message = `npub:${query}`; - } else if (regex_nostr_key.test(query)) { + } else if (REGEX_NOSTR_KEY.test(query)) { message = `public_key:${query}`; } else { message = `nip05:${query}`; diff --git a/app/src/routes/(market)/(profile)/profile/[0=npub]/+page.ts b/app/src/routes/(market)/(profile)/profile/[0=npub]/+page.ts @@ -1,5 +1,6 @@ import { _env } from "$lib/utils/_env"; import { load_profile_indexed } from "$lib/utils/profile"; +import { error } from "@sveltejs/kit"; import type { EntryGenerator, PageLoad } from "./$types"; const { RADROOTS_MARKET_INDEXES_URL: idx_url } = _env; @@ -12,7 +13,9 @@ export const entries: EntryGenerator = async () => { export const load: PageLoad = async ({ fetch, params }) => { const { 0: npub } = params; - return load_profile_indexed(fetch, "npub", npub); + const result = await load_profile_indexed(fetch, "npub", npub); + if (!result.ok) throw error(result.status ?? 500, result.message); + return result.data; }; export const prerender = true; diff --git a/app/src/routes/(market)/(profile)/profile/[0=public_key]/+page.ts b/app/src/routes/(market)/(profile)/profile/[0=public_key]/+page.ts @@ -1,5 +1,6 @@ import { _env } from "$lib/utils/_env"; import { load_profile_indexed } from "$lib/utils/profile"; +import { error } from "@sveltejs/kit"; import type { EntryGenerator, PageLoad } from "./$types"; const { RADROOTS_MARKET_INDEXES_URL: idx_url } = _env; @@ -12,7 +13,9 @@ export const entries: EntryGenerator = async () => { export const load: PageLoad = async ({ fetch, params }) => { const { 0: public_key } = params; - return load_profile_indexed(fetch, "author", public_key); + const result = await load_profile_indexed(fetch, "author", public_key); + if (!result.ok) throw error(result.status ?? 500, result.message); + return result.data; }; export const prerender = true; diff --git a/app/src/routes/+layout.svelte b/app/src/routes/+layout.svelte @@ -1,38 +1,36 @@ <script lang="ts"> - import { ndk, ndk_global, ndk_user, nostr_login } from "@radroots/apps-lib"; import { idb, init_theme } from "@radroots/apps-lib-market"; - import { lib_nostr_key_generate } from "@radroots/utils-nostr"; + import { nostr_login_nip01 } from "@radroots/apps-nostr"; + import { nostr_context_create, nostr_context_default, nostr_key_generate, nostr_relays_clear, nostr_relays_open } from "@radroots/nostr"; + import { _env } from "$lib/utils/_env"; import { onMount, type Snippet } from "svelte"; import "../app.css"; let { children }: { children: Snippet } = $props(); let loaded = $state(false); + const nostr_context = nostr_context_default(); + const nostr_context_global = nostr_context_create(); onMount(async () => { await init_theme(); loaded = true; - await $ndk.connect(); - console.log(`[ndk] connected`); + const relay_urls = _env.RADROOTS_MARKET_RELAY_URL + ? [_env.RADROOTS_MARKET_RELAY_URL] + : []; + nostr_relays_clear(nostr_context); + if (relay_urls.length) nostr_relays_open(nostr_context, relay_urls); const global_relays = await idb.read_global("global_relays"); - if (!global_relays) { - console.log(`[ndk_global] no global relays added`); + nostr_relays_clear(nostr_context_global); + if (!global_relays || !global_relays.length) { + console.log(`[nostr] no global relays added`); } else { - $ndk_global.explicitRelayUrls = global_relays; + nostr_relays_open(nostr_context_global, global_relays); } - - await $ndk_global.connect(); - console.log(`[ndk_global] connected`); - - await nostr_login({ - nostr_key: lib_nostr_key_generate(), - }); - - console.log(`$ndk `, $ndk); - console.log(`$ndk_user `, $ndk_user); + nostr_login_nip01(nostr_key_generate()); }); </script> diff --git a/indexer/Cargo.toml b/indexer/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "radroots-market-indexer" +name = "radroots-radroots-indexer" version = "0.1.0" authors = ["Radroots Authors"] description = "Radroots market event indexer" @@ -15,6 +15,7 @@ radroots-core = { workspace = true } radroots-events = { workspace = true } radroots-events-codec = { workspace = true } radroots-events-indexed = { workspace = true } +radroots-nostr = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } @@ -33,7 +34,6 @@ bincode = { version = "2.0", features = ["derive", "serde"] } rusqlite = { version = "0.32.1", features = ["bundled"] } sled = "0.34.7" sha2 = "0.10.9" -nostr = "0.43.0" [dev-dependencies] tempfile = "3" diff --git a/indexer/src/domain/events/listing.rs b/indexer/src/domain/events/listing.rs @@ -1,11 +1,19 @@ use thiserror::Error; +use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreMoney, + RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, +}; use radroots_events::{ + kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA}, listing::{ - RadrootsListing, RadrootsListingEventIndex, RadrootsListingEventMetadata, - RadrootsListingImage, RadrootsListingImageSize, RadrootsListingLocation, - RadrootsListingProduct, RadrootsListingQuantity, + RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, + RadrootsListingDeliveryMethod, RadrootsListingEventIndex, RadrootsListingEventMetadata, + RadrootsListingFarmRef, RadrootsListingImage, RadrootsListingImageSize, + RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus, }, + plot::RadrootsPlotRef, + resource_area::RadrootsResourceAreaRef, RadrootsNostrEvent, }; @@ -17,6 +25,17 @@ pub enum RadrootsListingEventIndexError { ParseError, } +#[derive(Default)] +struct ListingBinDraft { + quantity: Option<RadrootsCoreQuantity>, + price_per_canonical_unit: Option<RadrootsCoreQuantityPrice>, + display_amount: Option<RadrootsCoreDecimal>, + display_unit: Option<RadrootsCoreUnit>, + display_label: Option<String>, + display_price: Option<RadrootsCoreMoney>, + display_price_unit: Option<RadrootsCoreUnit>, +} + fn parse_listing_from_tags( tags: &[Vec<String>], ) -> Result<RadrootsListing, RadrootsListingEventIndexError> { @@ -33,6 +52,89 @@ fn parse_listing_from_tags( let required = |v: Option<String>| v.ok_or(RadrootsListingEventIndexError::ParseError); let d_tag = required(get_first("d"))?; + let farm_pubkey = required(get_first("p"))?; + let farm_pubkey = farm_pubkey.trim().to_string(); + if farm_pubkey.is_empty() { + return Err(RadrootsListingEventIndexError::ParseError); + } + let parse_addr = |value: &str| -> Result<(u32, String, String), RadrootsListingEventIndexError> { + let mut parts = value.splitn(3, ':'); + let kind = parts + .next() + .and_then(|v| v.parse::<u32>().ok()) + .ok_or(RadrootsListingEventIndexError::ParseError)?; + let pubkey = parts + .next() + .ok_or(RadrootsListingEventIndexError::ParseError)? + .to_string(); + let d_tag = parts + .next() + .ok_or(RadrootsListingEventIndexError::ParseError)? + .to_string(); + if pubkey.trim().is_empty() || d_tag.trim().is_empty() { + return Err(RadrootsListingEventIndexError::ParseError); + } + Ok((kind, pubkey, d_tag)) + }; + + let mut farm_addr_pubkey: Option<String> = None; + let mut farm_d_tag: Option<String> = None; + for tag in tags.iter().filter(|t| t.first().map(|k| k == "a").unwrap_or(false)) { + let value = tag.get(1).ok_or(RadrootsListingEventIndexError::ParseError)?; + let (kind, pubkey, d_tag) = parse_addr(value)?; + if kind == KIND_FARM { + farm_addr_pubkey = Some(pubkey); + farm_d_tag = Some(d_tag); + break; + } + } + let farm_addr_pubkey = farm_addr_pubkey.ok_or(RadrootsListingEventIndexError::ParseError)?; + let farm_d_tag = farm_d_tag.ok_or(RadrootsListingEventIndexError::ParseError)?; + if farm_addr_pubkey != farm_pubkey || farm_d_tag.trim().is_empty() { + return Err(RadrootsListingEventIndexError::ParseError); + } + let farm = RadrootsListingFarmRef { + pubkey: farm_pubkey, + d_tag: farm_d_tag, + }; + + let resource_area = if let Some(tag) = tags + .iter() + .find(|t| t.first().map(|k| k == "radroots:resource_area").unwrap_or(false)) + { + let value = tag.get(1).ok_or(RadrootsListingEventIndexError::ParseError)?; + let (kind, pubkey, d_tag) = parse_addr(value)?; + if kind != KIND_RESOURCE_AREA { + return Err(RadrootsListingEventIndexError::ParseError); + } + Some(RadrootsResourceAreaRef { pubkey, d_tag }) + } else { + None + }; + + let plot = if let Some(tag) = tags + .iter() + .find(|t| t.first().map(|k| k == "radroots:plot").unwrap_or(false)) + { + let value = tag.get(1).ok_or(RadrootsListingEventIndexError::ParseError)?; + let (kind, pubkey, d_tag) = parse_addr(value)?; + if kind != KIND_PLOT { + return Err(RadrootsListingEventIndexError::ParseError); + } + Some(RadrootsPlotRef { pubkey, d_tag }) + } else { + None + }; + + let location_tags: Vec<&Vec<String>> = tags + .iter() + .filter(|t| t.first().map(|k| k == "location").unwrap_or(false)) + .collect(); + let product_location = if location_tags.len() > 1 { + location_tags.first().and_then(|t| t.get(1).cloned()) + } else { + None + }; let product = RadrootsListingProduct { key: required(get_first("key"))?, @@ -41,84 +143,164 @@ fn parse_listing_from_tags( summary: get_first("summary"), process: get_first("process"), lot: get_first("lot"), - location: get_first("location"), + location: product_location, profile: get_first("profile"), year: get_first("year"), }; - let mut quantities: Vec<RadrootsListingQuantity> = Vec::new(); + let parse_decimal = |value: &str| value.parse::<RadrootsCoreDecimal>().ok(); + let parse_unit = |value: &str| value.parse::<RadrootsCoreUnit>().ok(); + let parse_currency = |value: &str| value.parse::<RadrootsCoreCurrency>().ok(); + + let mut bin_order: Vec<String> = Vec::new(); + let mut bin_drafts: std::collections::BTreeMap<String, ListingBinDraft> = + std::collections::BTreeMap::new(); + + let mut upsert_bin = |bin_id: String, update: ListingBinDraft| { + let entry = bin_drafts.entry(bin_id.clone()).or_default(); + if !bin_order.iter().any(|id| id == &bin_id) { + bin_order.push(bin_id); + } + if update.quantity.is_some() { + entry.quantity = update.quantity; + } + if update.price_per_canonical_unit.is_some() { + entry.price_per_canonical_unit = update.price_per_canonical_unit; + } + if update.display_amount.is_some() { + entry.display_amount = update.display_amount; + } + if update.display_unit.is_some() { + entry.display_unit = update.display_unit; + } + if update.display_label.is_some() { + entry.display_label = update.display_label; + } + if update.display_price.is_some() { + entry.display_price = update.display_price; + } + if update.display_price_unit.is_some() { + entry.display_price_unit = update.display_price_unit; + } + }; + for t in tags .iter() - .filter(|t| t.first().map(|k| k == "quantity").unwrap_or(false)) + .filter(|t| t.first().map(|k| k == "radroots:bin").unwrap_or(false)) { - if t.len() >= 3 { - let amount = match t[1].parse::<radroots_core::RadrootsCoreDecimal>() { - Ok(v) => v, - Err(_) => continue, - }; - let unit = match t[2].parse::<radroots_core::RadrootsCoreUnit>() { - Ok(v) => v, - Err(_) => continue, - }; - let label = t.get(3).cloned(); - quantities.push(RadrootsListingQuantity { - value: radroots_core::RadrootsCoreQuantity { - amount, - unit, - label: label.clone(), - }, - label, - count: None, - }); + if t.len() < 4 { + continue; + } + let bin_id = t.get(1).map(|v| v.trim().to_string()).unwrap_or_default(); + if bin_id.is_empty() { + continue; } + let amount = t.get(2).and_then(|v| parse_decimal(v)); + let unit = t.get(3).and_then(|v| parse_unit(v)); + let (Some(amount), Some(unit)) = (amount, unit) else { + continue; + }; + let mut draft = ListingBinDraft::default(); + draft.quantity = Some(RadrootsCoreQuantity { + amount, + unit, + label: None, + }); + let display_amount = t.get(4).and_then(|v| parse_decimal(v)); + let display_unit = t.get(5).and_then(|v| parse_unit(v)); + if let (Some(display_amount), Some(display_unit)) = (display_amount, display_unit) { + draft.display_amount = Some(display_amount); + draft.display_unit = Some(display_unit); + let label = t + .get(6) + .map(|v| v.trim().to_string()) + .filter(|v| !v.is_empty()); + draft.display_label = label; + } + upsert_bin(bin_id, draft); } - let mut prices: Vec<radroots_core::RadrootsCoreQuantityPrice> = Vec::new(); for t in tags .iter() - .filter(|t| t.first().map(|k| k == "price").unwrap_or(false)) + .filter(|t| t.first().map(|k| k == "radroots:price").unwrap_or(false)) { - if t.len() >= 5 { - let money_amount = match t[1].parse::<radroots_core::RadrootsCoreDecimal>() { - Ok(v) => v, - Err(_) => continue, - }; - let money_currency = match t[2].parse::<radroots_core::RadrootsCoreCurrency>() { - Ok(v) => v, - Err(_) => continue, - }; - let qty_amount = match t[3].parse::<radroots_core::RadrootsCoreDecimal>() { - Ok(v) => v, - Err(_) => continue, - }; - let qty_unit = match t[4].parse::<radroots_core::RadrootsCoreUnit>() { - Ok(v) => v, - Err(_) => continue, - }; - - let price = radroots_core::RadrootsCoreQuantityPrice { - amount: radroots_core::RadrootsCoreMoney { - amount: money_amount, - currency: money_currency, - }, - quantity: radroots_core::RadrootsCoreQuantity { - amount: qty_amount, - unit: qty_unit, - label: None, - }, - }; - prices.push(price); + if t.len() < 6 { + continue; } + let bin_id = t.get(1).map(|v| v.trim().to_string()).unwrap_or_default(); + if bin_id.is_empty() { + continue; + } + let money_amount = t.get(2).and_then(|v| parse_decimal(v)); + let money_currency = t.get(3).and_then(|v| parse_currency(v)); + let qty_amount = t.get(4).and_then(|v| parse_decimal(v)); + let qty_unit = t.get(5).and_then(|v| parse_unit(v)); + let (Some(money_amount), Some(money_currency), Some(qty_amount), Some(qty_unit)) = + (money_amount, money_currency, qty_amount, qty_unit) + else { + continue; + }; + let mut draft = ListingBinDraft::default(); + draft.price_per_canonical_unit = Some(RadrootsCoreQuantityPrice { + amount: RadrootsCoreMoney { + amount: money_amount, + currency: money_currency, + }, + quantity: RadrootsCoreQuantity { + amount: qty_amount, + unit: qty_unit, + label: None, + }, + }); + let display_amount = t.get(6).and_then(|v| parse_decimal(v)); + let display_unit = t.get(7).and_then(|v| parse_unit(v)); + if let (Some(display_amount), Some(display_unit)) = (display_amount, display_unit) { + draft.display_price = Some(RadrootsCoreMoney { + amount: display_amount, + currency: money_currency, + }); + draft.display_price_unit = Some(display_unit); + } + upsert_bin(bin_id, draft); + } + + let bins: Vec<RadrootsListingBin> = bin_order + .iter() + .filter_map(|bin_id| bin_drafts.get(bin_id).map(|draft| (bin_id, draft))) + .filter_map(|(bin_id, draft)| { + let quantity = draft.quantity.clone()?; + let price_per_canonical_unit = draft.price_per_canonical_unit.clone()?; + Some(RadrootsListingBin { + bin_id: bin_id.clone(), + quantity, + price_per_canonical_unit, + display_amount: draft.display_amount, + display_unit: draft.display_unit, + display_label: draft.display_label.clone(), + display_price: draft.display_price.clone(), + display_price_unit: draft.display_price_unit, + }) + }) + .collect(); + if bins.is_empty() { + return Err(RadrootsListingEventIndexError::ParseError); + } + + let primary_bin_id = required(get_first("radroots:primary_bin"))? + .trim() + .to_string(); + if primary_bin_id.is_empty() { + return Err(RadrootsListingEventIndexError::ParseError); + } + if !bins.iter().any(|bin| bin.bin_id == primary_bin_id) { + return Err(RadrootsListingEventIndexError::ParseError); } let mut primary: Option<String> = None; let mut city: Option<String> = None; let mut region: Option<String> = None; let mut country: Option<String> = None; - if let Some(t) = tags - .iter() - .find(|t| t.first().map(|k| k == "location").unwrap_or(false)) - { + if let Some(t) = location_tags.last() { if t.len() >= 2 { primary = Some(t[1].clone()); } @@ -180,15 +362,20 @@ fn parse_listing_from_tags( let images = tags .iter() - .filter(|t| t.first().map(|k| k == "img").unwrap_or(false)) + .filter(|t| t.first().map(|k| k == "image").unwrap_or(false)) .map(|t| { let url = t.get(1).cloned().unwrap_or_default(); - let size = if t.len() >= 4 { - let w = t[2].parse::<u32>().ok(); - let h = t[3].parse::<u32>().ok(); - match (w, h) { - (Some(w), Some(h)) => Some(RadrootsListingImageSize { w, h }), - _ => None, + let size = if t.len() >= 3 { + let mut parts = t[2].split('x'); + let w = parts.next().and_then(|v| v.parse::<u32>().ok()); + let h = parts.next().and_then(|v| v.parse::<u32>().ok()); + if parts.next().is_none() { + match (w, h) { + (Some(w), Some(h)) => Some(RadrootsListingImageSize { w, h }), + _ => None, + } + } else { + None } } else { None @@ -198,12 +385,80 @@ fn parse_listing_from_tags( .collect::<Vec<_>>(); let images = if images.is_empty() { None } else { Some(images) }; + let inventory_available = get_first("inventory") + .and_then(|value| parse_decimal(&value)); + + let availability = if let Some(value) = get_first("status") + .map(|v| v.trim().to_string()) + .filter(|v| !v.is_empty()) + { + let status = match value.as_str() { + "active" => RadrootsListingStatus::Active, + "sold" => RadrootsListingStatus::Sold, + _ => RadrootsListingStatus::Other { value }, + }; + Some(RadrootsListingAvailability::Status { status }) + } else { + let start = get_first("published_at").and_then(|v| v.parse::<u64>().ok()); + let end = get_first("expires_at").and_then(|v| v.parse::<u64>().ok()); + if start.is_some() || end.is_some() { + Some(RadrootsListingAvailability::Window { start, end }) + } else { + None + } + }; + + let delivery_method = tags + .iter() + .find(|t| t.first().map(|k| k == "delivery").unwrap_or(false)) + .and_then(|t| t.get(1).map(|v| v.trim().to_string())) + .and_then(|kind| { + if kind.is_empty() { + return None; + } + let method = match kind.as_str() { + "pickup" => RadrootsListingDeliveryMethod::Pickup, + "local_delivery" => RadrootsListingDeliveryMethod::LocalDelivery, + "shipping" => RadrootsListingDeliveryMethod::Shipping, + "other" => { + let detail = tags + .iter() + .find(|t| t.first().map(|k| k == "delivery").unwrap_or(false)) + .and_then(|t| t.get(2)) + .map(|v| v.trim().to_string()) + .filter(|v| !v.is_empty())?; + RadrootsListingDeliveryMethod::Other { method: detail } + } + _ => return None, + }; + Some(method) + }); + + let mut discounts: Vec<RadrootsCoreDiscount> = Vec::new(); + for t in tags + .iter() + .filter(|t| t.first().map(|k| k == "radroots:discount").unwrap_or(false)) + { + if let Some(payload) = t.get(1) { + if let Ok(discount) = serde_json::from_str::<RadrootsCoreDiscount>(payload) { + discounts.push(discount); + } + } + } + let discounts = if discounts.is_empty() { None } else { Some(discounts) }; + Ok(RadrootsListing { d_tag, + farm, product, - quantities, - prices, - discounts: None, + primary_bin_id, + bins, + resource_area, + plot, + discounts, + inventory_available, + availability, + delivery_method, location, images, }) diff --git a/indexer/src/domain/events/profile.rs b/indexer/src/domain/events/profile.rs @@ -1,6 +1,9 @@ use anyhow::Result; use radroots_events::{ - profile::{RadrootsProfile, RadrootsProfileEventIndex, RadrootsProfileEventMetadata}, + profile::{ + radroots_profile_type_from_tag_value, RadrootsProfile, RadrootsProfileEventIndex, + RadrootsProfileEventMetadata, + }, RadrootsNostrEvent, }; use thiserror::Error; @@ -34,6 +37,7 @@ pub fn create_radroots_profile_event_metadata( author, published_at, kind, + profile_type: None, profile, }) } @@ -52,13 +56,19 @@ impl ToRadrootsProfileEventIndex for RelayIndexerEvent { let id = self.id.clone(); let author = self.author.clone(); - let metadata = create_radroots_profile_event_metadata( + let mut metadata = create_radroots_profile_event_metadata( id.clone(), author.clone(), self.created_at, kind_u32, &self.content, )?; + metadata.profile_type = self + .tags + .iter() + .filter(|tag| tag.get(0).map(|k| k == "t").unwrap_or(false)) + .filter_map(|tag| tag.get(1)) + .find_map(|value| radroots_profile_type_from_tag_value(value)); Ok(RadrootsProfileEventIndex { event: RadrootsNostrEvent { diff --git a/indexer/src/main.rs b/indexer/src/main.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Parser; -use radroots_market_indexer::{cli, run, telemetry, Settings}; +use radroots_radroots_indexer::{cli, run, telemetry, Settings}; use tracing::info; #[tokio::main] diff --git a/indexer/src/utils/nostr.rs b/indexer/src/utils/nostr.rs @@ -1,19 +1,18 @@ use std::collections::HashMap; -use nostr::key::{Error as PublicKeyError, PublicKey}; -use nostr::prelude::ToBech32; +use radroots_nostr::prelude::{radroots_nostr_parse_pubkey, RadrootsNostrToBech32}; use thiserror::Error; #[derive(Debug, Error)] pub enum NostrUtilsError { - #[error("Invalid hex for public key: {0}")] - InvalidPublicKey(#[from] PublicKeyError), + #[error("Invalid public key: {0}")] + InvalidPublicKey(#[from] radroots_nostr::parse::ParseError), #[error("Tag parsing error: {0}")] TagParseError(String), } pub fn public_key_to_npub(public_key_hex: &str) -> Result<String, NostrUtilsError> { - let pubkey = PublicKey::from_hex(public_key_hex)?; + let pubkey = radroots_nostr_parse_pubkey(public_key_hex)?; let bech32 = match pubkey.to_bech32() { Ok(value) => value, Err(err) => match err {}, diff --git a/indexer/tests/indexer_determinism.rs b/indexer/tests/indexer_determinism.rs @@ -1,12 +1,12 @@ -use radroots_market_indexer::config::{Indexer, Listings, Relay, Settings}; -use radroots_market_indexer::domain::indexer::kind::IndexerEventKind; -use radroots_market_indexer::domain::indexer::models::{ +use radroots_radroots_indexer::config::{Indexer, Listings, Relay, Settings}; +use radroots_radroots_indexer::domain::indexer::kind::IndexerEventKind; +use radroots_radroots_indexer::domain::indexer::models::{ EventCommentIndexes, EventFollowIndexes, EventIndexes, EventJobFeedbackIndexes, EventJobRequestIndexes, EventJobResultIndexes, EventListingIndexes, EventPostIndexes, EventProfileIndexes, EventReactionIndexes, WriteEventIndexes, }; -use radroots_market_indexer::domain::resolvers::profile::ProfileResolver; -use radroots_market_indexer::relay::event::RelayIndexerEvent; +use radroots_radroots_indexer::domain::resolvers::profile::ProfileResolver; +use radroots_radroots_indexer::relay::event::RelayIndexerEvent; use radroots_events::kinds::{KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN}; use std::path::Path; use tempfile::tempdir; diff --git a/turbo.json b/turbo.json @@ -33,7 +33,8 @@ "dependsOn": [ "@radroots/apps-lib#build", "@radroots/apps-lib-market#build", - "@radroots/utils-nostr#build", + "@radroots/apps-nostr#build", + "@radroots/nostr#build", "@radroots/core-bindings#build", "@radroots/events-bindings#build", "@radroots/events-indexed-bindings#build", @@ -41,4 +42,4 @@ ] } } -} -\ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock @@ -424,38 +424,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@nostr-dev-kit/ndk-cache-dexie@2.6.34": - version "2.6.34" - resolved "https://registry.yarnpkg.com/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.6.34.tgz#42f25ddd9d7b0639f1628e914eec503c5ecc8fa1" - integrity sha512-NFk9I7E/eXIevLDnjyZHHwxdL4E891KVGlkr0k07CItoIf8A2cxtZJ7Pe4Ei5fe49nipL9iA+sEmOq9xveLR7g== - dependencies: - "@nostr-dev-kit/ndk" "2.14.33" - debug "^4.3.7" - dexie "^4.0.8" - nostr-tools "^2.4.0" - typescript-lru-cache "^2.0.0" - -"@nostr-dev-kit/ndk-svelte@2.4.38": - version "2.4.38" - resolved "https://registry.yarnpkg.com/@nostr-dev-kit/ndk-svelte/-/ndk-svelte-2.4.38.tgz#888ec21240912a0a5262ebfa00f18cba97ab439f" - integrity sha512-bxCXGaYqpQGg1iQDsYgzIpwxLwJ+IBz2SnGyJiDo794qeG2LmKdrVJ6ia7MVtjwaGJ86bWSD5ohy48/wRHHgnQ== - dependencies: - "@nostr-dev-kit/ndk" "2.14.33" - -"@nostr-dev-kit/ndk@2.14.33": - version "2.14.33" - resolved "https://registry.yarnpkg.com/@nostr-dev-kit/ndk/-/ndk-2.14.33.tgz#c31c7fec80321fe7cc6eddd34e2240baa585e30e" - integrity sha512-akiafJZj4ZAAYse+qNSjrx6Yg4Y2gB4UyMlo6I30ITVikRAtgPejXgtLGmjWCcgtf56b9g79AikAr3IZtr1pLA== - dependencies: - "@noble/curves" "^1.6.0" - "@noble/hashes" "^1.5.0" - "@noble/secp256k1" "^2.1.0" - "@scure/base" "^1.1.9" - debug "^4.3.6" - light-bolt11-decoder "^3.2.0" - tseep "^1.3.1" - typescript-lru-cache "^2" - "@oclif/core@>=3.26.0": version "4.5.2" resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.5.2.tgz#4db8a365fa7e9e33af272294f710a7f3f25538e2"