commit 027e986a4fe2ab526c992e9cee2026c702ff12ba
parent 9c6fc86afab20c81118be10bd873999478435439
Author: triesap <137732411+triesap@users.noreply.github.com>
Date: Fri, 8 Aug 2025 21:01:47 +0000
utils-nostr: add `listing` nip-99 schemas, types, and event parsing utils
Diffstat:
5 files changed, 190 insertions(+), 7 deletions(-)
diff --git a/utils-nostr/package.json b/utils-nostr/package.json
@@ -2,6 +2,7 @@
"name": "@radroots/utils-nostr",
"version": "0.0.1",
"private": true,
+ "license": "GPLv3",
"type": "module",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.js",
@@ -18,7 +19,7 @@
"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 && rimraf tsconfig.tsbuildinfo",
+ "clean": "rimraf dist",
"dev": "npm run watch",
"watch": "tsc -w"
},
diff --git a/utils-nostr/src/events/parse.ts b/utils-nostr/src/events/parse.ts
@@ -1,6 +1,7 @@
import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { nostr_event_metadata_schema } from "../schemas/lib.js";
-import { type NostrEventMetadata } from "../types/lib.js";
+import { nostr_event_listing_schema, nostr_event_metadata_schema, nostr_tag_listing_schema, nostr_tag_location_schema, nostr_tag_price_schema, nostr_tag_quantity_schema } from "../schemas/lib.js";
+import { NostrEventListing, type NostrEventMetadata } from "../types/lib.js";
+import { get_event_tag, get_event_tags } from "./lib.js";
export const parse_nostr_metadata_event = (event: NDKEvent): NostrEventMetadata | undefined => {
if (!event || typeof event.content !== 'string' || event.kind !== 0) return undefined;
@@ -14,4 +15,74 @@ export const parse_nostr_metadata_event = (event: NDKEvent): NostrEventMetadata
}
};
+export const parse_nostr_listing_event = (event: NDKEvent): NostrEventListing | undefined => {
+ if (!event || event.kind !== 30402 || !Array.isArray(event.tags)) return;
+ try {
+ const tags = event.tags;
+
+ const d_tag = get_event_tag(tags, 'd');
+
+ const listing_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 listing = nostr_tag_listing_schema.parse(listing_raw);
+
+ const quantities = get_event_tags(tags, 'quantity')
+ .map(q => {
+ if (q.length < 3) return undefined;
+ return nostr_tag_quantity_schema.parse({
+ amt: q[1],
+ unit: q[2],
+ label: q[3]
+ });
+ })
+ .filter(Boolean);
+
+ const prices = get_event_tags(tags, 'price')
+ .map(p => {
+ if (p.length < 6) return undefined;
+ return nostr_tag_price_schema.parse({
+ amt: p[1],
+ currency: p[2],
+ qty_amt: p[3],
+ qty_unit: p[4],
+ qty_key: p[5]
+ });
+ })
+ .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];
+
+ const location = Object.keys(location_raw).length
+ ? nostr_tag_location_schema.parse(location_raw)
+ : undefined;
+
+ const parsed = nostr_event_listing_schema.parse({
+ d_tag,
+ listing,
+ quantities,
+ prices,
+ location
+ });
+
+ return parsed;
+ } catch {
+ return undefined;
+ }
+};
diff --git a/utils-nostr/src/events/subscription.ts b/utils-nostr/src/events/subscription.ts
@@ -1,9 +1,10 @@
import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { type NostrEventMetadata } from "../types/lib.js";
-import { parse_nostr_metadata_event } from "./parse.js";
+import { NostrEventListing, type NostrEventMetadata } from "../types/lib.js";
+import { parse_nostr_listing_event, parse_nostr_metadata_event } from "./parse.js";
export type NdkEventPayload =
| { kind: 0; metadata: NostrEventMetadata; }
+ | { kind: 30402; listing: NostrEventListing; }
export const on_ndk_event = (event: NDKEvent): NdkEventPayload | undefined => {
if (!event || typeof event.kind !== 'number') return undefined;
@@ -14,7 +15,11 @@ export const on_ndk_event = (event: NDKEvent): NdkEventPayload | undefined => {
if (!data) return;
return { kind: event.kind, metadata: data };
};
-
+ case 30402: {
+ const data = parse_nostr_listing_event(event);
+ if (!data) return;
+ return { kind: event.kind, listing: data };
+ };
default: return undefined;
}
};
diff --git a/utils-nostr/src/schemas/lib.ts b/utils-nostr/src/schemas/lib.ts
@@ -12,3 +12,100 @@ export const nostr_event_metadata_schema = z.object({
lud16: z.string().optional(),
bot: z.boolean().optional(),
});
+
+export const nostr_tag_listing_schema = z.object({
+ key: z.string(),
+ title: z.string(),
+ category: z.string(),
+ summary: z.string().optional(),
+ process: z.string().optional(),
+ lot: z.string().optional(),
+ location: z.string().optional(),
+ profile: z.string().optional(),
+ year: z.string().optional()
+});
+
+export const nostr_tag_quantity_schema = z.object({
+ amt: z.string(),
+ unit: z.string(),
+ label: z.string().optional()
+});
+
+export const nostr_tag_price_schema = z.object({
+ amt: z.string(),
+ currency: z.string(),
+ qty_amt: z.string(),
+ qty_unit: z.string(),
+ qty_key: z.string()
+});
+
+export const nostr_tag_discount_schema = z.union([
+ z.object({
+ quantity: z.object({
+ ref_quantity: z.string(),
+ threshold: z.string(),
+ value: z.string(),
+ currency: z.string()
+ })
+ }),
+ z.object({
+ mass: z.object({
+ unit: z.string(),
+ threshold: z.string(),
+ threshold_unit: z.string(),
+ value: z.string(),
+ currency: z.string()
+ })
+ }),
+ z.object({
+ subtotal: z.object({
+ threshold: z.string(),
+ currency: z.string(),
+ value: z.string(),
+ measure: z.string()
+ })
+ }),
+ z.object({
+ total: z.object({
+ total_min: z.string(),
+ value: z.string(),
+ measure: z.string()
+ })
+ })
+]);
+
+export const nostr_tag_location_schema = z.object({
+ primary: z.string(),
+ city: z.string().optional(),
+ region: z.string().optional(),
+ country: z.string().optional(),
+ lat: z.number().optional(),
+ lng: z.number().optional()
+});
+
+export const nostr_tag_image_schema = z.object({
+ url: z.string(),
+ size: z
+ .object({
+ w: z.number(),
+ h: z.number()
+ })
+ .optional()
+});
+
+export const nostr_tag_client_schema = z.object({
+ name: z.string(),
+ pubkey: z.string(),
+ relay: z.string()
+});
+
+export const nostr_event_listing_schema = z.object({
+ d_tag: z.string(),
+ listing: nostr_tag_listing_schema,
+ quantities: z.array(nostr_tag_quantity_schema),
+ prices: z.array(nostr_tag_price_schema),
+ discounts: z.array(nostr_tag_discount_schema).optional(),
+ location: nostr_tag_location_schema.optional(),
+ images: z.array(nostr_tag_image_schema).optional(),
+ client: nostr_tag_client_schema.optional()
+});
+\ No newline at end of file
diff --git a/utils-nostr/src/types/lib.ts b/utils-nostr/src/types/lib.ts
@@ -1,4 +1,12 @@
import { z } from 'zod';
-import { nostr_event_metadata_schema } from "../schemas/lib.js";
+import { nostr_event_listing_schema, nostr_event_metadata_schema, nostr_tag_client_schema, nostr_tag_discount_schema, nostr_tag_image_schema, nostr_tag_listing_schema, nostr_tag_location_schema, nostr_tag_price_schema, nostr_tag_quantity_schema } from "../schemas/lib.js";
export type NostrEventMetadata = z.infer<typeof nostr_event_metadata_schema>;
+export type NostrEventListing = z.infer<typeof nostr_event_listing_schema>
+export type NostrTagClient = z.infer<typeof nostr_tag_client_schema>
+export type NostrTagMediaUpload = z.infer<typeof nostr_tag_image_schema>
+export type NostrTagLocation = z.infer<typeof nostr_tag_location_schema>
+export type NostrTagPriceDiscount = z.infer<typeof nostr_tag_discount_schema>
+export type NostrTagPrice = z.infer<typeof nostr_tag_price_schema>
+export type NostrTagQuantity = z.infer<typeof nostr_tag_quantity_schema>
+export type NostrTagListing = z.infer<typeof nostr_tag_listing_schema>