lib

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

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:
MCargo.lock | 6++++++
Mcrates/sdk/Cargo.toml | 19++++++++++++++++++-
Mcrates/sdk/README | 15+++++++++++++++
Acrates/sdk/src/adapters/mod.rs | 6++++++
Acrates/sdk/src/adapters/relay.rs | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/adapters/signer.rs | 24++++++++++++++++++++++++
Acrates/sdk/src/adapters/signing.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/identity.rs | 33+++++++++++++++++++++++++++++++++
Mcrates/sdk/src/lib.rs | 8++++++++
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,