myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit f77c592056b6d85e403b8232fbc3d552bce20c05
parent 68e5e33368f4afd6aab1309dfb2b6446c0be92d9
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Mar 2026 22:18:26 +0000

app: add async transport bootstrap

- add repo-local transport config for enablement, relay lists, and connection timeout policy
- prepare an async runtime and Nostr client bootstrap path backed by local radroots-nostr client support
- capture transport readiness in startup snapshots so the relay-facing listener can land on stable runtime state
- validate with cargo fmt, cargo check, cargo test, cargo check --locked, cargo test --locked, and cargo fmt --check

Diffstat:
MCargo.lock | 562+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MCargo.toml | 2++
Msrc/app/mod.rs | 5+++--
Msrc/app/runtime.rs | 47++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/config.rs | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 7+++++--
Msrc/main.rs | 5+++--
Asrc/transport.rs | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 801 insertions(+), 20 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -13,6 +13,17 @@ 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" @@ -69,6 +80,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" @@ -160,6 +208,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[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" @@ -304,11 +358,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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -349,6 +409,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" @@ -370,7 +436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -401,6 +467,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" @@ -425,18 +555,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" @@ -501,6 +655,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 = "icu_collections" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -706,6 +876,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" + +[[package]] name = "matchers" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -734,7 +910,7 @@ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -742,16 +918,24 @@ name = "myc" version = "0.1.0" dependencies = [ "radroots-identity", + "radroots-nostr", "radroots-nostr-signer", "serde", "tempfile", "thiserror 2.0.18", + "tokio", "toml", "tracing", "tracing-subscriber", ] [[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" @@ -767,6 +951,7 @@ version = "0.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" dependencies = [ + "aes", "base64 0.22.1", "bech32", "bip39", @@ -786,12 +971,65 @@ 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" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -838,7 +1076,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -978,6 +1216,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" @@ -1023,6 +1267,18 @@ dependencies = [ ] [[package]] +name = "radroots-nostr" +version = "0.1.0-alpha.1" +dependencies = [ + "nostr", + "nostr-sdk", + "radroots-identity", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] name = "radroots-nostr-connect" version = "0.1.0-alpha.1" dependencies = [ @@ -1072,8 +1328,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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1083,7 +1349,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]] @@ -1096,6 +1372,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 = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1113,6 +1398,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" @@ -1165,7 +1464,41 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -1201,7 +1534,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", ] @@ -1274,6 +1607,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" @@ -1310,12 +1654,28 @@ dependencies = [ ] [[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.61.2", +] + +[[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1359,7 +1719,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1482,12 +1842,14 @@ version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1502,6 +1864,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" @@ -1616,6 +2016,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.2", + "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" @@ -1665,6 +2084,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" @@ -1678,6 +2103,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" @@ -1744,6 +2175,20 @@ dependencies = [ ] [[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] name = "wasm-bindgen-macro" version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1820,6 +2265,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.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1827,6 +2290,15 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" @@ -1835,6 +2307,70 @@ dependencies = [ ] [[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -15,9 +15,11 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] radroots-identity = { path = "../lib/crates/identity" } +radroots-nostr = { path = "../lib/crates/nostr", features = ["client"] } radroots-nostr-signer = { path = "../lib/crates/nostr-signer" } serde = { version = "1.0", features = ["derive"] } thiserror = "2.0" +tokio = { version = "1.48", features = ["macros", "rt-multi-thread"] } toml = "0.8" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } diff --git a/src/app/mod.rs b/src/app/mod.rs @@ -25,8 +25,8 @@ impl MycApp { self.runtime.snapshot() } - pub fn run(self) -> Result<(), MycError> { - self.runtime.run() + pub async fn run(self) -> Result<(), MycError> { + self.runtime.run().await } } @@ -75,5 +75,6 @@ mod tests { assert!(!snapshot.signer_public_key_hex.is_empty()); assert!(!snapshot.user_identity_id.is_empty()); assert!(!snapshot.user_public_key_hex.is_empty()); + assert!(!snapshot.transport.enabled); } } diff --git a/src/app/runtime.rs b/src/app/runtime.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use crate::config::MycConfig; use crate::error::MycError; +use crate::transport::{MycNostrTransport, MycTransportSnapshot}; use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; use radroots_nostr_signer::prelude::{RadrootsNostrFileSignerStore, RadrootsNostrSignerManager}; @@ -29,6 +30,7 @@ pub struct MycStartupSnapshot { pub signer_public_key_hex: String, pub user_identity_id: String, pub user_public_key_hex: String, + pub transport: MycTransportSnapshot, } #[derive(Clone)] @@ -43,6 +45,7 @@ pub struct MycRuntime { config: MycConfig, paths: MycRuntimePaths, signer: MycSignerContext, + transport: Option<MycNostrTransport>, } impl MycRuntime { @@ -52,10 +55,12 @@ impl MycRuntime { let paths = MycRuntimePaths::from_config(&config); Self::prepare_filesystem_for(&paths)?; let signer = MycSignerContext::bootstrap(&paths)?; + let transport = MycNostrTransport::bootstrap(&config.transport, &signer.signer_identity)?; let runtime = Self { paths, config, signer, + transport, }; Ok(runtime) } @@ -88,6 +93,10 @@ impl MycRuntime { &self.signer.manager } + pub fn transport(&self) -> Option<&MycNostrTransport> { + self.transport.as_ref() + } + pub fn snapshot(&self) -> MycStartupSnapshot { let signer_public = self.signer.signer_identity.to_public(); let user_public = self.signer.user_identity.to_public(); @@ -103,10 +112,15 @@ impl MycRuntime { signer_public_key_hex: signer_public.public_key_hex, user_identity_id: user_public.id.into_string(), user_public_key_hex: user_public.public_key_hex, + transport: self + .transport + .as_ref() + .map(MycNostrTransport::snapshot) + .unwrap_or_else(MycTransportSnapshot::disabled), } } - pub fn run(self) -> Result<(), MycError> { + pub async fn run(self) -> Result<(), MycError> { let snapshot = self.snapshot(); tracing::info!( instance_name = %snapshot.instance_name, @@ -119,6 +133,9 @@ impl MycRuntime { signer_public_key_hex = %snapshot.signer_public_key_hex, user_identity_id = %snapshot.user_identity_id, user_public_key_hex = %snapshot.user_public_key_hex, + transport_enabled = snapshot.transport.enabled, + transport_relay_count = snapshot.transport.relay_count, + transport_connect_timeout_secs = snapshot.transport.connect_timeout_secs, "myc runtime bootstrapped" ); Ok(()) @@ -250,6 +267,7 @@ mod tests { runtime.user_identity().public_key_hex(), runtime.snapshot().user_public_key_hex ); + assert!(!runtime.snapshot().transport.enabled); } #[test] @@ -329,4 +347,31 @@ mod tests { runtime.snapshot().user_identity_id ); } + + #[test] + fn bootstrap_prepares_transport_when_enabled() { + let temp = tempfile::tempdir().expect("tempdir"); + let mut config = MycConfig::default(); + config.paths.state_dir = temp.path().join("state"); + config.paths.signer_identity_path = temp.path().join("signer.json"); + config.paths.user_identity_path = temp.path().join("user.json"); + config.transport.enabled = true; + config.transport.connect_timeout_secs = 15; + config.transport.relays = vec!["wss://relay.example.com".to_owned()]; + write_test_identity( + &config.paths.signer_identity_path, + "1111111111111111111111111111111111111111111111111111111111111111", + ); + write_test_identity( + &config.paths.user_identity_path, + "2222222222222222222222222222222222222222222222222222222222222222", + ); + + let runtime = MycRuntime::bootstrap(config).expect("runtime"); + + assert!(runtime.transport().is_some()); + assert!(runtime.snapshot().transport.enabled); + assert_eq!(runtime.snapshot().transport.relay_count, 1); + assert_eq!(runtime.snapshot().transport.connect_timeout_secs, 15); + } } diff --git a/src/config.rs b/src/config.rs @@ -2,6 +2,7 @@ use std::fs; use std::path::{Path, PathBuf}; use radroots_identity::DEFAULT_IDENTITY_PATH; +use radroots_nostr::prelude::RadrootsNostrRelayUrl; use serde::{Deserialize, Serialize}; use tracing_subscriber::EnvFilter; @@ -15,6 +16,7 @@ pub struct MycConfig { pub service: MycServiceConfig, pub logging: MycLoggingConfig, pub paths: MycPathsConfig, + pub transport: MycTransportConfig, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -37,12 +39,21 @@ pub struct MycPathsConfig { pub user_identity_path: PathBuf, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct MycTransportConfig { + pub enabled: bool, + pub connect_timeout_secs: u64, + pub relays: Vec<String>, +} + impl Default for MycConfig { fn default() -> Self { Self { service: MycServiceConfig::default(), logging: MycLoggingConfig::default(), paths: MycPathsConfig::default(), + transport: MycTransportConfig::default(), } } } @@ -73,6 +84,16 @@ impl Default for MycPathsConfig { } } +impl Default for MycTransportConfig { + fn default() -> Self { + Self { + enabled: false, + connect_timeout_secs: 10, + relays: Vec::new(), + } + } +} + impl MycConfig { pub fn load_from_default_path_if_exists() -> Result<Self, MycError> { Self::load_from_path_if_exists(DEFAULT_CONFIG_PATH) @@ -140,6 +161,19 @@ impl MycConfig { )); } + if self.transport.connect_timeout_secs == 0 { + return Err(MycError::InvalidConfig( + "transport.connect_timeout_secs must be greater than zero".to_owned(), + )); + } + + let parsed_relays = self.transport.parse_relays()?; + if self.transport.enabled && parsed_relays.is_empty() { + return Err(MycError::InvalidConfig( + "transport.relays must not be empty when transport.enabled is true".to_owned(), + )); + } + Ok(()) } @@ -153,6 +187,21 @@ impl MycConfig { } } +impl MycTransportConfig { + pub fn parse_relays(&self) -> Result<Vec<RadrootsNostrRelayUrl>, MycError> { + self.relays + .iter() + .map(|value| { + RadrootsNostrRelayUrl::parse(value).map_err(|source| { + MycError::InvalidConfig(format!( + "transport.relays contains invalid relay url `{value}`: {source}" + )) + }) + }) + .collect() + } +} + #[cfg(test)] mod tests { use super::*; @@ -171,6 +220,9 @@ mod tests { config.paths.user_identity_path, PathBuf::from(DEFAULT_IDENTITY_PATH) ); + assert!(!config.transport.enabled); + assert_eq!(config.transport.connect_timeout_secs, 10); + assert!(config.transport.relays.is_empty()); } #[test] @@ -187,6 +239,11 @@ mod tests { state_dir = "/tmp/myc" signer_identity_path = "/tmp/myc-identity.json" user_identity_path = "/tmp/myc-user.json" + + [transport] + enabled = true + connect_timeout_secs = 15 + relays = ["wss://relay.example.com", "wss://relay2.example.com"] "#, ) .expect("config"); @@ -202,6 +259,15 @@ mod tests { config.paths.user_identity_path, PathBuf::from("/tmp/myc-user.json") ); + assert!(config.transport.enabled); + assert_eq!(config.transport.connect_timeout_secs, 15); + assert_eq!( + config.transport.relays, + vec![ + "wss://relay.example.com".to_owned(), + "wss://relay2.example.com".to_owned() + ] + ); } #[test] @@ -226,4 +292,13 @@ mod tests { assert!(err.to_string().contains("config parse error")); } + + #[test] + fn validate_rejects_enabled_transport_without_relays() { + let mut config = MycConfig::default(); + config.transport.enabled = true; + + let err = config.validate().expect_err("missing relays"); + assert!(err.to_string().contains("transport.relays")); + } } diff --git a/src/lib.rs b/src/lib.rs @@ -4,15 +4,18 @@ pub mod app; pub mod config; pub mod error; pub mod logging; +pub mod transport; pub use app::{MycApp, MycRuntime, MycRuntimePaths, MycSignerContext, MycStartupSnapshot}; pub use config::{ DEFAULT_CONFIG_PATH, MycConfig, MycLoggingConfig, MycPathsConfig, MycServiceConfig, + MycTransportConfig, }; pub use error::MycError; +pub use transport::{MycNostrTransport, MycTransportSnapshot}; -pub fn run() -> Result<(), MycError> { +pub async fn run() -> Result<(), MycError> { let config = MycConfig::load_from_default_path_if_exists()?; logging::init_logging(&config.logging)?; - MycApp::bootstrap(config)?.run() + MycApp::bootstrap(config)?.run().await } diff --git a/src/main.rs b/src/main.rs @@ -1,7 +1,8 @@ #![forbid(unsafe_code)] -fn main() { - if let Err(err) = myc::run() { +#[tokio::main] +async fn main() { + if let Err(err) = myc::run().await { eprintln!("myc: {err}"); std::process::exit(1); } diff --git a/src/transport.rs b/src/transport.rs @@ -0,0 +1,118 @@ +use radroots_identity::RadrootsIdentity; +use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrRelayUrl}; + +use crate::config::MycTransportConfig; +use crate::error::MycError; + +#[derive(Clone)] +pub struct MycNostrTransport { + client: RadrootsNostrClient, + relays: Vec<RadrootsNostrRelayUrl>, + connect_timeout_secs: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MycTransportSnapshot { + pub enabled: bool, + pub relay_count: usize, + pub connect_timeout_secs: u64, +} + +impl MycNostrTransport { + pub fn bootstrap( + config: &MycTransportConfig, + signer_identity: &RadrootsIdentity, + ) -> Result<Option<Self>, MycError> { + if !config.enabled { + return Ok(None); + } + + Ok(Some(Self { + client: RadrootsNostrClient::from_identity(signer_identity), + relays: config.parse_relays()?, + connect_timeout_secs: config.connect_timeout_secs, + })) + } + + pub fn client(&self) -> &RadrootsNostrClient { + &self.client + } + + pub fn relays(&self) -> &[RadrootsNostrRelayUrl] { + self.relays.as_slice() + } + + pub fn connect_timeout_secs(&self) -> u64 { + self.connect_timeout_secs + } + + pub fn snapshot(&self) -> MycTransportSnapshot { + MycTransportSnapshot { + enabled: true, + relay_count: self.relays.len(), + connect_timeout_secs: self.connect_timeout_secs, + } + } +} + +impl MycTransportSnapshot { + pub fn disabled() -> Self { + Self { + enabled: false, + relay_count: 0, + connect_timeout_secs: 0, + } + } +} + +#[cfg(test)] +mod tests { + use radroots_identity::RadrootsIdentity; + + use crate::config::MycTransportConfig; + + use super::{MycNostrTransport, MycTransportSnapshot}; + + fn signer_identity() -> RadrootsIdentity { + RadrootsIdentity::from_secret_key_str( + "1111111111111111111111111111111111111111111111111111111111111111", + ) + .expect("identity") + } + + #[test] + fn bootstrap_returns_none_when_transport_disabled() { + let config = MycTransportConfig::default(); + + let transport = + MycNostrTransport::bootstrap(&config, &signer_identity()).expect("disabled transport"); + + assert!(transport.is_none()); + } + + #[test] + fn bootstrap_builds_transport_snapshot_when_enabled() { + let mut config = MycTransportConfig::default(); + config.enabled = true; + config.connect_timeout_secs = 15; + config.relays = vec![ + "wss://relay.example.com".to_owned(), + "wss://relay2.example.com".to_owned(), + ]; + + let transport = MycNostrTransport::bootstrap(&config, &signer_identity()) + .expect("transport") + .expect("enabled transport"); + + assert_eq!(transport.relays().len(), 2); + assert_eq!(transport.connect_timeout_secs(), 15); + assert_eq!( + transport.snapshot(), + MycTransportSnapshot { + enabled: true, + relay_count: 2, + connect_timeout_secs: 15, + } + ); + } +}