use alloc::borrow::{Cow, ToOwned};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use core::str::FromStr;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::Hash;
#[cfg(feature = "std")]
use bitcoin::secp256k1::rand;
use bitcoin::secp256k1::rand::{CryptoRng, Rng, RngCore};
use bitcoin::secp256k1::schnorr::Signature;
use bitcoin::secp256k1::{self, Message as Secp256k1Message, Secp256k1, Signing};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use super::nip04;
use super::nip26::{self, sign_delegation_with_ctx, Conditions};
use crate::event::unsigned::{self, UnsignedEvent};
use crate::key::{self, Keys};
use crate::types::url::form_urlencoded::byte_serialize;
use crate::types::url::{ParseError, Url};
#[cfg(feature = "std")]
use crate::SECP256K1;
use crate::{Event, JsonUtil, PublicKey};
#[derive(Debug)]
pub enum Error {
Key(key::Error),
Json(serde_json::Error),
Url(ParseError),
Secp256k1(secp256k1::Error),
NIP04(nip04::Error),
NIP26(nip26::Error),
UnsignedEvent(unsigned::Error),
InvalidRequest,
InvalidParamsLength,
UnsupportedMethod(String),
InvalidURI,
InvalidURIScheme,
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Key(e) => write!(f, "Key: {e}"),
Self::Json(e) => write!(f, "Json: {e}"),
Self::Url(e) => write!(f, "Url: {e}"),
Self::Secp256k1(e) => write!(f, "Secp256k1: {e}"),
Self::NIP04(e) => write!(f, "NIP04: {e}"),
Self::NIP26(e) => write!(f, "NIP26: {e}"),
Self::UnsignedEvent(e) => write!(f, "{e}"),
Self::InvalidRequest => write!(f, "Invalid request"),
Self::InvalidParamsLength => write!(f, "Too many/few params"),
Self::UnsupportedMethod(name) => write!(f, "Unsupported method: {name}"),
Self::InvalidURI => write!(f, "Invalid uri"),
Self::InvalidURIScheme => write!(f, "Invalid uri scheme"),
}
}
}
impl From<key::Error> for Error {
fn from(e: key::Error) -> Self {
Self::Key(e)
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Self::Json(e)
}
}
impl From<ParseError> for Error {
fn from(e: ParseError) -> Self {
Self::Url(e)
}
}
impl From<secp256k1::Error> for Error {
fn from(e: secp256k1::Error) -> Self {
Self::Secp256k1(e)
}
}
impl From<nip04::Error> for Error {
fn from(e: nip04::Error) -> Self {
Self::NIP04(e)
}
}
impl From<nip26::Error> for Error {
fn from(e: nip26::Error) -> Self {
Self::NIP26(e)
}
}
impl From<unsigned::Error> for Error {
fn from(e: unsigned::Error) -> Self {
Self::UnsignedEvent(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Request {
Describe,
GetPublicKey,
SignEvent(UnsignedEvent),
Connect(PublicKey),
Disconnect,
Delegate {
public_key: PublicKey,
conditions: Conditions,
},
Nip04Encrypt {
public_key: PublicKey,
text: String,
},
Nip04Decrypt {
public_key: PublicKey,
text: String,
},
SignSchnorr(String),
}
impl Request {
pub fn method(&self) -> String {
match self {
Self::Describe => "describe".to_string(),
Self::GetPublicKey => "get_public_key".to_string(),
Self::SignEvent(_) => "sign_event".to_string(),
Self::Connect(_) => "connect".to_string(),
Self::Disconnect => "disconnect".to_string(),
Self::Delegate { .. } => "delegate".to_string(),
Self::Nip04Encrypt { .. } => "nip04_encrypt".to_string(),
Self::Nip04Decrypt { .. } => "nip04_decrypt".to_string(),
Self::SignSchnorr(_) => "sign_schnorr".to_string(),
}
}
pub fn params(&self) -> Vec<Value> {
match self {
Self::Describe => Vec::new(),
Self::GetPublicKey => Vec::new(),
Self::SignEvent(event) => vec![json!(event)],
Self::Connect(pubkey) => vec![json!(pubkey)],
Self::Disconnect => Vec::new(),
Self::Delegate {
public_key,
conditions,
} => vec![json!(public_key), json!(conditions)],
Self::Nip04Encrypt { public_key, text } => vec![json!(public_key), json!(text)],
Self::Nip04Decrypt { public_key, text } => vec![json!(public_key), json!(text)],
Self::SignSchnorr(value) => vec![json!(value)],
}
}
#[cfg(feature = "std")]
pub fn generate_response(self, keys: &Keys) -> Result<Option<Response>, Error> {
self.generate_response_with_ctx(&SECP256K1, &mut rand::thread_rng(), keys)
}
pub fn generate_response_with_ctx<C, R>(
self,
secp: &Secp256k1<C>,
rng: &mut R,
keys: &Keys,
) -> Result<Option<Response>, Error>
where
C: Signing,
R: Rng + CryptoRng,
{
let res: Option<Response> = match self {
Self::Describe => Some(Response::Describe(vec![
String::from("describe"),
String::from("get_public_key"),
String::from("sign_event"),
String::from("connect"),
String::from("disconnect"),
String::from("delegate"),
String::from("nip04_encrypt"),
String::from("nip04_decrypt"),
String::from("sign_schnorr"),
])),
Self::GetPublicKey => Some(Response::GetPublicKey(keys.public_key())),
Self::SignEvent(unsigned_event) => {
let signed_event = unsigned_event.sign_with_ctx(secp, rng, keys)?;
Some(Response::SignEvent(signed_event))
}
Self::Connect(_) => None,
Self::Disconnect => None,
Self::Delegate {
public_key,
conditions,
} => {
let sig =
sign_delegation_with_ctx(secp, rng, keys, public_key, conditions.clone())?;
let delegation_result = DelegationResult {
from: keys.public_key(),
to: public_key,
cond: conditions,
sig,
};
Some(Response::Delegate(delegation_result))
}
Self::Nip04Encrypt { public_key, text } => {
let encrypted_content =
nip04::encrypt_with_rng(rng, keys.secret_key()?, &public_key, text)?;
Some(Response::Nip04Encrypt(encrypted_content))
}
Self::Nip04Decrypt { public_key, text } => {
let decrypted_content = nip04::decrypt(keys.secret_key()?, &public_key, text)?;
Some(Response::Nip04Decrypt(decrypted_content))
}
Self::SignSchnorr(value) => {
let hash = Sha256Hash::hash(value.as_bytes());
let message = Secp256k1Message::from(hash);
let sig: Signature = keys.sign_schnorr_with_ctx(secp, &message, rng)?;
Some(Response::SignSchnorr(sig))
}
};
Ok(res)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct DelegationResult {
pub from: PublicKey,
pub to: PublicKey,
pub cond: Conditions,
pub sig: Signature,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Response {
Describe(Vec<String>),
GetPublicKey(PublicKey),
SignEvent(Event),
Delegate(DelegationResult),
Nip04Encrypt(String),
Nip04Decrypt(String),
SignSchnorr(Signature),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Message {
Request {
id: String,
method: String,
params: Vec<Value>,
},
Response {
id: String,
result: Option<Value>,
error: Option<String>,
},
}
impl Message {
#[cfg(feature = "std")]
pub fn request(req: Request) -> Self {
Self::request_with_rng(&mut rand::thread_rng(), req)
}
pub fn request_with_rng<R>(rng: &mut R, req: Request) -> Self
where
R: RngCore,
{
Self::Request {
id: rng.next_u32().to_string(),
method: req.method(),
params: req.params(),
}
}
pub fn response<S>(req_id: S, res: Option<Response>, error: Option<S>) -> Self
where
S: Into<String>,
{
Self::Response {
id: req_id.into(),
result: res.map(|res| match res {
Response::Describe(v) => json!(v),
Response::GetPublicKey(pubkey) => json!(pubkey),
Response::SignEvent(sig) => json!(sig),
Response::Delegate(delegation_result) => json!(delegation_result),
Response::Nip04Encrypt(encrypted_content) => json!(encrypted_content),
Response::Nip04Decrypt(decrypted_content) => json!(decrypted_content),
Response::SignSchnorr(sig) => json!(sig),
}),
error: error.map(|e| e.into()),
}
}
pub fn is_request(&self) -> bool {
match self {
Message::Request { .. } => true,
Message::Response { .. } => false,
}
}
pub fn id(&self) -> String {
match self {
Self::Request { id, .. } => id.to_owned(),
Self::Response { id, .. } => id.to_owned(),
}
}
pub fn to_request(&self) -> Result<Request, Error> {
if let Message::Request { method, params, .. } = self {
match method.as_str() {
"describe" => Ok(Request::Describe),
"get_public_key" => Ok(Request::GetPublicKey),
"sign_event" => {
if let Some(value) = params.first() {
let unsigned_event: UnsignedEvent =
serde_json::from_value(value.to_owned())?;
Ok(Request::SignEvent(unsigned_event))
} else {
Err(Error::InvalidRequest)
}
}
"connect" => {
if params.len() != 1 {
return Err(Error::InvalidParamsLength);
}
let pubkey: PublicKey = serde_json::from_value(params[0].to_owned())?;
Ok(Request::Connect(pubkey))
}
"disconnect" => Ok(Request::Disconnect),
"delegate" => {
if params.len() != 2 {
return Err(Error::InvalidParamsLength);
}
Ok(Request::Delegate {
public_key: serde_json::from_value(params[0].clone())?,
conditions: serde_json::from_value(params[1].clone())?,
})
}
"nip04_encrypt" => {
if params.len() != 2 {
return Err(Error::InvalidParamsLength);
}
Ok(Request::Nip04Encrypt {
public_key: serde_json::from_value(params[0].clone())?,
text: serde_json::from_value(params[1].clone())?,
})
}
"nip04_decrypt" => {
if params.len() != 2 {
return Err(Error::InvalidParamsLength);
}
Ok(Request::Nip04Decrypt {
public_key: serde_json::from_value(params[0].clone())?,
text: serde_json::from_value(params[1].clone())?,
})
}
"sign_schnorr" => {
if params.len() != 1 {
return Err(Error::InvalidParamsLength);
}
let value: String = serde_json::from_value(params[0].clone())?;
Ok(Request::SignSchnorr(value))
}
other => Err(Error::UnsupportedMethod(other.to_string())),
}
} else {
Err(Error::InvalidRequest)
}
}
#[cfg(feature = "std")]
pub fn generate_response(&self, keys: &Keys) -> Result<Option<Self>, Error> {
self.generate_response_wit_ctx(&SECP256K1, &mut rand::thread_rng(), keys)
}
pub fn generate_response_wit_ctx<C, R>(
&self,
secp: &Secp256k1<C>,
rng: &mut R,
keys: &Keys,
) -> Result<Option<Self>, Error>
where
C: Signing,
R: Rng + CryptoRng,
{
let req = self.to_request()?;
if let Some(res) = req.generate_response_with_ctx(secp, rng, keys)? {
Ok(Some(Self::response(self.id(), Some(res), None)))
} else {
Ok(None)
}
}
pub fn generate_error_response<S>(&self, error: S) -> Result<Self, Error>
where
S: Into<String>,
{
let _req: Request = self.to_request()?;
Ok(Self::response(self.id(), None, Some(error.into())))
}
}
impl JsonUtil for Message {
type Err = Error;
}
fn url_encode<T>(data: T) -> String
where
T: AsRef<[u8]>,
{
byte_serialize(data.as_ref()).collect()
}
pub const NOSTR_CONNECT_URI_SCHEME: &str = "nostrconnect";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct NostrConnectMetadata {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Url>>,
}
impl NostrConnectMetadata {
pub fn new<S>(name: S) -> Self
where
S: Into<String>,
{
Self {
name: name.into(),
url: None,
description: None,
icons: None,
}
}
pub fn as_json(&self) -> String {
json!(self).to_string()
}
pub fn url(self, url: Url) -> Self {
Self {
url: Some(url),
..self
}
}
pub fn description<S>(self, description: S) -> Self
where
S: Into<String>,
{
Self {
description: Some(description.into()),
..self
}
}
pub fn icons(self, icons: Vec<Url>) -> Self {
Self {
icons: Some(icons),
..self
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct NostrConnectURI {
pub public_key: PublicKey,
pub relay_url: Url,
pub metadata: NostrConnectMetadata,
}
impl NostrConnectURI {
pub fn new<S>(public_key: PublicKey, relay_url: Url, app_name: S) -> Self
where
S: Into<String>,
{
Self::with_metadata(public_key, relay_url, NostrConnectMetadata::new(app_name))
}
pub fn with_metadata(
public_key: PublicKey,
relay_url: Url,
metadata: NostrConnectMetadata,
) -> Self {
Self {
public_key,
relay_url,
metadata,
}
}
pub fn url(self, url: Url) -> Self {
Self {
metadata: self.metadata.url(url),
..self
}
}
pub fn description<S>(self, description: S) -> Self
where
S: Into<String>,
{
Self {
metadata: self.metadata.description(description),
..self
}
}
pub fn icons(self, icons: Vec<Url>) -> Self {
Self {
metadata: self.metadata.icons(icons),
..self
}
}
}
impl FromStr for NostrConnectURI {
type Err = Error;
fn from_str(uri: &str) -> Result<Self, Self::Err> {
let url = Url::parse(uri)?;
if url.scheme() != NOSTR_CONNECT_URI_SCHEME {
return Err(Error::InvalidURIScheme);
}
if let Some(pubkey) = url.domain() {
let public_key = PublicKey::from_str(pubkey)?;
let mut relay_url: Option<Url> = None;
let mut metadata: Option<NostrConnectMetadata> = None;
for (key, value) in url.query_pairs() {
match key {
Cow::Borrowed("relay") => {
let value = value.to_string();
relay_url = Some(Url::parse(&value)?);
}
Cow::Borrowed("metadata") => {
let value = value.to_string();
metadata = Some(serde_json::from_str(&value)?);
}
_ => (),
}
}
if let Some(relay_url) = relay_url {
if let Some(metadata) = metadata {
return Ok(Self {
public_key,
relay_url,
metadata,
});
}
}
}
Err(Error::InvalidURI)
}
}
impl fmt::Display for NostrConnectURI {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{NOSTR_CONNECT_URI_SCHEME}://{}?relay={}&metadata={}",
self.public_key,
url_encode(self.relay_url.to_string()),
url_encode(self.metadata.as_json())
)
}
}
#[cfg(test)]
mod test {
use core::str::FromStr;
use super::*;
#[test]
fn test_uri() {
let pubkey =
PublicKey::from_str("b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4")
.unwrap();
let relay_url = Url::parse("wss://relay.damus.io").unwrap();
let app_name = "Example";
let uri = NostrConnectURI::new(pubkey, relay_url, app_name);
assert_eq!(
uri.to_string(),
"nostrconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io%2F&metadata=%7B%22name%22%3A%22Example%22%7D".to_string()
);
}
#[test]
fn test_parse_uri() {
let uri = "nostrconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io%2F&metadata=%7B%22name%22%3A%22Example%22%7D";
let uri = NostrConnectURI::from_str(uri).unwrap();
let pubkey =
PublicKey::from_str("b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4")
.unwrap();
let relay_url = Url::parse("wss://relay.damus.io").unwrap();
let app_name = "Example";
assert_eq!(uri, NostrConnectURI::new(pubkey, relay_url, app_name));
}
}