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:
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.