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:
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, ®istry)?;
connect::register(&mut m, ®istry)?;
get_public_key::register(&mut m, ®istry)?;
+ sign_event::register(&mut m, ®istry)?;
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,
+ }
+ }
}