cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 390ed99bc5e4115456aa5ee94a3695794ebeda61
parent 82d344492ad13527c23f9c7b87b952c2ebd74205
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Apr 2026 22:49:57 +0000

cli: add direct relay publish boundary

- enable the rr-rs direct relay client surface for the CLI
- add a local-key helper that signs WireEventParts before relay publish
- capture relay targets acknowledgements failures and signed event metadata
- cover missing relay rejection before async runtime work

Diffstat:
MCargo.lock | 461+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MCargo.toml | 3++-
Asrc/runtime/direct_relay.rs | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/runtime/mod.rs | 1+
4 files changed, 639 insertions(+), 8 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -160,6 +160,43 @@ dependencies = [ ] [[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 = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -268,6 +305,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -506,11 +549,17 @@ 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 = "dbus" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -579,6 +628,12 @@ dependencies = [ ] [[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -685,6 +740,70 @@ 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-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-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[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" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -709,18 +828,42 @@ dependencies = [ [[package]] name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] [[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -788,6 +931,22 @@ dependencies = [ ] [[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] name = "iana-time-zone" version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -972,6 +1131,8 @@ version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1084,6 +1245,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" + +[[package]] name = "matchers" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1126,6 +1293,12 @@ dependencies = [ ] [[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" @@ -1161,6 +1334,59 @@ 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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" +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" @@ -1267,7 +1493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1446,6 +1672,12 @@ 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" @@ -1480,6 +1712,7 @@ dependencies = [ "tar", "tempfile", "thiserror 2.0.18", + "tokio", "toml", "url", "zeroize", @@ -1548,6 +1781,8 @@ name = "radroots_nostr" version = "0.1.0-alpha.2" dependencies = [ "nostr", + "nostr-sdk", + "radroots_identity", "serde", "serde_json", "thiserror 1.0.69", @@ -1726,8 +1961,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 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]] @@ -1737,7 +1982,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]] @@ -1750,6 +2005,15 @@ dependencies = [ ] [[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] name = "redox_syscall" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1776,6 +2040,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[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" @@ -1857,6 +2135,40 @@ dependencies = [ ] [[package]] +name = "rustls" +version = "0.23.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +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 = [ + "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" @@ -1889,7 +2201,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "rand", + "rand 0.8.5", "secp256k1-sys", "serde", ] @@ -1998,6 +2310,17 @@ dependencies = [ ] [[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" @@ -2040,12 +2363,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] name = "sqlite-wasm-rs" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2256,10 +2595,12 @@ version = "1.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -2276,6 +2617,44 @@ dependencies = [ ] [[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] name = "toml" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2400,6 +2779,25 @@ dependencies = [ ] [[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.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2471,6 +2869,12 @@ dependencies = [ ] [[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" @@ -2484,6 +2888,12 @@ 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" @@ -2572,6 +2982,16 @@ dependencies = [ ] [[package]] +name = "wasm-bindgen-futures" +version = "0.4.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "wasm-bindgen-macro" version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2648,6 +3068,24 @@ dependencies = [ ] [[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" @@ -2717,6 +3155,15 @@ dependencies = [ [[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.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" diff --git a/Cargo.toml b/Cargo.toml @@ -30,7 +30,7 @@ radroots_events_codec = { path = "../lib/crates/events_codec", features = ["nost radroots_identity = { path = "../lib/crates/identity" } radroots_log = { path = "../lib/crates/log" } radroots_nostr_accounts = { path = "../lib/crates/nostr_accounts", features = ["os-keyring"] } -radroots_nostr = { path = "../lib/crates/nostr" } +radroots_nostr = { path = "../lib/crates/nostr", features = ["client"] } radroots_nostr_signer = { path = "../lib/crates/nostr_signer" } radroots_protected_store = { path = "../lib/crates/protected_store", features = ["std"] } radroots_replica_db = { path = "../lib/crates/replica_db" } @@ -42,6 +42,7 @@ radroots_trade = { path = "../lib/crates/trade" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "2.0" +tokio = { version = "1", features = ["rt-multi-thread", "time"] } toml = "0.8" url = "2.5" zeroize = "1.8" diff --git a/src/runtime/direct_relay.rs b/src/runtime/direct_relay.rs @@ -0,0 +1,182 @@ +use std::time::Duration; + +use radroots_events_codec::wire::WireEventParts; +use radroots_identity::RadrootsIdentity; +use radroots_nostr::prelude::{ + RadrootsNostrClient, RadrootsNostrError, RadrootsNostrEvent, RadrootsNostrOutput, + radroots_nostr_build_event, +}; + +const RELAY_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DirectRelayFailure { + pub relay: String, + pub reason: String, +} + +#[derive(Debug, Clone)] +pub struct DirectRelayPublishReceipt { + pub event: RadrootsNostrEvent, + pub event_id: String, + pub target_relays: Vec<String>, + pub acknowledged_relays: Vec<String>, + pub failed_relays: Vec<DirectRelayFailure>, +} + +#[derive(Debug, thiserror::Error)] +pub enum DirectRelayPublishError { + #[error("direct relay publish requires at least one configured relay")] + MissingRelays, + #[error("failed to build async runtime for direct relay publish: {0}")] + Runtime(String), + #[error("failed to build Nostr event for direct relay publish: {0}")] + Build(#[source] RadrootsNostrError), + #[error("failed to sign Nostr event for direct relay publish: {0}")] + Sign(#[source] RadrootsNostrError), + #[error("failed to configure relay `{relay}` for direct relay publish: {source}")] + RelayConfig { + relay: String, + #[source] + source: RadrootsNostrError, + }, + #[error("direct relay connection failed: {0}")] + Connect(String), + #[error("direct relay publish failed for event `{event_id}`: {reason}")] + Publish { event_id: String, reason: String }, +} + +pub fn publish_parts_with_identity( + identity: &RadrootsIdentity, + relay_urls: &[String], + parts: WireEventParts, +) -> Result<DirectRelayPublishReceipt, DirectRelayPublishError> { + if relay_urls.is_empty() { + return Err(DirectRelayPublishError::MissingRelays); + } + + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .map_err(|error| DirectRelayPublishError::Runtime(error.to_string()))?; + + runtime.block_on(publish_parts_with_identity_async( + identity, relay_urls, parts, + )) +} + +async fn publish_parts_with_identity_async( + identity: &RadrootsIdentity, + relay_urls: &[String], + parts: WireEventParts, +) -> Result<DirectRelayPublishReceipt, DirectRelayPublishError> { + let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags) + .map_err(DirectRelayPublishError::Build)?; + let event = builder + .sign_with_keys(identity.keys()) + .map_err(|error| DirectRelayPublishError::Sign(error.into()))?; + let event_id = event.id.to_hex(); + let client = RadrootsNostrClient::from_identity(identity); + + for relay_url in relay_urls { + client.add_write_relay(relay_url).await.map_err(|source| { + DirectRelayPublishError::RelayConfig { + relay: relay_url.clone(), + source, + } + })?; + } + + let connection_output = client.try_connect(RELAY_CONNECT_TIMEOUT).await; + if connection_output.success.is_empty() { + return Err(DirectRelayPublishError::Connect(summarize_failures( + &relay_failures_from_output(&connection_output), + ))); + } + + let publish_output = + client + .send_event(&event) + .await + .map_err(|source| DirectRelayPublishError::Publish { + event_id: event_id.clone(), + reason: source.to_string(), + })?; + let failed_relays = relay_failures_from_output(&publish_output); + if publish_output.success.is_empty() { + return Err(DirectRelayPublishError::Publish { + event_id: event_id.clone(), + reason: summarize_failures(&failed_relays), + }); + } + + Ok(DirectRelayPublishReceipt { + event, + event_id, + target_relays: relay_urls.to_vec(), + acknowledged_relays: publish_output + .success + .iter() + .map(ToString::to_string) + .collect(), + failed_relays, + }) +} + +fn relay_failures_from_output<T: std::fmt::Debug>( + output: &RadrootsNostrOutput<T>, +) -> Vec<DirectRelayFailure> { + output + .failed + .iter() + .map(|(relay, reason)| DirectRelayFailure { + relay: relay.to_string(), + reason: reason.to_string(), + }) + .collect() +} + +fn summarize_failures(failed_relays: &[DirectRelayFailure]) -> String { + if failed_relays.is_empty() { + return "no relay acknowledged the operation".to_owned(); + } + + failed_relays + .iter() + .map(|failure| format!("{}: {}", failure.relay, failure.reason)) + .collect::<Vec<_>>() + .join("; ") +} + +pub fn event_created_at_u32(event: &RadrootsNostrEvent) -> u32 { + u32::try_from(event.created_at.as_secs()).unwrap_or(u32::MAX) +} + +pub fn event_signature(event: &RadrootsNostrEvent) -> String { + event.sig.to_string() +} + +#[cfg(test)] +mod tests { + use radroots_events_codec::wire::WireEventParts; + use radroots_identity::RadrootsIdentity; + + use super::{DirectRelayPublishError, publish_parts_with_identity}; + + #[test] + fn publish_parts_requires_relays_before_runtime_work() { + let identity = RadrootsIdentity::generate(); + let err = publish_parts_with_identity( + &identity, + &[], + WireEventParts { + kind: 30402, + content: "listing".to_owned(), + tags: Vec::new(), + }, + ) + .expect_err("missing relay error"); + + assert!(matches!(err, DirectRelayPublishError::MissingRelays)); + } +} diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs @@ -1,5 +1,6 @@ pub mod accounts; pub mod config; +pub mod direct_relay; pub mod farm; pub mod farm_config; pub mod find;