commit 9fc2698c8ab6731b93e3fb747af4486f178252ac
parent 64dc049f7564df0346dfde99a64705ff383a600f
Author: triesap <tyson@radroots.org>
Date: Sat, 13 Jun 2026 15:08:32 -0700
sdk: own Rust SDK and wasm packages
- move radroots_sdk into the SDK workspace as the first-class Rust SDK crate
- add SDK-owned wasm binding crates and tracked npm package manifests
- teach xtask to generate wasm packages with wasm-pack --no-pack
- document SDK workspace ownership across Rust, TypeScript, and wasm surfaces
Diffstat:
44 files changed, 14948 insertions(+), 88 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -13,12 +13,125 @@ dependencies = [
]
[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
+[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-utility"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151"
+dependencies = [
+ "futures-util",
+ "gloo-timers",
+ "tokio",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-wsocket"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069"
+dependencies = [
+ "async-utility",
+ "futures",
+ "futures-util",
+ "js-sys",
+ "tokio",
+ "tokio-rustls",
+ "tokio-socks",
+ "tokio-tungstenite",
+ "url",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "atomic-destructor"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -26,6 +139,12 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
@@ -71,6 +190,15 @@ dependencies = [
]
[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -95,6 +223,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[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"
@@ -120,6 +260,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -144,6 +290,19 @@ dependencies = [
]
[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -155,6 +314,60 @@ dependencies = [
]
[[package]]
+name = "config"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
+dependencies = [
+ "async-trait",
+ "convert_case",
+ "json5",
+ "nom",
+ "pathdiff",
+ "ron",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "toml",
+ "yaml-rust2",
+]
+
+[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.17",
+ "once_cell",
+ "tiny-keccak",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -164,17 +377,50 @@ dependencies = [
]
[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
- "rand_core",
+ "rand_core 0.6.4",
"typenum",
]
[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+
+[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -197,12 +443,76 @@ dependencies = [
]
[[package]]
+name = "dlv-list"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+dependencies = [
+ "const-random",
+]
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -212,12 +522,71 @@ dependencies = [
]
[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -229,8 +598,13 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
+ "futures-channel",
"futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
"futures-task",
+ "memchr",
"pin-project-lite",
"slab",
]
@@ -259,39 +633,249 @@ dependencies = [
]
[[package]]
-name = "hex"
-version = "0.4.3"
+name = "getrandom"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+ "wasm-bindgen",
+]
[[package]]
-name = "hex-conservative"
-version = "0.2.2"
+name = "getrandom"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
- "arrayvec",
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
]
[[package]]
-name = "hmac"
-version = "0.12.1"
+name = "gloo-timers"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
dependencies = [
- "digest",
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
]
[[package]]
-name = "icu_collections"
-version = "2.2.0"
+name = "hashbrown"
+version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
- "displaydoc",
- "potential_utf",
- "utf8_iter",
- "yoke",
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "hashlink"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hex-conservative"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots 1.0.7",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
"zerofrom",
"zerovec",
]
@@ -365,6 +949,12 @@ dependencies = [
]
[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -386,6 +976,18 @@ dependencies = [
]
[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -408,6 +1010,12 @@ dependencies = [
]
[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -425,37 +1033,154 @@ dependencies = [
]
[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
name = "litemap"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
+name = "log"
+version = "0.4.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
+
+[[package]]
+name = "lru"
+version = "0.16.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
name = "memchr"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[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"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "negentropy"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
name = "nostr"
version = "0.44.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d8f0fe13526800300a36bf3b7c5f752e62e32ab81c74a8e5caa2865708625a"
dependencies = [
- "base64",
+ "aes",
+ "base64 0.22.1",
"bech32",
"bip39",
"bitcoin_hashes",
"cbc",
"chacha20",
"chacha20poly1305",
- "getrandom",
+ "getrandom 0.2.17",
"hex",
"instant",
"scrypt",
@@ -467,12 +1192,81 @@ dependencies = [
]
[[package]]
+name = "nostr-database"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1"
+dependencies = [
+ "lru",
+ "nostr",
+ "tokio",
+]
+
+[[package]]
+name = "nostr-gossip"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6"
+dependencies = [
+ "nostr",
+]
+
+[[package]]
+name = "nostr-relay-pool"
+version = "0.44.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91b2c039df4f96c4bf7dae52a74fd5516ad6dda83a11c0c69dea91b5255a4f37"
+dependencies = [
+ "async-utility",
+ "async-wsocket",
+ "atomic-destructor",
+ "hex",
+ "lru",
+ "negentropy",
+ "nostr",
+ "nostr-database",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "nostr-sdk"
+version = "0.44.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393"
+dependencies = [
+ "async-utility",
+ "nostr",
+ "nostr-database",
+ "nostr-gossip",
+ "nostr-relay-pool",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
+ "libm",
]
[[package]]
@@ -482,23 +1276,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[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"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
+name = "ordered-multimap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.14.5",
+]
+
+[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
- "rand_core",
+ "rand_core 0.6.4",
"subtle",
]
[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -515,18 +1331,67 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
-name = "pin-project-lite"
-version = "0.2.17"
+name = "pest"
+version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662"
+dependencies = [
+ "memchr",
+ "ucd-trie",
+]
[[package]]
-name = "poly1305"
-version = "0.8.0"
+name = "pest_derive"
+version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77"
dependencies = [
- "cpufeatures",
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220"
+dependencies = [
+ "pest",
+ "sha2",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+dependencies = [
+ "cpufeatures",
"opaque-debug",
"universal-hash",
]
@@ -541,6 +1406,12 @@ dependencies = [
]
[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -550,6 +1421,16 @@ dependencies = [
]
[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -559,6 +1440,61 @@ dependencies = [
]
[[package]]
+name = "quinn"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand 0.9.4",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -568,6 +1504,18 @@ dependencies = [
]
[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
name = "radroots_core"
version = "0.1.0-alpha.2"
dependencies = [
@@ -588,8 +1536,11 @@ dependencies = [
name = "radroots_events"
version = "0.1.0-alpha.2"
dependencies = [
+ "hex",
"radroots_core",
"serde",
+ "serde_json",
+ "sha2",
]
[[package]]
@@ -604,10 +1555,23 @@ dependencies = [
name = "radroots_events_codec"
version = "0.1.0-alpha.2"
dependencies = [
+ "nostr",
+ "radroots_core",
+ "radroots_events",
+ "serde",
+ "serde_json",
+]
+
+[[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]]
@@ -627,12 +1591,14 @@ name = "radroots_identity"
version = "0.1.0-alpha.2"
dependencies = [
"nostr",
+ "radroots_events",
"radroots_protected_store",
+ "radroots_runtime",
"radroots_runtime_paths",
"radroots_secret_vault",
"serde",
"serde_json",
- "thiserror",
+ "thiserror 1.0.69",
"tracing",
]
@@ -645,11 +1611,63 @@ dependencies = [
]
[[package]]
+name = "radroots_log"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "chrono",
+ "thiserror 1.0.69",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "radroots_nostr"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "nostr",
+ "nostr-sdk",
+ "radroots_identity",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "radroots_nostr_connect"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "nostr",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "url",
+]
+
+[[package]]
+name = "radroots_nostr_signer"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "hex",
+ "nostr",
+ "radroots_identity",
+ "radroots_nostr",
+ "radroots_nostr_connect",
+ "radroots_runtime",
+ "serde",
+ "serde_json",
+ "sha2",
+ "thiserror 1.0.69",
+ "url",
+ "uuid",
+]
+
+[[package]]
name = "radroots_protected_store"
version = "0.1.0-alpha.2"
dependencies = [
"chacha20poly1305",
- "getrandom",
+ "getrandom 0.2.17",
"radroots_secret_vault",
"serde",
"serde_json",
@@ -657,6 +1675,19 @@ dependencies = [
]
[[package]]
+name = "radroots_replica_db"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "hex",
+ "radroots_replica_db_schema",
+ "radroots_sql_core",
+ "radroots_types",
+ "serde",
+ "serde_json",
+ "sha2",
+]
+
+[[package]]
name = "radroots_replica_db_schema"
version = "0.1.0-alpha.2"
dependencies = [
@@ -674,11 +1705,111 @@ 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 = [
+ "base64 0.22.1",
+ "hex",
+ "radroots_core",
+ "radroots_events",
+ "radroots_events_codec",
+ "radroots_replica_db",
+ "radroots_replica_db_schema",
+ "radroots_sql_core",
+ "radroots_types",
+ "serde",
+ "serde_json",
+ "sha2",
+ "uuid",
+]
+
+[[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 = [
+ "anyhow",
+ "chacha20poly1305",
+ "config",
+ "getrandom 0.2.17",
+ "radroots_log",
+ "radroots_protected_store",
+ "radroots_runtime_paths",
+ "radroots_secret_vault",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "thiserror 1.0.69",
+ "tokio",
+ "toml",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
name = "radroots_runtime_paths"
version = "0.1.0-alpha.2"
dependencies = [
"serde",
- "thiserror",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "radroots_sdk"
+version = "0.1.0"
+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]]
@@ -705,10 +1836,43 @@ name = "radroots_secret_vault"
version = "0.1.0-alpha.2"
[[package]]
+name = "radroots_sql_core"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "chrono",
+ "radroots_sql_wasm_bridge",
+ "rusqlite",
+ "serde",
+ "serde-wasm-bindgen",
+ "serde_json",
+ "uuid",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "radroots_sql_wasm_bridge"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "radroots_sql_wasm_core"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "radroots_sql_core",
+ "radroots_sql_wasm_bridge",
+ "serde",
+ "serde-wasm-bindgen",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "radroots_trade"
version = "0.1.0-alpha.2"
dependencies = [
- "base64",
+ "base64 0.22.1",
"hex",
"radroots_core",
"radroots_events",
@@ -716,7 +1880,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
- "thiserror",
+ "thiserror 1.0.69",
]
[[package]]
@@ -750,8 +1914,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
]
[[package]]
@@ -761,7 +1935,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
]
[[package]]
@@ -770,49 +1954,232 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
- "getrandom",
+ "getrandom 0.2.17",
]
[[package]]
-name = "rust_decimal"
-version = "1.42.0"
+name = "rand_core"
+version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
- "arrayvec",
- "num-traits",
- "serde",
- "wasm-bindgen",
+ "getrandom 0.3.4",
]
[[package]]
-name = "rust_decimal_macros"
-version = "1.40.0"
+name = "regex-automata"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
- "quote",
- "syn",
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
]
[[package]]
-name = "rustversion"
-version = "1.0.22"
+name = "regex-syntax"
+version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
-name = "salsa20"
-version = "0.10.2"
+name = "reqwest"
+version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
- "cipher",
-]
-
-[[package]]
-name = "scrypt"
-version = "0.11.0"
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots 1.0.7",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "ron"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+dependencies = [
+ "base64 0.21.7",
+ "bitflags",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "rusqlite"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
+dependencies = [
+ "bitflags",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink 0.9.1",
+ "libsqlite3-sys",
+ "smallvec",
+]
+
+[[package]]
+name = "rust-ini"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+]
+
+[[package]]
+name = "rust_decimal"
+version = "1.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995"
+dependencies = [
+ "arrayvec",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "rust_decimal_macros"
+version = "1.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "salsa20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
+dependencies = [
+ "cipher",
+]
+
+[[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 = "scrypt"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
@@ -828,7 +2195,7 @@ version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
dependencies = [
- "rand",
+ "rand 0.8.6",
"secp256k1-sys",
"serde",
]
@@ -843,6 +2210,12 @@ dependencies = [
]
[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -853,6 +2226,17 @@ dependencies = [
]
[[package]]
+name = "serde-wasm-bindgen"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -886,6 +2270,38 @@ dependencies = [
]
[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -897,12 +2313,31 @@ dependencies = [
]
[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -915,6 +2350,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -927,6 +2372,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
+name = "symlink"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
+
+[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -938,6 +2389,15 @@ dependencies = [
]
[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -949,12 +2409,34 @@ dependencies = [
]
[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
- "thiserror-impl",
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
]
[[package]]
@@ -969,6 +2451,65 @@ dependencies = [
]
[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
name = "tinystr"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -994,53 +2535,299 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
-name = "tracing"
-version = "0.1.44"
+name = "tokio"
+version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [
+ "bytes",
+ "libc",
+ "mio",
"pin-project-lite",
- "tracing-core",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "tracing-core"
-version = "0.1.36"
+name = "tokio-macros"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "typenum"
-version = "1.20.1"
+name = "tokio-rustls"
+version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
[[package]]
-name = "unicode-ident"
-version = "1.0.24"
+name = "tokio-socks"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615"
+dependencies = [
+ "either",
+ "futures-util",
+ "thiserror 1.0.69",
+ "tokio",
+]
[[package]]
-name = "unicode-normalization"
-version = "0.1.25"
+name = "tokio-tungstenite"
+version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
+checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
- "tinyvec",
+ "futures-util",
+ "log",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tungstenite",
+ "webpki-roots 0.26.11",
]
[[package]]
-name = "universal-hash"
-version = "0.5.1"
+name = "toml"
+version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
- "crypto-common",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-appender"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
+dependencies = [
+ "crossbeam-channel",
+ "symlink",
+ "thiserror 2.0.18",
+ "time",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.9.4",
+ "rustls",
+ "rustls-pki-types",
+ "sha1",
+ "thiserror 2.0.18",
+ "utf-8",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
"subtle",
]
[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
name = "url"
version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1054,24 +2841,90 @@ dependencies = [
]
[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
+name = "uuid"
+version = "1.23.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
+dependencies = [
+ "getrandom 0.4.2",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
name = "version_check"
version = "0.9.5"
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"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
+]
+
+[[package]]
name = "wasm-bindgen"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1086,6 +2939,16 @@ dependencies = [
]
[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "wasm-bindgen-macro"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1118,6 +2981,79 @@ dependencies = [
]
[[package]]
+name = "wasm-bindgen-test"
+version = "0.3.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28a0782b173cf2e98f62aacb487c5c021b7c5925df46098675f28e1fa85e159"
+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.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee997551ce1ad5adda03f7ce37ec34b4140fe9f547fd07b46d55901d1ba1a06b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "wasm-bindgen-test-shared"
+version = "0.2.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae0f005eb61f765eff31a79ef71df7e21819731268a868df41192c1e41d6d3e5"
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
name = "web-sys"
version = "0.3.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1128,12 +3064,378 @@ dependencies = [
]
[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+dependencies = [
+ "webpki-roots 1.0.7",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[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 = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
name = "writeable"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
+name = "yaml-rust2"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink 0.8.4",
+]
+
+[[package]]
name = "yoke"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -2,10 +2,14 @@
members = [
"crates/binding_model",
"crates/core_bindings",
+ "crates/events_codec_wasm",
"crates/events_bindings",
"crates/events_indexed_bindings",
"crates/identity_bindings",
+ "crates/replica_db_wasm",
"crates/replica_db_schema_bindings",
+ "crates/replica_sync_wasm",
+ "crates/sdk",
"crates/trade_bindings",
"crates/types_bindings",
"crates/xtask",
@@ -24,13 +28,42 @@ readme = "README"
[workspace.dependencies]
radroots_core = { path = "../lib/crates/core", version = "0.1.0-alpha.2", default-features = false }
radroots_events = { path = "../lib/crates/events", version = "0.1.0-alpha.2", default-features = false }
+radroots_events_codec = { path = "../lib/crates/events_codec", version = "0.1.0-alpha.2", default-features = false }
radroots_events_indexed = { path = "../lib/crates/events_indexed", version = "0.1.0-alpha.2", default-features = false }
radroots_identity = { path = "../lib/crates/identity", version = "0.1.0-alpha.2", default-features = false, features = [
"std",
] }
+radroots_nostr = { path = "../lib/crates/nostr", version = "0.1.0-alpha.2", default-features = false }
+radroots_nostr_connect = { path = "../lib/crates/nostr_connect", version = "0.1.0-alpha.2", default-features = false }
+radroots_nostr_signer = { path = "../lib/crates/nostr_signer", version = "0.1.0-alpha.2", default-features = false }
+radroots_replica_db = { path = "../lib/crates/replica_db", version = "0.1.0-alpha.2", default-features = false }
radroots_replica_db_schema = { path = "../lib/crates/replica_db_schema", version = "0.1.0-alpha.2", default-features = false }
+radroots_replica_sync = { path = "../lib/crates/replica_sync", version = "0.1.0-alpha.2", default-features = false }
+radroots_sql_core = { path = "../lib/crates/sql_core", version = "0.1.0-alpha.2", default-features = false }
+radroots_sql_wasm_core = { path = "../lib/crates/sql_wasm_core", version = "0.1.0-alpha.2", default-features = false }
radroots_trade = { path = "../lib/crates/trade", version = "0.1.0-alpha.2", default-features = false, features = [
"serde_json",
"std",
] }
radroots_types = { path = "../lib/crates/types", version = "0.1.0-alpha.2", default-features = false }
+
+base64 = { version = "0.22", default-features = false, features = ["alloc"] }
+futures = { version = "0.3" }
+js-sys = { version = "0.3" }
+nostr = { version = "0.44.2" }
+reqwest = { version = "0.12", default-features = false, features = [
+ "json",
+ "rustls-tls",
+] }
+serde = { version = "1", default-features = false, features = [
+ "derive",
+ "alloc",
+] }
+serde_json = { version = "1", default-features = false, features = ["alloc"] }
+serde-wasm-bindgen = { version = "0.6" }
+tempfile = { version = "3" }
+tokio = { version = "1" }
+tokio-tungstenite = "0.26.2"
+uuid = { version = "1.22.0", features = ["v4", "v7"] }
+wasm-bindgen = { version = "0.2" }
+wasm-bindgen-test = { version = "0.3" }
diff --git a/crates/events_codec_wasm/Cargo.toml b/crates/events_codec_wasm/Cargo.toml
@@ -0,0 +1,34 @@
+[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
@@ -0,0 +1,24 @@
+# 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/src/lib.rs b/crates/events_codec_wasm/src/lib.rs
@@ -0,0 +1,1149 @@
+#![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
@@ -0,0 +1,36 @@
+[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
@@ -0,0 +1,25 @@
+# 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/src/lib.rs b/crates/replica_db_wasm/src/lib.rs
@@ -0,0 +1,29 @@
+#![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
@@ -0,0 +1,13 @@
+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
@@ -0,0 +1,891 @@
+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
@@ -0,0 +1,35 @@
+[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
@@ -0,0 +1,25 @@
+# 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/src/lib.rs b/crates/replica_sync_wasm/src/lib.rs
@@ -0,0 +1,124 @@
+#![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
@@ -0,0 +1,78 @@
+[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
@@ -0,0 +1,23 @@
+# 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
@@ -0,0 +1,8 @@
+#[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
@@ -0,0 +1,835 @@
+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
@@ -0,0 +1,96 @@
+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
@@ -0,0 +1,24 @@
+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
@@ -0,0 +1,63 @@
+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
@@ -0,0 +1,2711 @@
+#[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
@@ -0,0 +1,388 @@
+#[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
@@ -0,0 +1,9 @@
+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
@@ -0,0 +1,33 @@
+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
@@ -0,0 +1,72 @@
+#![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
@@ -0,0 +1,40 @@
+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
@@ -0,0 +1,281 @@
+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
@@ -0,0 +1,12 @@
+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
@@ -0,0 +1,906 @@
+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
@@ -0,0 +1,562 @@
+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
@@ -0,0 +1,269 @@
+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
@@ -0,0 +1,1952 @@
+#![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
@@ -0,0 +1,1406 @@
+#![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
@@ -0,0 +1,85 @@
+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/check.rs b/crates/xtask/src/check.rs
@@ -3,7 +3,9 @@ use std::{fs, path::Path};
use crate::{
fs::workspace_root,
output::package_outputs,
- package_matrix::{FORBIDDEN_PACKAGE_NAMES, package_specs, validate_package_matrix},
+ package_matrix::{
+ FORBIDDEN_PACKAGE_NAMES, package_specs, validate_package_matrix, wasm_package_specs,
+ },
};
pub fn check() -> Result<(), String> {
@@ -20,6 +22,10 @@ pub fn check() -> Result<(), String> {
return Err(format!("missing package index: {}", index_path.display()));
}
}
+ for spec in wasm_package_specs() {
+ let package_dir = root.join(spec.package_dir);
+ check_package_json(&package_dir.join("package.json"), spec.package_name)?;
+ }
for output in package_outputs() {
for expected in output.files() {
let path = root
@@ -47,6 +53,9 @@ fn check_binding_crate_sources(root: &Path) -> Result<(), String> {
}
check_no_typescript_files(&crate_src_dir)?;
}
+ for spec in wasm_package_specs() {
+ check_no_typescript_files(&root.join(spec.crate_dir).join("src"))?;
+ }
Ok(())
}
diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs
@@ -5,6 +5,7 @@ mod manifest;
mod output;
mod package_matrix;
mod ts;
+mod wasm;
fn main() {
if let Err(error) = run(std::env::args().skip(1)) {
@@ -17,6 +18,9 @@ fn run(args: impl IntoIterator<Item = String>) -> Result<(), String> {
let args = args.into_iter().collect::<Vec<_>>();
match args.as_slice() {
[command, target] if command == "generate" && target == "ts" => generate::generate_ts(),
+ [command, target, rest @ ..] if command == "generate" && target == "wasm" => {
+ wasm::generate(rest)
+ }
[command] if command == "check" => check::check(),
[] => Err(usage()),
_ => Err(usage()),
@@ -24,7 +28,8 @@ fn run(args: impl IntoIterator<Item = String>) -> Result<(), String> {
}
fn usage() -> String {
- "usage: cargo xtask generate ts | cargo xtask check".to_owned()
+ "usage: cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask check"
+ .to_owned()
}
#[cfg(test)]
diff --git a/crates/xtask/src/package_matrix.rs b/crates/xtask/src/package_matrix.rs
@@ -9,6 +9,17 @@ pub struct PackageSpec {
pub package_dir: &'static str,
}
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct WasmPackageSpec {
+ pub key: &'static str,
+ pub crate_name: &'static str,
+ pub crate_dir: &'static str,
+ pub package_name: &'static str,
+ pub package_dir: &'static str,
+ pub out_name: &'static str,
+ pub out_dir: &'static str,
+}
+
pub const PACKAGE_SPECS: [PackageSpec; 7] = [
PackageSpec {
key: "core",
@@ -61,6 +72,36 @@ pub const PACKAGE_SPECS: [PackageSpec; 7] = [
},
];
+pub const WASM_PACKAGE_SPECS: [WasmPackageSpec; 3] = [
+ WasmPackageSpec {
+ key: "events_codec",
+ crate_name: "radroots_events_codec_wasm",
+ crate_dir: "crates/events_codec_wasm",
+ package_name: "@radroots/events-codec-wasm",
+ package_dir: "packages/events-codec-wasm",
+ out_name: "radroots_events_codec_wasm",
+ out_dir: "../../packages/events-codec-wasm/dist",
+ },
+ WasmPackageSpec {
+ key: "replica_db",
+ crate_name: "radroots_replica_db_wasm",
+ crate_dir: "crates/replica_db_wasm",
+ package_name: "@radroots/replica-db-wasm",
+ package_dir: "packages/replica-db-wasm",
+ out_name: "radroots_replica_db_wasm",
+ out_dir: "../../packages/replica-db-wasm/dist",
+ },
+ WasmPackageSpec {
+ key: "replica_sync",
+ crate_name: "radroots_replica_sync_wasm",
+ crate_dir: "crates/replica_sync_wasm",
+ package_name: "@radroots/replica-sync-wasm",
+ package_dir: "packages/replica-sync-wasm",
+ out_name: "radroots_replica_sync_wasm",
+ out_dir: "../../packages/replica-sync-wasm/dist",
+ },
+];
+
pub const FORBIDDEN_PACKAGE_NAMES: [&str; 2] =
["@radroots/tangle-db-schema-bindings", "@radroots/contracts"];
@@ -68,6 +109,10 @@ pub fn package_specs() -> &'static [PackageSpec] {
&PACKAGE_SPECS
}
+pub fn wasm_package_specs() -> &'static [WasmPackageSpec] {
+ &WASM_PACKAGE_SPECS
+}
+
pub fn validate_package_matrix() -> Result<(), String> {
let mut crate_names = BTreeSet::new();
let mut package_names = BTreeSet::new();
@@ -95,12 +140,37 @@ pub fn validate_package_matrix() -> Result<(), String> {
));
}
}
+ for spec in wasm_package_specs() {
+ if FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name) {
+ return Err(format!(
+ "forbidden package in matrix: {}",
+ spec.package_name
+ ));
+ }
+ if !crate_names.insert(spec.crate_name) {
+ return Err(format!("duplicate crate in matrix: {}", spec.crate_name));
+ }
+ if !package_names.insert(spec.package_name) {
+ return Err(format!(
+ "duplicate package in matrix: {}",
+ spec.package_name
+ ));
+ }
+ if !package_dirs.insert(spec.package_dir) {
+ return Err(format!(
+ "duplicate package directory in matrix: {}",
+ spec.package_dir
+ ));
+ }
+ }
Ok(())
}
#[cfg(test)]
mod tests {
- use super::{FORBIDDEN_PACKAGE_NAMES, package_specs, validate_package_matrix};
+ use super::{
+ FORBIDDEN_PACKAGE_NAMES, package_specs, validate_package_matrix, wasm_package_specs,
+ };
#[test]
fn package_matrix_is_valid() {
@@ -110,6 +180,7 @@ mod tests {
#[test]
fn approved_package_count_is_stable() {
assert_eq!(package_specs().len(), 7);
+ assert_eq!(wasm_package_specs().len(), 3);
}
#[test]
@@ -117,6 +188,9 @@ mod tests {
for spec in package_specs() {
assert!(!FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name));
}
+ for spec in wasm_package_specs() {
+ assert!(!FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name));
+ }
}
#[test]
@@ -127,4 +201,13 @@ mod tests {
.any(|spec| spec.package_name == "@radroots/replica-db-schema-bindings")
);
}
+
+ #[test]
+ fn wasm_packages_use_sdk_package_names() {
+ assert!(
+ wasm_package_specs()
+ .iter()
+ .any(|spec| spec.package_name == "@radroots/replica-db-wasm")
+ );
+ }
}
diff --git a/crates/xtask/src/wasm.rs b/crates/xtask/src/wasm.rs
@@ -0,0 +1,112 @@
+use std::{env, fs, path::Path, process::Command};
+
+use crate::{
+ fs::workspace_root,
+ package_matrix::{WasmPackageSpec, validate_package_matrix, wasm_package_specs},
+};
+
+pub fn generate(args: &[String]) -> Result<(), String> {
+ validate_package_matrix()?;
+ let specs = selected_specs(args)?;
+ let root = workspace_root()?;
+ for spec in specs {
+ let dist_dir = root.join(spec.package_dir).join("dist");
+ if dist_dir.exists() {
+ fs::remove_dir_all(&dist_dir)
+ .map_err(|error| format!("failed to remove {}: {error}", dist_dir.display()))?;
+ }
+ let mut command = Command::new("wasm-pack");
+ command
+ .current_dir(&root)
+ .arg("build")
+ .arg(spec.crate_dir)
+ .arg("--release")
+ .arg("--target")
+ .arg("web")
+ .arg("--out-dir")
+ .arg(spec.out_dir)
+ .arg("--out-name")
+ .arg(spec.out_name)
+ .arg("--no-pack");
+ if let Some(rustc) = rustup_tool("rustc") {
+ if let Some(parent) = Path::new(&rustc).parent() {
+ prepend_path(&mut command, parent);
+ }
+ command.env("RUSTC", rustc);
+ }
+ if let Some(cargo) = rustup_tool("cargo") {
+ command.env("CARGO", cargo);
+ }
+ let status = command
+ .status()
+ .map_err(|error| format!("failed to start wasm-pack for {}: {error}", spec.key))?;
+ if !status.success() {
+ return Err(format!("wasm-pack failed for {}", spec.key));
+ }
+ println!("generated wasm package {}", spec.package_name);
+ }
+ Ok(())
+}
+
+fn selected_specs(args: &[String]) -> Result<Vec<WasmPackageSpec>, String> {
+ match args {
+ [] => Ok(wasm_package_specs().to_vec()),
+ [flag, key] if flag == "--package" => wasm_package_specs()
+ .iter()
+ .copied()
+ .find(|spec| spec.key == key)
+ .map(|spec| vec![spec])
+ .ok_or_else(|| format!("unknown wasm package: {key}")),
+ _ => Err("usage: cargo xtask generate wasm [--package <key>]".to_owned()),
+ }
+}
+
+fn rustup_tool(name: &str) -> Option<String> {
+ let output = Command::new("rustup")
+ .arg("which")
+ .arg(name)
+ .output()
+ .ok()?;
+ if !output.status.success() {
+ return None;
+ }
+ let path = String::from_utf8(output.stdout).ok()?;
+ let trimmed = path.trim();
+ (!trimmed.is_empty()).then(|| trimmed.to_owned())
+}
+
+fn prepend_path(command: &mut Command, prefix: &Path) {
+ let existing = env::var_os("PATH").unwrap_or_default();
+ let mut paths = vec![prefix.to_path_buf()];
+ paths.extend(env::split_paths(&existing));
+ if let Ok(joined) = env::join_paths(paths) {
+ command.env("PATH", joined);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{rustup_tool, selected_specs};
+
+ #[test]
+ fn selects_all_specs_by_default() {
+ assert_eq!(selected_specs(&[]).expect("all specs").len(), 3);
+ }
+
+ #[test]
+ fn selects_one_spec_by_key() {
+ let specs = selected_specs(&["--package".to_owned(), "replica_db".to_owned()])
+ .expect("replica db spec");
+ assert_eq!(specs[0].package_name, "@radroots/replica-db-wasm");
+ }
+
+ #[test]
+ fn rejects_unknown_spec_key() {
+ assert!(selected_specs(&["--package".to_owned(), "missing".to_owned()]).is_err());
+ }
+
+ #[test]
+ fn rustup_tool_resolution_is_non_panicking() {
+ let _ = rustup_tool("rustc");
+ }
+}
diff --git a/package.json b/package.json
@@ -3,6 +3,8 @@
"private": true,
"packageManager": "pnpm@10.25.0",
"scripts": {
+ "generate:ts": "cargo xtask generate ts",
+ "generate:wasm": "cargo xtask generate wasm",
"build": "pnpm -r build",
"typecheck": "pnpm -r typecheck"
},
diff --git a/packages/events-codec-wasm/package.json b/packages/events-codec-wasm/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@radroots/events-codec-wasm",
+ "description": "WebAssembly bindings for radroots_events_codec",
+ "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"
+ }
+ },
+ "scripts": {
+ "build": "cd ../.. && cargo xtask generate wasm --package events_codec",
+ "typecheck": "node -e \"require('fs').accessSync('dist/radroots_events_codec_wasm.d.ts')\""
+ },
+ "sideEffects": false
+}
diff --git a/packages/replica-db-wasm/package.json b/packages/replica-db-wasm/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@radroots/replica-db-wasm",
+ "description": "WebAssembly bindings for radroots_replica_db",
+ "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"
+ }
+ },
+ "scripts": {
+ "build": "cd ../.. && cargo xtask generate wasm --package replica_db",
+ "typecheck": "node -e \"require('fs').accessSync('dist/radroots_replica_db_wasm.d.ts')\""
+ },
+ "sideEffects": false
+}
diff --git a/packages/replica-sync-wasm/package.json b/packages/replica-sync-wasm/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@radroots/replica-sync-wasm",
+ "description": "WebAssembly bindings for radroots_replica_sync",
+ "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"
+ }
+ },
+ "scripts": {
+ "build": "cd ../.. && cargo xtask generate wasm --package replica_sync",
+ "typecheck": "node -e \"require('fs').accessSync('dist/radroots_replica_sync_wasm.d.ts')\""
+ },
+ "sideEffects": false
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
@@ -20,6 +20,8 @@ importers:
specifier: workspace:*
version: link:../core-bindings
+ packages/events-codec-wasm: {}
+
packages/events-indexed-bindings: {}
packages/identity-bindings: {}
@@ -30,6 +32,10 @@ importers:
specifier: workspace:*
version: link:../types-bindings
+ packages/replica-db-wasm: {}
+
+ packages/replica-sync-wasm: {}
+
packages/trade-bindings:
dependencies:
'@radroots/core-bindings':
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
@@ -1,2 +1,3 @@
[toolchain]
channel = "1.92.0"
+targets = ["wasm32-unknown-unknown"]