use std::error::Error as StdError;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use thiserror::Error as ThisError;
use crate::event::{Event, UnsignedEvent, UnsignedEventError};
use crate::key::{Keys, PublicKey};
#[cfg(not(target_arch = "wasm32"))]
pub type SignerFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[cfg(target_arch = "wasm32")]
pub type SignerFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
#[cfg(not(target_arch = "wasm32"))]
pub fn boxed_signer_future<'a, F, T>(future: F) -> SignerFuture<'a, T>
where
F: Future<Output = T> + Send + 'a,
{
Box::pin(future)
}
#[cfg(target_arch = "wasm32")]
pub fn boxed_signer_future<'a, F, T>(future: F) -> SignerFuture<'a, T>
where
F: Future<Output = T> + 'a,
{
Box::pin(future)
}
#[derive(Debug, ThisError)]
#[non_exhaustive]
pub enum SignerError {
#[error(transparent)]
AuthorMismatch(#[from] UnsignedEventError),
#[error(
"signer rejected the request{}: {message}",
code.as_ref().map_or_else(String::new, |c| format!(" (code = {c})"))
)]
Rejected {
message: String,
code: Option<String>,
},
#[error("signer backend failure: {0}")]
Backend(Box<dyn StdError + Send + Sync>),
#[error("signer does not support `{0}`")]
Unsupported(&'static str),
}
impl SignerError {
pub fn backend<E>(err: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self::Backend(Box::new(err))
}
pub fn rejected<S>(message: S) -> Self
where
S: Into<String>,
{
Self::Rejected {
message: message.into(),
code: None,
}
}
pub fn rejected_with_code<S, C>(message: S, code: C) -> Self
where
S: Into<String>,
C: Into<String>,
{
Self::Rejected {
message: message.into(),
code: Some(code.into()),
}
}
}
pub trait NostrSigner: fmt::Debug + Send + Sync {
fn get_public_key(&self) -> SignerFuture<'_, Result<PublicKey, SignerError>>;
fn sign_event(&self, unsigned: UnsignedEvent) -> SignerFuture<'_, Result<Event, SignerError>>;
fn nip04_encrypt<'a>(
&'a self,
_peer: &'a PublicKey,
_plaintext: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
boxed_signer_future(async { Err(SignerError::Unsupported("nip04_encrypt")) })
}
fn nip04_decrypt<'a>(
&'a self,
_peer: &'a PublicKey,
_ciphertext: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
boxed_signer_future(async { Err(SignerError::Unsupported("nip04_decrypt")) })
}
fn nip44_encrypt<'a>(
&'a self,
_peer: &'a PublicKey,
_plaintext: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
boxed_signer_future(async { Err(SignerError::Unsupported("nip44_encrypt")) })
}
fn nip44_decrypt<'a>(
&'a self,
_peer: &'a PublicKey,
_payload: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
boxed_signer_future(async { Err(SignerError::Unsupported("nip44_decrypt")) })
}
}
impl NostrSigner for Keys {
fn get_public_key(&self) -> SignerFuture<'_, Result<PublicKey, SignerError>> {
let key = *self.public_key();
boxed_signer_future(async move { Ok(key) })
}
fn sign_event(&self, unsigned: UnsignedEvent) -> SignerFuture<'_, Result<Event, SignerError>> {
boxed_signer_future(async move {
let event = unsigned.sign_with_keys(self)?;
Ok(event)
})
}
#[cfg(feature = "nip04")]
fn nip04_encrypt<'a>(
&'a self,
peer: &'a PublicKey,
plaintext: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
boxed_signer_future(async move {
crate::nips::nip04::encrypt(self.secret_key(), peer, plaintext)
.map_err(SignerError::backend)
})
}
#[cfg(feature = "nip04")]
fn nip04_decrypt<'a>(
&'a self,
peer: &'a PublicKey,
ciphertext: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
boxed_signer_future(async move {
crate::nips::nip04::decrypt(self.secret_key(), peer, ciphertext)
.map_err(SignerError::backend)
})
}
#[cfg(feature = "nip44")]
fn nip44_encrypt<'a>(
&'a self,
peer: &'a PublicKey,
plaintext: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
boxed_signer_future(async move {
crate::nips::nip44::encrypt(self.secret_key(), peer, plaintext)
.map_err(SignerError::backend)
})
}
#[cfg(feature = "nip44")]
fn nip44_decrypt<'a>(
&'a self,
peer: &'a PublicKey,
payload: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
boxed_signer_future(async move {
crate::nips::nip44::decrypt(self.secret_key(), peer, payload)
.map_err(SignerError::backend)
})
}
}
impl<S> NostrSigner for Arc<S>
where
S: NostrSigner + ?Sized,
{
fn get_public_key(&self) -> SignerFuture<'_, Result<PublicKey, SignerError>> {
(**self).get_public_key()
}
fn sign_event(&self, unsigned: UnsignedEvent) -> SignerFuture<'_, Result<Event, SignerError>> {
(**self).sign_event(unsigned)
}
fn nip04_encrypt<'a>(
&'a self,
peer: &'a PublicKey,
plaintext: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
(**self).nip04_encrypt(peer, plaintext)
}
fn nip04_decrypt<'a>(
&'a self,
peer: &'a PublicKey,
ciphertext: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
(**self).nip04_decrypt(peer, ciphertext)
}
fn nip44_encrypt<'a>(
&'a self,
peer: &'a PublicKey,
plaintext: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
(**self).nip44_encrypt(peer, plaintext)
}
fn nip44_decrypt<'a>(
&'a self,
peer: &'a PublicKey,
payload: &'a str,
) -> SignerFuture<'a, Result<String, SignerError>> {
(**self).nip44_decrypt(peer, payload)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::EventBuilder;
use crate::types::Timestamp;
fn fixture_keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
}
fn block_on<F: Future>(f: F) -> F::Output {
use std::pin::pin;
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut fut = pin!(f);
match fut.as_mut().poll(&mut cx) {
Poll::Ready(out) => out,
Poll::Pending => unreachable!("test futures must be synchronous"),
}
}
#[test]
fn keys_implement_signer() {
let keys = fixture_keys();
let pk = block_on(keys.get_public_key()).unwrap();
assert_eq!(pk, *keys.public_key());
}
#[test]
fn keys_sign_event() {
let keys = fixture_keys();
let unsigned = EventBuilder::text_note("hi")
.created_at(Timestamp::from_secs(1))
.build_unsigned(*keys.public_key())
.unwrap();
let event = block_on(keys.sign_event(unsigned)).unwrap();
event.verify().unwrap();
}
#[test]
fn arc_signer_dispatches() {
let keys: Arc<dyn NostrSigner> = Arc::new(fixture_keys());
let pk = block_on(keys.get_public_key()).unwrap();
assert_eq!(pk.to_byte_array().len(), 32);
}
#[test]
fn rejected_error_carries_reason() {
let err = SignerError::rejected("user denied");
let s = err.to_string();
assert!(s.contains("user denied"));
assert!(!s.contains("code"), "no structured code → no code suffix");
}
#[test]
fn rejected_with_code_surfaces_machine_readable_code() {
let err = SignerError::rejected_with_code("user denied", "user_rejected");
let s = err.to_string();
assert!(s.contains("user denied"));
assert!(
s.contains("code = user_rejected"),
"expected code suffix in: {s}",
);
}
#[test]
fn backend_error_round_trip() {
let inner = std::io::Error::other("oops");
let err = SignerError::backend(inner);
assert!(err.to_string().contains("oops"));
}
#[derive(Debug)]
struct SignOnlySigner(Keys);
impl NostrSigner for SignOnlySigner {
fn get_public_key(&self) -> SignerFuture<'_, Result<PublicKey, SignerError>> {
self.0.get_public_key()
}
fn sign_event(
&self,
unsigned: UnsignedEvent,
) -> SignerFuture<'_, Result<Event, SignerError>> {
self.0.sign_event(unsigned)
}
}
#[test]
fn default_encryption_methods_return_unsupported() {
let alice = SignOnlySigner(fixture_keys());
let bob_pk =
*Keys::parse("0000000000000000000000000000000000000000000000000000000000000007")
.unwrap()
.public_key();
let cases: [(&str, SignerFuture<'_, _>); 4] = [
("nip04_encrypt", alice.nip04_encrypt(&bob_pk, "hi")),
("nip04_decrypt", alice.nip04_decrypt(&bob_pk, "")),
("nip44_encrypt", alice.nip44_encrypt(&bob_pk, "hi")),
("nip44_decrypt", alice.nip44_decrypt(&bob_pk, "")),
];
for (label, fut) in cases {
let err = block_on(fut).unwrap_err();
assert!(
matches!(err, SignerError::Unsupported(name) if name == label),
"expected Unsupported({label}), got {err:?}",
);
}
}
#[cfg(feature = "nip04")]
#[test]
fn keys_nip04_round_trip_through_signer_trait() {
let alice = fixture_keys();
let bob = Keys::parse("0000000000000000000000000000000000000000000000000000000000000007")
.unwrap();
let payload = block_on(alice.nip04_encrypt(bob.public_key(), "legacy hi")).unwrap();
let recovered = block_on(bob.nip04_decrypt(alice.public_key(), &payload)).unwrap();
assert_eq!(recovered, "legacy hi");
}
#[cfg(feature = "nip44")]
#[test]
fn keys_nip44_round_trip_through_signer_trait() {
let alice = fixture_keys();
let bob = Keys::parse("0000000000000000000000000000000000000000000000000000000000000007")
.unwrap();
let payload = block_on(alice.nip44_encrypt(bob.public_key(), "secret")).unwrap();
let recovered = block_on(bob.nip44_decrypt(alice.public_key(), &payload)).unwrap();
assert_eq!(recovered, "secret");
}
#[cfg(feature = "nip44")]
#[test]
fn arc_dyn_signer_forwards_nip44_methods() {
let alice: Arc<dyn NostrSigner> = Arc::new(fixture_keys());
let bob = Keys::parse("0000000000000000000000000000000000000000000000000000000000000007")
.unwrap();
let payload = block_on(alice.nip44_encrypt(bob.public_key(), "via dyn")).unwrap();
let recovered =
block_on(bob.nip44_decrypt(&block_on(alice.get_public_key()).unwrap(), &payload))
.unwrap();
assert_eq!(recovered, "via dyn");
}
}