radrootsd

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

commit 87a0f082773f2dc9b4f4295d2ef5bef9d7f16a57
parent 4e83e5953f54cbb2927dd126a60d31ed87568021
Author: triesap <triesap@radroots.dev>
Date:   Tue,  6 Jan 2026 01:20:00 +0000

nip46: add sign_event rpc

- store user pubkey in nip46 sessions
- add shared nip46 request helper
- implement nip46.sign_event rpc flow
- verify signed events before return

Diffstat:
Msrc/api/jsonrpc/methods/nip46/connect.rs | 1+
Msrc/api/jsonrpc/methods/nip46/get_public_key.rs | 80++++++++++++-------------------------------------------------------------------
Msrc/api/jsonrpc/methods/nip46/mod.rs | 2++
Asrc/api/jsonrpc/methods/nip46/sign_event.rs | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/api/jsonrpc/mod.rs | 1+
Asrc/nip46/client.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/nip46/mod.rs | 1+
Msrc/nip46/session.rs | 16++++++++++++++++
8 files changed, 224 insertions(+), 68 deletions(-)

diff --git a/src/api/jsonrpc/methods/nip46/connect.rs b/src/api/jsonrpc/methods/nip46/connect.rs @@ -105,6 +105,7 @@ async fn connect_bunker( client_keys, client_pubkey, remote_signer_pubkey, + user_pubkey: None, relays: info.relays.clone(), }; ctx.state.nip46_sessions.insert(session).await; diff --git a/src/api/jsonrpc/methods/nip46/get_public_key.rs b/src/api/jsonrpc/methods/nip46/get_public_key.rs @@ -1,24 +1,12 @@ -use std::time::Duration; - use anyhow::Result; use jsonrpsee::server::RpcModule; use serde::{Deserialize, Serialize}; -use crate::api::jsonrpc::params::DEFAULT_TIMEOUT_SECS; use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; use crate::nip46::session::Nip46Session; -use radroots_nostr::prelude::{ - radroots_nostr_filter_tag, - RadrootsNostrEventBuilder, - RadrootsNostrFilter, - RadrootsNostrKind, - RadrootsNostrPublicKey, -}; -use nostr::nips::{ - nip44, - nip46::{NostrConnectMessage, NostrConnectMethod, NostrConnectRequest, ResponseResult}, -}; -use nostr::JsonUtil; +use crate::nip46::client; +use radroots_nostr::prelude::RadrootsNostrPublicKey; +use nostr::nips::nip46::{NostrConnectMethod, NostrConnectRequest, ResponseResult}; #[derive(Debug, Deserialize)] struct Nip46GetPublicKeyParams { @@ -43,6 +31,14 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res .await .ok_or_else(|| RpcError::InvalidParams("unknown session".to_string()))?; let pubkey = request_public_key(&session).await?; + let updated = ctx + .state + .nip46_sessions + .set_user_pubkey(&session_id, pubkey.clone()) + .await; + if !updated { + return Err(RpcError::Other("nip46 session update failed".to_string())); + } Ok::<Nip46GetPublicKeyResponse, RpcError>(Nip46GetPublicKeyResponse { pubkey: pubkey.to_hex(), }) @@ -53,25 +49,8 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res async fn request_public_key( session: &Nip46Session, ) -> Result<RadrootsNostrPublicKey, RpcError> { - session.client.connect().await; - let request = NostrConnectRequest::GetPublicKey; - let message = NostrConnectMessage::request(&request); - let request_id = message.id().to_string(); - let event = RadrootsNostrEventBuilder::nostr_connect( - &session.client_keys, - session.remote_signer_pubkey.clone(), - message, - ) - .map_err(|e| RpcError::Other(format!("nip46 get_public_key failed: {e}")))?; - - session - .client - .send_event_builder(event) - .await - .map_err(|e| RpcError::Other(format!("nip46 get_public_key failed: {e}")))?; - - let response = wait_for_response(session, &request_id).await?; + let response = client::request(session, request, "get_public_key").await?; let response = response .to_response(NostrConnectMethod::GetPublicKey) .map_err(|e| RpcError::Other(format!("nip46 get_public_key failed: {e}")))?; @@ -90,38 +69,3 @@ async fn request_public_key( )), } } - -async fn wait_for_response( - session: &Nip46Session, - request_id: &str, -) -> Result<NostrConnectMessage, RpcError> { - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::NostrConnect) - .author(session.remote_signer_pubkey.clone()); - let filter = radroots_nostr_filter_tag(filter, "p", vec![session.client_pubkey.to_hex()]) - .map_err(|e| RpcError::Other(format!("nip46 get_public_key failed: {e}")))?; - - let events = session - .client - .fetch_events(filter, Duration::from_secs(DEFAULT_TIMEOUT_SECS)) - .await - .map_err(|e| RpcError::Other(format!("nip46 get_public_key failed: {e}")))?; - - for event in events { - let decrypted = nip44::decrypt( - session.client_keys.secret_key(), - &session.remote_signer_pubkey, - &event.content, - ) - .map_err(|e| RpcError::Other(format!("nip46 get_public_key failed: {e}")))?; - let message = NostrConnectMessage::from_json(&decrypted) - .map_err(|e| RpcError::Other(format!("nip46 get_public_key failed: {e}")))?; - if message.is_response() && message.id() == request_id { - return Ok(message); - } - } - - Err(RpcError::Other( - "nip46 get_public_key response not found".to_string(), - )) -} diff --git a/src/api/jsonrpc/methods/nip46/mod.rs b/src/api/jsonrpc/methods/nip46/mod.rs @@ -8,11 +8,13 @@ use crate::api::jsonrpc::{MethodRegistry, RpcContext}; pub mod status; pub mod connect; pub mod get_public_key; +pub mod sign_event; pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { let mut m = RpcModule::new(ctx); status::register(&mut m, &registry)?; connect::register(&mut m, &registry)?; get_public_key::register(&mut m, &registry)?; + sign_event::register(&mut m, &registry)?; Ok(m) } diff --git a/src/api/jsonrpc/methods/nip46/sign_event.rs b/src/api/jsonrpc/methods/nip46/sign_event.rs @@ -0,0 +1,107 @@ +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::{Deserialize, Serialize}; + +use crate::api::jsonrpc::nostr::{event_view, NostrEventView}; +use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; +use crate::nip46::client; +use crate::nip46::session::Nip46Session; +use radroots_nostr::prelude::{RadrootsNostrKind, RadrootsNostrTag, RadrootsNostrTimestamp}; +use nostr::nips::nip46::{NostrConnectMethod, NostrConnectRequest, ResponseResult}; +use nostr::UnsignedEvent; + +#[derive(Debug, Deserialize)] +struct Nip46SignEventParams { + session_id: String, + event: Nip46UnsignedEvent, +} + +#[derive(Clone, Debug, Deserialize)] +struct Nip46UnsignedEvent { + kind: u16, + content: String, + tags: Vec<Vec<String>>, + created_at: u64, +} + +#[derive(Clone, Debug, Serialize)] +struct Nip46SignEventResponse { + event: NostrEventView, +} + +pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { + registry.track("nip46.sign_event"); + m.register_async_method("nip46.sign_event", |params, ctx, _| async move { + let Nip46SignEventParams { session_id, event } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + let session = ctx + .state + .nip46_sessions + .get(&session_id) + .await + .ok_or_else(|| RpcError::InvalidParams("unknown session".to_string()))?; + let signed_event = sign_event(&session, event).await?; + Ok::<Nip46SignEventResponse, RpcError>(Nip46SignEventResponse { + event: event_view(&signed_event), + }) + })?; + Ok(()) +} + +async fn sign_event( + session: &Nip46Session, + input: Nip46UnsignedEvent, +) -> Result<nostr::Event, RpcError> { + let user_pubkey = session.user_pubkey.clone().ok_or_else(|| { + RpcError::InvalidParams("missing user pubkey; call nip46.get_public_key".to_string()) + })?; + let tags = parse_tags(input.tags)?; + let unsigned = UnsignedEvent::new( + user_pubkey, + RadrootsNostrTimestamp::from_secs(input.created_at), + RadrootsNostrKind::from_u16(input.kind), + tags, + input.content, + ); + + let request = NostrConnectRequest::SignEvent(unsigned); + let response = client::request(session, request, "sign_event").await?; + let response = response + .to_response(NostrConnectMethod::SignEvent) + .map_err(|e| RpcError::Other(format!("nip46 sign_event failed: {e}")))?; + + if let Some(error) = response.error { + return Err(RpcError::Other(format!("nip46 sign_event error: {error}"))); + } + + let event = match response.result { + Some(ResponseResult::SignEvent(event)) => *event, + Some(_) => { + return Err(RpcError::Other( + "nip46 sign_event unexpected response".to_string(), + )) + } + None => { + return Err(RpcError::Other( + "nip46 sign_event missing response".to_string(), + )) + } + }; + + event + .verify() + .map_err(|e| RpcError::Other(format!("nip46 sign_event invalid event: {e}")))?; + + Ok(event) +} + +fn parse_tags(tags: Vec<Vec<String>>) -> Result<Vec<RadrootsNostrTag>, RpcError> { + tags.into_iter() + .enumerate() + .map(|(idx, tag)| { + RadrootsNostrTag::parse(tag) + .map_err(|e| RpcError::InvalidParams(format!("invalid tag {idx}: {e}"))) + }) + .collect() +} diff --git a/src/api/jsonrpc/mod.rs b/src/api/jsonrpc/mod.rs @@ -21,6 +21,7 @@ pub mod methods; pub use context::RpcContext; pub use error::RpcError; pub use registry::MethodRegistry; +pub(crate) use params::DEFAULT_TIMEOUT_SECS; pub async fn start_rpc( state: Radrootsd, diff --git a/src/nip46/client.rs b/src/nip46/client.rs @@ -0,0 +1,84 @@ +#![forbid(unsafe_code)] + +use std::time::Duration; + +use crate::api::jsonrpc::{DEFAULT_TIMEOUT_SECS, RpcError}; +use crate::nip46::session::Nip46Session; +use radroots_nostr::prelude::{ + radroots_nostr_filter_tag, + RadrootsNostrEventBuilder, + RadrootsNostrFilter, + RadrootsNostrKind, +}; +use nostr::nips::{nip44, nip46::NostrConnectMessage, nip46::NostrConnectRequest}; +use nostr::JsonUtil; + +pub async fn request( + session: &Nip46Session, + request: NostrConnectRequest, + label: &str, +) -> Result<NostrConnectMessage, RpcError> { + let request_id = send_request(session, request, label).await?; + wait_for_response(session, &request_id, label).await +} + +async fn send_request( + session: &Nip46Session, + request: NostrConnectRequest, + label: &str, +) -> Result<String, RpcError> { + session.client.connect().await; + + let message = NostrConnectMessage::request(&request); + let request_id = message.id().to_string(); + let event = RadrootsNostrEventBuilder::nostr_connect( + &session.client_keys, + session.remote_signer_pubkey.clone(), + message, + ) + .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?; + + session + .client + .send_event_builder(event) + .await + .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?; + + Ok(request_id) +} + +async fn wait_for_response( + session: &Nip46Session, + request_id: &str, + label: &str, +) -> Result<NostrConnectMessage, RpcError> { + let filter = RadrootsNostrFilter::new() + .kind(RadrootsNostrKind::NostrConnect) + .author(session.remote_signer_pubkey.clone()); + let filter = radroots_nostr_filter_tag(filter, "p", vec![session.client_pubkey.to_hex()]) + .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?; + + let events = session + .client + .fetch_events(filter, Duration::from_secs(DEFAULT_TIMEOUT_SECS)) + .await + .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?; + + for event in events { + let decrypted = nip44::decrypt( + session.client_keys.secret_key(), + &session.remote_signer_pubkey, + &event.content, + ) + .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?; + let message = NostrConnectMessage::from_json(&decrypted) + .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?; + if message.is_response() && message.id() == request_id { + return Ok(message); + } + } + + Err(RpcError::Other(format!( + "nip46 {label} response not found" + ))) +} diff --git a/src/nip46/mod.rs b/src/nip46/mod.rs @@ -1,4 +1,5 @@ #![forbid(unsafe_code)] pub mod connection; +pub mod client; pub mod session; diff --git a/src/nip46/session.rs b/src/nip46/session.rs @@ -23,6 +23,7 @@ pub struct Nip46Session { pub client_keys: RadrootsNostrKeys, pub client_pubkey: RadrootsNostrPublicKey, pub remote_signer_pubkey: RadrootsNostrPublicKey, + pub user_pubkey: Option<RadrootsNostrPublicKey>, pub relays: Vec<String>, } @@ -42,4 +43,19 @@ impl Nip46SessionStore { let sessions = self.inner.lock().await; sessions.get(session_id).cloned() } + + pub async fn set_user_pubkey( + &self, + session_id: &str, + pubkey: RadrootsNostrPublicKey, + ) -> bool { + let mut sessions = self.inner.lock().await; + match sessions.get_mut(session_id) { + Some(session) => { + session.user_pubkey = Some(pubkey); + true + } + None => false, + } + } }