use std::borrow::Cow;
use std::fmt;
use std::str::FromStr;
use secp256k1::{SecretKey, XOnlyPublicKey};
use serde::{Deserialize, Serialize};
use serde_json::json;
use url::form_urlencoded::byte_serialize;
use url::Url;
use super::nip04;
use crate::{key, Keys};
#[derive(Debug)]
pub enum Error {
Key(key::Error),
JSON(serde_json::Error),
Url(url::ParseError),
Secp256k1(secp256k1::Error),
NIP04(nip04::Error),
UnsignedEvent(crate::event::unsigned::Error),
InvalidRequest,
InvalidParamsLength,
UnsupportedMethod(String),
InvalidURI,
InvalidURIScheme,
}
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, "{e}"),
Self::JSON(e) => write!(f, "{e}"),
Self::Url(e) => write!(f, "{e}"),
Self::Secp256k1(e) => write!(f, "{e}"),
Self::NIP04(e) => write!(f, "{e}"),
Self::UnsignedEvent(e) => write!(f, "{e}"),
Self::InvalidRequest => write!(f, "Invalid NIP47 Request"),
Self::InvalidParamsLength => write!(f, "Invalid NIP47 Params length"),
Self::UnsupportedMethod(e) => write!(f, "{e}"),
Self::InvalidURI => write!(f, "Invalid NIP47 URI"),
Self::InvalidURIScheme => write!(f, "Invalid NIP47 URI Scheme"),
}
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Self::JSON(e)
}
}
impl From<url::ParseError> for Error {
fn from(e: url::ParseError) -> Self {
Self::Url(e)
}
}
impl From<secp256k1::Error> for Error {
fn from(e: secp256k1::Error) -> Self {
Self::Secp256k1(e)
}
}
impl From<key::Error> for Error {
fn from(e: key::Error) -> Self {
Self::Key(e)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ErrorCode {
#[serde(rename = "RATE_LIMITED")]
RateLimited,
#[serde(rename = "NOT_IMPLEMENTED")]
NotImplemented,
#[serde(rename = "INSUFFICIENT_BALANCE")]
InsufficantBalance,
#[serde(rename = "QUOTA_EXCEEDED")]
QuotaExceeded,
#[serde(rename = "RESTRICTED")]
Restricted,
#[serde(rename = "UNAUTHORIZED")]
Unauthorized,
#[serde(rename = "INTERNAL")]
Internal,
#[serde(rename = "OTHER")]
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NIP47Error {
pub code: ErrorCode,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Method {
#[serde(rename = "pay_invoice")]
PayInvoice,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RequestParams {
pub invoice: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Request {
pub method: Method,
pub params: RequestParams,
}
impl Request {
pub fn as_json(&self) -> String {
json!(self).to_string()
}
pub fn from_json<S>(json: S) -> Result<Self, Error>
where
S: AsRef<str>,
{
match serde_json::from_str(json.as_ref()) {
Ok(response) => Ok(response),
Err(_err) => {
let json = json.as_ref().replace('\\', "");
Ok(serde_json::from_str(&json)?)
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseResult {
pub preimage: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
pub result_type: Method,
pub error: Option<NIP47Error>,
pub result: Option<ResponseResult>,
}
impl Response {
pub fn as_json(&self) -> String {
json!(self).to_string()
}
pub fn from_json<S>(json: S) -> Result<Self, Error>
where
S: AsRef<str>,
{
match serde_json::from_str(json.as_ref()) {
Ok(response) => Ok(response),
Err(_err) => {
let json = json.as_ref().replace('\\', "");
Ok(serde_json::from_str(&json)?)
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct NostrWalletConnectInfo {}
fn url_encode<T>(data: T) -> String
where
T: AsRef<[u8]>,
{
byte_serialize(data.as_ref()).collect()
}
pub const NOSTR_WALLET_CONNECT_URI_SCHEME: &str = "nostr+walletconnect";
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct NostrWalletConnectURI {
pub public_key: XOnlyPublicKey,
pub relay_url: Url,
pub secret: SecretKey,
pub lud16: Option<String>,
}
impl NostrWalletConnectURI {
pub fn new(
public_key: XOnlyPublicKey,
relay_url: Url,
secret: Option<SecretKey>,
lud16: Option<String>,
) -> Result<Self, Error> {
let secret = match secret {
Some(secret) => secret,
None => {
let keys = Keys::generate();
keys.secret_key()?
}
};
Ok(Self {
public_key,
relay_url,
secret,
lud16,
})
}
}
impl FromStr for NostrWalletConnectURI {
type Err = Error;
fn from_str(uri: &str) -> Result<Self, Self::Err> {
let url = Url::parse(uri)?;
if url.scheme() != NOSTR_WALLET_CONNECT_URI_SCHEME {
return Err(Error::InvalidURIScheme);
}
if let Some(pubkey) = url.domain() {
let public_key = XOnlyPublicKey::from_str(pubkey)?;
let mut relay_url: Option<Url> = None;
let mut secret: Option<SecretKey> = None;
let mut lud16: Option<String> = 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("secret") => {
let value = value.to_string();
secret = Some(SecretKey::from_str(&value)?);
}
Cow::Borrowed("lud16") => {
lud16 = Some(value.to_string());
}
_ => (),
}
}
if let Some(relay_url) = relay_url {
if let Some(secret) = secret {
return Ok(Self {
public_key,
relay_url,
secret,
lud16,
});
}
}
}
Err(Error::InvalidURI)
}
}
impl fmt::Display for NostrWalletConnectURI {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{NOSTR_WALLET_CONNECT_URI_SCHEME}://{}?relay={}&secret={}",
self.public_key,
url_encode(self.relay_url.to_string()),
url_encode(self.secret.display_secret().to_string())
)?;
if let Some(lud16) = &self.lud16 {
write!(f, "&lud16={}", url_encode(lud16))?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use super::*;
use crate::{key::FromSkStr, Result};
#[test]
fn test_uri() -> Result<()> {
let pubkey = XOnlyPublicKey::from_str(
"b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4",
)?;
let relay_url = Url::parse("wss://relay.damus.io")?;
let secret =
Keys::from_sk_str("71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c")?;
let uri = NostrWalletConnectURI::new(
pubkey,
relay_url,
Some(secret.secret_key()?),
Some("nostr@nostr.com".to_string()),
)?;
assert_eq!(
uri.to_string(),
"nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io%2F&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c&lud16=nostr%40nostr.com".to_string()
);
Ok(())
}
#[test]
fn test_parse_uri() -> Result<()> {
let uri = "nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io%2F&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c&lud16=nostr%40nostr.com";
let uri = NostrWalletConnectURI::from_str(uri)?;
let pubkey = XOnlyPublicKey::from_str(
"b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4",
)?;
let relay_url = Url::parse("wss://relay.damus.io")?;
let secret =
Keys::from_sk_str("71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c")?;
assert_eq!(
uri,
NostrWalletConnectURI::new(
pubkey,
relay_url,
Some(secret.secret_key()?),
Some("nostr@nostr.com".to_string())
)
.unwrap()
);
Ok(())
}
#[test]
fn seralize_request() -> Result<()> {
let request = Request {
method: Method::PayInvoice,
params: RequestParams { invoice: "lnbc210n1pj99rx0pp5ehevgz9nf7d97h05fgkdeqxzytm6yuxd7048axru03fpzxxvzt7shp5gv7ef0s26pw5gy5dpwvsh6qgc8se8x2lmz2ev90l9vjqzcns6u6scqzzsxqyz5vqsp".to_string() }
};
assert_eq!(Request::from_json(request.as_json()).unwrap(), request);
assert_eq!(request.as_json(), "{\"method\":\"pay_invoice\",\"params\":{\"invoice\":\"lnbc210n1pj99rx0pp5ehevgz9nf7d97h05fgkdeqxzytm6yuxd7048axru03fpzxxvzt7shp5gv7ef0s26pw5gy5dpwvsh6qgc8se8x2lmz2ev90l9vjqzcns6u6scqzzsxqyz5vqsp\"}}");
Ok(())
}
#[test]
fn test_parse_request() -> Result<()> {
let request = "{\\\"params\\\":{\\\"invoice\\\":\\\"lnbc210n1pj99rx0pp5ehevgz9nf7d97h05fgkdeqxzytm6yuxd7048axru03fpzxxvzt7shp5gv7ef0s26pw5gy5dpwvsh6qgc8se8x2lmz2ev90l9vjqzcns6u6scqzzsxqyz5vqsp5rdjyt9jr2avv2runy330766avkweqp30ndnyt9x6dp5juzn7q0nq9qyyssq2mykpgu04q0hlga228kx9v95meaqzk8a9cnvya305l4c353u3h04azuh9hsmd503x6jlzjrsqzark5dxx30s46vuatwzjhzmkt3j4tgqu35rms\\\"},\\\"method\\\":\\\"pay_invoice\\\"}";
let request = Request::from_json(request).unwrap();
assert_eq!(request.method, Method::PayInvoice);
assert_eq!(request.params.invoice, "lnbc210n1pj99rx0pp5ehevgz9nf7d97h05fgkdeqxzytm6yuxd7048axru03fpzxxvzt7shp5gv7ef0s26pw5gy5dpwvsh6qgc8se8x2lmz2ev90l9vjqzcns6u6scqzzsxqyz5vqsp5rdjyt9jr2avv2runy330766avkweqp30ndnyt9x6dp5juzn7q0nq9qyyssq2mykpgu04q0hlga228kx9v95meaqzk8a9cnvya305l4c353u3h04azuh9hsmd503x6jlzjrsqzark5dxx30s46vuatwzjhzmkt3j4tgqu35rms".to_string());
Ok(())
}
}