| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675 |
- //! NUT-18: Payment Requests
- //!
- //! <https://github.com/cashubtc/nuts/blob/main/18.md>
- use std::fmt;
- use std::str::FromStr;
- use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
- use bitcoin::base64::{alphabet, Engine};
- use serde::{Deserialize, Serialize};
- use super::{Error, Nut10SecretRequest, Transport};
- use crate::mint_url::MintUrl;
- use crate::nuts::{CurrencyUnit, Proofs};
- use crate::Amount;
- const PAYMENT_REQUEST_PREFIX: &str = "creqA";
- /// Payment Request
- #[derive(Debug, Clone, Hash, 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")]
- #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::default")]
- pub transports: Vec<Transport>,
- /// Nut10
- #[serde(skip_serializing_if = "Option::is_none")]
- pub nut10: Option<Nut10SecretRequest>,
- }
- impl PaymentRequest {
- /// Create a new PaymentRequestBuilder
- pub fn builder() -> PaymentRequestBuilder {
- PaymentRequestBuilder::default()
- }
- }
- impl AsRef<Option<String>> for PaymentRequest {
- fn as_ref(&self) -> &Option<String> {
- &self.payment_id
- }
- }
- 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[..])?)
- }
- }
- /// Builder for PaymentRequest
- #[derive(Debug, Default, Clone)]
- pub struct PaymentRequestBuilder {
- payment_id: Option<String>,
- amount: Option<Amount>,
- unit: Option<CurrencyUnit>,
- single_use: Option<bool>,
- mints: Option<Vec<MintUrl>>,
- description: Option<String>,
- transports: Vec<Transport>,
- nut10: Option<Nut10SecretRequest>,
- }
- impl PaymentRequestBuilder {
- /// Set payment ID
- pub fn payment_id<S>(mut self, payment_id: S) -> Self
- where
- S: Into<String>,
- {
- self.payment_id = Some(payment_id.into());
- self
- }
- /// Set amount
- pub fn amount<A>(mut self, amount: A) -> Self
- where
- A: Into<Amount>,
- {
- self.amount = Some(amount.into());
- self
- }
- /// Set unit
- pub fn unit(mut self, unit: CurrencyUnit) -> Self {
- self.unit = Some(unit);
- self
- }
- /// Set single use flag
- pub fn single_use(mut self, single_use: bool) -> Self {
- self.single_use = Some(single_use);
- self
- }
- /// Add a mint URL
- pub fn add_mint(mut self, mint_url: MintUrl) -> Self {
- self.mints.get_or_insert_with(Vec::new).push(mint_url);
- self
- }
- /// Set mints
- pub fn mints(mut self, mints: Vec<MintUrl>) -> Self {
- self.mints = Some(mints);
- self
- }
- /// Set description
- pub fn description<S: Into<String>>(mut self, description: S) -> Self {
- self.description = Some(description.into());
- self
- }
- /// Add a transport
- pub fn add_transport(mut self, transport: Transport) -> Self {
- self.transports.push(transport);
- self
- }
- /// Set transports
- pub fn transports(mut self, transports: Vec<Transport>) -> Self {
- self.transports = transports;
- self
- }
- /// Set Nut10 secret
- pub fn nut10(mut self, nut10: Nut10SecretRequest) -> Self {
- self.nut10 = Some(nut10);
- self
- }
- /// Build the PaymentRequest
- pub fn build(self) -> PaymentRequest {
- PaymentRequest {
- payment_id: self.payment_id,
- amount: self.amount,
- unit: self.unit,
- single_use: self.single_use,
- mints: self.mints,
- description: self.description,
- transports: self.transports,
- nut10: self.nut10,
- }
- }
- }
- /// Payment Request
- #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
- pub struct PaymentRequestPayload {
- /// Id
- pub id: Option<String>,
- /// Memo
- pub memo: Option<String>,
- /// Mint
- pub mint: MintUrl,
- /// Unit
- pub unit: CurrencyUnit,
- /// Proofs
- pub proofs: Proofs,
- }
- #[cfg(test)]
- mod tests {
- use std::str::FromStr;
- use lightning_invoice::Bolt11Invoice;
- use super::*;
- use crate::nuts::nut10::Kind;
- use crate::nuts::SpendingConditions;
- use crate::TransportType;
- const PAYMENT_REQUEST: &str = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
- #[test]
- fn test_decode_payment_req() {
- let req = PaymentRequest::from_str(PAYMENT_REQUEST).expect("valid payment request");
- assert_eq!(&req.payment_id.unwrap(), "b7a90176");
- assert_eq!(req.amount.unwrap(), 10.into());
- assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
- assert_eq!(
- req.mints.unwrap(),
- vec![MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url")]
- );
- assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
- let transport = req.transports.first().unwrap();
- let expected_transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
- assert_eq!(transport, &expected_transport);
- }
- #[test]
- fn test_roundtrip_payment_req() {
- let transport = Transport {_type: TransportType::Nostr, 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()
- .expect("valid mint url")]),
- description: None,
- transports: vec![transport.clone()],
- nut10: None,
- };
- let request_str = request.to_string();
- let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
- assert_eq!(&req.payment_id.unwrap(), "b7a90176");
- assert_eq!(req.amount.unwrap(), 10.into());
- assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
- assert_eq!(
- req.mints.unwrap(),
- vec![MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url")]
- );
- assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
- let t = req.transports.first().unwrap();
- assert_eq!(&transport, t);
- }
- #[test]
- fn test_payment_request_builder() {
- let transport = Transport {
- _type: TransportType::Nostr,
- target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(),
- tags: Some(vec![vec!["n".to_string(), "17".to_string()]])
- };
- let mint_url =
- MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url");
- // Build a payment request using the builder pattern
- let request = PaymentRequest::builder()
- .payment_id("b7a90176")
- .amount(Amount::from(10))
- .unit(CurrencyUnit::Sat)
- .add_mint(mint_url.clone())
- .add_transport(transport.clone())
- .build();
- // Verify the built request
- assert_eq!(&request.payment_id.clone().unwrap(), "b7a90176");
- assert_eq!(request.amount.unwrap(), 10.into());
- assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
- assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
- let t = request.transports.first().unwrap();
- assert_eq!(&transport, t);
- // Test serialization and deserialization
- let request_str = request.to_string();
- let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
- assert_eq!(req.payment_id, request.payment_id);
- assert_eq!(req.amount, request.amount);
- assert_eq!(req.unit, request.unit);
- }
- #[test]
- fn test_transport_builder() {
- // Build a transport using the builder pattern
- let transport = Transport::builder()
- .transport_type(TransportType::Nostr)
- .target("nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5")
- .add_tag(vec!["n".to_string(), "17".to_string()])
- .build()
- .expect("Valid transport");
- // Verify the built transport
- assert_eq!(transport._type, TransportType::Nostr);
- assert_eq!(transport.target, "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5");
- assert_eq!(
- transport.tags,
- Some(vec![vec!["n".to_string(), "17".to_string()]])
- );
- // Test error case - missing required fields
- let result = crate::nuts::nut18::transport::TransportBuilder::default().build();
- assert!(result.is_err());
- }
- #[test]
- fn test_nut10_secret_request() {
- use crate::nuts::nut10::Kind;
- // Create a Nut10SecretRequest
- let secret_request = Nut10SecretRequest::new(
- Kind::P2PK,
- "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
- Some(vec![vec!["key".to_string(), "value".to_string()]]),
- );
- // Convert to a full Nut10Secret
- let full_secret: crate::nuts::Nut10Secret = secret_request.clone().into();
- // Check conversion
- assert_eq!(full_secret.kind(), Kind::P2PK);
- assert_eq!(
- full_secret.secret_data().data(),
- "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
- );
- assert_eq!(
- full_secret.secret_data().tags().clone(),
- Some(vec![vec!["key".to_string(), "value".to_string()]]).as_ref()
- );
- // Convert back to Nut10SecretRequest
- let converted_back = Nut10SecretRequest::from(full_secret);
- // Check round-trip conversion
- assert_eq!(converted_back.kind, secret_request.kind);
- assert_eq!(converted_back.data, secret_request.data);
- assert_eq!(converted_back.tags, secret_request.tags);
- // Test in PaymentRequest builder
- let payment_request = PaymentRequest::builder()
- .payment_id("test123")
- .amount(Amount::from(100))
- .nut10(secret_request.clone())
- .build();
- assert_eq!(payment_request.nut10, Some(secret_request));
- }
- #[test]
- fn test_nut10_secret_request_multiple_mints() {
- let mint_urls = [
- "https://8333.space:3338",
- "https://mint.minibits.cash/Bitcoin",
- "https://antifiat.cash",
- "https://mint.macadamia.cash",
- ]
- .iter()
- .map(|m| MintUrl::from_str(m).unwrap())
- .collect();
- let payment_request = PaymentRequestBuilder::default()
- .unit(CurrencyUnit::Sat)
- .amount(10)
- .mints(mint_urls)
- .build();
- let payment_request_str = payment_request.to_string();
- let r = PaymentRequest::from_str(&payment_request_str).unwrap();
- assert_eq!(payment_request, r);
- }
- #[test]
- fn test_nut10_secret_request_htlc() {
- let bolt11 = "lnbc100n1p5z3a63pp56854ytysg7e5z9fl3w5mgvrlqjfcytnjv8ff5hm5qt6gl6alxesqdqqcqzzsxqyz5vqsp5p0x0dlhn27s63j4emxnk26p7f94u0lyarnfp5yqmac9gzy4ngdss9qxpqysgqne3v0hnzt2lp0hc69xpzckk0cdcar7glvjhq60lsrfe8gejdm8c564prrnsft6ctxxyrewp4jtezrq3gxxqnfjj0f9tw2qs9y0lslmqpfu7et9";
- let bolt11 = Bolt11Invoice::from_str(bolt11).unwrap();
- let nut10 = SpendingConditions::HTLCConditions {
- data: *bolt11.payment_hash(),
- conditions: None,
- };
- let payment_request = PaymentRequestBuilder::default()
- .unit(CurrencyUnit::Sat)
- .amount(10)
- .nut10(nut10.into())
- .build();
- let payment_request_str = payment_request.to_string();
- let r = PaymentRequest::from_str(&payment_request_str).unwrap();
- assert_eq!(payment_request, r);
- }
- #[test]
- fn test_nut10_secret_request_p2pk() {
- // Use a public key for P2PK condition
- let pubkey_hex = "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198";
- // Create P2PK spending conditions
- let nut10 = SpendingConditions::P2PKConditions {
- data: crate::nuts::PublicKey::from_str(pubkey_hex).unwrap(),
- conditions: None,
- };
- // Build payment request with P2PK condition
- let payment_request = PaymentRequestBuilder::default()
- .unit(CurrencyUnit::Sat)
- .amount(10)
- .payment_id("test-p2pk-id")
- .description("P2PK locked payment")
- .nut10(nut10.into())
- .build();
- // Convert to string representation
- let payment_request_str = payment_request.to_string();
- // Parse back from string
- let decoded_request = PaymentRequest::from_str(&payment_request_str).unwrap();
- // Verify round-trip serialization
- assert_eq!(payment_request, decoded_request);
- // Verify the P2PK data was preserved correctly
- if let Some(nut10_secret) = decoded_request.nut10 {
- assert_eq!(nut10_secret.kind, Kind::P2PK);
- assert_eq!(nut10_secret.data, pubkey_hex);
- } else {
- panic!("NUT10 secret data missing in decoded payment request");
- }
- }
- /// Test vectors from NUT-18 specification
- /// https://github.com/cashubtc/nuts/blob/main/tests/18-tests.md
- #[test]
- fn test_basic_payment_request() {
- // Basic payment request with required fields
- let json = r#"{
- "i": "b7a90176",
- "a": 10,
- "u": "sat",
- "m": ["https://8333.space:3338"],
- "t": [
- {
- "t": "nostr",
- "a": "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5",
- "g": [["n", "17"]]
- }
- ]
- }"#;
- let expected_encoded = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF3aHR0cHM6Ly84MzMzLnNwYWNlOjMzMzg=";
- // Parse the JSON into a PaymentRequest
- let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
- let payment_request_cloned = payment_request.clone();
- // Verify the payment request fields
- assert_eq!(
- payment_request_cloned.payment_id.as_ref().unwrap(),
- "b7a90176"
- );
- assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(10));
- assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
- assert_eq!(
- payment_request_cloned.mints.unwrap(),
- vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
- );
- let transport = payment_request.transports.first().unwrap();
- assert_eq!(transport._type, TransportType::Nostr);
- assert_eq!(transport.target, "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5");
- assert_eq!(
- transport.tags,
- Some(vec![vec!["n".to_string(), "17".to_string()]])
- );
- // Test encoding - the encoded form should match the expected output
- let encoded = payment_request.to_string();
- // For now, let's verify it can be decoded back correctly
- let decoded = PaymentRequest::from_str(&encoded).unwrap();
- assert_eq!(payment_request, decoded);
- // Test decoding the expected encoded string
- let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
- assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "b7a90176");
- assert_eq!(decoded_from_spec.amount.unwrap(), Amount::from(10));
- assert_eq!(decoded_from_spec.unit.unwrap(), CurrencyUnit::Sat);
- assert_eq!(
- decoded_from_spec.mints.unwrap(),
- vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
- );
- }
- #[test]
- fn test_nostr_transport_payment_request() {
- // Nostr transport payment request with multiple mints
- let json = r#"{
- "i": "f92a51b8",
- "a": 100,
- "u": "sat",
- "m": ["https://mint1.example.com", "https://mint2.example.com"],
- "t": [
- {
- "t": "nostr",
- "a": "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq28spj3",
- "g": [["n", "17"], ["n", "9735"]]
- }
- ]
- }"#;
- let expected_encoded = "creqApWF0gaNhdGVub3N0cmFheD9ucHViMXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXEyOHNwajNhZ4KCYW5iMTeCYW5kOTczNWFpaGY5MmE1MWI4YWEYZGF1Y3NhdGFtgngZaHR0cHM6Ly9taW50MS5leGFtcGxlLmNvbXgZaHR0cHM6Ly9taW50Mi5leGFtcGxlLmNvbQ==";
- // Parse the JSON into a PaymentRequest
- let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
- let payment_request_cloned = payment_request.clone();
- // Verify the payment request fields
- assert_eq!(
- payment_request_cloned.payment_id.as_ref().unwrap(),
- "f92a51b8"
- );
- assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(100));
- assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
- assert_eq!(
- payment_request_cloned.mints.unwrap(),
- vec![
- MintUrl::from_str("https://mint1.example.com").unwrap(),
- MintUrl::from_str("https://mint2.example.com").unwrap()
- ]
- );
- let transport = payment_request_cloned.transports.first().unwrap();
- assert_eq!(transport._type, TransportType::Nostr);
- assert_eq!(
- transport.target,
- "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq28spj3"
- );
- assert_eq!(
- transport.tags,
- Some(vec![
- vec!["n".to_string(), "17".to_string()],
- vec!["n".to_string(), "9735".to_string()]
- ])
- );
- // Test round-trip serialization
- let encoded = payment_request.to_string();
- let decoded = PaymentRequest::from_str(&encoded).unwrap();
- assert_eq!(payment_request, decoded);
- // Test decoding the expected encoded string
- let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
- assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "f92a51b8");
- }
- #[test]
- fn test_minimal_payment_request() {
- // Minimal payment request with only required fields
- let json = r#"{
- "i": "7f4a2b39",
- "u": "sat",
- "m": ["https://mint.example.com"]
- }"#;
- let expected_encoded =
- "creqAo2FpaDdmNGEyYjM5YXVjc2F0YW2BeBhodHRwczovL21pbnQuZXhhbXBsZS5jb20=";
- // Parse the JSON into a PaymentRequest
- let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
- let payment_request_cloned = payment_request.clone();
- // Verify the payment request fields
- assert_eq!(
- payment_request_cloned.payment_id.as_ref().unwrap(),
- "7f4a2b39"
- );
- assert_eq!(payment_request_cloned.amount, None);
- assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
- assert_eq!(
- payment_request_cloned.mints.unwrap(),
- vec![MintUrl::from_str("https://mint.example.com").unwrap()]
- );
- assert_eq!(payment_request_cloned.transports, vec![]);
- // Test round-trip serialization
- let encoded = payment_request.to_string();
- let decoded = PaymentRequest::from_str(&encoded).unwrap();
- assert_eq!(payment_request, decoded);
- // Test decoding the expected encoded string
- let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
- assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "7f4a2b39");
- }
- #[test]
- fn test_nut10_locking_payment_request() {
- // Payment request with NUT-10 P2PK locking
- let json = r#"{
- "i": "c9e45d2a",
- "a": 500,
- "u": "sat",
- "m": ["https://mint.example.com"],
- "nut10": {
- "k": "P2PK",
- "d": "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331",
- "t": [["timeout", "3600"]]
- }
- }"#;
- let expected_encoded = "creqApWFpaGM5ZTQ1ZDJhYWEZAfRhdWNzYXRhbYF4GGh0dHBzOi8vbWludC5leGFtcGxlLmNvbWVudXQxMKNha2RQMlBLYWR4QjAyYzNiNWJiMjdlMzYxNDU3YzkyZDkzZDc4ZGQ3M2QzZDUzNzMyMTEwYjJjZmU4YjUwZmJjMGFiYzYxNWU5YzMzMWF0gYJndGltZW91dGQzNjAw";
- // Parse the JSON into a PaymentRequest
- let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
- let payment_request_cloned = payment_request.clone();
- // Verify the payment request fields
- assert_eq!(
- payment_request_cloned.payment_id.as_ref().unwrap(),
- "c9e45d2a"
- );
- assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(500));
- assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
- assert_eq!(
- payment_request_cloned.mints.unwrap(),
- vec![MintUrl::from_str("https://mint.example.com").unwrap()]
- );
- // Test NUT-10 locking
- let nut10 = payment_request_cloned.nut10.unwrap();
- assert_eq!(nut10.kind, Kind::P2PK);
- assert_eq!(
- nut10.data,
- "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331"
- );
- assert_eq!(
- nut10.tags,
- Some(vec![vec!["timeout".to_string(), "3600".to_string()]])
- );
- // Test round-trip serialization
- let encoded = payment_request.to_string();
- let decoded = PaymentRequest::from_str(&encoded).unwrap();
- assert_eq!(payment_request, decoded);
- // Test decoding the expected encoded string
- let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
- assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "c9e45d2a");
- }
- }
|