lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 32cc941b99c79002f34e28b8fcc40b8d7cc11f96
parent 3e65b0b4a4818685e3f6a879366f8f5a183929cc
Author: triesap <tyson@radroots.org>
Date:   Sat, 13 Jun 2026 15:09:03 -0700

sdk: remove SDK-owned crates from core workspace

- remove the Rust SDK and wasm binding crates after moving ownership to the SDK repo
- delete the old wasm Makefile surface and generated package manifests
- update rr-rs contracts, docs, Nix apps, and coverage policy for core-only ownership
- keep wasm binding references as SDK-owned consumer surfaces in specs

Diffstat:
MAGENT_INSTRUCTIONS.md | 2+-
MCargo.lock | 161-------------------------------------------------------------------------------
MCargo.toml | 7-------
DMakefile | 58----------------------------------------------------------
Mcrates/events_codec/README | 4++--
Dcrates/events_codec_wasm/Cargo.toml | 34----------------------------------
Dcrates/events_codec_wasm/README | 24------------------------
Dcrates/events_codec_wasm/pkg/package.json | 20--------------------
Dcrates/events_codec_wasm/src/lib.rs | 1149-------------------------------------------------------------------------------
Dcrates/replica_db_wasm/Cargo.toml | 36------------------------------------
Dcrates/replica_db_wasm/README | 25-------------------------
Dcrates/replica_db_wasm/pkg/package.json | 20--------------------
Dcrates/replica_db_wasm/src/lib.rs | 29-----------------------------
Dcrates/replica_db_wasm/src/utils.rs | 13-------------
Dcrates/replica_db_wasm/src/wasm_impl.rs | 891-------------------------------------------------------------------------------
Dcrates/replica_sync_wasm/Cargo.toml | 35-----------------------------------
Dcrates/replica_sync_wasm/README | 25-------------------------
Dcrates/replica_sync_wasm/pkg/package.json | 20--------------------
Dcrates/replica_sync_wasm/src/lib.rs | 124-------------------------------------------------------------------------------
Dcrates/sdk/Cargo.toml | 78------------------------------------------------------------------------------
Dcrates/sdk/README | 23-----------------------
Dcrates/sdk/src/adapters/mod.rs | 8--------
Dcrates/sdk/src/adapters/radrootsd.rs | 835-------------------------------------------------------------------------------
Dcrates/sdk/src/adapters/relay.rs | 96-------------------------------------------------------------------------------
Dcrates/sdk/src/adapters/signer.rs | 24------------------------
Dcrates/sdk/src/adapters/signing.rs | 63---------------------------------------------------------------
Dcrates/sdk/src/client.rs | 2711-------------------------------------------------------------------------------
Dcrates/sdk/src/config.rs | 388-------------------------------------------------------------------------------
Dcrates/sdk/src/farm.rs | 9---------
Dcrates/sdk/src/identity.rs | 33---------------------------------
Dcrates/sdk/src/lib.rs | 72------------------------------------------------------------------------
Dcrates/sdk/src/listing.rs | 40----------------------------------------
Dcrates/sdk/src/order.rs | 281-------------------------------------------------------------------------------
Dcrates/sdk/src/profile.rs | 12------------
Dcrates/sdk/tests/client.rs | 906-------------------------------------------------------------------------------
Dcrates/sdk/tests/config.rs | 562-------------------------------------------------------------------------------
Dcrates/sdk/tests/facade.rs | 269-------------------------------------------------------------------------------
Dcrates/sdk/tests/radrootsd.rs | 1952-------------------------------------------------------------------------------
Dcrates/sdk/tests/relay_direct.rs | 1406-------------------------------------------------------------------------------
Dcrates/sdk/tests/replica_ingest.rs | 85-------------------------------------------------------------------------------
Mcrates/xtask/src/contract.rs | 9---------
Mcrates/xtask/src/phase1_1.rs | 2--
Mdocs/nix.md | 3---
Mnix/apps.nix | 6------
Mnix/common.nix | 14+-------------
Mpolicy/coverage/policy.toml | 1-
Mspec/RCLD.md | 4++--
Mspec/README.md | 45+++++++++++++++++++--------------------------
Mspec/exports/ts.toml | 1-
Mspec/manifest.toml | 15++++++---------
Mspec/operations.toml | 6+++---
Mspec/replica.toml | 6++++--
Mspec/social-events.md | 8++++----
53 files changed, 42 insertions(+), 12608 deletions(-)

diff --git a/AGENT_INSTRUCTIONS.md b/AGENT_INSTRUCTIONS.md @@ -208,6 +208,6 @@ If Agent Mail is active for the task: - use `.beads/PRIME.md` for the repository coordination conventions - use the active Beads issue id as the Agent Mail thread id and reservation reason when both tools are active - reserve files before the first write for coordinated multi-agent work -- use shared build slots for long-running singleton lanes such as contract, release-preflight, or wasm-build runs +- use shared build slots for long-running singleton lanes such as contract or release-preflight runs If Beads or Agent Mail is not active, the repo still follows the same coding and validation standards; only the task-state and coordination backend changes. diff --git a/Cargo.lock b/Cargo.lock @@ -619,12 +619,6 @@ dependencies = [ ] [[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2785,16 +2779,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "minicov" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" -dependencies = [ - "cc", - "walkdir", -] - -[[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3075,7 +3059,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -3151,12 +3134,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] name = "opaque-debug" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4030,18 +4007,6 @@ dependencies = [ ] [[package]] -name = "radroots_events_codec_wasm" -version = "0.1.0-alpha.2" -dependencies = [ - "radroots_core", - "radroots_events", - "radroots_events_codec", - "serde", - "serde_json", - "wasm-bindgen", -] - -[[package]] name = "radroots_events_indexed" version = "0.1.0-alpha.2" dependencies = [ @@ -4281,23 +4246,6 @@ dependencies = [ ] [[package]] -name = "radroots_replica_db_wasm" -version = "0.1.0-alpha.2" -dependencies = [ - "js-sys", - "radroots_replica_db", - "radroots_replica_db_schema", - "radroots_replica_sync", - "radroots_sql_core", - "radroots_sql_wasm_core", - "serde", - "serde-wasm-bindgen", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-test", -] - -[[package]] name = "radroots_replica_sync" version = "0.1.0-alpha.2" dependencies = [ @@ -4317,22 +4265,6 @@ dependencies = [ ] [[package]] -name = "radroots_replica_sync_wasm" -version = "0.1.0-alpha.2" -dependencies = [ - "base64 0.22.1", - "radroots_events", - "radroots_replica_sync", - "radroots_sql_core", - "radroots_sql_wasm_core", - "serde", - "serde-wasm-bindgen", - "serde_json", - "uuid", - "wasm-bindgen", -] - -[[package]] name = "radroots_runtime" version = "0.1.0-alpha.2" dependencies = [ @@ -4386,32 +4318,6 @@ dependencies = [ ] [[package]] -name = "radroots_sdk" -version = "0.1.0-alpha.2" -dependencies = [ - "futures", - "nostr", - "radroots_core", - "radroots_events", - "radroots_events_codec", - "radroots_identity", - "radroots_nostr", - "radroots_nostr_connect", - "radroots_nostr_signer", - "radroots_replica_db", - "radroots_replica_db_schema", - "radroots_replica_sync", - "radroots_sql_core", - "radroots_trade", - "reqwest", - "serde", - "serde_json", - "tempfile", - "tokio", - "tokio-tungstenite", -] - -[[package]] name = "radroots_secret_vault" version = "0.1.0-alpha.2" dependencies = [ @@ -5078,15 +4984,6 @@ dependencies = [ ] [[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] name = "scale-info" version = "2.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7538,16 +7435,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7640,45 +7527,6 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen-test" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" -dependencies = [ - "async-trait", - "cast", - "js-sys", - "libm", - "minicov", - "nu-ansi-term", - "num-traits", - "oorandom", - "serde", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", - "wasm-bindgen-test-shared", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "wasm-bindgen-test-shared" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" - -[[package]] name = "wasm-encoder" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7792,15 +7640,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "crates/events", "crates/event_store", "crates/events_codec", - "crates/events_codec_wasm", "crates/events_indexed", "crates/geocoder", "crates/identity", @@ -35,13 +34,10 @@ members = [ "crates/test_fixtures", "crates/replica_db_schema", "crates/replica_sync", - "crates/replica_sync_wasm", "crates/replica_db", - "crates/replica_db_wasm", "crates/runtime_paths", "crates/runtime_distribution", "crates/runtime_manager", - "crates/sdk", "crates/sp1_guest_trade", "crates/sp1_host_trade", "crates/trade", @@ -78,7 +74,6 @@ radroots_runtime = { path = "crates/runtime", version = "0.1.0-alpha.2", default radroots_runtime_paths = { path = "crates/runtime_paths", version = "0.1.0-alpha.2", default-features = false } radroots_runtime_distribution = { path = "crates/runtime_distribution", version = "0.1.0-alpha.2", default-features = false } radroots_runtime_manager = { path = "crates/runtime_manager", version = "0.1.0-alpha.2", default-features = false } -radroots_sdk = { path = "crates/sdk", version = "0.1.0-alpha.2", default-features = false } radroots_log = { path = "crates/log", version = "0.1.0-alpha.2", default-features = false } radroots_net = { path = "crates/net", version = "0.1.0-alpha.2", default-features = false } radroots_nostr_runtime = { path = "crates/nostr_runtime", version = "0.1.0-alpha.2", default-features = false } @@ -99,8 +94,6 @@ radroots_test_fixtures = { path = "crates/test_fixtures", version = "0.1.0-alpha radroots_replica_db_schema = { path = "crates/replica_db_schema", version = "0.1.0-alpha.2", default-features = false } radroots_replica_sync = { path = "crates/replica_sync", version = "0.1.0-alpha.2", default-features = false } radroots_replica_db = { path = "crates/replica_db", version = "0.1.0-alpha.2", default-features = false } -radroots_replica_db_wasm = { path = "crates/replica_db_wasm", version = "0.1.0-alpha.2" } -radroots_replica_sync_wasm = { path = "crates/replica_sync_wasm", version = "0.1.0-alpha.2" } radroots_trade = { path = "crates/trade", version = "0.1.0-alpha.2", default-features = false } radroots_types = { path = "crates/types", version = "0.1.0-alpha.2", default-features = false } radroots_protected_store = { path = "crates/protected_store", version = "0.1.0-alpha.2", default-features = false } diff --git a/Makefile b/Makefile @@ -1,58 +0,0 @@ -.PHONY: all build clean help \ - clean-hyphenated-wasm-output \ - build-events-codec-wasm build-replica-db-wasm build-replica-sync-wasm - -SHELL := /bin/bash -.SHELLFLAGS := -e -o pipefail -c - -EVENTS_CODEC_WASM_PACKAGE := @radroots/radroots_events-codec-wasm -EVENTS_CODEC_WASM_DESCRIPTION := WebAssembly bindings for radroots_events_codec -REPLICA_DB_WASM_PACKAGE := @radroots/radroots_replica-db-wasm -REPLICA_DB_WASM_DESCRIPTION := WebAssembly bindings for radroots_replica_db -REPLICA_SYNC_WASM_PACKAGE := @radroots/radroots_replica-sync-wasm -REPLICA_SYNC_WASM_DESCRIPTION := WebAssembly bindings for radroots_replica_sync - -HYPHENATED_WASM_OUTPUT_DIRS := \ - crates/events-codec-wasm \ - crates/replica-db-wasm \ - crates/replica-sync-wasm - -BUILD_TARGETS := \ - build-events-codec-wasm \ - build-replica-db-wasm \ - build-replica-sync-wasm - -all: build - -build: clean-hyphenated-wasm-output $(BUILD_TARGETS) - -clean: - cargo clean - -help: - @echo "Commands:" - @echo " make all" - @echo " make build" - @echo " make clean" - @echo " make help" - @printf "%s\n" $(BUILD_TARGETS) - -clean-hyphenated-wasm-output: - rm -rf $(HYPHENATED_WASM_OUTPUT_DIRS) - -normalize_wasm_package_json = python3 -c 'import json, pathlib, sys; path = pathlib.Path(sys.argv[1]); data = json.loads(path.read_text()); data["name"] = sys.argv[2]; data["description"] = sys.argv[3]; path.write_text(json.dumps(data, indent=2) + "\n")' "$(1)" "$(2)" "$(3)" - -build-replica-db-wasm: - wasm-pack build crates/replica_db_wasm --release --target web \ - --out-dir pkg/dist --scope radroots - $(call normalize_wasm_package_json,crates/replica_db_wasm/pkg/dist/package.json,$(REPLICA_DB_WASM_PACKAGE),$(REPLICA_DB_WASM_DESCRIPTION)) - -build-events-codec-wasm: - wasm-pack build crates/events_codec_wasm --release --target web \ - --out-dir pkg/dist --scope radroots - $(call normalize_wasm_package_json,crates/events_codec_wasm/pkg/dist/package.json,$(EVENTS_CODEC_WASM_PACKAGE),$(EVENTS_CODEC_WASM_DESCRIPTION)) - -build-replica-sync-wasm: - wasm-pack build crates/replica_sync_wasm --release --target web \ - --out-dir pkg/dist --scope radroots - $(call normalize_wasm_package_json,crates/replica_sync_wasm/pkg/dist/package.json,$(REPLICA_SYNC_WASM_PACKAGE),$(REPLICA_SYNC_WASM_DESCRIPTION)) diff --git a/crates/events_codec/README b/crates/events_codec/README @@ -40,8 +40,8 @@ The group codecs use bare metadata marker tags such as `private`, `restricted`, `hidden`, and `closed`, `supported_kinds` declarations, and `code` tags for invite and join flows. They preserve optional reason content on user management and moderation events. LiveKit room metadata and live participant state are -deferred. The companion `radroots_events_codec_wasm` crate exposes -deterministic JSON tag builders for the same Field and NIP-29 families. +deferred. SDK-owned wasm bindings expose deterministic JSON tag builders for the +same Field and NIP-29 families. ## Copyright diff --git a/crates/events_codec_wasm/Cargo.toml b/crates/events_codec_wasm/Cargo.toml @@ -1,34 +0,0 @@ -[package] -name = "radroots_events_codec_wasm" -publish = false -version = "0.1.0-alpha.2" -edition.workspace = true -authors = ["Tyson Lupul <tyson@radroots.org>"] -rust-version.workspace = true -license.workspace = true -description = "WebAssembly bindings for radroots_events_codec" -repository.workspace = true -homepage.workspace = true -documentation = "https://docs.rs/radroots_events_codec_wasm" -readme = "README" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -radroots_events = { workspace = true, default-features = false, features = [ - "std", - "serde", -] } -radroots_events_codec = { workspace = true, default-features = false, features = [ - "std", - "serde_json", -] } -serde = { workspace = true } -serde_json = { workspace = true } -wasm-bindgen = { workspace = true } - -[dev-dependencies] -radroots_core = { workspace = true, default-features = false, features = [ - "std", -] } diff --git a/crates/events_codec_wasm/README b/crates/events_codec_wasm/README @@ -1,24 +0,0 @@ -# radroots_events_codec_wasm - -This is the README for `radroots_events_codec_wasm`, which provides WebAssembly -bindings for `radroots_events_codec` in the `radroots` core libraries. - -## Overview - - * wasm-bindgen entry points for event content and tag encoding and decoding; - * specialized tag helpers for farm, list, job, message, plot, and reaction - event families; - * JSON and `JsValue` boundaries built around `serde-wasm-bindgen` and base64 - helpers; - * built as `cdylib` and `rlib` artifacts for wasm consumers. - -## Copyright - -Except as otherwise noted, all files in the `radroots_events_codec_wasm` -distribution are - - Copyright (c) 2020-2026 Tyson Lupul - -For information on usage and redistribution, and for a DISCLAIMER OF ALL -WARRANTIES, see LICENSE included in the `radroots_events_codec_wasm` -distribution. diff --git a/crates/events_codec_wasm/pkg/package.json b/crates/events_codec_wasm/pkg/package.json @@ -1,20 +0,0 @@ -{ - "name": "@radroots/radroots_events-codec-wasm", - "description": "WebAssembly bindings for radroots_events_codec", - "version": "0.1.0", - "private": true, - "type": "module", - "files": [ - "dist" - ], - "main": "./dist/radroots_events_codec_wasm.js", - "types": "./dist/radroots_events_codec_wasm.d.ts", - "exports": { - ".": { - "types": "./dist/radroots_events_codec_wasm.d.ts", - "import": "./dist/radroots_events_codec_wasm.js", - "default": "./dist/radroots_events_codec_wasm.js" - } - }, - "sideEffects": false -} diff --git a/crates/events_codec_wasm/src/lib.rs b/crates/events_codec_wasm/src/lib.rs @@ -1,1149 +0,0 @@ -#![forbid(unsafe_code)] - -use radroots_events::article::RadrootsArticle; -use radroots_events::calendar::{ - RadrootsCalendar, RadrootsCalendarDateEvent, RadrootsCalendarEventRsvp, - RadrootsCalendarTimeEvent, -}; -use radroots_events::comment::RadrootsComment; -use radroots_events::coop::RadrootsCoop; -use radroots_events::document::RadrootsDocument; -use radroots_events::farm::RadrootsFarm; -use radroots_events::farm_crdt::RadrootsFarmCrdtChange; -use radroots_events::farm_file::RadrootsFarmFileMetadata; -use radroots_events::farm_workspace::RadrootsFarmWorkspaceManifest; -use radroots_events::file_metadata::RadrootsFileMetadata; -use radroots_events::follow::RadrootsFollow; -use radroots_events::gift_wrap::RadrootsGiftWrap; -use radroots_events::group::{ - RadrootsGroupAdmins, RadrootsGroupCreateGroup, RadrootsGroupCreateInvite, - RadrootsGroupDeleteEvent, RadrootsGroupDeleteGroup, RadrootsGroupEditMetadata, - RadrootsGroupJoinRequest, RadrootsGroupLeaveRequest, RadrootsGroupMembers, - RadrootsGroupMetadata, RadrootsGroupPutUser, RadrootsGroupRemoveUser, RadrootsGroupRoles, -}; -use radroots_events::http_auth::RadrootsHttpAuth; -use radroots_events::job_feedback::RadrootsJobFeedback; -use radroots_events::job_request::RadrootsJobRequest; -use radroots_events::job_result::RadrootsJobResult; -use radroots_events::list::RadrootsList; -use radroots_events::list_set::RadrootsListSet; -use radroots_events::listing::RadrootsListing; -use radroots_events::message::RadrootsMessage; -use radroots_events::message_file::RadrootsMessageFile; -use radroots_events::plot::RadrootsPlot; -use radroots_events::post::RadrootsPost; -use radroots_events::reaction::RadrootsReaction; -use radroots_events::relay_auth::RadrootsRelayAuth; -use radroots_events::report::RadrootsReport; -use radroots_events::repost::{RadrootsGenericRepost, RadrootsRepost}; -use radroots_events::seal::RadrootsSeal; -use radroots_events_codec::article::encode::article_build_tags; -use radroots_events_codec::calendar::encode::{ - calendar_collection_build_tags, calendar_date_event_build_tags, calendar_time_event_build_tags, - rsvp_build_tags, -}; -use radroots_events_codec::comment::encode::comment_build_tags; -use radroots_events_codec::coop::encode::coop_build_tags; -use radroots_events_codec::document::encode::document_build_tags; -use radroots_events_codec::farm::encode::farm_build_tags; -use radroots_events_codec::farm_crdt::encode::farm_crdt_change_build_tags_with_author; -use radroots_events_codec::farm_file::encode::farm_file_metadata_build_tags; -use radroots_events_codec::farm_workspace::encode::farm_workspace_build_tags; -use radroots_events_codec::file_metadata::encode::file_metadata_build_tags; -use radroots_events_codec::follow::encode::follow_build_tags; -use radroots_events_codec::gift_wrap::encode::gift_wrap_build_tags; -use radroots_events_codec::group::encode::{ - group_admins_build_tags, group_create_group_build_tags, group_create_invite_build_tags, - group_delete_event_build_tags, group_delete_group_build_tags, group_edit_metadata_build_tags, - group_join_request_build_tags, group_leave_request_build_tags, group_members_build_tags, - group_metadata_build_tags, group_put_user_build_tags, group_remove_user_build_tags, - group_roles_build_tags, -}; -use radroots_events_codec::http_auth::encode::http_auth_build_tags; -use radroots_events_codec::job::feedback::encode::job_feedback_build_tags; -use radroots_events_codec::job::request::encode::job_request_build_tags; -use radroots_events_codec::job::result::encode::job_result_build_tags; -use radroots_events_codec::list::encode::list_build_tags; -use radroots_events_codec::list_set::encode::list_set_build_tags; -use radroots_events_codec::listing::tags::{ - listing_tags as listing_tags_impl, listing_tags_full as listing_tags_full_impl, -}; -use radroots_events_codec::message::encode::message_build_tags; -use radroots_events_codec::message_file::encode::message_file_build_tags; -use radroots_events_codec::plot::encode::plot_build_tags; -use radroots_events_codec::post::encode::post_build_tags; -use radroots_events_codec::reaction::encode::reaction_build_tags; -use radroots_events_codec::relay_auth::encode::relay_auth_build_tags; -use radroots_events_codec::report::encode::report_build_tags; -use radroots_events_codec::repost::encode::{generic_repost_build_tags, repost_build_tags}; -use radroots_events_codec::seal::encode::seal_build_tags; -use serde::de::DeserializeOwned; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::JsValue; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; - -#[cfg(target_arch = "wasm32")] -type RadrootsJsValue = JsValue; - -#[cfg(not(target_arch = "wasm32"))] -type RadrootsJsValue = String; - -fn err_js<E: ToString>(err: E) -> RadrootsJsValue { - #[cfg(target_arch = "wasm32")] - { - JsValue::from_str(&err.to_string()) - } - #[cfg(not(target_arch = "wasm32"))] - { - err.to_string() - } -} - -fn normalized_payload(input: &str) -> &str { - if input.is_empty() { "{}" } else { input } -} - -fn parse_json<T: DeserializeOwned>(input: &str) -> Result<T, RadrootsJsValue> { - serde_json::from_str(normalized_payload(input)).map_err(err_js) -} - -fn tags_to_json(tags: Vec<Vec<String>>) -> Result<String, RadrootsJsValue> { - serde_json::to_string(&tags).map_err(err_js) -} - -fn build_tags_json<T, E, F>(input: &str, build: F) -> Result<String, RadrootsJsValue> -where - T: DeserializeOwned, - E: ToString, - F: FnOnce(&T) -> Result<Vec<Vec<String>>, E>, -{ - let value = parse_json::<T>(input)?; - let tags = build(&value).map_err(err_js)?; - tags_to_json(tags) -} - -fn build_tags_json_infallible<T, F>(input: &str, build: F) -> Result<String, RadrootsJsValue> -where - T: DeserializeOwned, - F: FnOnce(&T) -> Vec<Vec<String>>, -{ - let value = parse_json::<T>(input)?; - let tags = build(&value); - tags_to_json(tags) -} - -#[derive(serde::Deserialize)] -struct FarmCrdtTagsInput { - change: RadrootsFarmCrdtChange, - author_pubkey: String, -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = listing_tags))] -pub fn listing_tags(listing_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsListing, _, _>(listing_json, listing_tags_impl) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = listing_tags_full))] -pub fn listing_tags_full(listing_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsListing, _, _>(listing_json, listing_tags_full_impl) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = post_tags))] -pub fn post_tags(post_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsPost, _, _>(post_json, post_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = comment_tags))] -pub fn comment_tags(comment_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsComment, _, _>(comment_json, comment_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = article_tags))] -pub fn article_tags(article_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsArticle, _, _>(article_json, article_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = file_metadata_tags))] -pub fn file_metadata_tags(metadata_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsFileMetadata, _, _>(metadata_json, file_metadata_build_tags) -} - -#[cfg_attr( - target_arch = "wasm32", - wasm_bindgen(js_name = calendar_date_event_tags) -)] -pub fn calendar_date_event_tags(event_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsCalendarDateEvent, _, _>(event_json, calendar_date_event_build_tags) -} - -#[cfg_attr( - target_arch = "wasm32", - wasm_bindgen(js_name = calendar_time_event_tags) -)] -pub fn calendar_time_event_tags(event_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsCalendarTimeEvent, _, _>(event_json, calendar_time_event_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = calendar_tags))] -pub fn calendar_tags(calendar_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsCalendar, _, _>(calendar_json, calendar_collection_build_tags) -} - -#[cfg_attr( - target_arch = "wasm32", - wasm_bindgen(js_name = calendar_event_rsvp_tags) -)] -pub fn calendar_event_rsvp_tags(rsvp_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsCalendarEventRsvp, _, _>(rsvp_json, rsvp_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = repost_tags))] -pub fn repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsRepost, _, _>(repost_json, repost_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = generic_repost_tags))] -pub fn generic_repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGenericRepost, _, _>(repost_json, generic_repost_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = report_tags))] -pub fn report_tags(report_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsReport, _, _>(report_json, report_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = follow_tags))] -pub fn follow_tags(follow_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsFollow, _, _>(follow_json, follow_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = document_tags))] -pub fn document_tags(document_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsDocument, _, _>(document_json, document_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = coop_tags))] -pub fn coop_tags(coop_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsCoop, _, _>(coop_json, coop_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_tags))] -pub fn farm_tags(farm_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsFarm, _, _>(farm_json, farm_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = list_tags))] -pub fn list_tags(list_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsList, _, _>(list_json, list_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = list_set_tags))] -pub fn list_set_tags(list_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsListSet, _, _>(list_json, list_set_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = plot_tags))] -pub fn plot_tags(plot_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsPlot, _, _>(plot_json, plot_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_request_tags))] -pub fn job_request_tags(job_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json_infallible::<RadrootsJobRequest, _>(job_json, job_request_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_result_tags))] -pub fn job_result_tags(job_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json_infallible::<RadrootsJobResult, _>(job_json, job_result_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_feedback_tags))] -pub fn job_feedback_tags(job_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json_infallible::<RadrootsJobFeedback, _>(job_json, job_feedback_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = reaction_tags))] -pub fn reaction_tags(reaction_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsReaction, _, _>(reaction_json, reaction_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = message_tags))] -pub fn message_tags(message_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsMessage, _, _>(message_json, message_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = message_file_tags))] -pub fn message_file_tags(message_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsMessageFile, _, _>(message_json, message_file_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = seal_tags))] -pub fn seal_tags(seal_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsSeal, _, _>(seal_json, seal_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = gift_wrap_tags))] -pub fn gift_wrap_tags(gift_wrap_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGiftWrap, _, _>(gift_wrap_json, gift_wrap_build_tags) -} - -#[cfg_attr( - target_arch = "wasm32", - wasm_bindgen(js_name = farm_workspace_manifest_tags) -)] -pub fn farm_workspace_manifest_tags(workspace_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsFarmWorkspaceManifest, _, _>( - workspace_json, - farm_workspace_build_tags, - ) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_crdt_change_tags))] -pub fn farm_crdt_change_tags(input_json: &str) -> Result<String, RadrootsJsValue> { - let input = parse_json::<FarmCrdtTagsInput>(input_json)?; - let tags = farm_crdt_change_build_tags_with_author(&input.change, Some(&input.author_pubkey)) - .map_err(err_js)?; - tags_to_json(tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_file_metadata_tags))] -pub fn farm_file_metadata_tags(file_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsFarmFileMetadata, _, _>(file_json, farm_file_metadata_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = relay_auth_tags))] -pub fn relay_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsRelayAuth, _, _>(auth_json, relay_auth_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = http_auth_tags))] -pub fn http_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsHttpAuth, _, _>(auth_json, http_auth_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_put_user_tags))] -pub fn group_put_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupPutUser, _, _>(group_json, group_put_user_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_remove_user_tags))] -pub fn group_remove_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupRemoveUser, _, _>(group_json, group_remove_user_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_group_tags))] -pub fn group_create_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupCreateGroup, _, _>(group_json, group_create_group_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_edit_metadata_tags))] -pub fn group_edit_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupEditMetadata, _, _>(group_json, group_edit_metadata_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_group_tags))] -pub fn group_delete_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupDeleteGroup, _, _>(group_json, group_delete_group_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_event_tags))] -pub fn group_delete_event_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupDeleteEvent, _, _>(group_json, group_delete_event_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_invite_tags))] -pub fn group_create_invite_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupCreateInvite, _, _>(group_json, group_create_invite_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_join_request_tags))] -pub fn group_join_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupJoinRequest, _, _>(group_json, group_join_request_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_leave_request_tags))] -pub fn group_leave_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupLeaveRequest, _, _>(group_json, group_leave_request_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_metadata_tags))] -pub fn group_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupMetadata, _, _>(group_json, group_metadata_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_admins_tags))] -pub fn group_admins_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupAdmins, _, _>(group_json, group_admins_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_members_tags))] -pub fn group_members_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupMembers, _, _>(group_json, group_members_build_tags) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_roles_tags))] -pub fn group_roles_tags(group_json: &str) -> Result<String, RadrootsJsValue> { - build_tags_json::<RadrootsGroupRoles, _, _>(group_json, group_roles_build_tags) -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, RadrootsCoreUnit, - }; - use radroots_events::farm::RadrootsFarmRef; - use radroots_events::farm_crdt::{ - RADROOTS_FARM_CRDT_CHANGE_SCHEMA, RadrootsCrdtBackend, RadrootsFarmCrdtDocumentKind, - RadrootsFarmSemanticKind, - }; - use radroots_events::farm_file::{ - RadrootsFarmFileDimensions, RadrootsFarmFileMetadata, RadrootsFarmFileSource, - }; - use radroots_events::farm_workspace::{ - RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION, RADROOTS_FARM_WORKSPACE_SCHEMA, - RadrootsFarmWorkspaceManifest, RadrootsFarmWorkspaceMediaServer, RadrootsFarmWorkspaceRef, - RadrootsFarmWorkspaceRelay, RadrootsFarmWorkspaceRelayMode, - }; - use radroots_events::group::{ - RadrootsGroupAdmins, RadrootsGroupCreateGroup, RadrootsGroupCreateInvite, - RadrootsGroupDeleteEvent, RadrootsGroupDeleteGroup, RadrootsGroupEditMetadata, - RadrootsGroupEditableMetadata, RadrootsGroupJoinRequest, RadrootsGroupLeaveRequest, - RadrootsGroupMembers, RadrootsGroupMetadata, RadrootsGroupPutUser, RadrootsGroupRemoveUser, - RadrootsGroupRole, RadrootsGroupRoles, RadrootsGroupUserRef, - }; - use radroots_events::http_auth::RadrootsHttpAuth; - use radroots_events::job::JobInputType; - use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam}; - use radroots_events::kinds::KIND_FARM_FILE_METADATA; - use radroots_events::listing::{RadrootsListingBin, RadrootsListingProduct}; - use radroots_events::relay_auth::RadrootsRelayAuth; - use radroots_events::social::{ - RadrootsCalendarDateValue, RadrootsCalendarEventFreeBusy, RadrootsCalendarEventRsvpStatus, - RadrootsCalendarParticipant, RadrootsReportFileTarget, RadrootsReportType, - RadrootsSocialFarmAnchor, RadrootsSocialLocation, RadrootsSocialMediaDimensions, - RadrootsSocialMediaMetadata, RadrootsSocialTarget, - }; - - fn sample_listing() -> RadrootsListing { - let quantity = - RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each); - let price = RadrootsCoreQuantityPrice::new( - RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10u32), RadrootsCoreCurrency::USD), - quantity.clone(), - ); - - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "farm_pubkey".to_string(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), - }, - product: RadrootsListingProduct { - key: "sku".to_string(), - title: "widget".to_string(), - category: "tools".to_string(), - summary: None, - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".parse().expect("primary bin id"), - bins: vec![RadrootsListingBin { - bin_id: "bin-1".parse().expect("bin id"), - quantity, - price_per_canonical_unit: price, - display_amount: None, - display_unit: None, - display_label: None, - display_price: None, - display_price_unit: None, - }], - resource_area: None, - plot: None, - discounts: None, - inventory_available: None, - availability: None, - delivery_method: None, - location: None, - images: None, - } - } - - fn synthetic_pubkey(seed: char) -> String { - seed.to_string().repeat(64) - } - - fn synthetic_event_id(seed: char) -> String { - seed.to_string().repeat(64) - } - - fn social_farm_anchor() -> RadrootsSocialFarmAnchor { - RadrootsSocialFarmAnchor { - farm: RadrootsFarmRef { - pubkey: synthetic_pubkey('a'), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), - }, - relays: Some(vec!["wss://relay.example.test".to_string()]), - } - } - - fn event_target(kind: u32, seed: char) -> RadrootsSocialTarget { - RadrootsSocialTarget::Event { - id: synthetic_event_id(seed), - author: Some(synthetic_pubkey('b')), - event_kind: Some(kind), - relays: Some(vec!["wss://relay.example.test".to_string()]), - } - } - - fn address_target(kind: u32, d_tag: &str) -> RadrootsSocialTarget { - let author = synthetic_pubkey('c'); - RadrootsSocialTarget::Address { - address: format!("{kind}:{author}:{d_tag}"), - author: Some(author), - event_kind: Some(kind), - relays: Some(vec!["wss://relay2.example.test".to_string()]), - } - } - - fn social_location() -> RadrootsSocialLocation { - RadrootsSocialLocation { - name: Some("field edge".to_string()), - geohash: Some("c23nb62w20st".to_string()), - } - } - - fn sample_post() -> RadrootsPost { - RadrootsPost { - content: "field update".to_string(), - farm: Some(social_farm_anchor()), - address_refs: Some(vec![address_target(30023, "AAAAAAAAAAAAAAAAAAAAAQ")]), - location: Some(social_location()), - topics: Some(vec!["soil".to_string(), "market".to_string()]), - quote_refs: Some(vec![event_target(30023, 'd')]), - media: Some(vec![RadrootsSocialMediaMetadata { - url: Some("https://media.example.test/field.jpg".to_string()), - mime_type: Some("image/jpeg".to_string()), - sha256: Some( - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), - ), - original_sha256: None, - size: Some(4096), - dimensions: Some(RadrootsSocialMediaDimensions { - width: 1200, - height: 800, - }), - blurhash: None, - thumbnails: None, - image: None, - summary: Some("field photo".to_string()), - alt: Some("rows after harvest".to_string()), - fallback: None, - magnet: Some("magnet:?xt=urn:btih:abc".to_string()), - content_hashes: Some(vec!["sha256:field".to_string()]), - services: Some(vec!["https://media.example.test".to_string()]), - imeta: None, - }]), - } - } - - fn sample_article() -> RadrootsArticle { - RadrootsArticle { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), - title: "soil notes".to_string(), - content: "# soil notes".to_string(), - summary: Some("cover crop observations".to_string()), - image: Some("https://media.example.test/article.jpg".to_string()), - published_at: Some(1_780_000_000), - farm: Some(social_farm_anchor()), - location: Some(social_location()), - topics: Some(vec!["soil".to_string(), "cover-crops".to_string()]), - } - } - - fn sample_public_file_metadata() -> RadrootsFileMetadata { - RadrootsFileMetadata { - url: "https://media.example.test/public.jpg".to_string(), - mime_type: "image/jpeg".to_string(), - sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), - original_sha256: Some( - "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(), - ), - size: Some(4096), - dimensions: Some(RadrootsSocialMediaDimensions { - width: 1200, - height: 800, - }), - blurhash: None, - thumbnails: None, - summary: Some("public field photo".to_string()), - alt: Some("rows after harvest".to_string()), - fallback: Some("https://media.example.test/fallback.jpg".to_string()), - magnet: Some("magnet:?xt=urn:btih:abc".to_string()), - content_hashes: Some(vec!["sha256:field".to_string()]), - services: Some(vec!["https://media.example.test".to_string()]), - content: Some("caption".to_string()), - } - } - - fn sample_calendar_date_event() -> RadrootsCalendarDateEvent { - RadrootsCalendarDateEvent { - d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), - title: "market day".to_string(), - start: "2026-06-20".to_string(), - description: Some("Farm stand pickup window.".to_string()), - end: Some("2026-06-21".to_string()), - days: Some(vec![RadrootsCalendarDateValue { - value: "2026-06-20".to_string(), - }]), - location: Some(social_location()), - summary: Some("weekly pickup".to_string()), - image: None, - participants: Some(vec![RadrootsCalendarParticipant { - pubkey: synthetic_pubkey('e'), - relay: Some("wss://relay.example.test".to_string()), - role: Some("host".to_string()), - }]), - } - } - - fn sample_calendar_time_event() -> RadrootsCalendarTimeEvent { - RadrootsCalendarTimeEvent { - d_tag: "AAAAAAAAAAAAAAAAAAAA-A".to_string(), - title: "wash pack shift".to_string(), - start: 1_781_895_600, - dates: vec![RadrootsCalendarDateValue { - value: "2026-06-20".to_string(), - }], - description: Some("Prepare CSA bins before pickup.".to_string()), - end: Some(1_781_899_200), - start_tzid: Some("America/Vancouver".to_string()), - end_tzid: Some("America/Vancouver".to_string()), - location: Some(social_location()), - summary: Some("field crew".to_string()), - image: None, - participants: None, - } - } - - fn sample_calendar() -> RadrootsCalendar { - RadrootsCalendar { - d_tag: "AAAAAAAAAAAAAAAAAAAA_A".to_string(), - title: "farm calendar".to_string(), - events: vec![address_target(31923, "AAAAAAAAAAAAAAAAAAAA-A")], - description: Some("Shared schedule for farm operations.".to_string()), - summary: Some("field schedule".to_string()), - image: None, - } - } - - fn sample_calendar_rsvp() -> RadrootsCalendarEventRsvp { - RadrootsCalendarEventRsvp { - d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), - event: address_target(31923, "AAAAAAAAAAAAAAAAAAAA-A"), - event_id: Some(synthetic_event_id('f')), - status: RadrootsCalendarEventRsvpStatus::Tentative, - free_busy: Some(RadrootsCalendarEventFreeBusy::Busy), - note: Some("depends on harvest".to_string()), - participants: None, - } - } - - fn sample_comment() -> RadrootsComment { - RadrootsComment { - root: event_target(30023, 'a'), - parent: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"), - content: "great notes".to_string(), - } - } - - fn sample_reaction() -> RadrootsReaction { - RadrootsReaction { - target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"), - content: String::new(), - } - } - - fn sample_repost() -> RadrootsRepost { - RadrootsRepost { - target: event_target(1, 'b'), - content: Some("field update".to_string()), - } - } - - fn sample_generic_repost() -> RadrootsGenericRepost { - RadrootsGenericRepost { - target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"), - target_kind: 30023, - content: Some("article share".to_string()), - } - } - - fn sample_report() -> RadrootsReport { - RadrootsReport { - reported_pubkey: synthetic_pubkey('b'), - report_type: RadrootsReportType::Spam, - event: Some(event_target(1, 'c')), - file: Some(RadrootsReportFileTarget { - sha256: Some( - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), - ), - url: Some("https://media.example.test/bad.jpg".to_string()), - magnet: None, - }), - content: Some("spam report".to_string()), - } - } - - fn sample_job_request() -> RadrootsJobRequest { - RadrootsJobRequest { - kind: 5100, - inputs: vec![RadrootsJobInput { - data: "alpha".to_string(), - input_type: JobInputType::Text, - relay: None, - marker: None, - }], - output: None, - params: vec![RadrootsJobParam { - key: "mode".to_string(), - value: "fast".to_string(), - }], - bid_sat: Some(42), - relays: vec!["wss://relay.example.com".to_string()], - providers: vec!["provider-a".to_string()], - topics: vec!["topic-a".to_string()], - encrypted: false, - } - } - - fn sample_workspace_manifest() -> RadrootsFarmWorkspaceManifest { - RadrootsFarmWorkspaceManifest { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), - schema: RADROOTS_FARM_WORKSPACE_SCHEMA.to_string(), - farm_group_id: "field-group".to_string(), - name: "Small Regen Farm".to_string(), - owner_pubkey: "workspace_owner_pubkey".to_string(), - farm: Some(RadrootsFarmRef { - pubkey: "farm_pubkey".to_string(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), - }), - relays: vec![RadrootsFarmWorkspaceRelay { - url: "wss://relay.example.invalid/farm/field-group".to_string(), - mode: RadrootsFarmWorkspaceRelayMode::ReadWrite, - }], - media_servers: vec![RadrootsFarmWorkspaceMediaServer { - url: "https://media.example.invalid/farm/field-group".to_string(), - service: "RadrootsPrivateMedia".to_string(), - }], - supported_kinds: vec![78, 30078, KIND_FARM_FILE_METADATA], - protocol_version: RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION.to_string(), - created_at_ms: 1_780_000_000_000, - updated_at_ms: None, - } - } - - fn sample_crdt_change() -> RadrootsFarmCrdtChange { - RadrootsFarmCrdtChange { - schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(), - workspace: RadrootsFarmWorkspaceRef { - pubkey: "workspace_pubkey".to_string(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), - }, - farm_group_id: "field-group".to_string(), - document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), - document_kind: RadrootsFarmCrdtDocumentKind::FarmTask, - crdt_backend: RadrootsCrdtBackend::Automerge, - crdt_backend_version: Some("0.x".to_string()), - actor_id: "actor_abc".to_string(), - change_hash: "crdt_hash_abc".to_string(), - dependencies: Vec::new(), - encoded_change: "abc-DEF_012".to_string(), - semantic_kind: RadrootsFarmSemanticKind::FarmTaskCreate, - business_time_ms: 1_780_000_000_000, - author_member_id: Some("member_abc".to_string()), - app_version: Some("0.1.0".to_string()), - } - } - - fn sample_file_metadata() -> RadrootsFarmFileMetadata { - RadrootsFarmFileMetadata { - d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), - workspace: RadrootsFarmWorkspaceRef { - pubkey: "workspace_pubkey".to_string(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), - }, - farm_group_id: "field-group".to_string(), - owner_document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), - owner_document_kind: RadrootsFarmCrdtDocumentKind::FarmTask, - caption: Some("Tomatoes harvested from Patch Y.".to_string()), - url: "https://media.example.invalid/blob/sha256".to_string(), - mime_type: "image/jpeg".to_string(), - sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), - original_sha256: None, - size_bytes: Some(123_456), - dimensions: Some(RadrootsFarmFileDimensions { w: 1600, h: 1200 }), - blurhash: None, - thumb: Some(RadrootsFarmFileSource { - url: "https://media.example.invalid/thumb/sha256".to_string(), - mime_type: Some("image/jpeg".to_string()), - dimensions: Some(RadrootsFarmFileDimensions { w: 320, h: 240 }), - }), - image: None, - alt: Some("Harvested tomatoes in a crate".to_string()), - fallbacks: Vec::new(), - } - } - - fn sample_group_metadata() -> RadrootsGroupEditableMetadata { - RadrootsGroupEditableMetadata { - name: Some("Small Regen Farm".to_string()), - about: Some("Field app group".to_string()), - picture: Some("https://media.example.invalid/group.png".to_string()), - is_private: false, - is_restricted: true, - is_closed: false, - is_hidden: false, - supported_kinds: Some(vec![78, 30078, KIND_FARM_FILE_METADATA]), - } - } - - fn sample_group_user(role: &str) -> RadrootsGroupUserRef { - RadrootsGroupUserRef { - pubkey: format!("{role}_pubkey"), - roles: vec![role.to_string()], - } - } - - fn sample_group_role() -> RadrootsGroupRole { - RadrootsGroupRole { - name: "member".to_string(), - description: Some("can read and write group events".to_string()), - permissions: vec!["read".to_string(), "write".to_string()], - } - } - - fn assert_tags_json(value: Result<String, RadrootsJsValue>) { - let tags = tags_json(value); - assert!(!tags.is_empty()); - } - - fn tags_json(value: Result<String, RadrootsJsValue>) -> Vec<Vec<String>> { - let json = value.expect("tags json"); - serde_json::from_str(&json).expect("tags") - } - - fn has_tag(tags: &[Vec<String>], key: &str, value: &str) -> bool { - tags.iter().any(|tag| { - tag.first().map(|entry| entry.as_str()) == Some(key) - && tag.get(1).map(|entry| entry.as_str()) == Some(value) - }) - } - - #[test] - fn bindings_reject_invalid_json() { - let bindings: [fn(&str) -> Result<String, RadrootsJsValue>; 46] = [ - listing_tags, - listing_tags_full, - post_tags, - comment_tags, - article_tags, - file_metadata_tags, - calendar_date_event_tags, - calendar_time_event_tags, - calendar_tags, - calendar_event_rsvp_tags, - repost_tags, - generic_repost_tags, - report_tags, - follow_tags, - document_tags, - coop_tags, - farm_tags, - list_tags, - list_set_tags, - plot_tags, - job_request_tags, - job_result_tags, - job_feedback_tags, - reaction_tags, - message_tags, - message_file_tags, - seal_tags, - gift_wrap_tags, - farm_workspace_manifest_tags, - farm_crdt_change_tags, - farm_file_metadata_tags, - relay_auth_tags, - http_auth_tags, - group_put_user_tags, - group_remove_user_tags, - group_create_group_tags, - group_edit_metadata_tags, - group_delete_group_tags, - group_delete_event_tags, - group_create_invite_tags, - group_join_request_tags, - group_leave_request_tags, - group_metadata_tags, - group_admins_tags, - group_members_tags, - group_roles_tags, - ]; - - for binding in bindings { - assert!(binding("{").is_err()); - } - assert!(listing_tags("").is_err()); - } - - #[test] - fn bindings_encode_to_json_when_input_is_valid() { - let listing_json = serde_json::to_string(&sample_listing()).expect("listing json"); - let listing_tags_json = listing_tags(&listing_json).expect("listing tags"); - let listing_tags: Vec<Vec<String>> = - serde_json::from_str(&listing_tags_json).expect("listing tags json"); - assert!(!listing_tags.is_empty()); - - let request_json = serde_json::to_string(&sample_job_request()).expect("request json"); - let request_tags_json = job_request_tags(&request_json).expect("request tags"); - let request_tags: Vec<Vec<String>> = - serde_json::from_str(&request_tags_json).expect("request tags json"); - assert!(!request_tags.is_empty()); - } - - #[test] - fn social_bindings_encode_to_json_when_input_is_valid() { - assert_tags_json(post_tags( - &serde_json::to_string(&sample_post()).expect("post json"), - )); - assert_tags_json(comment_tags( - &serde_json::to_string(&sample_comment()).expect("comment json"), - )); - assert_tags_json(article_tags( - &serde_json::to_string(&sample_article()).expect("article json"), - )); - assert_tags_json(file_metadata_tags( - &serde_json::to_string(&sample_public_file_metadata()).expect("file json"), - )); - assert_tags_json(calendar_date_event_tags( - &serde_json::to_string(&sample_calendar_date_event()).expect("date json"), - )); - let time_tags = tags_json(calendar_time_event_tags( - &serde_json::to_string(&sample_calendar_time_event()).expect("time json"), - )); - assert!(has_tag(&time_tags, "D", "2026-06-20")); - assert_tags_json(calendar_tags( - &serde_json::to_string(&sample_calendar()).expect("calendar json"), - )); - assert_tags_json(calendar_event_rsvp_tags( - &serde_json::to_string(&sample_calendar_rsvp()).expect("rsvp json"), - )); - assert_tags_json(reaction_tags( - &serde_json::to_string(&sample_reaction()).expect("reaction json"), - )); - assert_tags_json(repost_tags( - &serde_json::to_string(&sample_repost()).expect("repost json"), - )); - assert_tags_json(generic_repost_tags( - &serde_json::to_string(&sample_generic_repost()).expect("generic repost json"), - )); - assert_tags_json(report_tags( - &serde_json::to_string(&sample_report()).expect("report json"), - )); - } - - #[test] - fn social_bindings_surface_builder_errors() { - let mut article = sample_article(); - article.d_tag.clear(); - assert!(article_tags(&serde_json::to_string(&article).expect("article json")).is_err()); - - let mut comment = sample_comment(); - comment.root = event_target(1, 'a'); - assert!(comment_tags(&serde_json::to_string(&comment).expect("comment json")).is_err()); - - let mut reaction = sample_reaction(); - reaction.target = RadrootsSocialTarget::External { - id: "https://example.test/object".to_string(), - external_kind: "web".to_string(), - hint: None, - }; - assert!(reaction_tags(&serde_json::to_string(&reaction).expect("reaction json")).is_err()); - - let mut rsvp = sample_calendar_rsvp(); - rsvp.event = event_target(31923, 'f'); - assert!( - calendar_event_rsvp_tags(&serde_json::to_string(&rsvp).expect("rsvp json")).is_err() - ); - - let mut report = sample_report(); - report.reported_pubkey.clear(); - assert!(report_tags(&serde_json::to_string(&report).expect("report json")).is_err()); - } - - #[test] - fn field_bindings_encode_to_json_when_input_is_valid() { - let workspace_json = - serde_json::to_string(&sample_workspace_manifest()).expect("workspace json"); - assert_tags_json(farm_workspace_manifest_tags(&workspace_json)); - - let crdt_json = serde_json::json!({ - "change": sample_crdt_change(), - "author_pubkey": "author_pubkey" - }) - .to_string(); - assert_tags_json(farm_crdt_change_tags(&crdt_json)); - - let file_json = serde_json::to_string(&sample_file_metadata()).expect("file json"); - assert_tags_json(farm_file_metadata_tags(&file_json)); - - let relay_auth_json = serde_json::to_string(&RadrootsRelayAuth { - relay: "wss://relay.example.invalid/farm/field-group".to_string(), - challenge: "relay-provided-challenge".to_string(), - }) - .expect("relay auth json"); - assert_tags_json(relay_auth_tags(&relay_auth_json)); - - let http_auth_json = serde_json::to_string(&RadrootsHttpAuth { - url: "https://media.example.invalid/upload".to_string(), - method: "POST".to_string(), - payload_sha256: Some( - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), - ), - }) - .expect("http auth json"); - assert_tags_json(http_auth_tags(&http_auth_json)); - } - - #[test] - fn group_bindings_encode_to_json_when_input_is_valid() { - let metadata = sample_group_metadata(); - assert_tags_json(group_put_user_tags( - &serde_json::to_string(&RadrootsGroupPutUser { - group_id: "field-group".to_string(), - message: Some("add member".to_string()), - pubkey: "member_pubkey".to_string(), - roles: vec!["member".to_string()], - }) - .expect("put user json"), - )); - assert_tags_json(group_remove_user_tags( - &serde_json::to_string(&RadrootsGroupRemoveUser { - group_id: "field-group".to_string(), - message: Some("remove member".to_string()), - pubkey: "member_pubkey".to_string(), - }) - .expect("remove user json"), - )); - assert_tags_json(group_create_group_tags( - &serde_json::to_string(&RadrootsGroupCreateGroup { - group_id: "field-group".to_string(), - message: Some("create group".to_string()), - metadata: metadata.clone(), - }) - .expect("create group json"), - )); - assert_tags_json(group_edit_metadata_tags( - &serde_json::to_string(&RadrootsGroupEditMetadata { - group_id: "field-group".to_string(), - message: Some("edit metadata".to_string()), - metadata: metadata.clone(), - }) - .expect("edit metadata json"), - )); - assert_tags_json(group_delete_group_tags( - &serde_json::to_string(&RadrootsGroupDeleteGroup { - group_id: "field-group".to_string(), - message: Some("delete group".to_string()), - }) - .expect("delete group json"), - )); - assert_tags_json(group_delete_event_tags( - &serde_json::to_string(&RadrootsGroupDeleteEvent { - group_id: "field-group".to_string(), - message: Some("delete event".to_string()), - event_id: "event_id".to_string(), - }) - .expect("delete event json"), - )); - let invite_tags = tags_json(group_create_invite_tags( - &serde_json::to_string(&RadrootsGroupCreateInvite { - group_id: "field-group".to_string(), - message: Some("join the field group".to_string()), - code: "invite-code".to_string(), - }) - .expect("invite json"), - )); - assert!(invite_tags.contains(&vec!["code".to_string(), "invite-code".to_string()])); - assert_tags_json(group_join_request_tags( - &serde_json::to_string(&RadrootsGroupJoinRequest { - group_id: "field-group".to_string(), - message: Some("requesting access".to_string()), - code: Some("invite-code".to_string()), - }) - .expect("join json"), - )); - assert_tags_json(group_leave_request_tags( - &serde_json::to_string(&RadrootsGroupLeaveRequest { - group_id: "field-group".to_string(), - message: Some("leaving".to_string()), - }) - .expect("leave json"), - )); - let metadata_tags = tags_json(group_metadata_tags( - &serde_json::to_string(&RadrootsGroupMetadata { - d_tag: "field-group".to_string(), - metadata, - }) - .expect("metadata json"), - )); - assert!(metadata_tags.contains(&vec!["restricted".to_string()])); - assert!(metadata_tags.contains(&vec![ - "supported_kinds".to_string(), - "78".to_string(), - "30078".to_string(), - KIND_FARM_FILE_METADATA.to_string() - ])); - assert_tags_json(group_admins_tags( - &serde_json::to_string(&RadrootsGroupAdmins { - d_tag: "field-group".to_string(), - description: Some("group admins".to_string()), - admins: vec![sample_group_user("admin")], - }) - .expect("admins json"), - )); - assert_tags_json(group_members_tags( - &serde_json::to_string(&RadrootsGroupMembers { - d_tag: "field-group".to_string(), - description: Some("group members".to_string()), - members: vec![sample_group_user("member")], - }) - .expect("members json"), - )); - assert_tags_json(group_roles_tags( - &serde_json::to_string(&RadrootsGroupRoles { - d_tag: "field-group".to_string(), - description: Some("group roles".to_string()), - roles: vec![sample_group_role()], - }) - .expect("roles json"), - )); - } - - #[test] - fn listing_bindings_surface_builder_errors() { - let mut listing_json = serde_json::to_value(sample_listing()).expect("listing value"); - listing_json["bins"] = serde_json::Value::Array(Vec::new()); - let listing_json = serde_json::to_string(&listing_json).expect("listing json"); - - assert!(listing_tags(&listing_json).is_err()); - assert!(listing_tags_full(&listing_json).is_err()); - } -} diff --git a/crates/replica_db_wasm/Cargo.toml b/crates/replica_db_wasm/Cargo.toml @@ -1,36 +0,0 @@ -[package] -name = "radroots_replica_db_wasm" -publish = false -version = "0.1.0-alpha.2" -edition.workspace = true -authors = ["Tyson Lupul <tyson@radroots.org>"] -rust-version.workspace = true -license.workspace = true -description = "WebAssembly bindings for radroots_replica_db" -repository.workspace = true -homepage.workspace = true -documentation = "https://docs.rs/radroots_replica_db_wasm" -readme = "README" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -radroots_sql_core = { workspace = true, features = ["bridge"] } -radroots_sql_wasm_core = { workspace = true, default-features = false, features = [ - "bridge", -] } -radroots_replica_db = { workspace = true } -radroots_replica_db_schema = { workspace = true } -radroots_replica_sync = { workspace = true, features = ["std"] } -js-sys = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -serde-wasm-bindgen = { workspace = true } -wasm-bindgen = { workspace = true } - -[dev-dependencies] -wasm-bindgen-test = { workspace = true } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/replica_db_wasm/README b/crates/replica_db_wasm/README @@ -1,25 +0,0 @@ -# radroots_replica_db_wasm - -This is the README for `radroots_replica_db_wasm`, which provides WebAssembly -bindings for `radroots_replica_db` in the `radroots` core libraries. - -## Overview - - * a wasm32-only wrapper around the replica database runtime surface; - * integration with `radroots_sql_wasm_core` for browser and worker SQL - execution; - * a small target-specific entry point layer rather than a separate database - implementation; - * intended for JavaScript-facing callers that need the replica database APIs - in wasm builds. - -## Copyright - -Except as otherwise noted, all files in the `radroots_replica_db_wasm` -distribution are - - Copyright (c) 2020-2026 Tyson Lupul - -For information on usage and redistribution, and for a DISCLAIMER OF ALL -WARRANTIES, see LICENSE included in the `radroots_replica_db_wasm` -distribution. diff --git a/crates/replica_db_wasm/pkg/package.json b/crates/replica_db_wasm/pkg/package.json @@ -1,20 +0,0 @@ -{ - "name": "@radroots/radroots_replica-db-wasm", - "description": "WebAssembly bindings for radroots_replica_db", - "version": "0.1.0", - "private": true, - "type": "module", - "files": [ - "dist" - ], - "main": "./dist/radroots_replica_db_wasm.js", - "types": "./dist/radroots_replica_db_wasm.d.ts", - "exports": { - ".": { - "types": "./dist/radroots_replica_db_wasm.d.ts", - "import": "./dist/radroots_replica_db_wasm.js", - "default": "./dist/radroots_replica_db_wasm.js" - } - }, - "sideEffects": false -} diff --git a/crates/replica_db_wasm/src/lib.rs b/crates/replica_db_wasm/src/lib.rs @@ -1,29 +0,0 @@ -#![cfg(any(target_arch = "wasm32", coverage_nightly))] -#![forbid(unsafe_code)] - -#[cfg(target_arch = "wasm32")] -mod utils; -#[cfg(target_arch = "wasm32")] -mod wasm_impl; -#[cfg(target_arch = "wasm32")] -pub use wasm_impl::*; - -#[cfg(coverage_nightly)] -pub fn coverage_branch_probe(input: bool) -> &'static str { - if input { - "replica-db-wasm" - } else { - "replica-db-wasm" - } -} - -#[cfg(all(test, coverage_nightly))] -mod tests { - use super::coverage_branch_probe; - - #[test] - fn coverage_branch_probe_hits_both_paths() { - assert_eq!(coverage_branch_probe(true), "replica-db-wasm"); - assert_eq!(coverage_branch_probe(false), "replica-db-wasm"); - } -} diff --git a/crates/replica_db_wasm/src/utils.rs b/crates/replica_db_wasm/src/utils.rs @@ -1,13 +0,0 @@ -use serde::Serialize; -use wasm_bindgen::prelude::*; - -use radroots_sql_core::SqlError; - -pub fn value_to_js<T>(value: T) -> Result<JsValue, JsValue> -where - T: Serialize, -{ - let json = serde_json::to_string(&value) - .map_err(|err| radroots_sql_wasm_core::err_js(SqlError::from(err)))?; - Ok(JsValue::from_str(&json)) -} diff --git a/crates/replica_db_wasm/src/wasm_impl.rs b/crates/replica_db_wasm/src/wasm_impl.rs @@ -1,891 +0,0 @@ -use crate::utils::value_to_js; -use radroots_replica_db::migrations; -use radroots_replica_db::{ReplicaDbExportManifestRs, export_manifest}; -use radroots_replica_sync::radroots_replica_sync_status; -use radroots_sql_core::{ - WasmSqlExecutor, export_lock_begin, export_lock_end, with_export_lock_bypass, -}; -use radroots_sql_wasm_core::{err_js, parse_json}; -use wasm_bindgen::JsValue; -use wasm_bindgen::prelude::*; - -use radroots_replica_db_schema::farm::{ - IFarmCreate, IFarmDelete, IFarmFindMany, IFarmFindOne, IFarmUpdate, -}; - -use radroots_replica_db_schema::farm_gcs_location::{ - IFarmGcsLocationCreate, IFarmGcsLocationDelete, IFarmGcsLocationFindMany, - IFarmGcsLocationFindOne, IFarmGcsLocationUpdate, -}; - -use radroots_replica_db_schema::farm_member::{ - IFarmMemberCreate, IFarmMemberDelete, IFarmMemberFindMany, IFarmMemberFindOne, - IFarmMemberUpdate, -}; - -use radroots_replica_db_schema::farm_member_claim::{ - IFarmMemberClaimCreate, IFarmMemberClaimDelete, IFarmMemberClaimFindMany, - IFarmMemberClaimFindOne, IFarmMemberClaimUpdate, -}; - -use radroots_replica_db_schema::farm_tag::{ - IFarmTagCreate, IFarmTagDelete, IFarmTagFindMany, IFarmTagFindOne, IFarmTagUpdate, -}; - -use radroots_replica_db_schema::gcs_location::{ - IGcsLocationCreate, IGcsLocationDelete, IGcsLocationFindMany, IGcsLocationFindOne, - IGcsLocationUpdate, -}; - -use radroots_replica_db_schema::log_error::{ - ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate, -}; - -use radroots_replica_db_schema::media_image::{ - IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, - IMediaImageUpdate, -}; - -use radroots_replica_db_schema::nostr_profile::{ - INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, - INostrProfileUpdate, -}; - -use radroots_replica_db_schema::nostr_event_head::{ - INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, - INostrEventHeadUpdate, -}; - -use radroots_replica_db_schema::nostr_relay::{ - INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne, - INostrRelayUpdate, -}; - -use radroots_replica_db_schema::trade_product::{ - ITradeProductCreate, ITradeProductDelete, ITradeProductFindMany, ITradeProductFindOne, - ITradeProductUpdate, -}; - -use radroots_replica_db_schema::plot::{ - IPlotCreate, IPlotDelete, IPlotFindMany, IPlotFindOne, IPlotUpdate, -}; - -use radroots_replica_db_schema::plot_gcs_location::{ - IPlotGcsLocationCreate, IPlotGcsLocationDelete, IPlotGcsLocationFindMany, - IPlotGcsLocationFindOne, IPlotGcsLocationUpdate, -}; - -use radroots_replica_db_schema::plot_tag::{ - IPlotTagCreate, IPlotTagDelete, IPlotTagFindMany, IPlotTagFindOne, IPlotTagUpdate, -}; - -use radroots_replica_db_schema::nostr_profile_relay::INostrProfileRelayRelation; - -use radroots_replica_db_schema::trade_product_location::ITradeProductLocationRelation; - -use radroots_replica_db_schema::trade_product_media::ITradeProductMediaRelation; - -#[wasm_bindgen(js_name = replica_db_run_migrations)] -pub fn replica_db_run_migrations() -> Result<(), JsValue> { - let exec = WasmSqlExecutor::new(); - migrations::run_all_up(&exec).map_err(err_js) -} - -#[wasm_bindgen(js_name = replica_db_reset_database)] -pub fn replica_db_reset_database() -> Result<(), JsValue> { - let exec = WasmSqlExecutor::new(); - migrations::run_all_down(&exec).map_err(err_js) -} - -#[wasm_bindgen(js_name = replica_db_export_json)] -pub fn replica_db_export_json() -> Result<JsValue, JsValue> { - let exec = WasmSqlExecutor::new(); - let dump = radroots_replica_db::backup::export_database_backup(&exec).map_err(err_js)?; - value_to_js(dump) -} - -#[wasm_bindgen(js_name = replica_db_import_json)] -pub fn replica_db_import_json(dump_json: &str) -> Result<(), JsValue> { - let exec = WasmSqlExecutor::new(); - radroots_replica_db::backup::restore_database_backup_json(&exec, dump_json).map_err(err_js) -} - -#[wasm_bindgen(js_name = replica_db_export_begin)] -pub fn replica_db_export_begin() -> Result<JsValue, JsValue> { - export_lock_begin().map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let result = with_export_lock_bypass(|| export_snapshot(&exec)); - match result { - Ok(value) => Ok(value), - Err(err) => { - export_lock_end(); - Err(err) - } - } -} - -#[wasm_bindgen(js_name = replica_db_export_finish)] -pub fn replica_db_export_finish() -> Result<(), JsValue> { - export_lock_end(); - Ok(()) -} - -fn export_snapshot(exec: &WasmSqlExecutor) -> Result<JsValue, JsValue> { - let status = radroots_replica_sync_status(exec).map_err(|err| { - err_js(radroots_sql_core::SqlError::InvalidArgument( - err.to_string(), - )) - })?; - if status.pending_count > 0 { - return Err(err_js(radroots_sql_core::SqlError::InvalidArgument( - format!( - "replica db export requires synced state (pending {}/{})", - status.pending_count, status.expected_count - ), - ))); - } - let manifest = export_manifest(exec).map_err(err_js)?; - export_snapshot_value(manifest) -} - -fn export_snapshot_value(manifest: ReplicaDbExportManifestRs) -> Result<JsValue, JsValue> { - let bytes_js = radroots_sql_wasm_core::export_bytes(); - export_snapshot_value_with_bytes(manifest, bytes_js) -} - -fn export_snapshot_value_with_bytes( - manifest: ReplicaDbExportManifestRs, - bytes_js: JsValue, -) -> Result<JsValue, JsValue> { - let manifest_js = serde_wasm_bindgen::to_value(&manifest).map_err(|err| { - err_js(radroots_sql_core::SqlError::SerializationError( - err.to_string(), - )) - })?; - let obj = js_sys::Object::new(); - js_sys::Reflect::set(&obj, &JsValue::from_str("manifest_rs"), &manifest_js) - .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?; - js_sys::Reflect::set(&obj, &JsValue::from_str("db_bytes"), &bytes_js) - .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?; - Ok(JsValue::from(obj)) -} - -#[cfg(all(test, target_arch = "wasm32"))] -mod tests { - use super::export_snapshot_value_with_bytes; - use js_sys::{Reflect, Uint8Array}; - use wasm_bindgen::JsValue; - - #[wasm_bindgen_test::wasm_bindgen_test] - fn export_snapshot_value_includes_fields() { - let manifest = radroots_replica_db::ReplicaDbExportManifestRs { - export_version: "1".to_string(), - replica_db_version: "0.0.0".to_string(), - backup_format_version: "0.0.0".to_string(), - schema_hash: "hash".to_string(), - schema: Vec::new(), - migrations: Vec::new(), - table_counts: Vec::new(), - }; - let bytes = Uint8Array::new_with_length(2); - let js = - export_snapshot_value_with_bytes(manifest, JsValue::from(bytes)).expect("snapshot"); - let manifest_rs = - Reflect::get(&js, &JsValue::from_str("manifest_rs")).expect("manifest_rs"); - let db_bytes = Reflect::get(&js, &JsValue::from_str("db_bytes")).expect("db_bytes"); - assert!(manifest_rs.is_object()); - assert!(db_bytes.is_object()); - } -} - -#[wasm_bindgen(js_name = replica_db_farm_create)] -pub fn replica_db_farm_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_find_one)] -pub fn replica_db_farm_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_find_many)] -pub fn replica_db_farm_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_update)] -pub fn replica_db_farm_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_delete)] -pub fn replica_db_farm_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_create)] -pub fn replica_db_plot_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_find_one)] -pub fn replica_db_plot_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_find_many)] -pub fn replica_db_plot_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_update)] -pub fn replica_db_plot_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_delete)] -pub fn replica_db_plot_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_gcs_location_create)] -pub fn replica_db_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::gcs_location::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_gcs_location_find_one)] -pub fn replica_db_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::gcs_location::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_gcs_location_find_many)] -pub fn replica_db_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::gcs_location::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_gcs_location_update)] -pub fn replica_db_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::gcs_location::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_gcs_location_delete)] -pub fn replica_db_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::gcs_location::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_gcs_location_create)] -pub fn replica_db_farm_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::farm_gcs_location::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_gcs_location_find_one)] -pub fn replica_db_farm_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_gcs_location::find_one(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_gcs_location_find_many)] -pub fn replica_db_farm_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_gcs_location::find_many(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_gcs_location_update)] -pub fn replica_db_farm_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::farm_gcs_location::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_gcs_location_delete)] -pub fn replica_db_farm_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::farm_gcs_location::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_gcs_location_create)] -pub fn replica_db_plot_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::plot_gcs_location::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_gcs_location_find_one)] -pub fn replica_db_plot_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot_gcs_location::find_one(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_gcs_location_find_many)] -pub fn replica_db_plot_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot_gcs_location::find_many(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_gcs_location_update)] -pub fn replica_db_plot_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::plot_gcs_location::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_gcs_location_delete)] -pub fn replica_db_plot_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::plot_gcs_location::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_tag_create)] -pub fn replica_db_farm_tag_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmTagCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_tag::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_tag_find_one)] -pub fn replica_db_farm_tag_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmTagFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_tag::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_tag_find_many)] -pub fn replica_db_farm_tag_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmTagFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_tag::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_tag_update)] -pub fn replica_db_farm_tag_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmTagUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_tag::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_tag_delete)] -pub fn replica_db_farm_tag_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmTagDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_tag::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_tag_create)] -pub fn replica_db_plot_tag_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotTagCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot_tag::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_tag_find_one)] -pub fn replica_db_plot_tag_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotTagFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot_tag::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_tag_find_many)] -pub fn replica_db_plot_tag_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotTagFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot_tag::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_tag_update)] -pub fn replica_db_plot_tag_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotTagUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot_tag::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_plot_tag_delete)] -pub fn replica_db_plot_tag_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotTagDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::plot_tag::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_member_create)] -pub fn replica_db_farm_member_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_member::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_member_find_one)] -pub fn replica_db_farm_member_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::farm_member::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_member_find_many)] -pub fn replica_db_farm_member_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::farm_member::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_member_update)] -pub fn replica_db_farm_member_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_member::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_member_delete)] -pub fn replica_db_farm_member_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_member::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_member_claim_create)] -pub fn replica_db_farm_member_claim_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberClaimCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::farm_member_claim::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_member_claim_find_one)] -pub fn replica_db_farm_member_claim_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberClaimFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_member_claim::find_one(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_member_claim_find_many)] -pub fn replica_db_farm_member_claim_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberClaimFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::farm_member_claim::find_many(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_member_claim_update)] -pub fn replica_db_farm_member_claim_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberClaimUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::farm_member_claim::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_farm_member_claim_delete)] -pub fn replica_db_farm_member_claim_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberClaimDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::farm_member_claim::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_log_error_create)] -pub fn replica_db_log_error_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ILogErrorCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::log_error::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_log_error_find_one)] -pub fn replica_db_log_error_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ILogErrorFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::log_error::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_log_error_find_many)] -pub fn replica_db_log_error_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ILogErrorFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::log_error::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_log_error_update)] -pub fn replica_db_log_error_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ILogErrorUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::log_error::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_log_error_delete)] -pub fn replica_db_log_error_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ILogErrorDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::log_error::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_media_image_create)] -pub fn replica_db_media_image_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IMediaImageCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::media_image::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_media_image_find_one)] -pub fn replica_db_media_image_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IMediaImageFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::media_image::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_media_image_find_many)] -pub fn replica_db_media_image_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IMediaImageFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::media_image::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_media_image_update)] -pub fn replica_db_media_image_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IMediaImageUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::media_image::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_media_image_delete)] -pub fn replica_db_media_image_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IMediaImageDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::media_image::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_profile_create)] -pub fn replica_db_nostr_profile_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_profile::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_profile_find_one)] -pub fn replica_db_nostr_profile_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_profile::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_profile_find_many)] -pub fn replica_db_nostr_profile_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_profile::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_profile_update)] -pub fn replica_db_nostr_profile_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_profile::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_profile_delete)] -pub fn replica_db_nostr_profile_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_profile::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_event_head_create)] -pub fn replica_db_nostr_event_head_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventHeadCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_event_head::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_event_head_find_one)] -pub fn replica_db_nostr_event_head_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventHeadFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_event_head::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_event_head_find_many)] -pub fn replica_db_nostr_event_head_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventHeadFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::nostr_event_head::find_many(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_event_head_update)] -pub fn replica_db_nostr_event_head_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventHeadUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_event_head::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_event_head_delete)] -pub fn replica_db_nostr_event_head_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventHeadDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_event_head::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_relay_create)] -pub fn replica_db_nostr_relay_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrRelayCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::nostr_relay::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_relay_find_one)] -pub fn replica_db_nostr_relay_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrRelayFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_relay::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_relay_find_many)] -pub fn replica_db_nostr_relay_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrRelayFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_relay::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_relay_update)] -pub fn replica_db_nostr_relay_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrRelayUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::nostr_relay::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_relay_delete)] -pub fn replica_db_nostr_relay_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrRelayDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::nostr_relay::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_trade_product_create)] -pub fn replica_db_trade_product_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::trade_product::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_trade_product_find_one)] -pub fn replica_db_trade_product_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::trade_product::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_trade_product_find_many)] -pub fn replica_db_trade_product_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::trade_product::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_trade_product_update)] -pub fn replica_db_trade_product_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::trade_product::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_trade_product_delete)] -pub fn replica_db_trade_product_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::trade_product::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_profile_relay_set)] -pub fn replica_db_nostr_profile_relay_set(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileRelayRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_profile_relay::set(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_nostr_profile_relay_unset)] -pub fn replica_db_nostr_profile_relay_unset(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileRelayRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::nostr_profile_relay::unset(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_trade_product_location_set)] -pub fn replica_db_trade_product_location_set(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductLocationRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::trade_product_location::set(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_trade_product_location_unset)] -pub fn replica_db_trade_product_location_unset(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductLocationRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::trade_product_location::unset(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_trade_product_media_set)] -pub fn replica_db_trade_product_media_set(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductMediaRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::trade_product_media::set(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = replica_db_trade_product_media_unset)] -pub fn replica_db_trade_product_media_unset(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductMediaRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_replica_db::trade_product_media::unset(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} diff --git a/crates/replica_sync_wasm/Cargo.toml b/crates/replica_sync_wasm/Cargo.toml @@ -1,35 +0,0 @@ -[package] -name = "radroots_replica_sync_wasm" -publish = false -version = "0.1.0-alpha.2" -edition.workspace = true -authors = ["Tyson Lupul <tyson@radroots.org>"] -rust-version.workspace = true -license.workspace = true -description = "WebAssembly bindings for radroots_replica_sync" -repository.workspace = true -homepage.workspace = true -documentation = "https://docs.rs/radroots_replica_sync_wasm" -readme = "README" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -base64 = { workspace = true } -radroots_events = { workspace = true, default-features = false, features = [ - "serde", -] } -radroots_sql_core = { workspace = true, features = ["bridge"] } -radroots_sql_wasm_core = { workspace = true, default-features = false, features = [ - "bridge", -] } -radroots_replica_sync = { workspace = true, features = ["std"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -serde-wasm-bindgen = { workspace = true } -uuid = { workspace = true, features = ["js"] } -wasm-bindgen = { workspace = true } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/replica_sync_wasm/README b/crates/replica_sync_wasm/README @@ -1,25 +0,0 @@ -# radroots_replica_sync_wasm - -This is the README for `radroots_replica_sync_wasm`, which provides WebAssembly -bindings for `radroots_replica_sync` in the `radroots` core libraries. - -## Overview - - * wasm32 entry points for full sync and single-event ingest operations; - * integration with `WasmSqlExecutor` from `radroots_sql_core` for database - access; - * UUIDv7 id generation and JSON and base64 boundary handling for JavaScript - callers; - * a small target-specific wrapper around the Rust sync crate rather than a - separate sync engine. - -## Copyright - -Except as otherwise noted, all files in the `radroots_replica_sync_wasm` -distribution are - - Copyright (c) 2020-2026 Tyson Lupul - -For information on usage and redistribution, and for a DISCLAIMER OF ALL -WARRANTIES, see LICENSE included in the `radroots_replica_sync_wasm` -distribution. diff --git a/crates/replica_sync_wasm/pkg/package.json b/crates/replica_sync_wasm/pkg/package.json @@ -1,20 +0,0 @@ -{ - "name": "@radroots/radroots_replica-sync-wasm", - "description": "WebAssembly bindings for radroots_replica_sync", - "version": "0.1.0", - "private": true, - "type": "module", - "files": [ - "dist" - ], - "main": "./dist/radroots_replica_sync_wasm.js", - "types": "./dist/radroots_replica_sync_wasm.d.ts", - "exports": { - ".": { - "types": "./dist/radroots_replica_sync_wasm.d.ts", - "import": "./dist/radroots_replica_sync_wasm.js", - "default": "./dist/radroots_replica_sync_wasm.js" - } - }, - "sideEffects": false -} diff --git a/crates/replica_sync_wasm/src/lib.rs b/crates/replica_sync_wasm/src/lib.rs @@ -1,124 +0,0 @@ -#![cfg(any(target_arch = "wasm32", coverage_nightly))] -#![forbid(unsafe_code)] - -#[cfg(target_arch = "wasm32")] -use base64::Engine; -#[cfg(target_arch = "wasm32")] -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -#[cfg(target_arch = "wasm32")] -use radroots_events::RadrootsNostrEvent; -#[cfg(target_arch = "wasm32")] -use radroots_replica_sync::{ - RadrootsReplicaIdFactory, RadrootsReplicaIngestOutcome, RadrootsReplicaSyncRequest, - radroots_replica_ingest_event_with_factory, radroots_replica_sync_all, -}; -#[cfg(target_arch = "wasm32")] -use radroots_sql_core::WasmSqlExecutor; -#[cfg(target_arch = "wasm32")] -use serde::Deserialize; -#[cfg(target_arch = "wasm32")] -use uuid::Uuid; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; - -#[cfg(target_arch = "wasm32")] -fn err_js<E: ToString>(err: E) -> JsValue { - JsValue::from_str(&err.to_string()) -} - -#[cfg(target_arch = "wasm32")] -struct WasmIdFactory; - -#[cfg(target_arch = "wasm32")] -impl RadrootsReplicaIdFactory for WasmIdFactory { - fn new_d_tag(&self) -> String { - let uuid = Uuid::now_v7(); - URL_SAFE_NO_PAD.encode(uuid.as_bytes()) - } -} - -#[cfg(target_arch = "wasm32")] -#[derive(Deserialize)] -struct NostrEventEnvelope { - id: String, - #[serde(default)] - author: Option<String>, - #[serde(default)] - pubkey: Option<String>, - created_at: u32, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, -} - -#[cfg(target_arch = "wasm32")] -fn parse_request(request_json: &str) -> Result<RadrootsReplicaSyncRequest, JsValue> { - serde_json::from_str(request_json).map_err(err_js) -} - -#[cfg(target_arch = "wasm32")] -fn parse_event(event_json: &str) -> Result<RadrootsNostrEvent, JsValue> { - let envelope: NostrEventEnvelope = serde_json::from_str(event_json).map_err(err_js)?; - let author = match (envelope.author, envelope.pubkey) { - (Some(author), Some(pubkey)) if author != pubkey => { - return Err(JsValue::from_str("author/pubkey mismatch")); - } - (Some(author), _) => author, - (None, Some(pubkey)) => pubkey, - (None, None) => return Err(JsValue::from_str("missing author/pubkey")), - }; - Ok(RadrootsNostrEvent { - id: envelope.id, - author, - created_at: envelope.created_at, - kind: envelope.kind, - tags: envelope.tags, - content: envelope.content, - sig: envelope.sig, - }) -} - -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen(js_name = replica_sync_sync_all)] -pub fn replica_sync_sync_all(request_json: &str) -> Result<JsValue, JsValue> { - let request = parse_request(request_json)?; - let exec = WasmSqlExecutor::new(); - let bundle = radroots_replica_sync_all(&exec, &request).map_err(err_js)?; - serde_wasm_bindgen::to_value(&bundle).map_err(err_js) -} - -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen(js_name = replica_sync_ingest_event)] -pub fn replica_sync_ingest_event(event_json: &str) -> Result<JsValue, JsValue> { - let event = parse_event(event_json)?; - let exec = WasmSqlExecutor::new(); - let factory = WasmIdFactory; - let outcome = - radroots_replica_ingest_event_with_factory(&exec, &event, &factory).map_err(err_js)?; - let value = match outcome { - RadrootsReplicaIngestOutcome::Applied => "applied", - RadrootsReplicaIngestOutcome::Skipped => "skipped", - }; - Ok(JsValue::from_str(value)) -} - -#[cfg(coverage_nightly)] -pub fn coverage_branch_probe(input: bool) -> &'static str { - if input { - "replica-sync-wasm" - } else { - "replica-sync-wasm" - } -} - -#[cfg(all(test, coverage_nightly))] -mod tests { - use super::coverage_branch_probe; - - #[test] - fn coverage_branch_probe_hits_both_paths() { - assert_eq!(coverage_branch_probe(true), "replica-sync-wasm"); - assert_eq!(coverage_branch_probe(false), "replica-sync-wasm"); - } -} diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -1,78 +0,0 @@ -[package] -name = "radroots_sdk" -publish = ["crates-io"] -version.workspace = true -edition.workspace = true -authors = ["Tyson Lupul <tyson@radroots.org>"] -rust-version.workspace = true -license.workspace = true -description = "Curated Radroots SDK for profile, farm, listing, and trade event workflows" -repository.workspace = true -homepage.workspace = true -documentation = "https://docs.rs/radroots_sdk" -readme = "README" - -[features] -default = ["std", "serde", "serde_json", "identity-models"] -std = ["radroots_events/std", "radroots_events_codec/std", "radroots_trade/std"] -serde = ["dep:serde", "radroots_events/serde", "radroots_trade/serde"] -serde_json = [ - "dep:serde_json", - "serde", - "nostr", - "radroots_events_codec/serde_json", - "radroots_trade/serde_json", -] -nostr = ["radroots_events_codec/nostr"] -identity-models = [ - "dep:radroots_identity", - "radroots_identity/profile", - "radroots_identity/std", -] -identity-storage = ["identity-models", "std", "radroots_identity/std"] -signing = ["dep:radroots_nostr", "nostr"] -relay-client = ["signing", "std", "serde_json", "radroots_nostr/client"] -radrootsd-client = ["std", "serde_json", "dep:reqwest"] -signer-adapters = [ - "identity-models", - "signing", - "std", - "dep:radroots_nostr_connect", - "dep:radroots_nostr_signer", -] - -[dependencies] -radroots_events = { workspace = true, default-features = false } -radroots_events_codec = { workspace = true, default-features = false } -radroots_trade = { workspace = true, default-features = false } -radroots_identity = { workspace = true, optional = true, default-features = false } -radroots_nostr = { workspace = true, optional = true, default-features = false } -radroots_nostr_connect = { workspace = true, optional = true } -radroots_nostr_signer = { workspace = true, optional = true, default-features = false } -reqwest = { workspace = true, optional = true, default-features = false, features = [ - "json", - "rustls-tls", -] } -serde = { workspace = true, optional = true, default-features = false, features = [ - "derive", - "alloc", -] } -serde_json = { workspace = true, optional = true, default-features = false, features = [ - "alloc", -] } - -[dev-dependencies] -futures = { workspace = true } -nostr = { workspace = true } -radroots_core = { workspace = true, default-features = false, features = [ - "std", -] } -radroots_replica_db = { workspace = true, default-features = false, features = [ - "native", -] } -radroots_replica_db_schema = { workspace = true } -radroots_replica_sync = { workspace = true, features = ["std"] } -radroots_sql_core = { workspace = true, features = ["native"] } -tempfile = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -tokio-tungstenite = "0.26.2" diff --git a/crates/sdk/README b/crates/sdk/README @@ -1,23 +0,0 @@ -# radroots_sdk - -Curated Rad Roots Rust SDK for the public marketplace event contract. - -This crate provides the recommended Rust entrypoint for building, parsing, and -validating Rad Roots profile, farm, listing, and trade events. It is a thin -facade over the underlying `rr-rs` substrate crates and does not duplicate the -core event or trade implementations. - -The deterministic event contract lives at the crate root: - -- `profile` -- `farm` -- `listing` -- `trade` - -Optional advanced substrate is explicitly feature-scoped: - -- `identity-models`: identity data types without local storage coupling -- `identity-storage`: encrypted identity-file helpers -- `signing`: Nostr builder and local signing adapters -- `relay-client`: relay client and publish adapters -- `signer-adapters`: NIP-46 and signer-session primitives diff --git a/crates/sdk/src/adapters/mod.rs b/crates/sdk/src/adapters/mod.rs @@ -1,8 +0,0 @@ -#[cfg(feature = "radrootsd-client")] -pub mod radrootsd; -#[cfg(feature = "relay-client")] -pub mod relay; -#[cfg(feature = "signer-adapters")] -pub mod signer; -#[cfg(feature = "signing")] -pub mod signing; diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs @@ -1,835 +0,0 @@ -use core::fmt; -use core::time::Duration; - -use crate::config::RadrootsdAuth; -use crate::farm::RadrootsFarm; -use crate::listing; -use crate::listing::RadrootsListing; -use crate::order; -use crate::profile::{RadrootsProfile, RadrootsProfileType}; -use crate::{RadrootsNostrEvent, RadrootsNostrEventPtr}; -use radroots_events::kinds::KIND_LISTING; -use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue}; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; -use serde_json::{Value, json}; - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SdkRadrootsdSignerAuthority { - pub provider_runtime_id: String, - pub account_identity_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider_signer_session_id: Option<String>, -} - -impl fmt::Debug for SdkRadrootsdSignerAuthority { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdSignerAuthority"); - debug.field("provider_runtime_id", &self.provider_runtime_id); - debug.field("account_identity_id", &self.account_identity_id); - debug.field( - "provider_signer_session_id", - &self - .provider_signer_session_id - .as_ref() - .map(|_| "<redacted>"), - ); - debug.finish() - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum SdkRadrootsdSignerSessionMode { - #[serde(alias = "bunker")] - Bunker, - #[serde(alias = "nostrconnect")] - Nostrconnect, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SdkRadrootsdSignerSessionRole { - InboundLocalSigner, - OutboundRemoteSigner, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SdkRadrootsdBridgeDeliveryPolicy { - Any, - Quorum, - All, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SdkRadrootsdBridgeJobStatus { - Accepted, - Published, - Failed, -} - -#[derive(Clone, PartialEq, Eq, Serialize)] -pub struct SdkRadrootsdSignerSessionConnectRequest { - pub url: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub client_secret_key: Option<String>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub signer_authority: Option<SdkRadrootsdSignerAuthority>, -} - -impl SdkRadrootsdSignerSessionConnectRequest { - pub fn bunker(url: impl Into<String>) -> Self { - Self { - url: url.into(), - client_secret_key: None, - signer_authority: None, - } - } - - pub fn nostrconnect(url: impl Into<String>, client_secret_key: impl Into<String>) -> Self { - Self { - url: url.into(), - client_secret_key: Some(client_secret_key.into()), - signer_authority: None, - } - } - - pub fn with_signer_authority(mut self, signer_authority: SdkRadrootsdSignerAuthority) -> Self { - self.signer_authority = Some(signer_authority); - self - } -} - -impl fmt::Debug for SdkRadrootsdSignerSessionConnectRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdSignerSessionConnectRequest"); - debug.field("url", &self.url); - debug.field( - "client_secret_key", - &self.client_secret_key.as_ref().map(|_| "<redacted>"), - ); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[derive(Clone, Serialize)] -pub struct SdkRadrootsdProfilePublishRequest { - pub profile: RadrootsProfile, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub profile_type: Option<RadrootsProfileType>, - pub signer_session_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub signer_authority: Option<SdkRadrootsdSignerAuthority>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub idempotency_key: Option<String>, -} - -impl fmt::Debug for SdkRadrootsdProfilePublishRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdProfilePublishRequest"); - debug.field("profile", &self.profile); - debug.field("profile_type", &self.profile_type); - debug.field("signer_session_id", &"<redacted>"); - debug.field("signer_authority", &self.signer_authority); - debug.field("idempotency_key", &self.idempotency_key); - debug.finish() - } -} - -#[derive(Clone, Serialize)] -pub struct SdkRadrootsdFarmPublishRequest { - pub farm: RadrootsFarm, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub kind: Option<u32>, - pub signer_session_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub signer_authority: Option<SdkRadrootsdSignerAuthority>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub idempotency_key: Option<String>, -} - -impl fmt::Debug for SdkRadrootsdFarmPublishRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdFarmPublishRequest"); - debug.field("farm", &self.farm); - debug.field("kind", &self.kind); - debug.field("signer_session_id", &"<redacted>"); - debug.field("signer_authority", &self.signer_authority); - debug.field("idempotency_key", &self.idempotency_key); - debug.finish() - } -} - -#[derive(Clone, Serialize)] -pub struct SdkRadrootsdListingPublishRequest { - pub listing: RadrootsListing, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub kind: Option<u32>, - pub signer_session_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub signer_authority: Option<SdkRadrootsdSignerAuthority>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub idempotency_key: Option<String>, -} - -impl fmt::Debug for SdkRadrootsdListingPublishRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdListingPublishRequest"); - debug.field("listing", &self.listing); - debug.field("kind", &self.kind); - debug.field("signer_session_id", &"<redacted>"); - debug.field("signer_authority", &self.signer_authority); - debug.field("idempotency_key", &self.idempotency_key); - debug.finish() - } -} - -#[derive(Clone, PartialEq, Eq, Serialize)] -pub(crate) struct SdkRadrootsdOrderRequestPublishRequest { - pub order: order::RadrootsOrderRequest, - pub listing_event: RadrootsNostrEventPtr, - pub signer_session_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub signer_authority: Option<SdkRadrootsdSignerAuthority>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub idempotency_key: Option<String>, -} - -impl fmt::Debug for SdkRadrootsdOrderRequestPublishRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdOrderRequestPublishRequest"); - debug.field("order", &self.order); - debug.field("listing_event", &self.listing_event); - debug.field("signer_session_id", &"<redacted>"); - debug.field("signer_authority", &self.signer_authority); - debug.field("idempotency_key", &self.idempotency_key); - debug.finish() - } -} - -impl SdkRadrootsdListingPublishRequest { - pub fn from_event( - event: &RadrootsNostrEvent, - signer_session_id: impl Into<String>, - signer_authority: Option<SdkRadrootsdSignerAuthority>, - idempotency_key: Option<String>, - ) -> Result<Self, listing::RadrootsListingParseError> { - if event.kind != KIND_LISTING { - return Err(listing::RadrootsListingParseError::InvalidKind(event.kind)); - } - Ok(Self { - listing: listing::parse_event(event)?, - kind: Some(event.kind), - signer_session_id: signer_session_id.into(), - signer_authority, - idempotency_key, - }) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -pub(crate) struct SdkRadrootsdSignerSessionConnectResponse { - pub session_id: String, - pub mode: SdkRadrootsdSignerSessionMode, - pub remote_signer_pubkey: String, - pub client_pubkey: String, - pub relays: Vec<String>, -} - -#[derive(Clone, PartialEq, Eq, Deserialize)] -pub(crate) struct SdkRadrootsdSignerSessionViewResponse { - pub session_id: String, - pub role: SdkRadrootsdSignerSessionRole, - pub client_pubkey: String, - pub signer_pubkey: String, - #[serde(default)] - pub user_pubkey: Option<String>, - pub relays: Vec<String>, - pub permissions: Vec<String>, - #[serde(default)] - pub name: Option<String>, - #[serde(default)] - pub url: Option<String>, - #[serde(default)] - pub image: Option<String>, - pub auth_required: bool, - pub authorized: bool, - #[serde(default)] - pub auth_url: Option<String>, - #[serde(default)] - pub expires_in_secs: Option<u64>, - #[serde(default)] - pub signer_authority: Option<SdkRadrootsdSignerAuthority>, -} - -impl fmt::Debug for SdkRadrootsdSignerSessionViewResponse { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdSignerSessionViewResponse"); - debug.field("session_id", &"<redacted>"); - debug.field("role", &self.role); - debug.field("client_pubkey", &self.client_pubkey); - debug.field("signer_pubkey", &self.signer_pubkey); - debug.field("user_pubkey", &self.user_pubkey); - debug.field("relays", &self.relays); - debug.field("permissions", &self.permissions); - debug.field("name", &self.name); - debug.field("url", &self.url); - debug.field("image", &self.image); - debug.field("auth_required", &self.auth_required); - debug.field("authorized", &self.authorized); - debug.field("auth_url", &self.auth_url); - debug.field("expires_in_secs", &self.expires_in_secs); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -pub(crate) struct SdkRadrootsdSignerSessionAuthorizeResponse { - pub authorized: bool, - pub replayed: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -pub(crate) struct SdkRadrootsdSignerSessionPublicKeyResponse { - pub pubkey: String, -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -pub(crate) struct SdkRadrootsdSignerSessionRequireAuthResponse { - pub required: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -pub(crate) struct SdkRadrootsdSignerSessionCloseResponse { - pub closed: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -pub struct SdkRadrootsdBridgePublishResponse { - pub deduplicated: bool, - pub job: SdkRadrootsdBridgeJob, -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -pub struct SdkRadrootsdBridgeStatusResponse { - pub enabled: bool, - pub ready: bool, - pub auth_mode: String, - pub signer_mode: String, - pub default_signer_mode: String, - pub supported_signer_modes: Vec<String>, - pub available_nip46_signer_sessions: usize, - pub relay_count: usize, - pub delivery_policy: SdkRadrootsdBridgeDeliveryPolicy, - #[serde(default)] - pub delivery_quorum: Option<usize>, - pub publish_max_attempts: usize, - pub publish_initial_backoff_millis: u64, - pub publish_max_backoff_millis: u64, - pub job_status_retention: usize, - pub retained_jobs: usize, - pub retained_idempotency_keys: usize, - pub accepted_jobs: usize, - pub published_jobs: usize, - pub failed_jobs: usize, - pub recovered_failed_jobs: usize, - pub methods: Vec<String>, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct SdkRadrootsdBridgeRelayPublishResult { - pub relay_url: String, - pub acknowledged: bool, - #[serde(default)] - pub detail: Option<String>, -} - -#[derive(Clone, PartialEq, Eq, Deserialize)] -pub struct SdkRadrootsdBridgeJob { - pub job_id: String, - pub command: String, - pub status: String, - pub terminal: bool, - pub recovered_after_restart: bool, - pub signer_mode: String, - #[serde(default)] - pub signer_session_id: Option<String>, - pub event_kind: u32, - #[serde(default)] - pub event_id: Option<String>, - #[serde(default)] - pub event_addr: Option<String>, - pub relay_count: usize, - pub acknowledged_relay_count: usize, -} - -impl fmt::Debug for SdkRadrootsdBridgeJob { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdBridgeJob"); - debug.field("job_id", &self.job_id); - debug.field("command", &self.command); - debug.field("status", &self.status); - debug.field("terminal", &self.terminal); - debug.field("recovered_after_restart", &self.recovered_after_restart); - debug.field("signer_mode", &"<redacted>"); - debug.field( - "signer_session_id", - &self.signer_session_id.as_ref().map(|_| "<redacted>"), - ); - debug.field("event_kind", &self.event_kind); - debug.field("event_id", &self.event_id); - debug.field("event_addr", &self.event_addr); - debug.field("relay_count", &self.relay_count); - debug.field("acknowledged_relay_count", &self.acknowledged_relay_count); - debug.finish() - } -} - -#[derive(Clone, PartialEq, Eq, Deserialize)] -pub struct SdkRadrootsdBridgeJobView { - pub job_id: String, - pub command: String, - #[serde(default)] - pub idempotency_key: Option<String>, - pub status: SdkRadrootsdBridgeJobStatus, - pub terminal: bool, - pub recovered_after_restart: bool, - pub requested_at_unix: u64, - #[serde(default)] - pub completed_at_unix: Option<u64>, - pub signer_mode: String, - #[serde(default)] - pub signer_session_id: Option<String>, - pub event_kind: u32, - #[serde(default)] - pub event_id: Option<String>, - #[serde(default)] - pub event_addr: Option<String>, - pub delivery_policy: SdkRadrootsdBridgeDeliveryPolicy, - #[serde(default)] - pub delivery_quorum: Option<usize>, - pub relay_count: usize, - pub acknowledged_relay_count: usize, - pub required_acknowledged_relay_count: usize, - pub attempt_count: usize, - #[serde(default)] - pub attempt_summaries: Vec<String>, - #[serde(default)] - pub relay_results: Vec<SdkRadrootsdBridgeRelayPublishResult>, - pub relay_outcome_summary: String, -} - -impl fmt::Debug for SdkRadrootsdBridgeJobView { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdBridgeJobView"); - debug.field("job_id", &self.job_id); - debug.field("command", &self.command); - debug.field("idempotency_key", &self.idempotency_key); - debug.field("status", &self.status); - debug.field("terminal", &self.terminal); - debug.field("recovered_after_restart", &self.recovered_after_restart); - debug.field("requested_at_unix", &self.requested_at_unix); - debug.field("completed_at_unix", &self.completed_at_unix); - debug.field("signer_mode", &self.signer_mode.as_str()); - debug.field( - "signer_session_id", - &self.signer_session_id.as_ref().map(|_| "<redacted>"), - ); - debug.field("event_kind", &self.event_kind); - debug.field("event_id", &self.event_id); - debug.field("event_addr", &self.event_addr); - debug.field("delivery_policy", &self.delivery_policy); - debug.field("delivery_quorum", &self.delivery_quorum); - debug.field("relay_count", &self.relay_count); - debug.field("acknowledged_relay_count", &self.acknowledged_relay_count); - debug.field( - "required_acknowledged_relay_count", - &self.required_acknowledged_relay_count, - ); - debug.field("attempt_count", &self.attempt_count); - debug.field("attempt_summaries", &self.attempt_summaries); - debug.field("relay_results", &self.relay_results); - debug.field("relay_outcome_summary", &self.relay_outcome_summary); - debug.finish() - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RadrootsdError { - InvalidAuthHeader(String), - Http(String), - JsonRpc(String), - MalformedResponse(String), -} - -impl core::fmt::Display for RadrootsdError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::InvalidAuthHeader(value) => { - write!(f, "invalid radrootsd bearer token header: {value}") - } - Self::Http(value) => write!(f, "{value}"), - Self::JsonRpc(value) => write!(f, "{value}"), - Self::MalformedResponse(value) => write!(f, "{value}"), - } - } -} - -impl std::error::Error for RadrootsdError {} - -#[derive(Debug, Deserialize)] -struct JsonRpcEnvelope<T> { - result: Option<T>, - error: Option<JsonRpcError>, -} - -#[derive(Debug, Deserialize)] -struct JsonRpcError { - code: i64, - message: String, -} - -#[derive(Debug, Serialize)] -struct SdkRadrootsdSignerSessionParams<'a> { - session_id: &'a str, -} - -#[derive(Debug, Serialize)] -struct SdkRadrootsdSignerSessionRequireAuthParams<'a> { - session_id: &'a str, - auth_url: &'a str, -} - -#[derive(Debug, Serialize)] -struct SdkRadrootsdBridgeJobParams<'a> { - job_id: &'a str, -} - -pub async fn publish_listing( - endpoint: &str, - auth: &RadrootsdAuth, - request: &SdkRadrootsdListingPublishRequest, - timeout: Duration, -) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-listing-publish", - "bridge.listing.publish", - request, - timeout, - ) - .await -} - -pub(crate) async fn publish_profile( - endpoint: &str, - auth: &RadrootsdAuth, - request: &SdkRadrootsdProfilePublishRequest, - timeout: Duration, -) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-profile-publish", - "bridge.profile.publish", - request, - timeout, - ) - .await -} - -pub(crate) async fn publish_farm( - endpoint: &str, - auth: &RadrootsdAuth, - request: &SdkRadrootsdFarmPublishRequest, - timeout: Duration, -) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-farm-publish", - "bridge.farm.publish", - request, - timeout, - ) - .await -} - -pub(crate) async fn publish_order_request( - endpoint: &str, - auth: &RadrootsdAuth, - request: &SdkRadrootsdOrderRequestPublishRequest, - timeout: Duration, -) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-order-request-publish", - "bridge.order.request", - request, - timeout, - ) - .await -} - -pub(crate) async fn connect_signer_session( - endpoint: &str, - auth: &RadrootsdAuth, - request: &SdkRadrootsdSignerSessionConnectRequest, - timeout: Duration, -) -> Result<SdkRadrootsdSignerSessionConnectResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-connect", - "nip46.connect", - request, - timeout, - ) - .await -} - -pub(crate) async fn signer_session_status( - endpoint: &str, - auth: &RadrootsdAuth, - session_id: &str, - timeout: Duration, -) -> Result<SdkRadrootsdSignerSessionViewResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-session-status", - "nip46.session.status", - &SdkRadrootsdSignerSessionParams { session_id }, - timeout, - ) - .await -} - -pub(crate) async fn list_signer_sessions( - endpoint: &str, - auth: &RadrootsdAuth, - timeout: Duration, -) -> Result<Vec<SdkRadrootsdSignerSessionViewResponse>, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-session-list", - "nip46.session.list", - &json!({}), - timeout, - ) - .await -} - -pub(crate) async fn authorize_signer_session( - endpoint: &str, - auth: &RadrootsdAuth, - session_id: &str, - timeout: Duration, -) -> Result<SdkRadrootsdSignerSessionAuthorizeResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-session-authorize", - "nip46.session.authorize", - &SdkRadrootsdSignerSessionParams { session_id }, - timeout, - ) - .await -} - -pub(crate) async fn get_signer_session_public_key( - endpoint: &str, - auth: &RadrootsdAuth, - session_id: &str, - timeout: Duration, -) -> Result<SdkRadrootsdSignerSessionPublicKeyResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-get-public-key", - "nip46.get_public_key", - &SdkRadrootsdSignerSessionParams { session_id }, - timeout, - ) - .await -} - -pub(crate) async fn require_signer_session_auth( - endpoint: &str, - auth: &RadrootsdAuth, - session_id: &str, - auth_url: &str, - timeout: Duration, -) -> Result<SdkRadrootsdSignerSessionRequireAuthResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-session-require-auth", - "nip46.session.require_auth", - &SdkRadrootsdSignerSessionRequireAuthParams { - session_id, - auth_url, - }, - timeout, - ) - .await -} - -pub(crate) async fn close_signer_session( - endpoint: &str, - auth: &RadrootsdAuth, - session_id: &str, - timeout: Duration, -) -> Result<SdkRadrootsdSignerSessionCloseResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-session-close", - "nip46.session.close", - &SdkRadrootsdSignerSessionParams { session_id }, - timeout, - ) - .await -} - -pub(crate) async fn bridge_status( - endpoint: &str, - auth: &RadrootsdAuth, - timeout: Duration, -) -> Result<SdkRadrootsdBridgeStatusResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-bridge-status", - "bridge.status", - &json!({}), - timeout, - ) - .await -} - -pub(crate) async fn bridge_job_status( - endpoint: &str, - auth: &RadrootsdAuth, - job_id: &str, - timeout: Duration, -) -> Result<SdkRadrootsdBridgeJobView, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-bridge-job-status", - "bridge.job.status", - &SdkRadrootsdBridgeJobParams { job_id }, - timeout, - ) - .await -} - -pub(crate) async fn list_bridge_jobs( - endpoint: &str, - auth: &RadrootsdAuth, - timeout: Duration, -) -> Result<Vec<SdkRadrootsdBridgeJobView>, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-bridge-job-list", - "bridge.job.list", - &json!({}), - timeout, - ) - .await -} - -fn auth_headers(auth: &RadrootsdAuth) -> Result<HeaderMap, RadrootsdError> { - let mut headers = HeaderMap::new(); - match auth { - RadrootsdAuth::None => Ok(headers), - RadrootsdAuth::BearerToken(token) => { - let value = HeaderValue::from_str(format!("Bearer {token}").as_str()) - .map_err(|err| RadrootsdError::InvalidAuthHeader(err.to_string()))?; - headers.insert(AUTHORIZATION, value); - Ok(headers) - } - } -} - -pub fn bridge_listing_publish_request_json( - request: &SdkRadrootsdListingPublishRequest, -) -> Result<Value, RadrootsdError> { - serde_json::to_value(request).map_err(|err| { - RadrootsdError::MalformedResponse(format!( - "serialize radrootsd listing publish request: {err}" - )) - }) -} - -async fn jsonrpc_call<P, R>( - endpoint: &str, - auth: &RadrootsdAuth, - request_id: &str, - method: &str, - params: &P, - timeout: Duration, -) -> Result<R, RadrootsdError> -where - P: Serialize + ?Sized, - R: DeserializeOwned, -{ - let client = reqwest::Client::builder() - .timeout(timeout) - .build() - .map_err(|err| RadrootsdError::Http(format!("build radrootsd client: {err}")))?; - let mut request_builder = client - .post(endpoint) - .headers(auth_headers(auth)?) - .json(&json!({ - "jsonrpc": "2.0", - "id": request_id, - "method": method, - "params": params, - })); - - request_builder = request_builder.header(CONTENT_TYPE, "application/json"); - - let response = request_builder - .send() - .await - .map_err(|err| RadrootsdError::Http(format!("send radrootsd {method} request: {err}")))?; - let status = response.status(); - let body = response - .text() - .await - .map_err(|err| RadrootsdError::Http(format!("read radrootsd response body: {err}")))?; - - if !status.is_success() { - return Err(RadrootsdError::Http(format!( - "radrootsd returned http {}: {}", - status.as_u16(), - body - ))); - } - - let envelope: JsonRpcEnvelope<R> = serde_json::from_str(body.as_str()).map_err(|err| { - RadrootsdError::MalformedResponse(format!("decode radrootsd {method} response: {err}")) - })?; - match (envelope.result, envelope.error) { - (Some(result), None) => Ok(result), - (None, Some(error)) => Err(RadrootsdError::JsonRpc(format!( - "radrootsd {method} failed {}: {}", - error.code, error.message - ))), - (Some(_), Some(error)) => Err(RadrootsdError::MalformedResponse(format!( - "radrootsd {method} returned result and error: {} {}", - error.code, error.message - ))), - (None, None) => Err(RadrootsdError::MalformedResponse(format!( - "radrootsd {method} returned neither result nor error" - ))), - } -} diff --git a/crates/sdk/src/adapters/relay.rs b/crates/sdk/src/adapters/relay.rs @@ -1,96 +0,0 @@ -use core::time::Duration; - -use crate::adapters::signing::SignedNostrEvent; -use crate::identity::RadrootsIdentity; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrClientOptions, RadrootsNostrError, RadrootsNostrEventId, - RadrootsNostrOutput, -}; - -pub type RelayClient = RadrootsNostrClient; -pub type RelayClientOptions = RadrootsNostrClientOptions; -pub type RelayError = RadrootsNostrError; -pub type RelayEventId = RadrootsNostrEventId; -pub type RelayOutput<T> = RadrootsNostrOutput<T>; - -pub fn signerless_client() -> RelayClient { - RelayClient::new_signerless() -} - -pub fn signerless_client_with_options( - options: RelayClientOptions, -) -> Result<RelayClient, RelayError> { - RelayClient::new_signerless_with_options(options) -} - -pub fn client_from_identity(identity: &RadrootsIdentity) -> RelayClient { - RelayClient::from_identity(identity) -} - -pub async fn configure_write_relays( - client: &RelayClient, - relay_urls: &[String], - connect_timeout: Duration, -) -> Result<(), RelayError> { - for relay_url in relay_urls { - client.add_write_relay(relay_url).await?; - } - client.connect().await; - client.wait_for_connection(connect_timeout).await; - Ok(()) -} - -pub async fn connected_client_from_identity( - identity: &RadrootsIdentity, - relay_urls: &[String], - connect_timeout: Duration, -) -> Result<RelayClient, RelayError> { - let client = client_from_identity(identity); - configure_write_relays(&client, relay_urls, connect_timeout).await?; - Ok(client) -} - -pub async fn connected_relay_urls(client: &RelayClient) -> Vec<String> { - let mut relay_urls = client - .relays() - .await - .into_values() - .filter(|relay| relay.is_connected()) - .map(|relay| relay.url().to_string()) - .collect::<Vec<_>>(); - relay_urls.sort(); - relay_urls -} - -pub async fn publish_signed_event( - client: &RelayClient, - event: &SignedNostrEvent, -) -> Result<RelayOutput<RelayEventId>, RelayError> { - client.send_event(event).await -} - -#[cfg(test)] -mod tests { - use super::{client_from_identity, signerless_client, signerless_client_with_options}; - use crate::identity::RadrootsIdentity; - use tokio::runtime::Runtime; - - #[test] - fn client_constructors_build_without_runtime_net() { - let identity = RadrootsIdentity::generate(); - let _client = client_from_identity(&identity); - let _signerless = signerless_client(); - let _signerless_with_options = - signerless_client_with_options(super::RelayClientOptions::new()) - .expect("signerless client with options"); - } - - #[test] - fn signerless_client_has_no_signer() { - let runtime = Runtime::new().expect("tokio runtime"); - runtime.block_on(async { - let client = signerless_client(); - assert!(!client.has_signer().await); - }); - } -} diff --git a/crates/sdk/src/adapters/signer.rs b/crates/sdk/src/adapters/signer.rs @@ -1,24 +0,0 @@ -pub use radroots_nostr_connect::prelude::{ - RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR, RADROOTS_NOSTR_CONNECT_RPC_KIND, - RadrootsNostrConnectBunkerUri, RadrootsNostrConnectClientMetadata, - RadrootsNostrConnectClientUri, RadrootsNostrConnectError, RadrootsNostrConnectMethod, - RadrootsNostrConnectPendingConnectionPollOutcome, RadrootsNostrConnectPermission, - RadrootsNostrConnectPermissions, RadrootsNostrConnectRemoteSessionCapability, - RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, - RadrootsNostrConnectResponseEnvelope, RadrootsNostrConnectUri, -}; -pub use radroots_nostr_signer::prelude::{ - RadrootsNostrEmbeddedSignerBackend, RadrootsNostrLocalSignerAvailability, - RadrootsNostrLocalSignerCapability, RadrootsNostrRemoteSessionSignerCapability, - RadrootsNostrSignerBackend, RadrootsNostrSignerBackendCapabilities, - RadrootsNostrSignerCapability, RadrootsNostrSignerConnectEvaluation, - RadrootsNostrSignerConnectProposal, RadrootsNostrSignerError, - RadrootsNostrSignerHandledRequest, RadrootsNostrSignerHandledRequestOutcome, - RadrootsNostrSignerManager, RadrootsNostrSignerNip46Codec, - RadrootsNostrSignerNip46ConnectDecision, RadrootsNostrSignerNip46Handler, - RadrootsNostrSignerNip46Policy, RadrootsNostrSignerNip46Signer, - RadrootsNostrSignerPublishTransition, RadrootsNostrSignerRequestAction, - RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerRequestResponseHint, - RadrootsNostrSignerSessionLookup, connect_response_outcome, handled_request_for_action, - response_from_hint, -}; diff --git a/crates/sdk/src/adapters/signing.rs b/crates/sdk/src/adapters/signing.rs @@ -1,63 +0,0 @@ -use crate::WireEventParts; -use crate::identity::RadrootsIdentity; -use radroots_nostr::prelude::{RadrootsNostrError, radroots_nostr_build_event}; - -pub type SignedNostrEvent = radroots_nostr::prelude::RadrootsNostrEvent; -pub type NostrEventBuilder = radroots_nostr::prelude::RadrootsNostrEventBuilder; -pub type SigningError = RadrootsNostrError; - -pub fn event_builder_from_parts(parts: WireEventParts) -> Result<NostrEventBuilder, SigningError> { - radroots_nostr_build_event(parts.kind, parts.content, parts.tags) -} - -pub fn sign_parts_with_identity( - identity: &RadrootsIdentity, - parts: WireEventParts, -) -> Result<SignedNostrEvent, SigningError> { - let builder = event_builder_from_parts(parts)?; - sign_builder_with_identity(identity, builder) -} - -pub fn sign_builder_with_identity( - identity: &RadrootsIdentity, - builder: NostrEventBuilder, -) -> Result<SignedNostrEvent, SigningError> { - builder.sign_with_keys(identity.keys()).map_err(Into::into) -} - -#[cfg(test)] -mod tests { - use super::{event_builder_from_parts, sign_parts_with_identity}; - use crate::{WireEventParts, identity::RadrootsIdentity}; - - #[test] - fn event_builder_from_parts_preserves_kind_and_content() { - let builder = event_builder_from_parts(WireEventParts { - kind: 30402, - content: "hello".into(), - tags: vec![vec!["x".into(), "y".into()]], - }) - .expect("builder"); - let identity = RadrootsIdentity::generate(); - let event = builder.build(identity.keys().public_key()); - - assert_eq!(u16::from(event.kind), 30402); - assert_eq!(event.content, "hello"); - } - - #[test] - fn sign_parts_with_identity_signs_event() { - let identity = RadrootsIdentity::generate(); - let event = sign_parts_with_identity( - &identity, - WireEventParts { - kind: 30402, - content: "hello".into(), - tags: vec![], - }, - ) - .expect("signed event"); - - assert_eq!(event.pubkey.to_hex(), identity.public_key_hex()); - } -} diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -1,2711 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; -use core::fmt; -#[cfg(feature = "std")] -use std::{string::String, vec::Vec}; - -#[cfg(feature = "radrootsd-client")] -use crate::adapters::radrootsd; -#[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -use crate::adapters::{relay, signing}; -use crate::config::SignerConfig; -use crate::config::{RadrootsSdkConfig, SdkConfigError, SdkTransportMode}; -#[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -use crate::identity::RadrootsIdentity; -use crate::{ - NostrTags, RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsProfile, RadrootsProfileType, - TradeListingValidateResult, WireEventParts, farm, listing, order, profile, -}; -#[cfg(any( - feature = "radrootsd-client", - all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ) -))] -use core::time::Duration; -use radroots_events::ids::RadrootsEventId; -#[cfg(feature = "radrootsd-client")] -use radroots_events::kinds::{KIND_FARM, KIND_LISTING}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SdkPublishReceipt { - pub transport: SdkTransportMode, - pub event_kind: Option<u32>, - pub event_id: Option<String>, - pub transport_receipt: SdkTransportReceipt, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SdkTransportReceipt { - RelayDirect(SdkRelayPublishReceipt), - Radrootsd(SdkRadrootsdPublishReceipt), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SdkRelayPublishReceipt { - pub event: RadrootsNostrEvent, - pub event_id: String, - pub event_kind: u32, - pub created_at: u32, - pub signature: String, - pub target_relays: Vec<String>, - pub connected_relays: Vec<String>, - pub acknowledged_relays: Vec<String>, - pub failed_relays: Vec<SdkRelayFailure>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SdkRelayFailure { - pub relay_url: String, - pub error: String, -} - -#[derive(Clone, PartialEq, Eq, Default)] -pub struct SdkRadrootsdPublishReceipt { - pub accepted: bool, - pub deduplicated: bool, - pub job_id: Option<String>, - pub status: Option<String>, - pub signer_mode: Option<String>, - pub signer_session_id: Option<String>, - pub event_addr: Option<String>, - pub relay_count: Option<usize>, - pub acknowledged_relay_count: Option<usize>, -} - -impl fmt::Debug for SdkRadrootsdPublishReceipt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdPublishReceipt"); - debug.field("accepted", &self.accepted); - debug.field("deduplicated", &self.deduplicated); - debug.field("job_id", &self.job_id); - debug.field("status", &self.status); - debug.field( - "signer_mode", - &self.signer_mode.as_ref().map(|_| "<redacted>"), - ); - debug.field( - "signer_session_id", - &self.signer_session_id.as_ref().map(|_| "<redacted>"), - ); - debug.field("event_addr", &self.event_addr); - debug.field("relay_count", &self.relay_count); - debug.field("acknowledged_relay_count", &self.acknowledged_relay_count); - debug.finish() - } -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdPublishReceipt { - pub fn job(&self) -> Option<SdkRadrootsdBridgeJobRef> { - self.job_id - .as_ref() - .map(|job_id| SdkRadrootsdBridgeJobRef::new(job_id.clone())) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SdkPublishError { - Config(SdkConfigError), - Encode(String), - UnsupportedTransport { - transport: SdkTransportMode, - operation: &'static str, - }, - UnsupportedSignerMode { - transport: SdkTransportMode, - signer: SignerConfig, - required: SignerConfig, - operation: &'static str, - }, - Relay(String), - RelaySetup { - transport: SdkTransportMode, - operation: &'static str, - target_relays: Vec<String>, - error: String, - }, - RelayNotAcknowledged { - transport: SdkTransportMode, - failed_relays: Vec<SdkRelayFailure>, - }, - Radrootsd(String), -} - -impl From<SdkConfigError> for SdkPublishError { - fn from(value: SdkConfigError) -> Self { - Self::Config(value) - } -} - -impl core::fmt::Display for SdkPublishError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Config(err) => write!(f, "{err}"), - Self::Encode(message) => write!(f, "{message}"), - Self::UnsupportedTransport { - transport, - operation, - } => { - write!( - f, - "{operation} requires a different sdk transport mode than {transport:?}" - ) - } - Self::UnsupportedSignerMode { - transport, - signer, - required, - operation, - } => write!( - f, - "{operation} requires signer mode `{required}` for {transport:?} transport, got `{signer}`" - ), - Self::Relay(message) => write!(f, "{message}"), - Self::RelaySetup { - transport, - operation, - target_relays, - error, - } => { - if target_relays.is_empty() { - write!( - f, - "{operation} failed to prepare {transport:?} relay publish: {error}" - ) - } else { - let relays = target_relays.join(", "); - write!( - f, - "{operation} failed to prepare {transport:?} relay publish for {relays}: {error}" - ) - } - } - Self::RelayNotAcknowledged { - transport, - failed_relays, - } => { - if failed_relays.is_empty() { - write!(f, "{transport:?} publish was not acknowledged by any relay") - } else { - let summary = failed_relays - .iter() - .map(|failure| format!("{}: {}", failure.relay_url, failure.error)) - .collect::<Vec<_>>() - .join(", "); - write!( - f, - "{transport:?} publish was not acknowledged by any relay: {summary}" - ) - } - } - Self::Radrootsd(message) => write!(f, "{message}"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for SdkPublishError {} - -#[cfg(feature = "radrootsd-client")] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SdkRadrootsdSessionError { - Config(SdkConfigError), - UnsupportedTransport { - transport: SdkTransportMode, - operation: &'static str, - }, - Radrootsd(String), -} - -#[cfg(feature = "radrootsd-client")] -impl From<SdkConfigError> for SdkRadrootsdSessionError { - fn from(value: SdkConfigError) -> Self { - Self::Config(value) - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Display for SdkRadrootsdSessionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Config(err) => write!(f, "{err}"), - Self::UnsupportedTransport { - transport, - operation, - } => { - write!( - f, - "{operation} requires a different sdk transport mode than {transport:?}" - ) - } - Self::Radrootsd(message) => write!(f, "{message}"), - } - } -} - -#[cfg(all(feature = "radrootsd-client", feature = "std"))] -impl std::error::Error for SdkRadrootsdSessionError {} - -#[cfg(feature = "radrootsd-client")] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SdkRadrootsdBridgeError { - Config(SdkConfigError), - UnsupportedTransport { - transport: SdkTransportMode, - operation: &'static str, - }, - Radrootsd(String), -} - -#[cfg(feature = "radrootsd-client")] -impl From<SdkConfigError> for SdkRadrootsdBridgeError { - fn from(value: SdkConfigError) -> Self { - Self::Config(value) - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Display for SdkRadrootsdBridgeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Config(err) => write!(f, "{err}"), - Self::UnsupportedTransport { - transport, - operation, - } => write!( - f, - "{operation} requires a different sdk transport mode than {transport:?}" - ), - Self::Radrootsd(message) => write!(f, "{message}"), - } - } -} - -#[cfg(all(feature = "radrootsd-client", feature = "std"))] -impl std::error::Error for SdkRadrootsdBridgeError {} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionRef { - session_id: String, -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdSignerSessionRef { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SdkRadrootsdSignerSessionRef") - .field("session_id", &"<redacted>") - .finish() - } -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdSignerSessionRef { - pub fn from_session_id(session_id: impl Into<String>) -> Self { - Self { - session_id: session_id.into(), - } - } - - pub fn session_id(&self) -> &str { - self.session_id.as_str() - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdBridgeJobRef { - job_id: String, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdBridgeJobRef { - pub fn new(job_id: impl Into<String>) -> Self { - Self { - job_id: job_id.into(), - } - } - - pub fn job_id(&self) -> &str { - self.job_id.as_str() - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdBridgeStatus { - pub enabled: bool, - pub ready: bool, - pub auth_mode: String, - pub signer_mode: String, - pub default_signer_mode: String, - pub supported_signer_modes: Vec<String>, - pub available_nip46_signer_sessions: usize, - pub relay_count: usize, - pub delivery_policy: radrootsd::SdkRadrootsdBridgeDeliveryPolicy, - pub delivery_quorum: Option<usize>, - pub publish_max_attempts: usize, - pub publish_initial_backoff_millis: u64, - pub publish_max_backoff_millis: u64, - pub job_status_retention: usize, - pub retained_jobs: usize, - pub retained_idempotency_keys: usize, - pub accepted_jobs: usize, - pub published_jobs: usize, - pub failed_jobs: usize, - pub recovered_failed_jobs: usize, - pub methods: Vec<String>, -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdBridgeJobView { - job: SdkRadrootsdBridgeJobRef, - pub command: String, - pub idempotency_key: Option<String>, - pub status: radrootsd::SdkRadrootsdBridgeJobStatus, - pub terminal: bool, - pub recovered_after_restart: bool, - pub requested_at_unix: u64, - pub completed_at_unix: Option<u64>, - pub signer_mode: String, - pub signer_session_id: Option<String>, - pub event_kind: u32, - pub event_id: Option<String>, - pub event_addr: Option<String>, - pub delivery_policy: radrootsd::SdkRadrootsdBridgeDeliveryPolicy, - pub delivery_quorum: Option<usize>, - pub relay_count: usize, - pub acknowledged_relay_count: usize, - pub required_acknowledged_relay_count: usize, - pub attempt_count: usize, - pub attempt_summaries: Vec<String>, - pub relay_results: Vec<radrootsd::SdkRadrootsdBridgeRelayPublishResult>, - pub relay_outcome_summary: String, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdBridgeJobView { - pub fn job(&self) -> &SdkRadrootsdBridgeJobRef { - &self.job - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdBridgeStatusResponse> for SdkRadrootsdBridgeStatus { - fn from(value: radrootsd::SdkRadrootsdBridgeStatusResponse) -> Self { - Self { - enabled: value.enabled, - ready: value.ready, - auth_mode: value.auth_mode, - signer_mode: value.signer_mode, - default_signer_mode: value.default_signer_mode, - supported_signer_modes: value.supported_signer_modes, - available_nip46_signer_sessions: value.available_nip46_signer_sessions, - relay_count: value.relay_count, - delivery_policy: value.delivery_policy, - delivery_quorum: value.delivery_quorum, - publish_max_attempts: value.publish_max_attempts, - publish_initial_backoff_millis: value.publish_initial_backoff_millis, - publish_max_backoff_millis: value.publish_max_backoff_millis, - job_status_retention: value.job_status_retention, - retained_jobs: value.retained_jobs, - retained_idempotency_keys: value.retained_idempotency_keys, - accepted_jobs: value.accepted_jobs, - published_jobs: value.published_jobs, - failed_jobs: value.failed_jobs, - recovered_failed_jobs: value.recovered_failed_jobs, - methods: value.methods, - } - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdBridgeJobView> for SdkRadrootsdBridgeJobView { - fn from(value: radrootsd::SdkRadrootsdBridgeJobView) -> Self { - Self { - job: SdkRadrootsdBridgeJobRef::new(value.job_id), - command: value.command, - idempotency_key: value.idempotency_key, - status: value.status, - terminal: value.terminal, - recovered_after_restart: value.recovered_after_restart, - requested_at_unix: value.requested_at_unix, - completed_at_unix: value.completed_at_unix, - signer_mode: value.signer_mode, - signer_session_id: value.signer_session_id, - event_kind: value.event_kind, - event_id: value.event_id, - event_addr: value.event_addr, - delivery_policy: value.delivery_policy, - delivery_quorum: value.delivery_quorum, - relay_count: value.relay_count, - acknowledged_relay_count: value.acknowledged_relay_count, - required_acknowledged_relay_count: value.required_acknowledged_relay_count, - attempt_count: value.attempt_count, - attempt_summaries: value.attempt_summaries, - relay_results: value.relay_results, - relay_outcome_summary: value.relay_outcome_summary, - } - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionHandle { - session: SdkRadrootsdSignerSessionRef, - mode: radrootsd::SdkRadrootsdSignerSessionMode, - remote_signer_pubkey: String, - client_pubkey: String, - relays: Vec<String>, -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionView { - session: SdkRadrootsdSignerSessionRef, - pub role: radrootsd::SdkRadrootsdSignerSessionRole, - pub client_pubkey: String, - pub signer_pubkey: String, - pub user_pubkey: Option<String>, - pub relays: Vec<String>, - pub permissions: Vec<String>, - pub name: Option<String>, - pub url: Option<String>, - pub image: Option<String>, - pub auth_required: bool, - pub authorized: bool, - pub auth_url: Option<String>, - pub expires_in_secs: Option<u64>, - pub signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdSignerSessionView { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdSignerSessionView"); - debug.field("session", &self.session); - debug.field("role", &self.role); - debug.field("client_pubkey", &self.client_pubkey); - debug.field("signer_pubkey", &self.signer_pubkey); - debug.field("user_pubkey", &self.user_pubkey); - debug.field("relays", &self.relays); - debug.field("permissions", &self.permissions); - debug.field("name", &self.name); - debug.field("url", &self.url); - debug.field("image", &self.image); - debug.field("auth_required", &self.auth_required); - debug.field("authorized", &self.authorized); - debug.field("auth_url", &self.auth_url); - debug.field("expires_in_secs", &self.expires_in_secs); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdSignerSessionView { - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionAuthorizeResult { - pub authorized: bool, - pub replayed: bool, -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionPublicKeyResult { - pub pubkey: String, -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionRequireAuthResult { - pub required: bool, -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionCloseResult { - pub closed: bool, -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionViewResponse> for SdkRadrootsdSignerSessionView { - fn from(value: radrootsd::SdkRadrootsdSignerSessionViewResponse) -> Self { - Self { - session: SdkRadrootsdSignerSessionRef { - session_id: value.session_id, - }, - role: value.role, - client_pubkey: value.client_pubkey, - signer_pubkey: value.signer_pubkey, - user_pubkey: value.user_pubkey, - relays: value.relays, - permissions: value.permissions, - name: value.name, - url: value.url, - image: value.image, - auth_required: value.auth_required, - authorized: value.authorized, - auth_url: value.auth_url, - expires_in_secs: value.expires_in_secs, - signer_authority: value.signer_authority, - } - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionAuthorizeResponse> - for SdkRadrootsdSignerSessionAuthorizeResult -{ - fn from(value: radrootsd::SdkRadrootsdSignerSessionAuthorizeResponse) -> Self { - Self { - authorized: value.authorized, - replayed: value.replayed, - } - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionPublicKeyResponse> - for SdkRadrootsdSignerSessionPublicKeyResult -{ - fn from(value: radrootsd::SdkRadrootsdSignerSessionPublicKeyResponse) -> Self { - Self { - pubkey: value.pubkey, - } - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionRequireAuthResponse> - for SdkRadrootsdSignerSessionRequireAuthResult -{ - fn from(value: radrootsd::SdkRadrootsdSignerSessionRequireAuthResponse) -> Self { - Self { - required: value.required, - } - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionCloseResponse> - for SdkRadrootsdSignerSessionCloseResult -{ - fn from(value: radrootsd::SdkRadrootsdSignerSessionCloseResponse) -> Self { - Self { - closed: value.closed, - } - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdSignerSessionHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdSignerSessionHandle"); - debug.field("session", &self.session); - debug.field("mode", &self.mode); - debug.field("remote_signer_pubkey", &self.remote_signer_pubkey); - debug.field("client_pubkey", &self.client_pubkey); - debug.field("relays", &self.relays); - debug.finish() - } -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdSignerSessionHandle { - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } - - pub fn mode(&self) -> radrootsd::SdkRadrootsdSignerSessionMode { - self.mode - } - - pub fn remote_signer_pubkey(&self) -> &str { - self.remote_signer_pubkey.as_str() - } - - pub fn client_pubkey(&self) -> &str { - self.client_pubkey.as_str() - } - - pub fn relays(&self) -> &[String] { - self.relays.as_slice() - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionConnectResponse> for SdkRadrootsdSignerSessionHandle { - fn from(value: radrootsd::SdkRadrootsdSignerSessionConnectResponse) -> Self { - Self { - session: SdkRadrootsdSignerSessionRef { - session_id: value.session_id, - }, - mode: value.mode, - remote_signer_pubkey: value.remote_signer_pubkey, - client_pubkey: value.client_pubkey, - relays: value.relays, - } - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdProfilePublishOptions { - session: SdkRadrootsdSignerSessionRef, - idempotency_key: Option<String>, - signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdProfilePublishOptions { - pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self { - Self { - session: session.session().clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn from_signer_session_ref(session: &SdkRadrootsdSignerSessionRef) -> Self { - Self { - session: session.clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self { - self.idempotency_key = Some(idempotency_key.into()); - self - } - - pub fn with_signer_authority( - mut self, - signer_authority: radrootsd::SdkRadrootsdSignerAuthority, - ) -> Self { - self.signer_authority = Some(signer_authority); - self - } - - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } - - pub fn idempotency_key(&self) -> Option<&str> { - self.idempotency_key.as_deref() - } - - pub fn signer_authority(&self) -> Option<&radrootsd::SdkRadrootsdSignerAuthority> { - self.signer_authority.as_ref() - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdProfilePublishOptions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdProfilePublishOptions"); - debug.field("session", &self.session); - debug.field("idempotency_key", &self.idempotency_key); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdFarmPublishOptions { - session: SdkRadrootsdSignerSessionRef, - idempotency_key: Option<String>, - signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdFarmPublishOptions { - pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self { - Self { - session: session.session().clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn from_signer_session_ref(session: &SdkRadrootsdSignerSessionRef) -> Self { - Self { - session: session.clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self { - self.idempotency_key = Some(idempotency_key.into()); - self - } - - pub fn with_signer_authority( - mut self, - signer_authority: radrootsd::SdkRadrootsdSignerAuthority, - ) -> Self { - self.signer_authority = Some(signer_authority); - self - } - - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } - - pub fn idempotency_key(&self) -> Option<&str> { - self.idempotency_key.as_deref() - } - - pub fn signer_authority(&self) -> Option<&radrootsd::SdkRadrootsdSignerAuthority> { - self.signer_authority.as_ref() - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdFarmPublishOptions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdFarmPublishOptions"); - debug.field("session", &self.session); - debug.field("idempotency_key", &self.idempotency_key); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdListingPublishOptions { - session: SdkRadrootsdSignerSessionRef, - idempotency_key: Option<String>, - signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdListingPublishOptions { - pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self { - Self { - session: session.session().clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn from_signer_session_ref(session: &SdkRadrootsdSignerSessionRef) -> Self { - Self { - session: session.clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self { - self.idempotency_key = Some(idempotency_key.into()); - self - } - - pub fn with_signer_authority( - mut self, - signer_authority: radrootsd::SdkRadrootsdSignerAuthority, - ) -> Self { - self.signer_authority = Some(signer_authority); - self - } - - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } - - pub fn idempotency_key(&self) -> Option<&str> { - self.idempotency_key.as_deref() - } - - pub fn signer_authority(&self) -> Option<&radrootsd::SdkRadrootsdSignerAuthority> { - self.signer_authority.as_ref() - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdListingPublishOptions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdListingPublishOptions"); - debug.field("session", &self.session); - debug.field("idempotency_key", &self.idempotency_key); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdOrderRequestPublishOptions { - session: SdkRadrootsdSignerSessionRef, - idempotency_key: Option<String>, - signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdOrderRequestPublishOptions { - pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self { - Self { - session: session.session().clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn from_signer_session_ref(session: &SdkRadrootsdSignerSessionRef) -> Self { - Self { - session: session.clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self { - self.idempotency_key = Some(idempotency_key.into()); - self - } - - pub fn with_signer_authority( - mut self, - signer_authority: radrootsd::SdkRadrootsdSignerAuthority, - ) -> Self { - self.signer_authority = Some(signer_authority); - self - } - - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } - - pub fn idempotency_key(&self) -> Option<&str> { - self.idempotency_key.as_deref() - } - - pub fn signer_authority(&self) -> Option<&radrootsd::SdkRadrootsdSignerAuthority> { - self.signer_authority.as_ref() - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdOrderRequestPublishOptions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdOrderRequestPublishOptions"); - debug.field("session", &self.session); - debug.field("idempotency_key", &self.idempotency_key); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsSdkClient { - config: RadrootsSdkConfig, - resolved_transport_target: SdkResolvedTransportTarget, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SdkResolvedTransportTarget { - RelayDirect { relay_urls: Vec<String> }, - Radrootsd { endpoint: String }, -} - -impl RadrootsSdkClient { - pub fn from_config(config: RadrootsSdkConfig) -> Result<Self, SdkConfigError> { - let resolved_transport_target = match config.transport { - SdkTransportMode::RelayDirect => SdkResolvedTransportTarget::RelayDirect { - relay_urls: config.resolved_relay_urls()?, - }, - SdkTransportMode::Radrootsd => SdkResolvedTransportTarget::Radrootsd { - endpoint: config.resolved_radrootsd_endpoint()?, - }, - }; - Ok(Self { - config, - resolved_transport_target, - }) - } - - pub fn config(&self) -> &RadrootsSdkConfig { - &self.config - } - - pub fn transport(&self) -> SdkTransportMode { - self.config.transport - } - - pub fn signer(&self) -> SignerConfig { - self.config.signer - } - - pub fn resolved_transport_target(&self) -> &SdkResolvedTransportTarget { - &self.resolved_transport_target - } - - pub fn profile(&self) -> ProfileClient<'_> { - ProfileClient { client: self } - } - - pub fn farm(&self) -> FarmClient<'_> { - FarmClient { client: self } - } - - pub fn listing(&self) -> ListingClient<'_> { - ListingClient { client: self } - } - - pub fn order(&self) -> TradeClient<'_> { - TradeClient { client: self } - } - - #[cfg(feature = "radrootsd-client")] - pub fn radrootsd(&self) -> RadrootsdClient<'_> { - RadrootsdClient { client: self } - } - - #[cfg(any( - feature = "radrootsd-client", - all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ) - ))] - fn require_signer_mode( - &self, - required: SignerConfig, - operation: &'static str, - ) -> Result<(), SdkPublishError> { - let signer = self.signer(); - if signer == required { - return Ok(()); - } - Err(SdkPublishError::UnsupportedSignerMode { - transport: self.transport(), - signer, - required, - operation, - }) - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - async fn publish_parts_via_relay_with_identity( - &self, - identity: &RadrootsIdentity, - parts: WireEventParts, - operation: &'static str, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - if self.transport() != SdkTransportMode::RelayDirect { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation, - }); - } - self.require_signer_mode(SignerConfig::LocalIdentity, operation)?; - - let relay_urls = match &self.resolved_transport_target { - SdkResolvedTransportTarget::RelayDirect { relay_urls } => relay_urls.clone(), - SdkResolvedTransportTarget::Radrootsd { .. } => { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation, - }); - } - }; - let client = relay::connected_client_from_identity( - identity, - &relay_urls, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkPublishError::RelaySetup { - transport: SdkTransportMode::RelayDirect, - operation, - target_relays: relay_urls.clone(), - error: err.to_string(), - })?; - let connected_relays = relay::connected_relay_urls(&client).await; - if connected_relays.is_empty() { - return Err(SdkPublishError::RelaySetup { - transport: SdkTransportMode::RelayDirect, - operation, - target_relays: relay_urls, - error: "no relay connection was established".to_owned(), - }); - } - let signed_event = signing::sign_parts_with_identity(identity, parts) - .map_err(|err| SdkPublishError::Relay(err.to_string()))?; - let output = relay::publish_signed_event(&client, &signed_event) - .await - .map_err(|err| SdkPublishError::RelaySetup { - transport: SdkTransportMode::RelayDirect, - operation, - target_relays: relay_urls.clone(), - error: err.to_string(), - })?; - sdk_publish_receipt_from_relay_output(signed_event, relay_urls, connected_relays, output) - } - - #[cfg(feature = "radrootsd-client")] - async fn publish_listing_via_radrootsd( - &self, - request: &radrootsd::SdkRadrootsdListingPublishRequest, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "listing.publish_via_radrootsd", - }); - } - self.require_signer_mode(SignerConfig::Nip46, "listing.publish_via_radrootsd")?; - - let endpoint = match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(), - SdkResolvedTransportTarget::RelayDirect { .. } => { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "listing.publish_via_radrootsd", - }); - } - }; - let response = radrootsd::publish_listing( - endpoint, - &self.config.radrootsd.auth, - request, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?; - Ok(sdk_publish_receipt_from_radrootsd_bridge_response(response)) - } - - #[cfg(feature = "radrootsd-client")] - async fn publish_profile_via_radrootsd( - &self, - request: &radrootsd::SdkRadrootsdProfilePublishRequest, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "profile.publish_via_radrootsd", - }); - } - self.require_signer_mode(SignerConfig::Nip46, "profile.publish_via_radrootsd")?; - - let endpoint = match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(), - SdkResolvedTransportTarget::RelayDirect { .. } => { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "profile.publish_via_radrootsd", - }); - } - }; - let response = radrootsd::publish_profile( - endpoint, - &self.config.radrootsd.auth, - request, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?; - Ok(sdk_publish_receipt_from_radrootsd_bridge_response(response)) - } - - #[cfg(feature = "radrootsd-client")] - async fn publish_farm_via_radrootsd( - &self, - request: &radrootsd::SdkRadrootsdFarmPublishRequest, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "farm.publish_via_radrootsd", - }); - } - self.require_signer_mode(SignerConfig::Nip46, "farm.publish_via_radrootsd")?; - - let endpoint = match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(), - SdkResolvedTransportTarget::RelayDirect { .. } => { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "farm.publish_via_radrootsd", - }); - } - }; - let response = radrootsd::publish_farm( - endpoint, - &self.config.radrootsd.auth, - request, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?; - Ok(sdk_publish_receipt_from_radrootsd_bridge_response(response)) - } - - #[cfg(feature = "radrootsd-client")] - async fn publish_order_request_via_radrootsd( - &self, - request: &radrootsd::SdkRadrootsdOrderRequestPublishRequest, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "order.publish_order_request_via_radrootsd", - }); - } - self.require_signer_mode( - SignerConfig::Nip46, - "order.publish_order_request_via_radrootsd", - )?; - - let endpoint = match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(), - SdkResolvedTransportTarget::RelayDirect { .. } => { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "order.publish_order_request_via_radrootsd", - }); - } - }; - let response = radrootsd::publish_order_request( - endpoint, - &self.config.radrootsd.auth, - request, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?; - Ok(sdk_publish_receipt_from_radrootsd_bridge_response(response)) - } - - #[cfg(feature = "radrootsd-client")] - async fn connect_radrootsd_signer_session( - &self, - request: &radrootsd::SdkRadrootsdSignerSessionConnectRequest, - ) -> Result<SdkRadrootsdSignerSessionHandle, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.connect", - }); - } - - let endpoint = self.require_radrootsd_endpoint("radrootsd.signer_sessions.connect")?; - let response = radrootsd::connect_signer_session( - endpoint, - &self.config.radrootsd.auth, - request, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - fn require_radrootsd_endpoint( - &self, - operation: &'static str, - ) -> Result<&str, SdkRadrootsdSessionError> { - match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => Ok(endpoint.as_str()), - SdkResolvedTransportTarget::RelayDirect { .. } => { - Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation, - }) - } - } - } - - #[cfg(feature = "radrootsd-client")] - async fn radrootsd_signer_session_status( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionView, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.status", - }); - } - - let response = radrootsd::signer_session_status( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.status")?, - &self.config.radrootsd.auth, - session.session_id(), - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn radrootsd_list_signer_sessions( - &self, - ) -> Result<Vec<SdkRadrootsdSignerSessionView>, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.list", - }); - } - - let response = radrootsd::list_signer_sessions( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.list")?, - &self.config.radrootsd.auth, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into_iter().map(Into::into).collect()) - } - - #[cfg(feature = "radrootsd-client")] - async fn authorize_radrootsd_signer_session( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionAuthorizeResult, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.authorize", - }); - } - - let response = radrootsd::authorize_signer_session( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.authorize")?, - &self.config.radrootsd.auth, - session.session_id(), - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn get_radrootsd_signer_session_public_key( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionPublicKeyResult, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.get_public_key", - }); - } - - let response = radrootsd::get_signer_session_public_key( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.get_public_key")?, - &self.config.radrootsd.auth, - session.session_id(), - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn require_radrootsd_signer_session_auth( - &self, - session: &SdkRadrootsdSignerSessionRef, - auth_url: &str, - ) -> Result<SdkRadrootsdSignerSessionRequireAuthResult, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.require_auth", - }); - } - - let response = radrootsd::require_signer_session_auth( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.require_auth")?, - &self.config.radrootsd.auth, - session.session_id(), - auth_url, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn close_radrootsd_signer_session( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.close", - }); - } - - let response = radrootsd::close_signer_session( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.close")?, - &self.config.radrootsd.auth, - session.session_id(), - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - fn require_radrootsd_bridge_endpoint( - &self, - operation: &'static str, - ) -> Result<&str, SdkRadrootsdBridgeError> { - match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => Ok(endpoint.as_str()), - SdkResolvedTransportTarget::RelayDirect { .. } => { - Err(SdkRadrootsdBridgeError::UnsupportedTransport { - transport: self.transport(), - operation, - }) - } - } - } - - #[cfg(feature = "radrootsd-client")] - async fn radrootsd_bridge_status( - &self, - ) -> Result<SdkRadrootsdBridgeStatus, SdkRadrootsdBridgeError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdBridgeError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.bridge.status", - }); - } - - let response = radrootsd::bridge_status( - self.require_radrootsd_bridge_endpoint("radrootsd.bridge.status")?, - &self.config.radrootsd.auth, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdBridgeError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn radrootsd_bridge_job_status( - &self, - job: &SdkRadrootsdBridgeJobRef, - ) -> Result<SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdBridgeError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.bridge.job", - }); - } - - let response = radrootsd::bridge_job_status( - self.require_radrootsd_bridge_endpoint("radrootsd.bridge.job")?, - &self.config.radrootsd.auth, - job.job_id(), - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdBridgeError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn radrootsd_bridge_jobs( - &self, - ) -> Result<Vec<SdkRadrootsdBridgeJobView>, SdkRadrootsdBridgeError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdBridgeError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.bridge.jobs", - }); - } - - let response = radrootsd::list_bridge_jobs( - self.require_radrootsd_bridge_endpoint("radrootsd.bridge.jobs")?, - &self.config.radrootsd.auth, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdBridgeError::Radrootsd(err.to_string()))?; - Ok(response.into_iter().map(Into::into).collect()) - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Debug, Clone, Copy)] -pub struct RadrootsdClient<'a> { - client: &'a RadrootsSdkClient, -} - -#[cfg(feature = "radrootsd-client")] -impl<'a> RadrootsdClient<'a> { - pub fn sdk(&self) -> &'a RadrootsSdkClient { - self.client - } - - pub fn transport(&self) -> SdkTransportMode { - self.client.transport() - } - - pub fn signer(&self) -> SignerConfig { - self.client.signer() - } - - pub fn signer_sessions(&self) -> RadrootsdSignerSessionClient<'a> { - RadrootsdSignerSessionClient { - client: self.client, - } - } - - pub fn bridge(&self) -> RadrootsdBridgeClient<'a> { - RadrootsdBridgeClient { - client: self.client, - } - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Debug, Clone, Copy)] -pub struct RadrootsdSignerSessionClient<'a> { - client: &'a RadrootsSdkClient, -} - -#[cfg(feature = "radrootsd-client")] -impl<'a> RadrootsdSignerSessionClient<'a> { - pub fn sdk(&self) -> &'a RadrootsSdkClient { - self.client - } - - pub fn transport(&self) -> SdkTransportMode { - self.client.transport() - } - - pub fn signer(&self) -> SignerConfig { - self.client.signer() - } - - pub async fn connect( - &self, - request: &radrootsd::SdkRadrootsdSignerSessionConnectRequest, - ) -> Result<SdkRadrootsdSignerSessionHandle, SdkRadrootsdSessionError> { - self.client.connect_radrootsd_signer_session(request).await - } - - pub async fn connect_bunker( - &self, - url: impl Into<String>, - ) -> Result<SdkRadrootsdSignerSessionHandle, SdkRadrootsdSessionError> { - let request = radrootsd::SdkRadrootsdSignerSessionConnectRequest::bunker(url); - self.connect(&request).await - } - - pub async fn connect_nostrconnect( - &self, - url: impl Into<String>, - client_secret_key: impl Into<String>, - ) -> Result<SdkRadrootsdSignerSessionHandle, SdkRadrootsdSessionError> { - let request = radrootsd::SdkRadrootsdSignerSessionConnectRequest::nostrconnect( - url, - client_secret_key, - ); - self.connect(&request).await - } - - pub async fn status( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionView, SdkRadrootsdSessionError> { - self.client.radrootsd_signer_session_status(session).await - } - - pub async fn list( - &self, - ) -> Result<Vec<SdkRadrootsdSignerSessionView>, SdkRadrootsdSessionError> { - self.client.radrootsd_list_signer_sessions().await - } - - pub async fn authorize( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionAuthorizeResult, SdkRadrootsdSessionError> { - self.client - .authorize_radrootsd_signer_session(session) - .await - } - - pub async fn get_public_key( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionPublicKeyResult, SdkRadrootsdSessionError> { - self.client - .get_radrootsd_signer_session_public_key(session) - .await - } - - pub async fn require_auth( - &self, - session: &SdkRadrootsdSignerSessionRef, - auth_url: impl AsRef<str>, - ) -> Result<SdkRadrootsdSignerSessionRequireAuthResult, SdkRadrootsdSessionError> { - self.client - .require_radrootsd_signer_session_auth(session, auth_url.as_ref()) - .await - } - - pub async fn close( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSessionError> { - self.client.close_radrootsd_signer_session(session).await - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Debug, Clone, Copy)] -pub struct RadrootsdBridgeClient<'a> { - client: &'a RadrootsSdkClient, -} - -#[cfg(feature = "radrootsd-client")] -impl<'a> RadrootsdBridgeClient<'a> { - pub fn sdk(&self) -> &'a RadrootsSdkClient { - self.client - } - - pub fn transport(&self) -> SdkTransportMode { - self.client.transport() - } - - pub fn signer(&self) -> SignerConfig { - self.client.signer() - } - - pub async fn status(&self) -> Result<SdkRadrootsdBridgeStatus, SdkRadrootsdBridgeError> { - self.client.radrootsd_bridge_status().await - } - - pub async fn job( - &self, - job: &SdkRadrootsdBridgeJobRef, - ) -> Result<SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeError> { - self.client.radrootsd_bridge_job_status(job).await - } - - pub async fn jobs(&self) -> Result<Vec<SdkRadrootsdBridgeJobView>, SdkRadrootsdBridgeError> { - self.client.radrootsd_bridge_jobs().await - } -} - -#[derive(Debug, Clone, Copy)] -pub struct ProfileClient<'a> { - client: &'a RadrootsSdkClient, -} - -impl<'a> ProfileClient<'a> { - pub fn sdk(&self) -> &'a RadrootsSdkClient { - self.client - } - - pub fn transport(&self) -> SdkTransportMode { - self.client.transport() - } - - pub fn signer(&self) -> SignerConfig { - self.client.signer() - } - - #[cfg(feature = "serde_json")] - pub fn build_draft( - &self, - profile_value: &RadrootsProfile, - profile_type: Option<RadrootsProfileType>, - ) -> Result<WireEventParts, profile::ProfileEncodeError> { - profile::build_draft(profile_value, profile_type) - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_with_identity( - &self, - identity: &RadrootsIdentity, - profile_value: &RadrootsProfile, - profile_type: Option<RadrootsProfileType>, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let parts = profile::build_draft(profile_value, profile_type) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client - .publish_parts_via_relay_with_identity(identity, parts, "profile.publish_with_identity") - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: WireEventParts, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft, - "profile.publish_draft_with_identity", - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_profile_via_radrootsd( - &self, - profile_value: &RadrootsProfile, - profile_type: Option<RadrootsProfileType>, - session: &SdkRadrootsdSignerSessionHandle, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.publish_profile_via_radrootsd_with_options( - profile_value, - profile_type, - &SdkRadrootsdProfilePublishOptions::from_signer_session(session), - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_profile_via_radrootsd_with_options( - &self, - profile_value: &RadrootsProfile, - profile_type: Option<RadrootsProfileType>, - options: &SdkRadrootsdProfilePublishOptions, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let request = radrootsd::SdkRadrootsdProfilePublishRequest { - profile: profile_value.clone(), - profile_type, - signer_session_id: options.session().session_id().to_owned(), - signer_authority: options.signer_authority().cloned(), - idempotency_key: options.idempotency_key().map(str::to_owned), - }; - self.client.publish_profile_via_radrootsd(&request).await - } -} - -#[derive(Debug, Clone, Copy)] -pub struct FarmClient<'a> { - client: &'a RadrootsSdkClient, -} - -impl<'a> FarmClient<'a> { - pub fn sdk(&self) -> &'a RadrootsSdkClient { - self.client - } - - pub fn transport(&self) -> SdkTransportMode { - self.client.transport() - } - - pub fn signer(&self) -> SignerConfig { - self.client.signer() - } - - #[cfg(feature = "serde_json")] - pub fn build_draft( - &self, - farm_value: &farm::RadrootsFarm, - ) -> Result<WireEventParts, farm::EventEncodeError> { - farm::build_draft(farm_value) - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_with_identity( - &self, - identity: &RadrootsIdentity, - farm_value: &farm::RadrootsFarm, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let parts = farm::build_draft(farm_value) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client - .publish_parts_via_relay_with_identity(identity, parts, "farm.publish_with_identity") - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: WireEventParts, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft, - "farm.publish_draft_with_identity", - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_farm_via_radrootsd( - &self, - farm_value: &farm::RadrootsFarm, - session: &SdkRadrootsdSignerSessionHandle, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.publish_farm_via_radrootsd_with_options( - farm_value, - &SdkRadrootsdFarmPublishOptions::from_signer_session(session), - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_farm_via_radrootsd_with_options( - &self, - farm_value: &farm::RadrootsFarm, - options: &SdkRadrootsdFarmPublishOptions, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let request = radrootsd::SdkRadrootsdFarmPublishRequest { - farm: farm_value.clone(), - kind: Some(KIND_FARM), - signer_session_id: options.session().session_id().to_owned(), - signer_authority: options.signer_authority().cloned(), - idempotency_key: options.idempotency_key().map(str::to_owned), - }; - self.client.publish_farm_via_radrootsd(&request).await - } -} - -#[derive(Debug, Clone, Copy)] -pub struct ListingClient<'a> { - client: &'a RadrootsSdkClient, -} - -impl<'a> ListingClient<'a> { - pub fn sdk(&self) -> &'a RadrootsSdkClient { - self.client - } - - pub fn transport(&self) -> SdkTransportMode { - self.client.transport() - } - - pub fn signer(&self) -> SignerConfig { - self.client.signer() - } - - pub fn build_tags( - &self, - listing_value: &listing::RadrootsListing, - ) -> Result<NostrTags, listing::EventEncodeError> { - listing::build_tags(listing_value) - } - - #[cfg(feature = "serde_json")] - pub fn build_draft( - &self, - listing_value: &listing::RadrootsListing, - ) -> Result<listing::RadrootsListingDraft, listing::EventEncodeError> { - listing::build_draft(listing_value) - } - - #[cfg(feature = "serde_json")] - pub fn parse_event( - &self, - event: &RadrootsNostrEvent, - ) -> Result<listing::RadrootsListing, listing::RadrootsListingParseError> { - listing::parse_event(event) - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_with_identity( - &self, - identity: &RadrootsIdentity, - listing_value: &listing::RadrootsListing, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let parts = listing::build_draft(listing_value) - .map_err(|err| SdkPublishError::Encode(err.to_string()))? - .into_wire_parts(); - self.client - .publish_parts_via_relay_with_identity(identity, parts, "listing.publish_with_identity") - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: listing::RadrootsListingDraft, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "listing.publish_draft_with_identity", - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_listing_via_radrootsd( - &self, - listing_value: &listing::RadrootsListing, - session: &SdkRadrootsdSignerSessionHandle, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.publish_listing_via_radrootsd_with_options( - listing_value, - &SdkRadrootsdListingPublishOptions::from_signer_session(session), - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_listing_via_radrootsd_with_options( - &self, - listing_value: &listing::RadrootsListing, - options: &SdkRadrootsdListingPublishOptions, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let request = radrootsd::SdkRadrootsdListingPublishRequest { - listing: listing_value.clone(), - kind: Some(KIND_LISTING), - signer_session_id: options.session().session_id().to_owned(), - signer_authority: options.signer_authority().cloned(), - idempotency_key: options.idempotency_key().map(str::to_owned), - }; - self.client.publish_listing_via_radrootsd(&request).await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_draft_via_radrootsd( - &self, - draft: listing::RadrootsListingDraft, - session: &SdkRadrootsdSignerSessionHandle, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.publish_draft_via_radrootsd_with_options( - draft, - &SdkRadrootsdListingPublishOptions::from_signer_session(session), - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_draft_via_radrootsd_with_options( - &self, - draft: listing::RadrootsListingDraft, - options: &SdkRadrootsdListingPublishOptions, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let parts = draft.into_wire_parts(); - let event = RadrootsNostrEvent { - id: String::new(), - author: String::new(), - created_at: 0, - kind: parts.kind, - tags: parts.tags, - content: parts.content, - sig: String::new(), - }; - let request = radrootsd::SdkRadrootsdListingPublishRequest::from_event( - &event, - options.session().session_id().to_owned(), - options.signer_authority().cloned(), - options.idempotency_key().map(str::to_owned), - ) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client.publish_listing_via_radrootsd(&request).await - } -} - -#[derive(Debug, Clone, Copy)] -pub struct TradeClient<'a> { - client: &'a RadrootsSdkClient, -} - -impl<'a> TradeClient<'a> { - pub fn sdk(&self) -> &'a RadrootsSdkClient { - self.client - } - - pub fn transport(&self) -> SdkTransportMode { - self.client.transport() - } - - pub fn signer(&self) -> SignerConfig { - self.client.signer() - } - - #[cfg(feature = "serde_json")] - pub fn parse_listing_address( - &self, - listing_addr: &str, - ) -> Result<order::RadrootsOrderListingAddress, order::RadrootsOrderListingAddressError> { - order::parse_listing_address(listing_addr) - } - - #[cfg(feature = "serde_json")] - pub fn validate_listing_event( - &self, - event: &RadrootsNostrEvent, - ) -> Result<TradeListingValidateResult, order::RadrootsTradeValidationListingError> { - order::validate_listing_event(event) - } - - #[cfg(feature = "serde_json")] - pub fn build_order_request_draft( - &self, - listing_event: &RadrootsNostrEventPtr, - payload: &order::RadrootsOrderRequest, - ) -> Result<order::RadrootsOrderRequestDraft, order::EventEncodeError> { - order::build_order_request_draft(listing_event, payload) - } - - #[cfg(feature = "serde_json")] - pub fn build_order_decision_draft( - &self, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderDecision, - ) -> Result<order::RadrootsOrderDecisionDraft, order::EventEncodeError> { - order::build_order_decision_draft(root_event_id, prev_event_id, payload) - } - - #[cfg(feature = "serde_json")] - pub fn build_order_revision_proposal_draft( - &self, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderRevisionProposal, - ) -> Result<order::RadrootsOrderRevisionProposalDraft, order::EventEncodeError> { - order::build_order_revision_proposal_draft(root_event_id, prev_event_id, payload) - } - - #[cfg(feature = "serde_json")] - pub fn build_order_revision_decision_draft( - &self, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderRevisionDecision, - ) -> Result<order::RadrootsOrderRevisionDecisionDraft, order::EventEncodeError> { - order::build_order_revision_decision_draft(root_event_id, prev_event_id, payload) - } - - #[cfg(feature = "serde_json")] - pub fn build_fulfillment_update_draft( - &self, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderFulfillmentUpdate, - ) -> Result<order::RadrootsOrderFulfillmentUpdateDraft, order::EventEncodeError> { - order::build_fulfillment_update_draft(root_event_id, prev_event_id, payload) - } - - #[cfg(feature = "serde_json")] - pub fn build_order_cancellation_draft( - &self, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderCancellation, - ) -> Result<order::RadrootsOrderCancellationDraft, order::EventEncodeError> { - order::build_order_cancellation_draft(root_event_id, prev_event_id, payload) - } - - #[cfg(feature = "serde_json")] - pub fn build_buyer_receipt_draft( - &self, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderReceipt, - ) -> Result<order::RadrootsOrderReceiptDraft, order::EventEncodeError> { - order::build_buyer_receipt_draft(root_event_id, prev_event_id, payload) - } - - #[cfg(feature = "serde_json")] - pub fn parse_order_request( - &self, - event: &RadrootsNostrEvent, - ) -> Result< - order::RadrootsOrderEnvelope<order::RadrootsOrderRequest>, - order::RadrootsOrderEnvelopeParseError, - > { - order::parse_order_request(event) - } - - #[cfg(feature = "serde_json")] - pub fn parse_order_decision( - &self, - event: &RadrootsNostrEvent, - ) -> Result< - order::RadrootsOrderEnvelope<order::RadrootsOrderDecision>, - order::RadrootsOrderEnvelopeParseError, - > { - order::parse_order_decision(event) - } - - #[cfg(feature = "serde_json")] - pub fn parse_order_revision_proposal( - &self, - event: &RadrootsNostrEvent, - ) -> Result< - order::RadrootsOrderEnvelope<order::RadrootsOrderRevisionProposal>, - order::RadrootsOrderEnvelopeParseError, - > { - order::parse_order_revision_proposal(event) - } - - #[cfg(feature = "serde_json")] - pub fn parse_order_revision_decision( - &self, - event: &RadrootsNostrEvent, - ) -> Result< - order::RadrootsOrderEnvelope<order::RadrootsOrderRevisionDecision>, - order::RadrootsOrderEnvelopeParseError, - > { - order::parse_order_revision_decision(event) - } - - #[cfg(feature = "serde_json")] - pub fn parse_fulfillment_update( - &self, - event: &RadrootsNostrEvent, - ) -> Result< - order::RadrootsOrderEnvelope<order::RadrootsOrderFulfillmentUpdate>, - order::RadrootsOrderEnvelopeParseError, - > { - order::parse_fulfillment_update(event) - } - - #[cfg(feature = "serde_json")] - pub fn parse_order_cancellation( - &self, - event: &RadrootsNostrEvent, - ) -> Result< - order::RadrootsOrderEnvelope<order::RadrootsOrderCancellation>, - order::RadrootsOrderEnvelopeParseError, - > { - order::parse_order_cancellation(event) - } - - #[cfg(feature = "serde_json")] - pub fn parse_buyer_receipt( - &self, - event: &RadrootsNostrEvent, - ) -> Result< - order::RadrootsOrderEnvelope<order::RadrootsOrderReceipt>, - order::RadrootsOrderEnvelopeParseError, - > { - order::parse_buyer_receipt(event) - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_order_request_with_identity( - &self, - identity: &RadrootsIdentity, - listing_event: &RadrootsNostrEventPtr, - payload: &order::RadrootsOrderRequest, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = order::build_order_request_draft(listing_event, payload) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_order_request_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_order_revision_proposal_with_identity( - &self, - identity: &RadrootsIdentity, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderRevisionProposal, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = - order::build_order_revision_proposal_draft(root_event_id, prev_event_id, payload) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_order_revision_proposal_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_order_revision_decision_with_identity( - &self, - identity: &RadrootsIdentity, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderRevisionDecision, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = - order::build_order_revision_decision_draft(root_event_id, prev_event_id, payload) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_order_revision_decision_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_order_decision_with_identity( - &self, - identity: &RadrootsIdentity, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderDecision, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = order::build_order_decision_draft(root_event_id, prev_event_id, payload) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_order_decision_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_fulfillment_update_with_identity( - &self, - identity: &RadrootsIdentity, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderFulfillmentUpdate, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = order::build_fulfillment_update_draft(root_event_id, prev_event_id, payload) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_fulfillment_update_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_order_revision_proposal_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: order::RadrootsOrderRevisionProposalDraft, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_order_revision_proposal_draft_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_order_revision_decision_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: order::RadrootsOrderRevisionDecisionDraft, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_order_revision_decision_draft_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_order_cancellation_with_identity( - &self, - identity: &RadrootsIdentity, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderCancellation, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = order::build_order_cancellation_draft(root_event_id, prev_event_id, payload) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_order_cancellation_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_buyer_receipt_with_identity( - &self, - identity: &RadrootsIdentity, - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &order::RadrootsOrderReceipt, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = order::build_buyer_receipt_draft(root_event_id, prev_event_id, payload) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_buyer_receipt_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_order_request_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: order::RadrootsOrderRequestDraft, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_order_request_draft_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_order_decision_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: order::RadrootsOrderDecisionDraft, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_order_decision_draft_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_fulfillment_update_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: order::RadrootsOrderFulfillmentUpdateDraft, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_fulfillment_update_draft_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_order_cancellation_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: order::RadrootsOrderCancellationDraft, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_order_cancellation_draft_with_identity", - ) - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_buyer_receipt_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: order::RadrootsOrderReceiptDraft, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "order.publish_buyer_receipt_draft_with_identity", - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_order_request_via_radrootsd( - &self, - order: &order::RadrootsOrderRequest, - listing_event: &RadrootsNostrEventPtr, - session: &SdkRadrootsdSignerSessionHandle, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.publish_order_request_via_radrootsd_with_options( - order, - listing_event, - &SdkRadrootsdOrderRequestPublishOptions::from_signer_session(session), - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_order_request_via_radrootsd_with_options( - &self, - order: &order::RadrootsOrderRequest, - listing_event: &RadrootsNostrEventPtr, - options: &SdkRadrootsdOrderRequestPublishOptions, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let request = radrootsd::SdkRadrootsdOrderRequestPublishRequest { - order: order.clone(), - listing_event: listing_event.clone(), - signer_session_id: options.session().session_id().to_owned(), - signer_authority: options.signer_authority().cloned(), - idempotency_key: options.idempotency_key().map(str::to_owned), - }; - self.client - .publish_order_request_via_radrootsd(&request) - .await - } -} - -#[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -fn sdk_publish_receipt_from_relay_output( - signed_event: signing::SignedNostrEvent, - target_relays: Vec<String>, - connected_relays: Vec<String>, - output: relay::RelayOutput<relay::RelayEventId>, -) -> Result<SdkPublishReceipt, SdkPublishError> { - let event = sdk_event_from_signed_event(&signed_event); - let event_id = event.id.clone(); - let event_kind = event.kind; - let created_at = event.created_at; - let signature = event.sig.clone(); - let target_relays = sorted_unique_strings(target_relays); - let connected_relays = sorted_unique_strings(connected_relays); - let mut acknowledged_relays = output - .success - .into_iter() - .map(|relay| relay.to_string()) - .collect::<Vec<_>>(); - acknowledged_relays = sorted_unique_strings(acknowledged_relays); - - let mut failed_relays = output - .failed - .into_iter() - .map(|(relay_url, error)| SdkRelayFailure { - relay_url: relay_url.to_string(), - error, - }) - .collect::<Vec<_>>(); - failed_relays.sort_by(|left, right| left.relay_url.cmp(&right.relay_url)); - - if acknowledged_relays.is_empty() { - return Err(SdkPublishError::RelayNotAcknowledged { - transport: SdkTransportMode::RelayDirect, - failed_relays, - }); - } - - Ok(SdkPublishReceipt { - transport: SdkTransportMode::RelayDirect, - event_kind: Some(event_kind), - event_id: Some(event_id.clone()), - transport_receipt: SdkTransportReceipt::RelayDirect(SdkRelayPublishReceipt { - event, - event_id, - event_kind, - created_at, - signature, - target_relays, - connected_relays, - acknowledged_relays, - failed_relays, - }), - }) -} - -#[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -fn sdk_event_from_signed_event(event: &signing::SignedNostrEvent) -> RadrootsNostrEvent { - RadrootsNostrEvent { - id: event.id.to_string(), - author: event.pubkey.to_string(), - created_at: u32::try_from(event.created_at.as_secs()).unwrap_or(u32::MAX), - kind: event.kind.as_u16() as u32, - tags: event - .tags - .iter() - .map(|tag| tag.as_slice().to_vec()) - .collect(), - content: event.content.clone(), - sig: event.sig.to_string(), - } -} - -#[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -fn sorted_unique_strings(mut values: Vec<String>) -> Vec<String> { - values.sort(); - values.dedup(); - values -} - -#[cfg(feature = "radrootsd-client")] -fn sdk_publish_receipt_from_radrootsd_bridge_response( - response: radrootsd::SdkRadrootsdBridgePublishResponse, -) -> SdkPublishReceipt { - let job = response.job; - SdkPublishReceipt { - transport: SdkTransportMode::Radrootsd, - event_kind: Some(job.event_kind), - event_id: job.event_id.clone(), - transport_receipt: SdkTransportReceipt::Radrootsd(SdkRadrootsdPublishReceipt { - accepted: true, - deduplicated: response.deduplicated, - job_id: Some(job.job_id), - status: Some(job.status), - signer_mode: Some(job.signer_mode), - signer_session_id: job.signer_session_id, - event_addr: job.event_addr, - relay_count: Some(job.relay_count), - acknowledged_relay_count: Some(job.acknowledged_relay_count), - }), - } -} - -#[cfg(all( - test, - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -mod tests { - use super::{ - SdkPublishError, SdkRelayFailure, SdkTransportMode, sdk_publish_receipt_from_relay_output, - }; - use crate::WireEventParts; - use crate::adapters::relay::RelayOutput; - use crate::adapters::signing::sign_parts_with_identity; - use crate::identity::RadrootsIdentity; - use radroots_nostr::prelude::RadrootsNostrEventId; - use std::collections::{HashMap, HashSet}; - - #[test] - fn relay_output_maps_to_normalized_publish_receipt() { - let identity = RadrootsIdentity::generate(); - let signed_event = sign_parts_with_identity( - &identity, - WireEventParts { - kind: 30402, - content: "listing".to_owned(), - tags: vec![vec!["d".to_owned(), "AAAAAAAAAAAAAAAAAAAAAg".to_owned()]], - }, - ) - .expect("signed event"); - let event_id = signed_event.id.to_string(); - let event_created_at = u32::try_from(signed_event.created_at.as_secs()).unwrap(); - let event_signature = signed_event.sig.to_string(); - let output = RelayOutput { - val: RadrootsNostrEventId::parse(event_id.as_str()).expect("event id"), - success: HashSet::from([ - nostr::RelayUrl::parse("ws://127.0.0.1:8080").expect("relay a"), - nostr::RelayUrl::parse("ws://127.0.0.1:8081").expect("relay b"), - ]), - failed: HashMap::from([( - nostr::RelayUrl::parse("ws://127.0.0.1:8082").expect("relay c"), - "timeout".to_owned(), - )]), - }; - - let receipt = sdk_publish_receipt_from_relay_output( - signed_event, - vec![ - "ws://127.0.0.1:8081".to_owned(), - "ws://127.0.0.1:8080".to_owned(), - ], - vec!["ws://127.0.0.1:8080".to_owned()], - output, - ) - .expect("receipt"); - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(30402)); - assert_eq!(receipt.event_id, Some(event_id.clone())); - let relay_receipt = match receipt.transport_receipt { - super::SdkTransportReceipt::RelayDirect(relay_receipt) => relay_receipt, - super::SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - }; - assert_eq!(relay_receipt.event.id, event_id); - assert_eq!(relay_receipt.event_id, relay_receipt.event.id); - assert_eq!(relay_receipt.event_kind, 30402); - assert_eq!(relay_receipt.created_at, event_created_at); - assert_eq!(relay_receipt.signature, event_signature); - assert_eq!( - relay_receipt.target_relays, - vec![ - "ws://127.0.0.1:8080".to_owned(), - "ws://127.0.0.1:8081".to_owned(), - ] - ); - assert_eq!( - relay_receipt.connected_relays, - vec!["ws://127.0.0.1:8080".to_owned()] - ); - } - - #[test] - fn relay_output_without_acknowledgement_is_rejected() { - let identity = RadrootsIdentity::generate(); - let signed_event = sign_parts_with_identity( - &identity, - WireEventParts { - kind: 30402, - content: "listing".to_owned(), - tags: vec![], - }, - ) - .expect("signed event"); - let output = RelayOutput { - val: RadrootsNostrEventId::parse(signed_event.id.to_string().as_str()) - .expect("event id"), - success: HashSet::new(), - failed: HashMap::from([( - nostr::RelayUrl::parse("ws://127.0.0.1:8082").expect("relay c"), - "blocked".to_owned(), - )]), - }; - - let error = sdk_publish_receipt_from_relay_output(signed_event, vec![], vec![], output) - .expect_err("error"); - - assert_eq!( - error, - SdkPublishError::RelayNotAcknowledged { - transport: SdkTransportMode::RelayDirect, - failed_relays: vec![SdkRelayFailure { - relay_url: "ws://127.0.0.1:8082".to_owned(), - error: "blocked".to_owned(), - }], - } - ); - } -} diff --git a/crates/sdk/src/config.rs b/crates/sdk/src/config.rs @@ -1,388 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; -use core::fmt; -#[cfg(feature = "std")] -use std::{env, string::String, vec::Vec}; - -pub const RADROOTS_SDK_PRODUCTION_RELAY_URL: &str = "wss://radroots.org"; -pub const RADROOTS_SDK_STAGING_RELAY_URL: &str = "wss://staging.radroots.org"; -pub const RADROOTS_SDK_LOCAL_RELAY_URL: &str = "ws://127.0.0.1:8080"; - -pub const RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT: &str = "https://rpc.radroots.org/jsonrpc"; -pub const RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT: &str = - "https://rpc.staging.radroots.org/jsonrpc"; -pub const RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT: &str = "http://127.0.0.1:7070"; - -pub const RADROOTS_SDK_DEFAULT_TIMEOUT_MS: u64 = 10_000; - -#[cfg(feature = "std")] -const LOCAL_RELAY_SCHEME_ENV: &str = "NOSTR_RS_RELAY_PUBLIC_SCHEME"; -#[cfg(feature = "std")] -const LOCAL_RELAY_HOST_ENV: &str = "NOSTR_RS_RELAY_PUBLIC_HOST"; -#[cfg(feature = "std")] -const LOCAL_RELAY_PORT_ENV: &str = "NOSTR_RS_RELAY_PUBLIC_PORT"; -#[cfg(feature = "std")] -const LOCAL_RADROOTSD_ENDPOINT_ENV: &str = "RADROOTSD_RPC_URL"; -#[cfg(feature = "std")] -const LOCAL_RADROOTSD_HOST_ENV: &str = "RADROOTSD_RPC_HOST"; -#[cfg(feature = "std")] -const LOCAL_RADROOTSD_PORT_ENV: &str = "RADROOTSD_RPC_PORT"; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsSdkConfig { - pub environment: SdkEnvironment, - pub transport: SdkTransportMode, - pub relay: RelayConfig, - pub radrootsd: RadrootsdConfig, - pub signer: SignerConfig, - pub network: NetworkConfig, -} - -impl RadrootsSdkConfig { - pub fn production() -> Self { - Self::for_environment(SdkEnvironment::Production) - } - - pub fn staging() -> Self { - Self::for_environment(SdkEnvironment::Staging) - } - - pub fn local() -> Self { - Self::for_environment(SdkEnvironment::Local) - } - - pub fn custom() -> Self { - Self::for_environment(SdkEnvironment::Custom) - } - - pub fn for_environment(environment: SdkEnvironment) -> Self { - Self { - environment, - transport: SdkTransportMode::RelayDirect, - relay: RelayConfig::default(), - radrootsd: RadrootsdConfig::default(), - signer: SignerConfig::default(), - network: NetworkConfig::default(), - } - } - - pub fn resolved_relay_urls(&self) -> Result<Vec<String>, SdkConfigError> { - self.relay.resolved_urls(self.environment) - } - - pub fn resolved_radrootsd_endpoint(&self) -> Result<String, SdkConfigError> { - self.radrootsd.resolved_endpoint(self.environment) - } -} - -impl Default for RadrootsSdkConfig { - fn default() -> Self { - Self::production() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SdkEnvironment { - Production, - Staging, - Local, - Custom, -} - -impl SdkEnvironment { - pub fn default_relay_urls(self) -> Option<Vec<String>> { - match self { - Self::Production => Some(vec![RADROOTS_SDK_PRODUCTION_RELAY_URL.to_owned()]), - Self::Staging => Some(vec![RADROOTS_SDK_STAGING_RELAY_URL.to_owned()]), - Self::Local => Some(vec![RADROOTS_SDK_LOCAL_RELAY_URL.to_owned()]), - Self::Custom => None, - } - } - - pub fn default_radrootsd_endpoint(self) -> Option<&'static str> { - match self { - Self::Production => Some(RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT), - Self::Staging => Some(RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT), - Self::Local => Some(RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT), - Self::Custom => None, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SdkTransportMode { - RelayDirect, - Radrootsd, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct RelayConfig { - pub urls: Vec<String>, -} - -impl RelayConfig { - pub fn resolved_urls( - &self, - environment: SdkEnvironment, - ) -> Result<Vec<String>, SdkConfigError> { - if self.urls.is_empty() { - if environment == SdkEnvironment::Local { - #[cfg(feature = "std")] - if let Some(local_url) = resolve_local_relay_url_from_env() { - return Ok(vec![normalize_relay_url(local_url.as_str())?]); - } - } - return environment - .default_relay_urls() - .ok_or(SdkConfigError::MissingCustomRelayUrls); - } - - normalize_relay_urls(&self.urls) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsdConfig { - pub endpoint: Option<String>, - pub auth: RadrootsdAuth, -} - -impl RadrootsdConfig { - pub fn resolved_endpoint(&self, environment: SdkEnvironment) -> Result<String, SdkConfigError> { - match self.endpoint.as_deref() { - Some(endpoint) => normalize_radrootsd_endpoint(endpoint), - None => { - if environment == SdkEnvironment::Local { - #[cfg(feature = "std")] - if let Some(endpoint) = resolve_local_radrootsd_endpoint_from_env() { - return normalize_radrootsd_endpoint(endpoint.as_str()); - } - } - - environment - .default_radrootsd_endpoint() - .map(str::to_owned) - .ok_or(SdkConfigError::MissingCustomRadrootsdEndpoint) - } - } - } -} - -impl Default for RadrootsdConfig { - fn default() -> Self { - Self { - endpoint: None, - auth: RadrootsdAuth::default(), - } - } -} - -#[derive(Clone, PartialEq, Eq, Default)] -pub enum RadrootsdAuth { - #[default] - None, - BearerToken(String), -} - -impl fmt::Debug for RadrootsdAuth { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::None => f.write_str("None"), - Self::BearerToken(_) => f.write_str("BearerToken(\"<redacted>\")"), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum SignerConfig { - #[default] - DraftOnly, - LocalIdentity, - Nip46, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NetworkConfig { - pub timeout_ms: u64, -} - -impl Default for NetworkConfig { - fn default() -> Self { - Self { - timeout_ms: RADROOTS_SDK_DEFAULT_TIMEOUT_MS, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SdkConfigError { - MissingCustomRelayUrls, - MissingCustomRadrootsdEndpoint, - EmptyRelayUrl, - InvalidRelayUrl(String), - EmptyRadrootsdEndpoint, - InvalidRadrootsdEndpoint(String), -} - -impl fmt::Display for SdkConfigError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::MissingCustomRelayUrls => { - f.write_str("custom sdk environment requires explicit relay urls") - } - Self::MissingCustomRadrootsdEndpoint => { - f.write_str("custom sdk environment requires an explicit radrootsd endpoint") - } - Self::EmptyRelayUrl => f.write_str("relay url must not be empty"), - Self::InvalidRelayUrl(value) => { - write!(f, "relay url must use ws or wss, got `{value}`") - } - Self::EmptyRadrootsdEndpoint => f.write_str("radrootsd endpoint must not be empty"), - Self::InvalidRadrootsdEndpoint(value) => { - write!( - f, - "radrootsd endpoint must use http or https, got `{value}`" - ) - } - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for SdkConfigError {} - -impl fmt::Display for SignerConfig { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::DraftOnly => f.write_str("draft_only"), - Self::LocalIdentity => f.write_str("local_identity"), - Self::Nip46 => f.write_str("nip46"), - } - } -} - -fn normalize_relay_urls(values: &[String]) -> Result<Vec<String>, SdkConfigError> { - let mut normalized = Vec::new(); - for value in values { - let relay = normalize_relay_url(value.as_str())?; - if !normalized.iter().any(|existing| existing == &relay) { - normalized.push(relay); - } - } - Ok(normalized) -} - -fn normalize_relay_url(value: &str) -> Result<String, SdkConfigError> { - let trimmed = value.trim(); - if trimmed.is_empty() { - return Err(SdkConfigError::EmptyRelayUrl); - } - - let rest = if let Some(rest) = trimmed.strip_prefix("ws://") { - rest - } else if let Some(rest) = trimmed.strip_prefix("wss://") { - rest - } else { - return Err(SdkConfigError::InvalidRelayUrl(trimmed.to_owned())); - }; - - if relay_authority_is_invalid(rest) { - return Err(SdkConfigError::InvalidRelayUrl(trimmed.to_owned())); - } - - Ok(trimmed.to_owned()) -} - -fn relay_authority_is_invalid(rest: &str) -> bool { - let authority_end = rest - .char_indices() - .find(|(_, ch)| matches!(ch, '/' | '?' | '#')) - .map(|(index, _)| index) - .unwrap_or(rest.len()); - let authority = &rest[..authority_end]; - - if authority.is_empty() || authority.chars().any(char::is_whitespace) { - return true; - } - if authority.contains('@') { - return true; - } - - if let Some(after_open) = authority.strip_prefix('[') { - let Some(close_index) = after_open.find(']') else { - return true; - }; - let host = &after_open[..close_index]; - let after_host = &after_open[close_index + 1..]; - if host.is_empty() { - return true; - } - return relay_port_suffix_is_invalid(after_host); - } - - let colon_count = authority.bytes().filter(|byte| *byte == b':').count(); - match colon_count { - 0 => false, - 1 => { - let (host, port) = authority - .split_once(':') - .expect("one colon in relay authority"); - host.is_empty() || relay_port_is_invalid(port) - } - _ => true, - } -} - -fn relay_port_suffix_is_invalid(after_host: &str) -> bool { - if after_host.is_empty() { - return false; - } - let Some(port) = after_host.strip_prefix(':') else { - return true; - }; - relay_port_is_invalid(port) -} - -fn relay_port_is_invalid(port: &str) -> bool { - port.is_empty() || !port.bytes().all(|byte| byte.is_ascii_digit()) -} - -fn normalize_radrootsd_endpoint(value: &str) -> Result<String, SdkConfigError> { - let trimmed = value.trim(); - if trimmed.is_empty() { - return Err(SdkConfigError::EmptyRadrootsdEndpoint); - } - if !(trimmed.starts_with("http://") || trimmed.starts_with("https://")) { - return Err(SdkConfigError::InvalidRadrootsdEndpoint(trimmed.to_owned())); - } - Ok(trimmed.to_owned()) -} - -#[cfg(feature = "std")] -fn resolve_local_relay_url_from_env() -> Option<String> { - let scheme = read_trimmed_env(LOCAL_RELAY_SCHEME_ENV)?; - let host = read_trimmed_env(LOCAL_RELAY_HOST_ENV)?; - let port = read_trimmed_env(LOCAL_RELAY_PORT_ENV)?; - Some(format!("{scheme}://{host}:{port}")) -} - -#[cfg(feature = "std")] -fn resolve_local_radrootsd_endpoint_from_env() -> Option<String> { - if let Some(endpoint) = read_trimmed_env(LOCAL_RADROOTSD_ENDPOINT_ENV) { - return Some(endpoint); - } - - let host = read_trimmed_env(LOCAL_RADROOTSD_HOST_ENV)?; - let port = read_trimmed_env(LOCAL_RADROOTSD_PORT_ENV)?; - Some(format!("http://{host}:{port}")) -} - -#[cfg(feature = "std")] -fn read_trimmed_env(key: &str) -> Option<String> { - let value = env::var(key).ok()?; - let trimmed = value.trim(); - if trimmed.is_empty() { - return None; - } - Some(trimmed.to_owned()) -} diff --git a/crates/sdk/src/farm.rs b/crates/sdk/src/farm.rs @@ -1,9 +0,0 @@ -pub use radroots_events::farm::*; -pub use radroots_events_codec::error::EventEncodeError; - -use crate::WireEventParts; - -#[cfg(feature = "serde_json")] -pub fn build_draft(farm: &RadrootsFarm) -> Result<WireEventParts, EventEncodeError> { - radroots_events_codec::farm::encode::to_wire_parts(farm) -} diff --git a/crates/sdk/src/identity.rs b/crates/sdk/src/identity.rs @@ -1,33 +0,0 @@ -pub use radroots_identity::{ - DEFAULT_IDENTITY_PATH, IdentityError, RADROOTS_USERNAME_MAX_LEN, RADROOTS_USERNAME_MIN_LEN, - RADROOTS_USERNAME_REGEX, RadrootsIdentity, RadrootsIdentityFile, RadrootsIdentityId, - RadrootsIdentityProfile, RadrootsIdentityPublic, RadrootsIdentitySecretKeyFormat, - radroots_username_is_valid, radroots_username_normalize, -}; - -#[cfg(feature = "identity-storage")] -pub use radroots_identity::{ - RADROOTS_ENCRYPTED_IDENTITY_DEFAULT_KEY_SLOT, RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX, - RadrootsEncryptedIdentityFile, encrypted_identity_wrapping_key_path, load_encrypted_identity, - load_encrypted_identity_with_key_slot, load_identity_profile, rotate_encrypted_identity, - rotate_encrypted_identity_with_key_slot, store_encrypted_identity, - store_encrypted_identity_with_key_slot, store_identity_profile, -}; - -#[cfg(all(feature = "identity-models", feature = "identity-storage"))] -#[cfg(test)] -mod tests { - use super::{RadrootsEncryptedIdentityFile, RadrootsIdentity}; - - #[test] - fn encrypted_identity_file_round_trips() { - let temp = tempfile::tempdir().expect("tempdir"); - let file = RadrootsEncryptedIdentityFile::new(temp.path().join("identity.enc.json")); - let identity = RadrootsIdentity::generate(); - - file.store(&identity).expect("store identity"); - let loaded = file.load().expect("load identity"); - - assert_eq!(loaded.public_key_hex(), identity.public_key_hex()); - } -} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -1,72 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] -#![forbid(unsafe_code)] - -#[cfg(not(feature = "std"))] -extern crate alloc; - -#[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; -#[cfg(feature = "std")] -use std::{string::String, vec::Vec}; - -#[cfg(any( - feature = "radrootsd-client", - feature = "signing", - feature = "relay-client", - feature = "signer-adapters" -))] -pub mod adapters; -pub mod client; -pub mod config; -pub mod farm; -#[cfg(feature = "identity-models")] -pub mod identity; -pub mod listing; -pub mod order; -pub mod profile; - -#[cfg(feature = "radrootsd-client")] -pub use crate::adapters::radrootsd::{ - SdkRadrootsdBridgeDeliveryPolicy, SdkRadrootsdBridgeJobStatus, - SdkRadrootsdBridgeRelayPublishResult, SdkRadrootsdSignerAuthority, - SdkRadrootsdSignerSessionConnectRequest, SdkRadrootsdSignerSessionMode, - SdkRadrootsdSignerSessionRole, -}; -pub use crate::client::{ - FarmClient, ListingClient, ProfileClient, RadrootsSdkClient, SdkPublishError, - SdkPublishReceipt, SdkRadrootsdPublishReceipt, SdkRelayFailure, SdkRelayPublishReceipt, - SdkResolvedTransportTarget, SdkTransportReceipt, TradeClient, -}; -#[cfg(feature = "radrootsd-client")] -pub use crate::client::{ - RadrootsdBridgeClient, RadrootsdClient, RadrootsdSignerSessionClient, SdkRadrootsdBridgeError, - SdkRadrootsdBridgeJobRef, SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeStatus, - SdkRadrootsdFarmPublishOptions, SdkRadrootsdListingPublishOptions, - SdkRadrootsdOrderRequestPublishOptions, SdkRadrootsdProfilePublishOptions, - SdkRadrootsdSessionError, SdkRadrootsdSignerSessionAuthorizeResult, - SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSignerSessionHandle, - SdkRadrootsdSignerSessionPublicKeyResult, SdkRadrootsdSignerSessionRef, - SdkRadrootsdSignerSessionRequireAuthResult, SdkRadrootsdSignerSessionView, -}; -pub use crate::config::{ - NetworkConfig, RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT, RADROOTS_SDK_LOCAL_RELAY_URL, - RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT, RADROOTS_SDK_PRODUCTION_RELAY_URL, - RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT, RADROOTS_SDK_STAGING_RELAY_URL, RadrootsSdkConfig, - RadrootsdAuth, RadrootsdConfig, RelayConfig, SdkConfigError, SdkEnvironment, SdkTransportMode, - SignerConfig, -}; -pub use radroots_events::{ - RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsNostrEventRef, - draft::{RadrootsFrozenEventDraft, RadrootsSignedNostrEvent}, - farm::RadrootsFarm, - listing::RadrootsListing, - profile::{RadrootsProfile, RadrootsProfileType}, -}; -#[cfg(feature = "serde_json")] -pub use radroots_events_codec::order::{ - RadrootsOrderEnvelopeParseError, RadrootsOrderListingAddress, RadrootsOrderListingAddressError, -}; -pub use radroots_events_codec::wire::WireEventParts; -pub use radroots_trade::listing::validation::RadrootsTradeListing as TradeListingValidateResult; - -pub type NostrTags = Vec<Vec<String>>; diff --git a/crates/sdk/src/listing.rs b/crates/sdk/src/listing.rs @@ -1,40 +0,0 @@ -pub use radroots_events::listing::*; -pub use radroots_events::order::RadrootsListingParseError; -pub use radroots_events::trade_validation::RadrootsTradeValidationListingError; -pub use radroots_events_codec::error::EventEncodeError; -pub use radroots_trade::listing::validation::RadrootsTradeListing as TradeListingValidateResult; - -use crate::{NostrTags, RadrootsNostrEvent, WireEventParts}; - -#[derive(Debug, Clone)] -pub struct RadrootsListingDraft { - parts: WireEventParts, -} - -impl RadrootsListingDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -pub fn build_tags(listing: &RadrootsListing) -> Result<NostrTags, EventEncodeError> { - radroots_events_codec::listing::encode::listing_build_tags(listing) -} - -#[cfg(feature = "serde_json")] -pub fn build_draft(listing: &RadrootsListing) -> Result<RadrootsListingDraft, EventEncodeError> { - Ok(RadrootsListingDraft { - parts: radroots_events_codec::listing::encode::to_wire_parts(listing)?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn parse_event( - event: &RadrootsNostrEvent, -) -> Result<RadrootsListing, RadrootsListingParseError> { - radroots_trade::listing::parse_listing_event(event) -} diff --git a/crates/sdk/src/order.rs b/crates/sdk/src/order.rs @@ -1,281 +0,0 @@ -pub use radroots_events::order::*; -pub use radroots_events::trade_validation::*; -pub use radroots_events_codec::error::EventEncodeError; -#[cfg(feature = "serde_json")] -pub use radroots_events_codec::order::{ - RadrootsOrderEnvelopeParseError, RadrootsOrderEventContext, RadrootsOrderListingAddress, - RadrootsOrderListingAddressError, -}; -pub use radroots_trade::listing::validation::RadrootsTradeListing as TradeListingValidateResult; - -use crate::{RadrootsNostrEvent, RadrootsNostrEventPtr, WireEventParts}; -use radroots_events::ids::RadrootsEventId; - -#[derive(Debug, Clone)] -pub struct RadrootsOrderRequestDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsOrderDecisionDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsOrderRevisionProposalDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsOrderRevisionDecisionDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsOrderFulfillmentUpdateDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsOrderCancellationDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsOrderReceiptDraft { - parts: WireEventParts, -} - -impl RadrootsOrderRequestDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsOrderDecisionDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsOrderRevisionProposalDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsOrderRevisionDecisionDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsOrderFulfillmentUpdateDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsOrderCancellationDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsOrderReceiptDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -#[cfg(feature = "serde_json")] -pub fn build_order_request_draft( - listing_event: &RadrootsNostrEventPtr, - payload: &RadrootsOrderRequest, -) -> Result<RadrootsOrderRequestDraft, EventEncodeError> { - Ok(RadrootsOrderRequestDraft { - parts: radroots_events_codec::order::order_request_event_build(listing_event, payload)?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_order_decision_draft( - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &RadrootsOrderDecision, -) -> Result<RadrootsOrderDecisionDraft, EventEncodeError> { - Ok(RadrootsOrderDecisionDraft { - parts: radroots_events_codec::order::order_decision_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_order_revision_proposal_draft( - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &RadrootsOrderRevisionProposal, -) -> Result<RadrootsOrderRevisionProposalDraft, EventEncodeError> { - Ok(RadrootsOrderRevisionProposalDraft { - parts: radroots_events_codec::order::order_revision_proposal_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_order_revision_decision_draft( - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &RadrootsOrderRevisionDecision, -) -> Result<RadrootsOrderRevisionDecisionDraft, EventEncodeError> { - Ok(RadrootsOrderRevisionDecisionDraft { - parts: radroots_events_codec::order::order_revision_decision_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_fulfillment_update_draft( - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &RadrootsOrderFulfillmentUpdate, -) -> Result<RadrootsOrderFulfillmentUpdateDraft, EventEncodeError> { - Ok(RadrootsOrderFulfillmentUpdateDraft { - parts: radroots_events_codec::order::order_fulfillment_update_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_order_cancellation_draft( - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &RadrootsOrderCancellation, -) -> Result<RadrootsOrderCancellationDraft, EventEncodeError> { - Ok(RadrootsOrderCancellationDraft { - parts: radroots_events_codec::order::order_cancellation_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_buyer_receipt_draft( - root_event_id: &RadrootsEventId, - prev_event_id: &RadrootsEventId, - payload: &RadrootsOrderReceipt, -) -> Result<RadrootsOrderReceiptDraft, EventEncodeError> { - Ok(RadrootsOrderReceiptDraft { - parts: radroots_events_codec::order::order_receipt_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn parse_order_request( - event: &RadrootsNostrEvent, -) -> Result<RadrootsOrderEnvelope<RadrootsOrderRequest>, RadrootsOrderEnvelopeParseError> { - radroots_events_codec::order::order_request_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_order_decision( - event: &RadrootsNostrEvent, -) -> Result<RadrootsOrderEnvelope<RadrootsOrderDecision>, RadrootsOrderEnvelopeParseError> { - radroots_events_codec::order::order_decision_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_order_revision_proposal( - event: &RadrootsNostrEvent, -) -> Result<RadrootsOrderEnvelope<RadrootsOrderRevisionProposal>, RadrootsOrderEnvelopeParseError> { - radroots_events_codec::order::order_revision_proposal_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_order_revision_decision( - event: &RadrootsNostrEvent, -) -> Result<RadrootsOrderEnvelope<RadrootsOrderRevisionDecision>, RadrootsOrderEnvelopeParseError> { - radroots_events_codec::order::order_revision_decision_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_fulfillment_update( - event: &RadrootsNostrEvent, -) -> Result<RadrootsOrderEnvelope<RadrootsOrderFulfillmentUpdate>, RadrootsOrderEnvelopeParseError> -{ - radroots_events_codec::order::order_fulfillment_update_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_order_cancellation( - event: &RadrootsNostrEvent, -) -> Result<RadrootsOrderEnvelope<RadrootsOrderCancellation>, RadrootsOrderEnvelopeParseError> { - radroots_events_codec::order::order_cancellation_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_buyer_receipt( - event: &RadrootsNostrEvent, -) -> Result<RadrootsOrderEnvelope<RadrootsOrderReceipt>, RadrootsOrderEnvelopeParseError> { - radroots_events_codec::order::order_receipt_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_listing_address( - listing_addr: &str, -) -> Result<RadrootsOrderListingAddress, RadrootsOrderListingAddressError> { - RadrootsOrderListingAddress::parse(listing_addr) -} - -#[cfg(feature = "serde_json")] -pub fn validate_listing_event( - event: &RadrootsNostrEvent, -) -> Result<TradeListingValidateResult, RadrootsTradeValidationListingError> { - radroots_trade::listing::validation::validate_listing_event(event) -} diff --git a/crates/sdk/src/profile.rs b/crates/sdk/src/profile.rs @@ -1,12 +0,0 @@ -pub use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; -pub use radroots_events_codec::profile::error::ProfileEncodeError; - -use crate::WireEventParts; - -#[cfg(feature = "serde_json")] -pub fn build_draft( - profile: &RadrootsProfile, - profile_type: Option<RadrootsProfileType>, -) -> Result<WireEventParts, ProfileEncodeError> { - radroots_events_codec::profile::encode::to_wire_parts_with_profile_type(profile, profile_type) -} diff --git a/crates/sdk/tests/client.rs b/crates/sdk/tests/client.rs @@ -1,906 +0,0 @@ -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, RadrootsCoreUnit, -}; -use radroots_events::farm::{RadrootsFarm, RadrootsFarmRef}; -use radroots_events::ids::{RadrootsEventId, RadrootsPublicKey}; -use radroots_events::kinds::{ - KIND_FARM, KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, - KIND_ORDER_FULFILLMENT_UPDATE, KIND_ORDER_RECEIPT, KIND_ORDER_REQUEST, - KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, KIND_PROFILE, -}; -use radroots_events::listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, -}; -use radroots_events::order::{ - RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, - RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderFulfillmentState, - RadrootsOrderFulfillmentUpdate, RadrootsOrderInventoryCommitment, RadrootsOrderItem, - RadrootsOrderPricingBasis, RadrootsOrderReceipt, RadrootsOrderRequest, - RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, -}; -use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; -use radroots_sdk::{ - RADROOTS_SDK_PRODUCTION_RELAY_URL, RadrootsNostrEvent, RadrootsNostrEventPtr, - RadrootsSdkClient, RadrootsSdkConfig, RelayConfig, SdkConfigError, SdkEnvironment, - SdkPublishError, SdkRadrootsdPublishReceipt, SdkRelayFailure, SdkResolvedTransportTarget, - SdkTransportMode, SignerConfig, WireEventParts, -}; - -fn sample_farm() -> RadrootsFarm { - RadrootsFarm { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - name: "North Farm".into(), - about: Some("Organic coffee".into()), - website: None, - picture: None, - banner: None, - location: None, - tags: Some(vec!["coffee".into()]), - } -} - -fn sample_listing() -> RadrootsListing { - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "seller".into(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - }, - product: RadrootsListingProduct { - key: "coffee".into(), - title: "Coffee".into(), - category: "coffee".into(), - summary: Some("Single origin coffee".into()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".parse().expect("primary bin id"), - bins: vec![RadrootsListingBin { - bin_id: "bin-1".parse().expect("bin id"), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1000u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice { - amount: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(20u32), - RadrootsCoreCurrency::USD, - ), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - }, - display_amount: None, - display_unit: None, - display_label: None, - display_price: None, - display_price_unit: None, - }], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(RadrootsCoreDecimal::from(5u32)), - availability: Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Active, - }), - delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), - location: Some(RadrootsListingLocation { - primary: "North Farm".into(), - city: None, - region: None, - country: None, - lat: None, - lng: None, - geohash: None, - }), - images: None, - } -} - -fn sample_profile() -> RadrootsProfile { - RadrootsProfile { - name: "north-farm".into(), - display_name: Some("North Farm".into()), - nip05: None, - about: Some("Farm profile".into()), - website: None, - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - } -} - -fn decimal(raw: &str) -> RadrootsCoreDecimal { - raw.parse().expect("decimal") -} - -fn usd(raw: &str) -> RadrootsCoreMoney { - RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) -} - -fn listing_event_ptr() -> RadrootsNostrEventPtr { - RadrootsNostrEventPtr { - id: event_id_wire('a'), - relays: Some("wss://listing.relay.example".into()), - } -} - -fn public_key(value: String) -> RadrootsPublicKey { - value.parse().expect("public key") -} - -fn event_id(character: char) -> RadrootsEventId { - core::iter::repeat_n(character, 64) - .collect::<String>() - .parse() - .expect("event id") -} - -fn event_id_wire(character: char) -> String { - event_id(character).into_string() -} - -fn sample_order_request(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderRequest { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderRequest { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - items: vec![RadrootsOrderItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - }], - economics: RadrootsOrderEconomics { - quote_id: "quote-1".parse().expect("quote id"), - quote_version: 1, - pricing_basis: RadrootsOrderPricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsOrderEconomicItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - quantity_amount: decimal("1"), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: decimal("5"), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: usd("10"), - }], - discounts: Vec::new(), - adjustments: Vec::new(), - subtotal: usd("10"), - discount_total: usd("0"), - adjustment_total: usd("0"), - total: usd("10"), - }, - } -} - -fn sample_order_decision(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderDecision { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderDecision { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - decision: RadrootsOrderDecisionOutcome::Accepted { - inventory_commitments: vec![RadrootsOrderInventoryCommitment { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - }], - }, - } -} - -fn sample_order_revision_proposal( - buyer_pubkey: String, - seller_pubkey: String, - root_event_id: String, - prev_event_id: String, -) -> RadrootsOrderRevisionProposal { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderRevisionProposal { - revision_id: "revision-1".parse().expect("revision id"), - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - root_event_id: root_event_id.parse().expect("root event id"), - prev_event_id: prev_event_id.parse().expect("previous event id"), - items: vec![RadrootsOrderItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 3, - }], - economics: RadrootsOrderEconomics { - quote_id: "revision-quote-1".parse().expect("revision quote id"), - quote_version: 2, - pricing_basis: RadrootsOrderPricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsOrderEconomicItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 3, - quantity_amount: decimal("1"), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: decimal("5"), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: usd("15"), - }], - discounts: Vec::new(), - adjustments: Vec::new(), - subtotal: usd("15"), - discount_total: usd("0"), - adjustment_total: usd("0"), - total: usd("15"), - }, - reason: "update count".into(), - } -} - -fn sample_order_revision_decision( - proposal: &RadrootsOrderRevisionProposal, - decision: RadrootsOrderRevisionOutcome, -) -> RadrootsOrderRevisionDecision { - RadrootsOrderRevisionDecision { - revision_id: proposal.revision_id.clone(), - order_id: proposal.order_id.clone(), - listing_addr: proposal.listing_addr.clone(), - buyer_pubkey: proposal.buyer_pubkey.clone(), - seller_pubkey: proposal.seller_pubkey.clone(), - root_event_id: proposal.root_event_id.clone(), - prev_event_id: event_id('3'), - decision, - } -} - -fn sample_fulfillment_update( - buyer_pubkey: String, - seller_pubkey: String, -) -> RadrootsOrderFulfillmentUpdate { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderFulfillmentUpdate { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - status: RadrootsOrderFulfillmentState::ReadyForPickup, - } -} - -fn sample_order_cancellation( - buyer_pubkey: String, - seller_pubkey: String, -) -> RadrootsOrderCancellation { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderCancellation { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - reason: "schedule changed".into(), - } -} - -fn sample_buyer_receipt(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderReceipt { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderReceipt { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - received: true, - issue: None, - received_at: 1_785_000_000, - } -} - -fn event_from_parts( - id: &str, - author: &str, - created_at: u32, - parts: WireEventParts, -) -> RadrootsNostrEvent { - RadrootsNostrEvent { - id: id.into(), - author: author.into(), - created_at, - kind: parts.kind, - tags: parts.tags, - content: parts.content, - sig: String::new(), - } -} - -#[test] -fn client_default_config_uses_production_relay_direct() { - let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::default()).expect("sdk client"); - - assert_eq!(client.transport(), SdkTransportMode::RelayDirect); - assert_eq!( - client.resolved_transport_target(), - &SdkResolvedTransportTarget::RelayDirect { - relay_urls: vec![RADROOTS_SDK_PRODUCTION_RELAY_URL.to_string()], - } - ); -} - -#[test] -fn client_rejects_invalid_config_on_construction() { - let mut config = RadrootsSdkConfig::custom(); - config.transport = SdkTransportMode::RelayDirect; - config.relay = RelayConfig { - urls: vec!["https://radroots.org".into()], - }; - - let error = RadrootsSdkClient::from_config(config).expect_err("invalid config"); - assert_eq!( - error, - SdkConfigError::InvalidRelayUrl("https://radroots.org".into()) - ); -} - -#[test] -fn client_rejects_invalid_radrootsd_config_on_construction() { - let mut missing = RadrootsSdkConfig::custom(); - missing.transport = SdkTransportMode::Radrootsd; - - assert_eq!( - RadrootsSdkClient::from_config(missing).expect_err("missing radrootsd endpoint"), - SdkConfigError::MissingCustomRadrootsdEndpoint - ); - - let mut invalid = RadrootsSdkConfig::custom(); - invalid.transport = SdkTransportMode::Radrootsd; - invalid.radrootsd.endpoint = Some("wss://rpc.bad".into()); - - assert_eq!( - RadrootsSdkClient::from_config(invalid).expect_err("invalid radrootsd endpoint"), - SdkConfigError::InvalidRadrootsdEndpoint("wss://rpc.bad".into()) - ); -} - -#[test] -fn client_allows_custom_relay_without_radrootsd_endpoint() { - let mut config = RadrootsSdkConfig::custom(); - config.transport = SdkTransportMode::RelayDirect; - config.relay = RelayConfig { - urls: vec!["wss://radroots.org".into()], - }; - - let client = RadrootsSdkClient::from_config(config).expect("relay-only sdk client"); - assert_eq!( - client.resolved_transport_target(), - &SdkResolvedTransportTarget::RelayDirect { - relay_urls: vec!["wss://radroots.org".to_string()], - } - ); -} - -#[test] -fn client_allows_custom_radrootsd_without_relay_urls() { - let endpoint = "https://custom.radroots.org/jsonrpc"; - let mut config = RadrootsSdkConfig::custom(); - config.transport = SdkTransportMode::Radrootsd; - config.radrootsd.endpoint = Some(endpoint.into()); - - let client = RadrootsSdkClient::from_config(config).expect("radrootsd-only sdk client"); - assert_eq!( - client.resolved_transport_target(), - &SdkResolvedTransportTarget::Radrootsd { - endpoint: endpoint.to_string(), - } - ); -} - -#[test] -fn namespace_clients_reflect_explicit_transport_mode() { - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::LocalIdentity; - - let client = RadrootsSdkClient::from_config(config).expect("sdk client"); - - assert_eq!(client.transport(), SdkTransportMode::Radrootsd); - assert_eq!(client.profile().transport(), SdkTransportMode::Radrootsd); - assert_eq!(client.farm().transport(), SdkTransportMode::Radrootsd); - assert_eq!(client.listing().transport(), SdkTransportMode::Radrootsd); - assert_eq!(client.order().transport(), SdkTransportMode::Radrootsd); - #[cfg(feature = "radrootsd-client")] - assert_eq!(client.radrootsd().transport(), SdkTransportMode::Radrootsd); - assert_eq!(client.signer(), SignerConfig::LocalIdentity); - assert_eq!(client.profile().signer(), SignerConfig::LocalIdentity); - assert_eq!(client.farm().signer(), SignerConfig::LocalIdentity); - assert_eq!(client.listing().signer(), SignerConfig::LocalIdentity); - assert_eq!(client.order().signer(), SignerConfig::LocalIdentity); - #[cfg(feature = "radrootsd-client")] - assert_eq!(client.radrootsd().signer(), SignerConfig::LocalIdentity); -} - -#[test] -fn namespace_clients_expose_parent_sdk_and_draft_facades() { - let client = - RadrootsSdkClient::from_config(RadrootsSdkConfig::production()).expect("sdk client"); - let profile = client.profile(); - let farm = client.farm(); - let listing = client.listing(); - let order = client.order(); - - assert_eq!(client.config().environment, SdkEnvironment::Production); - assert!(std::ptr::eq(profile.sdk(), &client)); - assert!(std::ptr::eq(farm.sdk(), &client)); - assert!(std::ptr::eq(listing.sdk(), &client)); - assert!(std::ptr::eq(order.sdk(), &client)); - - let profile_draft = profile - .build_draft(&sample_profile(), Some(RadrootsProfileType::Farm)) - .expect("profile draft"); - assert_eq!(profile_draft.kind, KIND_PROFILE); - - let farm_draft = farm.build_draft(&sample_farm()).expect("farm draft"); - assert_eq!(farm_draft.kind, KIND_FARM); - - let listing_draft = listing - .build_draft(&sample_listing()) - .expect("listing draft"); - assert_eq!(listing_draft.as_wire_parts().kind, KIND_LISTING); - assert_eq!(listing_draft.into_wire_parts().kind, KIND_LISTING); - - let mut invalid_listing = sample_listing(); - invalid_listing.farm.pubkey.clear(); - assert!(listing.build_draft(&invalid_listing).is_err()); -} - -#[test] -fn listing_and_order_clients_wrap_existing_sdk_facades() { - let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::local()).expect("sdk client"); - let listing_value = sample_listing(); - let buyer_pubkey = "b".repeat(64); - let seller_pubkey = "a".repeat(64); - - let tags = client - .listing() - .build_tags(&listing_value) - .expect("listing tags"); - assert!(!tags.is_empty()); - - let draft = client - .listing() - .build_draft(&listing_value) - .expect("listing draft"); - assert_eq!(draft.as_wire_parts().kind, KIND_LISTING); - - let event = RadrootsNostrEvent { - id: "listing-1".into(), - author: "seller".into(), - created_at: 1, - kind: draft.as_wire_parts().kind, - tags: draft.as_wire_parts().tags.clone(), - content: draft.as_wire_parts().content.clone(), - sig: String::new(), - }; - let parsed = client - .listing() - .parse_event(&event) - .expect("parsed listing"); - assert_eq!(parsed.d_tag, listing_value.d_tag); - - let validated = client - .order() - .validate_listing_event(&event) - .expect("validated listing"); - assert_eq!(validated.listing_id, listing_value.d_tag); - - let listing_addr = format!("{KIND_LISTING}:{seller_pubkey}:{}", listing_value.d_tag); - let payload = sample_order_request(buyer_pubkey.clone(), seller_pubkey.clone()); - let envelope = client - .order() - .build_order_request_draft(&listing_event_ptr(), &payload) - .expect("order draft"); - assert_eq!(envelope.as_wire_parts().kind, KIND_ORDER_REQUEST); - let envelope_event = RadrootsNostrEvent { - id: "order-event-1".into(), - author: buyer_pubkey, - created_at: 2, - kind: envelope.as_wire_parts().kind, - tags: envelope.as_wire_parts().tags.clone(), - content: envelope.as_wire_parts().content.clone(), - sig: String::new(), - }; - assert_eq!( - client - .order() - .parse_order_request(&envelope_event) - .expect("order envelope") - .payload - .order_id, - payload.order_id - ); - let parsed_addr = client - .order() - .parse_listing_address(&listing_addr) - .expect("listing address"); - assert_eq!(parsed_addr.listing_id, listing_value.d_tag); -} - -#[test] -fn order_facades_round_trip_all_draft_types() { - let client = - RadrootsSdkClient::from_config(RadrootsSdkConfig::production()).expect("sdk client"); - let order_client = client.order(); - let buyer_pubkey = "b".repeat(64); - let seller_pubkey = "a".repeat(64); - let root_event_id = event_id('1'); - let decision_event_id = event_id('2'); - let proposal_event_id = event_id('3'); - let fulfillment_event_id = event_id('4'); - - let order_request = sample_order_request(buyer_pubkey.clone(), seller_pubkey.clone()); - let order_draft = order_client - .build_order_request_draft(&listing_event_ptr(), &order_request) - .expect("order request draft"); - assert_eq!(order_draft.as_wire_parts().kind, KIND_ORDER_REQUEST); - let order_event = event_from_parts( - root_event_id.as_str(), - &buyer_pubkey, - 1, - order_draft.clone().into_wire_parts(), - ); - let order_envelope = order_client - .parse_order_request(&order_event) - .expect("order request envelope"); - assert_eq!(order_envelope.payload.economics.total, usd("10")); - - let decision = sample_order_decision(buyer_pubkey.clone(), seller_pubkey.clone()); - let decision_draft = order_client - .build_order_decision_draft(&root_event_id, &root_event_id, &decision) - .expect("order decision draft"); - assert_eq!(decision_draft.as_wire_parts().kind, KIND_ORDER_DECISION); - let decision_event = event_from_parts( - decision_event_id.as_str(), - &seller_pubkey, - 2, - decision_draft.clone().into_wire_parts(), - ); - assert_eq!( - order_client - .parse_order_decision(&decision_event) - .expect("order decision envelope") - .payload - .decision, - decision.decision - ); - - let proposal = sample_order_revision_proposal( - buyer_pubkey.clone(), - seller_pubkey.clone(), - root_event_id.to_string(), - decision_event_id.to_string(), - ); - let proposal_draft = order_client - .build_order_revision_proposal_draft(&root_event_id, &decision_event_id, &proposal) - .expect("revision proposal draft"); - assert_eq!( - proposal_draft.as_wire_parts().kind, - KIND_ORDER_REVISION_PROPOSAL - ); - let proposal_event = event_from_parts( - proposal_event_id.as_str(), - &seller_pubkey, - 3, - proposal_draft.clone().into_wire_parts(), - ); - assert_eq!( - order_client - .parse_order_revision_proposal(&proposal_event) - .expect("revision proposal envelope") - .payload - .economics - .total, - usd("15") - ); - - let revision_decision = - sample_order_revision_decision(&proposal, RadrootsOrderRevisionOutcome::Accepted); - let revision_decision_draft = order_client - .build_order_revision_decision_draft( - &root_event_id, - &revision_decision.prev_event_id, - &revision_decision, - ) - .expect("revision decision draft"); - assert_eq!( - revision_decision_draft.as_wire_parts().kind, - KIND_ORDER_REVISION_DECISION - ); - let revision_decision_event = event_from_parts( - "order-revision-decision-event-1", - &buyer_pubkey, - 4, - revision_decision_draft.clone().into_wire_parts(), - ); - assert_eq!( - order_client - .parse_order_revision_decision(&revision_decision_event) - .expect("revision decision envelope") - .payload - .revision_id, - revision_decision.revision_id - ); - - let fulfillment = sample_fulfillment_update(buyer_pubkey.clone(), seller_pubkey.clone()); - let fulfillment_draft = order_client - .build_fulfillment_update_draft(&root_event_id, &decision_event_id, &fulfillment) - .expect("fulfillment draft"); - assert_eq!( - fulfillment_draft.as_wire_parts().kind, - KIND_ORDER_FULFILLMENT_UPDATE - ); - let fulfillment_event = event_from_parts( - fulfillment_event_id.as_str(), - &seller_pubkey, - 5, - fulfillment_draft.clone().into_wire_parts(), - ); - assert_eq!( - order_client - .parse_fulfillment_update(&fulfillment_event) - .expect("fulfillment envelope") - .payload - .status, - fulfillment.status - ); - - let cancellation = sample_order_cancellation(buyer_pubkey.clone(), seller_pubkey.clone()); - let cancellation_draft = order_client - .build_order_cancellation_draft(&root_event_id, &decision_event_id, &cancellation) - .expect("cancellation draft"); - assert_eq!( - cancellation_draft.as_wire_parts().kind, - KIND_ORDER_CANCELLATION - ); - let cancellation_event = event_from_parts( - "order-cancellation-event-1", - &buyer_pubkey, - 6, - cancellation_draft.clone().into_wire_parts(), - ); - assert_eq!( - order_client - .parse_order_cancellation(&cancellation_event) - .expect("cancellation envelope") - .payload - .reason, - cancellation.reason - ); - - let receipt = sample_buyer_receipt(buyer_pubkey.clone(), seller_pubkey.clone()); - let receipt_draft = order_client - .build_buyer_receipt_draft(&root_event_id, &fulfillment_event_id, &receipt) - .expect("receipt draft"); - assert_eq!(receipt_draft.as_wire_parts().kind, KIND_ORDER_RECEIPT); - let receipt_event = event_from_parts( - "receipt-event-1", - &buyer_pubkey, - 7, - receipt_draft.clone().into_wire_parts(), - ); - assert!( - order_client - .parse_buyer_receipt(&receipt_event) - .expect("receipt envelope") - .payload - .received - ); -} - -#[test] -fn order_draft_facades_return_encoder_errors() { - let client = - RadrootsSdkClient::from_config(RadrootsSdkConfig::production()).expect("sdk client"); - let order = client.order(); - let buyer_pubkey = "b".repeat(64); - let seller_pubkey = "a".repeat(64); - let root_event_id = event_id('1'); - let decision_event_id = event_id('2'); - - let mut invalid_order = sample_order_request(buyer_pubkey.clone(), seller_pubkey.clone()); - invalid_order.items.clear(); - assert!( - order - .build_order_request_draft(&listing_event_ptr(), &invalid_order) - .is_err() - ); - - let mut invalid_decision = sample_order_decision(buyer_pubkey.clone(), seller_pubkey.clone()); - invalid_decision.decision = RadrootsOrderDecisionOutcome::Accepted { - inventory_commitments: Vec::new(), - }; - assert!( - order - .build_order_decision_draft(&root_event_id, &root_event_id, &invalid_decision) - .is_err() - ); - - let proposal = sample_order_revision_proposal( - buyer_pubkey.clone(), - seller_pubkey.clone(), - root_event_id.to_string(), - decision_event_id.to_string(), - ); - let different_root_event_id = event_id('d'); - assert!( - order - .build_order_revision_proposal_draft( - &different_root_event_id, - &decision_event_id, - &proposal, - ) - .is_err() - ); - - let revision_decision = - sample_order_revision_decision(&proposal, RadrootsOrderRevisionOutcome::Accepted); - let different_prev_event_id = event_id('e'); - assert!( - order - .build_order_revision_decision_draft( - &root_event_id, - &different_prev_event_id, - &revision_decision, - ) - .is_err() - ); - - let mut fulfillment = sample_fulfillment_update(buyer_pubkey.clone(), seller_pubkey.clone()); - fulfillment.status = RadrootsOrderFulfillmentState::AcceptedNotFulfilled; - assert!( - order - .build_fulfillment_update_draft(&root_event_id, &decision_event_id, &fulfillment) - .is_err() - ); - - let mut cancellation = sample_order_cancellation(buyer_pubkey.clone(), seller_pubkey.clone()); - cancellation.reason.clear(); - assert!( - order - .build_order_cancellation_draft(&root_event_id, &decision_event_id, &cancellation) - .is_err() - ); - - let mut receipt = sample_buyer_receipt(buyer_pubkey, seller_pubkey); - receipt.received = false; - assert!( - order - .build_buyer_receipt_draft(&root_event_id, &decision_event_id, &receipt) - .is_err() - ); -} - -#[test] -fn publish_receipts_and_errors_format_public_details() { - let receipt = SdkRadrootsdPublishReceipt { - accepted: true, - deduplicated: true, - job_id: Some("job-1".into()), - status: Some("accepted".into()), - signer_mode: Some("secret-mode".into()), - signer_session_id: Some("secret-session".into()), - event_addr: Some("3432:pubkey:order-1".into()), - relay_count: Some(2), - acknowledged_relay_count: Some(1), - }; - let debug = format!("{receipt:?}"); - - assert!(debug.contains("SdkRadrootsdPublishReceipt")); - assert!(debug.contains("<redacted>")); - assert!(!debug.contains("secret-mode")); - assert!(!debug.contains("secret-session")); - - let relay_failure = SdkRelayFailure { - relay_url: "wss://relay.example".into(), - error: "closed".into(), - }; - let formatted = [ - SdkPublishError::from(SdkConfigError::EmptyRelayUrl).to_string(), - SdkPublishError::Encode("encode failed".into()).to_string(), - SdkPublishError::UnsupportedTransport { - transport: SdkTransportMode::Radrootsd, - operation: "order.publish", - } - .to_string(), - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::RelayDirect, - signer: SignerConfig::DraftOnly, - required: SignerConfig::LocalIdentity, - operation: "order.publish", - } - .to_string(), - SdkPublishError::Relay("relay failed".into()).to_string(), - SdkPublishError::RelaySetup { - transport: SdkTransportMode::RelayDirect, - operation: "order.publish", - target_relays: Vec::new(), - error: "setup failed".into(), - } - .to_string(), - SdkPublishError::RelaySetup { - transport: SdkTransportMode::RelayDirect, - operation: "order.publish", - target_relays: vec!["wss://relay.example".into()], - error: "setup failed".into(), - } - .to_string(), - SdkPublishError::RelayNotAcknowledged { - transport: SdkTransportMode::RelayDirect, - failed_relays: Vec::new(), - } - .to_string(), - SdkPublishError::RelayNotAcknowledged { - transport: SdkTransportMode::RelayDirect, - failed_relays: vec![relay_failure], - } - .to_string(), - SdkPublishError::Radrootsd("radrootsd failed".into()).to_string(), - ]; - - assert!( - formatted - .iter() - .any(|message| message == "relay url must not be empty") - ); - assert!(formatted.iter().any(|message| message == "encode failed")); - assert!( - formatted - .iter() - .any(|message| message.contains("requires signer mode `local_identity`")) - ); - assert!(formatted.iter().any(|message| { - message.contains("failed to prepare RelayDirect relay publish for wss://relay.example") - })); - assert!( - formatted - .iter() - .any(|message| message.contains("wss://relay.example: closed")) - ); - assert!( - formatted - .iter() - .any(|message| message == "radrootsd failed") - ); -} - -#[test] -fn farm_client_wraps_existing_farm_facade() { - let client = - RadrootsSdkClient::from_config(RadrootsSdkConfig::production()).expect("sdk client"); - let farm = sample_farm(); - - let draft = client.farm().build_draft(&farm).expect("farm draft"); - assert!(!draft.tags.is_empty()); -} diff --git a/crates/sdk/tests/config.rs b/crates/sdk/tests/config.rs @@ -1,562 +0,0 @@ -use radroots_sdk::{ - NetworkConfig, RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT, RADROOTS_SDK_LOCAL_RELAY_URL, - RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT, RADROOTS_SDK_PRODUCTION_RELAY_URL, - RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT, RADROOTS_SDK_STAGING_RELAY_URL, RadrootsSdkConfig, - RadrootsdAuth, RelayConfig, SdkConfigError, SdkEnvironment, SdkTransportMode, SignerConfig, -}; -use std::{ - ffi::OsString, - sync::{Mutex, OnceLock}, -}; - -const LOCAL_SDK_ENV_KEYS: &[&str] = &[ - "NOSTR_RS_RELAY_PUBLIC_SCHEME", - "NOSTR_RS_RELAY_PUBLIC_HOST", - "NOSTR_RS_RELAY_PUBLIC_PORT", - "RADROOTSD_RPC_URL", - "RADROOTSD_RPC_HOST", - "RADROOTSD_RPC_PORT", -]; - -fn sdk_env_lock() -> &'static Mutex<()> { - static LOCK: OnceLock<Mutex<()>> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) -} - -struct LocalSdkEnvRestore { - saved: Vec<(&'static str, Option<OsString>)>, -} - -impl LocalSdkEnvRestore { - fn apply(pairs: &[(&str, &str)]) -> Self { - let saved = LOCAL_SDK_ENV_KEYS - .iter() - .map(|key| (*key, std::env::var_os(key))) - .collect::<Vec<_>>(); - - for key in LOCAL_SDK_ENV_KEYS { - unsafe { - std::env::remove_var(key); - } - } - for (key, value) in pairs { - assert!( - LOCAL_SDK_ENV_KEYS.contains(key), - "unexpected local sdk env key `{key}`" - ); - unsafe { - std::env::set_var(key, value); - } - } - - Self { saved } - } -} - -impl Drop for LocalSdkEnvRestore { - fn drop(&mut self) { - for (key, original) in self.saved.drain(..) { - match original { - Some(value) => unsafe { - std::env::set_var(key, value); - }, - None => unsafe { - std::env::remove_var(key); - }, - } - } - } -} - -struct EnvKeyRestore { - key: &'static str, - saved: Option<OsString>, -} - -impl EnvKeyRestore { - fn capture(key: &'static str) -> Self { - Self { - key, - saved: std::env::var_os(key), - } - } -} - -impl Drop for EnvKeyRestore { - fn drop(&mut self) { - match &self.saved { - Some(value) => unsafe { - std::env::set_var(self.key, value); - }, - None => unsafe { - std::env::remove_var(self.key); - }, - } - } -} - -fn with_local_sdk_env<F>(pairs: &[(&str, &str)], test: F) -where - F: FnOnce(), -{ - let _guard = sdk_env_lock().lock().expect("sdk env lock"); - let _env_restore = LocalSdkEnvRestore::apply(pairs); - - test(); -} - -#[test] -fn local_sdk_env_restore_preserves_original_os_string_values() { - let _guard = sdk_env_lock().lock().expect("sdk env lock"); - let key = "NOSTR_RS_RELAY_PUBLIC_HOST"; - let _restore_key = EnvKeyRestore::capture(key); - let original = OsString::from("relay.before.example"); - - unsafe { - std::env::set_var(key, &original); - } - - { - let _env_restore = LocalSdkEnvRestore::apply(&[("RADROOTSD_RPC_PORT", "18080")]); - - assert_eq!(std::env::var_os(key), None); - } - - assert_eq!(std::env::var_os(key), Some(original)); -} - -#[test] -fn env_key_restore_restores_existing_value() { - let _guard = sdk_env_lock().lock().expect("sdk env lock"); - let key = "NOSTR_RS_RELAY_PUBLIC_HOST"; - let _restore_outer = EnvKeyRestore::capture(key); - let original = OsString::from("relay.before.example"); - let changed = OsString::from("relay.changed.example"); - - unsafe { - std::env::set_var(key, &original); - } - - { - let _restore_inner = EnvKeyRestore::capture(key); - - unsafe { - std::env::set_var(key, &changed); - } - } - - assert_eq!(std::env::var_os(key), Some(original)); -} - -#[cfg(unix)] -#[test] -fn local_sdk_env_restore_preserves_non_unicode_original_values() { - use std::os::unix::ffi::OsStringExt; - - let _guard = sdk_env_lock().lock().expect("sdk env lock"); - let key = "NOSTR_RS_RELAY_PUBLIC_HOST"; - let _restore_key = EnvKeyRestore::capture(key); - let original = OsString::from_vec(vec![b'r', b'e', b'l', b'a', b'y', 0x80]); - - unsafe { - std::env::set_var(key, &original); - } - - { - let _env_restore = LocalSdkEnvRestore::apply(&[("RADROOTSD_RPC_PORT", "18080")]); - - assert_eq!(std::env::var_os(key), None); - } - - assert_eq!(std::env::var_os(key), Some(original)); -} - -#[test] -fn default_config_uses_production_relay_direct_draft_only() { - let config = RadrootsSdkConfig::default(); - - assert_eq!(config.environment, SdkEnvironment::Production); - assert_eq!(config.transport, SdkTransportMode::RelayDirect); - assert_eq!(config.signer, SignerConfig::DraftOnly); - assert_eq!(config.network, NetworkConfig::default()); - assert_eq!(config.radrootsd.auth, RadrootsdAuth::None); -} - -#[test] -fn production_environment_resolves_radroots_org_defaults() { - let config = RadrootsSdkConfig::production(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec![RADROOTS_SDK_PRODUCTION_RELAY_URL.to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("radrootsd endpoint"), - RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT - ); -} - -#[test] -fn staging_environment_resolves_staging_defaults() { - let config = RadrootsSdkConfig::staging(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec![RADROOTS_SDK_STAGING_RELAY_URL.to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("radrootsd endpoint"), - RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT - ); -} - -#[test] -fn local_environment_resolves_localhost_defaults() { - with_local_sdk_env(&[], || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec![RADROOTS_SDK_LOCAL_RELAY_URL.to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("radrootsd endpoint"), - RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT - ); - }); -} - -#[test] -fn local_environment_prefers_root_env_contract_when_present() { - with_local_sdk_env( - &[ - ("NOSTR_RS_RELAY_PUBLIC_SCHEME", "ws"), - ("NOSTR_RS_RELAY_PUBLIC_HOST", "127.0.0.1"), - ("NOSTR_RS_RELAY_PUBLIC_PORT", "18080"), - ("RADROOTSD_RPC_URL", "http://127.0.0.1:17070/jsonrpc"), - ], - || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec!["ws://127.0.0.1:18080".to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("radrootsd endpoint"), - "http://127.0.0.1:17070/jsonrpc" - ); - }, - ); -} - -#[test] -fn local_environment_ignores_partial_or_blank_env_contracts() { - with_local_sdk_env( - &[ - ("NOSTR_RS_RELAY_PUBLIC_SCHEME", "ws"), - ("NOSTR_RS_RELAY_PUBLIC_HOST", " "), - ("NOSTR_RS_RELAY_PUBLIC_PORT", "18080"), - ("RADROOTSD_RPC_HOST", "127.0.0.1"), - ], - || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec![RADROOTS_SDK_LOCAL_RELAY_URL.to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("radrootsd endpoint"), - RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT - ); - }, - ); -} - -#[test] -fn local_environment_handles_invalid_and_missing_relay_port_env() { - with_local_sdk_env( - &[ - ("NOSTR_RS_RELAY_PUBLIC_SCHEME", "http"), - ("NOSTR_RS_RELAY_PUBLIC_HOST", "127.0.0.1"), - ("NOSTR_RS_RELAY_PUBLIC_PORT", "18080"), - ], - || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config.resolved_relay_urls().expect_err("invalid relay env"), - SdkConfigError::InvalidRelayUrl("http://127.0.0.1:18080".to_owned()) - ); - }, - ); - - with_local_sdk_env( - &[ - ("NOSTR_RS_RELAY_PUBLIC_SCHEME", "ws"), - ("NOSTR_RS_RELAY_PUBLIC_HOST", "127.0.0.1"), - ], - || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec![RADROOTS_SDK_LOCAL_RELAY_URL.to_owned()] - ); - }, - ); -} - -#[test] -fn local_environment_builds_radrootsd_endpoint_from_host_port_env() { - with_local_sdk_env( - &[ - ("RADROOTSD_RPC_HOST", "127.0.0.1"), - ("RADROOTSD_RPC_PORT", "17070"), - ], - || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("host port endpoint"), - "http://127.0.0.1:17070" - ); - }, - ); -} - -#[test] -fn explicit_coordinates_override_environment_defaults_exactly() { - let mut config = RadrootsSdkConfig::production(); - config.relay.urls = vec![ - " wss://relay.custom.one ".to_owned(), - "wss://relay.custom.one".to_owned(), - "ws://relay.custom.two".to_owned(), - ]; - config.radrootsd.endpoint = Some(" https://rpc.custom.radroots.org ".to_owned()); - - assert_eq!( - config.resolved_relay_urls().expect("relay overrides"), - vec![ - "wss://relay.custom.one".to_owned(), - "ws://relay.custom.two".to_owned(), - ] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("endpoint override"), - "https://rpc.custom.radroots.org" - ); -} - -#[test] -fn custom_environment_requires_explicit_coordinates() { - let config = RadrootsSdkConfig::custom(); - - assert_eq!( - config - .resolved_relay_urls() - .expect_err("custom relay error"), - SdkConfigError::MissingCustomRelayUrls - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect_err("custom radrootsd error"), - SdkConfigError::MissingCustomRadrootsdEndpoint - ); -} - -#[test] -fn custom_environment_accepts_explicit_coordinates() { - let mut config = RadrootsSdkConfig::custom(); - config.relay.urls = vec!["wss://relay.custom.radroots.org".to_owned()]; - config.radrootsd.endpoint = Some("https://rpc.custom.radroots.org".to_owned()); - - assert_eq!( - config.resolved_relay_urls().expect("custom relay"), - vec!["wss://relay.custom.radroots.org".to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("custom endpoint"), - "https://rpc.custom.radroots.org" - ); -} - -#[test] -fn empty_coordinate_values_fail_loudly() { - let mut config = RadrootsSdkConfig::production(); - config.relay = RelayConfig { - urls: vec![" ".to_owned()], - }; - config.radrootsd.endpoint = Some(" ".to_owned()); - - assert_eq!( - config.resolved_relay_urls().expect_err("empty relay"), - SdkConfigError::EmptyRelayUrl - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect_err("empty radrootsd endpoint"), - SdkConfigError::EmptyRadrootsdEndpoint - ); -} - -#[test] -fn invalid_coordinate_schemes_fail_loudly() { - let mut config = RadrootsSdkConfig::production(); - config.relay.urls = vec!["https://relay.bad".to_owned()]; - config.radrootsd.endpoint = Some("wss://rpc.bad".to_owned()); - - assert_eq!( - config - .resolved_relay_urls() - .expect_err("relay scheme error"), - SdkConfigError::InvalidRelayUrl("https://relay.bad".to_owned()) - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect_err("endpoint scheme error"), - SdkConfigError::InvalidRadrootsdEndpoint("wss://rpc.bad".to_owned()) - ); -} - -#[test] -fn invalid_relay_authorities_fail_loudly() { - let invalid_relays = [ - "wss://", - "wss:///relay", - "ws://:8080", - "wss://relay.example:", - "wss://relay example", - "wss://user@relay.example", - "wss://relay.example:abc", - "wss://2001:db8::1", - ]; - - for relay_url in invalid_relays { - let mut config = RadrootsSdkConfig::production(); - config.relay.urls = vec![relay_url.to_owned()]; - - assert_eq!( - config - .resolved_relay_urls() - .expect_err("relay authority error"), - SdkConfigError::InvalidRelayUrl(relay_url.to_owned()) - ); - } -} - -#[test] -fn invalid_bracketed_relay_authorities_fail_loudly() { - let invalid_relays = [ - "wss://[2001:db8::1", - "wss://[]:443", - "wss://[2001:db8::1]suffix", - "wss://[2001:db8::1]:abc", - ]; - - for relay_url in invalid_relays { - let mut config = RadrootsSdkConfig::production(); - config.relay.urls = vec![relay_url.to_owned()]; - - assert_eq!( - config - .resolved_relay_urls() - .expect_err("bracketed relay authority error"), - SdkConfigError::InvalidRelayUrl(relay_url.to_owned()) - ); - } -} - -#[test] -fn valid_relay_authorities_still_resolve() { - let mut config = RadrootsSdkConfig::production(); - config.relay.urls = vec![ - " wss://relay.example/nostr ".to_owned(), - "ws://127.0.0.1:8080".to_owned(), - "wss://[2001:db8::1]/relay".to_owned(), - "wss://[2001:db8::1]:443/relay".to_owned(), - ]; - - assert_eq!( - config.resolved_relay_urls().expect("valid relays"), - vec![ - "wss://relay.example/nostr".to_owned(), - "ws://127.0.0.1:8080".to_owned(), - "wss://[2001:db8::1]/relay".to_owned(), - "wss://[2001:db8::1]:443/relay".to_owned() - ] - ); -} - -#[test] -fn signer_modes_format_as_config_tokens() { - assert_eq!(SignerConfig::DraftOnly.to_string(), "draft_only"); - assert_eq!(SignerConfig::LocalIdentity.to_string(), "local_identity"); - assert_eq!(SignerConfig::Nip46.to_string(), "nip46"); -} - -#[test] -fn config_errors_format_operator_facing_messages() { - let formatted = [ - SdkConfigError::MissingCustomRelayUrls.to_string(), - SdkConfigError::MissingCustomRadrootsdEndpoint.to_string(), - SdkConfigError::EmptyRelayUrl.to_string(), - SdkConfigError::InvalidRelayUrl("http://relay.example".into()).to_string(), - SdkConfigError::EmptyRadrootsdEndpoint.to_string(), - SdkConfigError::InvalidRadrootsdEndpoint("ws://rpc.example".into()).to_string(), - ]; - - assert_eq!( - formatted, - [ - "custom sdk environment requires explicit relay urls", - "custom sdk environment requires an explicit radrootsd endpoint", - "relay url must not be empty", - "relay url must use ws or wss, got `http://relay.example`", - "radrootsd endpoint must not be empty", - "radrootsd endpoint must use http or https, got `ws://rpc.example`", - ] - ); -} - -#[test] -fn radrootsd_auth_debug_formats_none_and_redacts_bearer_tokens() { - assert_eq!(format!("{:?}", RadrootsdAuth::None), "None"); - - let bearer = RadrootsdAuth::BearerToken("sdk-secret-token".to_owned()); - let debug = format!("{bearer:?}"); - - assert!(!debug.contains("sdk-secret-token")); - assert_eq!(debug, "BearerToken(\"<redacted>\")"); -} - -#[test] -fn sdk_config_debug_redacts_bearer_tokens() { - let mut config = RadrootsSdkConfig::production(); - config.radrootsd.auth = RadrootsdAuth::BearerToken("sdk-secret-token".to_owned()); - - let debug = format!("{config:?}"); - - assert!(!debug.contains("sdk-secret-token")); - assert!(debug.contains("BearerToken(\"<redacted>\")")); -} diff --git a/crates/sdk/tests/facade.rs b/crates/sdk/tests/facade.rs @@ -1,269 +0,0 @@ -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, RadrootsCoreUnit, -}; -use radroots_events::farm::{RadrootsFarm, RadrootsFarmRef}; -use radroots_events::ids::RadrootsPublicKey; -use radroots_events::kinds::{KIND_FARM, KIND_LISTING, KIND_ORDER_REQUEST, KIND_PROFILE}; -use radroots_events::listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, -}; -use radroots_events::order::{ - RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderItem, - RadrootsOrderPricingBasis, RadrootsOrderRequest, -}; -use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; -use radroots_sdk::{RadrootsNostrEvent, RadrootsNostrEventPtr, farm, listing, order, profile}; - -fn sample_profile() -> RadrootsProfile { - RadrootsProfile { - name: "North Farm".into(), - display_name: Some("North Farm".into()), - nip05: None, - about: Some("Organic coffee".into()), - website: Some("https://example.com".into()), - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - } -} - -fn sample_farm() -> RadrootsFarm { - RadrootsFarm { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - name: "North Farm".into(), - about: Some("Organic coffee".into()), - website: None, - picture: None, - banner: None, - location: None, - tags: Some(vec!["coffee".into()]), - } -} - -fn sample_listing() -> RadrootsListing { - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "seller".into(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - }, - product: RadrootsListingProduct { - key: "coffee".into(), - title: "Coffee".into(), - category: "coffee".into(), - summary: Some("Single origin coffee".into()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".parse().expect("primary bin id"), - bins: vec![RadrootsListingBin { - bin_id: "bin-1".parse().expect("bin id"), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1000u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice { - amount: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(20u32), - RadrootsCoreCurrency::USD, - ), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - }, - display_amount: None, - display_unit: None, - display_label: None, - display_price: None, - display_price_unit: None, - }], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(RadrootsCoreDecimal::from(5u32)), - availability: Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Active, - }), - delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), - location: Some(RadrootsListingLocation { - primary: "North Farm".into(), - city: None, - region: None, - country: None, - lat: None, - lng: None, - geohash: None, - }), - images: None, - } -} - -fn listing_event(listing_value: &RadrootsListing) -> RadrootsNostrEvent { - let parts = listing::build_draft(listing_value).expect("listing draft"); - RadrootsNostrEvent { - id: "event-1".into(), - author: "seller".into(), - created_at: 1, - kind: parts.as_wire_parts().kind, - tags: parts.as_wire_parts().tags.clone(), - content: parts.as_wire_parts().content.clone(), - sig: String::new(), - } -} - -fn listing_event_ptr() -> RadrootsNostrEventPtr { - RadrootsNostrEventPtr { - id: core::iter::repeat_n('a', 64).collect(), - relays: Some("wss://listing.relay.example".into()), - } -} - -fn public_key(character: char) -> RadrootsPublicKey { - core::iter::repeat_n(character, 64) - .collect::<String>() - .parse() - .expect("public key") -} - -fn sample_order_request() -> RadrootsOrderRequest { - let seller_pubkey = public_key('a'); - - RadrootsOrderRequest { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey: public_key('b'), - seller_pubkey, - items: vec![RadrootsOrderItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - }], - economics: RadrootsOrderEconomics { - quote_id: "quote-1".parse().expect("quote id"), - quote_version: 1, - pricing_basis: RadrootsOrderPricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsOrderEconomicItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - quantity_amount: RadrootsCoreDecimal::from(1u32), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: RadrootsCoreDecimal::from(5u32), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(10u32), - RadrootsCoreCurrency::USD, - ), - }], - discounts: Vec::new(), - adjustments: Vec::new(), - subtotal: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(10u32), - RadrootsCoreCurrency::USD, - ), - discount_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - adjustment_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(10u32), - RadrootsCoreCurrency::USD, - ), - }, - } -} - -#[test] -fn profile_build_draft_wraps_profile_encoder() { - let parts = - profile::build_draft(&sample_profile(), Some(RadrootsProfileType::Farm)).expect("profile"); - - assert_eq!(parts.kind, KIND_PROFILE); - assert!(parts.tags.iter().any(|tag| { - tag.first().map(|value| value.as_str()) == Some("t") - && tag.get(1).map(|value| value.as_str()) == Some("radroots:type:farm") - })); -} - -#[test] -fn farm_build_draft_wraps_farm_encoder() { - let parts = farm::build_draft(&sample_farm()).expect("farm"); - - assert_eq!(parts.kind, KIND_FARM); - assert!( - parts - .tags - .iter() - .any(|tag| tag.first().map(|value| value.as_str()) == Some("d")) - ); -} - -#[test] -fn listing_facade_wraps_build_parse_and_validate() { - let listing_value = sample_listing(); - let tags = listing::build_tags(&listing_value).expect("listing tags"); - assert!(!tags.is_empty()); - - let event = listing_event(&listing_value); - let parsed = listing::parse_event(&event).expect("parsed listing"); - assert_eq!(parsed.d_tag, listing_value.d_tag); - - let validated = order::validate_listing_event(&event).expect("validated listing"); - assert_eq!(validated.listing_id, listing_value.d_tag); - assert_eq!(event.kind, KIND_LISTING); -} - -#[test] -fn listing_parse_rejects_non_listing_kind() { - let listing_value = sample_listing(); - let mut event = listing_event(&listing_value); - event.kind = KIND_PROFILE; - - assert_eq!( - listing::parse_event(&event).expect_err("listing kind error"), - listing::RadrootsListingParseError::InvalidKind(KIND_PROFILE) - ); -} - -#[test] -fn order_facade_wraps_build_parse_and_address_ops() { - let listing_value = sample_listing(); - let seller_pubkey = "a".repeat(64); - let listing_addr = format!("{KIND_LISTING}:{seller_pubkey}:{}", listing_value.d_tag); - let payload = sample_order_request(); - let parts = - order::build_order_request_draft(&listing_event_ptr(), &payload).expect("order draft"); - - assert_eq!(parts.as_wire_parts().kind, KIND_ORDER_REQUEST); - - let parsed_addr = order::parse_listing_address(&listing_addr).expect("listing address"); - assert_eq!(parsed_addr.listing_id, listing_value.d_tag); - - let event = RadrootsNostrEvent { - id: core::iter::repeat_n('b', 64).collect(), - author: payload.buyer_pubkey.to_string(), - created_at: 2, - kind: parts.as_wire_parts().kind, - tags: parts.as_wire_parts().tags.clone(), - content: parts.as_wire_parts().content.clone(), - sig: String::new(), - }; - let envelope = order::parse_order_request(&event).expect("order envelope"); - assert_eq!(envelope.payload.order_id, payload.order_id); - assert_eq!(envelope.payload.listing_addr, listing_addr); -} diff --git a/crates/sdk/tests/radrootsd.rs b/crates/sdk/tests/radrootsd.rs @@ -1,1952 +0,0 @@ -#![cfg(feature = "radrootsd-client")] - -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, RadrootsCoreUnit, -}; -use radroots_events::farm::{RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef}; -use radroots_events::ids::RadrootsPublicKey; -use radroots_events::kinds::{ - KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_ORDER_REQUEST, KIND_PROFILE, -}; -use radroots_sdk::adapters::radrootsd::{ - SdkRadrootsdBridgeJob, SdkRadrootsdBridgePublishResponse, SdkRadrootsdListingPublishRequest, - SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest, - SdkRadrootsdSignerSessionMode, -}; -use radroots_sdk::listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingParseError, - RadrootsListingProduct, RadrootsListingStatus, -}; -use radroots_sdk::order::{ - RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics, - RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest, -}; -use radroots_sdk::{ - RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsProfile, RadrootsProfileType, - RadrootsSdkClient, RadrootsSdkConfig, RadrootsdAuth, RadrootsdConfig, SdkConfigError, - SdkEnvironment, SdkPublishError, SdkRadrootsdBridgeDeliveryPolicy, SdkRadrootsdBridgeError, - SdkRadrootsdBridgeJobStatus, SdkRadrootsdFarmPublishOptions, SdkRadrootsdListingPublishOptions, - SdkRadrootsdOrderRequestPublishOptions, SdkRadrootsdProfilePublishOptions, - SdkRadrootsdPublishReceipt, SdkRadrootsdSessionError, SdkRadrootsdSignerSessionHandle, - SdkRadrootsdSignerSessionRole, SdkRadrootsdSignerSessionView, SdkTransportMode, - SdkTransportReceipt, SignerConfig, -}; -use serde_json::{Value, json}; -use std::collections::VecDeque; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpListener; -use tokio::sync::{mpsc, oneshot}; - -type TestResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>; - -struct JsonRpcServer { - endpoint: String, - shutdown_tx: Option<oneshot::Sender<()>>, -} - -impl JsonRpcServer { - async fn spawn( - expected_auth: Option<&str>, - response_body: Value, - ) -> TestResult<(Self, oneshot::Receiver<Value>)> { - let listener = TcpListener::bind("127.0.0.1:0").await?; - let addr = listener.local_addr()?; - let endpoint = format!("http://{addr}/jsonrpc"); - let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); - let (request_tx, request_rx) = oneshot::channel(); - let expected_auth = expected_auth.map(str::to_owned); - let response_text = response_body.to_string(); - - tokio::spawn(async move { - loop { - tokio::select! { - _ = &mut shutdown_rx => break, - accept = listener.accept() => { - let Ok((mut stream, _)) = accept else { - break; - }; - let mut buffer = Vec::new(); - let mut chunk = [0_u8; 4096]; - let header_end = loop { - let Ok(read) = stream.read(&mut chunk).await else { - return; - }; - if read == 0 { - return; - } - buffer.extend_from_slice(&chunk[..read]); - if let Some(index) = find_headers_end(&buffer) { - break index; - } - }; - - let headers = String::from_utf8_lossy(&buffer[..header_end]).into_owned(); - let content_length = parse_content_length(headers.as_str()).unwrap_or(0); - let body_start = header_end + 4; - while buffer.len().saturating_sub(body_start) < content_length { - let Ok(read) = stream.read(&mut chunk).await else { - return; - }; - if read == 0 { - break; - } - buffer.extend_from_slice(&chunk[..read]); - } - - if let Some(expected_auth) = expected_auth.as_deref() { - let actual_auth = parse_authorization(headers.as_str()); - if actual_auth.as_deref() != Some(expected_auth) { - let _ = write_http_response( - &mut stream, - 401, - json!({ - "jsonrpc": "2.0", - "id": "sdk-test", - "error": { - "code": -32001, - "message": format!( - "unexpected authorization header: {:?}", - actual_auth - ), - } - }) - .to_string() - .as_str(), - ) - .await; - return; - } - } - - let body = &buffer[body_start..body_start + content_length]; - let Ok(request_json) = serde_json::from_slice::<Value>(body) else { - return; - }; - let _ = request_tx.send(request_json); - let _ = write_http_response(&mut stream, 200, response_text.as_str()).await; - break; - } - } - } - }); - - Ok(( - Self { - endpoint, - shutdown_tx: Some(shutdown_tx), - }, - request_rx, - )) - } - - fn endpoint(&self) -> &str { - self.endpoint.as_str() - } -} - -struct JsonRpcSequenceServer { - endpoint: String, - shutdown_tx: Option<oneshot::Sender<()>>, -} - -impl JsonRpcSequenceServer { - async fn spawn( - expected_auth: Option<&str>, - response_bodies: Vec<Value>, - ) -> TestResult<(Self, mpsc::UnboundedReceiver<Value>)> { - let listener = TcpListener::bind("127.0.0.1:0").await?; - let addr = listener.local_addr()?; - let endpoint = format!("http://{addr}/jsonrpc"); - let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); - let (request_tx, request_rx) = mpsc::unbounded_channel(); - let expected_auth = expected_auth.map(str::to_owned); - let mut response_texts = response_bodies - .into_iter() - .map(|value| value.to_string()) - .collect::<VecDeque<_>>(); - - tokio::spawn(async move { - loop { - if response_texts.is_empty() { - break; - } - - tokio::select! { - _ = &mut shutdown_rx => break, - accept = listener.accept() => { - let Ok((mut stream, _)) = accept else { - break; - }; - let mut buffer = Vec::new(); - let mut chunk = [0_u8; 4096]; - let header_end = loop { - let Ok(read) = stream.read(&mut chunk).await else { - return; - }; - if read == 0 { - return; - } - buffer.extend_from_slice(&chunk[..read]); - if let Some(index) = find_headers_end(&buffer) { - break index; - } - }; - - let headers = String::from_utf8_lossy(&buffer[..header_end]).into_owned(); - let content_length = parse_content_length(headers.as_str()).unwrap_or(0); - let body_start = header_end + 4; - while buffer.len().saturating_sub(body_start) < content_length { - let Ok(read) = stream.read(&mut chunk).await else { - return; - }; - if read == 0 { - break; - } - buffer.extend_from_slice(&chunk[..read]); - } - - if let Some(expected_auth) = expected_auth.as_deref() { - let actual_auth = parse_authorization(headers.as_str()); - if actual_auth.as_deref() != Some(expected_auth) { - let _ = write_http_response( - &mut stream, - 401, - json!({ - "jsonrpc": "2.0", - "id": "sdk-test", - "error": { - "code": -32001, - "message": format!( - "unexpected authorization header: {:?}", - actual_auth - ), - } - }) - .to_string() - .as_str(), - ) - .await; - return; - } - } - - let body = &buffer[body_start..body_start + content_length]; - let Ok(request_json) = serde_json::from_slice::<Value>(body) else { - return; - }; - let _ = request_tx.send(request_json); - let Some(response_text) = response_texts.pop_front() else { - return; - }; - let _ = write_http_response(&mut stream, 200, response_text.as_str()).await; - } - } - } - }); - - Ok(( - Self { - endpoint, - shutdown_tx: Some(shutdown_tx), - }, - request_rx, - )) - } - - fn endpoint(&self) -> &str { - self.endpoint.as_str() - } -} - -impl Drop for JsonRpcSequenceServer { - fn drop(&mut self) { - if let Some(shutdown_tx) = self.shutdown_tx.take() { - let _ = shutdown_tx.send(()); - } - } -} - -impl Drop for JsonRpcServer { - fn drop(&mut self) { - if let Some(shutdown_tx) = self.shutdown_tx.take() { - let _ = shutdown_tx.send(()); - } - } -} - -fn find_headers_end(buffer: &[u8]) -> Option<usize> { - buffer.windows(4).position(|window| window == b"\r\n\r\n") -} - -fn parse_content_length(headers: &str) -> Option<usize> { - headers.lines().find_map(|line| { - let (name, value) = line.split_once(':')?; - if !name.eq_ignore_ascii_case("content-length") { - return None; - } - value.trim().parse().ok() - }) -} - -fn parse_authorization(headers: &str) -> Option<String> { - headers.lines().find_map(|line| { - let (name, value) = line.split_once(':')?; - if !name.eq_ignore_ascii_case("authorization") { - return None; - } - Some(value.trim().to_owned()) - }) -} - -async fn write_http_response( - stream: &mut tokio::net::TcpStream, - status: u16, - body: &str, -) -> Result<(), std::io::Error> { - let status_text = match status { - 200 => "OK", - 401 => "Unauthorized", - _ => "Internal Server Error", - }; - let response = format!( - "HTTP/1.1 {status} {status_text}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", - body.len(), - body - ); - stream.write_all(response.as_bytes()).await -} - -fn sample_listing() -> RadrootsListing { - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "seller".into(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - }, - product: RadrootsListingProduct { - key: "coffee".into(), - title: "Coffee".into(), - category: "coffee".into(), - summary: Some("Single origin coffee".into()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".parse().expect("primary bin id"), - bins: vec![RadrootsListingBin { - bin_id: "bin-1".parse().expect("bin id"), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1000u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice { - amount: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(20u32), - RadrootsCoreCurrency::USD, - ), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - }, - display_amount: None, - display_unit: None, - display_label: None, - display_price: None, - display_price_unit: None, - }], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(RadrootsCoreDecimal::from(5u32)), - availability: Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Active, - }), - delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), - location: Some(RadrootsListingLocation { - primary: "North Farm".into(), - city: None, - region: None, - country: None, - lat: None, - lng: None, - geohash: None, - }), - images: None, - } -} - -fn sample_profile() -> RadrootsProfile { - RadrootsProfile { - name: "North Farm".into(), - display_name: Some("North Farm".into()), - nip05: None, - about: Some("Coffee farm".into()), - website: Some("https://example.invalid/north-farm".into()), - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - } -} - -fn sample_farm() -> RadrootsFarm { - RadrootsFarm { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - name: "North Farm".into(), - about: Some("Coffee farm".into()), - website: Some("https://example.invalid/north-farm".into()), - picture: None, - banner: None, - location: Some(RadrootsFarmLocation { - primary: Some("North Farm".into()), - city: Some("San Francisco".into()), - region: Some("CA".into()), - country: Some("US".into()), - gcs: None, - }), - tags: Some(vec!["coffee".into()]), - } -} - -fn sample_order_request_economics() -> RadrootsOrderEconomics { - RadrootsOrderEconomics { - quote_id: "quote-1".parse().expect("quote id"), - quote_version: 1, - pricing_basis: RadrootsOrderPricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsOrderEconomicItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - quantity_amount: RadrootsCoreDecimal::from(1u32), - quantity_unit: RadrootsCoreUnit::MassG, - unit_price_amount: RadrootsCoreDecimal::from(20u32), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(40u32), - RadrootsCoreCurrency::USD, - ), - }], - discounts: Vec::<RadrootsOrderEconomicLine>::new(), - adjustments: Vec::<RadrootsOrderEconomicLine>::new(), - subtotal: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(40u32), - RadrootsCoreCurrency::USD, - ), - discount_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - adjustment_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - total: RadrootsCoreMoney::new(RadrootsCoreDecimal::from(40u32), RadrootsCoreCurrency::USD), - } -} - -fn sample_order_request() -> RadrootsOrderRequest { - let seller_pubkey: RadrootsPublicKey = "a".repeat(64).parse().expect("seller public key"); - - RadrootsOrderRequest { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey: "b".repeat(64).parse().expect("buyer public key"), - seller_pubkey, - items: vec![RadrootsOrderItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - }], - economics: sample_order_request_economics(), - } -} - -fn listing_event_ptr_with_relays(relays: Option<&str>) -> RadrootsNostrEventPtr { - RadrootsNostrEventPtr { - id: "a".repeat(64), - relays: relays.map(str::to_owned), - } -} - -fn sdk_event( - author: &str, - created_at: u32, - draft: radroots_sdk::listing::RadrootsListingDraft, -) -> RadrootsNostrEvent { - let parts = draft.into_wire_parts(); - RadrootsNostrEvent { - id: "event-1".to_owned(), - author: author.to_owned(), - created_at, - kind: parts.kind, - tags: parts.tags, - content: parts.content, - sig: "f".repeat(128), - } -} - -fn radrootsd_test_client(endpoint: &str) -> Result<RadrootsSdkClient, SdkConfigError> { - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::Nip46; - config.radrootsd = RadrootsdConfig { - endpoint: Some(endpoint.to_owned()), - auth: RadrootsdAuth::BearerToken("sdk-secret".to_owned()), - }; - RadrootsSdkClient::from_config(config) -} - -fn sample_session_view_json(session_id: &str) -> Value { - json!({ - "session_id": session_id, - "role": "outbound_remote_signer", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "user_pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "relays": ["wss://radroots.org"], - "permissions": ["sign_event:30402"], - "name": "Radroots Signer", - "url": "https://radroots.org/signers/demo", - "image": "https://radroots.org/signers/demo.png", - "auth_required": false, - "authorized": true, - "auth_url": null, - "expires_in_secs": 120, - "signer_authority": { - "provider_runtime_id": "runtime-1", - "account_identity_id": "identity-1", - "provider_signer_session_id": "provider-session-123" - } - }) -} - -fn sample_bridge_status_json() -> Value { - json!({ - "enabled": true, - "ready": true, - "auth_mode": "bearer_token", - "signer_mode": "selectable_per_request", - "default_signer_mode": "embedded_service_identity", - "supported_signer_modes": ["embedded_service_identity", "nip46_session"], - "available_nip46_signer_sessions": 2, - "relay_count": 1, - "delivery_policy": "quorum", - "delivery_quorum": 1, - "publish_max_attempts": 3, - "publish_initial_backoff_millis": 250, - "publish_max_backoff_millis": 4000, - "job_status_retention": 64, - "retained_jobs": 4, - "retained_idempotency_keys": 2, - "accepted_jobs": 1, - "published_jobs": 2, - "failed_jobs": 1, - "recovered_failed_jobs": 0, - "methods": ["bridge.status", "bridge.job.status", "bridge.job.list", "bridge.listing.publish"] - }) -} - -fn sample_bridge_job_json(job_id: &str) -> Value { - sample_bridge_job_json_for(job_id, "bridge.listing.publish", 30402) -} - -fn sample_bridge_job_json_for(job_id: &str, command: &str, event_kind: u32) -> Value { - json!({ - "job_id": job_id, - "command": command, - "idempotency_key": "idem-bridge-1", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "requested_at_unix": 1720000000u64, - "completed_at_unix": 1720000001u64, - "signer_mode": "nip46_session", - "signer_session_id": "session-123", - "event_kind": event_kind, - "event_id": "event-bridge-1", - "event_addr": "30402:seller:listing-bridge-1", - "delivery_policy": "quorum", - "delivery_quorum": 1, - "relay_count": 2, - "acknowledged_relay_count": 1, - "required_acknowledged_relay_count": 1, - "attempt_count": 1, - "attempt_summaries": ["attempt 1: 1/2 relays acknowledged"], - "relay_results": [ - { - "relay_url": "wss://radroots.org", - "acknowledged": true, - "detail": null - }, - { - "relay_url": "wss://backup.radroots.org", - "acknowledged": false, - "detail": "timeout" - } - ], - "relay_outcome_summary": "quorum satisfied with 1/2 relay acknowledgements" - }) -} - -async fn connected_bunker_session_handle( - session_id: &str, -) -> TestResult<SdkRadrootsdSignerSessionHandle> { - let (server, _) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": session_id, - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await - .map_err(Into::into) -} - -#[test] -fn radrootsd_debug_redacts_signer_session_values() { - let signer_authority = SdkRadrootsdSignerAuthority { - provider_runtime_id: "runtime-1".to_owned(), - account_identity_id: "identity-1".to_owned(), - provider_signer_session_id: Some("provider-session-123".to_owned()), - }; - let request = SdkRadrootsdListingPublishRequest { - listing: sample_listing(), - kind: Some(30402), - signer_session_id: "session-123".to_owned(), - signer_authority: Some(signer_authority), - idempotency_key: Some("idem-1".to_owned()), - }; - let job = SdkRadrootsdBridgeJob { - job_id: "job-1".to_owned(), - command: "bridge.listing.publish".to_owned(), - status: "published".to_owned(), - terminal: true, - recovered_after_restart: false, - signer_mode: "nip46_session:session-123".to_owned(), - signer_session_id: Some("session-123".to_owned()), - event_kind: 30402, - event_id: Some("event-1".to_owned()), - event_addr: Some("30402:seller:listing-1".to_owned()), - relay_count: 1, - acknowledged_relay_count: 1, - }; - let response = SdkRadrootsdBridgePublishResponse { - deduplicated: false, - job, - }; - let receipt = SdkRadrootsdPublishReceipt { - accepted: true, - deduplicated: false, - job_id: Some("job-1".to_owned()), - status: Some("published".to_owned()), - signer_mode: Some("nip46_session:session-123".to_owned()), - signer_session_id: Some("session-123".to_owned()), - event_addr: Some("30402:seller:listing-1".to_owned()), - relay_count: Some(1), - acknowledged_relay_count: Some(1), - }; - - let request_debug = format!("{request:?}"); - let response_debug = format!("{response:?}"); - let receipt_debug = format!("{receipt:?}"); - - assert!(!request_debug.contains("session-123")); - assert!(!request_debug.contains("provider-session-123")); - assert!(request_debug.contains("<redacted>")); - - assert!(!response_debug.contains("session-123")); - assert!(response_debug.contains("<redacted>")); - - assert!(!receipt_debug.contains("session-123")); - assert!(receipt_debug.contains("<redacted>")); - - let connect_request = SdkRadrootsdSignerSessionConnectRequest::nostrconnect( - "nostrconnect://bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - "client-secret-key", - ) - .with_signer_authority(SdkRadrootsdSignerAuthority { - provider_runtime_id: "runtime-1".to_owned(), - account_identity_id: "identity-1".to_owned(), - provider_signer_session_id: Some("provider-session-123".to_owned()), - }); - let connect_request_debug = format!("{connect_request:?}"); - assert!(!connect_request_debug.contains("client-secret-key")); - assert!(!connect_request_debug.contains("provider-session-123")); - assert!(connect_request_debug.contains("<redacted>")); -} - -#[tokio::test] -async fn radrootsd_signer_session_connect_returns_opaque_handle() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-123", - "mode": "Nostrconnect", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::Nip46; - config.radrootsd = RadrootsdConfig { - endpoint: Some(server.endpoint().to_owned()), - auth: RadrootsdAuth::BearerToken("sdk-secret".to_owned()), - }; - let client = RadrootsSdkClient::from_config(config)?; - let request = SdkRadrootsdSignerSessionConnectRequest::nostrconnect( - "nostrconnect://bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - "client-secret-key", - ); - - let handle: SdkRadrootsdSignerSessionHandle = client - .radrootsd() - .signer_sessions() - .connect(&request) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.connect"); - assert_eq!( - request_json["params"]["url"], - "nostrconnect://bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret" - ); - assert_eq!( - request_json["params"]["client_secret_key"], - "client-secret-key" - ); - assert_eq!(handle.mode(), SdkRadrootsdSignerSessionMode::Nostrconnect); - assert_eq!( - handle.remote_signer_pubkey(), - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - ); - assert_eq!( - handle.client_pubkey(), - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - ); - assert_eq!(handle.relays(), &["wss://radroots.org".to_owned()]); - - let handle_debug = format!("{handle:?}"); - assert!(!handle_debug.contains("session-123")); - assert!(handle_debug.contains("<redacted>")); - - let options = SdkRadrootsdListingPublishOptions::from_signer_session(&handle); - let options_debug = format!("{options:?}"); - assert!(!options_debug.contains("session-123")); - assert!(options_debug.contains("<redacted>")); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_connect_bunker_supports_bunker_mode() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-bunker", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - - let client = radrootsd_test_client(server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.connect"); - assert_eq!( - request_json["params"]["url"], - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret" - ); - assert!(request_json["params"]["client_secret_key"].is_null()); - assert_eq!(handle.mode(), SdkRadrootsdSignerSessionMode::Bunker); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_status_returns_typed_view() -> TestResult<()> { - let (connect_server, _) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-123", - "mode": "Nostrconnect", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - let connect_client = radrootsd_test_client(connect_server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = connect_client - .radrootsd() - .signer_sessions() - .connect_nostrconnect( - "nostrconnect://bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - "client-secret-key", - ) - .await?; - - let (status_server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-session-status", - "result": sample_session_view_json("session-123") - }), - ) - .await?; - let status_client = radrootsd_test_client(status_server.endpoint())?; - let session: SdkRadrootsdSignerSessionView = status_client - .radrootsd() - .signer_sessions() - .status(handle.session()) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.session.status"); - assert_eq!(request_json["params"]["session_id"], "session-123"); - assert_eq!(session.session(), handle.session()); - assert_eq!( - session.role, - SdkRadrootsdSignerSessionRole::OutboundRemoteSigner - ); - assert_eq!( - session.client_pubkey, - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - ); - assert_eq!( - session.signer_pubkey, - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - ); - assert_eq!( - session.user_pubkey.as_deref(), - Some("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc") - ); - assert_eq!(session.relays, vec!["wss://radroots.org".to_owned()]); - assert_eq!(session.permissions, vec!["sign_event:30402".to_owned()]); - assert_eq!(session.name.as_deref(), Some("Radroots Signer")); - assert_eq!( - session.url.as_deref(), - Some("https://radroots.org/signers/demo") - ); - assert_eq!( - session.image.as_deref(), - Some("https://radroots.org/signers/demo.png") - ); - assert!(session.authorized); - assert!(!session.auth_required); - assert_eq!(session.expires_in_secs, Some(120)); - assert_eq!( - session - .signer_authority - .as_ref() - .map(|value| value.provider_runtime_id.as_str()), - Some("runtime-1") - ); - - let debug = format!("{session:?}"); - assert!(!debug.contains("session-123")); - assert!(debug.contains("<redacted>")); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_list_returns_typed_views() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-session-list", - "result": [ - sample_session_view_json("session-123"), - sample_session_view_json("session-456") - ] - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let sessions: Vec<SdkRadrootsdSignerSessionView> = - client.radrootsd().signer_sessions().list().await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.session.list"); - assert_eq!(sessions.len(), 2); - assert_eq!( - sessions[0].role, - SdkRadrootsdSignerSessionRole::OutboundRemoteSigner - ); - let debug = format!("{:?}", sessions[0].session()); - assert!(!debug.contains("session-123")); - assert!(debug.contains("<redacted>")); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_authorize_returns_typed_result() -> TestResult<()> { - let (connect_server, _) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-123", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - let connect_client = radrootsd_test_client(connect_server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = connect_client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-session-authorize", - "result": { - "authorized": true, - "replayed": true - } - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let result = client - .radrootsd() - .signer_sessions() - .authorize(handle.session()) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.session.authorize"); - assert_eq!(request_json["params"]["session_id"], "session-123"); - assert!(result.authorized); - assert!(result.replayed); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_get_public_key_returns_typed_result() -> TestResult<()> { - let (connect_server, _) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-123", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - let connect_client = radrootsd_test_client(connect_server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = connect_client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-get-public-key", - "result": { - "pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - } - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let result = client - .radrootsd() - .signer_sessions() - .get_public_key(handle.session()) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.get_public_key"); - assert_eq!(request_json["params"]["session_id"], "session-123"); - assert_eq!( - result.pubkey, - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - ); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_require_auth_returns_typed_result() -> TestResult<()> { - let (connect_server, _) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-123", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - let connect_client = radrootsd_test_client(connect_server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = connect_client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-session-require-auth", - "result": { - "required": true - } - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let result = client - .radrootsd() - .signer_sessions() - .require_auth(handle.session(), "https://radroots.org/auth") - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.session.require_auth"); - assert_eq!(request_json["params"]["session_id"], "session-123"); - assert_eq!( - request_json["params"]["auth_url"], - "https://radroots.org/auth" - ); - assert!(result.required); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_close_returns_typed_result() -> TestResult<()> { - let (connect_server, _) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-123", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - let connect_client = radrootsd_test_client(connect_server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = connect_client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-session-close", - "result": { - "closed": true - } - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let result = client - .radrootsd() - .signer_sessions() - .close(handle.session()) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.session.close"); - assert_eq!(request_json["params"]["session_id"], "session-123"); - assert!(result.closed); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_connect_rejects_relay_transport_mode() -> TestResult<()> { - let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::production())?; - let request = SdkRadrootsdSignerSessionConnectRequest::bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ); - - let error = client - .radrootsd() - .signer_sessions() - .connect(&request) - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkRadrootsdSessionError::UnsupportedTransport { - transport: SdkTransportMode::RelayDirect, - operation: "radrootsd.signer_sessions.connect", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_listing_publish_accepts_sdk_built_draft() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-listing-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-1", - "command": "bridge.listing.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-123", - "signer_session_id": "session-123", - "event_kind": 30402, - "event_id": "event-1", - "event_addr": "30402:seller:listing-1", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-123").await?; - let client = radrootsd_test_client(server.endpoint())?; - let draft = client.listing().build_draft(&sample_listing())?; - let options = SdkRadrootsdListingPublishOptions::from_signer_session(&handle) - .with_idempotency_key("idem-1"); - - let receipt = client - .listing() - .publish_draft_via_radrootsd_with_options(draft, &options) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.listing.publish"); - assert_eq!(request_json["params"]["signer_session_id"], "session-123"); - assert_eq!(request_json["params"]["idempotency_key"], "idem-1"); - assert_eq!(request_json["params"]["kind"], 30402); - assert_eq!( - request_json["params"]["listing"]["d_tag"], - "AAAAAAAAAAAAAAAAAAAAAg" - ); - - assert_eq!(receipt.transport, SdkTransportMode::Radrootsd); - assert_eq!(receipt.event_kind, Some(30402)); - assert_eq!(receipt.event_id, Some("event-1".to_owned())); - match receipt.transport_receipt { - SdkTransportReceipt::Radrootsd(rpc_receipt) => { - assert!(rpc_receipt.accepted); - assert!(!rpc_receipt.deduplicated); - assert_eq!(rpc_receipt.job_id.as_deref(), Some("job-1")); - assert_eq!(rpc_receipt.status.as_deref(), Some("published")); - assert_eq!( - rpc_receipt.signer_session_id.as_deref(), - Some("session-123") - ); - assert_eq!( - rpc_receipt.event_addr.as_deref(), - Some("30402:seller:listing-1") - ); - assert_eq!(rpc_receipt.relay_count, Some(1)); - assert_eq!(rpc_receipt.acknowledged_relay_count, Some(1)); - } - SdkTransportReceipt::RelayDirect(_) => panic!("unexpected relay receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_listing_publish_accepts_typed_listing_value() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-listing-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-2", - "command": "bridge.listing.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-456", - "signer_session_id": "session-456", - "event_kind": 30402, - "event_id": "event-2", - "event_addr": "30402:seller:listing-2", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-456").await?; - let client = radrootsd_test_client(server.endpoint())?; - - let receipt = client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.listing.publish"); - assert_eq!(request_json["params"]["signer_session_id"], "session-456"); - assert!(request_json["params"]["idempotency_key"].is_null()); - assert_eq!(request_json["params"]["kind"], 30402); - assert_eq!( - request_json["params"]["listing"]["d_tag"], - "AAAAAAAAAAAAAAAAAAAAAg" - ); - - assert_eq!(receipt.transport, SdkTransportMode::Radrootsd); - assert_eq!(receipt.event_kind, Some(30402)); - assert_eq!(receipt.event_id, Some("event-2".to_owned())); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_profile_publish_accepts_typed_profile_value() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-profile-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-profile-1", - "command": "bridge.profile.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-profile-1", - "signer_session_id": "session-profile-1", - "event_kind": 0, - "event_id": "event-profile-1", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-profile-1").await?; - let client = radrootsd_test_client(server.endpoint())?; - let options = SdkRadrootsdProfilePublishOptions::from_signer_session(&handle) - .with_idempotency_key("profile-idem-1") - .with_signer_authority(SdkRadrootsdSignerAuthority { - provider_runtime_id: "runtime-profile".to_owned(), - account_identity_id: "identity-profile".to_owned(), - provider_signer_session_id: Some("provider-session-profile".to_owned()), - }); - - let receipt = client - .profile() - .publish_profile_via_radrootsd_with_options( - &sample_profile(), - Some(RadrootsProfileType::Farm), - &options, - ) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.profile.publish"); - assert_eq!( - request_json["params"]["signer_session_id"], - "session-profile-1" - ); - assert_eq!(request_json["params"]["profile_type"], "farm"); - assert_eq!(request_json["params"]["profile"]["name"], "North Farm"); - assert_eq!(request_json["params"]["idempotency_key"], "profile-idem-1"); - assert_eq!( - request_json["params"]["signer_authority"]["provider_runtime_id"], - "runtime-profile" - ); - assert_eq!(receipt.event_kind, Some(KIND_PROFILE)); - assert_eq!(receipt.event_id, Some("event-profile-1".to_owned())); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_farm_publish_accepts_typed_farm_value() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-farm-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-farm-1", - "command": "bridge.farm.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-farm-1", - "signer_session_id": "session-farm-1", - "event_kind": 30340, - "event_id": "event-farm-1", - "event_addr": "30340:seller:AAAAAAAAAAAAAAAAAAAAAA", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-farm-1").await?; - let client = radrootsd_test_client(server.endpoint())?; - let options = SdkRadrootsdFarmPublishOptions::from_signer_session(&handle) - .with_idempotency_key("farm-idem-1"); - - let receipt = client - .farm() - .publish_farm_via_radrootsd_with_options(&sample_farm(), &options) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.farm.publish"); - assert_eq!( - request_json["params"]["signer_session_id"], - "session-farm-1" - ); - assert_eq!(request_json["params"]["kind"], KIND_FARM); - assert_eq!( - request_json["params"]["farm"]["d_tag"], - "AAAAAAAAAAAAAAAAAAAAAA" - ); - assert_eq!(request_json["params"]["idempotency_key"], "farm-idem-1"); - assert_eq!(receipt.event_kind, Some(KIND_FARM)); - assert_eq!(receipt.event_id, Some("event-farm-1".to_owned())); - match receipt.transport_receipt { - SdkTransportReceipt::Radrootsd(receipt) => { - assert_eq!( - receipt.event_addr, - Some("30340:seller:AAAAAAAAAAAAAAAAAAAAAA".to_owned()) - ); - } - SdkTransportReceipt::RelayDirect(_) => panic!("unexpected relay receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_listing_publish_with_options_forwards_typed_continuity_metadata() --> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-listing-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-3", - "command": "bridge.listing.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-789", - "signer_session_id": "session-789", - "event_kind": 30402, - "event_id": "event-3", - "event_addr": "30402:seller:listing-3", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-789").await?; - let client = radrootsd_test_client(server.endpoint())?; - let options = SdkRadrootsdListingPublishOptions::from_signer_session(&handle) - .with_idempotency_key("idem-3") - .with_signer_authority(SdkRadrootsdSignerAuthority { - provider_runtime_id: "runtime-1".to_owned(), - account_identity_id: "identity-1".to_owned(), - provider_signer_session_id: Some("provider-session-123".to_owned()), - }); - - let receipt = client - .listing() - .publish_listing_via_radrootsd_with_options(&sample_listing(), &options) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.listing.publish"); - assert_eq!(request_json["params"]["signer_session_id"], "session-789"); - assert_eq!(request_json["params"]["idempotency_key"], "idem-3"); - assert_eq!( - request_json["params"]["signer_authority"]["provider_runtime_id"], - "runtime-1" - ); - assert_eq!( - request_json["params"]["signer_authority"]["account_identity_id"], - "identity-1" - ); - assert_eq!( - request_json["params"]["signer_authority"]["provider_signer_session_id"], - "provider-session-123" - ); - assert_eq!(receipt.event_id, Some("event-3".to_owned())); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_listing_publish_rejects_draft_only_signer_mode() -> TestResult<()> { - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::DraftOnly; - let client = RadrootsSdkClient::from_config(config)?; - let handle = connected_bunker_session_handle("session-123").await?; - - let error = client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::Radrootsd, - signer: SignerConfig::DraftOnly, - required: SignerConfig::Nip46, - operation: "listing.publish_via_radrootsd", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_listing_publish_rejects_local_identity_signer_mode() -> TestResult<()> { - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::LocalIdentity; - let client = RadrootsSdkClient::from_config(config)?; - let handle = connected_bunker_session_handle("session-123").await?; - - let error = client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::Radrootsd, - signer: SignerConfig::LocalIdentity, - required: SignerConfig::Nip46, - operation: "listing.publish_via_radrootsd", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_listing_publish_rejects_relay_transport_mode() -> TestResult<()> { - let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::production())?; - let handle = connected_bunker_session_handle("session-123").await?; - - let error = client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedTransport { - transport: SdkTransportMode::RelayDirect, - operation: "listing.publish_via_radrootsd", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_order_request_publish_accepts_session_handle() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-order-request-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-order-1", - "command": "bridge.order.request", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-order-1", - "signer_session_id": "session-order-1", - "event_kind": KIND_ORDER_REQUEST, - "event_id": "event-order-1", - "event_addr": format!("{KIND_LISTING}:{}:AAAAAAAAAAAAAAAAAAAAAg", "a".repeat(64)), - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-order-1").await?; - let client = radrootsd_test_client(server.endpoint())?; - let options = SdkRadrootsdOrderRequestPublishOptions::from_signer_session(&handle) - .with_idempotency_key("idem-order-1") - .with_signer_authority(SdkRadrootsdSignerAuthority { - provider_runtime_id: "runtime-1".to_owned(), - account_identity_id: "identity-1".to_owned(), - provider_signer_session_id: Some("provider-session-order-1".to_owned()), - }); - - let receipt = client - .order() - .publish_order_request_via_radrootsd_with_options( - &sample_order_request(), - &listing_event_ptr_with_relays(Some("wss://radroots.org")), - &options, - ) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.order.request"); - assert_eq!( - request_json["params"]["signer_session_id"], - "session-order-1" - ); - assert_eq!(request_json["params"]["idempotency_key"], "idem-order-1"); - assert_eq!(request_json["params"]["order"]["order_id"], "order-1"); - assert_eq!( - request_json["params"]["listing_event"]["id"], - "a".repeat(64) - ); - assert_eq!( - request_json["params"]["listing_event"]["relays"], - "wss://radroots.org" - ); - assert_eq!( - request_json["params"]["signer_authority"]["provider_runtime_id"], - "runtime-1" - ); - assert_eq!( - request_json["params"]["signer_authority"]["provider_signer_session_id"], - "provider-session-order-1" - ); - assert_eq!(receipt.event_kind, Some(KIND_ORDER_REQUEST)); - assert_eq!(receipt.event_id, Some("event-order-1".to_owned())); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_sdk_workflow_chains_session_listing_order_and_bridge_job() -> TestResult<()> { - let (server, mut request_rx) = JsonRpcSequenceServer::spawn( - Some("Bearer sdk-secret"), - vec![ - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-workflow-1", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-listing-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-workflow-listing", - "command": "bridge.listing.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-workflow-1", - "signer_session_id": "session-workflow-1", - "event_kind": 30402, - "event_id": "event-workflow-listing", - "event_addr": "30402:seller:listing-workflow-1", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-order-request-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-workflow-order", - "command": "bridge.order.request", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-workflow-1", - "signer_session_id": "session-workflow-1", - "event_kind": KIND_ORDER_REQUEST, - "event_id": "event-workflow-order", - "event_addr": format!("{KIND_LISTING}:{}:AAAAAAAAAAAAAAAAAAAAAg", "a".repeat(64)), - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-bridge-job-status", - "result": sample_bridge_job_json_for( - "job-workflow-order", - "bridge.order.request", - KIND_ORDER_REQUEST, - ) - }), - ], - ) - .await?; - - let client = radrootsd_test_client(server.endpoint())?; - let handle = client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - assert_eq!(handle.mode(), SdkRadrootsdSignerSessionMode::Bunker); - - let connect_request = request_rx.recv().await.expect("connect request"); - assert_eq!(connect_request["method"], "nip46.connect"); - - let listing_receipt = client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await?; - let listing_request = request_rx.recv().await.expect("listing publish request"); - assert_eq!(listing_request["method"], "bridge.listing.publish"); - assert_eq!( - listing_request["params"]["signer_session_id"], - "session-workflow-1" - ); - - let order_receipt = client - .order() - .publish_order_request_via_radrootsd( - &sample_order_request(), - &listing_event_ptr_with_relays(Some("wss://radroots.org")), - &handle, - ) - .await?; - let order_request = request_rx.recv().await.expect("order publish request"); - assert_eq!(order_request["method"], "bridge.order.request"); - assert_eq!( - order_request["params"]["signer_session_id"], - "session-workflow-1" - ); - assert_eq!(order_request["params"]["order"]["order_id"], "order-1"); - assert_eq!( - order_request["params"]["listing_event"]["id"], - "a".repeat(64) - ); - - let order_job = match &order_receipt.transport_receipt { - SdkTransportReceipt::Radrootsd(receipt) => receipt.job(), - SdkTransportReceipt::RelayDirect(_) => None, - } - .expect("order publish receipt should expose a bridge job ref"); - - let job_view = client.radrootsd().bridge().job(&order_job).await?; - let job_request = request_rx.recv().await.expect("bridge job request"); - assert_eq!(job_request["method"], "bridge.job.status"); - assert_eq!(job_request["params"]["job_id"], "job-workflow-order"); - - assert_eq!(listing_receipt.event_kind, Some(30402)); - assert_eq!(order_receipt.event_kind, Some(KIND_ORDER_REQUEST)); - assert_eq!(job_view.job().job_id(), "job-workflow-order"); - assert_eq!(job_view.command, "bridge.order.request"); - assert_eq!(job_view.status, SdkRadrootsdBridgeJobStatus::Published); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_bridge_status_returns_typed_status() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-bridge-status", - "result": sample_bridge_status_json() - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let status = client.radrootsd().bridge().status().await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.status"); - assert!(status.enabled); - assert!(status.ready); - assert_eq!( - status.delivery_policy, - SdkRadrootsdBridgeDeliveryPolicy::Quorum - ); - assert_eq!(status.delivery_quorum, Some(1)); - assert_eq!(status.available_nip46_signer_sessions, 2); - assert!( - status - .methods - .contains(&"bridge.listing.publish".to_owned()) - ); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_bridge_job_status_accepts_typed_job_ref_from_publish_receipt() -> TestResult<()> -{ - let (publish_server, publish_request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-listing-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-bridge-1", - "command": "bridge.listing.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-123", - "signer_session_id": "session-123", - "event_kind": 30402, - "event_id": "event-bridge-1", - "event_addr": "30402:seller:listing-bridge-1", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - let handle = connected_bunker_session_handle("session-123").await?; - let publish_client = radrootsd_test_client(publish_server.endpoint())?; - let publish_receipt = publish_client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await?; - let publish_request_json = publish_request_rx.await?; - assert_eq!(publish_request_json["method"], "bridge.listing.publish"); - - let job = match &publish_receipt.transport_receipt { - SdkTransportReceipt::Radrootsd(receipt) => receipt.job(), - SdkTransportReceipt::RelayDirect(_) => None, - } - .expect("publish receipt should expose a bridge job ref"); - - let (job_server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-bridge-job-status", - "result": sample_bridge_job_json("job-bridge-1") - }), - ) - .await?; - let job_client = radrootsd_test_client(job_server.endpoint())?; - let job_view = job_client.radrootsd().bridge().job(&job).await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.job.status"); - assert_eq!(request_json["params"]["job_id"], "job-bridge-1"); - assert_eq!(job_view.job().job_id(), "job-bridge-1"); - assert_eq!(job_view.status, SdkRadrootsdBridgeJobStatus::Published); - assert_eq!( - job_view.delivery_policy, - SdkRadrootsdBridgeDeliveryPolicy::Quorum - ); - assert_eq!(job_view.attempt_count, 1); - assert_eq!(job_view.relay_results.len(), 2); - assert_eq!(job_view.relay_results[0].relay_url, "wss://radroots.org"); - assert!(job_view.relay_results[0].acknowledged); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_bridge_job_list_returns_typed_views() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-bridge-job-list", - "result": [ - sample_bridge_job_json("job-bridge-1"), - sample_bridge_job_json("job-bridge-2") - ] - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let jobs = client.radrootsd().bridge().jobs().await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.job.list"); - assert_eq!(jobs.len(), 2); - assert_eq!(jobs[0].job().job_id(), "job-bridge-1"); - assert_eq!(jobs[1].job().job_id(), "job-bridge-2"); - assert_eq!(jobs[0].status, SdkRadrootsdBridgeJobStatus::Published); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_bridge_status_rejects_relay_transport_mode() -> TestResult<()> { - let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::production())?; - let error = client - .radrootsd() - .bridge() - .status() - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkRadrootsdBridgeError::UnsupportedTransport { - transport: SdkTransportMode::RelayDirect, - operation: "radrootsd.bridge.status", - } - )); - - Ok(()) -} - -#[test] -fn radrootsd_listing_request_from_event_rejects_listing_draft_kind() -> TestResult<()> { - let draft = radroots_sdk::listing::build_draft(&sample_listing())?; - let mut event = sdk_event("seller", 1_720_000_000, draft); - event.kind = KIND_LISTING_DRAFT; - - assert!(matches!( - SdkRadrootsdListingPublishRequest::from_event(&event, "session-123", None, None), - Err(RadrootsListingParseError::InvalidKind(KIND_LISTING_DRAFT)) - )); - - Ok(()) -} diff --git a/crates/sdk/tests/relay_direct.rs b/crates/sdk/tests/relay_direct.rs @@ -1,1406 +0,0 @@ -#![cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] - -use futures::{SinkExt, StreamExt}; -use nostr::{ClientMessage, JsonUtil, RelayMessage}; -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, RadrootsCoreUnit, -}; -use radroots_events::ids::{RadrootsEventId, RadrootsPublicKey}; -use radroots_sdk::farm::{RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef}; -use radroots_sdk::identity::RadrootsIdentity; -use radroots_sdk::listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, -}; -use radroots_sdk::order::{ - RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, - RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderFulfillmentState, - RadrootsOrderFulfillmentUpdate, RadrootsOrderInventoryCommitment, RadrootsOrderItem, - RadrootsOrderPricingBasis, RadrootsOrderReceipt, RadrootsOrderRequest, - RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, -}; -use radroots_sdk::profile::{RadrootsProfile, RadrootsProfileType}; -use radroots_sdk::{ - RadrootsNostrEventPtr, RadrootsSdkClient, RadrootsSdkConfig, RelayConfig, SdkEnvironment, - SdkPublishError, SdkTransportMode, SdkTransportReceipt, SignerConfig, -}; -use tokio::net::TcpListener; -use tokio::sync::oneshot; -use tokio_tungstenite::tungstenite::Message; - -type TestResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>; - -struct AckRelay { - url: String, - shutdown_tx: Option<oneshot::Sender<()>>, -} - -impl AckRelay { - async fn spawn() -> TestResult<Self> { - let listener = TcpListener::bind("127.0.0.1:0").await?; - let addr = listener.local_addr()?; - let url = format!("ws://{addr}"); - let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); - - tokio::spawn(async move { - loop { - tokio::select! { - _ = &mut shutdown_rx => break, - accept = listener.accept() => { - let Ok((stream, _)) = accept else { - break; - }; - tokio::spawn(async move { - let Ok(websocket) = tokio_tungstenite::accept_async(stream).await else { - return; - }; - let (mut writer, mut reader) = websocket.split(); - while let Some(message) = reader.next().await { - let Ok(message) = message else { - break; - }; - let Message::Text(text) = message else { - continue; - }; - let Ok(client_message) = ClientMessage::from_json(text.as_str()) else { - continue; - }; - if let ClientMessage::Event(event) = client_message { - let relay_message = - RelayMessage::ok(event.id, true, "").as_json(); - if writer - .send(Message::Text(relay_message.into())) - .await - .is_err() - { - break; - } - } - } - }); - } - } - } - }); - - Ok(Self { - url, - shutdown_tx: Some(shutdown_tx), - }) - } - - fn url(&self) -> &str { - self.url.as_str() - } -} - -impl Drop for AckRelay { - fn drop(&mut self) { - if let Some(shutdown_tx) = self.shutdown_tx.take() { - let _ = shutdown_tx.send(()); - } - } -} - -fn sample_listing() -> RadrootsListing { - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "seller".into(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - }, - product: RadrootsListingProduct { - key: "coffee".into(), - title: "Coffee".into(), - category: "coffee".into(), - summary: Some("Single origin coffee".into()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".parse().expect("primary bin id"), - bins: vec![RadrootsListingBin { - bin_id: "bin-1".parse().expect("bin id"), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1000u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice { - amount: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(20u32), - RadrootsCoreCurrency::USD, - ), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - }, - display_amount: None, - display_unit: None, - display_label: None, - display_price: None, - display_price_unit: None, - }], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(RadrootsCoreDecimal::from(5u32)), - availability: Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Active, - }), - delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), - location: Some(RadrootsListingLocation { - primary: "North Farm".into(), - city: None, - region: None, - country: None, - lat: None, - lng: None, - geohash: None, - }), - images: None, - } -} - -fn sample_profile() -> RadrootsProfile { - RadrootsProfile { - name: "north-farm".into(), - display_name: Some("North Farm".into()), - nip05: None, - about: Some("Farm profile".into()), - website: None, - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - } -} - -fn sample_farm() -> RadrootsFarm { - RadrootsFarm { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - name: "North Farm".into(), - about: Some("Vegetable farm".into()), - website: None, - picture: None, - banner: None, - location: Some(RadrootsFarmLocation { - primary: Some("North Road".into()), - city: None, - region: None, - country: Some("US".into()), - gcs: None, - }), - tags: Some(vec!["vegetables".into()]), - } -} - -fn decimal(raw: &str) -> RadrootsCoreDecimal { - raw.parse().expect("decimal") -} - -fn usd(raw: &str) -> RadrootsCoreMoney { - RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) -} - -fn listing_event_ptr() -> RadrootsNostrEventPtr { - RadrootsNostrEventPtr { - id: event_id_wire('a'), - relays: Some("wss://listing.relay.example".into()), - } -} - -fn public_key(value: String) -> RadrootsPublicKey { - value.parse().expect("public key") -} - -fn event_id(character: char) -> RadrootsEventId { - core::iter::repeat_n(character, 64) - .collect::<String>() - .parse() - .expect("event id") -} - -fn event_id_wire(character: char) -> String { - event_id(character).into_string() -} - -fn sample_order_request(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderRequest { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderRequest { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - items: vec![RadrootsOrderItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - }], - economics: RadrootsOrderEconomics { - quote_id: "quote-1".parse().expect("quote id"), - quote_version: 1, - pricing_basis: RadrootsOrderPricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsOrderEconomicItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - quantity_amount: decimal("1"), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: decimal("5"), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: usd("10"), - }], - discounts: Vec::new(), - adjustments: Vec::new(), - subtotal: usd("10"), - discount_total: usd("0"), - adjustment_total: usd("0"), - total: usd("10"), - }, - } -} - -fn sample_order_decision(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderDecision { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderDecision { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - decision: RadrootsOrderDecisionOutcome::Accepted { - inventory_commitments: vec![RadrootsOrderInventoryCommitment { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - }], - }, - } -} - -fn sample_order_revision_proposal( - buyer_pubkey: String, - seller_pubkey: String, - root_event_id: String, - prev_event_id: String, -) -> RadrootsOrderRevisionProposal { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderRevisionProposal { - revision_id: "revision-1".parse().expect("revision id"), - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - root_event_id: root_event_id.parse().expect("root event id"), - prev_event_id: prev_event_id.parse().expect("previous event id"), - items: vec![RadrootsOrderItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 3, - }], - economics: RadrootsOrderEconomics { - quote_id: "revision-quote-1".parse().expect("revision quote id"), - quote_version: 2, - pricing_basis: RadrootsOrderPricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsOrderEconomicItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 3, - quantity_amount: decimal("1"), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: decimal("5"), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: usd("15"), - }], - discounts: Vec::new(), - adjustments: Vec::new(), - subtotal: usd("15"), - discount_total: usd("0"), - adjustment_total: usd("0"), - total: usd("15"), - }, - reason: "update count".into(), - } -} - -fn sample_order_revision_decision( - proposal: &RadrootsOrderRevisionProposal, - decision: RadrootsOrderRevisionOutcome, -) -> RadrootsOrderRevisionDecision { - RadrootsOrderRevisionDecision { - revision_id: proposal.revision_id.clone(), - order_id: proposal.order_id.clone(), - listing_addr: proposal.listing_addr.clone(), - buyer_pubkey: proposal.buyer_pubkey.clone(), - seller_pubkey: proposal.seller_pubkey.clone(), - root_event_id: proposal.root_event_id.clone(), - prev_event_id: event_id('3'), - decision, - } -} - -fn sample_fulfillment_update( - buyer_pubkey: String, - seller_pubkey: String, -) -> RadrootsOrderFulfillmentUpdate { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderFulfillmentUpdate { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - status: RadrootsOrderFulfillmentState::ReadyForPickup, - } -} - -fn sample_order_cancellation( - buyer_pubkey: String, - seller_pubkey: String, -) -> RadrootsOrderCancellation { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderCancellation { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - reason: "schedule changed".into(), - } -} - -fn sample_buyer_receipt(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderReceipt { - let buyer_pubkey = public_key(buyer_pubkey); - let seller_pubkey = public_key(seller_pubkey); - RadrootsOrderReceipt { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey, - seller_pubkey, - received: true, - issue: None, - received_at: 1_785_000_000, - } -} - -#[tokio::test] -async fn relay_direct_farm_publish_accepts_sdk_built_draft() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - let draft = client.farm().build_draft(&sample_farm())?; - - let receipt = client - .farm() - .publish_draft_with_identity(&identity, draft) - .await?; - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(30340)); - assert!(receipt.event_id.is_some()); - match receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!( - receipt.event_id.as_deref(), - Some(relay_receipt.event_id.as_str()) - ); - assert_eq!(relay_receipt.event.kind, 30340); - assert_eq!(relay_receipt.event.author, identity.public_key_hex()); - assert_eq!( - relay_receipt.event.tags, - vec![ - vec!["d".to_owned(), "AAAAAAAAAAAAAAAAAAAAAA".to_owned()], - vec!["t".to_owned(), "vegetables".to_owned()] - ] - ); - assert_eq!(relay_receipt.target_relays, vec![relay.url().to_owned()]); - assert_eq!(relay_receipt.connected_relays, vec![relay.url().to_owned()]); - assert_eq!( - relay_receipt.acknowledged_relays, - vec![relay.url().to_owned()] - ); - assert!(relay_receipt.failed_relays.is_empty()); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_order_request_publish_accepts_sdk_built_draft() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let buyer_identity = RadrootsIdentity::generate(); - let seller_identity = RadrootsIdentity::generate(); - let listing_event = listing_event_ptr(); - let payload = sample_order_request( - buyer_identity.public_key_hex(), - seller_identity.public_key_hex(), - ); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - let draft = client - .order() - .build_order_request_draft(&listing_event, &payload)?; - assert_eq!(draft.as_wire_parts().kind, 3422); - - let receipt = client - .order() - .publish_order_request_draft_with_identity(&buyer_identity, draft) - .await?; - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(3422)); - assert!(receipt.event_id.is_some()); - match receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!( - receipt.event_id.as_deref(), - Some(relay_receipt.event_id.as_str()) - ); - assert_eq!(receipt.event_kind, Some(relay_receipt.event_kind)); - assert_eq!(relay_receipt.event.kind, 3422); - assert_eq!(relay_receipt.event_id, relay_receipt.event.id); - assert_eq!(relay_receipt.signature, relay_receipt.event.sig); - assert_eq!(relay_receipt.created_at, relay_receipt.event.created_at); - assert_eq!(relay_receipt.event.author, buyer_identity.public_key_hex()); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["p".to_owned(), seller_identity.public_key_hex()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["a".to_owned(), payload.listing_addr.to_string()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["d".to_owned(), payload.order_id.to_string()]) - ); - assert!(relay_receipt.event.tags.contains(&vec![ - "listing_event".to_owned(), - listing_event.id.clone(), - listing_event.relays.clone().expect("listing relay") - ])); - assert_eq!(relay_receipt.target_relays, vec![relay.url().to_owned()]); - assert_eq!(relay_receipt.connected_relays, vec![relay.url().to_owned()]); - assert_eq!( - relay_receipt.acknowledged_relays, - vec![relay.url().to_owned()] - ); - assert!(relay_receipt.failed_relays.is_empty()); - let envelope = client - .order() - .parse_order_request(&relay_receipt.event) - .expect("order request"); - assert_eq!(envelope.order_id, payload.order_id); - assert_eq!(envelope.listing_addr, payload.listing_addr); - assert_eq!(envelope.payload.economics.quote_id, "quote-1"); - assert_eq!(envelope.payload.economics.total, usd("10")); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_order_decision_publish_accepts_sdk_built_draft() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let buyer_identity = RadrootsIdentity::generate(); - let seller_identity = RadrootsIdentity::generate(); - let root_event_id = event_id('1'); - let payload = sample_order_decision( - buyer_identity.public_key_hex(), - seller_identity.public_key_hex(), - ); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - let draft = - client - .order() - .build_order_decision_draft(&root_event_id, &root_event_id, &payload)?; - assert_eq!(draft.as_wire_parts().kind, 3423); - - let receipt = client - .order() - .publish_order_decision_draft_with_identity(&seller_identity, draft) - .await?; - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(3423)); - assert!(receipt.event_id.is_some()); - match receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!( - receipt.event_id.as_deref(), - Some(relay_receipt.event_id.as_str()) - ); - assert_eq!(receipt.event_kind, Some(relay_receipt.event_kind)); - assert_eq!(relay_receipt.event.kind, 3423); - assert_eq!(relay_receipt.event.author, seller_identity.public_key_hex()); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["p".to_owned(), buyer_identity.public_key_hex()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["a".to_owned(), payload.listing_addr.to_string()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["d".to_owned(), payload.order_id.to_string()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_root".to_owned(), root_event_id.to_string()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_prev".to_owned(), root_event_id.to_string()]) - ); - assert_eq!(relay_receipt.target_relays, vec![relay.url().to_owned()]); - assert_eq!(relay_receipt.connected_relays, vec![relay.url().to_owned()]); - assert_eq!( - relay_receipt.acknowledged_relays, - vec![relay.url().to_owned()] - ); - assert!(relay_receipt.failed_relays.is_empty()); - let envelope = client - .order() - .parse_order_decision(&relay_receipt.event) - .expect("order decision"); - assert_eq!(envelope.order_id, payload.order_id); - assert_eq!(envelope.listing_addr, payload.listing_addr); - assert_eq!(envelope.payload.decision, payload.decision); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_order_revision_publish_accepts_sdk_built_payloads() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let buyer_identity = RadrootsIdentity::generate(); - let seller_identity = RadrootsIdentity::generate(); - let buyer_pubkey = buyer_identity.public_key_hex(); - let seller_pubkey = seller_identity.public_key_hex(); - let root_event_id = event_id('1'); - let decision_event_id = event_id('2'); - let proposal = sample_order_revision_proposal( - buyer_pubkey.clone(), - seller_pubkey.clone(), - root_event_id.to_string(), - decision_event_id.to_string(), - ); - let decision = - sample_order_revision_decision(&proposal, RadrootsOrderRevisionOutcome::Accepted); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let proposal_receipt = client - .order() - .publish_order_revision_proposal_with_identity( - &seller_identity, - &root_event_id, - &decision_event_id, - &proposal, - ) - .await?; - let decision_receipt = client - .order() - .publish_order_revision_decision_with_identity( - &buyer_identity, - &root_event_id, - &decision.prev_event_id, - &decision, - ) - .await?; - - assert_eq!(proposal_receipt.event_kind, Some(3424)); - assert_eq!(decision_receipt.event_kind, Some(3425)); - - match proposal_receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!(relay_receipt.event.kind, 3424); - assert_eq!(relay_receipt.event.author, seller_pubkey); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["p".to_owned(), buyer_pubkey.clone()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_root".to_owned(), root_event_id.to_string()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_prev".to_owned(), decision_event_id.to_string()]) - ); - let envelope = client - .order() - .parse_order_revision_proposal(&relay_receipt.event) - .expect("order revision proposal"); - assert_eq!(envelope.order_id, proposal.order_id); - assert_eq!(envelope.listing_addr, proposal.listing_addr); - assert_eq!(envelope.payload.revision_id, "revision-1"); - assert_eq!(envelope.payload.economics.total, usd("15")); - assert_eq!(envelope.payload.reason, "update count"); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - match decision_receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!(relay_receipt.event.kind, 3425); - assert_eq!(relay_receipt.event.author, buyer_pubkey); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["p".to_owned(), seller_pubkey]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_root".to_owned(), root_event_id.to_string()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_prev".to_owned(), event_id_wire('3')]) - ); - let envelope = client - .order() - .parse_order_revision_decision(&relay_receipt.event) - .expect("order revision decision"); - assert_eq!(envelope.order_id, decision.order_id); - assert_eq!(envelope.listing_addr, decision.listing_addr); - assert_eq!(envelope.payload.revision_id, decision.revision_id); - assert_eq!( - envelope.payload.decision, - RadrootsOrderRevisionOutcome::Accepted - ); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_order_lifecycle_publish_accepts_sdk_built_payloads() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let buyer_identity = RadrootsIdentity::generate(); - let seller_identity = RadrootsIdentity::generate(); - let buyer_pubkey = buyer_identity.public_key_hex(); - let seller_pubkey = seller_identity.public_key_hex(); - let root_event_id = event_id('1'); - let decision_event_id = event_id('2'); - let fulfillment_event_id = event_id('4'); - let fulfillment = sample_fulfillment_update(buyer_pubkey.clone(), seller_pubkey.clone()); - let cancellation = sample_order_cancellation(buyer_pubkey.clone(), seller_pubkey.clone()); - let receipt = sample_buyer_receipt(buyer_pubkey.clone(), seller_pubkey.clone()); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let fulfillment_receipt = client - .order() - .publish_fulfillment_update_with_identity( - &seller_identity, - &root_event_id, - &decision_event_id, - &fulfillment, - ) - .await?; - let cancellation_receipt = client - .order() - .publish_order_cancellation_with_identity( - &buyer_identity, - &root_event_id, - &root_event_id, - &cancellation, - ) - .await?; - let buyer_receipt = client - .order() - .publish_buyer_receipt_with_identity( - &buyer_identity, - &root_event_id, - &fulfillment_event_id, - &receipt, - ) - .await?; - - assert_eq!(fulfillment_receipt.event_kind, Some(3433)); - assert_eq!(cancellation_receipt.event_kind, Some(3432)); - assert_eq!(buyer_receipt.event_kind, Some(3434)); - - match fulfillment_receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!(relay_receipt.event.kind, 3433); - assert_eq!(relay_receipt.event.author, seller_pubkey); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["p".to_owned(), buyer_pubkey.clone()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_root".to_owned(), root_event_id.to_string()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_prev".to_owned(), decision_event_id.to_string()]) - ); - let envelope = client - .order() - .parse_fulfillment_update(&relay_receipt.event) - .expect("active fulfillment update"); - assert_eq!(envelope.order_id, fulfillment.order_id); - assert_eq!(envelope.listing_addr, fulfillment.listing_addr); - assert_eq!(envelope.payload.status, fulfillment.status); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - match cancellation_receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!(relay_receipt.event.kind, 3432); - assert_eq!(relay_receipt.event.author, buyer_pubkey); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["p".to_owned(), seller_pubkey.clone()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_root".to_owned(), root_event_id.to_string()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_prev".to_owned(), root_event_id.to_string()]) - ); - let envelope = client - .order() - .parse_order_cancellation(&relay_receipt.event) - .expect("order cancellation"); - assert_eq!(envelope.order_id, cancellation.order_id); - assert_eq!(envelope.listing_addr, cancellation.listing_addr); - assert_eq!(envelope.payload.reason, cancellation.reason); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - match buyer_receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!(relay_receipt.event.kind, 3434); - assert_eq!(relay_receipt.event.author, buyer_pubkey); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["p".to_owned(), seller_pubkey]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_root".to_owned(), root_event_id.to_string()]) - ); - assert!( - relay_receipt - .event - .tags - .contains(&vec!["e_prev".to_owned(), fulfillment_event_id.to_string()]) - ); - let envelope = client - .order() - .parse_buyer_receipt(&relay_receipt.event) - .expect("active buyer receipt"); - assert_eq!(envelope.order_id, receipt.order_id); - assert_eq!(envelope.listing_addr, receipt.listing_addr); - assert_eq!(envelope.payload.received, receipt.received); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_order_decision_publish_builds_and_publishes_payload() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let buyer_identity = RadrootsIdentity::generate(); - let seller_identity = RadrootsIdentity::generate(); - let payload = sample_order_decision( - buyer_identity.public_key_hex(), - seller_identity.public_key_hex(), - ); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - let root_event_id = event_id('1'); - - let receipt = client - .order() - .publish_order_decision_with_identity( - &seller_identity, - &root_event_id, - &root_event_id, - &payload, - ) - .await?; - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(3423)); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_order_request_publish_builds_and_publishes_payload() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let buyer_identity = RadrootsIdentity::generate(); - let seller_identity = RadrootsIdentity::generate(); - let payload = sample_order_request( - buyer_identity.public_key_hex(), - seller_identity.public_key_hex(), - ); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let receipt = client - .order() - .publish_order_request_with_identity(&buyer_identity, &listing_event_ptr(), &payload) - .await?; - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(3422)); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_order_request_publish_rejects_radrootsd_transport_mode() -> TestResult<()> { - let buyer_identity = RadrootsIdentity::generate(); - let seller_identity = RadrootsIdentity::generate(); - let payload = sample_order_request( - buyer_identity.public_key_hex(), - seller_identity.public_key_hex(), - ); - let mut config = RadrootsSdkConfig::production(); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::LocalIdentity; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .order() - .publish_order_request_with_identity(&buyer_identity, &listing_event_ptr(), &payload) - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedTransport { - transport: SdkTransportMode::Radrootsd, - operation: "order.publish_order_request_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_order_request_publish_rejects_draft_only_signer_mode() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let buyer_identity = RadrootsIdentity::generate(); - let seller_identity = RadrootsIdentity::generate(); - let payload = sample_order_request( - buyer_identity.public_key_hex(), - seller_identity.public_key_hex(), - ); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::DraftOnly; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .order() - .publish_order_request_with_identity(&buyer_identity, &listing_event_ptr(), &payload) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::RelayDirect, - signer: SignerConfig::DraftOnly, - required: SignerConfig::LocalIdentity, - operation: "order.publish_order_request_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_order_request_publish_rejects_invalid_economics() -> TestResult<()> { - let buyer_identity = RadrootsIdentity::generate(); - let seller_identity = RadrootsIdentity::generate(); - let mut payload = sample_order_request( - buyer_identity.public_key_hex(), - seller_identity.public_key_hex(), - ); - payload.economics.items[0].bin_count = 1; - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec!["ws://127.0.0.1:9".to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .order() - .publish_order_request_with_identity(&buyer_identity, &listing_event_ptr(), &payload) - .await - .expect_err("invalid economics"); - - assert!(matches!(error, SdkPublishError::Encode(_))); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_order_request_publish_reports_setup_error_detail() -> TestResult<()> { - let buyer_identity = RadrootsIdentity::generate(); - let seller_identity = RadrootsIdentity::generate(); - let payload = sample_order_request( - buyer_identity.public_key_hex(), - seller_identity.public_key_hex(), - ); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.network.timeout_ms = 10; - config.relay = RelayConfig { - urls: vec!["ws://127.0.0.1:9".to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .order() - .publish_order_request_with_identity(&buyer_identity, &listing_event_ptr(), &payload) - .await - .expect_err("relay setup error"); - - assert!(matches!( - error, - SdkPublishError::RelaySetup { - transport: SdkTransportMode::RelayDirect, - operation: "order.publish_order_request_with_identity", - target_relays, - error: _, - } if target_relays == vec!["ws://127.0.0.1:9".to_owned()] - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_farm_publish_rejects_radrootsd_transport_mode() -> TestResult<()> { - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::production(); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::LocalIdentity; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .farm() - .publish_with_identity(&identity, &sample_farm()) - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedTransport { - transport: SdkTransportMode::Radrootsd, - operation: "farm.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_farm_publish_rejects_draft_only_signer_mode() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::DraftOnly; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .farm() - .publish_with_identity(&identity, &sample_farm()) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::RelayDirect, - signer: SignerConfig::DraftOnly, - required: SignerConfig::LocalIdentity, - operation: "farm.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_profile_publish_accepts_sdk_built_draft() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - let draft = client - .profile() - .build_draft(&sample_profile(), Some(RadrootsProfileType::Farm))?; - - let receipt = client - .profile() - .publish_draft_with_identity(&identity, draft) - .await?; - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(0)); - assert!(receipt.event_id.is_some()); - match receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!( - receipt.event_id.as_deref(), - Some(relay_receipt.event_id.as_str()) - ); - assert_eq!(relay_receipt.event.kind, 0); - assert_eq!(relay_receipt.event.author, identity.public_key_hex()); - assert_eq!( - relay_receipt.event.tags, - vec![vec!["t".to_owned(), "radroots:type:farm".to_owned()]] - ); - assert_eq!(relay_receipt.target_relays, vec![relay.url().to_owned()]); - assert_eq!(relay_receipt.connected_relays, vec![relay.url().to_owned()]); - assert_eq!( - relay_receipt.acknowledged_relays, - vec![relay.url().to_owned()] - ); - assert!(relay_receipt.failed_relays.is_empty()); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_profile_publish_rejects_radrootsd_transport_mode() -> TestResult<()> { - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::production(); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::LocalIdentity; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .profile() - .publish_with_identity( - &identity, - &sample_profile(), - Some(RadrootsProfileType::Farm), - ) - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedTransport { - transport: SdkTransportMode::Radrootsd, - operation: "profile.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_profile_publish_rejects_draft_only_signer_mode() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::DraftOnly; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .profile() - .publish_with_identity( - &identity, - &sample_profile(), - Some(RadrootsProfileType::Farm), - ) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::RelayDirect, - signer: SignerConfig::DraftOnly, - required: SignerConfig::LocalIdentity, - operation: "profile.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_listing_publish_accepts_sdk_built_draft() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - let draft = client.listing().build_draft(&sample_listing())?; - - let receipt = client - .listing() - .publish_draft_with_identity(&identity, draft) - .await?; - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(30402)); - assert!(receipt.event_id.is_some()); - match receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!( - receipt.event_id.as_deref(), - Some(relay_receipt.event_id.as_str()) - ); - assert_eq!(receipt.event_kind, Some(relay_receipt.event_kind)); - assert_eq!(relay_receipt.event.kind, 30402); - assert_eq!(relay_receipt.event_id, relay_receipt.event.id); - assert_eq!(relay_receipt.signature, relay_receipt.event.sig); - assert_eq!(relay_receipt.created_at, relay_receipt.event.created_at); - assert_eq!(relay_receipt.event.author, identity.public_key_hex()); - assert_eq!(relay_receipt.target_relays, vec![relay.url().to_owned()]); - assert_eq!(relay_receipt.connected_relays, vec![relay.url().to_owned()]); - assert_eq!( - relay_receipt.acknowledged_relays, - vec![relay.url().to_owned()] - ); - assert!(relay_receipt.failed_relays.is_empty()); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_publish_rejects_radrootsd_transport_mode() -> TestResult<()> { - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::production(); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::LocalIdentity; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .listing() - .publish_with_identity(&identity, &sample_listing()) - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedTransport { - transport: SdkTransportMode::Radrootsd, - operation: "listing.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_publish_rejects_draft_only_signer_mode() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::DraftOnly; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .listing() - .publish_with_identity(&identity, &sample_listing()) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::RelayDirect, - signer: SignerConfig::DraftOnly, - required: SignerConfig::LocalIdentity, - operation: "listing.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_publish_rejects_nip46_signer_mode() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::Nip46; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .listing() - .publish_with_identity(&identity, &sample_listing()) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::RelayDirect, - signer: SignerConfig::Nip46, - required: SignerConfig::LocalIdentity, - operation: "listing.publish_with_identity", - } - )); - - Ok(()) -} diff --git a/crates/sdk/tests/replica_ingest.rs b/crates/sdk/tests/replica_ingest.rs @@ -1,85 +0,0 @@ -use radroots_replica_db::ReplicaSql; -use radroots_replica_db_schema::farm::IFarmFindMany; -use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event}; -use radroots_sdk::{RadrootsFarm, RadrootsNostrEvent, farm}; -use radroots_sql_core::{SqlExecutor, SqliteExecutor}; -use tempfile::{TempDir, tempdir}; - -fn seller_pubkey() -> String { - "a".repeat(64) -} - -fn sdk_event( - id: u64, - author: &str, - created_at: u32, - kind: u32, - content: String, - tags: Vec<Vec<String>>, -) -> RadrootsNostrEvent { - RadrootsNostrEvent { - id: format!("{id:064x}"), - author: author.to_owned(), - created_at, - kind, - tags, - content, - sig: "f".repeat(128), - } -} - -fn sample_farm() -> RadrootsFarm { - RadrootsFarm { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - name: "North Farm".into(), - about: Some("Organic coffee".into()), - website: None, - picture: None, - banner: None, - location: None, - tags: Some(vec!["coffee".into()]), - } -} - -fn open_replica() -> (TempDir, ReplicaSql<SqliteExecutor>) { - let dir = tempdir().expect("tempdir"); - let db_path = dir.path().join("replica.sqlite"); - let executor = SqliteExecutor::open(&db_path).expect("open sqlite"); - executor - .exec("PRAGMA foreign_keys = ON;", "[]") - .expect("enable foreign keys"); - let replica = ReplicaSql::new(executor); - replica.migrate_up().expect("migrate"); - (dir, replica) -} - -fn ingest_farm(replica: &ReplicaSql<SqliteExecutor>) -> RadrootsNostrEvent { - let farm_value = sample_farm(); - let author = seller_pubkey(); - let parts = farm::build_draft(&farm_value).expect("farm draft"); - let event = sdk_event( - 1, - &author, - 1_720_000_000, - parts.kind, - parts.content, - parts.tags, - ); - let outcome = radroots_replica_ingest_event(replica.executor(), &event).expect("ingest farm"); - assert_eq!(outcome, RadrootsReplicaIngestOutcome::Applied); - event -} - -#[test] -fn sdk_farm_draft_ingests_into_replica_projection() { - let (_dir, replica) = open_replica(); - let event = ingest_farm(&replica); - let farms = replica - .farm_find_many(&IFarmFindMany { filter: None }) - .expect("query farms") - .results; - assert_eq!(farms.len(), 1); - assert_eq!(farms[0].d_tag, sample_farm().d_tag); - assert_eq!(farms[0].name, sample_farm().name); - assert_eq!(farms[0].pubkey, event.author); -} diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -3612,9 +3612,6 @@ fn validate_contract_bundle_with_release_policy_override( if bundle.manifest.surface.algorithm_crates.is_empty() { return Err("contract surface.algorithm_crates must not be empty".to_string()); } - if bundle.manifest.surface.wasm_crates.is_empty() { - return Err("contract surface.wasm_crates must not be empty".to_string()); - } validate_export_mappings(bundle)?; if bundle.version.contract.version.trim().is_empty() { return Err("version.contract.version is required".to_string()); @@ -3983,9 +3980,6 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { if bundle.manifest.surface.algorithm_crates.is_empty() { return Err("contract surface.algorithm_crates must not be empty".to_string()); } - if bundle.manifest.surface.wasm_crates.is_empty() { - return Err("contract surface.wasm_crates must not be empty".to_string()); - } validate_export_mappings(bundle)?; if bundle.version.contract.version.trim().is_empty() { return Err("version.contract.version is required".to_string()); @@ -5581,9 +5575,6 @@ edition = "2024" assert_bundle_error("surface.algorithm_crates must not be empty", |bundle| { bundle.manifest.surface.algorithm_crates.clear(); }); - assert_bundle_error("surface.wasm_crates must not be empty", |bundle| { - bundle.manifest.surface.wasm_crates.clear(); - }); assert_bundle_error( "at least one language export mapping is required", |bundle| { diff --git a/crates/xtask/src/phase1_1.rs b/crates/xtask/src/phase1_1.rs @@ -31,7 +31,6 @@ pub fn validate_invariants(root: &Path) -> Result<(), String> { PathBuf::from("crates/events/src"), PathBuf::from("crates/events_codec/src"), PathBuf::from("crates/trade/src"), - PathBuf::from("crates/sdk/src"), ], &[ "RadrootsTradeMessageType", @@ -55,7 +54,6 @@ pub fn validate_invariants(root: &Path) -> Result<(), String> { "public_trade", "events::trade::", "events_codec::trade::", - "radroots_sdk::trade::", "trade_order_economics_digest", "trade_revision", "trade_lifecycle", diff --git a/docs/nix.md b/docs/nix.md @@ -63,7 +63,6 @@ The shells provide: - Rust `1.92.0` with `wasm32-unknown-unknown` - pinned nightly cargo for coverage from `rust-toolchain-coverage.toml` -- `wasm-pack` - `cargo-llvm-cov` - `pkg-config` - `clang` and `libclang` @@ -84,7 +83,6 @@ nix run .#fmt nix run .#check nix run .#contract nix run .#coverage-report -nix run .#wasm-builds nix run .#release-preflight ``` @@ -98,7 +96,6 @@ nix run .#release-preflight Repo-aware flows stay behind `nix run` apps because they need a real checkout: - coverage refresh and release preflight produce repo-local artifacts derived from measured per-crate gate reports -- wasm packaging writes package output directories - publish commands read runtime tokens and the live checkout state ## First Verification diff --git a/nix/apps.nix b/nix/apps.nix @@ -99,10 +99,4 @@ in pathPrefix = coveragePath; }; - wasm-builds = mkRepoApp { - name = "wasm-builds"; - description = "Build the wasm packages defined by the workspace makefile"; - runtimeInputs = common.runtimeInputs.wasm; - command = common.wasmBuildsCommand; - }; } diff --git a/nix/common.nix b/nix/common.nix @@ -18,7 +18,6 @@ let lib.fileset.unions [ ../Cargo.toml ../Cargo.lock - ../Makefile ../README ../rust-toolchain.toml ../spec @@ -85,12 +84,7 @@ let toolchains.coverage cargoLlvmCov ]; - wasmRuntimeInputs = stableRuntimeInputs ++ [ - pkgs.wasm-pack - ]; - releaseRuntimeInputs = coverageRuntimeInputs ++ [ - pkgs.wasm-pack - ]; + releaseRuntimeInputs = coverageRuntimeInputs; sdkContractCrates = [ "xtask" "radroots_core" @@ -100,7 +94,6 @@ let "radroots_identity" "radroots_replica_db_schema" "radroots_events_codec" - "radroots_events_codec_wasm" "radroots_nostr_connect" "radroots_nostr_signer" ]; @@ -186,9 +179,6 @@ let cargo test -q ${sdkContractCargoArgs} cargo run -q -p xtask -- sdk validate ''; - wasmBuildsCommand = '' - make build - ''; releasePreflightCommand = '' ./scripts/ci/release_preflight.sh ''; @@ -313,7 +303,6 @@ in sdkContractCargoArgs sharedEnv version - wasmBuildsCommand xtaskPackage ; @@ -324,6 +313,5 @@ in stable = stableRuntimeInputs; coverage = coverageRuntimeInputs; release = releaseRuntimeInputs; - wasm = wasmRuntimeInputs; }; } diff --git a/policy/coverage/policy.toml b/policy/coverage/policy.toml @@ -52,7 +52,6 @@ crates = [ "radroots_runtime_paths", "radroots_runtime_distribution", "radroots_runtime_manager", - "radroots_sdk", "radroots_identity", "radroots_secret_vault", "radroots_trade", diff --git a/spec/RCLD.md b/spec/RCLD.md @@ -147,7 +147,7 @@ Examples: - operation implemented in `radroots_events_codec` - type defined in `radroots_events` -- deterministic helper exposed via `radroots_events_codec_wasm` +- deterministic helper exposed by SDK-owned wasm bindings ## Tier 1 Domains And Operations @@ -572,7 +572,7 @@ model_crates = [ "radroots_identity", ] algorithm_crates = ["radroots_events_codec"] -wasm_crates = ["radroots_events_codec_wasm"] +wasm_crates = [] ``` This keeps current workspace knowledge available without making it the public contract unit. diff --git a/spec/README.md b/spec/README.md @@ -1,14 +1,13 @@ -# radroots_sdk +# radroots_core_contract -Curated SDK contract for the Rad Roots cross-language SDK. +Core wire, event, codec, and replica contract for Rad Roots SDK consumers. ## Purpose -This directory defines the `radroots_sdk` contract used to align Rust, -TypeScript, Python, Swift, and Kotlin surfaces. It defines the public -interoperability boundary for external integrators, keeps Rust as the canonical -source for exported models and transforms, and enforces deterministic, -machine-verifiable governance for contract changes and releases. +This directory defines the rr-rs core contract consumed by the first-class +Rad Roots SDK repository. It keeps Rust event models, wire codecs, replica core +semantics, conformance vectors, and release governance deterministic and +machine-verifiable. ## Contract Surface @@ -16,20 +15,18 @@ SDK contract metadata is defined in `spec/manifest.toml` and currently includes: - model crates: `radroots_core`, `radroots_events`, `radroots_trade`, `radroots_identity` - algorithm crate: `radroots_events_codec` -- wasm crate: `radroots_events_codec_wasm` -The curated public Rust entrypoint is `radroots_sdk`. -The crate list above records implementation provenance for the contract surface; -it is not a promise that every listed crate is a first-class end-user SDK -package. +The first-class Rust SDK and WebAssembly package surfaces are owned by the SDK +repository. The crate list above records rr-rs implementation provenance for the +core contract surface; it is not a promise that every listed crate is a +first-class end-user SDK package. Public SDK exports are intentionally narrower than the full Rust workspace. ## Field Event Substrate Field-oriented farming operations are represented in the public Rust substrate -through `radroots_events`, `radroots_events_codec`, and -`radroots_events_codec_wasm`. +through `radroots_events` and `radroots_events_codec`. The substrate includes workspace manifests, CRDT change envelopes, farm file metadata, NIP-42 relay auth, NIP-98 HTTP auth, and the supported NIP-29 group @@ -49,7 +46,7 @@ matching conformance vectors and language export mappings. ## Public Social Event Substrate Public social events are represented as event and codec substrate in -`radroots_events`, `radroots_events_codec`, and `radroots_events_codec_wasm`. +`radroots_events` and `radroots_events_codec`. The active social-event contract is defined in `spec/social-events.md`. It covers ordinary posts, comments, reactions, articles, public generic file metadata, @@ -59,17 +56,15 @@ NIP-65 relay lists through `RadrootsList`. The social surface is substrate-first. MVP social tag builders for posts, comments, reactions, articles, generic public file metadata, calendar date events, and calendar time events are promoted into curated SDK operation -metadata after their Rust models, codecs, wasm helpers, and deterministic -conformance vectors exist. Production-v1 repost, report, calendar collection, -and RSVP behavior remains available through event and codec APIs by default and -is covered by conformance vectors. +metadata after their Rust models, codecs, SDK-owned wasm helpers, and +deterministic conformance vectors exist. Production-v1 repost, report, calendar +collection, and RSVP behavior remains available through event and codec APIs by +default and is covered by conformance vectors. ## Rust Crate Tiers The public Rust story is tiered explicitly. -- Curated SDK entrypoint: - - `radroots_sdk` - Advanced substrate crates: - `radroots_core` - `radroots_events` @@ -99,14 +94,11 @@ The public Rust story is tiered explicitly. - `radroots_event_store` - `radroots_outbox` - `radroots_relay_transport` - - `radroots_events_codec_wasm` - `radroots_net` - `radroots_nostr_runtime` - `radroots_nostr_ndb` - `radroots_sql_wasm_bridge` - `radroots_sql_wasm_core` - - `radroots_replica_db_wasm` - - `radroots_replica_sync_wasm` - `radroots_simplex_chat_proto` - `radroots_simplex_smp_proto` @@ -161,9 +153,10 @@ Internal replica crate family: - `radroots_replica_db_schema` - `radroots_replica_db` -- `radroots_replica_db_wasm` - `radroots_replica_sync` -- `radroots_replica_sync_wasm` + +SDK-owned wasm bindings for replica storage and sync are recorded in +`spec/replica.toml`. ## Governance diff --git a/spec/exports/ts.toml b/spec/exports/ts.toml @@ -7,7 +7,6 @@ repository = "sdk-typescript" "radroots_events" = "@radroots/sdk" "radroots_trade" = "@radroots/sdk" "radroots_identity" = "@radroots/sdk" -"radroots_events_codec_wasm" = "@radroots/sdk" [artifacts] models_dir = "src/generated" diff --git a/spec/manifest.toml b/spec/manifest.toml @@ -1,5 +1,5 @@ [contract] -name = "radroots_sdk" +name = "radroots_core_contract" version = "0.1.0-alpha.2" source = "rust" @@ -11,14 +11,14 @@ model_crates = [ "radroots_identity", ] algorithm_crates = ["radroots_events_codec"] -wasm_crates = ["radroots_events_codec_wasm"] +wasm_crates = [] -[sdk] +[consumer_sdk] rust_package = "radroots_sdk" public_surface = "operation_first" website_ingest_contract = true -[sdk.rust_story] +[consumer_sdk.rust_story] entrypoints = ["radroots_sdk"] advanced_substrate = [ "radroots_core", @@ -51,14 +51,11 @@ deferred_publication = [ "radroots_event_store", "radroots_outbox", "radroots_relay_transport", - "radroots_events_codec_wasm", "radroots_net", "radroots_nostr_runtime", "radroots_nostr_ndb", "radroots_sql_wasm_bridge", "radroots_sql_wasm_core", - "radroots_replica_db_wasm", - "radroots_replica_sync_wasm", "radroots_simplex_chat_proto", "radroots_simplex_smp_proto", "radroots_sp1_guest_trade", @@ -68,9 +65,9 @@ deferred_publication = [ [surface.internal_replica_crates] schema = "radroots_replica_db_schema" storage = "radroots_replica_db" -storage_wasm = "radroots_replica_db_wasm" +storage_wasm_binding_crate = "radroots_replica_db_wasm" sync = "radroots_replica_sync" -sync_wasm = "radroots_replica_sync_wasm" +sync_wasm_binding_crate = "radroots_replica_sync_wasm" [export.ts] packages = ["@radroots/sdk"] diff --git a/spec/operations.toml b/spec/operations.toml @@ -1,5 +1,5 @@ [contract] -name = "radroots_sdk" +name = "radroots_core_contract" version = "0.1.0-alpha.2" source = "rust" @@ -53,9 +53,9 @@ model_crates = [ "radroots_identity", ] algorithm_crates = ["radroots_events_codec"] -wasm_crates = ["radroots_events_codec_wasm"] +wasm_crates = [] -[sdk] +[consumer_sdk] rust_package = "radroots_sdk" primary_domains = [ "profile", diff --git a/spec/replica.toml b/spec/replica.toml @@ -6,9 +6,11 @@ purpose = "offline-first local replica state and deterministic sync" [crate_family] schema = "radroots_replica_db_schema" storage = "radroots_replica_db" -storage_wasm = "radroots_replica_db_wasm" sync = "radroots_replica_sync" -sync_wasm = "radroots_replica_sync_wasm" + +[sdk_wasm_bindings] +storage = "radroots_replica_db_wasm" +sync = "radroots_replica_sync_wasm" [policy] transport_agnostic_sync_core = true diff --git a/spec/social-events.md b/spec/social-events.md @@ -2,7 +2,7 @@ Status: active implementation contract -Scope: public Radroots social Nostr event models, codecs, wasm builders, and deterministic +Scope: public Radroots social Nostr event models, codecs, SDK-owned wasm builders, and deterministic conformance vectors in this repository. ## Purpose @@ -13,7 +13,7 @@ services, and private Field business documents outside this repository's event-c The target implementation is standards-first and Radroots-named. Event models live in `radroots_events`, canonical encode/decode behavior lives in `radroots_events_codec`, optional JSON -to tags helpers live in `radroots_events_codec_wasm`, and deterministic fixtures live under +to tags helpers are provided by SDK-owned wasm bindings, and deterministic fixtures live under `spec/conformance`. ## Implementation Inventory @@ -116,11 +116,11 @@ helpers, and conformance vectors exist. Production-v1 repost, report, calendar c behavior remains substrate-visible by default unless a consumer proves that it should be promoted into the curated operation surface. -`radroots_events_codec_wasm` exposes the canonical JSON-to-tags helper names `post_tags`, +The SDK-owned events codec wasm binding exposes the canonical JSON-to-tags helper names `post_tags`, `comment_tags`, `reaction_tags`, `article_tags`, `file_metadata_tags`, `calendar_date_event_tags`, `calendar_time_event_tags`, `calendar_tags`, `calendar_event_rsvp_tags`, `repost_tags`, `generic_repost_tags`, and `report_tags` for the public -social substrate. The same wasm crate exposes `farm_workspace_manifest_tags`, +social substrate. The same SDK-owned wasm binding exposes `farm_workspace_manifest_tags`, `farm_crdt_change_tags`, `farm_file_metadata_tags`, `relay_auth_tags`, and `http_auth_tags` for the field event substrate.