commit d3da6167b3370e46d00ada7f285f46da7b104d5d
parent 08d6985262ecd4e040b27468679e6d50a863a05d
Author: triesap <tyson@radroots.org>
Date: Wed, 20 Aug 2025 15:04:21 -0700
workspace: add `crates/*` workspace with `radroots-core`, `radroots-events`, `radroots-events-codec`
Diffstat:
108 files changed, 4651 insertions(+), 1756 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,14 +1,58 @@
-/target
+# Dependencies
node_modules
-dist
+.pnp
+.pnp.js
+
+# Local env files
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# Testing
+coverage
+
+# Turbo
.turbo
-justfile
-notes*.txt
-.tmp*
-.vscode
-git-diff.txt
+# Vercel
+.vercel
+
+# Build Outputs
+.next/
+out/
+build
+dist
+.yarn
+target/
+yarn.lock
+
+# Debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# OS
.DS_Store
+Thumbs.db
+
+#secrets
*.pem
+*.crt
+*.key
-#bindings/**/*.ts
-\ No newline at end of file
+# local
+.tmp*
+.archive*
+.dev*
+.local*
+.vscode
+notes*.txt
+notes*.md
+notes*.json
+tree*.txt
+git-diff*.txt
+prompt*.txt
+tree*.txt
+justfile
diff --git a/Cargo.lock b/Cargo.lock
@@ -18,6 +18,12 @@ dependencies = [
]
[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -31,9 +37,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "cc"
-version = "1.2.31"
+version = "1.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
+checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f"
dependencies = [
"shlex",
]
@@ -106,9 +112,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.174"
+version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "log"
@@ -139,9 +145,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
-version = "1.0.95"
+version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
@@ -156,20 +162,80 @@ dependencies = [
]
[[package]]
-name = "radroots-common"
+name = "radroots-core"
version = "0.1.0"
dependencies = [
+ "rust_decimal",
+ "rust_decimal_macros",
+ "serde",
+ "typeshare",
+]
+
+[[package]]
+name = "radroots-events"
+version = "0.1.0"
+dependencies = [
+ "radroots-core",
"serde",
"serde_json",
- "thiserror",
"typeshare",
]
[[package]]
+name = "radroots-events-codec"
+version = "0.1.0"
+dependencies = [
+ "radroots-core",
+ "radroots-events",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "radroots-events-indexed"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "typeshare",
+]
+
+[[package]]
+name = "radroots-trade"
+version = "0.1.0"
+dependencies = [
+ "radroots-core",
+ "radroots-events",
+ "radroots-events-codec",
+ "serde",
+ "typeshare",
+]
+
+[[package]]
+name = "rust_decimal"
+version = "1.37.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d"
+dependencies = [
+ "arrayvec",
+ "num-traits",
+ "serde",
+]
+
+[[package]]
+name = "rust_decimal_macros"
+version = "1.37.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6268b74858287e1a062271b988a0c534bf85bbeb567fe09331bf40ed78113d5"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "rustversion"
-version = "1.0.21"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
@@ -217,9 +283,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
-version = "2.0.104"
+version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
@@ -227,26 +293,6 @@ dependencies = [
]
[[package]]
-name = "thiserror"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
-dependencies = [
- "thiserror-impl",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
name = "typeshare"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,12 +1,5 @@
-[package]
-name = "radroots-common"
-version = "0.1.0"
-authors = ["Radroots Authors"]
-license = "AGPLv3"
-edition = "2021"
-
-[dependencies]
-serde = "1.0"
-serde_json = "1.0"
-thiserror = "1.0"
-typeshare = "1.0.0"
+[workspace]
+members = [
+ "crates/*",
+]
+resolver = "2"
diff --git a/bindings/ts/package.json b/bindings/ts/package.json
@@ -1,45 +0,0 @@
-{
- "name": "@radroots/radroots-common-bindings",
- "version": "1.0.0",
- "private": true,
- "license": "AGPLv3",
- "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"
- }
- },
- "files": [
- "dist"
- ],
- "sideEffects": false,
- "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",
- "gen:types": "typeshare --lang typescript --output-file=src/types.ts ../../src",
- "gen:exports": "gen-package-exports.js --is_module",
- "gen": "npm run gen:types && npm run gen:exports"
- },
- "devDependencies": {
- "@radroots/dev": "*",
- "@radroots/tsconfig": "*",
- "rimraf": "^6.0.1",
- "ts-to-zod": "^3.15.0"
- },
- "dependencies": {
- "zod": "^4.0.5"
- },
- "publishConfig": {
- "access": "public"
- }
-}
-\ No newline at end of file
diff --git a/bindings/ts/src/events/schema.ts b/bindings/ts/src/events/schema.ts
@@ -1,133 +0,0 @@
-import { z } from "zod";
-
-export const radroots_nostr_event_ref_schema = z.object({
- id: z.string(),
- author: z.string(),
- kind: z.number(),
- d_tag: z.string().optional(),
- relays: z.array(z.string()).optional()
-});
-
-export const radroots_listing_image_schema = z.object({
- url: z.string(),
- size: z.object({
- w: z.number(),
- h: z.number()
- }).optional()
-});
-
-export const radroots_listing_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(),
- geohash: z.string().optional()
-});
-
-export const radroots_listing_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 radroots_listing_price_schema = z.object({
- amt: z.string(),
- currency: z.string(),
- qty_amt: z.string(),
- qty_unit: z.string(),
- qty_key: z.string()
-});
-
-export const radroots_listing_quantity_schema = z.object({
- amt: z.string(),
- unit: z.string(),
- label: z.string().optional()
-});
-
-export const radroots_listing_product_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 radroots_listing_schema = z.object({
- d_tag: z.string(),
- product: radroots_listing_product_schema,
- quantities: z.array(radroots_listing_quantity_schema),
- prices: z.array(radroots_listing_price_schema),
- discounts: z.array(radroots_listing_discount_schema).optional(),
- location: radroots_listing_location_schema.optional(),
- images: z.array(radroots_listing_image_schema).optional()
-});
-
-export const radroots_profile_schema = z.object({
- name: z.string(),
- display_name: z.string().optional(),
- nip05: z.string().optional(),
- about: z.string().optional(),
- website: z.string().optional(),
- picture: z.string().optional(),
- banner: z.string().optional(),
- lud06: z.string().optional(),
- lud16: z.string().optional(),
- bot: z.string().optional()
-});
-
-export const radroots_comment_schema = z.object({
- root: radroots_nostr_event_ref_schema,
- parent: radroots_nostr_event_ref_schema,
- content: z.string()
-});
-
-export const radroots_reaction_schema = z.object({
- root: radroots_nostr_event_ref_schema,
- content: z.string()
-});
-
-export const radroots_follow_profile_schema = z.object({
- published_at: z.number(),
- public_key: z.string(),
- relay_url: z.string().optional(),
- contact_name: z.string().optional()
-});
-
-export const radroots_follow_schema = z.object({
- list: z.array(radroots_follow_profile_schema)
-});
diff --git a/bindings/ts/src/events/types.ts b/bindings/ts/src/events/types.ts
@@ -1,16 +0,0 @@
-import { z } from "zod";
-import { radroots_comment_schema, radroots_follow_profile_schema, radroots_follow_schema, 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, radroots_nostr_event_ref_schema, radroots_profile_schema, radroots_reaction_schema } from "./schema.js";
-
-export type RadrootsNostrEventRef = z.infer<typeof radroots_nostr_event_ref_schema>;
-export type RadrootsListingImage = z.infer<typeof radroots_listing_image_schema>;
-export type RadrootsListingLocation = z.infer<typeof radroots_listing_location_schema>;
-export type RadrootsListingDiscount = z.infer<typeof radroots_listing_discount_schema>;
-export type RadrootsListingPrice = z.infer<typeof radroots_listing_price_schema>;
-export type RadrootsListingQuantity = z.infer<typeof radroots_listing_quantity_schema>;
-export type RadrootsListingProduct = z.infer<typeof radroots_listing_product_schema>;
-export type RadrootsListing = z.infer<typeof radroots_listing_schema>;
-export type RadrootsProfile = z.infer<typeof radroots_profile_schema>;
-export type RadrootsComment = z.infer<typeof radroots_comment_schema>;
-export type RadrootsReaction = z.infer<typeof radroots_reaction_schema>;
-export type RadrootsFollowProfile = z.infer<typeof radroots_follow_profile_schema>;
-export type RadrootsFollow = z.infer<typeof radroots_follow_schema>;
-\ No newline at end of file
diff --git a/bindings/ts/src/index.ts b/bindings/ts/src/index.ts
@@ -1,3 +0,0 @@
-export * from "./events/schema.js"
-export * from "./events/types.js"
-export * from "./types.js"
diff --git a/bindings/ts/src/types.ts b/bindings/ts/src/types.ts
@@ -1,95 +0,0 @@
-import { RadrootsComment, RadrootsFollow, RadrootsListing, RadrootsProfile, RadrootsReaction } from "./events/types.js";
-
-/*
- Generated by typeshare 1.13.2
-*/
-
-export interface RadrootsNostrEvent {
- id: string;
- author: string;
- created_at: number;
- kind: number;
- tags: string[][];
- content: string;
- sig: string;
-}
-
-export interface RadrootsCommentEventMetadata {
- id: string;
- author: string;
- published_at: number;
- comment: RadrootsComment;
-}
-
-export interface RadrootsCommentEventIndex {
- event: RadrootsNostrEvent;
- metadata: RadrootsCommentEventMetadata;
-}
-
-export interface RadrootsFollowEventMetadata {
- id: string;
- author: string;
- published_at: number;
- follow: RadrootsFollow;
-}
-
-export interface RadrootsFollowEventIndex {
- event: RadrootsNostrEvent;
- metadata: RadrootsFollowEventMetadata;
-}
-
-export interface RadrootsIndexShardMetadata {
- file: string;
- count: number;
- first_id: string;
- last_id: string;
- first_published_at: number;
- last_published_at: number;
- sha256: string;
-}
-
-export interface RadrootsIndexManifest {
- country: string;
- total: number;
- shard_size: number;
- first_published_at: number;
- last_published_at: number;
- shards: RadrootsIndexShardMetadata[];
-}
-
-export interface RadrootsListingEventMetadata {
- id: string;
- author: string;
- published_at: number;
- listing: RadrootsListing;
-}
-
-export interface RadrootsListingEventIndex {
- event: RadrootsNostrEvent;
- metadata: RadrootsListingEventMetadata;
-}
-
-export interface RadrootsProfileEventMetadata {
- id: string;
- author: string;
- published_at: number;
- profile: RadrootsProfile;
-}
-
-export interface RadrootsProfileEventIndex {
- event: RadrootsNostrEvent;
- metadata: RadrootsProfileEventMetadata;
-}
-
-export interface RadrootsReactionEventMetadata {
- id: string;
- author: string;
- published_at: number;
- reaction: RadrootsReaction;
-}
-
-export interface RadrootsReactionEventIndex {
- event: RadrootsNostrEvent;
- metadata: RadrootsReactionEventMetadata;
-}
-
diff --git a/bindings/ts/yarn.lock b/bindings/ts/yarn.lock
@@ -1,992 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@isaacs/balanced-match@^4.0.1":
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29"
- integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==
-
-"@isaacs/brace-expansion@^5.0.0":
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3"
- integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==
- dependencies:
- "@isaacs/balanced-match" "^4.0.1"
-
-"@isaacs/cliui@^8.0.2":
- version "8.0.2"
- resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
- integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
- dependencies:
- string-width "^5.1.2"
- string-width-cjs "npm:string-width@^4.2.0"
- strip-ansi "^7.0.1"
- strip-ansi-cjs "npm:strip-ansi@^6.0.1"
- wrap-ansi "^8.1.0"
- wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
-
-"@oclif/core@>=3.26.0":
- version "4.5.0"
- resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.5.0.tgz#0163f933098bfa52f86387f11900da1ad13235d3"
- integrity sha512-UYWyDFNKFyzgXVXO0DHfOvJ/8qpw4yPYe7fOHausDEVU44qjDr90ZnfYTljZPK8dhgMggxiZs9n+TFajnXRp7g==
- dependencies:
- ansi-escapes "^4.3.2"
- ansis "^3.17.0"
- clean-stack "^3.0.1"
- cli-spinners "^2.9.2"
- debug "^4.4.0"
- ejs "^3.1.10"
- get-package-type "^0.1.0"
- indent-string "^4.0.0"
- is-wsl "^2.2.0"
- lilconfig "^3.1.3"
- minimatch "^9.0.5"
- semver "^7.6.3"
- string-width "^4.2.3"
- supports-color "^8"
- tinyglobby "^0.2.14"
- widest-line "^3.1.0"
- wordwrap "^1.0.0"
- wrap-ansi "^7.0.0"
-
-"@typescript/vfs@^1.5.0":
- version "1.6.1"
- resolved "https://registry.yarnpkg.com/@typescript/vfs/-/vfs-1.6.1.tgz#fe7087d5a43715754f7ea9bf6e0b905176c9eebd"
- integrity sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==
- dependencies:
- debug "^4.1.1"
-
-ansi-escapes@^4.2.1, ansi-escapes@^4.3.2:
- version "4.3.2"
- resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
- integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
- dependencies:
- type-fest "^0.21.3"
-
-ansi-regex@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
- integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
-ansi-regex@^6.0.1:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654"
- integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==
-
-ansi-styles@^4.0.0, ansi-styles@^4.1.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
- integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
- dependencies:
- color-convert "^2.0.1"
-
-ansi-styles@^6.1.0:
- version "6.2.1"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
- integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
-
-ansis@^3.17.0:
- version "3.17.0"
- resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
- integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
-
-anymatch@~3.1.2:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
- integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
- dependencies:
- normalize-path "^3.0.0"
- picomatch "^2.0.4"
-
-async@^3.2.3:
- version "3.2.6"
- resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
- integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
-
-balanced-match@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
- integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-
-base64-js@^1.3.1:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
- integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-
-binary-extensions@^2.0.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
- integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
-
-bl@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
- integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
- dependencies:
- buffer "^5.5.0"
- inherits "^2.0.4"
- readable-stream "^3.4.0"
-
-brace-expansion@^1.1.7:
- version "1.1.12"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
- integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-brace-expansion@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
- integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
- dependencies:
- balanced-match "^1.0.0"
-
-braces@~3.0.2:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
- integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
- dependencies:
- fill-range "^7.1.1"
-
-buffer@^5.5.0:
- version "5.7.1"
- resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
- integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
- dependencies:
- base64-js "^1.3.1"
- ieee754 "^1.1.13"
-
-callsites@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
- integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
-
-case@^1.6.3:
- version "1.6.3"
- resolved "https://registry.yarnpkg.com/case/-/case-1.6.3.tgz#0a4386e3e9825351ca2e6216c60467ff5f1ea1c9"
- integrity sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==
-
-chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
- integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
- dependencies:
- ansi-styles "^4.1.0"
- supports-color "^7.1.0"
-
-chardet@^0.7.0:
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
- integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
-
-chokidar@^3.5.1:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
- integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
- dependencies:
- anymatch "~3.1.2"
- braces "~3.0.2"
- glob-parent "~5.1.2"
- is-binary-path "~2.1.0"
- is-glob "~4.0.1"
- normalize-path "~3.0.0"
- readdirp "~3.6.0"
- optionalDependencies:
- fsevents "~2.3.2"
-
-clean-stack@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-3.0.1.tgz#155bf0b2221bf5f4fba89528d24c5953f17fe3a8"
- integrity sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==
- dependencies:
- escape-string-regexp "4.0.0"
-
-cli-cursor@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
- integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
- dependencies:
- restore-cursor "^3.1.0"
-
-cli-spinners@^2.5.0, cli-spinners@^2.9.2:
- version "2.9.2"
- resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41"
- integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==
-
-cli-width@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
- integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
-
-clone@^1.0.2:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
- integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
-
-color-convert@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
- integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
- dependencies:
- color-name "~1.1.4"
-
-color-name@~1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
- integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-concat-map@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
- integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
-
-cross-spawn@^7.0.6:
- version "7.0.6"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
- integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
- dependencies:
- path-key "^3.1.0"
- shebang-command "^2.0.0"
- which "^2.0.1"
-
-debug@^4.1.1, debug@^4.2.0, debug@^4.4.0:
- version "4.4.1"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
- integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
- dependencies:
- ms "^2.1.3"
-
-defaults@^1.0.3:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a"
- integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==
- dependencies:
- clone "^1.0.2"
-
-eastasianwidth@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
- integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
-
-ejs@^3.1.10:
- version "3.1.10"
- resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
- integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==
- dependencies:
- jake "^10.8.5"
-
-emoji-regex@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
- integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
-
-emoji-regex@^9.2.2:
- version "9.2.2"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
- integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
-
-escape-string-regexp@4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
- integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
-
-escape-string-regexp@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
- integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
-
-esm@^3.2.25:
- version "3.2.25"
- resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
- integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
-
-external-editor@^3.0.3:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
- integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
- dependencies:
- chardet "^0.7.0"
- iconv-lite "^0.4.24"
- tmp "^0.0.33"
-
-fdir@^6.4.4:
- version "6.4.6"
- resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.6.tgz#2b268c0232697063111bbf3f64810a2a741ba281"
- integrity sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==
-
-figures@^3.0.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
- integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
- dependencies:
- escape-string-regexp "^1.0.5"
-
-filelist@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"
- integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==
- dependencies:
- minimatch "^5.0.1"
-
-fill-range@^7.1.1:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
- integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
- dependencies:
- to-regex-range "^5.0.1"
-
-foreground-child@^3.3.1:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
- integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
- dependencies:
- cross-spawn "^7.0.6"
- signal-exit "^4.0.1"
-
-fs-extra@^11.1.1:
- version "11.3.0"
- resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d"
- integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==
- dependencies:
- graceful-fs "^4.2.0"
- jsonfile "^6.0.1"
- universalify "^2.0.0"
-
-fsevents@~2.3.2:
- version "2.3.3"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
- integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
-
-get-package-type@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
- integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
-
-glob-parent@~5.1.2:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
- integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
- dependencies:
- is-glob "^4.0.1"
-
-glob@^11.0.0:
- version "11.0.3"
- resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6"
- integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==
- dependencies:
- foreground-child "^3.3.1"
- jackspeak "^4.1.1"
- minimatch "^10.0.3"
- minipass "^7.1.2"
- package-json-from-dist "^1.0.0"
- path-scurry "^2.0.0"
-
-graceful-fs@^4.1.6, graceful-fs@^4.2.0:
- version "4.2.11"
- resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
- integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
-
-has-flag@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
- integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
-
-iconv-lite@^0.4.24:
- version "0.4.24"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
- integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
- dependencies:
- safer-buffer ">= 2.1.2 < 3"
-
-ieee754@^1.1.13:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
- integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
-
-indent-string@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
- integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
-
-inherits@^2.0.3, inherits@^2.0.4:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
- integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
-inquirer@^8.2.0:
- version "8.2.6"
- resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562"
- integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==
- dependencies:
- ansi-escapes "^4.2.1"
- chalk "^4.1.1"
- cli-cursor "^3.1.0"
- cli-width "^3.0.0"
- external-editor "^3.0.3"
- figures "^3.0.0"
- lodash "^4.17.21"
- mute-stream "0.0.8"
- ora "^5.4.1"
- run-async "^2.4.0"
- rxjs "^7.5.5"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
- through "^2.3.6"
- wrap-ansi "^6.0.1"
-
-is-binary-path@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
- integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
- dependencies:
- binary-extensions "^2.0.0"
-
-is-docker@^2.0.0:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
- integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
-
-is-extglob@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
- integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
-
-is-fullwidth-code-point@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
- integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-
-is-glob@^4.0.1, is-glob@~4.0.1:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
- integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
- dependencies:
- is-extglob "^2.1.1"
-
-is-interactive@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
- integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==
-
-is-number@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
- integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
-
-is-observable@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-2.1.0.tgz#5c8d733a0b201c80dff7bb7c0df58c6a255c7c69"
- integrity sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==
-
-is-unicode-supported@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
- integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
-
-is-wsl@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
- integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
- dependencies:
- is-docker "^2.0.0"
-
-isexe@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
- integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
-
-jackspeak@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae"
- integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==
- dependencies:
- "@isaacs/cliui" "^8.0.2"
-
-jake@^10.8.5:
- version "10.9.2"
- resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f"
- integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==
- dependencies:
- async "^3.2.3"
- chalk "^4.0.2"
- filelist "^1.0.4"
- minimatch "^3.1.2"
-
-jsonfile@^6.0.1:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
- integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
- dependencies:
- universalify "^2.0.0"
- optionalDependencies:
- graceful-fs "^4.1.6"
-
-lilconfig@^3.1.3:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4"
- integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==
-
-lodash@^4.17.21:
- version "4.17.21"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
- integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
-log-symbols@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
- integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
- dependencies:
- chalk "^4.1.0"
- is-unicode-supported "^0.1.0"
-
-lru-cache@^11.0.0:
- version "11.1.0"
- resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.1.0.tgz#afafb060607108132dbc1cf8ae661afb69486117"
- integrity sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==
-
-mimic-fn@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
- integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
-
-minimatch@^10.0.3:
- version "10.0.3"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa"
- integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==
- dependencies:
- "@isaacs/brace-expansion" "^5.0.0"
-
-minimatch@^3.1.2:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
- integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
- dependencies:
- brace-expansion "^1.1.7"
-
-minimatch@^5.0.1:
- version "5.1.6"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
- integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
- dependencies:
- brace-expansion "^2.0.1"
-
-minimatch@^9.0.5:
- version "9.0.5"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
- integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
- dependencies:
- brace-expansion "^2.0.1"
-
-minipass@^7.1.2:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
- integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
-
-ms@^2.1.3:
- version "2.1.3"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
- integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-
-mute-stream@0.0.8:
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
- integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
-
-normalize-path@^3.0.0, normalize-path@~3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
- integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
-
-observable-fns@^0.6.1:
- version "0.6.1"
- resolved "https://registry.yarnpkg.com/observable-fns/-/observable-fns-0.6.1.tgz#636eae4fdd1132e88c0faf38d33658cc79d87e37"
- integrity sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==
-
-onetime@^5.1.0:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
- integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
- dependencies:
- mimic-fn "^2.1.0"
-
-ora@^5.4.0, ora@^5.4.1:
- version "5.4.1"
- resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18"
- integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==
- dependencies:
- bl "^4.1.0"
- chalk "^4.1.0"
- cli-cursor "^3.1.0"
- cli-spinners "^2.5.0"
- is-interactive "^1.0.0"
- is-unicode-supported "^0.1.0"
- log-symbols "^4.1.0"
- strip-ansi "^6.0.0"
- wcwidth "^1.0.1"
-
-os-tmpdir@~1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
- integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
-
-package-json-from-dist@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
- integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
-
-path-key@^3.1.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
- integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
-
-path-scurry@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580"
- integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==
- dependencies:
- lru-cache "^11.0.0"
- minipass "^7.1.2"
-
-picomatch@^2.0.4, picomatch@^2.2.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
- integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
-
-picomatch@^4.0.2:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
- integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
-
-prettier@3.0.3:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
- integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
-
-readable-stream@^3.4.0:
- version "3.6.2"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
- integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
- dependencies:
- inherits "^2.0.3"
- string_decoder "^1.1.1"
- util-deprecate "^1.0.1"
-
-readdirp@~3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
- integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
- dependencies:
- picomatch "^2.2.1"
-
-restore-cursor@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
- integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
- dependencies:
- onetime "^5.1.0"
- signal-exit "^3.0.2"
-
-rimraf@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.1.tgz#ffb8ad8844dd60332ab15f52bc104bc3ed71ea4e"
- integrity sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==
- dependencies:
- glob "^11.0.0"
- package-json-from-dist "^1.0.0"
-
-run-async@^2.4.0:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
- integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
-
-rxjs@^7.4.0, rxjs@^7.5.5:
- version "7.8.2"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b"
- integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==
- dependencies:
- tslib "^2.1.0"
-
-safe-buffer@~5.2.0:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
- integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
-
-"safer-buffer@>= 2.1.2 < 3":
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
- integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-
-semver@^7.6.3:
- version "7.7.2"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
- integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
-
-shebang-command@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
- integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
- dependencies:
- shebang-regex "^3.0.0"
-
-shebang-regex@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
- integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
-
-signal-exit@^3.0.2:
- version "3.0.7"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
- integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
-
-signal-exit@^4.0.1:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
- integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
-
-slash@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
- integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
-
-"string-width-cjs@npm:string-width@^4.2.0":
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.3:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-string-width@^5.0.1, string-width@^5.1.2:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
- integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
- dependencies:
- eastasianwidth "^0.2.0"
- emoji-regex "^9.2.2"
- strip-ansi "^7.0.1"
-
-string_decoder@^1.1.1:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
- integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
- dependencies:
- safe-buffer "~5.2.0"
-
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-strip-ansi@^7.0.1:
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
- integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
- dependencies:
- ansi-regex "^6.0.1"
-
-supports-color@^7.1.0:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
- integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
- dependencies:
- has-flag "^4.0.0"
-
-supports-color@^8:
- version "8.1.1"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
- integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
- dependencies:
- has-flag "^4.0.0"
-
-threads@^1.7.0:
- version "1.7.0"
- resolved "https://registry.yarnpkg.com/threads/-/threads-1.7.0.tgz#d9e9627bfc1ef22ada3b733c2e7558bbe78e589c"
- integrity sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==
- dependencies:
- callsites "^3.1.0"
- debug "^4.2.0"
- is-observable "^2.1.0"
- observable-fns "^0.6.1"
- optionalDependencies:
- tiny-worker ">= 2"
-
-through@^2.3.6:
- version "2.3.8"
- resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
- integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
-
-"tiny-worker@>= 2":
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/tiny-worker/-/tiny-worker-2.3.0.tgz#715ae34304c757a9af573ae9a8e3967177e6011e"
- integrity sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==
- dependencies:
- esm "^3.2.25"
-
-tinyglobby@^0.2.14:
- version "0.2.14"
- resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d"
- integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==
- dependencies:
- fdir "^6.4.4"
- picomatch "^4.0.2"
-
-tmp@^0.0.33:
- version "0.0.33"
- resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
- integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
- dependencies:
- os-tmpdir "~1.0.2"
-
-to-regex-range@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
- integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
- dependencies:
- is-number "^7.0.0"
-
-ts-to-zod@^3.15.0:
- version "3.15.0"
- resolved "https://registry.yarnpkg.com/ts-to-zod/-/ts-to-zod-3.15.0.tgz#3784780f2c52e69d5c48199d3e18f83aec5c5109"
- integrity sha512-Lu5ITqD8xCIo4JZp4Cg3iSK3J2x3TGwwuDtNHfAIlx1mXWKClRdzqV+x6CFEzhKtJlZzhyvJIqg7DzrWfsdVSg==
- dependencies:
- "@oclif/core" ">=3.26.0"
- "@typescript/vfs" "^1.5.0"
- case "^1.6.3"
- chokidar "^3.5.1"
- fs-extra "^11.1.1"
- inquirer "^8.2.0"
- lodash "^4.17.21"
- ora "^5.4.0"
- prettier "3.0.3"
- rxjs "^7.4.0"
- slash "^3.0.0"
- threads "^1.7.0"
- tslib "^2.3.1"
- tsutils "^3.21.0"
- typescript "^5.2.2"
- zod "^3.23.8"
-
-tslib@^1.8.1:
- version "1.14.1"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
- integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-
-tslib@^2.1.0, tslib@^2.3.1:
- version "2.8.1"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
- integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
-
-tsutils@^3.21.0:
- version "3.21.0"
- resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
- integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
- dependencies:
- tslib "^1.8.1"
-
-type-fest@^0.21.3:
- version "0.21.3"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
- integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
-
-typescript@^5.2.2:
- version "5.8.3"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
- integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
-
-universalify@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
- integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
-
-util-deprecate@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
- integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-
-wcwidth@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
- integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==
- dependencies:
- defaults "^1.0.3"
-
-which@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
- integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
- dependencies:
- isexe "^2.0.0"
-
-widest-line@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
- integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==
- dependencies:
- string-width "^4.0.0"
-
-wordwrap@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
- integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
-
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrap-ansi@^6.0.1:
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
- integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrap-ansi@^8.1.0:
- version "8.1.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
- integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
- dependencies:
- ansi-styles "^6.1.0"
- string-width "^5.0.1"
- strip-ansi "^7.0.1"
-
-zod@^3.23.8:
- version "3.25.76"
- resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
- integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "radroots-core"
+version = "0.1.0"
+authors = ["Radroots Authors"]
+license = "AGPLv3"
+edition = "2021"
+
+[features]
+default = ["std", "serde", "typeshare"]
+std = []
+serde = ["dep:serde", "rust_decimal/serde"]
+typeshare = ["dep:typeshare"]
+
+[dependencies]
+rust_decimal = { version = "1", default-features = false }
+rust_decimal_macros = "1"
+serde = { version = "1", default-features = false, features = ["derive"], optional = true }
+typeshare = { version = "1", optional = true }
diff --git a/crates/core/src/currency.rs b/crates/core/src/currency.rs
@@ -0,0 +1,120 @@
+use core::fmt;
+use core::str::FromStr;
+
+#[cfg(feature = "serde")]
+use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
+
+#[typeshare::typeshare]
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RadrootsCoreCurrency([u8; 3]);
+
+impl RadrootsCoreCurrency {
+ #[inline]
+ pub const fn from_const(bytes: [u8; 3]) -> Self {
+ Self(bytes)
+ }
+
+ #[inline]
+ pub fn from_str_upper(s: &str) -> Result<Self, RadrootsCoreCurrencyParseError> {
+ let b = s.as_bytes();
+ if b.len() != 3 || b.iter().any(|c| !c.is_ascii_uppercase()) {
+ return Err(RadrootsCoreCurrencyParseError::InvalidFormat);
+ }
+ Ok(Self([b[0], b[1], b[2]]))
+ }
+
+ #[inline]
+ pub fn as_str(&self) -> &str {
+ core::str::from_utf8(&self.0).expect("currency bytes are validated on construction")
+ }
+
+ pub const USD: RadrootsCoreCurrency = RadrootsCoreCurrency(*b"USD");
+ pub const EUR: RadrootsCoreCurrency = RadrootsCoreCurrency(*b"EUR");
+ pub const GBP: RadrootsCoreCurrency = RadrootsCoreCurrency(*b"GBP");
+ pub const JPY: RadrootsCoreCurrency = RadrootsCoreCurrency(*b"JPY");
+ pub const CAD: RadrootsCoreCurrency = RadrootsCoreCurrency(*b"CAD");
+ pub const AUD: RadrootsCoreCurrency = RadrootsCoreCurrency(*b"AUD");
+
+ #[inline]
+ pub const fn minor_unit_exponent(&self) -> u32 {
+ match self.0 {
+ [b'J', b'P', b'Y'] | [b'K', b'R', b'W'] | [b'V', b'N', b'D'] => 0,
+ [b'B', b'H', b'D']
+ | [b'I', b'Q', b'D']
+ | [b'J', b'O', b'D']
+ | [b'K', b'W', b'D']
+ | [b'L', b'Y', b'D']
+ | [b'O', b'M', b'R']
+ | [b'T', b'N', b'D'] => 3,
+ _ => 2,
+ }
+ }
+}
+
+impl fmt::Debug for RadrootsCoreCurrency {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_tuple("RadrootsCoreCurrency")
+ .field(&self.as_str())
+ .finish()
+ }
+}
+
+impl fmt::Display for RadrootsCoreCurrency {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(self.as_str())
+ }
+}
+
+impl TryFrom<&str> for RadrootsCoreCurrency {
+ type Error = RadrootsCoreCurrencyParseError;
+ fn try_from(s: &str) -> Result<Self, Self::Error> {
+ s.parse()
+ }
+}
+
+impl FromStr for RadrootsCoreCurrency {
+ type Err = RadrootsCoreCurrencyParseError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let s = s.trim();
+ if s.len() != 3 || !s.chars().all(|c| c.is_ascii_alphabetic()) {
+ return Err(RadrootsCoreCurrencyParseError::InvalidFormat);
+ }
+ let upper = s.to_ascii_uppercase();
+ Self::from_str_upper(&upper)
+ }
+}
+
+#[non_exhaustive]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsCoreCurrencyParseError {
+ InvalidFormat,
+}
+
+impl fmt::Display for RadrootsCoreCurrencyParseError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ RadrootsCoreCurrencyParseError::InvalidFormat => {
+ write!(f, "currency must be a 3-letter code")
+ }
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsCoreCurrencyParseError {}
+
+#[cfg(feature = "serde")]
+impl Serialize for RadrootsCoreCurrency {
+ fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
+ ser.serialize_str(self.as_str())
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for RadrootsCoreCurrency {
+ fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
+ let s = String::deserialize(de)?;
+ s.parse().map_err(D::Error::custom)
+ }
+}
diff --git a/crates/core/src/decimal.rs b/crates/core/src/decimal.rs
@@ -0,0 +1,149 @@
+use core::fmt;
+use core::ops::{Add, Div, Mul, Sub};
+use core::str::FromStr;
+use rust_decimal::prelude::ToPrimitive;
+use rust_decimal::Decimal;
+
+#[cfg(feature = "serde")]
+use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
+
+#[typeshare::typeshare]
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
+pub struct RadrootsCoreDecimal(pub Decimal);
+
+impl RadrootsCoreDecimal {
+ pub const ZERO: Self = Self(Decimal::ZERO);
+ pub const ONE: Self = Self(Decimal::ONE);
+
+ #[inline]
+ pub fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+ #[inline]
+ pub fn is_sign_negative(&self) -> bool {
+ self.0.is_sign_negative()
+ }
+ #[inline]
+ pub fn rescale(&mut self, scale: u32) {
+ self.0.rescale(scale);
+ }
+ #[inline]
+ pub fn normalize(&self) -> Decimal {
+ self.0.normalize()
+ }
+
+ #[inline]
+ pub fn scale(&self) -> u32 {
+ self.0.scale()
+ }
+
+ #[inline]
+ pub fn from_str_exact(s: &str) -> Result<Self, rust_decimal::Error> {
+ Decimal::from_str_exact(s).map(Self)
+ }
+
+ #[inline]
+ pub fn from_f64_display(n: f64) -> Result<Self, rust_decimal::Error> {
+ let s = format!("{:.17}", n);
+ Decimal::from_str(&s).map(Self)
+ }
+ #[inline]
+ pub fn to_f64_lossy(&self) -> f64 {
+ self.normalize().to_string().parse::<f64>().unwrap_or(0.0)
+ }
+
+ #[inline]
+ pub fn to_u64_exact(&self) -> Option<u64> {
+ if self.0.fract().is_zero() {
+ self.0.to_u64()
+ } else {
+ None
+ }
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for RadrootsCoreDecimal {
+ fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+ serializer.serialize_str(&self.normalize().to_string())
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for RadrootsCoreDecimal {
+ fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ let s = String::deserialize(deserializer)?;
+ Decimal::from_str(&s)
+ .map(RadrootsCoreDecimal)
+ .map_err(D::Error::custom)
+ }
+}
+
+impl fmt::Display for RadrootsCoreDecimal {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(&self.normalize().to_string())
+ }
+}
+
+impl From<Decimal> for RadrootsCoreDecimal {
+ fn from(d: Decimal) -> Self {
+ Self(d)
+ }
+}
+impl From<RadrootsCoreDecimal> for Decimal {
+ fn from(d: RadrootsCoreDecimal) -> Self {
+ d.0
+ }
+}
+impl From<u32> for RadrootsCoreDecimal {
+ fn from(v: u32) -> Self {
+ Self(Decimal::from(v))
+ }
+}
+impl From<i32> for RadrootsCoreDecimal {
+ fn from(v: i32) -> Self {
+ Self(Decimal::from(v))
+ }
+}
+impl From<u64> for RadrootsCoreDecimal {
+ fn from(v: u64) -> Self {
+ Self(Decimal::from(v))
+ }
+}
+impl From<i64> for RadrootsCoreDecimal {
+ fn from(v: i64) -> Self {
+ Self(Decimal::from(v))
+ }
+}
+
+impl Add for RadrootsCoreDecimal {
+ type Output = Self;
+ fn add(self, rhs: Self) -> Self {
+ Self(self.0 + rhs.0)
+ }
+}
+impl Sub for RadrootsCoreDecimal {
+ type Output = Self;
+ fn sub(self, rhs: Self) -> Self {
+ Self(self.0 - rhs.0)
+ }
+}
+impl Mul for RadrootsCoreDecimal {
+ type Output = Self;
+ fn mul(self, rhs: Self) -> Self {
+ Self(self.0 * rhs.0)
+ }
+}
+impl Div for RadrootsCoreDecimal {
+ type Output = Self;
+ fn div(self, rhs: Self) -> Self {
+ Self(self.0 / rhs.0)
+ }
+}
+
+impl FromStr for RadrootsCoreDecimal {
+ type Err = rust_decimal::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Decimal::from_str(s).map(RadrootsCoreDecimal)
+ }
+}
diff --git a/crates/core/src/discount.rs b/crates/core/src/discount.rs
@@ -0,0 +1,56 @@
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "snake_case", tag = "kind", content = "amount")]
+pub enum RadrootsCoreDiscountValue {
+ Money(crate::RadrootsCoreMoney),
+ Percent(crate::RadrootsCorePercent),
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "snake_case", tag = "kind", content = "amount")]
+pub enum RadrootsCoreDiscount {
+ QuantityThreshold {
+ ref_key: Option<String>,
+ threshold: crate::RadrootsCoreQuantity,
+ value: crate::RadrootsCoreMoney,
+ },
+ MassThreshold {
+ threshold: crate::RadrootsCoreQuantity,
+ value: crate::RadrootsCoreMoney,
+ },
+ SubtotalThreshold {
+ threshold: crate::RadrootsCoreMoney,
+ value: RadrootsCoreDiscountValue,
+ },
+ TotalThreshold {
+ total_min: crate::RadrootsCoreMoney,
+ value: crate::RadrootsCorePercent,
+ },
+}
+
+impl RadrootsCoreDiscount {
+ pub fn is_non_negative(&self) -> bool {
+ match self {
+ RadrootsCoreDiscount::QuantityThreshold {
+ threshold, value, ..
+ } => !threshold.amount.is_sign_negative() && !value.amount.is_sign_negative(),
+ RadrootsCoreDiscount::MassThreshold { threshold, value } => {
+ !threshold.amount.is_sign_negative() && !value.amount.is_sign_negative()
+ }
+ RadrootsCoreDiscount::SubtotalThreshold { threshold, value } => {
+ let money_ok = !threshold.amount.is_sign_negative();
+ let val_ok = match value {
+ RadrootsCoreDiscountValue::Money(m) => !m.amount.is_sign_negative(),
+ RadrootsCoreDiscountValue::Percent(p) => !p.value.is_sign_negative(),
+ };
+ money_ok && val_ok
+ }
+ RadrootsCoreDiscount::TotalThreshold { total_min, value } => {
+ !total_min.amount.is_sign_negative() && !value.value.is_sign_negative()
+ }
+ }
+ }
+}
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
@@ -0,0 +1,24 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+#![forbid(unsafe_code)]
+#[cfg(not(feature = "std"))]
+extern crate alloc;
+
+pub mod currency;
+pub mod decimal;
+pub mod discount;
+pub mod money;
+pub mod percent;
+pub mod quantity;
+pub mod quantity_price;
+#[cfg(feature = "serde")]
+pub mod serde_ext;
+pub mod unit;
+
+pub use currency::{RadrootsCoreCurrency, RadrootsCoreCurrencyParseError};
+pub use decimal::RadrootsCoreDecimal;
+pub use discount::{RadrootsCoreDiscount, RadrootsCoreDiscountValue};
+pub use money::{RadrootsCoreMoney, RadrootsCoreMoneyInvariantError};
+pub use percent::{RadrootsCorePercent, RadrootsCorePercentParseError};
+pub use quantity::{RadrootsCoreQuantity, RadrootsCoreQuantityInvariantError};
+pub use quantity_price::{RadrootsCoreQuantityPrice, RadrootsCoreQuantityPriceOps};
+pub use unit::{RadrootsCoreUnit, RadrootsCoreUnitParseError};
diff --git a/crates/core/src/money.rs b/crates/core/src/money.rs
@@ -0,0 +1,225 @@
+use core::fmt;
+use rust_decimal::prelude::ToPrimitive;
+use rust_decimal::Decimal;
+use rust_decimal::RoundingStrategy;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsCoreMoney {
+ pub amount: crate::RadrootsCoreDecimal,
+ pub currency: crate::RadrootsCoreCurrency,
+}
+
+#[non_exhaustive]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsCoreMoneyInvariantError {
+ NegativeAmount,
+ NotWholeMinorUnits,
+ AmountOverflow,
+ CurrencyMismatch,
+}
+
+impl fmt::Display for RadrootsCoreMoneyInvariantError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::NegativeAmount => write!(f, "money amount must be ≥ 0"),
+ Self::NotWholeMinorUnits => write!(f, "money not a whole number of minor units"),
+ Self::AmountOverflow => write!(f, "money minor-unit conversion overflow"),
+ Self::CurrencyMismatch => write!(f, "money currency mismatch"),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsCoreMoneyInvariantError {}
+
+impl RadrootsCoreMoney {
+ #[inline]
+ pub fn new(amount: crate::RadrootsCoreDecimal, currency: crate::RadrootsCoreCurrency) -> Self {
+ Self { amount, currency }
+ }
+
+ #[inline]
+ pub fn zero(currency: crate::RadrootsCoreCurrency) -> Self {
+ Self {
+ amount: crate::RadrootsCoreDecimal::ZERO,
+ currency,
+ }
+ }
+
+ #[inline]
+ pub fn is_zero(&self) -> bool {
+ self.amount.is_zero()
+ }
+
+ #[inline]
+ pub fn ensure_non_negative(&self) -> Result<(), RadrootsCoreMoneyInvariantError> {
+ if self.amount.is_sign_negative() {
+ return Err(RadrootsCoreMoneyInvariantError::NegativeAmount);
+ }
+ Ok(())
+ }
+
+ #[inline]
+ pub fn quantize_to_currency(mut self) -> Self {
+ let e = self.currency.minor_unit_exponent();
+ self.amount.0 = self
+ .amount
+ .0
+ .round_dp_with_strategy(e, RoundingStrategy::MidpointAwayFromZero);
+ self
+ }
+
+ #[inline]
+ pub fn with_scale(mut self, scale: u32) -> Self {
+ self.amount.rescale(scale);
+ self
+ }
+
+ #[inline]
+ pub fn checked_add(&self, rhs: &Self) -> Result<Self, RadrootsCoreMoneyInvariantError> {
+ if self.currency != rhs.currency {
+ return Err(RadrootsCoreMoneyInvariantError::CurrencyMismatch);
+ }
+ Ok(Self::new(self.amount + rhs.amount, self.currency))
+ }
+
+ #[inline]
+ pub fn checked_sub(&self, rhs: &Self) -> Result<Self, RadrootsCoreMoneyInvariantError> {
+ if self.currency != rhs.currency {
+ return Err(RadrootsCoreMoneyInvariantError::CurrencyMismatch);
+ }
+ Ok(Self::new(self.amount - rhs.amount, self.currency))
+ }
+
+ #[inline]
+ pub fn mul_decimal(&self, factor: crate::RadrootsCoreDecimal) -> Self {
+ Self::new(self.amount * factor, self.currency)
+ }
+
+ #[inline]
+ pub fn div_decimal(&self, divisor: crate::RadrootsCoreDecimal) -> Self {
+ Self::new(self.amount / divisor, self.currency)
+ }
+
+ #[inline]
+ pub fn from_minor_units_u64(amount_minor: u64, currency: crate::RadrootsCoreCurrency) -> Self {
+ let e = currency.minor_unit_exponent();
+ let major = Decimal::from_i128_with_scale(amount_minor as i128, e);
+ Self::new(crate::RadrootsCoreDecimal(major), currency)
+ }
+
+ #[inline]
+ pub fn from_minor_units_u32(amount_minor: u32, currency: crate::RadrootsCoreCurrency) -> Self {
+ Self::from_minor_units_u64(amount_minor as u64, currency)
+ }
+
+ #[inline]
+ fn pow10(e: u32) -> Decimal {
+ match e {
+ 0 => Decimal::ONE,
+ 1 => Decimal::from(10u32),
+ 2 => Decimal::from(100u32),
+ 3 => Decimal::from(1_000u32),
+ _ => {
+ let p = 10u128.pow(e.min(38));
+ Decimal::from(p)
+ }
+ }
+ }
+
+ #[inline]
+ pub fn to_minor_units_u64_exact(&self) -> Result<u64, RadrootsCoreMoneyInvariantError> {
+ let e = self.currency.minor_unit_exponent();
+ let scaled = self
+ .amount
+ .0
+ .round_dp_with_strategy(e, RoundingStrategy::MidpointAwayFromZero);
+ let as_minor = scaled * Self::pow10(e);
+
+ if !as_minor.fract().is_zero() {
+ return Err(RadrootsCoreMoneyInvariantError::NotWholeMinorUnits);
+ }
+ as_minor
+ .to_u64()
+ .ok_or(RadrootsCoreMoneyInvariantError::AmountOverflow)
+ }
+
+ #[inline]
+ pub fn to_minor_units_u64_rounded(
+ &self,
+ strategy: RoundingStrategy,
+ ) -> Result<u64, RadrootsCoreMoneyInvariantError> {
+ let e = self.currency.minor_unit_exponent();
+ let scaled = self.amount.0.round_dp_with_strategy(e, strategy);
+ let as_minor = scaled * Self::pow10(e);
+ if !as_minor.fract().is_zero() {
+ return Err(RadrootsCoreMoneyInvariantError::NotWholeMinorUnits);
+ }
+ as_minor
+ .to_u64()
+ .ok_or(RadrootsCoreMoneyInvariantError::AmountOverflow)
+ }
+
+ #[inline]
+ pub fn to_minor_units_u32_exact(&self) -> Result<u32, RadrootsCoreMoneyInvariantError> {
+ let v = self.to_minor_units_u64_exact()?;
+ u32::try_from(v).map_err(|_| RadrootsCoreMoneyInvariantError::AmountOverflow)
+ }
+
+ #[inline]
+ pub fn to_minor_units_u32_rounded(
+ &self,
+ strategy: RoundingStrategy,
+ ) -> Result<u32, RadrootsCoreMoneyInvariantError> {
+ let v = self.to_minor_units_u64_rounded(strategy)?;
+ u32::try_from(v).map_err(|_| RadrootsCoreMoneyInvariantError::AmountOverflow)
+ }
+}
+
+impl fmt::Display for RadrootsCoreMoney {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{} {}", self.amount, self.currency)
+ }
+}
+
+use core::ops::{Add, Div, Mul, Sub};
+
+impl Add for RadrootsCoreMoney {
+ type Output = Self;
+ fn add(self, rhs: Self) -> Self {
+ assert_eq!(
+ self.currency, rhs.currency,
+ "money currency mismatch: {} vs {}",
+ self.currency, rhs.currency
+ );
+ Self::new(self.amount + rhs.amount, self.currency)
+ }
+}
+
+impl Sub for RadrootsCoreMoney {
+ type Output = Self;
+ fn sub(self, rhs: Self) -> Self {
+ assert_eq!(
+ self.currency, rhs.currency,
+ "money currency mismatch: {} vs {}",
+ self.currency, rhs.currency
+ );
+ Self::new(self.amount - rhs.amount, self.currency)
+ }
+}
+
+impl Mul<crate::RadrootsCoreDecimal> for RadrootsCoreMoney {
+ type Output = Self;
+ fn mul(self, rhs: crate::RadrootsCoreDecimal) -> Self {
+ self.mul_decimal(rhs)
+ }
+}
+
+impl Div<crate::RadrootsCoreDecimal> for RadrootsCoreMoney {
+ type Output = Self;
+ fn div(self, rhs: crate::RadrootsCoreDecimal) -> Self {
+ self.div_decimal(rhs)
+ }
+}
diff --git a/crates/core/src/percent.rs b/crates/core/src/percent.rs
@@ -0,0 +1,78 @@
+use core::fmt;
+use core::str::FromStr;
+
+use crate::money::RadrootsCoreMoney;
+use crate::RadrootsCoreDecimal;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsCorePercent {
+ #[cfg_attr(feature = "serde", serde(with = "crate::serde_ext::decimal_str"))]
+ pub value: RadrootsCoreDecimal,
+}
+
+impl RadrootsCorePercent {
+ #[inline]
+ pub fn new(value: RadrootsCoreDecimal) -> Self {
+ Self { value }
+ }
+
+ #[inline]
+ pub fn from_ratio(ratio_0_to_1: RadrootsCoreDecimal) -> Self {
+ Self {
+ value: ratio_0_to_1 * RadrootsCoreDecimal::from(100u32),
+ }
+ }
+
+ #[inline]
+ pub fn to_ratio(&self) -> RadrootsCoreDecimal {
+ self.value / RadrootsCoreDecimal::from(100u32)
+ }
+
+ #[inline]
+ pub fn of_money(&self, base: &RadrootsCoreMoney) -> RadrootsCoreMoney {
+ base.mul_decimal(self.to_ratio())
+ }
+
+ #[inline]
+ pub fn of_money_quantized(&self, base: &RadrootsCoreMoney) -> RadrootsCoreMoney {
+ base.mul_decimal(self.to_ratio()).quantize_to_currency()
+ }
+}
+
+impl fmt::Display for RadrootsCorePercent {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}%", self.value.normalize())
+ }
+}
+
+impl FromStr for RadrootsCorePercent {
+ type Err = RadrootsCorePercentParseError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let trimmed = s.trim_end();
+ let no_pct = trimmed.strip_suffix('%').unwrap_or(trimmed).trim();
+ let dec = no_pct
+ .parse::<RadrootsCoreDecimal>()
+ .map_err(|_| RadrootsCorePercentParseError::InvalidNumber)?;
+ Ok(RadrootsCorePercent::new(dec))
+ }
+}
+
+#[non_exhaustive]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsCorePercentParseError {
+ InvalidNumber,
+}
+
+impl fmt::Display for RadrootsCorePercentParseError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ RadrootsCorePercentParseError::InvalidNumber => write!(f, "invalid percent string"),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsCorePercentParseError {}
diff --git a/crates/core/src/quantity.rs b/crates/core/src/quantity.rs
@@ -0,0 +1,235 @@
+use core::fmt;
+
+use crate::unit::RadrootsCoreUnit;
+use crate::RadrootsCoreDecimal;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsCoreQuantity {
+ #[cfg_attr(feature = "serde", serde(with = "crate::serde_ext::decimal_str"))]
+ pub amount: RadrootsCoreDecimal,
+ pub unit: RadrootsCoreUnit,
+ #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
+ pub label: Option<String>,
+}
+
+impl RadrootsCoreQuantity {
+ #[inline]
+ pub fn new(amount: RadrootsCoreDecimal, unit: RadrootsCoreUnit) -> Self {
+ Self {
+ amount,
+ unit,
+ label: None,
+ }
+ }
+
+ #[inline]
+ pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
+ self.label = Some(label.into());
+ self
+ }
+
+ #[inline]
+ pub fn with_optional_label<S: Into<String>>(mut self, label: Option<S>) -> Self {
+ self.label = label.map(|s| s.into());
+ self
+ }
+
+ #[inline]
+ pub fn clear_label(mut self) -> Self {
+ self.label = None;
+ self
+ }
+
+ #[inline]
+ pub fn zero(unit: RadrootsCoreUnit) -> Self {
+ Self {
+ amount: RadrootsCoreDecimal::ZERO,
+ unit,
+ label: None,
+ }
+ }
+
+ #[inline]
+ pub fn is_zero(&self) -> bool {
+ self.amount.is_zero()
+ }
+
+ #[inline]
+ pub fn ensure_non_negative(&self) -> Result<(), RadrootsCoreQuantityInvariantError> {
+ if self.amount.is_sign_negative() {
+ return Err(RadrootsCoreQuantityInvariantError::NegativeAmount);
+ }
+ Ok(())
+ }
+
+ #[inline]
+ pub fn with_scale(mut self, scale: u32) -> Self {
+ self.amount.rescale(scale);
+ self
+ }
+
+ #[inline]
+ pub fn try_add(
+ &self,
+ rhs: &RadrootsCoreQuantity,
+ ) -> Result<RadrootsCoreQuantity, RadrootsCoreQuantityInvariantError> {
+ if self.unit != rhs.unit {
+ return Err(RadrootsCoreQuantityInvariantError::UnitMismatch);
+ }
+ Ok(RadrootsCoreQuantity {
+ amount: self.amount + rhs.amount,
+ unit: self.unit,
+ label: self.label.clone(),
+ })
+ }
+
+ #[inline]
+ pub fn try_sub(
+ &self,
+ rhs: &RadrootsCoreQuantity,
+ ) -> Result<RadrootsCoreQuantity, RadrootsCoreQuantityInvariantError> {
+ if self.unit != rhs.unit {
+ return Err(RadrootsCoreQuantityInvariantError::UnitMismatch);
+ }
+ Ok(RadrootsCoreQuantity {
+ amount: self.amount - rhs.amount,
+ unit: self.unit,
+ label: self.label.clone(),
+ })
+ }
+
+ pub fn checked_add(&self, rhs: &RadrootsCoreQuantity) -> Option<RadrootsCoreQuantity> {
+ if self.unit == rhs.unit {
+ Some(RadrootsCoreQuantity {
+ amount: self.amount + rhs.amount,
+ unit: self.unit,
+ label: self.label.clone(),
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn checked_sub(&self, rhs: &RadrootsCoreQuantity) -> Option<RadrootsCoreQuantity> {
+ if self.unit == rhs.unit {
+ Some(RadrootsCoreQuantity {
+ amount: self.amount - rhs.amount,
+ unit: self.unit,
+ label: self.label.clone(),
+ })
+ } else {
+ None
+ }
+ }
+
+ #[inline]
+ pub fn mul_decimal(&self, factor: RadrootsCoreDecimal) -> RadrootsCoreQuantity {
+ RadrootsCoreQuantity {
+ amount: self.amount * factor,
+ unit: self.unit,
+ label: self.label.clone(),
+ }
+ }
+
+ #[inline]
+ pub fn div_decimal(&self, divisor: RadrootsCoreDecimal) -> RadrootsCoreQuantity {
+ RadrootsCoreQuantity {
+ amount: self.amount / divisor,
+ unit: self.unit,
+ label: self.label.clone(),
+ }
+ }
+}
+
+impl fmt::Display for RadrootsCoreQuantity {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{} {}", self.amount.normalize(), self.unit)?;
+ if let Some(label) = &self.label {
+ write!(f, " ({label})")?;
+ }
+ Ok(())
+ }
+}
+
+#[non_exhaustive]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsCoreQuantityInvariantError {
+ NegativeAmount,
+ UnitMismatch,
+}
+
+impl fmt::Display for RadrootsCoreQuantityInvariantError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ RadrootsCoreQuantityInvariantError::NegativeAmount => {
+ write!(f, "quantity amount must be ≥ 0")
+ }
+ RadrootsCoreQuantityInvariantError::UnitMismatch => {
+ write!(f, "quantity unit mismatch")
+ }
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsCoreQuantityInvariantError {}
+
+use core::ops::{Add, Div, Mul, Sub};
+
+impl Add for RadrootsCoreQuantity {
+ type Output = RadrootsCoreQuantity;
+ fn add(self, rhs: RadrootsCoreQuantity) -> RadrootsCoreQuantity {
+ assert!(
+ self.unit == rhs.unit,
+ "quantity unit mismatch: {} vs {}",
+ self.unit,
+ rhs.unit
+ );
+ RadrootsCoreQuantity {
+ amount: self.amount + rhs.amount,
+ unit: self.unit,
+ label: self.label,
+ }
+ }
+}
+
+impl Sub for RadrootsCoreQuantity {
+ type Output = RadrootsCoreQuantity;
+ fn sub(self, rhs: RadrootsCoreQuantity) -> RadrootsCoreQuantity {
+ assert!(
+ self.unit == rhs.unit,
+ "quantity unit mismatch: {} vs {}",
+ self.unit,
+ rhs.unit
+ );
+ RadrootsCoreQuantity {
+ amount: self.amount - rhs.amount,
+ unit: self.unit,
+ label: self.label,
+ }
+ }
+}
+
+impl Mul<RadrootsCoreDecimal> for RadrootsCoreQuantity {
+ type Output = RadrootsCoreQuantity;
+ fn mul(self, rhs: RadrootsCoreDecimal) -> RadrootsCoreQuantity {
+ RadrootsCoreQuantity {
+ amount: self.amount * rhs,
+ unit: self.unit,
+ label: self.label,
+ }
+ }
+}
+
+impl Div<RadrootsCoreDecimal> for RadrootsCoreQuantity {
+ type Output = RadrootsCoreQuantity;
+ fn div(self, rhs: RadrootsCoreDecimal) -> RadrootsCoreQuantity {
+ RadrootsCoreQuantity {
+ amount: self.amount / rhs,
+ unit: self.unit,
+ label: self.label,
+ }
+ }
+}
diff --git a/crates/core/src/quantity_price.rs b/crates/core/src/quantity_price.rs
@@ -0,0 +1,135 @@
+use crate::{RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreUnit};
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsCoreQuantityPrice {
+ #[cfg_attr(feature = "serde", serde(alias = "money", alias = "price"))]
+ pub amount: RadrootsCoreMoney,
+ #[cfg_attr(feature = "serde", serde(alias = "per", alias = "quantity"))]
+ pub quantity: RadrootsCoreQuantity,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsCoreQuantityPriceError {
+ PerQuantityZero,
+ UnitMismatch {
+ have: RadrootsCoreUnit,
+ want: RadrootsCoreUnit,
+ },
+ NonConvertibleUnits {
+ from: RadrootsCoreUnit,
+ to: RadrootsCoreUnit,
+ },
+}
+
+pub trait RadrootsCoreQuantityPriceOps {
+ #[must_use]
+ fn cost_for(&self, qty: &RadrootsCoreQuantity) -> RadrootsCoreMoney;
+
+ #[must_use]
+ fn cost_for_rounded(&self, qty: &RadrootsCoreQuantity) -> RadrootsCoreMoney;
+
+ #[must_use]
+ fn cost_for_with_quantized_price(&self, qty: &RadrootsCoreQuantity) -> RadrootsCoreMoney;
+
+ fn try_cost_for(
+ &self,
+ qty: &RadrootsCoreQuantity,
+ ) -> Result<RadrootsCoreMoney, RadrootsCoreQuantityPriceError>;
+
+ fn try_cost_for_rounded(
+ &self,
+ qty: &RadrootsCoreQuantity,
+ ) -> Result<RadrootsCoreMoney, RadrootsCoreQuantityPriceError>;
+}
+
+impl RadrootsCoreQuantityPrice {
+ #[inline]
+ pub fn new(amount: RadrootsCoreMoney, quantity: RadrootsCoreQuantity) -> Self {
+ Self { amount, quantity }
+ }
+
+ #[inline]
+ pub fn try_cost_for_amount_in(
+ &self,
+ amount: RadrootsCoreDecimal,
+ unit: RadrootsCoreUnit,
+ ) -> Result<RadrootsCoreMoney, RadrootsCoreQuantityPriceError> {
+ use crate::unit::convert_mass_decimal;
+
+ let target = self.quantity.unit;
+
+ let normalized = if unit == target {
+ amount
+ } else if unit.is_mass() && target.is_mass() {
+ convert_mass_decimal(amount, unit, target)
+ } else {
+ return Err(RadrootsCoreQuantityPriceError::NonConvertibleUnits {
+ from: unit,
+ to: target,
+ });
+ };
+
+ let qty = RadrootsCoreQuantity::new(normalized, target);
+ self.try_cost_for_rounded(&qty)
+ }
+}
+
+impl RadrootsCoreQuantityPriceOps for RadrootsCoreQuantityPrice {
+ #[inline]
+ fn cost_for(&self, qty: &RadrootsCoreQuantity) -> RadrootsCoreMoney {
+ if qty.amount.is_zero() {
+ return RadrootsCoreMoney::zero(self.amount.currency);
+ }
+ if self.quantity.amount.is_zero() {
+ return RadrootsCoreMoney::zero(self.amount.currency);
+ }
+
+ let ratio = qty.amount / self.quantity.amount;
+ self.amount.mul_decimal(ratio)
+ }
+
+ #[inline]
+ fn cost_for_rounded(&self, qty: &RadrootsCoreQuantity) -> RadrootsCoreMoney {
+ self.cost_for(qty).quantize_to_currency()
+ }
+
+ #[inline]
+ fn cost_for_with_quantized_price(&self, qty: &RadrootsCoreQuantity) -> RadrootsCoreMoney {
+ if qty.amount.is_zero() {
+ return RadrootsCoreMoney::zero(self.amount.currency);
+ }
+ if self.quantity.amount.is_zero() {
+ return RadrootsCoreMoney::zero(self.amount.currency);
+ }
+ let unit_price_q = self.amount.clone().quantize_to_currency();
+ unit_price_q.mul_decimal(qty.amount / self.quantity.amount)
+ }
+
+ #[inline]
+ fn try_cost_for(
+ &self,
+ qty: &RadrootsCoreQuantity,
+ ) -> Result<RadrootsCoreMoney, RadrootsCoreQuantityPriceError> {
+ if self.quantity.amount.is_zero() {
+ return Err(RadrootsCoreQuantityPriceError::PerQuantityZero);
+ }
+ if qty.unit != self.quantity.unit {
+ return Err(RadrootsCoreQuantityPriceError::UnitMismatch {
+ have: qty.unit,
+ want: self.quantity.unit,
+ });
+ }
+ let ratio = qty.amount / self.quantity.amount;
+ Ok(self.amount.mul_decimal(ratio))
+ }
+
+ #[inline]
+ fn try_cost_for_rounded(
+ &self,
+ qty: &RadrootsCoreQuantity,
+ ) -> Result<RadrootsCoreMoney, RadrootsCoreQuantityPriceError> {
+ Ok(self.try_cost_for(qty)?.quantize_to_currency())
+ }
+}
diff --git a/crates/core/src/serde_ext.rs b/crates/core/src/serde_ext.rs
@@ -0,0 +1,23 @@
+#![cfg(feature = "serde")]
+
+use serde::{de::Error as DeError, Deserialize, Deserializer, Serializer};
+
+pub mod decimal_str {
+ use super::*;
+ use crate::RadrootsCoreDecimal;
+ use core::str::FromStr;
+
+ pub fn serialize<S: Serializer>(
+ value: &RadrootsCoreDecimal,
+ serializer: S,
+ ) -> Result<S::Ok, S::Error> {
+ serializer.serialize_str(&value.normalize().to_string())
+ }
+
+ pub fn deserialize<'de, D: Deserializer<'de>>(
+ deserializer: D,
+ ) -> Result<RadrootsCoreDecimal, D::Error> {
+ let s = String::deserialize(deserializer)?;
+ RadrootsCoreDecimal::from_str(&s).map_err(D::Error::custom)
+ }
+}
diff --git a/crates/core/src/unit.rs b/crates/core/src/unit.rs
@@ -0,0 +1,161 @@
+use core::fmt;
+use core::str::FromStr;
+use rust_decimal_macros::dec;
+
+#[cfg(feature = "serde")]
+use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
+
+use crate::RadrootsCoreDecimal;
+
+#[typeshare::typeshare]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub enum RadrootsCoreUnit {
+ Each,
+ MassKg,
+ MassG,
+ MassOz,
+ MassLb,
+ VolumeL,
+ VolumeMl,
+}
+
+impl RadrootsCoreUnit {
+ #[inline]
+ pub fn code(&self) -> &'static str {
+ match self {
+ Self::Each => "each",
+ Self::MassKg => "kg",
+ Self::MassG => "g",
+ Self::MassOz => "oz",
+ Self::MassLb => "lb",
+ Self::VolumeL => "l",
+ Self::VolumeMl => "ml",
+ }
+ }
+
+ pub fn same_dimension(a: Self, b: Self) -> bool {
+ use RadrootsCoreUnit::*;
+ matches!(
+ (a, b),
+ (Each, Each)
+ | (MassKg, MassKg)
+ | (MassKg, MassG)
+ | (MassKg, MassOz)
+ | (MassKg, MassLb)
+ | (MassG, MassKg)
+ | (MassG, MassG)
+ | (MassG, MassOz)
+ | (MassG, MassLb)
+ | (MassOz, MassKg)
+ | (MassOz, MassG)
+ | (MassOz, MassOz)
+ | (MassOz, MassLb)
+ | (MassLb, MassKg)
+ | (MassLb, MassG)
+ | (MassLb, MassOz)
+ | (MassLb, MassLb)
+ | (VolumeL, VolumeL)
+ | (VolumeL, VolumeMl)
+ | (VolumeMl, VolumeL)
+ | (VolumeMl, VolumeMl)
+ )
+ }
+
+ #[inline]
+ pub fn is_mass(&self) -> bool {
+ matches!(
+ self,
+ Self::MassKg | Self::MassG | Self::MassOz | Self::MassLb
+ )
+ }
+}
+
+impl fmt::Display for RadrootsCoreUnit {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(self.code())
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsCoreUnitParseError {
+ UnknownUnit,
+ NotAMassUnit,
+}
+
+impl fmt::Display for RadrootsCoreUnitParseError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::UnknownUnit => write!(f, "unknown unit string"),
+ Self::NotAMassUnit => write!(f, "unit is not a mass unit"),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsCoreUnitParseError {}
+
+impl FromStr for RadrootsCoreUnit {
+ type Err = RadrootsCoreUnitParseError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let s = s.trim().to_ascii_lowercase();
+ match s.as_str() {
+ "each" | "ea" | "count" => Ok(RadrootsCoreUnit::Each),
+ "kg" | "kilogram" | "kilograms" => Ok(RadrootsCoreUnit::MassKg),
+ "g" | "gram" | "grams" => Ok(RadrootsCoreUnit::MassG),
+ "oz" | "ounce" | "ounces" => Ok(RadrootsCoreUnit::MassOz),
+ "lb" | "pound" | "pounds" => Ok(RadrootsCoreUnit::MassLb),
+ "l" | "liter" | "litre" | "liters" | "litres" => Ok(RadrootsCoreUnit::VolumeL),
+ "ml" | "milliliter" | "millilitre" | "milliliters" | "millilitres" => {
+ Ok(RadrootsCoreUnit::VolumeMl)
+ }
+ _ => Err(RadrootsCoreUnitParseError::UnknownUnit),
+ }
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for RadrootsCoreUnit {
+ fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
+ ser.serialize_str(self.code())
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for RadrootsCoreUnit {
+ fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
+ let s = String::deserialize(de)?;
+ s.parse().map_err(D::Error::custom)
+ }
+}
+
+#[inline]
+pub fn parse_mass_unit(s: &str) -> Result<RadrootsCoreUnit, RadrootsCoreUnitParseError> {
+ let u: RadrootsCoreUnit = RadrootsCoreUnit::from_str(s)?;
+ if u.is_mass() {
+ Ok(u)
+ } else {
+ Err(RadrootsCoreUnitParseError::NotAMassUnit)
+ }
+}
+
+#[inline]
+fn grams_factor_decimal(u: RadrootsCoreUnit) -> RadrootsCoreDecimal {
+ match u {
+ RadrootsCoreUnit::MassG => RadrootsCoreDecimal::ONE,
+ RadrootsCoreUnit::MassKg => RadrootsCoreDecimal::from(1000u32),
+ RadrootsCoreUnit::MassOz => RadrootsCoreDecimal(dec!(28.349523125)),
+ RadrootsCoreUnit::MassLb => RadrootsCoreDecimal(dec!(453.59237)),
+ _ => RadrootsCoreDecimal::ONE,
+ }
+}
+
+#[inline]
+pub fn convert_mass_decimal(
+ amount: RadrootsCoreDecimal,
+ from: RadrootsCoreUnit,
+ to: RadrootsCoreUnit,
+) -> RadrootsCoreDecimal {
+ let amount_g = amount * grams_factor_decimal(from);
+ amount_g / grams_factor_decimal(to)
+}
diff --git a/crates/events-codec/Cargo.toml b/crates/events-codec/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "radroots-events-codec"
+version = "0.1.0"
+authors = ["Radroots Authors"]
+license = "AGPLv3"
+edition = "2021"
+
+[features]
+default = ["std"]
+std = []
+serde = ["dep:serde", "radroots-core/serde", "radroots-events/serde"]
+serde_json = ["serde", "dep:serde_json"]
+
+[dependencies]
+radroots-core = { path = "../core", default-features = false }
+radroots-events = { version = "0.1.0", path = "../events" }
+serde = { version = "1", features = ["derive"], optional = true }
+serde_json = { version = "1", optional = true }
+\ No newline at end of file
diff --git a/crates/events-codec/src/job/encode.rs b/crates/events-codec/src/job/encode.rs
@@ -0,0 +1,91 @@
+use core::fmt;
+
+#[derive(Debug, Clone)]
+pub struct WireEventParts {
+ pub kind: u32,
+ pub content: String,
+ pub tags: Vec<Vec<String>>,
+}
+
+#[derive(Debug)]
+pub enum JobEncodeError {
+ MissingProvidersForEncrypted,
+ InvalidKind(u32),
+ EmptyRequiredField(&'static str),
+}
+
+impl fmt::Display for JobEncodeError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ JobEncodeError::MissingProvidersForEncrypted => {
+ write!(f, "encrypted=true requires at least one provider ('p') tag")
+ }
+ JobEncodeError::InvalidKind(k) => write!(f, "invalid job event kind: {}", k),
+ JobEncodeError::EmptyRequiredField(n) => write!(f, "empty required field: {}", n),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for JobEncodeError {}
+
+pub fn canonicalize_tags(tags: &mut Vec<Vec<String>>) {
+ tags.retain(|t| t.first().map(|s| !s.trim().is_empty()).unwrap_or(false));
+ for t in tags.iter_mut() {
+ for s in t.iter_mut() {
+ *s = s.trim().to_string();
+ }
+ }
+ tags.sort_by(|a, b| a.first().cmp(&b.first()).then_with(|| a.cmp(b)));
+ tags.dedup();
+}
+
+pub fn empty_content() -> String {
+ String::new()
+}
+
+#[cfg(feature = "serde_json")]
+pub fn json_content<T: serde::Serialize>(value: &T) -> Result<String, JobEncodeError> {
+ serde_json::to_string(value).map_err(|_| JobEncodeError::EmptyRequiredField("content-json"))
+}
+
+#[derive(Debug, Clone)]
+pub struct EventDraft {
+ pub kind: u32,
+ pub created_at: u32,
+ pub author: String,
+ pub content: String,
+ pub tags: Vec<Vec<String>>,
+}
+
+pub fn to_draft(parts: WireEventParts, author: impl Into<String>, created_at: u32) -> EventDraft {
+ EventDraft {
+ kind: parts.kind,
+ created_at,
+ author: author.into(),
+ content: parts.content,
+ tags: parts.tags,
+ }
+}
+
+pub fn push_status_tag(tags: &mut Vec<Vec<String>>, status: &str, extra: Option<&str>) {
+ let mut v = vec!["status".into(), status.into()];
+ if let Some(e) = extra {
+ v.push(e.into());
+ }
+ tags.push(v);
+}
+
+pub fn push_provider_tag(tags: &mut Vec<Vec<String>>, p: &str) {
+ tags.push(vec!["p".into(), p.into()]);
+}
+
+pub fn push_relay_tag(tags: &mut Vec<Vec<String>>, r: &str) {
+ tags.push(vec!["relays".into(), r.into()]);
+}
+
+pub fn assert_no_inputs_when_encrypted(tags: &[Vec<String>]) -> bool {
+ !tags
+ .iter()
+ .any(|t| t.get(0).map(|s| s == "i").unwrap_or(false))
+}
diff --git a/crates/events-codec/src/job/error.rs b/crates/events-codec/src/job/error.rs
@@ -0,0 +1,42 @@
+use core::fmt;
+
+#[derive(Debug)]
+pub enum JobParseError {
+ MissingTag(&'static str),
+ InvalidTag(&'static str),
+ InvalidNumber(&'static str, std::num::ParseIntError),
+ NonWholeSats(&'static str),
+ AmountOverflow(&'static str),
+ MissingChainTag(&'static str),
+}
+
+impl fmt::Display for JobParseError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ JobParseError::MissingTag(t) => write!(f, "missing tag: {}", t),
+ JobParseError::InvalidTag(t) => write!(f, "invalid tag structure for '{}'", t),
+ JobParseError::InvalidNumber(t, e) => write!(f, "invalid number in '{}': {}", t, e),
+ JobParseError::NonWholeSats(t) => {
+ write!(
+ f,
+ "amount in msats is not a whole number of sats for '{}'",
+ t
+ )
+ }
+ JobParseError::AmountOverflow(t) => {
+ write!(f, "amount overflow in '{}' (does not fit u32 sat)", t)
+ }
+ JobParseError::MissingChainTag(t) => write!(f, "missing required chain tag: {}", t),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for JobParseError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ JobParseError::InvalidNumber(_, e) => Some(e),
+ _ => None,
+ }
+ }
+}
diff --git a/crates/events-codec/src/job/feedback/decode.rs b/crates/events-codec/src/job/feedback/decode.rs
@@ -0,0 +1,128 @@
+use radroots_events::{
+ job::{
+ feedback::models::{
+ RadrootsJobFeedback, RadrootsJobFeedbackEventIndex, RadrootsJobFeedbackEventMetadata,
+ },
+ JobPaymentRequest,
+ },
+ RadrootsNostrEvent, RadrootsNostrEventPtr,
+};
+
+use crate::job::{
+ error::JobParseError,
+ util::{feedback_status_from_tag, parse_amount_tag_sat, parse_bool_encrypted},
+};
+
+pub fn job_feedback_from_tags(
+ kind: u32,
+ tags: &[Vec<String>],
+ content: &str,
+) -> Result<RadrootsJobFeedback, JobParseError> {
+ let etag = tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some("e"))
+ .or_else(|| {
+ tags.iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some("e_ref"))
+ })
+ .ok_or(JobParseError::MissingTag("e"))?;
+ let req_id = etag.get(1).ok_or(JobParseError::InvalidTag("e"))?.clone();
+ let relay_hint = etag.get(2).cloned();
+
+ let status_tag = tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some("status"))
+ .ok_or(JobParseError::MissingTag("status"))?;
+
+ let status = match status_tag.get(1).and_then(|s| feedback_status_from_tag(s)) {
+ Some(s) => s,
+ None => return Err(JobParseError::InvalidTag("status")),
+ };
+
+ let extra_info = status_tag.get(2).cloned();
+
+ let payment = parse_amount_tag_sat(tags)?.map(|(sat, bolt11)| JobPaymentRequest {
+ amount_sat: sat,
+ bolt11,
+ });
+
+ let encrypted = parse_bool_encrypted(tags);
+
+ let customer_pubkey = tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some("p"))
+ .and_then(|t| t.get(1).cloned());
+
+ Ok(RadrootsJobFeedback {
+ kind: kind as u16,
+ status,
+ extra_info,
+ request_event: RadrootsNostrEventPtr {
+ id: req_id,
+ relays: relay_hint,
+ },
+ customer_pubkey,
+ payment,
+ content: if content.is_empty() {
+ None
+ } else {
+ Some(content.to_string())
+ },
+ encrypted,
+ })
+}
+
+fn is_feedback_kind(kind: u32) -> bool {
+ kind == 7000
+}
+
+pub fn metadata_from_event(
+ id: String,
+ author: String,
+ published_at: u32,
+ kind: u32,
+ content: String,
+ tags: Vec<Vec<String>>,
+) -> Result<RadrootsJobFeedbackEventMetadata, JobParseError> {
+ if !is_feedback_kind(kind) {
+ return Err(JobParseError::InvalidTag("kind (expected 7000)"));
+ }
+ let job_feedback = job_feedback_from_tags(kind, &tags, &content)?;
+ Ok(RadrootsJobFeedbackEventMetadata {
+ id,
+ author,
+ published_at,
+ job_feedback,
+ })
+}
+
+pub fn index_from_event(
+ id: String,
+ author: String,
+ published_at: u32,
+ kind: u32,
+ content: String,
+ tags: Vec<Vec<String>>,
+ sig: String,
+) -> Result<RadrootsJobFeedbackEventIndex, JobParseError> {
+ let metadata = metadata_from_event(
+ id.clone(),
+ author.clone(),
+ published_at,
+ kind,
+ content.clone(),
+ tags.clone(),
+ )?;
+ Ok(RadrootsJobFeedbackEventIndex {
+ event: RadrootsNostrEvent {
+ id,
+ author,
+ created_at: published_at,
+ kind,
+ content,
+ tags,
+ sig,
+ },
+ metadata,
+ })
+}
diff --git a/crates/events-codec/src/job/feedback/encode.rs b/crates/events-codec/src/job/feedback/encode.rs
@@ -0,0 +1,56 @@
+use radroots_events::job::feedback::models::RadrootsJobFeedback;
+
+use crate::job::encode::{canonicalize_tags, JobEncodeError, WireEventParts};
+use crate::job::util::{feedback_status_tag, push_amount_tag_msat};
+
+pub fn job_feedback_build_tags(fb: &RadrootsJobFeedback) -> Vec<Vec<String>> {
+ let mut tags: Vec<Vec<String>> = Vec::new();
+
+ let mut st = vec![
+ "status".to_string(),
+ feedback_status_tag(fb.status).to_string(),
+ ];
+ if let Some(info) = &fb.extra_info {
+ st.push(info.clone());
+ }
+ tags.push(st);
+
+ let mut e = vec!["e".to_string(), fb.request_event.id.clone()];
+ if let Some(r) = &fb.request_event.relays {
+ e.push(r.clone());
+ }
+ tags.push(e);
+
+ if let Some(p) = &fb.customer_pubkey {
+ tags.push(vec!["p".into(), p.clone()]);
+ }
+
+ if let Some(pay) = &fb.payment {
+ push_amount_tag_msat(&mut tags, pay.amount_sat, pay.bolt11.clone());
+ }
+
+ if fb.encrypted {
+ tags.push(vec!["encrypted".into()]);
+ }
+
+ tags
+}
+
+pub fn to_wire_parts(
+ fb: &RadrootsJobFeedback,
+ content: &str,
+) -> Result<WireEventParts, JobEncodeError> {
+ let kind = fb.kind as u32;
+ if kind != 7000 {
+ return Err(JobEncodeError::InvalidKind(kind));
+ }
+
+ let mut tags = job_feedback_build_tags(fb);
+ canonicalize_tags(&mut tags);
+
+ Ok(WireEventParts {
+ kind,
+ content: content.to_string(),
+ tags,
+ })
+}
diff --git a/crates/events-codec/src/job/mod.rs b/crates/events-codec/src/job/mod.rs
@@ -0,0 +1,20 @@
+pub mod encode;
+pub mod error;
+pub mod util;
+
+pub mod feedback {
+ pub mod decode;
+ pub mod encode;
+}
+
+pub mod request {
+ pub mod decode;
+ pub mod encode;
+}
+
+pub mod result {
+ pub mod decode;
+ pub mod encode;
+}
+
+pub mod traits;
diff --git a/crates/events-codec/src/job/request/decode.rs b/crates/events-codec/src/job/request/decode.rs
@@ -0,0 +1,112 @@
+use radroots_events::{
+ job::request::models::{
+ RadrootsJobInput, RadrootsJobParam, RadrootsJobRequest, RadrootsJobRequestEventIndex,
+ RadrootsJobRequestEventMetadata,
+ },
+ RadrootsNostrEvent,
+};
+
+use crate::job::{
+ error::JobParseError,
+ util::{parse_bid_tag_sat, parse_bool_encrypted, parse_i_tags, parse_params},
+};
+
+pub fn job_request_from_tags(
+ kind: u32,
+ tags: &[Vec<String>],
+) -> Result<RadrootsJobRequest, JobParseError> {
+ let inputs: Vec<RadrootsJobInput> = parse_i_tags(tags);
+
+ let output = tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some("output"))
+ .and_then(|t| t.get(1).cloned());
+
+ let params: Vec<RadrootsJobParam> = parse_params(tags);
+
+ let bid_sat = parse_bid_tag_sat(tags)?;
+
+ let relays = tags
+ .iter()
+ .filter(|t| t.get(0).map(|s| s.as_str()) == Some("relays"))
+ .filter_map(|t| t.get(1).cloned())
+ .collect::<Vec<_>>();
+
+ let providers = tags
+ .iter()
+ .filter(|t| t.get(0).map(|s| s.as_str()) == Some("p"))
+ .filter_map(|t| t.get(1).cloned())
+ .collect::<Vec<_>>();
+
+ let topics = tags
+ .iter()
+ .filter(|t| t.get(0).map(|s| s.as_str()) == Some("t"))
+ .filter_map(|t| t.get(1).cloned())
+ .collect::<Vec<_>>();
+
+ let encrypted = parse_bool_encrypted(tags);
+
+ if encrypted && providers.is_empty() {
+ return Err(JobParseError::MissingTag("p"));
+ }
+
+ Ok(RadrootsJobRequest {
+ kind: kind as u16,
+ inputs,
+ output,
+ params,
+ bid_sat,
+ relays,
+ providers,
+ topics,
+ encrypted,
+ })
+}
+
+fn is_request_kind(kind: u32) -> bool {
+ (5000..=5999).contains(&kind)
+}
+
+pub fn metadata_from_event(
+ id: String,
+ author: String,
+ published_at: u32,
+ kind: u32,
+ tags: Vec<Vec<String>>,
+) -> Result<RadrootsJobRequestEventMetadata, JobParseError> {
+ if !is_request_kind(kind) {
+ return Err(JobParseError::InvalidTag("kind (expected 5000-5999)"));
+ }
+ let job_request = job_request_from_tags(kind, &tags)?;
+ Ok(RadrootsJobRequestEventMetadata {
+ id,
+ author,
+ published_at,
+ job_request,
+ })
+}
+
+pub fn index_from_event(
+ id: String,
+ author: String,
+ published_at: u32,
+ kind: u32,
+ content: String,
+ tags: Vec<Vec<String>>,
+ sig: String,
+) -> Result<RadrootsJobRequestEventIndex, JobParseError> {
+ let metadata =
+ metadata_from_event(id.clone(), author.clone(), published_at, kind, tags.clone())?;
+ Ok(RadrootsJobRequestEventIndex {
+ event: RadrootsNostrEvent {
+ id,
+ author,
+ created_at: published_at,
+ kind,
+ content,
+ tags,
+ sig,
+ },
+ metadata,
+ })
+}
diff --git a/crates/events-codec/src/job/request/encode.rs b/crates/events-codec/src/job/request/encode.rs
@@ -0,0 +1,72 @@
+use radroots_events::job::request::models::RadrootsJobRequest;
+
+use crate::job::encode::{canonicalize_tags, JobEncodeError, WireEventParts};
+use crate::job::util::{job_input_type_tag, push_bid_tag_msat};
+
+pub fn job_request_build_tags(req: &RadrootsJobRequest) -> Vec<Vec<String>> {
+ let mut tags: Vec<Vec<String>> = Vec::new();
+
+ for i in &req.inputs {
+ let mut t = vec!["i".to_string(), i.data.clone()];
+ t.push(job_input_type_tag(i.input_type).to_string());
+ if let Some(relay) = &i.relay {
+ t.push(relay.clone());
+ }
+ if let Some(marker) = &i.marker {
+ t.push(marker.clone());
+ }
+ tags.push(t);
+ }
+
+ if let Some(out) = &req.output {
+ tags.push(vec!["output".into(), out.clone()]);
+ }
+
+ for p in &req.params {
+ tags.push(vec!["param".into(), p.key.clone(), p.value.clone()]);
+ }
+
+ if let Some(bid_sat) = req.bid_sat {
+ push_bid_tag_msat(&mut tags, bid_sat);
+ }
+
+ for r in &req.relays {
+ tags.push(vec!["relays".into(), r.clone()]);
+ }
+
+ for p in &req.providers {
+ tags.push(vec!["p".into(), p.clone()]);
+ }
+
+ for t in &req.topics {
+ tags.push(vec!["t".into(), t.clone()]);
+ }
+
+ if req.encrypted {
+ tags.push(vec!["encrypted".into()]);
+ }
+
+ tags
+}
+
+pub fn to_wire_parts(
+ req: &RadrootsJobRequest,
+ content: &str,
+) -> Result<WireEventParts, JobEncodeError> {
+ let kind = req.kind as u32;
+ if !(5000..=5999).contains(&kind) {
+ return Err(JobEncodeError::InvalidKind(kind));
+ }
+ if req.encrypted && req.providers.is_empty() {
+ return Err(JobEncodeError::MissingProvidersForEncrypted);
+ }
+
+ let mut tags = job_request_build_tags(req);
+ canonicalize_tags(&mut tags);
+
+ Ok(WireEventParts {
+ kind,
+ content: content.to_string(),
+ tags,
+ })
+}
diff --git a/crates/events-codec/src/job/result/decode.rs b/crates/events-codec/src/job/result/decode.rs
@@ -0,0 +1,125 @@
+use radroots_events::{
+ job::{
+ request::models::RadrootsJobInput,
+ result::models::{
+ RadrootsJobResult, RadrootsJobResultEventIndex, RadrootsJobResultEventMetadata,
+ },
+ JobPaymentRequest,
+ },
+ RadrootsNostrEvent, RadrootsNostrEventPtr,
+};
+
+use crate::job::{
+ error::JobParseError,
+ util::{parse_amount_tag_sat, parse_bool_encrypted, parse_i_tags},
+};
+
+pub fn job_result_from_tags(
+ kind: u32,
+ tags: &[Vec<String>],
+ content: &str,
+) -> Result<RadrootsJobResult, JobParseError> {
+ let etag = tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some("e"))
+ .or_else(|| {
+ tags.iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some("e_ref"))
+ })
+ .ok_or(JobParseError::MissingTag("e"))?;
+
+ let req_id = etag.get(1).ok_or(JobParseError::InvalidTag("e"))?.clone();
+ let relay_hint = etag.get(2).cloned();
+
+ let request_json = tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some("request"))
+ .and_then(|t| t.get(1).cloned());
+
+ let inputs: Vec<RadrootsJobInput> = parse_i_tags(tags);
+
+ let payment = parse_amount_tag_sat(tags)?.map(|(sat, bolt11)| JobPaymentRequest {
+ amount_sat: sat,
+ bolt11,
+ });
+
+ let encrypted = parse_bool_encrypted(tags);
+
+ let customer_pubkey = tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some("p"))
+ .and_then(|t| t.get(1).cloned());
+
+ Ok(RadrootsJobResult {
+ kind: kind as u16,
+ request_event: RadrootsNostrEventPtr {
+ id: req_id,
+ relays: relay_hint,
+ },
+ request_json,
+ inputs,
+ customer_pubkey,
+ payment,
+ content: if content.is_empty() {
+ None
+ } else {
+ Some(content.to_string())
+ },
+ encrypted,
+ })
+}
+
+fn is_result_kind(kind: u32) -> bool {
+ (6000..=6999).contains(&kind)
+}
+
+pub fn metadata_from_event(
+ id: String,
+ author: String,
+ published_at: u32,
+ kind: u32,
+ content: String,
+ tags: Vec<Vec<String>>,
+) -> Result<RadrootsJobResultEventMetadata, JobParseError> {
+ if !is_result_kind(kind) {
+ return Err(JobParseError::InvalidTag("kind (expected 6000-6999)"));
+ }
+ let job_result = job_result_from_tags(kind, &tags, &content)?;
+ Ok(RadrootsJobResultEventMetadata {
+ id,
+ author,
+ published_at,
+ job_result,
+ })
+}
+
+pub fn index_from_event(
+ id: String,
+ author: String,
+ published_at: u32,
+ kind: u32,
+ content: String,
+ tags: Vec<Vec<String>>,
+ sig: String,
+) -> Result<RadrootsJobResultEventIndex, JobParseError> {
+ let metadata = metadata_from_event(
+ id.clone(),
+ author.clone(),
+ published_at,
+ kind,
+ content.clone(),
+ tags.clone(),
+ )?;
+ Ok(RadrootsJobResultEventIndex {
+ event: RadrootsNostrEvent {
+ id,
+ author,
+ created_at: published_at,
+ kind,
+ content,
+ tags,
+ sig,
+ },
+ metadata,
+ })
+}
diff --git a/crates/events-codec/src/job/result/encode.rs b/crates/events-codec/src/job/result/encode.rs
@@ -0,0 +1,72 @@
+use radroots_events::job::result::models::RadrootsJobResult;
+
+use crate::job::encode::{
+ assert_no_inputs_when_encrypted, canonicalize_tags, JobEncodeError, WireEventParts,
+};
+use crate::job::util::{job_input_type_tag, push_amount_tag_msat};
+
+pub fn job_result_build_tags(res: &RadrootsJobResult) -> Vec<Vec<String>> {
+ let mut tags: Vec<Vec<String>> = Vec::new();
+
+ let mut e = vec!["e".to_string(), res.request_event.id.clone()];
+ if let Some(r) = &res.request_event.relays {
+ e.push(r.clone());
+ }
+ tags.push(e);
+
+ if let Some(j) = &res.request_json {
+ tags.push(vec!["request".into(), j.clone()]);
+ }
+
+ if !res.encrypted {
+ for i in &res.inputs {
+ let mut t = vec!["i".to_string(), i.data.clone()];
+ t.push(job_input_type_tag(i.input_type).to_string());
+ if let Some(relay) = &i.relay {
+ t.push(relay.clone());
+ }
+ if let Some(marker) = &i.marker {
+ t.push(marker.clone());
+ }
+ tags.push(t);
+ }
+ }
+
+ if let Some(p) = &res.customer_pubkey {
+ tags.push(vec!["p".into(), p.clone()]);
+ }
+
+ if let Some(pay) = &res.payment {
+ push_amount_tag_msat(&mut tags, pay.amount_sat, pay.bolt11.clone());
+ }
+
+ if res.encrypted {
+ tags.push(vec!["encrypted".into()]);
+ }
+
+ tags
+}
+
+pub fn to_wire_parts(
+ res: &RadrootsJobResult,
+ content: &str,
+) -> Result<WireEventParts, JobEncodeError> {
+ let kind = res.kind as u32;
+ if !(6000..=6999).contains(&kind) {
+ return Err(JobEncodeError::InvalidKind(kind));
+ }
+
+ let mut tags = job_result_build_tags(res);
+
+ if res.encrypted && !assert_no_inputs_when_encrypted(&tags) {
+ return Err(JobEncodeError::EmptyRequiredField("inputs-when-encrypted"));
+ }
+
+ canonicalize_tags(&mut tags);
+
+ Ok(WireEventParts {
+ kind,
+ content: content.to_string(),
+ tags,
+ })
+}
diff --git a/crates/events-codec/src/job/traits.rs b/crates/events-codec/src/job/traits.rs
@@ -0,0 +1,181 @@
+#[cfg(not(feature = "std"))]
+extern crate alloc;
+
+#[cfg(not(feature = "std"))]
+use alloc::{string::String, vec::Vec};
+
+use radroots_events::job::{
+ feedback::models::{RadrootsJobFeedbackEventIndex, RadrootsJobFeedbackEventMetadata},
+ request::models::{RadrootsJobRequestEventIndex, RadrootsJobRequestEventMetadata},
+ result::models::{RadrootsJobResultEventIndex, RadrootsJobResultEventMetadata},
+};
+
+use crate::job::{
+ error::JobParseError,
+ feedback::decode::{
+ index_from_event as feedback_index_from_event,
+ metadata_from_event as feedback_metadata_from_event,
+ },
+ request::decode::{
+ index_from_event as request_index_from_event,
+ metadata_from_event as request_metadata_from_event,
+ },
+ result::decode::{
+ index_from_event as result_index_from_event,
+ metadata_from_event as result_metadata_from_event,
+ },
+};
+
+pub trait JobEventLike {
+ fn raw_id(&self) -> String;
+ fn raw_author(&self) -> String;
+ fn raw_published_at(&self) -> u32;
+ fn raw_kind(&self) -> u32;
+ fn raw_content(&self) -> String;
+ fn raw_tags(&self) -> Vec<Vec<String>>;
+ fn raw_sig(&self) -> String;
+
+ fn to_job_request_metadata(&self) -> Result<RadrootsJobRequestEventMetadata, JobParseError> {
+ request_metadata_from_event(
+ self.raw_id(),
+ self.raw_author(),
+ self.raw_published_at(),
+ self.raw_kind(),
+ self.raw_tags(),
+ )
+ }
+
+ fn to_job_request_event_index(&self) -> Result<RadrootsJobRequestEventIndex, JobParseError> {
+ request_index_from_event(
+ self.raw_id(),
+ self.raw_author(),
+ self.raw_published_at(),
+ self.raw_kind(),
+ self.raw_content(),
+ self.raw_tags(),
+ self.raw_sig(),
+ )
+ }
+
+ fn to_job_result_metadata(&self) -> Result<RadrootsJobResultEventMetadata, JobParseError> {
+ result_metadata_from_event(
+ self.raw_id(),
+ self.raw_author(),
+ self.raw_published_at(),
+ self.raw_kind(),
+ self.raw_content(),
+ self.raw_tags(),
+ )
+ }
+
+ fn to_job_result_event_index(&self) -> Result<RadrootsJobResultEventIndex, JobParseError> {
+ result_index_from_event(
+ self.raw_id(),
+ self.raw_author(),
+ self.raw_published_at(),
+ self.raw_kind(),
+ self.raw_content(),
+ self.raw_tags(),
+ self.raw_sig(),
+ )
+ }
+
+ fn to_job_feedback_metadata(&self) -> Result<RadrootsJobFeedbackEventMetadata, JobParseError> {
+ feedback_metadata_from_event(
+ self.raw_id(),
+ self.raw_author(),
+ self.raw_published_at(),
+ self.raw_kind(),
+ self.raw_content(),
+ self.raw_tags(),
+ )
+ }
+
+ fn to_job_feedback_event_index(&self) -> Result<RadrootsJobFeedbackEventIndex, JobParseError> {
+ feedback_index_from_event(
+ self.raw_id(),
+ self.raw_author(),
+ self.raw_published_at(),
+ self.raw_kind(),
+ self.raw_content(),
+ self.raw_tags(),
+ self.raw_sig(),
+ )
+ }
+}
+
+pub trait JobEventBorrow<'a> {
+ fn raw_id(&'a self) -> &'a str;
+ fn raw_author(&'a self) -> &'a str;
+ fn raw_content(&'a self) -> &'a str;
+ fn raw_kind(&'a self) -> u32;
+}
+
+#[derive(Clone, Copy)]
+pub struct BorrowedEventAdapter<'a, E: JobEventBorrow<'a>> {
+ inner: &'a E,
+ published_at: u32,
+ tags: &'a [Vec<String>],
+ sig: &'a str,
+}
+
+impl<'a, E: JobEventBorrow<'a>> BorrowedEventAdapter<'a, E> {
+ pub fn new(inner: &'a E, published_at: u32, tags: &'a [Vec<String>], sig: &'a str) -> Self {
+ Self {
+ inner,
+ published_at,
+ tags,
+ sig,
+ }
+ }
+}
+
+impl<'a, E: JobEventBorrow<'a>> JobEventLike for BorrowedEventAdapter<'a, E> {
+ #[inline]
+ fn raw_id(&self) -> String {
+ self.inner.raw_id().to_owned()
+ }
+ #[inline]
+ fn raw_author(&self) -> String {
+ self.inner.raw_author().to_owned()
+ }
+ #[inline]
+ fn raw_published_at(&self) -> u32 {
+ self.published_at
+ }
+ #[inline]
+ fn raw_kind(&self) -> u32 {
+ self.inner.raw_kind()
+ }
+ #[inline]
+ fn raw_content(&self) -> String {
+ self.inner.raw_content().to_owned()
+ }
+ #[inline]
+ fn raw_tags(&self) -> Vec<Vec<String>> {
+ self.tags.to_vec()
+ }
+ #[inline]
+ fn raw_sig(&self) -> String {
+ self.sig.to_owned()
+ }
+}
+
+impl<'a> JobEventBorrow<'a> for radroots_events::RadrootsNostrEvent {
+ #[inline]
+ fn raw_id(&'a self) -> &'a str {
+ &self.id
+ }
+ #[inline]
+ fn raw_author(&'a self) -> &'a str {
+ &self.author
+ }
+ #[inline]
+ fn raw_content(&'a self) -> &'a str {
+ &self.content
+ }
+ #[inline]
+ fn raw_kind(&'a self) -> u32 {
+ self.kind
+ }
+}
diff --git a/crates/events-codec/src/job/util.rs b/crates/events-codec/src/job/util.rs
@@ -0,0 +1,233 @@
+use radroots_events::job::{
+ request::models::{RadrootsJobInput, RadrootsJobParam},
+ JobFeedbackStatus, JobInputType,
+};
+
+use crate::job::error::JobParseError;
+
+fn looks_like_hex_id(s: &str) -> bool {
+ let n = s.len();
+ (n == 32 || n == 64) && s.chars().all(|c| c.is_ascii_hexdigit())
+}
+
+fn looks_like_url_or_nostr(s: &str) -> bool {
+ let ls = s.to_ascii_lowercase();
+ ls.starts_with("http://")
+ || ls.starts_with("https://")
+ || ls.starts_with("nostr:")
+ || ls.starts_with("note")
+ || ls.starts_with("nevent")
+ || ls.starts_with("naddr")
+ || looks_like_hex_id(s)
+}
+
+fn looks_like_ws_relay(s: &str) -> bool {
+ let ls = s.to_ascii_lowercase();
+ ls.starts_with("ws://") || ls.starts_with("wss://")
+}
+
+pub fn parse_bool_encrypted(tags: &[Vec<String>]) -> bool {
+ tags.iter()
+ .any(|t| t.get(0).map(|s| s.as_str()) == Some("encrypted"))
+}
+
+#[inline]
+pub fn job_input_type_tag(t: JobInputType) -> &'static str {
+ match t {
+ JobInputType::Url => "url",
+ JobInputType::Event => "event",
+ JobInputType::Job => "job",
+ JobInputType::Text => "text",
+ }
+}
+
+#[inline]
+pub fn job_input_type_from_tag(s: &str) -> Option<JobInputType> {
+ match s {
+ "url" => Some(JobInputType::Url),
+ "event" => Some(JobInputType::Event),
+ "job" => Some(JobInputType::Job),
+ "text" => Some(JobInputType::Text),
+ _ => None,
+ }
+}
+
+#[inline]
+pub fn feedback_status_tag(s: JobFeedbackStatus) -> &'static str {
+ match s {
+ JobFeedbackStatus::PaymentRequired => "payment-required",
+ JobFeedbackStatus::Processing => "processing",
+ JobFeedbackStatus::Error => "error",
+ JobFeedbackStatus::Success => "success",
+ JobFeedbackStatus::Partial => "partial",
+ }
+}
+
+#[inline]
+pub fn feedback_status_from_tag(s: &str) -> Option<JobFeedbackStatus> {
+ match s {
+ "payment-required" => Some(JobFeedbackStatus::PaymentRequired),
+ "processing" => Some(JobFeedbackStatus::Processing),
+ "error" => Some(JobFeedbackStatus::Error),
+ "success" => Some(JobFeedbackStatus::Success),
+ "partial" => Some(JobFeedbackStatus::Partial),
+ _ => None,
+ }
+}
+
+pub fn parse_i_tags(tags: &[Vec<String>]) -> Vec<RadrootsJobInput> {
+ let mut out = Vec::new();
+ for t in tags
+ .iter()
+ .filter(|t| t.get(0).map(|s| s.as_str()) == Some("i"))
+ {
+ if t.len() < 2 {
+ continue;
+ }
+
+ let mut data = String::new();
+ let mut input_type = JobInputType::Text;
+ let mut relay: Option<String> = None;
+ let mut marker: Option<String> = None;
+
+ match t.len() {
+ 2 => {
+ let v = &t[1];
+ if looks_like_url_or_nostr(v) {
+ data = v.clone();
+ let lv = v.to_ascii_lowercase();
+ input_type = if lv.starts_with("http://") || lv.starts_with("https://") {
+ JobInputType::Url
+ } else {
+ JobInputType::Event
+ };
+ } else {
+ marker = Some(v.clone());
+ }
+ }
+ 3 => {
+ data = t[1].clone();
+ let v = t[2].as_str();
+ if let Some(it) = job_input_type_from_tag(v) {
+ input_type = it;
+ } else {
+ marker = Some(t[2].clone());
+ }
+ }
+ 4 => {
+ data = t[1].clone();
+ input_type = job_input_type_from_tag(t[2].as_str()).unwrap_or(JobInputType::Text);
+ let v = &t[3];
+ if looks_like_ws_relay(v) {
+ relay = Some(v.clone());
+ } else if marker.is_none() {
+ marker = Some(v.clone());
+ }
+ }
+ _ => {
+ data = t[1].clone();
+ input_type = job_input_type_from_tag(t[2].as_str()).unwrap_or(JobInputType::Text);
+ if let Some(v) = t.get(3) {
+ if looks_like_ws_relay(v) {
+ relay = Some(v.clone());
+ if let Some(m) = t.get(4) {
+ marker = Some(m.clone());
+ }
+ } else {
+ marker = Some(v.clone());
+ }
+ }
+ if marker.is_none() {
+ if let Some(m) = t.get(4) {
+ marker = Some(m.clone());
+ }
+ }
+ }
+ }
+
+ out.push(RadrootsJobInput {
+ data,
+ input_type,
+ relay,
+ marker,
+ });
+ }
+ out
+}
+
+pub fn parse_params(tags: &[Vec<String>]) -> Vec<RadrootsJobParam> {
+ let mut params = Vec::new();
+ for t in tags
+ .iter()
+ .filter(|t| t.get(0).map(|s| s.as_str()) == Some("param"))
+ {
+ if t.len() >= 3 {
+ params.push(RadrootsJobParam {
+ key: t[1].clone(),
+ value: t[2].clone(),
+ });
+ }
+ }
+ params
+}
+
+pub fn parse_amount_tag_sat(
+ tags: &[Vec<String>],
+) -> Result<Option<(u32, Option<String>)>, JobParseError> {
+ let amt = match tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some("amount"))
+ {
+ Some(a) => a,
+ None => return Ok(None),
+ };
+ let msat_s = amt.get(1).ok_or(JobParseError::InvalidTag("amount"))?;
+ let msat_u64: u64 = msat_s
+ .parse()
+ .map_err(|e| JobParseError::InvalidNumber("amount", e))?;
+ if msat_u64 % 1000 != 0 {
+ return Err(JobParseError::NonWholeSats("amount"));
+ }
+ let sat_u64 = msat_u64 / 1000;
+ if sat_u64 > (u32::MAX as u64) {
+ return Err(JobParseError::AmountOverflow("amount"));
+ }
+ let bolt11 = amt.get(2).cloned();
+ Ok(Some((sat_u64 as u32, bolt11)))
+}
+
+pub fn push_amount_tag_msat(tags: &mut Vec<Vec<String>>, sat: u32, bolt11: Option<String>) {
+ let msat = (sat as u64) * 1000;
+ let mut v = vec!["amount".into(), msat.to_string()];
+ if let Some(b) = bolt11 {
+ v.push(b);
+ }
+ tags.push(v);
+}
+
+pub fn parse_bid_tag_sat(tags: &[Vec<String>]) -> Result<Option<u32>, JobParseError> {
+ let bid = match tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some("bid"))
+ {
+ Some(b) => b,
+ None => return Ok(None),
+ };
+ let msat_s = bid.get(1).ok_or(JobParseError::InvalidTag("bid"))?;
+ let msat_u64: u64 = msat_s
+ .parse()
+ .map_err(|e| JobParseError::InvalidNumber("bid", e))?;
+ if msat_u64 % 1000 != 0 {
+ return Err(JobParseError::NonWholeSats("bid"));
+ }
+ let sat_u64 = msat_u64 / 1000;
+ if sat_u64 > (u32::MAX as u64) {
+ return Err(JobParseError::AmountOverflow("bid"));
+ }
+ Ok(Some(sat_u64 as u32))
+}
+
+pub fn push_bid_tag_msat(tags: &mut Vec<Vec<String>>, bid_sat: u32) {
+ let msat = (bid_sat as u64) * 1000;
+ tags.push(vec!["bid".into(), msat.to_string()]);
+}
diff --git a/crates/events-codec/src/lib.rs b/crates/events-codec/src/lib.rs
@@ -0,0 +1,5 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+#[cfg(not(feature = "std"))]
+extern crate alloc;
+
+pub mod job;
diff --git a/crates/events-indexed/Cargo.toml b/crates/events-indexed/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "radroots-events-indexed"
+version = "0.1.0"
+authors = ["Radroots Authors"]
+license = "AGPLv3"
+edition = "2021"
+
+[features]
+default = ["serde", "typeshare"]
+serde = ["dep:serde"]
+typeshare = ["dep:typeshare"]
+std = []
+
+[dependencies]
+serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true }
+typeshare = { version = "1", optional = true }
+\ No newline at end of file
diff --git a/crates/events-indexed/src/checkpoint.rs b/crates/events-indexed/src/checkpoint.rs
@@ -0,0 +1,47 @@
+#![allow(clippy::module_name_repetitions)]
+#[cfg(not(feature = "std"))]
+use alloc::{string::String, vec::Vec};
+
+use crate::types::RadrootsEventsIndexedShardId;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsEventsIndexedShardCheckpoint {
+ pub shard_id: RadrootsEventsIndexedShardId,
+ #[cfg_attr(
+ feature = "serde",
+ serde(deserialize_with = "crate::serde_ext::epoch_seconds::de")
+ )]
+ pub last_created_at: u32,
+ pub last_event_id: Option<String>,
+ pub cursor: Option<String>,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsEventsIndexedIndexCheckpoint {
+ #[cfg_attr(
+ feature = "serde",
+ serde(deserialize_with = "crate::serde_ext::epoch_seconds::de")
+ )]
+ pub generated_at: u32,
+ pub shards: Vec<RadrootsEventsIndexedShardCheckpoint>,
+}
+
+impl RadrootsEventsIndexedIndexCheckpoint {
+ pub fn get(
+ &self,
+ id: &RadrootsEventsIndexedShardId,
+ ) -> Option<&RadrootsEventsIndexedShardCheckpoint> {
+ self.shards.iter().find(|s| &s.shard_id == id)
+ }
+ pub fn upsert(&mut self, cp: RadrootsEventsIndexedShardCheckpoint) {
+ if let Some(slot) = self.shards.iter_mut().find(|s| s.shard_id == cp.shard_id) {
+ *slot = cp;
+ } else {
+ self.shards.push(cp);
+ }
+ }
+}
diff --git a/crates/events-indexed/src/lib.rs b/crates/events-indexed/src/lib.rs
@@ -0,0 +1,15 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+#[cfg(not(feature = "std"))]
+extern crate alloc;
+
+pub mod checkpoint;
+pub mod manifest;
+pub mod serde_ext;
+pub mod types;
+
+pub use checkpoint::{RadrootsEventsIndexedIndexCheckpoint, RadrootsEventsIndexedShardCheckpoint};
+pub use manifest::{
+ validate_manifest, RadrootsEventsIndexedManifest, RadrootsEventsIndexedManifestError,
+ RadrootsEventsIndexedShardMetadata,
+};
+pub use types::{RadrootsEventsIndexedIdRange, RadrootsEventsIndexedShardId};
diff --git a/crates/events-indexed/src/manifest.rs b/crates/events-indexed/src/manifest.rs
@@ -0,0 +1,84 @@
+#![allow(clippy::module_name_repetitions)]
+#[cfg(not(feature = "std"))]
+use alloc::{string::String, vec::Vec};
+use core::fmt;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsEventsIndexedShardMetadata {
+ pub file: String,
+ pub count: u32,
+ pub first_id: String,
+ pub last_id: String,
+ pub first_published_at: u32,
+ pub last_published_at: u32,
+ pub sha256: String,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsEventsIndexedManifest {
+ pub country: String,
+ pub total: u32,
+ pub shard_size: u32,
+ pub first_published_at: u32,
+ pub last_published_at: u32,
+ pub shards: Vec<RadrootsEventsIndexedShardMetadata>,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum RadrootsEventsIndexedManifestError {
+ EmptyCountry,
+ EmptyShards,
+ EmptyFile(u32),
+ InvalidSha256(u32),
+ InconsistentTotals,
+}
+
+impl fmt::Display for RadrootsEventsIndexedManifestError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ RadrootsEventsIndexedManifestError::EmptyCountry => write!(f, "country is empty"),
+ RadrootsEventsIndexedManifestError::EmptyShards => write!(f, "no shards in manifest"),
+ RadrootsEventsIndexedManifestError::EmptyFile(i) => {
+ write!(f, "shard {} has empty file name", i)
+ }
+ RadrootsEventsIndexedManifestError::InvalidSha256(i) => {
+ write!(f, "shard {} has invalid sha256", i)
+ }
+ RadrootsEventsIndexedManifestError::InconsistentTotals => {
+ write!(f, "total does not match sum of shard counts")
+ }
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsEventsIndexedManifestError {}
+
+pub fn validate_manifest(
+ m: &RadrootsEventsIndexedManifest,
+) -> Result<(), RadrootsEventsIndexedManifestError> {
+ if m.country.trim().is_empty() {
+ return Err(RadrootsEventsIndexedManifestError::EmptyCountry);
+ }
+ if m.shards.is_empty() {
+ return Err(RadrootsEventsIndexedManifestError::EmptyShards);
+ }
+ let mut sum: u64 = 0;
+ for (i, s) in m.shards.iter().enumerate() {
+ if s.file.trim().is_empty() {
+ return Err(RadrootsEventsIndexedManifestError::EmptyFile(i as u32));
+ }
+ if s.sha256.len() != 64 || !s.sha256.chars().all(|c| c.is_ascii_hexdigit()) {
+ return Err(RadrootsEventsIndexedManifestError::InvalidSha256(i as u32));
+ }
+ sum += s.count as u64;
+ }
+ if sum as u32 != m.total {
+ return Err(RadrootsEventsIndexedManifestError::InconsistentTotals);
+ }
+ Ok(())
+}
diff --git a/crates/events-indexed/src/serde_ext.rs b/crates/events-indexed/src/serde_ext.rs
@@ -0,0 +1,17 @@
+#[cfg(feature = "serde")]
+pub mod epoch_seconds {
+ use serde::{de::Error as DeError, Deserialize, Deserializer};
+
+ pub fn de<'de, D>(de: D) -> Result<u32, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let v = u64::deserialize(de)?;
+ if v > u32::MAX as u64 {
+ return Err(D::Error::custom(
+ "timestamp must be epoch **seconds**, not ms",
+ ));
+ }
+ Ok(v as u32)
+ }
+}
diff --git a/crates/events-indexed/src/types.rs b/crates/events-indexed/src/types.rs
@@ -0,0 +1,21 @@
+#[cfg(not(feature = "std"))]
+use alloc::string::String;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RadrootsEventsIndexedShardId(pub String);
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsEventsIndexedIdRange {
+ pub start: String,
+ pub end: String,
+}
+
+impl RadrootsEventsIndexedIdRange {
+ pub fn is_valid(&self) -> bool {
+ !self.start.is_empty() && !self.end.is_empty() && self.start <= self.end
+ }
+}
diff --git a/crates/events/Cargo.toml b/crates/events/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "radroots-events"
+version = "0.1.0"
+authors = ["Radroots Authors"]
+license = "AGPLv3"
+edition = "2021"
+
+[features]
+default = ["std", "serde", "typeshare"]
+std = []
+serde = ["dep:serde"]
+typeshare = ["dep:typeshare"]
+
+[dependencies]
+radroots-core = { path = "../core", default-features = false, features = ["serde"] }
+serde = { version = "1", default-features = false, features = ["derive"], optional = true }
+typeshare = { version = "1", optional = true }
+
+[dev-dependencies]
+serde_json = "1"
+\ No newline at end of file
diff --git a/crates/events/src/comment/models.rs b/crates/events/src/comment/models.rs
@@ -0,0 +1,27 @@
+use serde::{Deserialize, Serialize};
+
+use crate::{RadrootsNostrEvent, RadrootsNostrEventRef};
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsCommentEventIndex {
+ pub event: RadrootsNostrEvent,
+ pub metadata: RadrootsCommentEventMetadata,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsCommentEventMetadata {
+ pub id: String,
+ pub author: String,
+ pub published_at: u32,
+ pub comment: RadrootsComment,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsComment {
+ pub root: RadrootsNostrEventRef,
+ pub parent: RadrootsNostrEventRef,
+ pub content: String,
+}
diff --git a/crates/events/src/follow/models.rs b/crates/events/src/follow/models.rs
@@ -0,0 +1,33 @@
+use crate::RadrootsNostrEvent;
+use serde::{Deserialize, Serialize};
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsFollowEventIndex {
+ pub event: RadrootsNostrEvent,
+ pub metadata: RadrootsFollowEventMetadata,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsFollowEventMetadata {
+ pub id: String,
+ pub author: String,
+ pub published_at: u32,
+ pub follow: RadrootsFollow,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsFollow {
+ pub list: Vec<RadrootsFollowProfile>,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsFollowProfile {
+ pub published_at: u32,
+ pub public_key: String,
+ pub relay_url: Option<String>,
+ pub contact_name: Option<String>,
+}
diff --git a/crates/events/src/job/feedback/models.rs b/crates/events/src/job/feedback/models.rs
@@ -0,0 +1,35 @@
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ job::{JobFeedbackStatus, JobPaymentRequest},
+ RadrootsNostrEvent, RadrootsNostrEventPtr,
+};
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsJobFeedbackEventIndex {
+ pub event: RadrootsNostrEvent,
+ pub metadata: RadrootsJobFeedbackEventMetadata,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsJobFeedbackEventMetadata {
+ pub id: String,
+ pub author: String,
+ pub published_at: u32,
+ pub job_feedback: RadrootsJobFeedback,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub struct RadrootsJobFeedback {
+ pub kind: u16,
+ pub status: JobFeedbackStatus,
+ pub extra_info: Option<String>,
+ pub request_event: RadrootsNostrEventPtr,
+ pub customer_pubkey: Option<String>,
+ pub payment: Option<JobPaymentRequest>,
+ pub content: Option<String>,
+ pub encrypted: bool,
+}
diff --git a/crates/events/src/job/mod.rs b/crates/events/src/job/mod.rs
@@ -0,0 +1,41 @@
+pub mod feedback {
+ pub mod models;
+}
+
+pub mod request {
+ pub mod models;
+}
+
+pub mod result {
+ pub mod models;
+}
+
+use serde::{Deserialize, Serialize};
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Copy)]
+#[serde(rename_all = "snake_case")]
+pub enum JobInputType {
+ Url,
+ Event,
+ Job,
+ Text,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Copy)]
+#[serde(rename_all = "snake_case")]
+pub enum JobFeedbackStatus {
+ PaymentRequired,
+ Processing,
+ Error,
+ Success,
+ Partial,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub struct JobPaymentRequest {
+ pub amount_sat: u32,
+ pub bolt11: Option<String>,
+}
diff --git a/crates/events/src/job/request/models.rs b/crates/events/src/job/request/models.rs
@@ -0,0 +1,50 @@
+use serde::{Deserialize, Serialize};
+
+
+use crate::{job::JobInputType, RadrootsNostrEvent};
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsJobRequestEventIndex {
+ pub event: RadrootsNostrEvent,
+ pub metadata: RadrootsJobRequestEventMetadata,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsJobRequestEventMetadata {
+ pub id: String,
+ pub author: String,
+ pub published_at: u32,
+ pub job_request: RadrootsJobRequest,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub struct RadrootsJobInput {
+ pub data: String,
+ pub input_type: JobInputType,
+ pub relay: Option<String>,
+ pub marker: Option<String>,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub struct RadrootsJobParam {
+ pub key: String,
+ pub value: String,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub struct RadrootsJobRequest {
+ pub kind: u16,
+ pub inputs: Vec<RadrootsJobInput>,
+ pub output: Option<String>,
+ pub params: Vec<RadrootsJobParam>,
+ pub bid_sat: Option<u32>,
+ pub relays: Vec<String>,
+ pub providers: Vec<String>,
+ pub topics: Vec<String>,
+ pub encrypted: bool,
+}
diff --git a/crates/events/src/job/result/models.rs b/crates/events/src/job/result/models.rs
@@ -0,0 +1,35 @@
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ job::{request::models::RadrootsJobInput, JobPaymentRequest},
+ RadrootsNostrEvent, RadrootsNostrEventPtr,
+};
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsJobResultEventIndex {
+ pub event: RadrootsNostrEvent,
+ pub metadata: RadrootsJobResultEventMetadata,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsJobResultEventMetadata {
+ pub id: String,
+ pub author: String,
+ pub published_at: u32,
+ pub job_result: RadrootsJobResult,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub struct RadrootsJobResult {
+ pub kind: u16,
+ pub request_event: RadrootsNostrEventPtr,
+ pub request_json: Option<String>,
+ pub inputs: Vec<RadrootsJobInput>,
+ pub customer_pubkey: Option<String>,
+ pub payment: Option<JobPaymentRequest>,
+ pub content: Option<String>,
+ pub encrypted: bool,
+}
diff --git a/crates/events/src/kinds.rs b/crates/events/src/kinds.rs
@@ -0,0 +1,38 @@
+#[typeshare::typeshare]
+pub const KIND_APPLICATION_HANDLER: u32 = 31990;
+
+#[typeshare::typeshare]
+pub const KIND_JOB_REQUEST_MIN: u32 = 5000;
+#[typeshare::typeshare]
+pub const KIND_JOB_REQUEST_MAX: u32 = 5999;
+#[typeshare::typeshare]
+pub const KIND_JOB_RESULT_MIN: u32 = 6000;
+#[typeshare::typeshare]
+pub const KIND_JOB_RESULT_MAX: u32 = 6999;
+#[typeshare::typeshare]
+pub const KIND_JOB_FEEDBACK: u32 = 7000;
+
+#[inline]
+pub const fn is_request_kind(kind: u32) -> bool {
+ kind >= KIND_JOB_REQUEST_MIN && kind <= KIND_JOB_REQUEST_MAX
+}
+#[inline]
+pub const fn is_result_kind(kind: u32) -> bool {
+ kind >= KIND_JOB_RESULT_MIN && kind <= KIND_JOB_RESULT_MAX
+}
+#[inline]
+pub const fn result_kind_for_request_kind(kind: u32) -> Option<u32> {
+ if is_request_kind(kind) {
+ Some(kind + 1000)
+ } else {
+ None
+ }
+}
+#[inline]
+pub const fn request_kind_for_result_kind(kind: u32) -> Option<u32> {
+ if is_result_kind(kind) {
+ Some(kind - 1000)
+ } else {
+ None
+ }
+}
diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs
@@ -0,0 +1,54 @@
+pub mod job;
+pub mod kinds;
+pub mod tag;
+
+pub mod comment {
+ pub mod models;
+}
+
+pub mod follow {
+ pub mod models;
+}
+
+pub mod listing {
+ pub mod models;
+}
+
+pub mod profile {
+ pub mod models;
+}
+
+pub mod reaction {
+ pub mod models;
+}
+
+use serde::{Deserialize, Serialize};
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsNostrEvent {
+ pub id: String,
+ pub author: String,
+ pub created_at: u32,
+ pub kind: u32,
+ pub tags: Vec<Vec<String>>,
+ pub content: String,
+ pub sig: String,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsNostrEventRef {
+ pub id: String,
+ pub author: String,
+ pub kind: u32,
+ pub d_tag: Option<String>,
+ pub relays: Option<Vec<String>>,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub struct RadrootsNostrEventPtr {
+ pub id: String,
+ pub relays: Option<String>,
+}
diff --git a/crates/events/src/listing/models.rs b/crates/events/src/listing/models.rs
@@ -0,0 +1,109 @@
+use radroots_core::{
+ RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCorePercent, RadrootsCoreQuantity,
+ RadrootsCoreQuantityPrice,
+};
+use serde::{Deserialize, Serialize};
+
+use crate::RadrootsNostrEvent;
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsListingEventIndex {
+ pub event: RadrootsNostrEvent,
+ pub metadata: RadrootsListingEventMetadata,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsListingEventMetadata {
+ pub id: String,
+ pub author: String,
+ pub published_at: u32,
+ pub listing: RadrootsListing,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsListing {
+ pub d_tag: String,
+ pub product: RadrootsListingProduct,
+ pub quantities: Vec<RadrootsListingQuantity>,
+ pub prices: Vec<RadrootsListingPrice>,
+ pub discounts: Option<Vec<RadrootsListingDiscount>>,
+ pub location: Option<RadrootsListingLocation>,
+ pub images: Option<Vec<RadrootsListingImage>>,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsListingProduct {
+ pub key: String,
+ pub title: String,
+ pub category: String,
+ pub summary: Option<String>,
+ pub process: Option<String>,
+ pub lot: Option<String>,
+ pub location: Option<String>,
+ pub profile: Option<String>,
+ pub year: Option<String>,
+}
+
+#[typeshare::typeshare]
+pub type RadrootsListingPrice = RadrootsCoreQuantityPrice;
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsListingQuantity {
+ pub value: RadrootsCoreQuantity,
+ pub label: Option<String>,
+ pub count: Option<u32>,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", tag = "kind", content = "amount")]
+pub enum RadrootsListingDiscount {
+ Quantity {
+ ref_quantity: String,
+ threshold: RadrootsCoreQuantity,
+ value: RadrootsCoreMoney,
+ },
+ Mass {
+ threshold: RadrootsCoreQuantity,
+ value: RadrootsCoreMoney,
+ },
+ Subtotal {
+ threshold: RadrootsCoreMoney,
+ value: RadrootsCoreDiscountValue,
+ },
+ Total {
+ total_min: RadrootsCoreMoney,
+ value: RadrootsCorePercent,
+ },
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsListingLocation {
+ pub primary: String,
+ pub city: Option<String>,
+ pub region: Option<String>,
+ pub country: Option<String>,
+ pub lat: Option<f64>,
+ pub lng: Option<f64>,
+ pub geohash: Option<String>,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsListingImage {
+ pub url: String,
+ pub size: Option<RadrootsListingImageSize>,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsListingImageSize {
+ pub w: u32,
+ pub h: u32,
+}
diff --git a/crates/events/src/profile/models.rs b/crates/events/src/profile/models.rs
@@ -0,0 +1,33 @@
+use crate::RadrootsNostrEvent;
+use serde::{Deserialize, Serialize};
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsProfileEventIndex {
+ pub event: RadrootsNostrEvent,
+ pub metadata: RadrootsProfileEventMetadata,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsProfileEventMetadata {
+ pub id: String,
+ pub author: String,
+ pub published_at: u32,
+ pub profile: RadrootsProfile,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsProfile {
+ pub name: String,
+ pub display_name: Option<String>,
+ pub nip05: Option<String>,
+ pub about: Option<String>,
+ pub website: Option<String>,
+ pub picture: Option<String>,
+ pub banner: Option<String>,
+ pub lud06: Option<String>,
+ pub lud16: Option<String>,
+ pub bot: Option<String>,
+}
diff --git a/crates/events/src/reaction/models.rs b/crates/events/src/reaction/models.rs
@@ -0,0 +1,25 @@
+use crate::{RadrootsNostrEvent, RadrootsNostrEventRef};
+use serde::{Deserialize, Serialize};
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsReactionEventIndex {
+ pub event: RadrootsNostrEvent,
+ pub metadata: RadrootsReactionEventMetadata,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsReactionEventMetadata {
+ pub id: String,
+ pub author: String,
+ pub published_at: u32,
+ pub reaction: RadrootsReaction,
+}
+
+#[typeshare::typeshare]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RadrootsReaction {
+ pub root: RadrootsNostrEventRef,
+ pub content: String,
+}
diff --git a/crates/events/src/tag.rs b/crates/events/src/tag.rs
@@ -0,0 +1,3 @@
+pub const TAG_E_ROOT: &str = "e_root";
+pub const TAG_E_PREV: &str = "e_prev";
+pub const TAG_D: &str = "d";
diff --git a/crates/trade/Cargo.toml b/crates/trade/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "radroots-trade"
+version = "0.1.0"
+authors = ["Radroots Authors"]
+license = "AGPLv3"
+edition = "2021"
+
+[features]
+default = ["std", "serde", "typeshare"]
+std = []
+serde = ["dep:serde", "radroots-core/serde", "radroots-events/serde", "radroots-events-codec/serde"]
+typeshare = ["dep:typeshare"]
+
+[dependencies]
+radroots-core = { path = "../core", default-features = false }
+radroots-events = { path = "../events", default-features = false }
+radroots-events-codec = { path = "../events-codec", default-features = false }
+serde = { version = "1", default-features = false, features = ["derive"], optional = true }
+typeshare = { version = "1", optional = true }
diff --git a/crates/trade/src/lib.rs b/crates/trade/src/lib.rs
@@ -0,0 +1,6 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+#[cfg(not(feature = "std"))]
+extern crate alloc;
+
+pub mod listing;
+pub mod prelude;
diff --git a/crates/trade/src/listing/kinds.rs b/crates/trade/src/listing/kinds.rs
@@ -0,0 +1,60 @@
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_ORDER_REQ: u16 = 5301;
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_ORDER_RES: u16 = 6301;
+
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_ACCEPT_REQ: u16 = 5302;
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_ACCEPT_RES: u16 = 6302;
+
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_CONVEYANCE_REQ: u16 = 5303;
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_CONVEYANCE_RES: u16 = 6303;
+
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_INVOICE_REQ: u16 = 5304;
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_INVOICE_RES: u16 = 6304;
+
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_PAYMENT_REQ: u16 = 5305;
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_PAYMENT_RES: u16 = 6305;
+
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_FULFILL_REQ: u16 = 5306;
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_FULFILL_RES: u16 = 6306;
+
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_RECEIPT_REQ: u16 = 5307;
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_RECEIPT_RES: u16 = 6307;
+
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_CANCEL_REQ: u16 = 5309;
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_CANCEL_RES: u16 = 6309;
+
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_REFUND_REQ: u16 = 5310;
+#[typeshare::typeshare]
+pub const KIND_TRADE_LISTING_REFUND_RES: u16 = 6310;
+
+#[inline]
+pub const fn is_trade_listing_request_kind(kind: u16) -> bool {
+ matches!(
+ kind,
+ KIND_TRADE_LISTING_ORDER_REQ
+ | KIND_TRADE_LISTING_ACCEPT_REQ
+ | KIND_TRADE_LISTING_CONVEYANCE_REQ
+ | KIND_TRADE_LISTING_INVOICE_REQ
+ | KIND_TRADE_LISTING_PAYMENT_REQ
+ | KIND_TRADE_LISTING_FULFILL_REQ
+ | KIND_TRADE_LISTING_RECEIPT_REQ
+ | KIND_TRADE_LISTING_CANCEL_REQ
+ | KIND_TRADE_LISTING_REFUND_REQ
+ )
+}
diff --git a/crates/trade/src/listing/meta.rs b/crates/trade/src/listing/meta.rs
@@ -0,0 +1,64 @@
+use core::fmt;
+use core::str::FromStr;
+
+pub const MARKER_LISTING: &str = "listing";
+pub const MARKER_PAYLOAD: &str = "payload";
+pub const MARKER_PREVIOUS: &str = "previous";
+
+pub const MARKER_ACCEPT_RESULT: &str = "accept_result";
+pub const MARKER_INVOICE_RESULT: &str = "invoice_result";
+pub const MARKER_FULFILLMENT_RESULT: &str = "fulfillment_result";
+pub const MARKER_PROOF: &str = "proof";
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+#[cfg_attr(
+ feature = "serde",
+ serde(rename_all = "snake_case", tag = "kind", content = "amount")
+)]
+pub enum TradeListingStage {
+ Order,
+ Accept,
+ Conveyance,
+ Invoice,
+ Payment,
+ Fulfillment,
+ Receipt,
+ Cancel,
+ Refund,
+}
+
+impl fmt::Display for TradeListingStage {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(match self {
+ TradeListingStage::Order => "order",
+ TradeListingStage::Accept => "accept",
+ TradeListingStage::Conveyance => "conveyance",
+ TradeListingStage::Invoice => "invoice",
+ TradeListingStage::Payment => "payment",
+ TradeListingStage::Fulfillment => "fulfillment",
+ TradeListingStage::Receipt => "receipt",
+ TradeListingStage::Cancel => "cancel",
+ TradeListingStage::Refund => "refund",
+ })
+ }
+}
+
+impl FromStr for TradeListingStage {
+ type Err = ();
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "order" => Ok(Self::Order),
+ "accept" => Ok(Self::Accept),
+ "conveyance" => Ok(Self::Conveyance),
+ "invoice" => Ok(Self::Invoice),
+ "payment" => Ok(Self::Payment),
+ "fulfillment" => Ok(Self::Fulfillment),
+ "receipt" => Ok(Self::Receipt),
+ "cancel" => Ok(Self::Cancel),
+ "refund" => Ok(Self::Refund),
+ _ => Err(()),
+ }
+ }
+}
diff --git a/crates/trade/src/listing/mod.rs b/crates/trade/src/listing/mod.rs
@@ -0,0 +1,15 @@
+pub mod kinds;
+pub mod meta;
+pub mod model;
+pub mod price_ext;
+pub mod tags;
+
+pub mod stage {
+ pub mod accept;
+ pub mod conveyance;
+ pub mod fulfillment;
+ pub mod invoice;
+ pub mod order;
+ pub mod payment;
+ pub mod receipt;
+}
diff --git a/crates/trade/src/listing/model.rs b/crates/trade/src/listing/model.rs
@@ -0,0 +1,22 @@
+#[cfg(not(feature = "std"))]
+use alloc::{string::String, vec::Vec};
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsTradeListingSubtotal {
+ pub price_amount: radroots_core::RadrootsCoreMoney,
+ pub price_currency: radroots_core::RadrootsCoreCurrency,
+ pub quantity_amount: radroots_core::RadrootsCoreDecimal,
+ pub quantity_unit: radroots_core::RadrootsCoreUnit,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsTradeListingTotal {
+ pub price_amount: radroots_core::RadrootsCoreMoney,
+ pub price_currency: radroots_core::RadrootsCoreCurrency,
+ pub quantity_amount: radroots_core::RadrootsCoreDecimal,
+ pub quantity_unit: radroots_core::RadrootsCoreUnit,
+}
diff --git a/crates/trade/src/listing/price_ext.rs b/crates/trade/src/listing/price_ext.rs
@@ -0,0 +1,44 @@
+use crate::listing::model::{RadrootsTradeListingSubtotal, RadrootsTradeListingTotal};
+use radroots_core::{
+ RadrootsCoreDecimal, RadrootsCoreQuantity, RadrootsCoreQuantityPrice,
+ RadrootsCoreQuantityPriceOps,
+};
+use radroots_events::listing::models::RadrootsListingQuantity;
+
+pub trait AsCoreQuantityPrice {
+ fn as_core_qp(&self) -> RadrootsCoreQuantityPrice;
+}
+
+pub trait ListingPricingExt {
+ fn subtotal_for(&self, qty: &RadrootsListingQuantity) -> RadrootsTradeListingSubtotal;
+ fn total_for(&self, qty: &RadrootsListingQuantity) -> RadrootsTradeListingTotal;
+}
+
+impl ListingPricingExt for RadrootsCoreQuantityPrice {
+ fn subtotal_for(&self, qty: &RadrootsListingQuantity) -> RadrootsTradeListingSubtotal {
+ let count = qty.count.unwrap_or(1);
+ let effective_qty = RadrootsCoreQuantity::new(
+ qty.value.amount * RadrootsCoreDecimal::from(count as u32),
+ qty.value.unit,
+ );
+
+ let money = self.cost_for_rounded(&effective_qty);
+
+ RadrootsTradeListingSubtotal {
+ price_amount: money.clone(),
+ price_currency: self.amount.currency,
+ quantity_amount: effective_qty.amount,
+ quantity_unit: effective_qty.unit,
+ }
+ }
+
+ fn total_for(&self, qty: &RadrootsListingQuantity) -> RadrootsTradeListingTotal {
+ let sub = self.subtotal_for(qty);
+ RadrootsTradeListingTotal {
+ price_amount: sub.price_amount,
+ price_currency: sub.price_currency,
+ quantity_amount: sub.quantity_amount,
+ quantity_unit: sub.quantity_unit,
+ }
+ }
+}
diff --git a/crates/trade/src/listing/stage/accept.rs b/crates/trade/src/listing/stage/accept.rs
@@ -0,0 +1,19 @@
+#[cfg(not(feature = "std"))]
+use alloc::string::String;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingAcceptRequest {
+ pub order_result_event_id: String,
+ pub listing_event_id: String,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingAcceptResult {
+ pub listing_event_id: String,
+ pub order_result_event_id: String,
+ pub accepted_by: String,
+}
diff --git a/crates/trade/src/listing/stage/conveyance.rs b/crates/trade/src/listing/stage/conveyance.rs
@@ -0,0 +1,41 @@
+#![cfg_attr(not(feature = "serde"), allow(unused_attributes))]
+
+#[cfg(not(feature = "std"))]
+use alloc::string::String;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+#[serde(rename_all = "snake_case", tag = "kind", content = "amount")]
+pub enum TradeListingConveyanceMethod {
+ SellerDelivery {
+ window: Option<String>,
+ notes: Option<String>,
+ },
+ BuyerPickup {
+ location_hint: Option<String>,
+ by_when: Option<String>,
+ },
+ ThirdParty {
+ provider: String,
+ ref_id: Option<String>,
+ notes: Option<String>,
+ },
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingConveyanceRequest {
+ pub accept_result_event_id: String,
+ pub method: TradeListingConveyanceMethod,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingConveyanceResult {
+ pub verified: bool,
+ pub method: TradeListingConveyanceMethod,
+ pub message: Option<String>,
+}
diff --git a/crates/trade/src/listing/stage/fulfillment.rs b/crates/trade/src/listing/stage/fulfillment.rs
@@ -0,0 +1,34 @@
+#[cfg(not(feature = "std"))]
+use alloc::string::String;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingFulfillmentRequest {
+ pub payment_result_event_id: String,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+#[cfg_attr(
+ feature = "serde",
+ serde(rename_all = "snake_case", tag = "kind", content = "amount")
+)]
+pub enum TradeListingFulfillmentState {
+ Preparing,
+ Shipped,
+ ReadyForPickup,
+ Delivered,
+ Canceled,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingFulfillmentResult {
+ pub state: TradeListingFulfillmentState,
+ pub tracking: Option<String>,
+ pub eta: Option<String>,
+ pub notes: Option<String>,
+}
diff --git a/crates/trade/src/listing/stage/invoice.rs b/crates/trade/src/listing/stage/invoice.rs
@@ -0,0 +1,19 @@
+#[cfg(not(feature = "std"))]
+use alloc::string::String;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingInvoiceRequest {
+ pub accept_result_event_id: String,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingInvoiceResult {
+ pub total_sat: u32,
+ pub bolt11: Option<String>,
+ pub note: Option<String>,
+ pub expires_at: Option<u32>,
+}
diff --git a/crates/trade/src/listing/stage/order.rs b/crates/trade/src/listing/stage/order.rs
@@ -0,0 +1,33 @@
+#[cfg(not(feature = "std"))]
+use alloc::{string::String, vec::Vec};
+use radroots_core::RadrootsCoreQuantityPrice;
+use radroots_events::listing::models::{RadrootsListingDiscount, RadrootsListingQuantity};
+
+use crate::listing::model::{RadrootsTradeListingSubtotal, RadrootsTradeListingTotal};
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingOrderRequestPayload {
+ pub price: RadrootsCoreQuantityPrice,
+ pub quantity: RadrootsListingQuantity,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingOrderRequest {
+ pub event: radroots_events::RadrootsNostrEventPtr,
+ pub payload: TradeListingOrderRequestPayload,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingOrderResult {
+ pub quantity: RadrootsListingQuantity,
+ pub price: RadrootsCoreQuantityPrice,
+ pub discounts: Vec<RadrootsListingDiscount>,
+ pub subtotal: RadrootsTradeListingSubtotal,
+ pub total: RadrootsTradeListingTotal,
+}
diff --git a/crates/trade/src/listing/stage/payment.rs b/crates/trade/src/listing/stage/payment.rs
@@ -0,0 +1,31 @@
+#![cfg_attr(not(feature = "serde"), allow(unused_attributes))]
+
+#[cfg(not(feature = "std"))]
+use alloc::string::String;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+#[serde(rename_all = "snake_case", tag = "kind", content = "amount")]
+pub enum TradeListingPaymentProof {
+ ZapEvent { id: String },
+ Preimage { hex: String },
+ Txid { id: String },
+ ExternalRef { provider: String, ref_id: String },
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingPaymentProofRequest {
+ pub invoice_result_event_id: String,
+ pub proof: TradeListingPaymentProof,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingPaymentResult {
+ pub verified: bool,
+ pub message: Option<String>,
+}
diff --git a/crates/trade/src/listing/stage/receipt.rs b/crates/trade/src/listing/stage/receipt.rs
@@ -0,0 +1,18 @@
+#[cfg(not(feature = "std"))]
+use alloc::string::String;
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingReceiptRequest {
+ pub fulfillment_result_event_id: String,
+ pub note: Option<String>,
+}
+
+#[typeshare::typeshare]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct TradeListingReceiptResult {
+ pub acknowledged: bool,
+ pub at: u32,
+}
diff --git a/crates/trade/src/listing/tags.rs b/crates/trade/src/listing/tags.rs
@@ -0,0 +1,38 @@
+#[cfg(not(feature = "std"))]
+use alloc::{string::String, vec::Vec};
+
+use radroots_events::tag::{TAG_D, TAG_E_PREV, TAG_E_ROOT};
+use radroots_events_codec::job::error::JobParseError;
+
+#[inline]
+pub fn push_trade_listing_chain_tags(
+ tags: &mut Vec<Vec<String>>,
+ e_root_id: impl Into<String>,
+ e_prev_id: Option<impl Into<String>>,
+ trade_id: Option<impl Into<String>>,
+) {
+ tags.push(vec![TAG_E_ROOT.into(), e_root_id.into()]);
+ if let Some(prev) = e_prev_id {
+ tags.push(vec![TAG_E_PREV.into(), prev.into()]);
+ }
+ if let Some(d) = trade_id {
+ tags.push(vec![TAG_D.into(), d.into()]);
+ }
+}
+
+#[inline]
+pub fn validate_trade_listing_chain(tags: &[Vec<String>]) -> Result<(), JobParseError> {
+ let has_root = tags
+ .iter()
+ .any(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_E_ROOT));
+ if !has_root {
+ return Err(JobParseError::MissingChainTag(TAG_E_ROOT));
+ }
+ let has_d = tags
+ .iter()
+ .any(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_D));
+ if !has_d {
+ return Err(JobParseError::MissingChainTag(TAG_D));
+ }
+ Ok(())
+}
diff --git a/crates/trade/src/prelude.rs b/crates/trade/src/prelude.rs
@@ -0,0 +1 @@
+pub use crate::listing::*;
diff --git a/package.json b/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "crates-packages",
+ "private": true,
+ "scripts": {
+ "build": "turbo run build"
+ },
+ "devDependencies": {
+ "turbo": "2.5.3",
+ "typescript": "^5.8.3"
+ },
+ "workspaces": {
+ "packages": [
+ "../../global/packages/*",
+ "packages/bindings/*"
+ ]
+ },
+ "packageManager": "yarn@1.22.22"
+}
+\ No newline at end of file
diff --git a/packages/bindings/core/package.json b/packages/bindings/core/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "@radroots/core-bindings",
+ "version": "1.0.0",
+ "private": true,
+ "license": "AGPLv3",
+ "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"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "sideEffects": false,
+ "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"
+ },
+ "devDependencies": {
+ "@radroots/dev": "*",
+ "@radroots/tsconfig": "*",
+ "rimraf": "^6.0.1",
+ "ts-to-zod": "^3.15.0"
+ },
+ "dependencies": {
+ "zod": "^4.0.5"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
+\ No newline at end of file
diff --git a/packages/bindings/core/src/index.ts b/packages/bindings/core/src/index.ts
@@ -0,0 +1 @@
+export * from "./types.js"
diff --git a/packages/bindings/core/src/types.ts b/packages/bindings/core/src/types.ts
@@ -0,0 +1,61 @@
+/*
+ Generated by typeshare 1.13.3
+*/
+
+export type RadrootsCoreCurrency = [number, number, number];
+
+export type RadrootsCoreDecimal = number;
+
+export interface RadrootsCoreMoney {
+ amount: RadrootsCoreDecimal;
+ currency: RadrootsCoreCurrency;
+}
+
+export interface RadrootsCorePercent {
+ value: RadrootsCoreDecimal;
+}
+
+export enum RadrootsCoreUnit {
+ Each = "Each",
+ MassKg = "MassKg",
+ MassG = "MassG",
+ MassOz = "MassOz",
+ MassLb = "MassLb",
+ VolumeL = "VolumeL",
+ VolumeMl = "VolumeMl",
+}
+
+export interface RadrootsCoreQuantity {
+ amount: RadrootsCoreDecimal;
+ unit: RadrootsCoreUnit;
+ label?: string;
+}
+
+export interface RadrootsCoreQuantityPrice {
+ amount: RadrootsCoreMoney;
+ quantity: RadrootsCoreQuantity;
+}
+
+export type RadrootsCoreDiscount =
+ | { kind: "quantity_threshold", amount: {
+ ref_key?: string;
+ threshold: RadrootsCoreQuantity;
+ value: RadrootsCoreMoney;
+}}
+ | { kind: "mass_threshold", amount: {
+ threshold: RadrootsCoreQuantity;
+ value: RadrootsCoreMoney;
+}}
+ | { kind: "subtotal_threshold", amount: {
+ threshold: RadrootsCoreMoney;
+ value: RadrootsCoreDiscountValue;
+}}
+ | { kind: "total_threshold", amount: {
+ total_min: RadrootsCoreMoney;
+ value: RadrootsCorePercent;
+}};
+
+export type RadrootsCoreDiscountValue =
+ | { kind: "money", amount: RadrootsCoreMoney }
+ | { kind: "percent", amount: RadrootsCorePercent };
+
diff --git a/bindings/ts/tsconfig.cjs.json b/packages/bindings/core/tsconfig.cjs.json
diff --git a/bindings/ts/tsconfig.esm.json b/packages/bindings/core/tsconfig.esm.json
diff --git a/bindings/ts/tsconfig.json b/packages/bindings/core/tsconfig.json
diff --git a/packages/bindings/events-indexed/package.json b/packages/bindings/events-indexed/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "@radroots/events-indexed-bindings",
+ "version": "1.0.0",
+ "private": true,
+ "license": "AGPLv3",
+ "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"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "sideEffects": false,
+ "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"
+ },
+ "devDependencies": {
+ "@radroots/dev": "*",
+ "@radroots/tsconfig": "*",
+ "rimraf": "^6.0.1",
+ "ts-to-zod": "^3.15.0"
+ },
+ "dependencies": {
+ "zod": "^4.0.5"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
+\ No newline at end of file
diff --git a/packages/bindings/events-indexed/src/index.ts b/packages/bindings/events-indexed/src/index.ts
@@ -0,0 +1 @@
+export * from "./types.js"
diff --git a/packages/bindings/events-indexed/src/types.ts b/packages/bindings/events-indexed/src/types.ts
@@ -0,0 +1,42 @@
+/*
+ Generated by typeshare 1.13.3
+*/
+
+export type RadrootsEventsIndexedShardId = string;
+
+export interface RadrootsEventsIndexedIdRange {
+ start: string;
+ end: string;
+}
+
+export interface RadrootsEventsIndexedShardCheckpoint {
+ shard_id: RadrootsEventsIndexedShardId;
+ last_created_at: number;
+ last_event_id?: string;
+ cursor?: string;
+}
+
+export interface RadrootsEventsIndexedIndexCheckpoint {
+ generated_at: number;
+ shards: RadrootsEventsIndexedShardCheckpoint[];
+}
+
+export interface RadrootsEventsIndexedShardMetadata {
+ file: string;
+ count: number;
+ first_id: string;
+ last_id: string;
+ first_published_at: number;
+ last_published_at: number;
+ sha256: string;
+}
+
+export interface RadrootsEventsIndexedManifest {
+ country: string;
+ total: number;
+ shard_size: number;
+ first_published_at: number;
+ last_published_at: number;
+ shards: RadrootsEventsIndexedShardMetadata[];
+}
+
diff --git a/bindings/ts/tsconfig.cjs.json b/packages/bindings/events-indexed/tsconfig.cjs.json
diff --git a/bindings/ts/tsconfig.esm.json b/packages/bindings/events-indexed/tsconfig.esm.json
diff --git a/bindings/ts/tsconfig.json b/packages/bindings/events-indexed/tsconfig.json
diff --git a/packages/bindings/events/package.json b/packages/bindings/events/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@radroots/events-bindings",
+ "version": "1.0.0",
+ "private": true,
+ "license": "AGPLv3",
+ "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"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "sideEffects": false,
+ "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"
+ },
+ "devDependencies": {
+ "@radroots/dev": "*",
+ "@radroots/tsconfig": "*",
+ "rimraf": "^6.0.1",
+ "ts-to-zod": "^3.15.0"
+ },
+ "dependencies": {
+ "@radroots/core-bindings": "*",
+ "zod": "^4.0.5"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
+\ No newline at end of file
diff --git a/packages/bindings/events/src/index.ts b/packages/bindings/events/src/index.ts
@@ -0,0 +1,3 @@
+export * from "./lib.js"
+export * from "./schemas.js"
+export * from "./types.js"
diff --git a/packages/bindings/events/src/lib.ts b/packages/bindings/events/src/lib.ts
@@ -0,0 +1,3 @@
+export const TAG_E_ROOT = "e_root";
+export const TAG_E_PREV = "e_prev";
+export const TAG_D = "d";
+\ No newline at end of file
diff --git a/packages/bindings/events/src/schemas.ts b/packages/bindings/events/src/schemas.ts
@@ -0,0 +1,119 @@
+import { z } from "zod";
+
+export const radroots_listing_image_schema = z.object({
+ url: z.string(),
+ size: z.object({
+ w: z.number(),
+ h: z.number()
+ }).optional()
+});
+
+export const radroots_listing_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(),
+ geohash: z.string().optional()
+});
+
+export const radroots_listing_discount_schema = z.union([
+ z.object({
+ kind: z.literal("quantity"),
+ amount: z.object({
+ ref_quantity: z.string(),
+ threshold: z.any(),
+ value: z.any()
+ })
+ }),
+ z.object({
+ kind: z.literal("mass"),
+ amount: z.object({
+ threshold: z.any(),
+ value: z.any()
+ })
+ }),
+ z.object({
+ kind: z.literal("subtotal"),
+ amount: z.object({
+ threshold: z.any(),
+ value: z.any()
+ })
+ }),
+ z.object({
+ kind: z.literal("total"),
+ amount: z.object({
+ total_min: z.any(),
+ value: z.any()
+ })
+ })
+]);
+
+export const radroots_listing_price_schema = z.object({
+ amount: z.any(),
+ quantity: z.any()
+});
+
+export const radroots_listing_quantity_schema = z.object({
+ value: z.any(),
+ label: z.string().optional(),
+ count: z.number().optional()
+});
+
+export const radroots_listing_product_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 radroots_listing_schema = z.object({
+ d_tag: z.string(),
+ product: radroots_listing_product_schema,
+ quantities: z.array(radroots_listing_quantity_schema),
+ prices: z.array(radroots_listing_price_schema),
+ discounts: z.array(radroots_listing_discount_schema).optional(),
+ location: radroots_listing_location_schema.optional(),
+ images: z.array(radroots_listing_image_schema).optional()
+});
+
+export const radroots_profile_schema = z.object({
+ name: z.string(),
+ display_name: z.string().optional(),
+ nip05: z.string().optional(),
+ about: z.string().optional(),
+ website: z.string().optional(),
+ picture: z.string().optional(),
+ banner: z.string().optional(),
+ lud06: z.string().optional(),
+ lud16: z.string().optional(),
+ bot: z.string().optional()
+});
+
+export const radroots_comment_schema = z.object({
+ root: z.any(),
+ parent: z.any(),
+ content: z.string()
+});
+
+export const radroots_reaction_schema = z.object({
+ root: z.any(),
+ content: z.string()
+});
+
+export const radroots_follow_profile_schema = z.object({
+ published_at: z.number(),
+ public_key: z.string(),
+ relay_url: z.string().optional(),
+ contact_name: z.string().optional()
+});
+
+export const radroots_follow_schema = z.object({
+ list: z.array(radroots_follow_profile_schema)
+});
diff --git a/packages/bindings/events/src/types.ts b/packages/bindings/events/src/types.ts
@@ -0,0 +1,301 @@
+import { RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCorePercent, RadrootsCoreQuantity, RadrootsCoreQuantityPrice } from "@radroots/core-bindings";
+
+/*
+ Generated by typeshare 1.13.3
+*/
+
+export type RadrootsListingPrice = RadrootsCoreQuantityPrice;
+
+export interface JobPaymentRequest {
+ amount_sat: number;
+ bolt11?: string;
+}
+
+export interface RadrootsNostrEventRef {
+ id: string;
+ author: string;
+ kind: number;
+ d_tag?: string;
+ relays?: string[];
+}
+
+export interface RadrootsComment {
+ root: RadrootsNostrEventRef;
+ parent: RadrootsNostrEventRef;
+ content: string;
+}
+
+export interface RadrootsNostrEvent {
+ id: string;
+ author: string;
+ created_at: number;
+ kind: number;
+ tags: string[][];
+ content: string;
+ sig: string;
+}
+
+export interface RadrootsCommentEventMetadata {
+ id: string;
+ author: string;
+ published_at: number;
+ comment: RadrootsComment;
+}
+
+export interface RadrootsCommentEventIndex {
+ event: RadrootsNostrEvent;
+ metadata: RadrootsCommentEventMetadata;
+}
+
+export interface RadrootsFollowProfile {
+ published_at: number;
+ public_key: string;
+ relay_url?: string;
+ contact_name?: string;
+}
+
+export interface RadrootsFollow {
+ list: RadrootsFollowProfile[];
+}
+
+export interface RadrootsFollowEventMetadata {
+ id: string;
+ author: string;
+ published_at: number;
+ follow: RadrootsFollow;
+}
+
+export interface RadrootsFollowEventIndex {
+ event: RadrootsNostrEvent;
+ metadata: RadrootsFollowEventMetadata;
+}
+
+export enum JobFeedbackStatus {
+ PaymentRequired = "payment_required",
+ Processing = "processing",
+ Error = "error",
+ Success = "success",
+ Partial = "partial",
+}
+
+export interface RadrootsNostrEventPtr {
+ id: string;
+ relays?: string;
+}
+
+export interface RadrootsJobFeedback {
+ kind: number;
+ status: JobFeedbackStatus;
+ extra_info?: string;
+ request_event: RadrootsNostrEventPtr;
+ customer_pubkey?: string;
+ payment?: JobPaymentRequest;
+ content?: string;
+ encrypted: boolean;
+}
+
+export interface RadrootsJobFeedbackEventMetadata {
+ id: string;
+ author: string;
+ published_at: number;
+ job_feedback: RadrootsJobFeedback;
+}
+
+export interface RadrootsJobFeedbackEventIndex {
+ event: RadrootsNostrEvent;
+ metadata: RadrootsJobFeedbackEventMetadata;
+}
+
+export enum JobInputType {
+ Url = "url",
+ Event = "event",
+ Job = "job",
+ Text = "text",
+}
+
+export interface RadrootsJobInput {
+ data: string;
+ input_type: JobInputType;
+ relay?: string;
+ marker?: string;
+}
+
+export interface RadrootsJobParam {
+ key: string;
+ value: string;
+}
+
+export interface RadrootsJobRequest {
+ kind: number;
+ inputs: RadrootsJobInput[];
+ output?: string;
+ params: RadrootsJobParam[];
+ bid_sat?: number;
+ relays: string[];
+ providers: string[];
+ topics: string[];
+ encrypted: boolean;
+}
+
+export interface RadrootsJobRequestEventMetadata {
+ id: string;
+ author: string;
+ published_at: number;
+ job_request: RadrootsJobRequest;
+}
+
+export interface RadrootsJobRequestEventIndex {
+ event: RadrootsNostrEvent;
+ metadata: RadrootsJobRequestEventMetadata;
+}
+
+export interface RadrootsJobResult {
+ kind: number;
+ request_event: RadrootsNostrEventPtr;
+ request_json?: string;
+ inputs: RadrootsJobInput[];
+ customer_pubkey?: string;
+ payment?: JobPaymentRequest;
+ content?: string;
+ encrypted: boolean;
+}
+
+export interface RadrootsJobResultEventMetadata {
+ id: string;
+ author: string;
+ published_at: number;
+ job_result: RadrootsJobResult;
+}
+
+export interface RadrootsJobResultEventIndex {
+ event: RadrootsNostrEvent;
+ metadata: RadrootsJobResultEventMetadata;
+}
+
+export interface RadrootsListingProduct {
+ key: string;
+ title: string;
+ category: string;
+ summary?: string;
+ process?: string;
+ lot?: string;
+ location?: string;
+ profile?: string;
+ year?: string;
+}
+
+export interface RadrootsListingQuantity {
+ value: RadrootsCoreQuantity;
+ label?: string;
+ count?: number;
+}
+
+export type RadrootsListingDiscount =
+ | { kind: "quantity", amount: {
+ ref_quantity: string;
+ threshold: RadrootsCoreQuantity;
+ value: RadrootsCoreMoney;
+}}
+ | { kind: "mass", amount: {
+ threshold: RadrootsCoreQuantity;
+ value: RadrootsCoreMoney;
+}}
+ | { kind: "subtotal", amount: {
+ threshold: RadrootsCoreMoney;
+ value: RadrootsCoreDiscountValue;
+}}
+ | { kind: "total", amount: {
+ total_min: RadrootsCoreMoney;
+ value: RadrootsCorePercent;
+}};
+
+export interface RadrootsListingLocation {
+ primary: string;
+ city?: string;
+ region?: string;
+ country?: string;
+ lat?: number;
+ lng?: number;
+ geohash?: string;
+}
+
+export interface RadrootsListingImageSize {
+ w: number;
+ h: number;
+}
+
+export interface RadrootsListingImage {
+ url: string;
+ size?: RadrootsListingImageSize;
+}
+
+export interface RadrootsListing {
+ d_tag: string;
+ product: RadrootsListingProduct;
+ quantities: RadrootsListingQuantity[];
+ prices: RadrootsListingPrice[];
+ discounts?: RadrootsListingDiscount[];
+ location?: RadrootsListingLocation;
+ images?: RadrootsListingImage[];
+}
+
+export interface RadrootsListingEventMetadata {
+ id: string;
+ author: string;
+ published_at: number;
+ listing: RadrootsListing;
+}
+
+export interface RadrootsListingEventIndex {
+ event: RadrootsNostrEvent;
+ metadata: RadrootsListingEventMetadata;
+}
+
+export interface RadrootsProfile {
+ name: string;
+ display_name?: string;
+ nip05?: string;
+ about?: string;
+ website?: string;
+ picture?: string;
+ banner?: string;
+ lud06?: string;
+ lud16?: string;
+ bot?: string;
+}
+
+export interface RadrootsProfileEventMetadata {
+ id: string;
+ author: string;
+ published_at: number;
+ profile: RadrootsProfile;
+}
+
+export interface RadrootsProfileEventIndex {
+ event: RadrootsNostrEvent;
+ metadata: RadrootsProfileEventMetadata;
+}
+
+export interface RadrootsReaction {
+ root: RadrootsNostrEventRef;
+ content: string;
+}
+
+export interface RadrootsReactionEventMetadata {
+ id: string;
+ author: string;
+ published_at: number;
+ reaction: RadrootsReaction;
+}
+
+export interface RadrootsReactionEventIndex {
+ event: RadrootsNostrEvent;
+ metadata: RadrootsReactionEventMetadata;
+}
+
+export const KIND_APPLICATION_HANDLER: number = 31990;
+export const KIND_JOB_REQUEST_MIN: number = 5000;
+export const KIND_JOB_REQUEST_MAX: number = 5999;
+export const KIND_JOB_RESULT_MIN: number = 6000;
+export const KIND_JOB_RESULT_MAX: number = 6999;
+export const KIND_JOB_FEEDBACK: number = 7000;
diff --git a/bindings/ts/tsconfig.cjs.json b/packages/bindings/events/tsconfig.cjs.json
diff --git a/bindings/ts/tsconfig.esm.json b/packages/bindings/events/tsconfig.esm.json
diff --git a/bindings/ts/tsconfig.json b/packages/bindings/events/tsconfig.json
diff --git a/packages/bindings/trade/package.json b/packages/bindings/trade/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@radroots/trade-bindings",
+ "version": "1.0.0",
+ "private": true,
+ "license": "AGPLv3",
+ "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"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "sideEffects": false,
+ "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"
+ },
+ "devDependencies": {
+ "@radroots/dev": "*",
+ "@radroots/tsconfig": "*",
+ "rimraf": "^6.0.1",
+ "ts-to-zod": "^3.15.0"
+ },
+ "dependencies": {
+ "@radroots/core-bindings": "*",
+ "@radroots/events-bindings": "*",
+ "zod": "^4.0.5"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
+\ No newline at end of file
diff --git a/packages/bindings/trade/src/index.ts b/packages/bindings/trade/src/index.ts
@@ -0,0 +1,2 @@
+export * from "./lib.js"
+export * from "./types.js"
diff --git a/packages/bindings/trade/src/lib.ts b/packages/bindings/trade/src/lib.ts
@@ -0,0 +1,7 @@
+export const MARKER_LISTING = "listing";
+export const MARKER_PAYLOAD = "payload";
+export const MARKER_PREVIOUS = "previous";
+export const MARKER_ACCEPT_RESULT = "accept_result";
+export const MARKER_INVOICE_RESULT = "invoice_result";
+export const MARKER_FULFILLMENT_RESULT = "fulfillment_result";
+export const MARKER_PROOF = "proof";
+\ No newline at end of file
diff --git a/packages/bindings/trade/src/types.ts b/packages/bindings/trade/src/types.ts
@@ -0,0 +1,171 @@
+import { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantityPrice, RadrootsCoreUnit } from "@radroots/core-bindings";
+import { RadrootsListingDiscount, RadrootsListingQuantity, RadrootsNostrEventPtr } from "@radroots/events-bindings";
+
+/*
+ Generated by typeshare 1.13.3
+*/
+
+export interface RadrootsTradeListingSubtotal {
+ price_amount: RadrootsCoreMoney;
+ price_currency: RadrootsCoreCurrency;
+ quantity_amount: RadrootsCoreDecimal;
+ quantity_unit: RadrootsCoreUnit;
+}
+
+export interface RadrootsTradeListingTotal {
+ price_amount: RadrootsCoreMoney;
+ price_currency: RadrootsCoreCurrency;
+ quantity_amount: RadrootsCoreDecimal;
+ quantity_unit: RadrootsCoreUnit;
+}
+
+export interface TradeListingAcceptRequest {
+ order_result_event_id: string;
+ listing_event_id: string;
+}
+
+export interface TradeListingAcceptResult {
+ listing_event_id: string;
+ order_result_event_id: string;
+ accepted_by: string;
+}
+
+export type TradeListingConveyanceMethod =
+ | { kind: "seller_delivery", amount: {
+ window?: string;
+ notes?: string;
+}}
+ | { kind: "buyer_pickup", amount: {
+ location_hint?: string;
+ by_when?: string;
+}}
+ | { kind: "third_party", amount: {
+ provider: string;
+ ref_id?: string;
+ notes?: string;
+}};
+
+export interface TradeListingConveyanceRequest {
+ accept_result_event_id: string;
+ method: TradeListingConveyanceMethod;
+}
+
+export interface TradeListingConveyanceResult {
+ verified: boolean;
+ method: TradeListingConveyanceMethod;
+ message?: string;
+}
+
+export interface TradeListingFulfillmentRequest {
+ payment_result_event_id: string;
+}
+
+export enum TradeListingFulfillmentState {
+ Preparing = "Preparing",
+ Shipped = "Shipped",
+ ReadyForPickup = "ReadyForPickup",
+ Delivered = "Delivered",
+ Canceled = "Canceled",
+}
+
+export interface TradeListingFulfillmentResult {
+ state: TradeListingFulfillmentState;
+ tracking?: string;
+ eta?: string;
+ notes?: string;
+}
+
+export interface TradeListingInvoiceRequest {
+ accept_result_event_id: string;
+}
+
+export interface TradeListingInvoiceResult {
+ total_sat: number;
+ bolt11?: string;
+ note?: string;
+ expires_at?: number;
+}
+
+export interface TradeListingOrderRequestPayload {
+ price: RadrootsCoreQuantityPrice;
+ quantity: RadrootsListingQuantity;
+}
+
+export interface TradeListingOrderRequest {
+ event: RadrootsNostrEventPtr;
+ payload: TradeListingOrderRequestPayload;
+}
+
+export interface TradeListingOrderResult {
+ quantity: RadrootsListingQuantity;
+ price: RadrootsCoreQuantityPrice;
+ discounts: RadrootsListingDiscount[];
+ subtotal: RadrootsTradeListingSubtotal;
+ total: RadrootsTradeListingTotal;
+}
+
+export type TradeListingPaymentProof =
+ | { kind: "zap_event", amount: {
+ id: string;
+}}
+ | { kind: "preimage", amount: {
+ hex: string;
+}}
+ | { kind: "txid", amount: {
+ id: string;
+}}
+ | { kind: "external_ref", amount: {
+ provider: string;
+ ref_id: string;
+}};
+
+export interface TradeListingPaymentProofRequest {
+ invoice_result_event_id: string;
+ proof: TradeListingPaymentProof;
+}
+
+export interface TradeListingPaymentResult {
+ verified: boolean;
+ message?: string;
+}
+
+export interface TradeListingReceiptRequest {
+ fulfillment_result_event_id: string;
+ note?: string;
+}
+
+export interface TradeListingReceiptResult {
+ acknowledged: boolean;
+ at: number;
+}
+
+export enum TradeListingStage {
+ Order = "Order",
+ Accept = "Accept",
+ Conveyance = "Conveyance",
+ Invoice = "Invoice",
+ Payment = "Payment",
+ Fulfillment = "Fulfillment",
+ Receipt = "Receipt",
+ Cancel = "Cancel",
+ Refund = "Refund",
+}
+
+export const KIND_TRADE_LISTING_ORDER_REQ: number = 5301;
+export const KIND_TRADE_LISTING_ORDER_RES: number = 6301;
+export const KIND_TRADE_LISTING_ACCEPT_REQ: number = 5302;
+export const KIND_TRADE_LISTING_ACCEPT_RES: number = 6302;
+export const KIND_TRADE_LISTING_CONVEYANCE_REQ: number = 5303;
+export const KIND_TRADE_LISTING_CONVEYANCE_RES: number = 6303;
+export const KIND_TRADE_LISTING_INVOICE_REQ: number = 5304;
+export const KIND_TRADE_LISTING_INVOICE_RES: number = 6304;
+export const KIND_TRADE_LISTING_PAYMENT_REQ: number = 5305;
+export const KIND_TRADE_LISTING_PAYMENT_RES: number = 6305;
+export const KIND_TRADE_LISTING_FULFILL_REQ: number = 5306;
+export const KIND_TRADE_LISTING_FULFILL_RES: number = 6306;
+export const KIND_TRADE_LISTING_RECEIPT_REQ: number = 5307;
+export const KIND_TRADE_LISTING_RECEIPT_RES: number = 6307;
+export const KIND_TRADE_LISTING_CANCEL_REQ: number = 5309;
+export const KIND_TRADE_LISTING_CANCEL_RES: number = 6309;
+export const KIND_TRADE_LISTING_REFUND_REQ: number = 5310;
+export const KIND_TRADE_LISTING_REFUND_RES: number = 6310;
diff --git a/bindings/ts/tsconfig.cjs.json b/packages/bindings/trade/tsconfig.cjs.json
diff --git a/bindings/ts/tsconfig.esm.json b/packages/bindings/trade/tsconfig.esm.json
diff --git a/bindings/ts/tsconfig.json b/packages/bindings/trade/tsconfig.json
diff --git a/prompt.txt b/prompt.txt
@@ -1,115 +0,0 @@
-
-########## file ##########
-src/events/listing/models.rs
-########## code ##########
-use serde::{Deserialize, Serialize};
-use typeshare::typeshare;
-use crate::events::RadrootsNostrEvent;
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingEventIndex {
- pub event: RadrootsNostrEvent,
- pub metadata: RadrootsListingEventMetadata,
-}
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingEventMetadata {
- pub id: String,
- pub author: String,
- pub published_at: u32,
- pub listing: RadrootsListing,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListing {
- pub d_tag: String,
- pub product: RadrootsListingProduct,
- pub quantities: Vec<RadrootsListingQuantity>,
- pub prices: Vec<RadrootsListingPrice>,
- pub discounts: Option<Vec<RadrootsListingDiscount>>,
- pub location: Option<RadrootsListingLocation>,
- pub images: Option<Vec<RadrootsListingImage>>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingProduct {
- pub key: String,
- pub title: String,
- pub category: String,
- pub summary: Option<String>,
- pub process: Option<String>,
- pub lot: Option<String>,
- pub location: Option<String>,
- pub profile: Option<String>,
- pub year: Option<String>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingQuantity {
- pub amt: String,
- pub unit: String,
- pub label: Option<String>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingPrice {
- pub amt: String,
- pub currency: String,
- pub qty_amt: String,
- pub qty_unit: String,
- pub qty_key: String,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub enum RadrootsListingDiscount {
- 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,
- },
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingLocation {
- pub primary: String,
- pub city: Option<String>,
- pub region: Option<String>,
- pub country: Option<String>,
- pub lat: Option<f64>,
- pub lng: Option<f64>,
- pub geohash: Option<String>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingImage {
- pub url: String,
- pub size: Option<RadrootsListingImageSize>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingImageSize {
- pub w: u32,
- pub h: u32,
-}
-
diff --git a/src/events/comment/models.rs b/src/events/comment/models.rs
@@ -1,26 +0,0 @@
-use serde::{Deserialize, Serialize};
-use typeshare::typeshare;
-use crate::events::{lib::RadrootsNostrEventRef, RadrootsNostrEvent};
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsCommentEventIndex {
- pub event: RadrootsNostrEvent,
- pub metadata: RadrootsCommentEventMetadata,
-}
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsCommentEventMetadata {
- pub id: String,
- pub author: String,
- pub published_at: u32,
- pub comment: RadrootsComment,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsComment {
- pub root: RadrootsNostrEventRef,
- pub parent: RadrootsNostrEventRef,
- pub content: String,
-}
diff --git a/src/events/follow/models.rs b/src/events/follow/models.rs
@@ -1,32 +0,0 @@
-use serde::{Deserialize, Serialize};
-use typeshare::typeshare;
-use crate::events::RadrootsNostrEvent;
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsFollowEventIndex {
- pub event: RadrootsNostrEvent,
- pub metadata: RadrootsFollowEventMetadata,
-}
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsFollowEventMetadata {
- pub id: String,
- pub author: String,
- pub published_at: u32,
- pub follow: RadrootsFollow,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsFollow {
- pub list: Vec<RadrootsFollowProfile>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsFollowProfile {
- pub published_at: u32,
- pub public_key: String,
- pub relay_url: Option<String>,
- pub contact_name: Option<String>,
-}
diff --git a/src/events/lib.rs b/src/events/lib.rs
@@ -1,10 +0,0 @@
-use serde::{Deserialize, Serialize};
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsNostrEventRef {
- pub id: String,
- pub author: String,
- pub kind: u32,
- pub d_tag: Option<String>,
- pub relays: Option<Vec<String>>,
-}
diff --git a/src/events/listing/models.rs b/src/events/listing/models.rs
@@ -1,110 +0,0 @@
-use serde::{Deserialize, Serialize};
-use typeshare::typeshare;
-use crate::events::RadrootsNostrEvent;
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingEventIndex {
- pub event: RadrootsNostrEvent,
- pub metadata: RadrootsListingEventMetadata,
-}
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingEventMetadata {
- pub id: String,
- pub author: String,
- pub published_at: u32,
- pub listing: RadrootsListing,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListing {
- pub d_tag: String,
- pub product: RadrootsListingProduct,
- pub quantities: Vec<RadrootsListingQuantity>,
- pub prices: Vec<RadrootsListingPrice>,
- pub discounts: Option<Vec<RadrootsListingDiscount>>,
- pub location: Option<RadrootsListingLocation>,
- pub images: Option<Vec<RadrootsListingImage>>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingProduct {
- pub key: String,
- pub title: String,
- pub category: String,
- pub summary: Option<String>,
- pub process: Option<String>,
- pub lot: Option<String>,
- pub location: Option<String>,
- pub profile: Option<String>,
- pub year: Option<String>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingQuantity {
- pub amt: String,
- pub unit: String,
- pub label: Option<String>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingPrice {
- pub amt: String,
- pub currency: String,
- pub qty_amt: String,
- pub qty_unit: String,
- pub qty_key: String,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub enum RadrootsListingDiscount {
- 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,
- },
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingLocation {
- pub primary: String,
- pub city: Option<String>,
- pub region: Option<String>,
- pub country: Option<String>,
- pub lat: Option<f64>,
- pub lng: Option<f64>,
- pub geohash: Option<String>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingImage {
- pub url: String,
- pub size: Option<RadrootsListingImageSize>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsListingImageSize {
- pub w: u32,
- pub h: u32,
-}
diff --git a/src/events/mod.rs b/src/events/mod.rs
@@ -1,32 +0,0 @@
-pub mod lib;
-
-pub mod comment {
- pub mod models;
-}
-
-pub mod listing {
- pub mod models;
-}
-
-pub mod profile {
- pub mod models;
-}
-
-pub mod reaction {
- pub mod models;
-}
-
-use serde::{Deserialize, Serialize};
-use typeshare::typeshare;
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsNostrEvent {
- pub id: String,
- pub author: String,
- pub created_at: u32,
- pub kind: u32,
- pub tags: Vec<Vec<String>>,
- pub content: String,
- pub sig: String,
-}
diff --git a/src/events/profile/models.rs b/src/events/profile/models.rs
@@ -1,33 +0,0 @@
-use serde::{Deserialize, Serialize};
-use typeshare::typeshare;
-use crate::events::RadrootsNostrEvent;
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsProfileEventIndex {
- pub event: RadrootsNostrEvent,
- pub metadata: RadrootsProfileEventMetadata,
-}
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsProfileEventMetadata {
- pub id: String,
- pub author: String,
- pub published_at: u32,
- pub profile: RadrootsProfile,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsProfile {
- pub name: String,
- pub display_name: Option<String>,
- pub nip05: Option<String>,
- pub about: Option<String>,
- pub website: Option<String>,
- pub picture: Option<String>,
- pub banner: Option<String>,
- pub lud06: Option<String>,
- pub lud16: Option<String>,
- pub bot: Option<String>,
-}
diff --git a/src/events/reaction/models.rs b/src/events/reaction/models.rs
@@ -1,25 +0,0 @@
-use serde::{Deserialize, Serialize};
-use typeshare::typeshare;
-use crate::events::{lib::RadrootsNostrEventRef, RadrootsNostrEvent};
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsReactionEventIndex {
- pub event: RadrootsNostrEvent,
- pub metadata: RadrootsReactionEventMetadata,
-}
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsReactionEventMetadata {
- pub id: String,
- pub author: String,
- pub published_at: u32,
- pub reaction: RadrootsReaction,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsReaction {
- pub root: RadrootsNostrEventRef,
- pub content: String,
-}
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,6 +0,0 @@
-pub const KIND_JOB_REQUEST: u16 = 5300;
-pub const KIND_JOB_RESPONSE: u16 = 6300;
-pub const KIND_APPLICATION_HANDLER: u16 = 31990;
-
-pub mod models;
-pub mod events;
-\ No newline at end of file
diff --git a/src/models/indexer.rs b/src/models/indexer.rs
@@ -1,25 +0,0 @@
-use serde::{Deserialize, Serialize};
-use typeshare::typeshare;
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsIndexShardMetadata {
- pub file: String,
- pub count: u32,
- pub first_id: String,
- pub last_id: String,
- pub first_published_at: u32,
- pub last_published_at: u32,
- pub sha256: String,
-}
-
-#[typeshare]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct RadrootsIndexManifest {
- pub country: String,
- pub total: u32,
- pub shard_size: u32,
- pub first_published_at: u32,
- pub last_published_at: u32,
- pub shards: Vec<RadrootsIndexShardMetadata>,
-}
-\ No newline at end of file
diff --git a/src/models/mod.rs b/src/models/mod.rs
@@ -1 +0,0 @@
-pub mod indexer;
diff --git a/turbo.json b/turbo.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "https://turborepo.com/schema.json",
+ "ui": "tui",
+ "tasks": {
+ "build": {
+ "dependsOn": [
+ "^build"
+ ],
+ "inputs": [
+ "$TURBO_DEFAULT$",
+ ".env*"
+ ],
+ "outputs": [
+ "dist/**"
+ ]
+ },
+ "lint": {
+ "dependsOn": [
+ "^lint"
+ ]
+ },
+ "check-types": {
+ "dependsOn": [
+ "^check-types"
+ ]
+ },
+ "dev": {
+ "cache": false,
+ "persistent": true
+ },
+ "@radroots/core-bindings#build": {
+ "dependsOn": [
+ "@radroots/dev#build",
+ "@radroots/tsconfig#build"
+ ]
+ },
+ "@radroots/events-bindings#build": {
+ "dependsOn": [
+ "@radroots/dev#build",
+ "@radroots/tsconfig#build",
+ "@radroots/core-bindings#build"
+ ]
+ },
+ "@radroots/events-indexed-bindings#build": {
+ "dependsOn": [
+ "@radroots/dev#build",
+ "@radroots/tsconfig#build"
+ ]
+ },
+ "@radroots/trade-bindings#build": {
+ "dependsOn": [
+ "@radroots/dev#build",
+ "@radroots/tsconfig#build",
+ "@radroots/core-bindings#build",
+ "@radroots/events-bindings#build"
+ ]
+ }
+ }
+}
+\ No newline at end of file