lib

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

commit a79d958b1872a803418f80063a627e8080f8956b
parent ea3b2c1b1c93febf1cb333e22d5f43bd02804676
Author: triesap <tyson@radroots.org>
Date:   Fri, 20 Mar 2026 16:58:43 +0000

workspace: remove retired app crates

Diffstat:
MCargo.lock | 327+------------------------------------------------------------------------------
MCargo.toml | 4----
Mcontract/coverage/profiles.toml | 5-----
Mcontract/coverage/required-crates.toml | 2--
Mcontract/coverage/rollout.toml | 42++++++++++++++++--------------------------
Mcontract/release/publish-set.toml | 4----
Dcrates/app-core/Cargo.toml | 52----------------------------------------------------
Dcrates/app-core/README.md | 14--------------
Dcrates/app-core/build.rs | 47-----------------------------------------------
Dcrates/app-core/src/error.rs | 7-------
Dcrates/app-core/src/lib.rs | 10----------
Dcrates/app-core/src/logging.rs | 45---------------------------------------------
Dcrates/app-core/src/runtime/app_info.rs | 26--------------------------
Dcrates/app-core/src/runtime/builder.rs | 53-----------------------------------------------------
Dcrates/app-core/src/runtime/info.rs | 119-------------------------------------------------------------------------------
Dcrates/app-core/src/runtime/key_management.rs | 225-------------------------------------------------------------------------------
Dcrates/app-core/src/runtime/mod.rs | 211-------------------------------------------------------------------------------
Dcrates/app-core/src/runtime/nostr.rs | 370-------------------------------------------------------------------------------
Dcrates/app-core/src/runtime/trade_listing.rs | 865-------------------------------------------------------------------------------
Dcrates/app-core/tests/logging_error.rs | 9---------
Dcrates/app-core/tests/no_nostr_runtime.rs | 153-------------------------------------------------------------------------------
Dcrates/app-wasm/Cargo.toml | 19-------------------
Dcrates/app-wasm/README.md | 14--------------
Dcrates/app-wasm/src/lib.rs | 31-------------------------------
Mcrates/xtask/src/coverage.rs | 21++++++++++-----------
25 files changed, 29 insertions(+), 2646 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -140,48 +140,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "askama" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" -dependencies = [ - "askama_derive", - "itoa", - "percent-encoding", - "serde", - "serde_json", -] - -[[package]] -name = "askama_derive" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" -dependencies = [ - "askama_parser", - "basic-toml", - "memchr", - "proc-macro2", - "quote", - "rustc-hash 2.1.1", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "askama_parser" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" -dependencies = [ - "memchr", - "serde", - "serde_derive", - "winnow", -] - -[[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -260,15 +218,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] -name = "basic-toml" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" -dependencies = [ - "serde", -] - -[[package]] name = "bech32" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -377,38 +326,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -586,7 +503,7 @@ dependencies = [ "rust-ini", "serde", "serde_json", - "toml 0.8.23", + "toml", "yaml-rust2", ] @@ -884,15 +801,6 @@ dependencies = [ ] [[package]] -name = "fs-err" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" -dependencies = [ - "autocfg", -] - -[[package]] name = "futures" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1050,17 +958,6 @@ dependencies = [ ] [[package]] -name = "goblin" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" -dependencies = [ - "log", - "plain", - "scroll", -] - -[[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2090,36 +1987,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "radroots-app-core" -version = "0.1.0-alpha.1" -dependencies = [ - "chrono", - "radroots-core", - "radroots-events", - "radroots-events-codec", - "radroots-identity", - "radroots-log", - "radroots-net-core", - "radroots-nostr", - "radroots-trade", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tracing", - "tracing-subscriber", - "uniffi", -] - -[[package]] -name = "radroots-app-wasm" -version = "0.1.0-alpha.1" -dependencies = [ - "radroots-app-core", - "wasm-bindgen", -] - -[[package]] name = "radroots-core" version = "0.1.0-alpha.1" dependencies = [ @@ -2371,7 +2238,7 @@ dependencies = [ "tempfile", "thiserror 1.0.69", "tokio", - "toml 0.8.23", + "toml", "tracing", ] @@ -2800,26 +2667,6 @@ dependencies = [ ] [[package]] -name = "scroll" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" -dependencies = [ - "scroll_derive", -] - -[[package]] -name = "scroll_derive" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "scrypt" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2888,10 +2735,6 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] [[package]] name = "serde" @@ -3022,12 +2865,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3040,12 +2877,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - -[[package]] name = "socket2" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3074,12 +2905,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3156,15 +2981,6 @@ dependencies = [ ] [[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "smawk", -] - -[[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3345,15 +3161,6 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "toml" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" @@ -3626,125 +3433,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] -name = "uniffi" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6d968cb62160c11f2573e6be724ef8b1b18a277aededd17033f8a912d73e2b4" -dependencies = [ - "anyhow", - "cargo_metadata", - "uniffi_bindgen", - "uniffi_core", - "uniffi_macros", - "uniffi_pipeline", -] - -[[package]] -name = "uniffi_bindgen" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b39ef1acbe1467d5d210f274fae344cb6f8766339330cb4c9688752899bf6b" -dependencies = [ - "anyhow", - "askama", - "camino", - "cargo_metadata", - "fs-err", - "glob", - "goblin", - "heck", - "indexmap", - "once_cell", - "serde", - "tempfile", - "textwrap", - "toml 0.5.11", - "uniffi_internal_macros", - "uniffi_meta", - "uniffi_pipeline", - "uniffi_udl", -] - -[[package]] -name = "uniffi_core" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d990b553d6b9a7ee9c3ae71134674739913d52350b56152b0e613595bb5a6f" -dependencies = [ - "anyhow", - "bytes", - "once_cell", - "static_assertions", -] - -[[package]] -name = "uniffi_internal_macros" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f4f224becf14885c10e6e400b95cc4d1985738140cb194ccc2044563f8a56b" -dependencies = [ - "anyhow", - "indexmap", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "uniffi_macros" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b481d385af334871d70904e6a5f129be7cd38c18fcf8dd8fd1f646b426a56d58" -dependencies = [ - "camino", - "fs-err", - "once_cell", - "proc-macro2", - "quote", - "serde", - "syn", - "toml 0.5.11", - "uniffi_meta", -] - -[[package]] -name = "uniffi_meta" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f817868a3b171bb7bf259e882138d104deafde65684689b4694c846d322491" -dependencies = [ - "anyhow", - "siphasher", - "uniffi_internal_macros", - "uniffi_pipeline", -] - -[[package]] -name = "uniffi_pipeline" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b147e133ad7824e32426b90bc41fda584363563f2ba747f590eca1fd6fd14e6" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "tempfile", - "uniffi_internal_macros", -] - -[[package]] -name = "uniffi_udl" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caed654fb73da5abbc7a7e9c741532284532ba4762d6fe5071372df22a41730a" -dependencies = [ - "anyhow", - "textwrap", - "uniffi_meta", - "weedle2", -] - -[[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4060,15 +3748,6 @@ dependencies = [ ] [[package]] -name = "weedle2" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" -dependencies = [ - "nom", -] - -[[package]] name = "which" version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4434,7 +4113,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "toml 0.8.23", + "toml", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml @@ -1,7 +1,5 @@ [workspace] members = [ - "crates/app-core", - "crates/app-wasm", "crates/core", "crates/events", "crates/events-codec", @@ -40,8 +38,6 @@ homepage = "https://radroots.org" readme = "README" [workspace.dependencies] -radroots-app-core = { path = "crates/app-core", version = "0.1.0-alpha.1", default-features = false } -radroots-app-wasm = { path = "crates/app-wasm", version = "0.1.0-alpha.1", default-features = false } radroots-core = { path = "crates/core", version = "0.1.0-alpha.1", default-features = false } radroots-events = { path = "crates/events", version = "0.1.0-alpha.1", default-features = false } radroots-events-codec = { path = "crates/events-codec", version = "0.1.0-alpha.1", default-features = false } diff --git a/contract/coverage/profiles.toml b/contract/coverage/profiles.toml @@ -3,11 +3,6 @@ no_default_features = false features = [] test_threads = 1 -[profiles.crates."radroots-app-core"] -no_default_features = true -features = [] -test_threads = 1 - [profiles.crates."radroots-log"] no_default_features = true features = [] diff --git a/contract/coverage/required-crates.toml b/contract/coverage/required-crates.toml @@ -9,8 +9,6 @@ crates = [ "radroots-events-codec-wasm", "radroots-replica-db-schema", "xtask", - "radroots-app-core", - "radroots-app-wasm", "radroots-events-indexed", "radroots-log", "radroots-net-core", diff --git a/contract/coverage/rollout.toml b/contract/coverage/rollout.toml @@ -55,91 +55,81 @@ status = "required" order = 9 [[rollout.crates]] -name = "radroots-app-core" -status = "required" -order = 10 - -[[rollout.crates]] -name = "radroots-app-wasm" -status = "required" -order = 11 - -[[rollout.crates]] name = "radroots-events-indexed" status = "required" -order = 12 +order = 10 [[rollout.crates]] name = "radroots-log" status = "required" -order = 13 +order = 11 [[rollout.crates]] name = "radroots-net-core" status = "required" -order = 14 +order = 12 [[rollout.crates]] name = "radroots-net" status = "required" -order = 15 +order = 13 [[rollout.crates]] name = "radroots-nostr" status = "required" -order = 16 +order = 14 [[rollout.crates]] name = "radroots-nostr-accounts" status = "required" -order = 17 +order = 15 [[rollout.crates]] name = "radroots-nostr-ndb" status = "required" -order = 18 +order = 16 [[rollout.crates]] name = "radroots-nostr-runtime" status = "required" -order = 19 +order = 17 [[rollout.crates]] name = "radroots-runtime" status = "required" -order = 20 +order = 18 [[rollout.crates]] name = "radroots-sql-core" status = "required" -order = 21 +order = 19 [[rollout.crates]] name = "radroots-sql-wasm-core" status = "required" -order = 22 +order = 20 [[rollout.crates]] name = "radroots-sql-wasm-bridge" status = "required" -order = 23 +order = 21 [[rollout.crates]] name = "radroots-replica-sync" status = "required" -order = 24 +order = 22 [[rollout.crates]] name = "radroots-replica-db" status = "required" -order = 25 +order = 23 [[rollout.crates]] name = "radroots-replica-sync-wasm" status = "required" -order = 26 +order = 24 [[rollout.crates]] name = "radroots-replica-db-wasm" status = "required" -order = 27 +order = 25 diff --git a/contract/release/publish-set.toml b/contract/release/publish-set.toml @@ -26,9 +26,7 @@ crates = [ "radroots-net-core", "radroots-replica-db-wasm", "radroots-replica-sync-wasm", - "radroots-app-core", "radroots-net", - "radroots-app-wasm", ] [internal] @@ -61,7 +59,5 @@ crates = [ "radroots-net-core", "radroots-replica-db-wasm", "radroots-replica-sync-wasm", - "radroots-app-core", "radroots-net", - "radroots-app-wasm", ] diff --git a/crates/app-core/Cargo.toml b/crates/app-core/Cargo.toml @@ -1,52 +0,0 @@ -[package] -name = "radroots-app-core" -version = "0.1.0-alpha.1" -edition.workspace = true -authors = ["Radroots Authors"] -rust-version.workspace = true -license.workspace = true -description = "core application runtime primitives for radroots app surfaces" -repository.workspace = true -homepage.workspace = true -documentation = "https://docs.rs/radroots-app-core" -readme.workspace = true - -[lib] -crate-type = ["rlib"] - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } - -[features] -default = ["rt", "nostr-client"] -rt = ["radroots-net-core/rt"] -nostr-client = [ - "radroots-net-core/nostr-client", - "dep:radroots-core", - "dep:radroots-events-codec", - "dep:radroots-trade", - "dep:radroots-identity", - "dep:radroots-nostr", -] -directories = ["radroots-net-core/directories"] -fs-persistence = ["radroots-net-core/fs-persistence"] - -[dependencies] -radroots-log = { workspace = true } -radroots-events = { workspace = true } -radroots-net-core = { workspace = true, features = ["std"] } -radroots-core = { workspace = true, optional = true } -radroots-identity = { workspace = true, optional = true, default-features = false, features = ["std"] } -radroots-events-codec = { workspace = true, features = ["serde_json"], optional = true } -radroots-trade = { workspace = true, features = ["std", "serde", "serde_json"], optional = true } -radroots-nostr = { workspace = true, features = ["events"], optional = true } -chrono = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } -uniffi = { workspace = true } -tokio = { workspace = true } - -[dev-dependencies] -tracing-subscriber = { workspace = true } diff --git a/crates/app-core/README.md b/crates/app-core/README.md @@ -1,14 +0,0 @@ -# radroots-app-core - -Core application runtime primitives for Rad Roots app surfaces. - -## Goals - -- define stable application runtime, error, and lifecycle interfaces -- keep runtime and network wiring deterministic across supported targets -- support reusable integration points with `radroots-net-core` -- provide reusable application primitives for higher-level Rad Roots crates - -## License - -Licensed under AGPL-3.0. See LICENSE. diff --git a/crates/app-core/build.rs b/crates/app-core/build.rs @@ -1,47 +0,0 @@ -use std::{ - env, - process::Command, - time::{SystemTime, UNIX_EPOCH}, -}; - -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-env-changed=RUSTC"); - println!("cargo:rerun-if-env-changed=PROFILE"); - - let rustc = env::var("RUSTC").expect("missing required env var RUSTC"); - if let Ok(out) = Command::new(rustc).arg("--version").output() { - if out.status.success() { - if let Ok(ver) = String::from_utf8(out.stdout) { - println!("cargo:rustc-env=RUSTC_VERSION={}", ver.trim()); - } - } - } - - if let Ok(out) = Command::new("git") - .args(["rev-parse", "--short=12", "HEAD"]) - .output() - { - if out.status.success() { - let mut sha = String::from_utf8_lossy(&out.stdout).trim().to_string(); - let dirty = Command::new("git") - .args(["status", "--porcelain"]) - .output() - .ok() - .map_or(false, |o| o.status.success() && !o.stdout.is_empty()); - if dirty { - sha.push_str("-dirty"); - } - println!("cargo:rustc-env=GIT_HASH={sha}"); - } - } - - let profile = env::var("PROFILE").expect("missing required env var PROFILE"); - println!("cargo:rustc-env=PROFILE={profile}"); - - let epoch = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs()) - .expect("system time before unix epoch"); - println!("cargo:rustc-env=BUILD_TIME_UNIX={epoch}"); -} diff --git a/crates/app-core/src/error.rs b/crates/app-core/src/error.rs @@ -1,7 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Error, uniffi::Error)] -pub enum RadrootsAppError { - #[error("{0}")] - Msg(String), -} diff --git a/crates/app-core/src/lib.rs b/crates/app-core/src/lib.rs @@ -1,10 +0,0 @@ -uniffi::setup_scaffolding!("radroots"); - -pub mod error; -pub mod logging; -pub mod runtime; - -pub use error::RadrootsAppError; -pub use radroots_net_core::net::{BuildInfo, NetInfo}; -pub use radroots_net_core::{Net, NetHandle}; -pub use runtime::RadrootsRuntime; diff --git a/crates/app-core/src/logging.rs b/crates/app-core/src/logging.rs @@ -1,45 +0,0 @@ -use std::path::PathBuf; - -#[cfg_attr(not(coverage_nightly), uniffi::export)] -pub fn init_logging( - dir: Option<String>, - file_name: Option<String>, - is_stdout: Option<bool>, -) -> Result<(), crate::RadrootsAppError> { - let opts = radroots_log::LoggingOptions { - dir: dir.map(PathBuf::from), - file_name: file_name.unwrap_or_else(|| "radroots.log".to_string()), - stdout: is_stdout.unwrap_or(true), - default_level: None, - }; - match radroots_log::init_logging(opts) { - Ok(()) => Ok(()), - Err(err) => Err(crate::RadrootsAppError::Msg(format!("{err}"))), - } -} - -#[cfg_attr(not(coverage_nightly), uniffi::export)] -pub fn init_logging_stdout() -> Result<(), crate::RadrootsAppError> { - match radroots_log::init_stdout() { - Ok(()) => Ok(()), - Err(err) => Err(crate::RadrootsAppError::Msg(format!("{err}"))), - } -} - -#[cfg_attr(not(coverage_nightly), uniffi::export)] -pub fn log_info(msg: String) -> Result<(), crate::RadrootsAppError> { - radroots_log::log_info(msg); - Ok(()) -} - -#[cfg_attr(not(coverage_nightly), uniffi::export)] -pub fn log_error(msg: String) -> Result<(), crate::RadrootsAppError> { - radroots_log::log_error(msg); - Ok(()) -} - -#[cfg_attr(not(coverage_nightly), uniffi::export)] -pub fn log_debug(msg: String) -> Result<(), crate::RadrootsAppError> { - radroots_log::log_debug(msg); - Ok(()) -} diff --git a/crates/app-core/src/runtime/app_info.rs b/crates/app-core/src/runtime/app_info.rs @@ -1,26 +0,0 @@ -#[derive(Debug, Clone, Default, serde::Serialize, uniffi::Record)] -pub struct AppInfoPlatform { - pub platform: Option<String>, - pub bundle_id: Option<String>, - pub version: Option<String>, - pub build_number: Option<String>, - pub build_sha: Option<String>, -} - -impl AppInfoPlatform { - pub fn new( - platform: Option<String>, - bundle_id: Option<String>, - version: Option<String>, - build_number: Option<String>, - build_sha: Option<String>, - ) -> Self { - Self { - platform, - bundle_id, - version, - build_number, - build_sha, - } - } -} diff --git a/crates/app-core/src/runtime/builder.rs b/crates/app-core/src/runtime/builder.rs @@ -1,53 +0,0 @@ -use radroots_net_core::NetHandle; -use radroots_net_core::builder::NetBuilder; -use radroots_net_core::config::NetConfig; - -use crate::RadrootsAppError; - -pub struct RuntimeBuilder { - config: NetConfig, - manage_runtime: bool, -} - -impl RuntimeBuilder { - pub fn new() -> Self { - Self { - config: NetConfig::default(), - manage_runtime: true, - } - } - - pub fn with_config(mut self, config: NetConfig) -> Self { - self.config = config; - self - } - - pub fn manage_runtime(mut self, manage: bool) -> Self { - self.manage_runtime = manage; - self - } - - pub fn build(self) -> Result<NetHandle, RadrootsAppError> { - #[cfg(feature = "rt")] - { - match NetBuilder::new() - .config(self.config) - .manage_runtime(self.manage_runtime) - .build() - { - Ok(handle) => Ok(handle), - Err(err) => Err(RadrootsAppError::Msg(format!("net build failed: {err}"))), - } - } - - #[cfg(not(feature = "rt"))] - { - let handle = NetBuilder::new() - .config(self.config) - .manage_runtime(self.manage_runtime) - .build() - .expect("net build must succeed when rt feature is disabled"); - Ok(handle) - } - } -} diff --git a/crates/app-core/src/runtime/info.rs b/crates/app-core/src/runtime/info.rs @@ -1,119 +0,0 @@ -use super::RadrootsRuntime; -use chrono::Utc; -use radroots_net_core::net; -use serde::Serialize; - -#[derive(Debug, Clone, Serialize, Default, uniffi::Record)] -pub struct NetBuildInfo { - pub crate_name: String, - pub crate_version: String, - pub rustc: Option<String>, - pub profile: Option<String>, - pub git_sha: Option<String>, - pub build_time_unix: Option<u64>, -} - -impl From<&net::BuildInfo> for NetBuildInfo { - fn from(b: &net::BuildInfo) -> Self { - Self { - crate_name: b.crate_name.to_string(), - crate_version: b.crate_version.to_string(), - rustc: b.rustc.map(|s| s.to_string()), - profile: b.profile.map(|s| s.to_string()), - git_sha: b.git_sha.map(|s| s.to_string()), - build_time_unix: b.build_time_unix, - } - } -} - -#[derive(Debug, Clone, Serialize, uniffi::Record)] -pub struct AppInfo { - pub build: NetBuildInfo, - pub started_unix_ms: i64, - pub uptime_millis: i64, - pub shutting_down: bool, - pub platform: Option<super::app_info::AppInfoPlatform>, -} - -#[derive(Debug, Clone, Serialize, uniffi::Record)] -pub struct RuntimeInfo { - pub app: AppInfo, - pub net: NetBuildInfo, -} - -pub fn gather_runtime_info(runtime: &RadrootsRuntime) -> RuntimeInfo { - let now_ms = Utc::now().timestamp_millis(); - let app_info = AppInfo { - build: app_build_info(), - started_unix_ms: runtime.started_unix_ms, - uptime_millis: now_ms - runtime.started_unix_ms, - shutting_down: runtime - .shutting_down - .load(std::sync::atomic::Ordering::SeqCst), - platform: runtime.platform_app.read().ok().and_then(|g| (*g).clone()), - }; - - let net_info = match runtime.net.lock() { - Ok(guard) => NetBuildInfo::from(&guard.info.build), - Err(_) => NetBuildInfo::default(), - }; - - RuntimeInfo { - app: app_info, - net: net_info, - } -} - -pub fn app_build_info() -> NetBuildInfo { - NetBuildInfo { - crate_name: env!("CARGO_PKG_NAME").to_string(), - crate_version: env!("CARGO_PKG_VERSION").to_string(), - rustc: env_opt_to_owned(option_env!("RUSTC_VERSION")), - profile: env_opt_to_owned(option_env!("PROFILE")), - git_sha: env_opt_to_owned(option_env!("GIT_HASH")), - build_time_unix: env_opt_to_u64(option_env!("BUILD_TIME_UNIX")), - } -} - -fn env_opt_to_owned(value: Option<&str>) -> Option<String> { - value.map(str::to_owned) -} - -fn env_opt_to_u64(value: Option<&str>) -> Option<u64> { - value.map(str::parse::<u64>).and_then(Result::ok) -} - -#[cfg(test)] -mod tests { - use super::NetBuildInfo; - use radroots_net_core::net; - - #[test] - fn net_build_info_from_copies_optional_fields() { - let source = net::BuildInfo { - crate_name: "radroots-net-core", - crate_version: "1.2.3", - rustc: Some("rustc 1.92.0"), - profile: Some("debug"), - git_sha: Some("abc123"), - build_time_unix: Some(1_700_000_000), - }; - - let out = NetBuildInfo::from(&source); - assert_eq!(out.crate_name, "radroots-net-core"); - assert_eq!(out.crate_version, "1.2.3"); - assert_eq!(out.rustc.as_deref(), Some("rustc 1.92.0")); - assert_eq!(out.profile.as_deref(), Some("debug")); - assert_eq!(out.git_sha.as_deref(), Some("abc123")); - assert_eq!(out.build_time_unix, Some(1_700_000_000)); - } - - #[test] - fn env_opt_helpers_cover_some_none_and_parse_failure() { - assert_eq!(super::env_opt_to_owned(Some("abc")).as_deref(), Some("abc")); - assert_eq!(super::env_opt_to_owned(None), None); - assert_eq!(super::env_opt_to_u64(Some("123")), Some(123)); - assert_eq!(super::env_opt_to_u64(Some("abc")), None); - assert_eq!(super::env_opt_to_u64(None), None); - } -} diff --git a/crates/app-core/src/runtime/key_management.rs b/crates/app-core/src/runtime/key_management.rs @@ -1,225 +0,0 @@ -use super::RadrootsRuntime; -use crate::RadrootsAppError; -#[cfg(feature = "nostr-client")] -use radroots_identity::{RadrootsIdentity, RadrootsIdentityId}; -#[cfg(feature = "nostr-client")] -use std::path::PathBuf; - -#[cfg_attr(not(coverage_nightly), uniffi::export)] -impl RadrootsRuntime { - pub fn accounts_has_selected_signing_identity(&self) -> bool { - #[cfg(feature = "nostr-client")] - { - if let Ok(guard) = self.net.lock() { - return guard - .accounts - .selected_signing_identity() - .ok() - .flatten() - .is_some(); - } - } - - #[cfg(not(feature = "nostr-client"))] - { - false - } - - #[cfg(feature = "nostr-client")] - false - } - - pub fn accounts_selected_npub(&self) -> Option<String> { - #[cfg(feature = "nostr-client")] - { - if let Ok(guard) = self.net.lock() { - return guard - .accounts - .selected_public_identity() - .ok() - .flatten() - .map(|identity| identity.public_key_npub); - } - } - - #[cfg(not(feature = "nostr-client"))] - { - None - } - - #[cfg(feature = "nostr-client")] - None - } - - pub fn accounts_list_ids(&self) -> Result<Vec<String>, RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let accounts = guard - .accounts - .list_accounts() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - return Ok(accounts - .into_iter() - .map(|account| account.account_id.to_string()) - .collect()); - } - #[cfg(not(feature = "nostr-client"))] - { - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn accounts_generate( - &self, - label: Option<String>, - make_selected: bool, - ) -> Result<String, RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let mut guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let account_id = guard - .accounts - .generate_identity(label, make_selected) - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - guard.nostr = None; - return Ok(account_id.to_string()); - } - #[cfg(not(feature = "nostr-client"))] - { - let _ = (label, make_selected); - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn accounts_import_secret( - &self, - secret_key: String, - label: Option<String>, - make_selected: bool, - ) -> Result<String, RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let mut guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let identity = RadrootsIdentity::from_secret_key_str(secret_key.as_str()) - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - let account_id = guard - .accounts - .upsert_identity(&identity, label, make_selected) - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - guard.nostr = None; - return Ok(account_id.to_string()); - } - #[cfg(not(feature = "nostr-client"))] - { - let _ = (secret_key, label, make_selected); - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn accounts_import_from_path( - &self, - path: String, - label: Option<String>, - make_selected: bool, - ) -> Result<String, RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let mut guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let account_id = guard - .accounts - .migrate_legacy_identity_file(PathBuf::from(path), label, make_selected) - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - guard.nostr = None; - return Ok(account_id.to_string()); - } - #[cfg(not(feature = "nostr-client"))] - { - let _ = (path, label, make_selected); - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn accounts_export_selected_secret_hex(&self) -> Result<Option<String>, RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let Some(selected_id) = guard - .accounts - .selected_account_id() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))? - else { - return Ok(None); - }; - return guard - .accounts - .export_secret_hex(&selected_id) - .map_err(|e| RadrootsAppError::Msg(format!("{e}"))); - } - #[cfg(not(feature = "nostr-client"))] - { - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn accounts_select(&self, account_id: String) -> Result<(), RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let mut guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let account_id = RadrootsIdentityId::parse(account_id.as_str()) - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - guard - .accounts - .select_account(&account_id) - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - guard.nostr = None; - Ok(()) - } - #[cfg(not(feature = "nostr-client"))] - { - let _ = account_id; - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn accounts_remove(&self, account_id: String) -> Result<(), RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let mut guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let account_id = RadrootsIdentityId::parse(account_id.as_str()) - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - guard - .accounts - .remove_account(&account_id) - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - guard.nostr = None; - Ok(()) - } - #[cfg(not(feature = "nostr-client"))] - { - let _ = account_id; - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } -} diff --git a/crates/app-core/src/runtime/mod.rs b/crates/app-core/src/runtime/mod.rs @@ -1,211 +0,0 @@ -pub mod app_info; -pub mod builder; -pub mod info; -pub mod key_management; -pub mod nostr; -#[cfg(feature = "nostr-client")] -pub mod trade_listing; - -use chrono::Utc; -use radroots_net_core::{NetHandle, builder::NetBuilder}; -#[cfg(feature = "nostr-client")] -use std::sync::Mutex; -use std::sync::{ - RwLock, - atomic::{AtomicBool, Ordering}, -}; -#[cfg(feature = "nostr-client")] -use tokio::sync::broadcast::Receiver; - -use self::{ - app_info::AppInfoPlatform, - info::{RuntimeInfo, gather_runtime_info}, -}; -use crate::RadrootsAppError; - -#[derive(uniffi::Object)] -pub struct RadrootsRuntime { - pub(crate) net: NetHandle, - pub(crate) started_unix_ms: i64, - pub(crate) shutting_down: AtomicBool, - pub(crate) platform_app: RwLock<Option<AppInfoPlatform>>, - #[cfg(feature = "nostr-client")] - pub(crate) post_events_rx: Mutex< - Option< - Receiver< - radroots_events_codec::parsed::RadrootsParsedData< - radroots_events::post::RadrootsPost, - >, - >, - >, - >, -} - -#[cfg_attr(not(coverage_nightly), uniffi::export)] -impl RadrootsRuntime { - #[cfg_attr(not(coverage_nightly), uniffi::constructor)] - pub fn new() -> Result<Self, RadrootsAppError> { - let cfg = radroots_net_core::config::NetConfig::default(); - #[cfg(feature = "rt")] - let handle = match NetBuilder::new().config(cfg).manage_runtime(true).build() { - Ok(handle) => handle, - Err(err) => return Err(RadrootsAppError::Msg(format!("net build failed: {err}"))), - }; - #[cfg(not(feature = "rt"))] - let handle = NetBuilder::new() - .config(cfg) - .manage_runtime(true) - .build() - .expect("net build must succeed when rt feature is disabled"); - - Ok(Self { - net: handle, - started_unix_ms: Utc::now().timestamp_millis(), - shutting_down: AtomicBool::new(false), - platform_app: RwLock::new(None), - #[cfg(feature = "nostr-client")] - post_events_rx: Mutex::new(None), - }) - } - - pub fn stop(&self) { - if self.shutting_down.swap(true, Ordering::SeqCst) { - let _ = crate::logging::log_info( - "Runtime stop already in progress or completed.".to_string(), - ); - return; - } - - #[cfg(feature = "rt")] - { - if let Ok(mut net) = self.net.lock() { - if let Some(_rt) = net.rt.take() { - let _ = crate::logging::log_info("Runtime stopped gracefully.".to_string()); - } else { - let _ = crate::logging::log_info("No runtime was active at stop.".to_string()); - } - } else { - let _ = crate::logging::log_info( - "Failed to acquire runtime lock during stop.".to_string(), - ); - } - } - - #[cfg(not(feature = "rt"))] - { - let _ = crate::logging::log_info( - "No managed runtime is available for this build.".to_string(), - ); - } - } - - pub fn uptime_millis(&self) -> i64 { - Utc::now().timestamp_millis() - self.started_unix_ms - } - - pub fn info(&self) -> RuntimeInfo { - gather_runtime_info(self) - } - - pub fn info_json(&self) -> String { - #[cfg(feature = "rt")] - { - return match serde_json::to_string_pretty(&self.info()) { - Ok(json) => json, - Err(err) => format!(r#"{{"error":"serialize RuntimeInfo: {err}"}}"#), - }; - } - #[cfg(not(feature = "rt"))] - { - serde_json::to_string_pretty(&self.info()).unwrap_or_default() - } - } - - pub fn set_app_info_platform( - &self, - platform: Option<String>, - bundle_id: Option<String>, - version: Option<String>, - build_number: Option<String>, - build_sha: Option<String>, - ) { - let platform_info = - AppInfoPlatform::new(platform, bundle_id, version, build_number, build_sha); - if let Ok(mut guard) = self.platform_app.write() { - *guard = Some(platform_info); - } - } -} - -#[cfg(test)] -mod tests { - use super::RadrootsRuntime; - use std::panic::{AssertUnwindSafe, catch_unwind}; - - fn init_info_logging() { - let _ = tracing_subscriber::fmt() - .with_test_writer() - .with_max_level(tracing::Level::INFO) - .try_init(); - } - - fn poison_net_lock(runtime: &RadrootsRuntime) { - let handle = runtime.net.clone(); - let _ = catch_unwind(AssertUnwindSafe(|| { - let _guard = handle.lock().expect("lock net"); - panic!("poison net lock"); - })); - } - - fn poison_platform_lock(runtime: &RadrootsRuntime) { - let _ = catch_unwind(AssertUnwindSafe(|| { - let _guard = runtime.platform_app.write().expect("lock platform"); - panic!("poison platform lock"); - })); - } - - #[test] - fn runtime_info_uses_default_net_info_when_lock_is_poisoned() { - init_info_logging(); - let runtime = RadrootsRuntime::new().expect("runtime"); - - let healthy = runtime.info(); - assert!(!healthy.net.crate_name.is_empty()); - poison_net_lock(&runtime); - - let _ = runtime.uptime_millis(); - let info = runtime.info(); - assert_eq!(info.net.crate_name, String::new()); - assert_eq!(info.net.crate_version, String::new()); - let json = runtime.info_json(); - assert!(json.contains("\"net\"")); - runtime.stop(); - runtime.stop(); - } - - #[test] - fn set_platform_info_handles_poisoned_lock() { - init_info_logging(); - let runtime = RadrootsRuntime::new().expect("runtime"); - runtime.set_app_info_platform( - Some("ios".to_string()), - Some("org.radroots.app".to_string()), - Some("1.0.0".to_string()), - Some("100".to_string()), - Some("abc123".to_string()), - ); - let info = runtime.info(); - assert_eq!( - info.app.platform.as_ref().and_then(|v| v.platform.clone()), - Some("ios".to_string()) - ); - poison_platform_lock(&runtime); - runtime.set_app_info_platform( - Some("ios".to_string()), - Some("org.radroots.app".to_string()), - Some("1.0.0".to_string()), - Some("100".to_string()), - Some("abc123".to_string()), - ); - } -} diff --git a/crates/app-core/src/runtime/nostr.rs b/crates/app-core/src/runtime/nostr.rs @@ -1,370 +0,0 @@ -use super::RadrootsRuntime; -use crate::RadrootsAppError; -#[cfg(feature = "nostr-client")] -use tokio::sync::broadcast::error::TryRecvError; - -#[derive(uniffi::Enum, Debug, Clone, Copy)] -pub enum NostrLight { - Red, - Yellow, - Green, -} - -#[derive(uniffi::Record, Debug, Clone)] -pub struct NostrConnectionStatus { - pub light: NostrLight, - pub connected: u32, - pub connecting: u32, - pub last_error: Option<String>, -} - -#[derive(uniffi::Record, Debug, Clone, Default)] -pub struct NostrProfile { - pub name: Option<String>, - pub display_name: Option<String>, - pub nip05: Option<String>, - pub about: Option<String>, - pub website: Option<String>, - pub picture: Option<String>, - pub banner: Option<String>, - pub lud06: Option<String>, - pub lud16: Option<String>, - pub bot: Option<String>, -} - -#[derive(uniffi::Record, Debug, Clone)] -pub struct NostrProfileEventMetadata { - pub id: String, - pub author: String, - pub published_at: u64, - pub profile: NostrProfile, -} - -#[derive(uniffi::Record, Debug, Clone)] -pub struct NostrEvent { - pub id: String, - pub author: String, - pub created_at: u64, - pub kind: u32, - pub content: String, -} - -#[derive(uniffi::Record, Debug, Clone)] -pub struct NostrPost { - pub content: String, -} - -#[derive(uniffi::Record, Debug, Clone)] -pub struct NostrPostEventMetadata { - pub id: String, - pub author: String, - pub published_at: u64, - pub post: NostrPost, -} - -#[cfg(feature = "nostr-client")] -fn map_post_event_metadata( - event: radroots_events_codec::parsed::RadrootsParsedData<radroots_events::post::RadrootsPost>, -) -> NostrPostEventMetadata { - NostrPostEventMetadata { - id: event.id, - author: event.author, - published_at: event.published_at as u64, - post: NostrPost { - content: event.data.content, - }, - } -} - -#[cfg_attr(not(coverage_nightly), uniffi::export)] -impl RadrootsRuntime { - pub fn nostr_set_default_relays(&self, relays: Vec<String>) -> Result<(), RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let mut guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - guard - .nostr_set_default_relays(&relays) - .map_err(|e| RadrootsAppError::Msg(format!("{e}"))) - } - #[cfg(not(feature = "nostr-client"))] - { - let _ = relays; - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn nostr_connect_if_key_present(&self) -> Result<(), RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let mut guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - guard - .nostr_connect_if_key_present() - .map_err(|e| RadrootsAppError::Msg(format!("{e}"))) - } - #[cfg(not(feature = "nostr-client"))] - { - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn nostr_connection_status(&self) -> NostrConnectionStatus { - #[cfg(feature = "nostr-client")] - { - let guard = self.net.lock(); - if let Ok(g) = guard { - if let Some(s) = g.nostr_connection_snapshot() { - let light = match s.light { - radroots_net_core::nostr_client::Light::Green => NostrLight::Green, - radroots_net_core::nostr_client::Light::Yellow => NostrLight::Yellow, - radroots_net_core::nostr_client::Light::Red => NostrLight::Red, - }; - return NostrConnectionStatus { - light, - connected: s.connected as u32, - connecting: s.connecting as u32, - last_error: s.last_error, - }; - } - } - NostrConnectionStatus { - light: NostrLight::Red, - connected: 0, - connecting: 0, - last_error: None, - } - } - - #[cfg(not(feature = "nostr-client"))] - { - NostrConnectionStatus { - light: NostrLight::Red, - connected: 0, - connecting: 0, - last_error: None, - } - } - } - - pub fn nostr_profile_for_self(&self) -> Option<NostrProfileEventMetadata> { - #[cfg(feature = "nostr-client")] - { - let guard = self.net.lock().ok()?; - let keys = guard.selected_nostr_keys()?; - let pk = keys.public_key(); - let mgr = guard.nostr.as_ref()?; - let out = mgr.fetch_profile_event_blocking(pk).ok()?; - return out.map(|m| NostrProfileEventMetadata { - id: m.id, - author: m.author, - published_at: m.published_at as u64, - profile: NostrProfile { - name: m.data.profile.name.into(), - display_name: m.data.profile.display_name.into(), - nip05: m.data.profile.nip05.into(), - about: m.data.profile.about.into(), - website: m.data.profile.website, - picture: m.data.profile.picture, - banner: m.data.profile.banner, - lud06: m.data.profile.lud06, - lud16: m.data.profile.lud16, - bot: m.data.profile.bot, - }, - }); - } - #[cfg(not(feature = "nostr-client"))] - { - None - } - } - - pub fn nostr_post_profile( - &self, - name: Option<String>, - display_name: Option<String>, - nip05: Option<String>, - about: Option<String>, - ) -> Result<String, RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let mgr = guard - .nostr - .as_ref() - .ok_or_else(|| RadrootsAppError::Msg("nostr not initialized".into()))?; - mgr.publish_profile_event_blocking(name, display_name, nip05, about) - .map_err(|e| RadrootsAppError::Msg(e.to_string())) - } - #[cfg(not(feature = "nostr-client"))] - { - let _ = (name, display_name, nip05, about); - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn nostr_post_text_note(&self, content: String) -> Result<String, RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let mgr = guard - .nostr - .as_ref() - .ok_or_else(|| RadrootsAppError::Msg("nostr not initialized".into()))?; - mgr.publish_post_event_blocking(content) - .map_err(|e| RadrootsAppError::Msg(e.to_string())) - } - #[cfg(not(feature = "nostr-client"))] - { - let _ = content; - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn nostr_fetch_text_notes( - &self, - limit: u16, - since_unix: Option<u64>, - ) -> Result<Vec<NostrPostEventMetadata>, RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let mgr = guard - .nostr - .as_ref() - .ok_or_else(|| RadrootsAppError::Msg("nostr not initialized".into()))?; - let items = mgr - .fetch_post_events_blocking(limit, since_unix) - .map_err(|e| RadrootsAppError::Msg(e.to_string()))?; - Ok(items.into_iter().map(map_post_event_metadata).collect()) - } - #[cfg(not(feature = "nostr-client"))] - { - let _ = (limit, since_unix); - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn nostr_post_reply( - &self, - parent_event_id_hex: String, - parent_author_hex: String, - content: String, - root_event_id_hex: Option<String>, - ) -> Result<String, RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let mgr = guard - .nostr - .as_ref() - .ok_or_else(|| RadrootsAppError::Msg("nostr not initialized".into()))?; - mgr.publish_post_reply_event_blocking( - parent_event_id_hex, - parent_author_hex, - content, - root_event_id_hex, - ) - .map_err(|e| RadrootsAppError::Msg(e.to_string())) - } - #[cfg(not(feature = "nostr-client"))] - { - let _ = ( - parent_event_id_hex, - parent_author_hex, - content, - root_event_id_hex, - ); - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn nostr_start_post_event_stream( - &self, - since_unix: Option<u64>, - ) -> Result<(), RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let mgr = guard - .nostr - .as_ref() - .ok_or_else(|| RadrootsAppError::Msg("nostr not initialized".into()))?; - mgr.start_post_event_stream(since_unix); - if let Ok(mut rx_guard) = self.post_events_rx.lock() { - if rx_guard.is_none() { - *rx_guard = Some(mgr.subscribe_post_events()); - } - } - Ok(()) - } - #[cfg(not(feature = "nostr-client"))] - { - let _ = since_unix; - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn nostr_next_post_event(&self) -> Option<NostrPostEventMetadata> { - #[cfg(feature = "nostr-client")] - { - let mut rx_guard = self.post_events_rx.lock().ok()?; - let rx = rx_guard.as_mut()?; - match rx.try_recv() { - Ok(event) => Some(map_post_event_metadata(event)), - Err(TryRecvError::Empty) => None, - Err(TryRecvError::Lagged(_)) => None, - Err(TryRecvError::Closed) => { - *rx_guard = None; - None - } - } - } - #[cfg(not(feature = "nostr-client"))] - { - None - } - } - - pub fn nostr_stop_post_event_stream(&self) -> Result<(), RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let guard = match self.net.lock() { - Ok(guard) => guard, - Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), - }; - let mgr = guard - .nostr - .as_ref() - .ok_or_else(|| RadrootsAppError::Msg("nostr not initialized".into()))?; - mgr.stop_post_event_stream(); - if let Ok(mut rx_guard) = self.post_events_rx.lock() { - *rx_guard = None; - } - Ok(()) - } - #[cfg(not(feature = "nostr-client"))] - { - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } -} diff --git a/crates/app-core/src/runtime/trade_listing.rs b/crates/app-core/src/runtime/trade_listing.rs @@ -1,865 +0,0 @@ -#![forbid(unsafe_code)] - -use core::str::FromStr; - -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, RadrootsCoreUnit, -}; -use radroots_events::{ - RadrootsNostrEventPtr, - listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, - RadrootsListingProduct, RadrootsListingStatus, - }, -}; -use radroots_events_codec::listing::encode::to_wire_parts as listing_to_wire_parts; -use radroots_nostr::prelude::{ - RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrTimestamp, radroots_event_from_nostr, - radroots_nostr_parse_pubkey, -}; -use radroots_trade::listing::{ - dvm::TradeListingAddress, - dvm::{ - TradeListingEnvelope, TradeListingMessagePayload, TradeListingMessageType, - TradeListingValidateRequest, - }, - kinds::TRADE_LISTING_KINDS, - order::{TradeOrder, TradeOrderItem, TradeOrderStatus}, - tags::trade_listing_dvm_tags, - validation::{RadrootsTradeListing, validate_listing_event}, -}; - -use super::RadrootsRuntime; -use crate::RadrootsAppError; - -const LISTING_KIND: u32 = 30402; - -#[derive(uniffi::Record, Debug, Clone)] -pub struct TradeListingDraft { - pub listing_id: Option<String>, - pub farm_pubkey: String, - pub farm_d_tag: String, - pub title: String, - pub description: String, - pub category: String, - pub bin_display_amount: String, - pub bin_display_unit: String, - pub unit_price: String, - pub currency: String, - pub bin_label: Option<String>, - pub bin_id: Option<String>, - pub inventory: String, - pub delivery_method: String, - pub location_primary: String, - pub location_city: Option<String>, - pub location_region: Option<String>, - pub location_country: Option<String>, -} - -#[derive(uniffi::Record, Debug, Clone)] -pub struct TradeListingSummary { - pub event_id: String, - pub seller_pubkey: String, - pub published_at: u64, - pub listing_id: String, - pub listing_addr: String, - pub title: String, - pub description: String, - pub product_type: String, - pub primary_bin_id: String, - pub unit_price_amount: String, - pub unit_price_currency: String, - pub unit_price_unit: String, - pub bin_display_amount: String, - pub bin_display_unit: String, - pub bin_display_label: Option<String>, - pub inventory_available: String, - pub availability: String, - pub location: String, - pub delivery_method: String, -} - -#[derive(uniffi::Record, Debug, Clone)] -pub struct TradeOrderDraft { - pub listing_addr: String, - pub seller_pubkey: String, - pub bin_id: String, - pub bin_count: String, - pub notes: Option<String>, - pub order_id: Option<String>, - pub recipient_pubkey: String, -} - -#[derive(uniffi::Record, Debug, Clone)] -pub struct TradeOrderSendResult { - pub event_id: String, - pub order_id: String, -} - -#[derive(uniffi::Record, Debug, Clone)] -pub struct TradeListingMessageSummary { - pub event_id: String, - pub author: String, - pub published_at: u64, - pub kind: u32, - pub message_type: String, - pub listing_addr: String, - pub order_id: Option<String>, - pub summary: String, - pub payload_json: String, -} - -#[cfg_attr(not(coverage_nightly), uniffi::export)] -impl RadrootsRuntime { - pub fn trade_listing_publish( - &self, - draft: TradeListingDraft, - ) -> Result<String, RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - #[cfg(feature = "nostr-client")] - { - let mgr = guard - .nostr - .as_ref() - .ok_or_else(|| RadrootsAppError::Msg("nostr not initialized".into()))?; - let listing = listing_from_draft(&draft)?; - let current_pubkey = current_pubkey_hex(self)?; - if listing.farm.pubkey != current_pubkey { - return Err(RadrootsAppError::Msg( - "farm_pubkey must match the active key".into(), - )); - } - let parts = listing_to_wire_parts(&listing) - .map_err(|e| RadrootsAppError::Msg(format!("listing encode failed: {e}")))?; - mgr.send_custom_event_blocking(parts.kind, parts.content, parts.tags) - .map_err(|e| RadrootsAppError::Msg(e.to_string())) - } - #[cfg(not(feature = "nostr-client"))] - { - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn trade_listings_fetch( - &self, - limit: u16, - since_unix: Option<u64>, - ) -> Result<Vec<TradeListingSummary>, RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - #[cfg(feature = "nostr-client")] - { - let mgr = guard - .nostr - .as_ref() - .ok_or_else(|| RadrootsAppError::Msg("nostr not initialized".into()))?; - let mut filter = - RadrootsNostrFilter::new().kind(RadrootsNostrKind::Custom(LISTING_KIND as u16)); - filter = filter.limit(limit.into()); - if let Some(since) = since_unix { - filter = filter.since(RadrootsNostrTimestamp::from(since)); - } - - let events = mgr - .fetch_events_blocking(filter, core::time::Duration::from_secs(10)) - .map_err(|e| RadrootsAppError::Msg(e.to_string()))?; - let mut out = Vec::new(); - for ev in events { - let event = radroots_event_from_nostr(&ev); - if event.kind != LISTING_KIND { - continue; - } - match validate_listing_event(&event) { - Ok(listing) => { - out.push(listing_summary_from_trade(listing, &event)); - } - Err(_) => continue, - } - } - out.sort_by(|a, b| b.published_at.cmp(&a.published_at)); - Ok(out) - } - #[cfg(not(feature = "nostr-client"))] - { - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn trade_listing_send_validation_request( - &self, - listing_event_id: String, - seller_pubkey: String, - listing_id: String, - recipient_pubkey: String, - ) -> Result<String, RadrootsAppError> { - let listing_addr = listing_addr_from_parts(&seller_pubkey, &listing_id)?; - let payload = - TradeListingMessagePayload::ListingValidateRequest(TradeListingValidateRequest { - listing_event: Some(RadrootsNostrEventPtr { - id: listing_event_id, - relays: None, - }), - }); - self.send_trade_listing_message( - TradeListingMessageType::ListingValidateRequest, - listing_addr, - None, - payload, - recipient_pubkey, - ) - } - - pub fn trade_listing_send_order_request( - &self, - draft: TradeOrderDraft, - ) -> Result<TradeOrderSendResult, RadrootsAppError> { - #[cfg(feature = "nostr-client")] - { - let order_id = normalize_optional_id(draft.order_id); - let order_id = order_id - .unwrap_or_else(|| format!("order-{}", chrono::Utc::now().timestamp_millis())); - let buyer_pubkey = current_pubkey_hex(self)?; - let seller_pubkey = normalize_pubkey(&draft.seller_pubkey)?; - - let bin_id = draft.bin_id.trim(); - if bin_id.is_empty() { - return Err(RadrootsAppError::Msg("bin_id is required".into())); - } - let bin_count = parse_u32(&draft.bin_count, "bin_count")?; - if bin_count == 0 { - return Err(RadrootsAppError::Msg("bin_count must be > 0".into())); - } - - let item = TradeOrderItem { - bin_id: bin_id.to_string(), - bin_count, - }; - - let notes = draft - .notes - .as_deref() - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()); - - let order = TradeOrder { - order_id: order_id.clone(), - listing_addr: draft.listing_addr.clone(), - buyer_pubkey, - seller_pubkey, - items: vec![item], - discounts: None, - notes, - status: TradeOrderStatus::Requested, - }; - - let payload = TradeListingMessagePayload::OrderRequest(order); - let event_id = self.send_trade_listing_message( - TradeListingMessageType::OrderRequest, - draft.listing_addr, - Some(order_id.clone()), - payload, - draft.recipient_pubkey, - )?; - - Ok(TradeOrderSendResult { event_id, order_id }) - } - #[cfg(not(feature = "nostr-client"))] - { - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } - - pub fn trade_listing_fetch_messages( - &self, - listing_addr: String, - order_id: Option<String>, - limit: u16, - since_unix: Option<u64>, - ) -> Result<Vec<TradeListingMessageSummary>, RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - #[cfg(feature = "nostr-client")] - { - let mgr = guard - .nostr - .as_ref() - .ok_or_else(|| RadrootsAppError::Msg("nostr not initialized".into()))?; - - let kinds: Vec<RadrootsNostrKind> = TRADE_LISTING_KINDS - .iter() - .map(|k| RadrootsNostrKind::Custom(*k)) - .collect(); - - let mut filter = RadrootsNostrFilter::new().kinds(kinds); - filter = filter.limit(limit.into()); - if let Some(since) = since_unix { - filter = filter.since(RadrootsNostrTimestamp::from(since)); - } - - let events = mgr - .fetch_events_blocking(filter, core::time::Duration::from_secs(10)) - .map_err(|e| RadrootsAppError::Msg(e.to_string()))?; - - let mut out = Vec::new(); - for ev in events { - let content = ev.content.clone(); - let envelope: TradeListingEnvelope<TradeListingMessagePayload> = - match serde_json::from_str(&content) { - Ok(env) => env, - Err(_) => continue, - }; - if envelope.validate().is_err() { - continue; - } - if envelope.listing_addr != listing_addr { - continue; - } - if let Some(ref oid) = order_id { - if envelope.order_id.as_deref() != Some(oid) { - continue; - } - } - let kind_u32 = ev.kind.as_u16() as u32; - if envelope.message_type.kind() as u32 != kind_u32 { - continue; - } - - let summary = message_summary(&envelope.payload); - out.push(TradeListingMessageSummary { - event_id: ev.id.to_string(), - author: ev.pubkey.to_string(), - published_at: ev.created_at.as_secs(), - kind: kind_u32, - message_type: message_type_label(envelope.message_type).to_string(), - listing_addr: envelope.listing_addr.clone(), - order_id: envelope.order_id.clone(), - summary, - payload_json: content, - }); - } - out.sort_by(|a, b| b.published_at.cmp(&a.published_at)); - Ok(out) - } - #[cfg(not(feature = "nostr-client"))] - { - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } -} - -impl RadrootsRuntime { - fn send_trade_listing_message( - &self, - message_type: TradeListingMessageType, - listing_addr: String, - order_id: Option<String>, - payload: TradeListingMessagePayload, - recipient_pubkey: String, - ) -> Result<String, RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - #[cfg(feature = "nostr-client")] - { - let mgr = guard - .nostr - .as_ref() - .ok_or_else(|| RadrootsAppError::Msg("nostr not initialized".into()))?; - let recipient_hex = normalize_pubkey(&recipient_pubkey)?; - let envelope = TradeListingEnvelope::new( - message_type, - listing_addr.clone(), - order_id.clone(), - payload, - ); - envelope - .validate() - .map_err(|e| RadrootsAppError::Msg(e.to_string()))?; - let content = serde_json::to_string(&envelope) - .map_err(|e| RadrootsAppError::Msg(format!("encode envelope failed: {e}")))?; - let tags = trade_listing_dvm_tags(recipient_hex, listing_addr, order_id); - mgr.send_custom_event_blocking(message_type.kind() as u32, content, tags) - .map_err(|e| RadrootsAppError::Msg(e.to_string())) - } - #[cfg(not(feature = "nostr-client"))] - { - Err(RadrootsAppError::Msg("nostr disabled".into())) - } - } -} - -fn listing_from_draft(draft: &TradeListingDraft) -> Result<RadrootsListing, RadrootsAppError> { - let listing_id = draft - .listing_id - .as_deref() - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - .unwrap_or_else(|| format!("listing-{}", chrono::Utc::now().timestamp_millis())); - let farm_pubkey = draft.farm_pubkey.trim(); - if farm_pubkey.is_empty() { - return Err(RadrootsAppError::Msg("farm_pubkey is required".into())); - } - let farm_pubkey = normalize_pubkey(farm_pubkey)?; - let farm_d_tag = draft.farm_d_tag.trim(); - if farm_d_tag.is_empty() { - return Err(RadrootsAppError::Msg("farm_d_tag is required".into())); - } - - let title = draft.title.trim(); - if title.is_empty() { - return Err(RadrootsAppError::Msg("title is required".into())); - } - let description = draft.description.trim(); - if description.is_empty() { - return Err(RadrootsAppError::Msg("description is required".into())); - } - let category = draft.category.trim(); - if category.is_empty() { - return Err(RadrootsAppError::Msg("category is required".into())); - } - let location_primary = draft.location_primary.trim(); - if location_primary.is_empty() { - return Err(RadrootsAppError::Msg("location is required".into())); - } - - let display_amount = parse_decimal(&draft.bin_display_amount, "bin_display_amount")?; - ensure_non_negative(&display_amount, "bin_display_amount")?; - let display_unit = parse_unit(&draft.bin_display_unit)?; - let unit_price_amount = parse_decimal(&draft.unit_price, "unit_price")?; - ensure_non_negative(&unit_price_amount, "unit_price")?; - let currency = parse_currency(&draft.currency)?; - let inventory = parse_decimal(&draft.inventory, "inventory")?; - ensure_non_negative(&inventory, "inventory")?; - - let display_quantity = RadrootsCoreQuantity::new(display_amount, display_unit); - let canonical_quantity = display_quantity - .to_canonical() - .map_err(|e| RadrootsAppError::Msg(format!("invalid bin_display_unit: {e}")))?; - let unit_price = RadrootsCoreMoney::new(unit_price_amount, currency); - let price_per_display_unit = RadrootsCoreQuantityPrice::new( - unit_price.clone(), - RadrootsCoreQuantity::new(RadrootsCoreDecimal::ONE, display_unit), - ); - let price_per_canonical_unit = price_per_display_unit - .try_to_canonical_unit_price() - .map_err(|e| RadrootsAppError::Msg(format!("invalid unit_price: {e:?}")))?; - let bin_label = clean_optional(&draft.bin_label); - let bin_id = normalize_optional_id(draft.bin_id.clone()).unwrap_or_else(|| "bin-1".to_string()); - let bin = RadrootsListingBin { - bin_id: bin_id.clone(), - quantity: canonical_quantity, - price_per_canonical_unit, - display_amount: Some(display_amount), - display_unit: Some(display_unit), - display_label: bin_label, - display_price: Some(unit_price), - display_price_unit: Some(display_unit), - }; - - let delivery_method = parse_delivery_method(&draft.delivery_method)?; - - Ok(RadrootsListing { - d_tag: listing_id, - farm: RadrootsListingFarmRef { - pubkey: farm_pubkey, - d_tag: farm_d_tag.to_string(), - }, - product: RadrootsListingProduct { - key: category.to_string(), - title: title.to_string(), - category: category.to_string(), - summary: Some(description.to_string()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: bin_id, - bins: vec![bin], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(inventory), - availability: Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Active, - }), - delivery_method: Some(delivery_method), - location: Some(RadrootsListingLocation { - primary: location_primary.to_string(), - city: clean_optional(&draft.location_city), - region: clean_optional(&draft.location_region), - country: clean_optional(&draft.location_country), - lat: None, - lng: None, - geohash: None, - }), - images: None, - }) -} - -fn listing_summary_from_trade( - listing: RadrootsTradeListing, - event: &radroots_events::RadrootsNostrEvent, -) -> TradeListingSummary { - let bin = listing - .listing - .bins - .iter() - .find(|bin| bin.bin_id == listing.primary_bin_id) - .or_else(|| listing.listing.bins.first()) - .expect("validated listing must include bins"); - let (display_amount, display_unit) = match (bin.display_amount.as_ref(), bin.display_unit) { - (Some(amount), Some(unit)) => (amount.clone(), unit), - _ => (bin.quantity.amount.clone(), bin.quantity.unit), - }; - let display_label = bin.display_label.clone().or(bin.quantity.label.clone()); - let display_label = clean_optional(&display_label); - let (unit_price_amount, unit_price_currency, unit_price_unit) = - match bin.price_per_canonical_unit.try_to_unit_price(display_unit) { - Ok(price) => ( - price.amount.amount.to_string(), - price.amount.currency.to_string(), - price.quantity.unit.to_string(), - ), - Err(_) => match (&bin.display_price, bin.display_price_unit) { - (Some(price), Some(unit)) => ( - price.amount.to_string(), - price.currency.to_string(), - unit.to_string(), - ), - _ => ( - bin.price_per_canonical_unit.amount.amount.to_string(), - bin.price_per_canonical_unit.amount.currency.to_string(), - bin.price_per_canonical_unit.quantity.unit.to_string(), - ), - }, - }; - - TradeListingSummary { - event_id: event.id.clone(), - seller_pubkey: event.author.clone(), - published_at: event.created_at as u64, - listing_id: listing.listing_id, - listing_addr: listing.listing_addr, - title: listing.title, - description: listing.description, - product_type: listing.product_type, - primary_bin_id: listing.primary_bin_id, - unit_price_amount, - unit_price_currency, - unit_price_unit, - bin_display_amount: display_amount.to_string(), - bin_display_unit: display_unit.to_string(), - bin_display_label: display_label, - inventory_available: listing.inventory_available.to_string(), - availability: availability_label(&listing.availability), - location: format_location(&listing.location), - delivery_method: delivery_method_label(&listing.delivery_method).to_string(), - } -} - -fn listing_addr_from_parts( - seller_pubkey: &str, - listing_id: &str, -) -> Result<String, RadrootsAppError> { - let listing_id = listing_id.trim(); - if listing_id.is_empty() { - return Err(RadrootsAppError::Msg("listing_id is required".into())); - } - let seller_hex = normalize_pubkey(seller_pubkey)?; - Ok(TradeListingAddress { - kind: LISTING_KIND as u16, - seller_pubkey: seller_hex, - listing_id: listing_id.to_string(), - } - .as_str()) -} - -fn normalize_pubkey(pubkey: &str) -> Result<String, RadrootsAppError> { - let key = radroots_nostr_parse_pubkey(pubkey.trim()) - .map_err(|e| RadrootsAppError::Msg(e.to_string()))?; - Ok(key.to_hex()) -} - -fn normalize_optional_id(id: Option<String>) -> Option<String> { - id.as_deref() - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) -} - -#[cfg(feature = "nostr-client")] -fn current_pubkey_hex(runtime: &RadrootsRuntime) -> Result<String, RadrootsAppError> { - let guard = runtime - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; - let keys = guard - .selected_nostr_keys() - .ok_or_else(|| RadrootsAppError::Msg("no selected signing identity".into()))?; - Ok(keys.public_key().to_hex()) -} - -fn parse_decimal(value: &str, label: &str) -> Result<RadrootsCoreDecimal, RadrootsAppError> { - RadrootsCoreDecimal::from_str(value.trim()) - .map_err(|e| RadrootsAppError::Msg(format!("invalid {label}: {e}"))) -} - -fn parse_unit(value: &str) -> Result<RadrootsCoreUnit, RadrootsAppError> { - RadrootsCoreUnit::from_str(value.trim()) - .map_err(|e| RadrootsAppError::Msg(format!("invalid unit: {e}"))) -} - -fn parse_currency(value: &str) -> Result<RadrootsCoreCurrency, RadrootsAppError> { - RadrootsCoreCurrency::from_str(value.trim()) - .map_err(|e| RadrootsAppError::Msg(format!("invalid currency: {e}"))) -} - -fn parse_u32(value: &str, label: &str) -> Result<u32, RadrootsAppError> { - value - .trim() - .parse::<u32>() - .map_err(|e| RadrootsAppError::Msg(format!("invalid {label}: {e}"))) -} - -fn ensure_non_negative(value: &RadrootsCoreDecimal, label: &str) -> Result<(), RadrootsAppError> { - if value.is_sign_negative() { - return Err(RadrootsAppError::Msg(format!( - "{label} must be non-negative" - ))); - } - Ok(()) -} - -fn parse_delivery_method(value: &str) -> Result<RadrootsListingDeliveryMethod, RadrootsAppError> { - let raw = value.trim(); - if raw.is_empty() { - return Err(RadrootsAppError::Msg("delivery_method is required".into())); - } - let lowered = raw.to_ascii_lowercase(); - Ok(match lowered.as_str() { - "pickup" => RadrootsListingDeliveryMethod::Pickup, - "local_delivery" | "local delivery" => RadrootsListingDeliveryMethod::LocalDelivery, - "shipping" => RadrootsListingDeliveryMethod::Shipping, - _ => RadrootsListingDeliveryMethod::Other { - method: raw.to_string(), - }, - }) -} - -fn availability_label(availability: &RadrootsListingAvailability) -> String { - match availability { - RadrootsListingAvailability::Status { status } => match status { - RadrootsListingStatus::Active => "active".to_string(), - RadrootsListingStatus::Sold => "sold".to_string(), - RadrootsListingStatus::Other { value } => value.clone(), - }, - RadrootsListingAvailability::Window { start, end } => { - let start = start - .map(|s| s.to_string()) - .unwrap_or_else(|| "unknown".into()); - let end = end - .map(|s| s.to_string()) - .unwrap_or_else(|| "unknown".into()); - format!("{start} - {end}") - } - } -} - -fn delivery_method_label(method: &RadrootsListingDeliveryMethod) -> &'static str { - match method { - RadrootsListingDeliveryMethod::Pickup => "pickup", - RadrootsListingDeliveryMethod::LocalDelivery => "local delivery", - RadrootsListingDeliveryMethod::Shipping => "shipping", - RadrootsListingDeliveryMethod::Other { .. } => "other", - } -} - -fn format_location(location: &RadrootsListingLocation) -> String { - let mut parts = Vec::with_capacity(4); - if !location.primary.trim().is_empty() { - parts.push(location.primary.trim()); - } - if let Some(city) = location.city.as_deref() { - if !city.trim().is_empty() { - parts.push(city.trim()); - } - } - if let Some(region) = location.region.as_deref() { - if !region.trim().is_empty() { - parts.push(region.trim()); - } - } - if let Some(country) = location.country.as_deref() { - if !country.trim().is_empty() { - parts.push(country.trim()); - } - } - if parts.is_empty() { - "n/a".to_string() - } else { - parts.join(", ") - } -} - -fn clean_optional(value: &Option<String>) -> Option<String> { - value - .as_deref() - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) -} - -fn message_type_label(message_type: TradeListingMessageType) -> &'static str { - match message_type { - TradeListingMessageType::ListingValidateRequest => "listing_validate_request", - TradeListingMessageType::ListingValidateResult => "listing_validate_result", - TradeListingMessageType::OrderRequest => "order_request", - TradeListingMessageType::OrderResponse => "order_response", - TradeListingMessageType::OrderRevision => "order_revision", - TradeListingMessageType::OrderRevisionAccept => "order_revision_accept", - TradeListingMessageType::OrderRevisionDecline => "order_revision_decline", - TradeListingMessageType::Question => "question", - TradeListingMessageType::Answer => "answer", - TradeListingMessageType::DiscountRequest => "discount_request", - TradeListingMessageType::DiscountOffer => "discount_offer", - TradeListingMessageType::DiscountAccept => "discount_accept", - TradeListingMessageType::DiscountDecline => "discount_decline", - TradeListingMessageType::Cancel => "cancel", - TradeListingMessageType::FulfillmentUpdate => "fulfillment_update", - TradeListingMessageType::Receipt => "receipt", - } -} - -fn message_summary(payload: &TradeListingMessagePayload) -> String { - match payload { - TradeListingMessagePayload::ListingValidateRequest(_) => { - "Listing validation requested".to_string() - } - TradeListingMessagePayload::ListingValidateResult(result) => { - if result.valid { - "Listing validated".to_string() - } else if let Some(first) = result.errors.first() { - format!("Listing invalid: {first}") - } else { - "Listing invalid".to_string() - } - } - TradeListingMessagePayload::OrderRequest(order) => { - let item = order.items.first(); - match item { - Some(i) => format!("Order requested: {}x {}", i.bin_count, i.bin_id), - None => "Order requested".to_string(), - } - } - TradeListingMessagePayload::OrderResponse(res) => { - if res.accepted { - "Order accepted".to_string() - } else if let Some(reason) = res.reason.as_deref() { - format!("Order declined: {reason}") - } else { - "Order declined".to_string() - } - } - TradeListingMessagePayload::OrderRevision(_) => "Order revision proposed".to_string(), - TradeListingMessagePayload::OrderRevisionAccept(_) => "Order revision accepted".to_string(), - TradeListingMessagePayload::OrderRevisionDecline(_) => { - "Order revision declined".to_string() - } - TradeListingMessagePayload::Question(q) => format!("Question: {}", q.question_text), - TradeListingMessagePayload::Answer(a) => format!("Answer: {}", a.answer_text), - TradeListingMessagePayload::DiscountRequest(_) => "Discount requested".to_string(), - TradeListingMessagePayload::DiscountOffer(_) => "Discount offered".to_string(), - TradeListingMessagePayload::DiscountAccept(_) => "Discount accepted".to_string(), - TradeListingMessagePayload::DiscountDecline(_) => "Discount declined".to_string(), - TradeListingMessagePayload::Cancel(c) => { - if let Some(reason) = c.reason.as_deref() { - format!("Order cancelled: {reason}") - } else { - "Order cancelled".to_string() - } - } - TradeListingMessagePayload::FulfillmentUpdate(update) => { - format!("Fulfillment update: {:?}", update.status) - } - TradeListingMessagePayload::Receipt(receipt) => { - if receipt.acknowledged { - "Receipt acknowledged".to_string() - } else { - "Receipt update".to_string() - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn listing_from_draft_requires_fields() { - let draft = TradeListingDraft { - listing_id: None, - farm_pubkey: "".into(), - farm_d_tag: "".into(), - title: "".into(), - description: "Desc".into(), - category: "Coffee".into(), - bin_display_amount: "1".into(), - bin_display_unit: "lb".into(), - unit_price: "10.00".into(), - currency: "USD".into(), - bin_label: None, - bin_id: None, - inventory: "10".into(), - delivery_method: "shipping".into(), - location_primary: "Farm".into(), - location_city: None, - location_region: None, - location_country: None, - }; - assert!(listing_from_draft(&draft).is_err()); - } - - #[test] - fn listing_from_draft_builds_listing() { - let draft = TradeListingDraft { - listing_id: Some("AAAAAAAAAAAAAAAAAAAAAg".into()), - farm_pubkey: "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6".into(), - farm_d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - title: "Coffee".into(), - description: "Washed".into(), - category: "coffee".into(), - bin_display_amount: "1".into(), - bin_display_unit: "lb".into(), - unit_price: "12.50".into(), - currency: "USD".into(), - bin_label: Some("bag".into()), - bin_id: None, - inventory: "5".into(), - delivery_method: "shipping".into(), - location_primary: "Farm".into(), - location_city: Some("Town".into()), - location_region: Some("Region".into()), - location_country: Some("US".into()), - }; - let listing = listing_from_draft(&draft).expect("listing builds"); - assert_eq!(listing.d_tag, "AAAAAAAAAAAAAAAAAAAAAg"); - assert_eq!(listing.product.title, "Coffee"); - assert!(listing.delivery_method.is_some()); - assert!(listing.location.is_some()); - } -} diff --git a/crates/app-core/tests/logging_error.rs b/crates/app-core/tests/logging_error.rs @@ -1,9 +0,0 @@ -use radroots_app_core::RadrootsAppError; -use radroots_app_core::logging; - -#[test] -fn init_logging_stdout_maps_global_subscriber_error() { - let _ = tracing_subscriber::fmt().try_init(); - let err = logging::init_logging_stdout(); - assert!(matches!(err, Err(RadrootsAppError::Msg(_)))); -} diff --git a/crates/app-core/tests/no_nostr_runtime.rs b/crates/app-core/tests/no_nostr_runtime.rs @@ -1,153 +0,0 @@ -#![cfg(not(feature = "nostr-client"))] - -use radroots_app_core::logging; -use radroots_app_core::runtime::builder::RuntimeBuilder; -use radroots_app_core::runtime::nostr::{ - NostrConnectionStatus, NostrEvent, NostrLight, NostrPost, NostrPostEventMetadata, NostrProfile, - NostrProfileEventMetadata, -}; -use radroots_app_core::{RadrootsAppError, RadrootsRuntime}; -use radroots_net_core::config::NetConfig; - -fn expect_disabled<T>(result: Result<T, RadrootsAppError>) { - match result { - Err(RadrootsAppError::Msg(message)) => assert_eq!(message, "nostr disabled"), - _ => panic!("expected nostr disabled error"), - } -} - -#[test] -fn runtime_info_and_platform_paths_are_exercised() { - let runtime = RadrootsRuntime::new().expect("runtime"); - assert!(runtime.uptime_millis() >= 0); - - runtime.stop(); - runtime.stop(); -} - -#[test] -fn key_management_disabled_paths_are_exercised() { - let runtime = RadrootsRuntime::new().expect("runtime"); - - assert!(!runtime.accounts_has_selected_signing_identity()); - assert_eq!(runtime.accounts_selected_npub(), None); - expect_disabled(runtime.accounts_list_ids()); - expect_disabled(runtime.accounts_generate(Some("alpha".to_string()), true)); - expect_disabled(runtime.accounts_import_secret( - "deadbeef".to_string(), - Some("alpha".to_string()), - true, - )); - expect_disabled(runtime.accounts_import_from_path( - "/tmp/nostr.json".to_string(), - Some("alpha".to_string()), - true, - )); - expect_disabled(runtime.accounts_export_selected_secret_hex()); - expect_disabled(runtime.accounts_select("account-1".to_string())); - expect_disabled(runtime.accounts_remove("account-1".to_string())); -} - -#[test] -fn nostr_disabled_paths_are_exercised() { - let runtime = RadrootsRuntime::new().expect("runtime"); - - let status = runtime.nostr_connection_status(); - assert_eq!(status.connected, 0); - assert_eq!(status.connecting, 0); - assert!(status.last_error.is_none()); - - assert!(runtime.nostr_profile_for_self().is_none()); - assert!(runtime.nostr_next_post_event().is_none()); - - expect_disabled(runtime.nostr_set_default_relays(vec!["wss://relay.example.com".to_string()])); - expect_disabled(runtime.nostr_connect_if_key_present()); - expect_disabled(runtime.nostr_post_profile(None, None, None, None)); - expect_disabled(runtime.nostr_post_text_note("hello".to_string())); - expect_disabled(runtime.nostr_fetch_text_notes(25, Some(0))); - expect_disabled(runtime.nostr_post_reply( - "event-id".to_string(), - "author".to_string(), - "reply".to_string(), - None, - )); - expect_disabled(runtime.nostr_start_post_event_stream(None)); - expect_disabled(runtime.nostr_stop_post_event_stream()); -} - -#[test] -fn runtime_builder_and_logging_paths_are_exercised() { - let handle = RuntimeBuilder::new() - .with_config(NetConfig::default()) - .manage_runtime(false) - .build() - .expect("build net handle"); - drop(handle); - let default_handle = RuntimeBuilder::new() - .build() - .expect("build default net handle"); - drop(default_handle); - - let err = logging::init_logging(Some("/dev/null/file.log".to_string()), None, Some(false)); - assert!(matches!(err, Err(RadrootsAppError::Msg(_)))); - let _ = logging::init_logging(None, None, None); - let _ = logging::init_logging(None, Some("app.log".to_string()), Some(false)); - let _ = logging::init_logging_stdout(); - - assert!(logging::log_info("info".to_string()).is_ok()); - assert!(logging::log_error("error".to_string()).is_ok()); - assert!(logging::log_debug("debug".to_string()).is_ok()); -} - -#[test] -fn nostr_records_and_enums_are_exercised() { - let status = NostrConnectionStatus { - light: NostrLight::Yellow, - connected: 1, - connecting: 2, - last_error: Some("err".to_string()), - }; - let _status_debug = format!("{status:?}"); - let _status_clone = status.clone(); - - let profile = NostrProfile::default(); - let _profile_debug = format!("{profile:?}"); - let _profile_clone = profile.clone(); - - let profile_event = NostrProfileEventMetadata { - id: "id".to_string(), - author: "author".to_string(), - published_at: 1, - profile, - }; - let _profile_event_debug = format!("{profile_event:?}"); - let _profile_event_clone = profile_event.clone(); - - let event = NostrEvent { - id: "event-id".to_string(), - author: "event-author".to_string(), - created_at: 2, - kind: 1, - content: "content".to_string(), - }; - let _event_debug = format!("{event:?}"); - let _event_clone = event.clone(); - - let post = NostrPost { - content: "post".to_string(), - }; - let _post_debug = format!("{post:?}"); - let _post_clone = post.clone(); - - let post_event = NostrPostEventMetadata { - id: "id".to_string(), - author: "author".to_string(), - published_at: 3, - post, - }; - let _post_event_debug = format!("{post_event:?}"); - let _post_event_clone = post_event.clone(); - - assert!(matches!(NostrLight::Red, NostrLight::Red)); - assert!(matches!(NostrLight::Green, NostrLight::Green)); -} diff --git a/crates/app-wasm/Cargo.toml b/crates/app-wasm/Cargo.toml @@ -1,19 +0,0 @@ -[package] -name = "radroots-app-wasm" -version = "0.1.0-alpha.1" -edition.workspace = true -authors = ["Radroots Authors"] -rust-version.workspace = true -license.workspace = true -description = "wasm application runtime bindings for radroots app surfaces" -repository.workspace = true -homepage.workspace = true -documentation = "https://docs.rs/radroots-app-wasm" -readme.workspace = true - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -radroots-app-core = { workspace = true, default-features = false } -wasm-bindgen = { workspace = true } diff --git a/crates/app-wasm/README.md b/crates/app-wasm/README.md @@ -1,14 +0,0 @@ -# radroots-app-wasm - -Wasm application runtime bindings for Rad Roots app surfaces. - -## Goals - -- define stable wasm runtime interfaces for app metadata and startup -- keep wasm runtime behavior deterministic across supported browser targets -- support feature-gated bindings backed by `radroots-app-core` -- provide reusable wasm entry points for higher-level Rad Roots app crates - -## License - -Licensed under AGPL-3.0. See LICENSE. diff --git a/crates/app-wasm/src/lib.rs b/crates/app-wasm/src/lib.rs @@ -1,31 +0,0 @@ -#![forbid(unsafe_code)] - -use wasm_bindgen::prelude::wasm_bindgen; - -#[wasm_bindgen] -pub fn app_wasm_build_info_json() -> String { - let runtime = radroots_app_core::RadrootsRuntime::new() - .expect("runtime init must succeed with radroots-app-core no-default-features"); - runtime.info_json() -} - -pub fn coverage_branch_probe(input: bool) -> &'static str { - if input { "app-wasm" } else { "app-wasm" } -} - -#[cfg(test)] -mod tests { - use super::{app_wasm_build_info_json, coverage_branch_probe}; - - #[test] - fn app_wasm_build_info_json_contains_runtime_keys() { - let json = app_wasm_build_info_json(); - assert!(json.contains("\"app\"")); - } - - #[test] - fn coverage_branch_probe_hits_both_paths() { - assert_eq!(coverage_branch_probe(true), "app-wasm"); - assert_eq!(coverage_branch_probe(false), "app-wasm"); - } -} diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -1092,7 +1092,7 @@ mod tests { fn coverage_profiles_default_when_contract_file_is_missing() { let root = temp_dir_path("profile_missing"); fs::create_dir_all(&root).expect("create root"); - let profile = read_coverage_profile(&root, "radroots-app-core").expect("read profile"); + let profile = read_coverage_profile(&root, "radroots-log").expect("read profile"); assert!(!profile.no_default_features); assert!(profile.features.is_empty()); assert_eq!(profile.test_threads, None); @@ -1111,14 +1111,14 @@ no_default_features = false features = ["std"] test_threads = 2 -[profiles.crates."radroots-app-core"] +[profiles.crates."radroots-log"] no_default_features = true features = ["rt"] "#, ) .expect("write profiles"); - let app_profile = read_coverage_profile(&root, "radroots-app-core").expect("app profile"); + let app_profile = read_coverage_profile(&root, "radroots-log").expect("app profile"); assert!(app_profile.no_default_features); assert_eq!(app_profile.features, vec!["rt".to_string()]); assert_eq!(app_profile.test_threads, Some(2)); @@ -1138,12 +1138,12 @@ features = ["rt"] fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( coverage_dir.join("profiles.toml"), - r#"[profiles.crates."radroots-app-core"] + r#"[profiles.crates."radroots-log"] test_threads = 4 "#, ) .expect("write profiles"); - let profile = read_coverage_profile(&root, "radroots-app-core") + let profile = read_coverage_profile(&root, "radroots-log") .expect("valid positive thread profile"); assert_eq!(profile.test_threads, Some(4)); fs::remove_dir_all(root).expect("remove root"); @@ -1156,14 +1156,14 @@ test_threads = 4 fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( coverage_dir.join("profiles.toml"), - r#"[profiles.crates."radroots-app-core"] + r#"[profiles.crates."radroots-log"] features = [""] test_threads = 0 "#, ) .expect("write profiles"); - let err = read_coverage_profile(&root, "radroots-app-core").expect_err("invalid profile"); + let err = read_coverage_profile(&root, "radroots-log").expect_err("invalid profile"); assert!( err.contains("empty feature value"), "unexpected error: {err}" @@ -1179,7 +1179,7 @@ test_threads = 0 fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write(coverage_dir.join("profiles.toml"), "[profiles.default\n") .expect("write invalid profiles"); - let err = read_coverage_profile(&root, "radroots-app-core").expect_err("invalid toml"); + let err = read_coverage_profile(&root, "radroots-log").expect_err("invalid toml"); assert!(err.contains("failed to parse")); fs::remove_dir_all(root).expect("remove root"); } @@ -1191,14 +1191,13 @@ test_threads = 0 fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( coverage_dir.join("profiles.toml"), - r#"[profiles.crates."radroots-app-core"] + r#"[profiles.crates."radroots-log"] test_threads = 0 "#, ) .expect("write profiles"); - let err = - read_coverage_profile(&root, "radroots-app-core").expect_err("invalid thread count"); + let err = read_coverage_profile(&root, "radroots-log").expect_err("invalid thread count"); assert!(err.contains("test_threads > 0")); fs::remove_dir_all(root).expect("remove root");