radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

commit a78e416c96e351b93b7f88e291ad3c2c38a43f70
parent 219c6b2c95b9cbf72d90bd5a760a0df1095d9e9f
Author: triesap <triesap@radroots.dev>
Date:   Tue,  6 Jan 2026 17:19:51 +0000

nip46: add relay-only nip04 encrypt/decrypt

- handle nip04 encrypt/decrypt in relay listener
- add nip04 encrypt/decrypt flow to tmp nostr-only test
- validate plaintext and error handling
- keep existing sign_event and connect flows intact

Diffstat:
Msrc/app/runtime.rs | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/transport/nostr/listener.rs | 17+++++++++++++++++
2 files changed, 72 insertions(+), 0 deletions(-)

diff --git a/src/app/runtime.rs b/src/app/runtime.rs @@ -6,6 +6,14 @@ use crate::app::{cli, config}; use crate::core::Radrootsd; use crate::transport::jsonrpc; use crate::transport::nostr::listener::spawn_nip46_listener; +use radroots_events::profile::RadrootsProfileType; +use radroots_events_codec::profile::encode::profile_type_tags; +use radroots_nostr::prelude::{ + radroots_nostr_build_metadata_event, + radroots_nostr_publish_identity_profile_with_type, + RadrootsNostrTag, + RadrootsNostrTagKind, +}; pub async fn run() -> Result<()> { let (args, settings): (cli::Args, config::Settings) = @@ -30,6 +38,53 @@ pub async fn run() -> Result<()> { } if !settings.config.relays.is_empty() { + let client = radrootsd.client.clone(); + let md = settings.metadata.clone(); + let identity = identity.clone(); + let has_metadata = serde_json::to_value(&md) + .ok() + .and_then(|v| v.as_object().cloned()) + .map(|o| !o.is_empty()) + .unwrap_or(false); + + tokio::spawn(async move { + client.connect().await; + let profile_published = + match radroots_nostr_publish_identity_profile_with_type( + &client, + &identity, + Some(RadrootsProfileType::Radrootsd), + ) + .await + { + Ok(Some(_)) => true, + Ok(None) => false, + Err(e) => { + tracing::warn!("Failed to publish identity profile: {e}"); + false + } + }; + if has_metadata && !profile_published { + let mut tags = Vec::new(); + for mut tag in profile_type_tags(RadrootsProfileType::Radrootsd) { + if tag.is_empty() { + continue; + } + let key = tag.remove(0); + tags.push(RadrootsNostrTag::custom( + RadrootsNostrTagKind::Custom(key.into()), + tag, + )); + } + let builder = radroots_nostr_build_metadata_event(&md).tags(tags); + if let Err(e) = client.send_event_builder(builder).await { + tracing::warn!("Failed to publish metadata on startup: {e}"); + } else { + tracing::info!("Published metadata on startup"); + } + } + }); + spawn_nip46_listener(radrootsd.clone()); } diff --git a/src/transport/nostr/listener.rs b/src/transport/nostr/listener.rs @@ -1,6 +1,7 @@ use std::time::Duration; use anyhow::{anyhow, Result}; +use nostr::nips::nip04; use nostr::nips::nip44; use nostr::nips::nip46::{ NostrConnectMessage, @@ -130,6 +131,22 @@ fn handle_request(radrootsd: &Radrootsd, request: NostrConnectRequest) -> NostrC Err(err) => NostrConnectResponse::with_error(format!("sign_event failed: {err}")), } } + NostrConnectRequest::Nip04Encrypt { public_key, text } => { + match nip04::encrypt(radrootsd.keys.secret_key(), &public_key, text) { + Ok(ciphertext) => { + NostrConnectResponse::with_result(ResponseResult::Nip04Encrypt { ciphertext }) + } + Err(err) => NostrConnectResponse::with_error(format!("nip04_encrypt failed: {err}")), + } + } + NostrConnectRequest::Nip04Decrypt { public_key, ciphertext } => { + match nip04::decrypt(radrootsd.keys.secret_key(), &public_key, ciphertext) { + Ok(plaintext) => { + NostrConnectResponse::with_result(ResponseResult::Nip04Decrypt { plaintext }) + } + Err(err) => NostrConnectResponse::with_error(format!("nip04_decrypt failed: {err}")), + } + } NostrConnectRequest::Ping => NostrConnectResponse::with_result(ResponseResult::Pong), _ => NostrConnectResponse::with_error("unsupported request"), }