commit 31216709a03aa95680e4af582d35184a61a5fc52
parent cc9a808ffe1f28fa6ff8ff601ed165b42ef661d3
Author: triesap <triesap@radroots.dev>
Date: Tue, 6 Jan 2026 15:52:15 +0000
nostr: add nip46 listener scaffold
- add transport module and nostr listener entrypoint
- subscribe to nip46 events addressed to service pubkey
- start listener alongside relay connection when relays configured
- log incoming nip46 request events without handling
Diffstat:
50 files changed, 104 insertions(+), 2415 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -20,17 +20,9 @@ Thumbs.db
*.pem
# local
-.tmp*
-.archive*
.local*
.vscode
notes*.txt
-notes*.md
-notes*.json
-tree*.txt
-git-diff*.txt
-prompt*.txt
-tree*.txt
justfile
# dev
diff --git a/src/api/jsonrpc/context.rs b/src/api/jsonrpc/context.rs
@@ -1,17 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::radrootsd::Radrootsd;
-
-use super::registry::MethodRegistry;
-
-#[derive(Clone)]
-pub struct RpcContext {
- pub state: Radrootsd,
- pub methods: MethodRegistry,
-}
-
-impl RpcContext {
- pub fn new(state: Radrootsd, methods: MethodRegistry) -> Self {
- Self { state, methods }
- }
-}
diff --git a/src/api/jsonrpc/error.rs b/src/api/jsonrpc/error.rs
@@ -1,30 +0,0 @@
-#![forbid(unsafe_code)]
-
-use jsonrpsee::types::{ErrorObject, ErrorObjectOwned};
-use thiserror::Error;
-
-#[derive(Debug, Error)]
-pub enum RpcError {
- #[error("failed to add relay {0}: {1}")]
- AddRelay(String, String),
- #[error("no relays configured; call relays.add first")]
- NoRelays,
- #[error("invalid params: {0}")]
- InvalidParams(String),
- #[error("method not found: {0}")]
- MethodNotFound(String),
- #[error("{0}")]
- Other(String),
-}
-
-impl From<RpcError> for ErrorObjectOwned {
- fn from(err: RpcError) -> Self {
- match err {
- RpcError::InvalidParams(msg) => ErrorObject::owned(-32602, msg, None::<()>),
- RpcError::MethodNotFound(name) => {
- ErrorObject::owned(-32601, format!("method not found: {name}"), None::<()>)
- }
- other => ErrorObject::owned(-32000, other.to_string(), None::<()>),
- }
- }
-}
diff --git a/src/api/jsonrpc/methods/events/mod.rs b/src/api/jsonrpc/methods/events/mod.rs
@@ -1,14 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod profile;
-
-pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> {
- let mut m = RpcModule::new(ctx);
- profile::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/profile.rs b/src/api/jsonrpc/methods/events/profile.rs
@@ -1,134 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-use serde_json::{Map, Value};
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use crate::nip46::client as nip46_client;
-use radroots_events::profile::{RadrootsProfile, RadrootsProfileType};
-use radroots_events_codec::profile::encode::to_wire_parts_with_profile_type;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishProfileParams {
- profile: RadrootsProfile,
- profile_type: RadrootsProfileType,
- session_id: Option<String>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.profile.publish");
- m.register_async_method("events.profile.publish", |params, ctx, _| async move {
- let PublishProfileParams {
- profile,
- profile_type,
- session_id,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let parts = to_wire_parts_with_profile_type(&profile, Some(profile_type))
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- let content = canonicalize_json_string(&parts.content)?;
- let builder = radroots_nostr_build_event(parts.kind, content, parts.tags)
- .map_err(|e| RpcError::Other(format!("failed to build profile event: {e}")))?;
-
- let response = match session_id {
- Some(session_id) => publish_with_session(ctx.as_ref().clone(), session_id, builder).await?,
- None => publish_with_runtime(ctx.as_ref().clone(), builder).await?,
- };
-
- Ok::<PublishResponse, RpcError>(response)
- })?;
-
- Ok(())
-}
-
-async fn publish_with_runtime(
- ctx: RpcContext,
- builder: radroots_nostr::prelude::RadrootsNostrEventBuilder,
-) -> Result<PublishResponse, RpcError> {
- let relays = ctx.state.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish metadata: {e}")))?;
-
- Ok(publish_response(output))
-}
-
-async fn publish_with_session(
- ctx: RpcContext,
- session_id: String,
- builder: radroots_nostr::prelude::RadrootsNostrEventBuilder,
-) -> Result<PublishResponse, RpcError> {
- let session = ctx
- .state
- .nip46_sessions
- .get(&session_id)
- .await
- .ok_or_else(|| RpcError::InvalidParams("unknown session".to_string()))?;
- if session.relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
- let user_pubkey = session.user_pubkey.clone().ok_or_else(|| {
- RpcError::InvalidParams("missing user pubkey; call nip46.get_public_key".to_string())
- })?;
- let unsigned = builder.build(user_pubkey);
- let signed = nip46_client::sign_event(&session, unsigned, "profile.publish").await?;
- let output = session
- .client
- .send_event(&signed)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish metadata: {e}")))?;
-
- Ok(publish_response(output))
-}
-
-fn canonicalize_json_string(content: &str) -> Result<String, RpcError> {
- let value: Value = serde_json::from_str(content)
- .map_err(|e| RpcError::InvalidParams(format!("invalid metadata json: {e}")))?;
- let canonical = canonicalize_value(value);
- serde_json::to_string(&canonical)
- .map_err(|e| RpcError::Other(format!("canonical json failed: {e}")))
-}
-
-fn canonicalize_value(value: Value) -> Value {
- match value {
- Value::Object(map) => canonicalize_object(map),
- Value::Array(values) => {
- let values = values
- .into_iter()
- .map(canonicalize_value)
- .collect::<Vec<_>>();
- Value::Array(values)
- }
- other => other,
- }
-}
-
-fn canonicalize_object(map: Map<String, Value>) -> Value {
- let mut entries = map.into_iter().collect::<Vec<_>>();
- entries.sort_by(|a, b| a.0.cmp(&b.0));
- let mut ordered = Map::new();
- for (key, value) in entries {
- ordered.insert(key, canonicalize_value(value));
- }
- Value::Object(ordered)
-}
-
-#[cfg(test)]
-mod tests {
- use super::canonicalize_json_string;
-
- #[test]
- fn canonicalize_json_string_orders_keys() {
- let input = r#"{"b":1,"a":{"d":2,"c":3}}"#;
- let canonical = canonicalize_json_string(input).expect("canonical");
- assert_eq!(canonical, r#"{"a":{"c":3,"d":2},"b":1}"#);
- }
-}
diff --git a/src/api/jsonrpc/methods/mod.rs b/src/api/jsonrpc/methods/mod.rs
@@ -1,21 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use super::{context::RpcContext, registry::MethodRegistry};
-
-pub mod nip46;
-pub mod relays;
-pub mod events;
-
-pub fn register_all(
- root: &mut RpcModule<RpcContext>,
- ctx: RpcContext,
- registry: MethodRegistry,
-) -> Result<()> {
- root.merge(relays::module(ctx.clone(), registry.clone())?)?;
- root.merge(nip46::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::module(ctx, registry)?)?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/nip46/connect.rs b/src/api/jsonrpc/methods/nip46/connect.rs
@@ -1,297 +0,0 @@
-use std::time::Duration;
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-use tokio::sync::broadcast;
-use tokio::time::sleep;
-use uuid::Uuid;
-
-use crate::api::jsonrpc::params::DEFAULT_TIMEOUT_SECS;
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use crate::nip46::connection::{parse_connect_url, Nip46ConnectInfo, Nip46ConnectMode};
-use crate::nip46::session::Nip46Session;
-use radroots_nostr::prelude::{
- radroots_nostr_filter_tag,
- radroots_nostr_parse_pubkey,
- RadrootsNostrClient,
- RadrootsNostrEventBuilder,
- RadrootsNostrFilter,
- RadrootsNostrKind,
- RadrootsNostrKeys,
- RadrootsNostrPublicKey,
- RadrootsNostrRelayPoolNotification,
- RadrootsNostrTimestamp,
-};
-use nostr::nips::{nip44, nip46::NostrConnectMessage, nip46::NostrConnectRequest};
-use nostr::JsonUtil;
-
-#[derive(Debug, Deserialize)]
-struct Nip46ConnectParams {
- url: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct Nip46ConnectResponse {
- session_id: String,
- mode: Nip46ConnectMode,
- remote_signer_pubkey: String,
- client_pubkey: String,
- relays: Vec<String>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("nip46.connect");
- m.register_async_method("nip46.connect", |params, ctx, _| async move {
- let Nip46ConnectParams { url } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- let response = connect_nip46(ctx.as_ref().clone(), url).await?;
- Ok::<Nip46ConnectResponse, RpcError>(response)
- })?;
- Ok(())
-}
-
-async fn connect_nip46(ctx: RpcContext, url: String) -> Result<Nip46ConnectResponse, RpcError> {
- let info = parse_connect_url(&url)?;
- match info.mode {
- Nip46ConnectMode::Bunker => connect_bunker(ctx, info).await,
- Nip46ConnectMode::Nostrconnect => Err(RpcError::InvalidParams(
- "nostrconnect mode not supported yet".to_string(),
- )),
- }
-}
-
-async fn connect_bunker(
- ctx: RpcContext,
- info: Nip46ConnectInfo,
-) -> Result<Nip46ConnectResponse, RpcError> {
- if info.relays.is_empty() {
- return Err(RpcError::InvalidParams("missing relay".to_string()));
- }
-
- let remote_signer_raw = info.remote_signer_pubkey.as_ref().ok_or_else(|| {
- RpcError::InvalidParams("missing remote signer pubkey".to_string())
- })?;
- let remote_signer_pubkey = radroots_nostr_parse_pubkey(remote_signer_raw)
- .map_err(|e| RpcError::InvalidParams(format!("invalid remote signer: {e}")))?;
-
- let client_keys = RadrootsNostrKeys::generate();
- let client_pubkey = client_keys.public_key();
- let client = RadrootsNostrClient::new(client_keys.clone());
-
- add_relays(&client, &info.relays).await?;
- client.connect().await;
- client
- .wait_for_connection(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
- .await;
-
- let request_id = send_connect_request(
- &client,
- &client_keys,
- &remote_signer_pubkey,
- info.secret.as_deref(),
- )
- .await?;
-
- let response = wait_for_connect_response(
- &client,
- &client_keys,
- &remote_signer_pubkey,
- &client_pubkey,
- &request_id,
- )
- .await?;
-
- validate_connect_response(&response, info.secret.as_deref())?;
-
- let session_id = Uuid::new_v4().to_string();
- let session = Nip46Session {
- id: session_id.clone(),
- client,
- client_keys,
- client_pubkey,
- remote_signer_pubkey,
- user_pubkey: None,
- relays: info.relays.clone(),
- };
- ctx.state.nip46_sessions.insert(session).await;
-
- Ok(Nip46ConnectResponse {
- session_id,
- mode: info.mode,
- remote_signer_pubkey: remote_signer_raw.to_string(),
- client_pubkey: client_pubkey.to_hex(),
- relays: info.relays,
- })
-}
-
-async fn add_relays(
- client: &RadrootsNostrClient,
- relays: &[String],
-) -> Result<(), RpcError> {
- for relay in relays {
- client
- .add_relay(relay)
- .await
- .map_err(|e| RpcError::AddRelay(relay.to_string(), e.to_string()))?;
- }
- Ok(())
-}
-
-async fn send_connect_request(
- client: &RadrootsNostrClient,
- client_keys: &RadrootsNostrKeys,
- remote_signer_pubkey: &RadrootsNostrPublicKey,
- secret: Option<&str>,
-) -> Result<String, RpcError> {
- let request = NostrConnectRequest::Connect {
- remote_signer_public_key: remote_signer_pubkey.clone(),
- secret: secret.map(str::to_string),
- };
- let message = NostrConnectMessage::request(&request);
- let request_id = message.id().to_string();
- let event = RadrootsNostrEventBuilder::nostr_connect(
- client_keys,
- remote_signer_pubkey.clone(),
- message,
- )
- .map_err(|e| RpcError::Other(format!("nip46 connect request failed: {e}")))?;
- client
- .send_event_builder(event)
- .await
- .map_err(|e| RpcError::Other(format!("nip46 connect request failed: {e}")))?;
- Ok(request_id)
-}
-
-async fn wait_for_connect_response(
- client: &RadrootsNostrClient,
- client_keys: &RadrootsNostrKeys,
- remote_signer_pubkey: &RadrootsNostrPublicKey,
- client_pubkey: &RadrootsNostrPublicKey,
- request_id: &str,
-) -> Result<NostrConnectMessage, RpcError> {
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::NostrConnect)
- .author(remote_signer_pubkey.clone())
- .since(RadrootsNostrTimestamp::now());
- let filter = radroots_nostr_filter_tag(filter, "p", vec![client_pubkey.to_hex()])
- .map_err(|e| RpcError::Other(format!("nip46 connect filter failed: {e}")))?;
- let mut notifications = client.notifications();
- let subscription = client
- .subscribe(filter, None)
- .await
- .map_err(|e| RpcError::Other(format!("nip46 connect failed: {e}")))?;
- let timeout = sleep(Duration::from_secs(DEFAULT_TIMEOUT_SECS));
- tokio::pin!(timeout);
-
- loop {
- tokio::select! {
- _ = &mut timeout => {
- client.unsubscribe(&subscription.val).await;
- return Err(RpcError::Other("nip46 connect response not found".to_string()));
- }
- msg = notifications.recv() => {
- let notification = match msg {
- Ok(notification) => notification,
- Err(broadcast::error::RecvError::Lagged(_)) => continue,
- Err(broadcast::error::RecvError::Closed) => {
- return Err(RpcError::Other("nip46 connect notification closed".to_string()));
- }
- };
- let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else {
- continue;
- };
- let event = (*event).clone();
- if event.kind != RadrootsNostrKind::NostrConnect || &event.pubkey != remote_signer_pubkey {
- continue;
- }
- let decrypted = nip44::decrypt(
- client_keys.secret_key(),
- remote_signer_pubkey,
- &event.content,
- )
- .map_err(|e| RpcError::Other(format!("nip46 connect decrypt failed: {e}")))?;
- let message = NostrConnectMessage::from_json(&decrypted)
- .map_err(|e| RpcError::Other(format!("nip46 connect response parse failed: {e}")))?;
- if message.is_response() && message.id() == request_id {
- client.unsubscribe(&subscription.val).await;
- return Ok(message);
- }
- }
- }
- }
-}
-
-fn validate_connect_response(
- response: &NostrConnectMessage,
- secret: Option<&str>,
-) -> Result<(), RpcError> {
- let (result, error) = match response {
- NostrConnectMessage::Response { result, error, .. } => (result, error),
- _ => {
- return Err(RpcError::Other(
- "nip46 connect response invalid".to_string(),
- ))
- }
- };
-
- if let Some(error) = error {
- return Err(RpcError::Other(format!("nip46 connect error: {error}")));
- }
-
- let result = result
- .as_deref()
- .ok_or_else(|| RpcError::Other("nip46 connect missing result".to_string()))?;
-
- if result == "ack" {
- return Ok(());
- }
-
- if secret.is_some_and(|expected| expected == result) {
- return Ok(());
- }
-
- Err(RpcError::Other(format!(
- "nip46 connect unexpected result: {result}"
- )))
-}
-
-#[cfg(test)]
-mod tests {
- use super::validate_connect_response;
- use nostr::nips::nip46::{NostrConnectMessage, NostrConnectResponse, ResponseResult};
-
- #[test]
- fn validate_connect_response_accepts_ack() {
- let message = NostrConnectMessage::response(
- "1",
- NostrConnectResponse::with_result(ResponseResult::Ack),
- );
- validate_connect_response(&message, None).expect("ack");
- }
-
- #[test]
- fn validate_connect_response_accepts_secret_match() {
- let message = NostrConnectMessage::Response {
- id: "1".to_string(),
- result: Some("secret".to_string()),
- error: None,
- };
- validate_connect_response(&message, Some("secret")).expect("secret");
-
- let err = validate_connect_response(&message, Some("other")).expect_err("mismatch");
- let msg = format!("{err}");
- assert!(msg.contains("unexpected result"));
- }
-
- #[test]
- #[ignore = "auth_url handling not implemented"]
- fn validate_connect_response_accepts_auth_url() {
- let message = NostrConnectMessage::response(
- "1",
- NostrConnectResponse::with_result(ResponseResult::AuthUrl),
- );
- validate_connect_response(&message, None).expect("auth_url");
- }
-}
diff --git a/src/api/jsonrpc/methods/nip46/get_public_key.rs b/src/api/jsonrpc/methods/nip46/get_public_key.rs
@@ -1,71 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use crate::nip46::session::Nip46Session;
-use crate::nip46::client;
-use radroots_nostr::prelude::RadrootsNostrPublicKey;
-use nostr::nips::nip46::{NostrConnectMethod, NostrConnectRequest, ResponseResult};
-
-#[derive(Debug, Deserialize)]
-struct Nip46GetPublicKeyParams {
- session_id: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct Nip46GetPublicKeyResponse {
- pubkey: String,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("nip46.get_public_key");
- m.register_async_method("nip46.get_public_key", |params, ctx, _| async move {
- let Nip46GetPublicKeyParams { session_id } = 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 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(),
- })
- })?;
- Ok(())
-}
-
-async fn request_public_key(
- session: &Nip46Session,
-) -> Result<RadrootsNostrPublicKey, RpcError> {
- let request = NostrConnectRequest::GetPublicKey;
- 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}")))?;
-
- if let Some(error) = response.error {
- return Err(RpcError::Other(format!("nip46 get_public_key error: {error}")));
- }
-
- match response.result {
- Some(ResponseResult::GetPublicKey(pubkey)) => Ok(pubkey),
- Some(_) => Err(RpcError::Other(
- "nip46 get_public_key unexpected response".to_string(),
- )),
- None => Err(RpcError::Other(
- "nip46 get_public_key missing response".to_string(),
- )),
- }
-}
diff --git a/src/api/jsonrpc/methods/nip46/mod.rs b/src/api/jsonrpc/methods/nip46/mod.rs
@@ -1,34 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod status;
-pub mod connect;
-pub mod ping;
-pub mod nip04_encrypt;
-pub mod nip04_decrypt;
-pub mod nip44_encrypt;
-pub mod nip44_decrypt;
-pub mod get_public_key;
-pub mod sign_event;
-pub mod session_status;
-pub mod session_close;
-
-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)?;
- ping::register(&mut m, ®istry)?;
- nip04_encrypt::register(&mut m, ®istry)?;
- nip04_decrypt::register(&mut m, ®istry)?;
- nip44_encrypt::register(&mut m, ®istry)?;
- nip44_decrypt::register(&mut m, ®istry)?;
- get_public_key::register(&mut m, ®istry)?;
- sign_event::register(&mut m, ®istry)?;
- session_status::register(&mut m, ®istry)?;
- session_close::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/nip46/nip04_decrypt.rs b/src/api/jsonrpc/methods/nip46/nip04_decrypt.rs
@@ -1,76 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use crate::nip46::client;
-use crate::nip46::session::Nip46Session;
-use radroots_nostr::prelude::radroots_nostr_parse_pubkey;
-use nostr::nips::nip46::{NostrConnectMethod, NostrConnectRequest, ResponseResult};
-
-#[derive(Debug, Deserialize)]
-struct Nip46Nip04DecryptParams {
- session_id: String,
- pubkey: String,
- ciphertext: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct Nip46Nip04DecryptResponse {
- plaintext: String,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("nip46.nip04_decrypt");
- m.register_async_method("nip46.nip04_decrypt", |params, ctx, _| async move {
- let Nip46Nip04DecryptParams {
- session_id,
- pubkey,
- ciphertext,
- } = 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 plaintext = request_nip04_decrypt(&session, pubkey, ciphertext).await?;
- Ok::<Nip46Nip04DecryptResponse, RpcError>(Nip46Nip04DecryptResponse { plaintext })
- })?;
- Ok(())
-}
-
-async fn request_nip04_decrypt(
- session: &Nip46Session,
- pubkey: String,
- ciphertext: String,
-) -> Result<String, RpcError> {
- let public_key = radroots_nostr_parse_pubkey(&pubkey)
- .map_err(|e| RpcError::InvalidParams(format!("invalid pubkey: {e}")))?;
- let request = NostrConnectRequest::Nip04Decrypt {
- public_key,
- ciphertext,
- };
- let response = client::request(session, request, "nip04_decrypt").await?;
- let response = response
- .to_response(NostrConnectMethod::Nip04Decrypt)
- .map_err(|e| RpcError::Other(format!("nip46 nip04_decrypt failed: {e}")))?;
-
- if let Some(error) = response.error {
- return Err(RpcError::Other(format!(
- "nip46 nip04_decrypt error: {error}"
- )));
- }
-
- match response.result {
- Some(ResponseResult::Nip04Decrypt { plaintext }) => Ok(plaintext),
- Some(_) => Err(RpcError::Other(
- "nip46 nip04_decrypt unexpected response".to_string(),
- )),
- None => Err(RpcError::Other(
- "nip46 nip04_decrypt missing response".to_string(),
- )),
- }
-}
diff --git a/src/api/jsonrpc/methods/nip46/nip04_encrypt.rs b/src/api/jsonrpc/methods/nip46/nip04_encrypt.rs
@@ -1,73 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use crate::nip46::client;
-use crate::nip46::session::Nip46Session;
-use radroots_nostr::prelude::radroots_nostr_parse_pubkey;
-use nostr::nips::nip46::{NostrConnectMethod, NostrConnectRequest, ResponseResult};
-
-#[derive(Debug, Deserialize)]
-struct Nip46Nip04EncryptParams {
- session_id: String,
- pubkey: String,
- text: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct Nip46Nip04EncryptResponse {
- ciphertext: String,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("nip46.nip04_encrypt");
- m.register_async_method("nip46.nip04_encrypt", |params, ctx, _| async move {
- let Nip46Nip04EncryptParams {
- session_id,
- pubkey,
- text,
- } = 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 ciphertext = request_nip04_encrypt(&session, pubkey, text).await?;
- Ok::<Nip46Nip04EncryptResponse, RpcError>(Nip46Nip04EncryptResponse { ciphertext })
- })?;
- Ok(())
-}
-
-async fn request_nip04_encrypt(
- session: &Nip46Session,
- pubkey: String,
- text: String,
-) -> Result<String, RpcError> {
- let public_key = radroots_nostr_parse_pubkey(&pubkey)
- .map_err(|e| RpcError::InvalidParams(format!("invalid pubkey: {e}")))?;
- let request = NostrConnectRequest::Nip04Encrypt { public_key, text };
- let response = client::request(session, request, "nip04_encrypt").await?;
- let response = response
- .to_response(NostrConnectMethod::Nip04Encrypt)
- .map_err(|e| RpcError::Other(format!("nip46 nip04_encrypt failed: {e}")))?;
-
- if let Some(error) = response.error {
- return Err(RpcError::Other(format!(
- "nip46 nip04_encrypt error: {error}"
- )));
- }
-
- match response.result {
- Some(ResponseResult::Nip04Encrypt { ciphertext }) => Ok(ciphertext),
- Some(_) => Err(RpcError::Other(
- "nip46 nip04_encrypt unexpected response".to_string(),
- )),
- None => Err(RpcError::Other(
- "nip46 nip04_encrypt missing response".to_string(),
- )),
- }
-}
diff --git a/src/api/jsonrpc/methods/nip46/nip44_decrypt.rs b/src/api/jsonrpc/methods/nip46/nip44_decrypt.rs
@@ -1,76 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use crate::nip46::client;
-use crate::nip46::session::Nip46Session;
-use radroots_nostr::prelude::radroots_nostr_parse_pubkey;
-use nostr::nips::nip46::{NostrConnectMethod, NostrConnectRequest, ResponseResult};
-
-#[derive(Debug, Deserialize)]
-struct Nip46Nip44DecryptParams {
- session_id: String,
- pubkey: String,
- ciphertext: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct Nip46Nip44DecryptResponse {
- plaintext: String,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("nip46.nip44_decrypt");
- m.register_async_method("nip46.nip44_decrypt", |params, ctx, _| async move {
- let Nip46Nip44DecryptParams {
- session_id,
- pubkey,
- ciphertext,
- } = 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 plaintext = request_nip44_decrypt(&session, pubkey, ciphertext).await?;
- Ok::<Nip46Nip44DecryptResponse, RpcError>(Nip46Nip44DecryptResponse { plaintext })
- })?;
- Ok(())
-}
-
-async fn request_nip44_decrypt(
- session: &Nip46Session,
- pubkey: String,
- ciphertext: String,
-) -> Result<String, RpcError> {
- let public_key = radroots_nostr_parse_pubkey(&pubkey)
- .map_err(|e| RpcError::InvalidParams(format!("invalid pubkey: {e}")))?;
- let request = NostrConnectRequest::Nip44Decrypt {
- public_key,
- ciphertext,
- };
- let response = client::request(session, request, "nip44_decrypt").await?;
- let response = response
- .to_response(NostrConnectMethod::Nip44Decrypt)
- .map_err(|e| RpcError::Other(format!("nip46 nip44_decrypt failed: {e}")))?;
-
- if let Some(error) = response.error {
- return Err(RpcError::Other(format!(
- "nip46 nip44_decrypt error: {error}"
- )));
- }
-
- match response.result {
- Some(ResponseResult::Nip44Decrypt { plaintext }) => Ok(plaintext),
- Some(_) => Err(RpcError::Other(
- "nip46 nip44_decrypt unexpected response".to_string(),
- )),
- None => Err(RpcError::Other(
- "nip46 nip44_decrypt missing response".to_string(),
- )),
- }
-}
diff --git a/src/api/jsonrpc/methods/nip46/nip44_encrypt.rs b/src/api/jsonrpc/methods/nip46/nip44_encrypt.rs
@@ -1,73 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use crate::nip46::client;
-use crate::nip46::session::Nip46Session;
-use radroots_nostr::prelude::radroots_nostr_parse_pubkey;
-use nostr::nips::nip46::{NostrConnectMethod, NostrConnectRequest, ResponseResult};
-
-#[derive(Debug, Deserialize)]
-struct Nip46Nip44EncryptParams {
- session_id: String,
- pubkey: String,
- text: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct Nip46Nip44EncryptResponse {
- ciphertext: String,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("nip46.nip44_encrypt");
- m.register_async_method("nip46.nip44_encrypt", |params, ctx, _| async move {
- let Nip46Nip44EncryptParams {
- session_id,
- pubkey,
- text,
- } = 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 ciphertext = request_nip44_encrypt(&session, pubkey, text).await?;
- Ok::<Nip46Nip44EncryptResponse, RpcError>(Nip46Nip44EncryptResponse { ciphertext })
- })?;
- Ok(())
-}
-
-async fn request_nip44_encrypt(
- session: &Nip46Session,
- pubkey: String,
- text: String,
-) -> Result<String, RpcError> {
- let public_key = radroots_nostr_parse_pubkey(&pubkey)
- .map_err(|e| RpcError::InvalidParams(format!("invalid pubkey: {e}")))?;
- let request = NostrConnectRequest::Nip44Encrypt { public_key, text };
- let response = client::request(session, request, "nip44_encrypt").await?;
- let response = response
- .to_response(NostrConnectMethod::Nip44Encrypt)
- .map_err(|e| RpcError::Other(format!("nip46 nip44_encrypt failed: {e}")))?;
-
- if let Some(error) = response.error {
- return Err(RpcError::Other(format!(
- "nip46 nip44_encrypt error: {error}"
- )));
- }
-
- match response.result {
- Some(ResponseResult::Nip44Encrypt { ciphertext }) => Ok(ciphertext),
- Some(_) => Err(RpcError::Other(
- "nip46 nip44_encrypt unexpected response".to_string(),
- )),
- None => Err(RpcError::Other(
- "nip46 nip44_encrypt missing response".to_string(),
- )),
- }
-}
diff --git a/src/api/jsonrpc/methods/nip46/ping.rs b/src/api/jsonrpc/methods/nip46/ping.rs
@@ -1,58 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use crate::nip46::client;
-use crate::nip46::session::Nip46Session;
-use nostr::nips::nip46::{NostrConnectMethod, NostrConnectRequest, ResponseResult};
-
-#[derive(Debug, Deserialize)]
-struct Nip46PingParams {
- session_id: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct Nip46PingResponse {
- result: String,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("nip46.ping");
- m.register_async_method("nip46.ping", |params, ctx, _| async move {
- let Nip46PingParams { session_id } = 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 response = request_ping(&session).await?;
- Ok::<Nip46PingResponse, RpcError>(Nip46PingResponse {
- result: response,
- })
- })?;
- Ok(())
-}
-
-async fn request_ping(session: &Nip46Session) -> Result<String, RpcError> {
- let request = NostrConnectRequest::Ping;
- let response = client::request(session, request, "ping").await?;
- let response = response
- .to_response(NostrConnectMethod::Ping)
- .map_err(|e| RpcError::Other(format!("nip46 ping failed: {e}")))?;
-
- if let Some(error) = response.error {
- return Err(RpcError::Other(format!("nip46 ping error: {error}")));
- }
-
- match response.result {
- Some(ResponseResult::Pong) => Ok("pong".to_string()),
- Some(_) => Err(RpcError::Other(
- "nip46 ping unexpected response".to_string(),
- )),
- None => Err(RpcError::Other("nip46 ping missing response".to_string())),
- }
-}
diff --git a/src/api/jsonrpc/methods/nip46/session_close.rs b/src/api/jsonrpc/methods/nip46/session_close.rs
@@ -1,34 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-
-#[derive(Debug, Deserialize)]
-struct Nip46SessionCloseParams {
- session_id: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct Nip46SessionCloseResponse {
- session_id: String,
- closed: bool,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("nip46.session.close");
- m.register_async_method("nip46.session.close", |params, ctx, _| async move {
- let Nip46SessionCloseParams { session_id } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- let closed = ctx.state.nip46_sessions.remove(&session_id).await;
- if !closed {
- return Err(RpcError::InvalidParams("unknown session".to_string()));
- }
- Ok::<Nip46SessionCloseResponse, RpcError>(Nip46SessionCloseResponse {
- session_id,
- closed,
- })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/nip46/session_status.rs b/src/api/jsonrpc/methods/nip46/session_status.rs
@@ -1,42 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-
-#[derive(Debug, Deserialize)]
-struct Nip46SessionStatusParams {
- session_id: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct Nip46SessionStatusResponse {
- session_id: String,
- client_pubkey: String,
- remote_signer_pubkey: String,
- user_pubkey: Option<String>,
- relays: Vec<String>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("nip46.session.status");
- m.register_async_method("nip46.session.status", |params, ctx, _| async move {
- let Nip46SessionStatusParams { session_id } = 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()))?;
- Ok::<Nip46SessionStatusResponse, RpcError>(Nip46SessionStatusResponse {
- session_id,
- client_pubkey: session.client_pubkey.to_hex(),
- remote_signer_pubkey: session.remote_signer_pubkey.to_hex(),
- user_pubkey: session.user_pubkey.map(|pubkey| pubkey.to_hex()),
- relays: session.relays,
- })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/nip46/sign_event.rs b/src/api/jsonrpc/methods/nip46/sign_event.rs
@@ -1,78 +0,0 @@
-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::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,
- );
-
- client::sign_event(session, unsigned, "sign_event").await
-}
-
-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/methods/nip46/status.rs b/src/api/jsonrpc/methods/nip46/status.rs
@@ -1,18 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-
-#[derive(Clone, Debug, Serialize)]
-struct Nip46StatusResponse {
- ready: bool,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("nip46.status");
- m.register_method("nip46.status", |_p, _ctx, _| {
- Ok::<Nip46StatusResponse, RpcError>(Nip46StatusResponse { ready: true })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/relays/add.rs b/src/api/jsonrpc/methods/relays/add.rs
@@ -1,28 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use radroots_nostr::prelude::radroots_nostr_add_relay;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::relays::RelayAddedResponse;
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-
-#[derive(Debug, Deserialize)]
-struct AddParams {
- url: String,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("relays.add");
- m.register_async_method("relays.add", |params, ctx, _| async move {
- let AddParams { url } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- radroots_nostr_add_relay(&ctx.state.client, &url)
- .await
- .map_err(|e| RpcError::AddRelay(url.clone(), e.to_string()))?;
-
- Ok::<RelayAddedResponse, RpcError>(RelayAddedResponse { added: url })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/relays/connect.rs b/src/api/jsonrpc/methods/relays/connect.rs
@@ -1,43 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::relays::RelayConnectResponse;
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-
-use radroots_nostr::prelude::{radroots_nostr_connect, RadrootsNostrRelayStatus};
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("relays.connect");
- m.register_async_method("relays.connect", |_p, ctx, _| async move {
- let relays = ctx.state.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let mut connected = 0usize;
- let mut connecting = 0usize;
- let mut disconnected = 0usize;
-
- for (_, r) in &relays {
- match r.status() {
- RadrootsNostrRelayStatus::Connected => connected += 1,
- RadrootsNostrRelayStatus::Connecting => connecting += 1,
- _ => disconnected += 1,
- }
- }
-
- let need_connect = disconnected > 0;
- if need_connect {
- let client = ctx.state.client.clone();
- tokio::spawn(async move { radroots_nostr_connect(&client).await });
- }
-
- Ok::<RelayConnectResponse, RpcError>(RelayConnectResponse {
- connected,
- connecting,
- disconnected,
- spawned_connect: need_connect,
- })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/relays/list.rs b/src/api/jsonrpc/methods/relays/list.rs
@@ -1,14 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("relays.list");
- m.register_async_method("relays.list", |_p, ctx, _| async move {
- let relays = ctx.state.client.relays().await;
- let out = relays.keys().map(|u| u.to_string()).collect::<Vec<_>>();
- Ok::<Vec<String>, RpcError>(out)
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/relays/mod.rs b/src/api/jsonrpc/methods/relays/mod.rs
@@ -1,22 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod add;
-pub mod connect;
-pub mod list;
-pub mod remove;
-pub mod status;
-
-pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> {
- let mut m = RpcModule::new(ctx);
-
- add::register(&mut m, ®istry)?;
- remove::register(&mut m, ®istry)?;
- list::register(&mut m, ®istry)?;
- status::register(&mut m, ®istry)?;
- connect::register(&mut m, ®istry)?;
-
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/relays/remove.rs b/src/api/jsonrpc/methods/relays/remove.rs
@@ -1,28 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use radroots_nostr::prelude::radroots_nostr_remove_relay;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::relays::RelayRemovedResponse;
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-
-#[derive(Debug, Deserialize)]
-struct RemoveParams {
- url: String,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("relays.remove");
- m.register_async_method("relays.remove", |params, ctx, _| async move {
- let RemoveParams { url } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- radroots_nostr_remove_relay(&ctx.state.client, &url)
- .await
- .map_err(|e| RpcError::Other(format!("failed to remove relay {url}: {e}")))?;
-
- Ok::<RelayRemovedResponse, RpcError>(RelayRemovedResponse { removed: url })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/relays/status.rs b/src/api/jsonrpc/methods/relays/status.rs
@@ -1,64 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::relays::RelayStatusRow;
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_nostr::prelude::fetch_nip11;
-
-#[derive(Debug, Default, Deserialize)]
-struct StatusParams {
- #[serde(default)]
- include_nip11: bool,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("relays.status");
- m.register_async_method("relays.status", |params, ctx, _| async move {
- let StatusParams { include_nip11 } = params
- .parse::<Option<StatusParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let relays = ctx.state.client.relays().await;
- let mut out = Vec::with_capacity(relays.len());
-
- for (relay_url, relay) in relays {
- let url_str = relay_url.to_string();
- let status_str = format!("{}", relay.status());
- let parsed = reqwest::Url::parse(&url_str).ok();
-
- let mut row = RelayStatusRow {
- url: url_str.clone(),
- status: status_str,
- scheme: None,
- host: None,
- onion: None,
- port: None,
- nip11: None,
- };
-
- if let Some(u) = &parsed {
- row.scheme = Some(u.scheme().to_string());
- if let Some(h) = u.host_str() {
- row.host = Some(h.to_string());
- row.onion = Some(h.ends_with(".onion"));
- }
- if let Some(p) = u.port() {
- row.port = Some(p);
- }
- }
-
- if include_nip11 {
- if let Some(doc) = fetch_nip11(&row.url).await {
- row.nip11 = Some(doc);
- }
- }
-
- out.push(row);
- }
-
- Ok::<Vec<RelayStatusRow>, RpcError>(out)
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/mod.rs b/src/api/jsonrpc/mod.rs
@@ -1,40 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::net::SocketAddr;
-
-use anyhow::Result;
-use jsonrpsee::server::{RpcModule, ServerHandle};
-
-use crate::config::RpcConfig;
-use crate::radrootsd::Radrootsd;
-
-mod context;
-mod error;
-mod nostr;
-mod params;
-mod relays;
-mod registry;
-mod server;
-
-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,
- addr: SocketAddr,
- rpc_cfg: &RpcConfig,
-) -> Result<ServerHandle> {
- let registry = MethodRegistry::default();
- let ctx = RpcContext::new(state, registry.clone());
- let server = server::build_server(addr, rpc_cfg).await?;
-
- let mut root = RpcModule::new(ctx.clone());
- methods::register_all(&mut root, ctx, registry)?;
-
- let handle = server.start(root);
- Ok(handle)
-}
diff --git a/src/api/jsonrpc/nostr.rs b/src/api/jsonrpc/nostr.rs
@@ -1,64 +0,0 @@
-#![forbid(unsafe_code)]
-
-use serde::Serialize;
-
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrEventId,
- RadrootsNostrOutput,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub struct NostrEventView {
- pub id: String,
- pub author: String,
- pub created_at: u64,
- pub kind: u32,
- pub tags: Vec<Vec<String>>,
- pub content: String,
- pub sig: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-pub struct PublishResponse {
- pub id: String,
- pub sent: Vec<String>,
- pub failed: Vec<(String, String)>,
-}
-
-pub(crate) fn event_tags(event: &RadrootsNostrEvent) -> Vec<Vec<String>> {
- event.tags.iter().map(|t| t.as_slice().to_vec()).collect()
-}
-
-pub(crate) fn event_view(event: &RadrootsNostrEvent) -> NostrEventView {
- event_view_with_tags(event, event_tags(event))
-}
-
-pub(crate) fn event_view_with_tags(
- event: &RadrootsNostrEvent,
- tags: Vec<Vec<String>>,
-) -> NostrEventView {
- NostrEventView {
- id: event.id.to_string(),
- author: event.pubkey.to_string(),
- created_at: event.created_at.as_secs(),
- kind: event.kind.as_u16() as u32,
- tags,
- content: event.content.clone(),
- sig: event.sig.to_string(),
- }
-}
-
-pub(crate) fn publish_response(
- output: RadrootsNostrOutput<RadrootsNostrEventId>,
-) -> PublishResponse {
- PublishResponse {
- id: output.id().to_string(),
- sent: output.success.into_iter().map(|u| u.to_string()).collect(),
- failed: output
- .failed
- .into_iter()
- .map(|(u, e)| (u.to_string(), e.to_string()))
- .collect(),
- }
-}
diff --git a/src/api/jsonrpc/params.rs b/src/api/jsonrpc/params.rs
@@ -1,132 +0,0 @@
-#![forbid(unsafe_code)]
-
-use serde::Deserialize;
-
-use crate::api::jsonrpc::RpcError;
-use radroots_nostr::prelude::{
- radroots_nostr_parse_pubkeys,
- RadrootsNostrFilter,
- RadrootsNostrPublicKey,
- RadrootsNostrTimestamp,
-};
-
-pub const DEFAULT_LIMIT: u64 = 50;
-pub const MAX_LIMIT: u64 = 1000;
-pub const DEFAULT_TIMEOUT_SECS: u64 = 10;
-
-#[derive(Debug, Default, Deserialize)]
-pub struct EventListParams {
- #[serde(default)]
- pub authors: Option<Vec<String>>,
- #[serde(default)]
- pub limit: Option<u64>,
- #[serde(default)]
- pub since: Option<u64>,
- #[serde(default)]
- pub until: Option<u64>,
- #[serde(default)]
- pub timeout_secs: Option<u64>,
-}
-
-pub(crate) fn limit_or(limit: Option<u64>) -> usize {
- limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize
-}
-
-pub(crate) fn timeout_or(timeout_secs: Option<u64>) -> u64 {
- timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS)
-}
-
-pub(crate) fn parse_pubkeys(
- label: &str,
- values: &[String],
-) -> Result<Vec<RadrootsNostrPublicKey>, RpcError> {
- radroots_nostr_parse_pubkeys(values)
- .map_err(|e| RpcError::InvalidParams(format!("invalid {label}: {e}")))
-}
-
-pub(crate) fn parse_pubkeys_opt(
- label: &str,
- values: Option<Vec<String>>,
-) -> Result<Option<Vec<RadrootsNostrPublicKey>>, RpcError> {
- match values {
- Some(values) => Ok(Some(parse_pubkeys(label, &values)?)),
- None => Ok(None),
- }
-}
-
-pub(crate) fn apply_time_bounds(
- mut filter: RadrootsNostrFilter,
- since: Option<u64>,
- until: Option<u64>,
-) -> RadrootsNostrFilter {
- if let Some(since) = since {
- filter = filter.since(RadrootsNostrTimestamp::from_secs(since));
- }
- if let Some(until) = until {
- filter = filter.until(RadrootsNostrTimestamp::from_secs(until));
- }
- filter
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- DEFAULT_LIMIT,
- DEFAULT_TIMEOUT_SECS,
- MAX_LIMIT,
- };
- use crate::api::jsonrpc::RpcError;
- use radroots_nostr::prelude::RadrootsNostrFilter;
-
- #[test]
- fn limit_or_defaults_and_caps() {
- assert_eq!(limit_or(None), DEFAULT_LIMIT as usize);
- assert_eq!(limit_or(Some(MAX_LIMIT + 1)), MAX_LIMIT as usize);
- assert_eq!(limit_or(Some(0)), 0);
- }
-
- #[test]
- fn timeout_or_defaults() {
- assert_eq!(timeout_or(None), DEFAULT_TIMEOUT_SECS);
- assert_eq!(timeout_or(Some(3)), 3);
- }
-
- #[test]
- fn apply_time_bounds_sets_since_until() {
- let filter = RadrootsNostrFilter::new();
- let filter = apply_time_bounds(filter, Some(10), Some(20));
- assert_eq!(filter.since.map(|t| t.as_secs()), Some(10));
- assert_eq!(filter.until.map(|t| t.as_secs()), Some(20));
- }
-
- #[test]
- fn apply_time_bounds_noop_when_empty() {
- let filter = RadrootsNostrFilter::new();
- let filter = apply_time_bounds(filter, None, None);
- assert!(filter.since.is_none());
- assert!(filter.until.is_none());
- }
-
- #[test]
- fn parse_pubkeys_opt_accepts_valid() {
- let key = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let out = parse_pubkeys_opt("author", Some(vec![key.to_string()])).expect("pubkey");
- let out = out.expect("some");
- assert_eq!(out.len(), 1);
- assert_eq!(out[0].to_string(), key);
- }
-
- #[test]
- fn parse_pubkeys_opt_rejects_invalid() {
- let err = parse_pubkeys_opt("author", Some(vec!["nope".to_string()]))
- .expect_err("error");
- match err {
- RpcError::InvalidParams(msg) => assert!(msg.contains("invalid author")),
- _ => panic!("unexpected error"),
- }
- }
-}
diff --git a/src/api/jsonrpc/registry.rs b/src/api/jsonrpc/registry.rs
@@ -1,23 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::sync::{Arc, RwLock};
-
-#[derive(Clone, Default)]
-pub struct MethodRegistry {
- inner: Arc<RwLock<Vec<String>>>,
-}
-
-impl MethodRegistry {
- pub fn track(&self, name: &'static str) {
- let mut methods = self.inner.write().unwrap_or_else(|e| e.into_inner());
- if methods.iter().any(|entry| entry == name) {
- return;
- }
- methods.push(name.to_string());
- methods.sort();
- }
-
- pub fn list(&self) -> Vec<String> {
- self.inner.read().unwrap_or_else(|e| e.into_inner()).clone()
- }
-}
diff --git a/src/api/jsonrpc/relays.rs b/src/api/jsonrpc/relays.rs
@@ -1,34 +0,0 @@
-#![forbid(unsafe_code)]
-
-use serde::Serialize;
-
-use radroots_events::relay_document::RadrootsRelayDocument;
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct RelayAddedResponse {
- pub added: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct RelayRemovedResponse {
- pub removed: String,
-}
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct RelayConnectResponse {
- pub connected: usize,
- pub connecting: usize,
- pub disconnected: usize,
- pub spawned_connect: bool,
-}
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct RelayStatusRow {
- pub url: String,
- pub status: String,
- pub scheme: Option<String>,
- pub host: Option<String>,
- pub onion: Option<bool>,
- pub port: Option<u16>,
- pub nip11: Option<RadrootsRelayDocument>,
-}
diff --git a/src/api/jsonrpc/server.rs b/src/api/jsonrpc/server.rs
@@ -1,30 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::net::SocketAddr;
-
-use anyhow::Result;
-use jsonrpsee::server::{BatchRequestConfig, Server, ServerBuilder, ServerConfigBuilder};
-
-use crate::config::RpcConfig;
-
-pub async fn build_server(addr: SocketAddr, rpc_cfg: &RpcConfig) -> Result<Server> {
- let mut builder = ServerConfigBuilder::new()
- .max_request_body_size(rpc_cfg.max_request_body_size)
- .max_response_body_size(rpc_cfg.max_response_body_size)
- .max_connections(rpc_cfg.max_connections)
- .max_subscriptions_per_connection(rpc_cfg.max_subscriptions_per_connection)
- .set_message_buffer_capacity(rpc_cfg.message_buffer_capacity);
-
- if let Some(limit) = rpc_cfg.batch_request_limit {
- let cfg = if limit == 0 {
- BatchRequestConfig::Disabled
- } else {
- BatchRequestConfig::Limit(limit)
- };
- builder = builder.set_batch_request_config(cfg);
- }
-
- let server_cfg = builder.build();
- let server = ServerBuilder::with_config(server_cfg).build(addr).await?;
- Ok(server)
-}
diff --git a/src/api/mod.rs b/src/api/mod.rs
@@ -1,3 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub mod jsonrpc;
diff --git a/src/app/mod.rs b/src/app/mod.rs
diff --git a/src/build/mod.rs b/src/build/mod.rs
@@ -1 +0,0 @@
-#![forbid(unsafe_code)]
diff --git a/src/cli.rs b/src/cli.rs
@@ -1,35 +0,0 @@
-use std::path::PathBuf;
-
-use clap::{Parser, ValueHint, command};
-
-#[derive(Parser, Debug, Clone)]
-#[command(
- about = env!("CARGO_PKG_DESCRIPTION"),
- author = env!("CARGO_PKG_AUTHORS"),
- version = env!("CARGO_PKG_VERSION")
-)]
-pub struct Args {
- #[arg(
- long,
- value_name = "PATH",
- value_hint = ValueHint::FilePath,
- default_value = "config.toml",
- help = "Path to the daemon configuration file (defaults to config.toml)"
- )]
- pub config: PathBuf,
-
- #[arg(
- long,
- value_name = "PATH",
- value_hint = ValueHint::FilePath,
- help = "Path to the daemon identity file (json, txt, or raw 32-byte key; defaults to identity.json)",
- )]
- pub identity: Option<PathBuf>,
-
- #[arg(
- long,
- action = clap::ArgAction::SetTrue,
- help = "Allow generating a new identity file if missing; if not set and identity file is absent, the daemon will fail"
- )]
- pub allow_generate_identity: bool,
-}
diff --git a/src/config.rs b/src/config.rs
@@ -1,83 +0,0 @@
-use radroots_nostr::prelude::RadrootsNostrMetadata;
-use serde::{Deserialize, Serialize};
-
-fn default_rpc_addr() -> String {
- "127.0.0.1:7070".to_string()
-}
-
-fn default_max_request_body_size() -> u32 {
- 10 * 1024 * 1024
-}
-
-fn default_max_response_body_size() -> u32 {
- 10 * 1024 * 1024
-}
-
-fn default_max_connections() -> u32 {
- 100
-}
-
-fn default_max_subscriptions_per_connection() -> u32 {
- 1024
-}
-
-fn default_message_buffer_capacity() -> u32 {
- 1024
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct RpcConfig {
- #[serde(default = "default_rpc_addr")]
- pub addr: String,
- #[serde(default = "default_max_request_body_size")]
- pub max_request_body_size: u32,
- #[serde(default = "default_max_response_body_size")]
- pub max_response_body_size: u32,
- #[serde(default = "default_max_connections")]
- pub max_connections: u32,
- #[serde(default = "default_max_subscriptions_per_connection")]
- pub max_subscriptions_per_connection: u32,
- #[serde(default = "default_message_buffer_capacity")]
- pub message_buffer_capacity: u32,
- #[serde(default)]
- pub batch_request_limit: Option<u32>,
-}
-
-impl Default for RpcConfig {
- fn default() -> Self {
- Self {
- addr: default_rpc_addr(),
- max_request_body_size: default_max_request_body_size(),
- max_response_body_size: default_max_response_body_size(),
- max_connections: default_max_connections(),
- max_subscriptions_per_connection: default_max_subscriptions_per_connection(),
- message_buffer_capacity: default_message_buffer_capacity(),
- batch_request_limit: None,
- }
- }
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct Configuration {
- pub logs_dir: String,
- #[serde(default)]
- pub rpc: RpcConfig,
- #[serde(default)]
- pub rpc_addr: Option<String>,
- #[serde(default)]
- pub relays: Vec<String>,
-}
-
-impl Configuration {
- pub fn rpc_addr(&self) -> &str {
- self.rpc_addr
- .as_deref()
- .unwrap_or(self.rpc.addr.as_str())
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Settings {
- pub metadata: RadrootsNostrMetadata,
- pub config: Configuration,
-}
diff --git a/src/core/mod.rs b/src/core/mod.rs
@@ -0,0 +1 @@
+pub mod state;
diff --git a/src/core/state.rs b/src/core/state.rs
@@ -0,0 +1,36 @@
+use std::time::Instant;
+
+use radroots_nostr::prelude::{
+ RadrootsNostrClient,
+ RadrootsNostrKeys,
+ RadrootsNostrMetadata,
+ RadrootsNostrPublicKey,
+};
+
+#[derive(Clone)]
+pub struct Radrootsd {
+ pub(crate) started: Instant,
+ pub client: RadrootsNostrClient,
+ pub pubkey: RadrootsNostrPublicKey,
+ pub metadata: RadrootsNostrMetadata,
+ pub info: serde_json::Value,
+}
+
+impl Radrootsd {
+ pub fn new(keys: RadrootsNostrKeys, metadata: RadrootsNostrMetadata) -> Self {
+ let pubkey = keys.public_key();
+ let client = RadrootsNostrClient::new(keys);
+ let info = serde_json::json!({
+ "version": env!("CARGO_PKG_VERSION"),
+ "build": option_env!("GIT_HASH").unwrap_or("unknown"),
+ });
+
+ Self {
+ started: Instant::now(),
+ client,
+ pubkey,
+ metadata,
+ info,
+ }
+ }
+}
diff --git a/src/events/mod.rs b/src/events/mod.rs
@@ -1 +0,0 @@
-#![forbid(unsafe_code)]
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,105 +1,5 @@
#![forbid(unsafe_code)]
-pub mod api;
-pub mod build;
-pub mod cli;
-pub mod config;
-pub mod events;
-pub mod nip46;
-pub mod radrootsd;
-pub mod validate;
-
-use anyhow::Result;
-
-pub use cli::Args as cli_args;
-use tracing::info;
-
-use crate::radrootsd::Radrootsd;
-use radroots_identity::RadrootsIdentity;
-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_radrootsd(settings: &config::Settings, args: &cli_args) -> Result<()> {
- let identity = RadrootsIdentity::load_or_generate(
- args.identity.as_ref(),
- args.allow_generate_identity,
- )?;
- let keys = identity.keys().clone();
-
- let radrootsd = Radrootsd::new(keys, settings.metadata.clone());
-
- for relay in settings.config.relays.iter() {
- radrootsd.client.add_relay(relay).await?;
- }
-
- 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");
- }
- }
- });
- }
-
- let addr: std::net::SocketAddr = settings.config.rpc_addr().parse()?;
- let handle = api::jsonrpc::start_rpc(radrootsd.clone(), addr, &settings.config.rpc).await?;
- info!("JSON-RPC listening on {addr}");
-
- let stop_handle = handle.clone();
-
- tokio::select! {
- _ = radroots_runtime::shutdown_signal() => {
- tracing::info!("Shutting down…");
- let _ = stop_handle.stop();
- }
- _ = handle.stopped() => {}
- }
-
- Ok(())
-}
+pub mod app;
+pub mod core;
+pub mod transport;
diff --git a/src/main.rs b/src/main.rs
@@ -1,32 +1 @@
-#![forbid(unsafe_code)]
-
-use anyhow::{Context, Result};
-use radrootsd::{cli_args, config, run_radrootsd};
-use std::process::ExitCode;
-use tracing::info;
-
-#[tokio::main]
-async fn main() -> ExitCode {
- match run().await {
- Ok(()) => ExitCode::SUCCESS,
- Err(err) => {
- tracing::error!(error = ?err, "Fatal error");
- eprintln!("Fatal error: {err:#}");
- ExitCode::FAILURE
- }
- }
-}
-
-async fn run() -> Result<()> {
- let (args, settings): (cli_args, config::Settings) =
- radroots_runtime::parse_and_load_path_with_init(
- |a: &cli_args| Some(a.config.as_path()),
- |cfg: &config::Settings| cfg.config.logs_dir.as_str(),
- None,
- )
- .context("load configuration")?;
-
- info!("Starting radrootsd");
-
- run_radrootsd(&settings, &args).await
-}
+fn main() {}
diff --git a/src/nip46/client.rs b/src/nip46/client.rs
@@ -1,156 +0,0 @@
-#![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,
- RadrootsNostrRelayPoolNotification,
- RadrootsNostrTimestamp,
-};
-use nostr::nips::{
- nip44,
- nip46::{NostrConnectMessage, NostrConnectMethod, NostrConnectRequest, ResponseResult},
-};
-use nostr::JsonUtil;
-use nostr::UnsignedEvent;
-use tokio::sync::broadcast;
-use tokio::time::sleep;
-
-pub async fn sign_event(
- session: &Nip46Session,
- unsigned: UnsignedEvent,
- label: &str,
-) -> Result<nostr::Event, RpcError> {
- let req = NostrConnectRequest::SignEvent(unsigned);
- let response = request(session, req, label).await?;
- let response = response
- .to_response(NostrConnectMethod::SignEvent)
- .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?;
-
- if let Some(error) = response.error {
- return Err(RpcError::Other(format!("nip46 {label} error: {error}")));
- }
-
- let event = match response.result {
- Some(ResponseResult::SignEvent(event)) => *event,
- Some(_) => {
- return Err(RpcError::Other(format!(
- "nip46 {label} unexpected response"
- )))
- }
- None => {
- return Err(RpcError::Other(format!(
- "nip46 {label} missing response"
- )))
- }
- };
-
- event
- .verify()
- .map_err(|e| RpcError::Other(format!("nip46 {label} invalid event: {e}")))?;
-
- Ok(event)
-}
-
-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;
- session
- .client
- .wait_for_connection(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
- .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())
- .since(RadrootsNostrTimestamp::now());
- 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 mut notifications = session.client.notifications();
- let subscription = session
- .client
- .subscribe(filter, None)
- .await
- .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?;
- let timeout = sleep(Duration::from_secs(DEFAULT_TIMEOUT_SECS));
- tokio::pin!(timeout);
-
- loop {
- tokio::select! {
- _ = &mut timeout => {
- session.client.unsubscribe(&subscription.val).await;
- return Err(RpcError::Other(format!("nip46 {label} response not found")));
- }
- msg = notifications.recv() => {
- let notification = match msg {
- Ok(notification) => notification,
- Err(broadcast::error::RecvError::Lagged(_)) => continue,
- Err(broadcast::error::RecvError::Closed) => {
- return Err(RpcError::Other(format!("nip46 {label} notification closed")));
- }
- };
- let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else {
- continue;
- };
- let event = (*event).clone();
- if event.kind != RadrootsNostrKind::NostrConnect
- || event.pubkey != session.remote_signer_pubkey
- {
- continue;
- }
- 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 {
- session.client.unsubscribe(&subscription.val).await;
- return Ok(message);
- }
- }
- }
- }
-}
diff --git a/src/nip46/connection.rs b/src/nip46/connection.rs
@@ -1,211 +0,0 @@
-#![forbid(unsafe_code)]
-
-use serde::Serialize;
-use url::Url;
-
-use crate::api::jsonrpc::RpcError;
-use radroots_nostr::prelude::radroots_nostr_parse_pubkey;
-
-#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum Nip46ConnectMode {
- Bunker,
- Nostrconnect,
-}
-
-#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
-pub struct Nip46ConnectInfo {
- pub mode: Nip46ConnectMode,
- pub remote_signer_pubkey: Option<String>,
- pub client_pubkey: Option<String>,
- pub relays: Vec<String>,
- pub secret: Option<String>,
- pub perms: Vec<String>,
- pub name: Option<String>,
- pub url: Option<String>,
- pub image: Option<String>,
-}
-
-pub fn parse_connect_url(raw: &str) -> Result<Nip46ConnectInfo, RpcError> {
- let url = Url::parse(raw)
- .map_err(|e| RpcError::InvalidParams(format!("invalid connect url: {e}")))?;
- match url.scheme() {
- "bunker" => parse_bunker_url(&url),
- "nostrconnect" => parse_nostrconnect_url(&url),
- scheme => Err(RpcError::InvalidParams(format!(
- "unsupported connect scheme: {scheme}"
- ))),
- }
-}
-
-fn parse_bunker_url(url: &Url) -> Result<Nip46ConnectInfo, RpcError> {
- let username = url.username();
- let host = url
- .host_str()
- .ok_or_else(|| RpcError::InvalidParams("missing remote signer".to_string()))?;
- let remote_signer_raw = if username.is_empty() { host } else { username };
- let remote_signer = radroots_nostr_parse_pubkey(remote_signer_raw)
- .map_err(|e| RpcError::InvalidParams(format!("invalid remote signer: {e}")))?
- .to_hex();
-
- let mut relays = parse_relays(url);
- if !username.is_empty() {
- if let Some(relay_host) = host_to_relay(url) {
- relays.insert(0, relay_host);
- }
- }
-
- Ok(Nip46ConnectInfo {
- mode: Nip46ConnectMode::Bunker,
- remote_signer_pubkey: Some(remote_signer),
- client_pubkey: None,
- relays,
- secret: parse_optional_param(url, "secret"),
- perms: parse_perms(url),
- name: parse_optional_param(url, "name"),
- url: parse_optional_param(url, "url"),
- image: parse_optional_param(url, "image"),
- })
-}
-
-fn parse_nostrconnect_url(url: &Url) -> Result<Nip46ConnectInfo, RpcError> {
- let host = url
- .host_str()
- .ok_or_else(|| RpcError::InvalidParams("missing client pubkey".to_string()))?;
- let client_pubkey = radroots_nostr_parse_pubkey(host)
- .map_err(|e| RpcError::InvalidParams(format!("invalid client pubkey: {e}")))?
- .to_hex();
-
- let relays = parse_relays(url);
- if relays.is_empty() {
- return Err(RpcError::InvalidParams("missing relay".to_string()));
- }
-
- let secret = parse_optional_param(url, "secret")
- .ok_or_else(|| RpcError::InvalidParams("missing secret".to_string()))?;
-
- Ok(Nip46ConnectInfo {
- mode: Nip46ConnectMode::Nostrconnect,
- remote_signer_pubkey: None,
- client_pubkey: Some(client_pubkey),
- relays,
- secret: Some(secret),
- perms: parse_perms(url),
- name: parse_optional_param(url, "name"),
- url: parse_optional_param(url, "url"),
- image: parse_optional_param(url, "image"),
- })
-}
-
-fn parse_relays(url: &Url) -> Vec<String> {
- url.query_pairs()
- .filter_map(|(key, value)| {
- if key == "relay" && !value.trim().is_empty() {
- Some(value.to_string())
- } else {
- None
- }
- })
- .collect()
-}
-
-fn parse_optional_param(url: &Url, key: &str) -> Option<String> {
- url.query_pairs()
- .find_map(|(k, value)| {
- if k == key {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- None
- } else {
- Some(trimmed.to_string())
- }
- } else {
- None
- }
- })
-}
-
-fn parse_perms(url: &Url) -> Vec<String> {
- parse_optional_param(url, "perms")
- .map(|value| {
- value
- .split(',')
- .map(str::trim)
- .filter(|entry| !entry.is_empty())
- .map(|entry| entry.to_string())
- .collect()
- })
- .unwrap_or_default()
-}
-
-fn host_to_relay(url: &Url) -> Option<String> {
- let host = url.host_str()?;
- let port = url.port();
- let base = match port {
- Some(port) => format!("{host}:{port}"),
- None => host.to_string(),
- };
- Some(format!("wss://{base}"))
-}
-
-#[cfg(test)]
-mod tests {
- use super::{parse_connect_url, Nip46ConnectMode};
-
- const HEX_PUBKEY: &str =
- "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
-
- #[test]
- fn parse_bunker_with_relay_host() {
- let url = format!("bunker://{HEX_PUBKEY}@relay.example.com");
- let info = parse_connect_url(&url).expect("info");
- assert_eq!(info.mode, Nip46ConnectMode::Bunker);
- assert_eq!(info.remote_signer_pubkey.as_deref(), Some(HEX_PUBKEY));
- assert_eq!(info.relays, vec!["wss://relay.example.com"]);
- }
-
- #[test]
- fn parse_bunker_with_query_relay() {
- let url = format!("bunker://{HEX_PUBKEY}?relay=wss%3A%2F%2Frelay.example.com&secret=abc");
- let info = parse_connect_url(&url).expect("info");
- assert_eq!(info.mode, Nip46ConnectMode::Bunker);
- assert_eq!(info.remote_signer_pubkey.as_deref(), Some(HEX_PUBKEY));
- assert_eq!(info.relays, vec!["wss://relay.example.com"]);
- assert_eq!(info.secret.as_deref(), Some("abc"));
- }
-
- #[test]
- fn parse_nostrconnect_requires_secret_and_relay() {
- let url = format!("nostrconnect://{HEX_PUBKEY}?relay=wss%3A%2F%2Frelay.example.com&secret=token&perms=sign_event%3A1,nip44_encrypt");
- let info = parse_connect_url(&url).expect("info");
- assert_eq!(info.mode, Nip46ConnectMode::Nostrconnect);
- assert_eq!(info.client_pubkey.as_deref(), Some(HEX_PUBKEY));
- assert_eq!(info.relays, vec!["wss://relay.example.com"]);
- assert_eq!(info.secret.as_deref(), Some("token"));
- assert_eq!(info.perms.len(), 2);
- }
-
- #[test]
- fn parse_bunker_with_metadata() {
- let url = format!("bunker://{HEX_PUBKEY}?relay=wss%3A%2F%2Frelay.example.com&name=Radroots&url=https%3A%2F%2Fradroots.org&image=https%3A%2F%2Fradroots.org%2Flogo.png");
- let info = parse_connect_url(&url).expect("info");
- assert_eq!(info.mode, Nip46ConnectMode::Bunker);
- assert_eq!(info.remote_signer_pubkey.as_deref(), Some(HEX_PUBKEY));
- assert_eq!(info.relays, vec!["wss://relay.example.com"]);
- assert_eq!(info.name.as_deref(), Some("Radroots"));
- assert_eq!(info.url.as_deref(), Some("https://radroots.org"));
- assert_eq!(info.image.as_deref(), Some("https://radroots.org/logo.png"));
- }
-
- #[test]
- fn parse_nostrconnect_with_metadata() {
- let url = format!("nostrconnect://{HEX_PUBKEY}?relay=wss%3A%2F%2Frelay.example.com&secret=token&name=Radroots&url=https%3A%2F%2Fradroots.org");
- let info = parse_connect_url(&url).expect("info");
- assert_eq!(info.mode, Nip46ConnectMode::Nostrconnect);
- assert_eq!(info.client_pubkey.as_deref(), Some(HEX_PUBKEY));
- assert_eq!(info.relays, vec!["wss://relay.example.com"]);
- assert_eq!(info.secret.as_deref(), Some("token"));
- assert_eq!(info.name.as_deref(), Some("Radroots"));
- assert_eq!(info.url.as_deref(), Some("https://radroots.org"));
- }
-}
diff --git a/src/nip46/mod.rs b/src/nip46/mod.rs
@@ -1,5 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub mod connection;
-pub mod client;
-pub mod session;
diff --git a/src/nip46/session.rs b/src/nip46/session.rs
@@ -1,66 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::collections::HashMap;
-use std::sync::Arc;
-
-use tokio::sync::Mutex;
-
-use radroots_nostr::prelude::{
- RadrootsNostrClient,
- RadrootsNostrKeys,
- RadrootsNostrPublicKey,
-};
-
-#[derive(Clone)]
-pub struct Nip46SessionStore {
- inner: Arc<Mutex<HashMap<String, Nip46Session>>>,
-}
-
-#[derive(Clone)]
-pub struct Nip46Session {
- pub id: String,
- pub client: RadrootsNostrClient,
- pub client_keys: RadrootsNostrKeys,
- pub client_pubkey: RadrootsNostrPublicKey,
- pub remote_signer_pubkey: RadrootsNostrPublicKey,
- pub user_pubkey: Option<RadrootsNostrPublicKey>,
- pub relays: Vec<String>,
-}
-
-impl Nip46SessionStore {
- pub fn new() -> Self {
- Self {
- inner: Arc::new(Mutex::new(HashMap::new())),
- }
- }
-
- pub async fn insert(&self, session: Nip46Session) {
- let mut sessions = self.inner.lock().await;
- sessions.insert(session.id.clone(), session);
- }
-
- pub async fn get(&self, session_id: &str) -> Option<Nip46Session> {
- let sessions = self.inner.lock().await;
- sessions.get(session_id).cloned()
- }
-
- pub async fn remove(&self, session_id: &str) -> bool {
- let mut sessions = self.inner.lock().await;
- sessions.remove(session_id).is_some()
- }
-
- 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,
- }
- }
-}
diff --git a/src/radrootsd.rs b/src/radrootsd.rs
@@ -1,41 +0,0 @@
-use std::time::Instant;
-
-use radroots_nostr::prelude::{
- RadrootsNostrClient,
- RadrootsNostrKeys,
- RadrootsNostrMetadata,
- RadrootsNostrPublicKey,
-};
-
-use crate::nip46::session::Nip46SessionStore;
-
-#[derive(Clone)]
-pub struct Radrootsd {
- pub(crate) started: Instant,
- pub client: RadrootsNostrClient,
- pub pubkey: RadrootsNostrPublicKey,
- pub metadata: RadrootsNostrMetadata,
- pub info: serde_json::Value,
- pub(crate) nip46_sessions: Nip46SessionStore,
-}
-
-impl Radrootsd {
- pub fn new(keys: RadrootsNostrKeys, metadata: RadrootsNostrMetadata) -> Self {
- let pubkey = keys.public_key();
- let client = RadrootsNostrClient::new(keys);
- let info = serde_json::json!({
- "version": env!("CARGO_PKG_VERSION"),
- "build": option_env!("GIT_HASH").unwrap_or("unknown"),
- });
- let nip46_sessions = Nip46SessionStore::new();
-
- Self {
- started: Instant::now(),
- client,
- pubkey,
- metadata,
- info,
- nip46_sessions,
- }
- }
-}
-\ No newline at end of file
diff --git a/src/transport/jsonrpc/mod.rs b/src/transport/jsonrpc/mod.rs
diff --git a/src/transport/mod.rs b/src/transport/mod.rs
@@ -0,0 +1,2 @@
+pub mod jsonrpc;
+pub mod nostr;
diff --git a/src/transport/nostr/listener.rs b/src/transport/nostr/listener.rs
@@ -0,0 +1,60 @@
+use std::time::Duration;
+
+use anyhow::{anyhow, Result};
+use tokio::sync::broadcast;
+use tracing::{info, warn};
+
+use crate::core::state::Radrootsd;
+use radroots_nostr::prelude::{
+ radroots_nostr_filter_tag,
+ RadrootsNostrFilter,
+ RadrootsNostrKind,
+ RadrootsNostrRelayPoolNotification,
+ RadrootsNostrTimestamp,
+};
+
+const DEFAULT_TIMEOUT_SECS: u64 = 10;
+
+pub fn spawn_nip46_listener(radrootsd: Radrootsd) {
+ tokio::spawn(async move {
+ if let Err(error) = run_nip46_listener(radrootsd).await {
+ warn!("NIP-46 listener stopped: {error}");
+ }
+ });
+}
+
+async fn run_nip46_listener(radrootsd: Radrootsd) -> Result<()> {
+ radrootsd.client.connect().await;
+ radrootsd
+ .client
+ .wait_for_connection(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
+ .await;
+
+ let filter = RadrootsNostrFilter::new()
+ .kind(RadrootsNostrKind::NostrConnect)
+ .since(RadrootsNostrTimestamp::now());
+ let filter =
+ radroots_nostr_filter_tag(filter, "p", vec![radrootsd.pubkey.to_hex()])?;
+ let mut notifications = radrootsd.client.notifications();
+ let subscription = radrootsd.client.subscribe(filter, None).await?;
+
+ info!("NIP-46 listener subscribed: {}", subscription.val);
+
+ loop {
+ let notification = match notifications.recv().await {
+ Ok(notification) => notification,
+ Err(broadcast::error::RecvError::Lagged(_)) => continue,
+ Err(broadcast::error::RecvError::Closed) => {
+ return Err(anyhow!("nip46 listener notification closed"));
+ }
+ };
+ let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else {
+ continue;
+ };
+ let event = (*event).clone();
+ if event.kind != RadrootsNostrKind::NostrConnect {
+ continue;
+ }
+ info!("NIP-46 request received: {}", event.id);
+ }
+}
diff --git a/src/transport/nostr/mod.rs b/src/transport/nostr/mod.rs
@@ -0,0 +1 @@
+pub mod listener;
diff --git a/src/validate/mod.rs b/src/validate/mod.rs
@@ -1 +0,0 @@
-#![forbid(unsafe_code)]