commit 52912c1fe42bcfe38e0b549cd21afa05905581da
parent da5c3476bc94b8a2bfbb000667b07148c9a56e11
Author: triesap <tyson@radroots.org>
Date: Sun, 12 Apr 2026 23:00:17 +0000
sdk: add optional identity and nostr adapters
Diffstat:
9 files changed, 242 insertions(+), 1 deletion(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -2647,7 +2647,13 @@ dependencies = [
"radroots_core",
"radroots_events",
"radroots_events_codec",
+ "radroots_identity",
+ "radroots_nostr",
+ "radroots_nostr_connect",
+ "radroots_nostr_signer",
"radroots_trade",
+ "tempfile",
+ "tokio",
]
[[package]]
diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml
@@ -13,7 +13,7 @@ documentation = "https://docs.rs/radroots_sdk"
readme = "README"
[features]
-default = ["std", "serde", "serde_json"]
+default = ["std", "serde", "serde_json", "identity-models"]
std = ["radroots_events/std", "radroots_events_codec/std", "radroots_trade/std"]
serde = ["radroots_events/serde", "radroots_trade/serde"]
serde_json = [
@@ -23,6 +23,17 @@ serde_json = [
"radroots_trade/serde_json",
]
nostr = ["radroots_events_codec/nostr"]
+identity-models = ["dep:radroots_identity", "radroots_identity/profile"]
+identity-storage = ["identity-models", "std", "radroots_identity/std"]
+signing = ["dep:radroots_nostr", "nostr"]
+relay-client = ["signing", "std", "radroots_nostr/client"]
+signer-adapters = [
+ "identity-models",
+ "signing",
+ "std",
+ "dep:radroots_nostr_connect",
+ "dep:radroots_nostr_signer",
+]
ts-rs = ["radroots_events/ts-rs", "radroots_trade/ts-rs"]
typeshare = ["radroots_events/typeshare"]
@@ -30,6 +41,12 @@ typeshare = ["radroots_events/typeshare"]
radroots_events = { workspace = true, default-features = false }
radroots_events_codec = { workspace = true, default-features = false }
radroots_trade = { workspace = true, default-features = false }
+radroots_identity = { workspace = true, optional = true, default-features = false }
+radroots_nostr = { workspace = true, optional = true, default-features = false }
+radroots_nostr_connect = { workspace = true, optional = true }
+radroots_nostr_signer = { workspace = true, optional = true, default-features = false }
[dev-dependencies]
radroots_core = { workspace = true, default-features = false, features = ["std"] }
+tempfile = { workspace = true }
+tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
diff --git a/crates/sdk/README b/crates/sdk/README
@@ -6,3 +6,18 @@ This crate provides the recommended Rust entrypoint for building, parsing, and
validating Rad Roots profile, farm, listing, and trade events. It is a thin
facade over the underlying `rr-rs` substrate crates and does not duplicate the
core event or trade implementations.
+
+The deterministic event contract lives at the crate root:
+
+- `profile`
+- `farm`
+- `listing`
+- `trade`
+
+Optional advanced substrate is explicitly feature-scoped:
+
+- `identity-models`: identity data types without local storage coupling
+- `identity-storage`: encrypted identity-file helpers
+- `signing`: Nostr builder and local signing adapters
+- `relay-client`: relay client and publish adapters
+- `signer-adapters`: NIP-46 and signer-session primitives
diff --git a/crates/sdk/src/adapters/mod.rs b/crates/sdk/src/adapters/mod.rs
@@ -0,0 +1,6 @@
+#[cfg(feature = "relay-client")]
+pub mod relay;
+#[cfg(feature = "signing")]
+pub mod signing;
+#[cfg(feature = "signer-adapters")]
+pub mod signer;
diff --git a/crates/sdk/src/adapters/relay.rs b/crates/sdk/src/adapters/relay.rs
@@ -0,0 +1,67 @@
+use crate::WireEventParts;
+use crate::adapters::signing::{SignedNostrEvent, event_builder_from_parts};
+use crate::identity::RadrootsIdentity;
+use radroots_nostr::prelude::{
+ RadrootsNostrClient, RadrootsNostrClientOptions, RadrootsNostrError,
+ RadrootsNostrEventId, RadrootsNostrOutput,
+};
+
+pub type RelayClient = RadrootsNostrClient;
+pub type RelayClientOptions = RadrootsNostrClientOptions;
+pub type RelayError = RadrootsNostrError;
+pub type RelayEventId = RadrootsNostrEventId;
+pub type RelayOutput<T> = RadrootsNostrOutput<T>;
+
+pub fn signerless_client() -> RelayClient {
+ RelayClient::new_signerless()
+}
+
+pub fn signerless_client_with_options(
+ options: RelayClientOptions,
+) -> Result<RelayClient, RelayError> {
+ RelayClient::new_signerless_with_options(options)
+}
+
+pub fn client_from_identity(identity: &RadrootsIdentity) -> RelayClient {
+ RelayClient::from_identity(identity)
+}
+
+pub async fn publish_parts(
+ client: &RelayClient,
+ parts: WireEventParts,
+) -> Result<RelayOutput<RelayEventId>, RelayError> {
+ client.send_event_builder(event_builder_from_parts(parts)?).await
+}
+
+pub async fn publish_signed_event(
+ client: &RelayClient,
+ event: &SignedNostrEvent,
+) -> Result<RelayOutput<RelayEventId>, RelayError> {
+ client.send_event(event).await
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{client_from_identity, signerless_client, signerless_client_with_options};
+ use crate::identity::RadrootsIdentity;
+ use tokio::runtime::Runtime;
+
+ #[test]
+ fn client_constructors_build_without_runtime_net() {
+ let identity = RadrootsIdentity::generate();
+ let _client = client_from_identity(&identity);
+ let _signerless = signerless_client();
+ let _signerless_with_options =
+ signerless_client_with_options(super::RelayClientOptions::new())
+ .expect("signerless client with options");
+ }
+
+ #[test]
+ fn signerless_client_has_no_signer() {
+ let runtime = Runtime::new().expect("tokio runtime");
+ runtime.block_on(async {
+ let client = signerless_client();
+ assert!(!client.has_signer().await);
+ });
+ }
+}
diff --git a/crates/sdk/src/adapters/signer.rs b/crates/sdk/src/adapters/signer.rs
@@ -0,0 +1,24 @@
+pub use radroots_nostr_connect::prelude::{
+ RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR, RADROOTS_NOSTR_CONNECT_RPC_KIND,
+ RadrootsNostrConnectBunkerUri, RadrootsNostrConnectClientMetadata,
+ RadrootsNostrConnectClientUri, RadrootsNostrConnectError, RadrootsNostrConnectMethod,
+ RadrootsNostrConnectPendingConnectionPollOutcome, RadrootsNostrConnectPermission,
+ RadrootsNostrConnectPermissions, RadrootsNostrConnectRemoteSessionCapability,
+ RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
+ RadrootsNostrConnectResponseEnvelope, RadrootsNostrConnectUri,
+};
+pub use radroots_nostr_signer::prelude::{
+ RadrootsNostrEmbeddedSignerBackend, RadrootsNostrSignerBackend,
+ RadrootsNostrSignerBackendCapabilities, RadrootsNostrSignerCapability,
+ RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerConnectProposal,
+ RadrootsNostrSignerError, RadrootsNostrSignerHandledRequest,
+ RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrLocalSignerAvailability,
+ RadrootsNostrLocalSignerCapability, RadrootsNostrRemoteSessionSignerCapability,
+ RadrootsNostrSignerManager, RadrootsNostrSignerNip46Codec,
+ RadrootsNostrSignerNip46ConnectDecision, RadrootsNostrSignerNip46Handler,
+ RadrootsNostrSignerNip46Policy, RadrootsNostrSignerNip46Signer,
+ RadrootsNostrSignerPublishTransition, RadrootsNostrSignerRequestAction,
+ RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerRequestResponseHint,
+ RadrootsNostrSignerSessionLookup, connect_response_outcome, handled_request_for_action,
+ response_from_hint,
+};
diff --git a/crates/sdk/src/adapters/signing.rs b/crates/sdk/src/adapters/signing.rs
@@ -0,0 +1,65 @@
+use crate::WireEventParts;
+use crate::identity::RadrootsIdentity;
+use radroots_nostr::prelude::{
+ RadrootsNostrError, radroots_nostr_build_event,
+};
+
+pub type SignedNostrEvent = radroots_nostr::prelude::RadrootsNostrEvent;
+pub type NostrEventBuilder = radroots_nostr::prelude::RadrootsNostrEventBuilder;
+pub type SigningError = RadrootsNostrError;
+
+pub fn event_builder_from_parts(parts: WireEventParts) -> Result<NostrEventBuilder, SigningError> {
+ radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
+}
+
+pub fn sign_parts_with_identity(
+ identity: &RadrootsIdentity,
+ parts: WireEventParts,
+) -> Result<SignedNostrEvent, SigningError> {
+ let builder = event_builder_from_parts(parts)?;
+ sign_builder_with_identity(identity, builder)
+}
+
+pub fn sign_builder_with_identity(
+ identity: &RadrootsIdentity,
+ builder: NostrEventBuilder,
+) -> Result<SignedNostrEvent, SigningError> {
+ builder.sign_with_keys(identity.keys()).map_err(Into::into)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{event_builder_from_parts, sign_parts_with_identity};
+ use crate::{WireEventParts, identity::RadrootsIdentity};
+
+ #[test]
+ fn event_builder_from_parts_preserves_kind_and_content() {
+ let builder = event_builder_from_parts(WireEventParts {
+ kind: 30402,
+ content: "hello".into(),
+ tags: vec![vec!["x".into(), "y".into()]],
+ })
+ .expect("builder");
+ let identity = RadrootsIdentity::generate();
+ let event = builder.build(identity.keys().public_key());
+
+ assert_eq!(u16::from(event.kind), 30402);
+ assert_eq!(event.content, "hello");
+ }
+
+ #[test]
+ fn sign_parts_with_identity_signs_event() {
+ let identity = RadrootsIdentity::generate();
+ let event = sign_parts_with_identity(
+ &identity,
+ WireEventParts {
+ kind: 30402,
+ content: "hello".into(),
+ tags: vec![],
+ },
+ )
+ .expect("signed event");
+
+ assert_eq!(event.pubkey.to_hex(), identity.public_key_hex());
+ }
+}
diff --git a/crates/sdk/src/identity.rs b/crates/sdk/src/identity.rs
@@ -0,0 +1,33 @@
+pub use radroots_identity::{
+ DEFAULT_IDENTITY_PATH, IdentityError, RADROOTS_USERNAME_MAX_LEN, RADROOTS_USERNAME_MIN_LEN,
+ RADROOTS_USERNAME_REGEX, RadrootsIdentity, RadrootsIdentityFile, RadrootsIdentityId,
+ RadrootsIdentityProfile, RadrootsIdentityPublic, RadrootsIdentitySecretKeyFormat,
+ radroots_username_is_valid, radroots_username_normalize,
+};
+
+#[cfg(feature = "identity-storage")]
+pub use radroots_identity::{
+ RADROOTS_ENCRYPTED_IDENTITY_DEFAULT_KEY_SLOT, RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX,
+ RadrootsEncryptedIdentityFile, encrypted_identity_wrapping_key_path, load_encrypted_identity,
+ load_encrypted_identity_with_key_slot, load_identity_profile, rotate_encrypted_identity,
+ rotate_encrypted_identity_with_key_slot, store_encrypted_identity,
+ store_encrypted_identity_with_key_slot, store_identity_profile,
+};
+
+#[cfg(all(feature = "identity-models", feature = "identity-storage"))]
+#[cfg(test)]
+mod tests {
+ use super::{RadrootsEncryptedIdentityFile, RadrootsIdentity};
+
+ #[test]
+ fn encrypted_identity_file_round_trips() {
+ let temp = tempfile::tempdir().expect("tempdir");
+ let file = RadrootsEncryptedIdentityFile::new(temp.path().join("identity.enc.json"));
+ let identity = RadrootsIdentity::generate();
+
+ file.store(&identity).expect("store identity");
+ let loaded = file.load().expect("load identity");
+
+ assert_eq!(loaded.public_key_hex(), identity.public_key_hex());
+ }
+}
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -10,9 +10,17 @@ use std::{string::String, vec::Vec};
use alloc::{string::String, vec::Vec};
pub mod farm;
+#[cfg(feature = "identity-models")]
+pub mod identity;
pub mod listing;
pub mod profile;
pub mod trade;
+#[cfg(any(
+ feature = "signing",
+ feature = "relay-client",
+ feature = "signer-adapters"
+))]
+pub mod adapters;
pub use radroots_events::{
RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsNostrEventRef,