mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-05 13:16:00 +01:00
feat(NUT18): Payment request
This commit is contained in:
@@ -234,6 +234,9 @@ pub enum Error {
|
||||
/// NUT14 Error
|
||||
#[error(transparent)]
|
||||
NUT14(#[from] crate::nuts::nut14::Error),
|
||||
/// NUT18 Error
|
||||
#[error(transparent)]
|
||||
NUT18(#[from] crate::nuts::nut18::Error),
|
||||
/// Database Error
|
||||
#[cfg(any(feature = "wallet", feature = "mint"))]
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod nut12;
|
||||
pub mod nut13;
|
||||
pub mod nut14;
|
||||
pub mod nut15;
|
||||
pub mod nut18;
|
||||
|
||||
pub use nut00::{
|
||||
BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof,
|
||||
|
||||
163
crates/cdk/src/nuts/nut18.rs
Normal file
163
crates/cdk/src/nuts/nut18.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
//! NUT-18: Payment Requests
|
||||
//!
|
||||
//! <https://github.com/cashubtc/nuts/blob/main/18.md>
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use bitcoin::base64::{
|
||||
alphabet,
|
||||
engine::{general_purpose, GeneralPurpose},
|
||||
Engine,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{mint_url::MintUrl, Amount};
|
||||
|
||||
use super::CurrencyUnit;
|
||||
|
||||
const PAYMENT_REQUEST_PREFIX: &str = "creqA";
|
||||
|
||||
/// NUT18 Error
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Invalid Prefix
|
||||
#[error("Invalid Prefix")]
|
||||
InvalidPrefix,
|
||||
/// Ciborium error
|
||||
#[error(transparent)]
|
||||
CiboriumError(#[from] ciborium::de::Error<std::io::Error>),
|
||||
/// Base64 error
|
||||
#[error(transparent)]
|
||||
Base64Error(#[from] bitcoin::base64::DecodeError),
|
||||
}
|
||||
|
||||
/// Transport
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Transport {
|
||||
/// Type
|
||||
#[serde(rename = "t")]
|
||||
pub _type: String,
|
||||
/// Target
|
||||
#[serde(rename = "a")]
|
||||
pub target: String,
|
||||
/// Tags
|
||||
#[serde(rename = "g")]
|
||||
pub tags: Option<Vec<Vec<String>>>,
|
||||
}
|
||||
|
||||
/// Payment Request
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PaymentRequest {
|
||||
/// `Payment id`
|
||||
#[serde(rename = "i")]
|
||||
pub payment_id: Option<String>,
|
||||
/// Amount
|
||||
#[serde(rename = "a")]
|
||||
pub amount: Option<Amount>,
|
||||
/// Unit
|
||||
#[serde(rename = "u")]
|
||||
pub unit: Option<CurrencyUnit>,
|
||||
/// Single use
|
||||
#[serde(rename = "s")]
|
||||
pub single_use: Option<bool>,
|
||||
/// Mints
|
||||
#[serde(rename = "m")]
|
||||
pub mints: Option<Vec<MintUrl>>,
|
||||
/// Description
|
||||
#[serde(rename = "d")]
|
||||
pub description: Option<String>,
|
||||
/// Transport
|
||||
#[serde(rename = "t")]
|
||||
pub transports: Vec<Transport>,
|
||||
}
|
||||
|
||||
impl fmt::Display for PaymentRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use serde::ser::Error;
|
||||
let mut data = Vec::new();
|
||||
ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
|
||||
let encoded = general_purpose::URL_SAFE.encode(data);
|
||||
write!(f, "{}{}", PAYMENT_REQUEST_PREFIX, encoded)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PaymentRequest {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s
|
||||
.strip_prefix(PAYMENT_REQUEST_PREFIX)
|
||||
.ok_or(Error::InvalidPrefix)?;
|
||||
|
||||
let decode_config = general_purpose::GeneralPurposeConfig::new()
|
||||
.with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
|
||||
let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
|
||||
|
||||
Ok(ciborium::from_reader(&decoded[..])?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
const PAYMENT_REQUEST: &str = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
|
||||
|
||||
#[test]
|
||||
fn test_decode_payment_req() -> anyhow::Result<()> {
|
||||
let req = PaymentRequest::from_str(PAYMENT_REQUEST)?;
|
||||
|
||||
assert_eq!(&req.payment_id.unwrap(), "b7a90176");
|
||||
assert_eq!(req.amount.unwrap(), 10.into());
|
||||
assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
|
||||
assert_eq!(
|
||||
req.mints.unwrap(),
|
||||
vec![MintUrl::from_str("https://nofees.testnut.cashu.space")?]
|
||||
);
|
||||
assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
|
||||
|
||||
let transport = req.transports.first().unwrap();
|
||||
|
||||
let expected_transport = Transport {_type: "nostr".to_string(), target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
|
||||
|
||||
assert_eq!(transport, &expected_transport);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_payment_req() -> anyhow::Result<()> {
|
||||
let transport = Transport {_type: "nostr".to_string(), target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
|
||||
|
||||
let request = PaymentRequest {
|
||||
payment_id: Some("b7a90176".to_string()),
|
||||
amount: Some(10.into()),
|
||||
unit: Some(CurrencyUnit::Sat),
|
||||
single_use: None,
|
||||
mints: Some(vec!["https://nofees.testnut.cashu.space".parse()?]),
|
||||
description: None,
|
||||
transports: vec![transport.clone()],
|
||||
};
|
||||
|
||||
let request_str = request.to_string();
|
||||
|
||||
let req = PaymentRequest::from_str(&request_str)?;
|
||||
|
||||
assert_eq!(&req.payment_id.unwrap(), "b7a90176");
|
||||
assert_eq!(req.amount.unwrap(), 10.into());
|
||||
assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
|
||||
assert_eq!(
|
||||
req.mints.unwrap(),
|
||||
vec![MintUrl::from_str("https://nofees.testnut.cashu.space")?]
|
||||
);
|
||||
assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
|
||||
|
||||
let t = req.transports.first().unwrap();
|
||||
assert_eq!(&transport, t);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user