app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 3af620fde90c4e4a357a81692cb1555fb4095e90
parent 734311bcf65edb3d522b917341c3c3cb72aed186
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 19:29:40 +0000

ffi: scaffold safe application DTOs

- pin UniFFI 0.32 and initialize the proc-macro boundary
- map revisioned Rust snapshots into public Kotlin-safe records
- keep credentials and generic server fields out of normal DTOs
- define the generated Kotlin namespace and native library name

Diffstat:
Mcore/Cargo.lock | 377+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/Cargo.toml | 1+
Mcore/crates/ffi/Cargo.toml | 8++++++++
Acore/crates/ffi/src/dto.rs | 258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/ffi/src/lib.rs | 15++++++++-------
Acore/crates/ffi/uniffi.toml | 3+++
Mdocs/architecture/rust-ffi-boundary.md | 9+++++++--
Mdocs/implementation/nostr-runtime-rcld.md | 2+-
8 files changed, 663 insertions(+), 10 deletions(-)

diff --git a/core/Cargo.lock b/core/Cargo.lock @@ -48,6 +48,12 @@ dependencies = [ ] [[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] name = "apple-native-keyring-store" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -65,6 +71,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] +name = "askama" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc" +dependencies = [ + "askama_macros", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738" +dependencies = [ + "askama_parser", + "basic-toml", + "glob", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.119", +] + +[[package]] +name = "askama_macros" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a" +dependencies = [ + "askama_derive", +] + +[[package]] +name = "askama_parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da" +dependencies = [ + "rustc-hash", + "serde", + "serde_derive", + "unicode-ident", + "winnow", +] + +[[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -272,6 +331,15 @@ 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" @@ -381,6 +449,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.19", +] + +[[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -634,6 +735,15 @@ dependencies = [ ] [[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", +] + +[[package]] name = "futures" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -764,6 +874,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] name = "gloo-timers" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -776,6 +892,17 @@ 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.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -800,6 +927,12 @@ dependencies = [ ] [[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -974,6 +1107,8 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -1104,6 +1239,12 @@ dependencies = [ ] [[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1130,6 +1271,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" [[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] name = "nostr" version = "0.44.1" source = "git+https://github.com/rust-nostr/nostr.git?rev=5bba5163eb77107f82c4a8262cf29d7f33a73219#5bba5163eb77107f82c4a8262cf29d7f33a73219" @@ -1388,6 +1539,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] name = "polling" version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1493,6 +1650,11 @@ dependencies = [ [[package]] name = "radroots-studio-ffi" version = "0.1.0-alpha.0" +dependencies = [ + "radroots-studio-application", + "radroots-studio-domain", + "uniffi", +] [[package]] name = "radroots-studio-nostr" @@ -1693,6 +1855,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1764,6 +1932,26 @@ 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 2.0.119", +] + +[[package]] name = "scrypt" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1847,6 +2035,16 @@ dependencies = [ ] [[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] name = "serde" version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1901,6 +2099,15 @@ dependencies = [ ] [[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] name = "sha1" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1957,6 +2164,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] name = "socket2" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1985,6 +2198,12 @@ 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 = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2037,6 +2256,15 @@ 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" @@ -2199,6 +2427,21 @@ dependencies = [ ] [[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2229,6 +2472,12 @@ dependencies = [ ] [[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2311,6 +2560,125 @@ dependencies = [ ] [[package]] +name = "uniffi" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a782a48d72cfd7a2d65cfc7c691dbf5375c43104b3c195f7eccc716dcc3540c8" +dependencies = [ + "anyhow", + "cargo_metadata", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533b0312c73e3b54eb78a4b257ceae390962dd4767995778309a74644643f9ac" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_core" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e32e261c5b0dfaba6488f536e71957dddd6b1a498ac7eb791bee56b60a086be" +dependencies = [ + "anyhow", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ae78069a5e6772ef694fd5bdb628532c88d2c2f0e7142bf6a384636eadb1af" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "uniffi_macros" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330be6770532e86320df31f54c70bb0be67588594e8e77fa56e9083a3fed5d0d" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78de021f5547e56ab16c665a49d67d4fd3d31e77422f7739a2e9359d328cd9e7" +dependencies = [ + "anyhow", + "siphasher", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8201bb1907ed8a42d80e11cbc25c8a033e7a31c3cff1d911f56eedb81d4948" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e57996bc58009cc29bf04845d627ae313c2547b87171c1c349d6c51a1656c0" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + +[[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2483,6 +2851,15 @@ dependencies = [ ] [[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/core/Cargo.toml b/core/Cargo.toml @@ -34,6 +34,7 @@ secrecy = "=0.10.3" url = "=2.5.8" zeroize = "=1.9.0" tokio = "=1.47.1" +uniffi = "=0.32.0" [patch.crates-io] nostr = { git = "https://github.com/rust-nostr/nostr.git", rev = "5bba5163eb77107f82c4a8262cf29d7f33a73219" } diff --git a/core/crates/ffi/Cargo.toml b/core/crates/ffi/Cargo.toml @@ -6,5 +6,13 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +radroots-studio-application = { path = "../application" } +radroots-studio-domain = { path = "../domain" } +uniffi.workspace = true + [lints] workspace = true diff --git a/core/crates/ffi/src/dto.rs b/core/crates/ffi/src/dto.rs @@ -0,0 +1,258 @@ +use radroots_studio_application::{ + ActiveAccountSnapshot, AppLifecycle, AppSnapshot, ProfileLoadState, RelayConnectionState, + SessionState, +}; +use radroots_studio_domain::{ + AccountSummary, KeyAvailability, ProfileMetadata, SafeError, SignerKind, +}; + +#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] +pub struct SafeErrorDto { + pub code: String, + pub message: String, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] +pub enum AppLifecycleDto { + Booting, + Ready, + Fatal, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] +pub enum SessionStateDto { + SignedOut, + Activating, + Active, + SigningOut, + Failed, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] +pub enum RelayConnectionStateDto { + Disconnected, + Connecting, + Connected, + Degraded, + Error, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] +pub enum ProfileLoadStateDto { + Empty, + Loading, + Cached, + Fresh, + Error, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] +pub enum SignerKindDto { + LocalSecret, + WatchOnly, + RemoteNip46, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] +pub enum KeyAvailabilityDto { + Available, + CredentialMissing, + StoreUnavailable, + NotRequired, +} + +#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] +pub struct ProfileDto { + pub name: Option<String>, + pub display_name: Option<String>, + pub nip05: Option<String>, + pub about: Option<String>, + pub picture: Option<String>, +} + +#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] +pub struct AccountDto { + pub public_key_hex: String, + pub npub: String, + pub display_label: String, + pub signer_kind: SignerKindDto, + pub key_availability: KeyAvailabilityDto, + pub created_at_seconds: i64, + pub last_used_at_seconds: Option<i64>, +} + +#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] +pub struct ActiveAccountDto { + pub account: AccountDto, + pub relay_state: RelayConnectionStateDto, + pub profile_state: ProfileLoadStateDto, + pub profile: Option<ProfileDto>, +} + +#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] +pub struct AppSnapshotDto { + pub revision: u64, + pub lifecycle: AppLifecycleDto, + pub lifecycle_error: Option<SafeErrorDto>, + pub configured_relays: Vec<String>, + pub accounts: Vec<AccountDto>, + pub selected_public_key_hex: Option<String>, + pub session: SessionStateDto, + pub session_subject_public_key_hex: Option<String>, + pub session_error: Option<SafeErrorDto>, + pub active_account: Option<ActiveAccountDto>, + pub recoverable_problem: Option<SafeErrorDto>, +} + +impl From<&AppSnapshot> for AppSnapshotDto { + fn from(snapshot: &AppSnapshot) -> Self { + let (lifecycle, lifecycle_error) = match snapshot.lifecycle() { + AppLifecycle::Booting => (AppLifecycleDto::Booting, None), + AppLifecycle::Ready => (AppLifecycleDto::Ready, None), + AppLifecycle::Fatal(error) => (AppLifecycleDto::Fatal, Some(error.into())), + }; + let (session, session_subject_public_key_hex, session_error) = match snapshot.session() { + SessionState::SignedOut => (SessionStateDto::SignedOut, None, None), + SessionState::Activating(public_key) => { + (SessionStateDto::Activating, Some(public_key.to_hex()), None) + } + SessionState::Active => (SessionStateDto::Active, None, None), + SessionState::SigningOut => (SessionStateDto::SigningOut, None, None), + SessionState::Failed(error) => (SessionStateDto::Failed, None, Some(error.into())), + }; + Self { + revision: snapshot.revision().value(), + lifecycle, + lifecycle_error, + configured_relays: snapshot + .relay_configuration() + .relays() + .iter() + .map(|relay| relay.as_str().to_owned()) + .collect(), + accounts: snapshot.accounts().iter().map(AccountDto::from).collect(), + selected_public_key_hex: snapshot + .selected_account() + .map(radroots_studio_domain::PublicKey::to_hex), + session, + session_subject_public_key_hex, + session_error, + active_account: snapshot.active_account().map(ActiveAccountDto::from), + recoverable_problem: snapshot.recoverable_problem().map(SafeErrorDto::from), + } + } +} + +impl From<&AccountSummary> for AccountDto { + fn from(account: &AccountSummary) -> Self { + Self { + public_key_hex: account.public_key().to_hex(), + npub: account.npub().as_str().to_owned(), + display_label: account.display_label(), + signer_kind: account.signer_kind().into(), + key_availability: account.key_availability().into(), + created_at_seconds: account.created_at().timestamp().as_seconds(), + last_used_at_seconds: account + .last_used_at() + .map(radroots_studio_domain::UnixTimestamp::as_seconds), + } + } +} + +impl From<&ActiveAccountSnapshot> for ActiveAccountDto { + fn from(active: &ActiveAccountSnapshot) -> Self { + Self { + account: active.account().into(), + relay_state: active.relay_state().into(), + profile_state: active.profile_state().into(), + profile: active.profile().map(ProfileDto::from), + } + } +} + +impl From<&ProfileMetadata> for ProfileDto { + fn from(profile: &ProfileMetadata) -> Self { + Self { + name: profile.name().map(str::to_owned), + display_name: profile.display_name().map(str::to_owned), + nip05: profile.nip05().map(str::to_owned), + about: profile.about().map(str::to_owned), + picture: profile.picture().map(str::to_owned), + } + } +} + +impl From<SafeError> for SafeErrorDto { + fn from(error: SafeError) -> Self { + Self { + code: format!("{:?}", error.code()), + message: error.message().as_str().to_owned(), + } + } +} + +impl From<SignerKind> for SignerKindDto { + fn from(value: SignerKind) -> Self { + match value { + SignerKind::LocalSecret => Self::LocalSecret, + SignerKind::WatchOnly => Self::WatchOnly, + SignerKind::RemoteNip46 => Self::RemoteNip46, + } + } +} + +impl From<KeyAvailability> for KeyAvailabilityDto { + fn from(value: KeyAvailability) -> Self { + match value { + KeyAvailability::Available => Self::Available, + KeyAvailability::CredentialMissing => Self::CredentialMissing, + KeyAvailability::StoreUnavailable => Self::StoreUnavailable, + KeyAvailability::NotRequired => Self::NotRequired, + } + } +} + +impl From<RelayConnectionState> for RelayConnectionStateDto { + fn from(value: RelayConnectionState) -> Self { + match value { + RelayConnectionState::Disconnected => Self::Disconnected, + RelayConnectionState::Connecting => Self::Connecting, + RelayConnectionState::Connected => Self::Connected, + RelayConnectionState::Degraded => Self::Degraded, + RelayConnectionState::Error(_) => Self::Error, + } + } +} + +impl From<ProfileLoadState> for ProfileLoadStateDto { + fn from(value: ProfileLoadState) -> Self { + match value { + ProfileLoadState::Empty => Self::Empty, + ProfileLoadState::Loading => Self::Loading, + ProfileLoadState::Cached => Self::Cached, + ProfileLoadState::Fresh => Self::Fresh, + ProfileLoadState::Error(_) => Self::Error, + } + } +} + +#[cfg(test)] +mod tests { + use radroots_studio_application::{AppCore, RelayConfiguration}; + + use super::AppSnapshotDto; + + #[test] + fn snapshot_dto_is_revisioned_public_and_secret_free() { + let core = AppCore::in_memory(RelayConfiguration::default()); + let snapshot = core.bootstrap().expect("bootstrap"); + let dto = AppSnapshotDto::from(&snapshot); + let debug = format!("{dto:?}"); + + assert_eq!(dto.revision, 1); + assert!(dto.accounts.is_empty()); + assert!(!debug.contains("nsec")); + assert!(!debug.contains("secret_key")); + assert!(!debug.contains("server_url")); + } +} diff --git a/core/crates/ffi/src/lib.rs b/core/crates/ffi/src/lib.rs @@ -1,9 +1,10 @@ #![doc = "Radroots Studio `UniFFI` boundary."] -#[cfg(test)] -mod tests { - #[test] - fn crate_is_available_to_the_workspace() { - assert_eq!(env!("CARGO_PKG_NAME"), "radroots-studio-ffi"); - } -} +mod dto; + +pub use dto::{ + AccountDto, ActiveAccountDto, AppLifecycleDto, AppSnapshotDto, KeyAvailabilityDto, ProfileDto, + ProfileLoadStateDto, RelayConnectionStateDto, SafeErrorDto, SessionStateDto, SignerKindDto, +}; + +uniffi::setup_scaffolding!(); diff --git a/core/crates/ffi/uniffi.toml b/core/crates/ffi/uniffi.toml @@ -0,0 +1,3 @@ +[bindings.kotlin] +package_name = "org.radroots.studio.ffi" +cdylib_name = "radroots_studio_ffi" diff --git a/docs/architecture/rust-ffi-boundary.md b/docs/architecture/rust-ffi-boundary.md @@ -2,8 +2,8 @@ ## Status -Initial boundary contract. Generated names and paths will be recorded when the -binding pipeline exists. +The proc-macro UniFFI namespace is `radroots_studio_ffi`. Kotlin bindings use +the package `org.radroots.studio.ffi`; generated sources remain build output. ## Contract @@ -20,3 +20,8 @@ Potentially blocking storage, credential, and relay operations must not run on the Compose thread. Kotlin closes its observer and AppCore during application disposal. Tests cover native loading, callback ordering, re-entry, deregistration, cancellation, stale completion, and close races. + +The public snapshot DTO carries revisioned lifecycle, account, session, relay, +profile, and safe-error values. It contains no credential field. A generated +account's nsec is confined to the explicit one-time receipt added with the +command boundary. diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -521,7 +521,7 @@ handoff commit sequence. ### RCLD-09 -- [ ] 41. Create UniFFI scaffolding and DTO mapping. +- [x] 41. Create UniFFI scaffolding and DTO mapping. - [ ] 42. Expose approved AppCore commands through UniFFI. - [ ] 43. Expose observer callback and deregistration through UniFFI. - [ ] 44. Build native library artifacts from Cargo.