payment_request.rs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. //! NUT-18: Payment Requests
  2. //!
  3. //! <https://github.com/cashubtc/nuts/blob/main/18.md>
  4. use std::fmt;
  5. use std::str::FromStr;
  6. use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
  7. use bitcoin::base64::{alphabet, Engine};
  8. use serde::{Deserialize, Serialize};
  9. use super::{Error, Nut10SecretRequest, Transport};
  10. use crate::mint_url::MintUrl;
  11. use crate::nuts::{CurrencyUnit, Proofs};
  12. use crate::Amount;
  13. const PAYMENT_REQUEST_PREFIX: &str = "creqA";
  14. /// Payment Request
  15. #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
  16. pub struct PaymentRequest {
  17. /// `Payment id`
  18. #[serde(rename = "i")]
  19. pub payment_id: Option<String>,
  20. /// Amount
  21. #[serde(rename = "a")]
  22. pub amount: Option<Amount>,
  23. /// Unit
  24. #[serde(rename = "u")]
  25. pub unit: Option<CurrencyUnit>,
  26. /// Single use
  27. #[serde(rename = "s")]
  28. pub single_use: Option<bool>,
  29. /// Mints
  30. #[serde(rename = "m")]
  31. pub mints: Option<Vec<MintUrl>>,
  32. /// Description
  33. #[serde(rename = "d")]
  34. pub description: Option<String>,
  35. /// Transport
  36. #[serde(rename = "t")]
  37. #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::default")]
  38. pub transports: Vec<Transport>,
  39. /// Nut10
  40. #[serde(skip_serializing_if = "Option::is_none")]
  41. pub nut10: Option<Nut10SecretRequest>,
  42. }
  43. impl PaymentRequest {
  44. /// Create a new PaymentRequestBuilder
  45. pub fn builder() -> PaymentRequestBuilder {
  46. PaymentRequestBuilder::default()
  47. }
  48. }
  49. impl AsRef<Option<String>> for PaymentRequest {
  50. fn as_ref(&self) -> &Option<String> {
  51. &self.payment_id
  52. }
  53. }
  54. impl fmt::Display for PaymentRequest {
  55. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  56. use serde::ser::Error;
  57. let mut data = Vec::new();
  58. ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
  59. let encoded = general_purpose::URL_SAFE.encode(data);
  60. write!(f, "{PAYMENT_REQUEST_PREFIX}{encoded}")
  61. }
  62. }
  63. impl FromStr for PaymentRequest {
  64. type Err = Error;
  65. fn from_str(s: &str) -> Result<Self, Self::Err> {
  66. let s = s
  67. .strip_prefix(PAYMENT_REQUEST_PREFIX)
  68. .ok_or(Error::InvalidPrefix)?;
  69. let decode_config = general_purpose::GeneralPurposeConfig::new()
  70. .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
  71. let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
  72. Ok(ciborium::from_reader(&decoded[..])?)
  73. }
  74. }
  75. /// Builder for PaymentRequest
  76. #[derive(Debug, Default, Clone)]
  77. pub struct PaymentRequestBuilder {
  78. payment_id: Option<String>,
  79. amount: Option<Amount>,
  80. unit: Option<CurrencyUnit>,
  81. single_use: Option<bool>,
  82. mints: Option<Vec<MintUrl>>,
  83. description: Option<String>,
  84. transports: Vec<Transport>,
  85. nut10: Option<Nut10SecretRequest>,
  86. }
  87. impl PaymentRequestBuilder {
  88. /// Set payment ID
  89. pub fn payment_id<S>(mut self, payment_id: S) -> Self
  90. where
  91. S: Into<String>,
  92. {
  93. self.payment_id = Some(payment_id.into());
  94. self
  95. }
  96. /// Set amount
  97. pub fn amount<A>(mut self, amount: A) -> Self
  98. where
  99. A: Into<Amount>,
  100. {
  101. self.amount = Some(amount.into());
  102. self
  103. }
  104. /// Set unit
  105. pub fn unit(mut self, unit: CurrencyUnit) -> Self {
  106. self.unit = Some(unit);
  107. self
  108. }
  109. /// Set single use flag
  110. pub fn single_use(mut self, single_use: bool) -> Self {
  111. self.single_use = Some(single_use);
  112. self
  113. }
  114. /// Add a mint URL
  115. pub fn add_mint(mut self, mint_url: MintUrl) -> Self {
  116. self.mints.get_or_insert_with(Vec::new).push(mint_url);
  117. self
  118. }
  119. /// Set mints
  120. pub fn mints(mut self, mints: Vec<MintUrl>) -> Self {
  121. self.mints = Some(mints);
  122. self
  123. }
  124. /// Set description
  125. pub fn description<S: Into<String>>(mut self, description: S) -> Self {
  126. self.description = Some(description.into());
  127. self
  128. }
  129. /// Add a transport
  130. pub fn add_transport(mut self, transport: Transport) -> Self {
  131. self.transports.push(transport);
  132. self
  133. }
  134. /// Set transports
  135. pub fn transports(mut self, transports: Vec<Transport>) -> Self {
  136. self.transports = transports;
  137. self
  138. }
  139. /// Set Nut10 secret
  140. pub fn nut10(mut self, nut10: Nut10SecretRequest) -> Self {
  141. self.nut10 = Some(nut10);
  142. self
  143. }
  144. /// Build the PaymentRequest
  145. pub fn build(self) -> PaymentRequest {
  146. PaymentRequest {
  147. payment_id: self.payment_id,
  148. amount: self.amount,
  149. unit: self.unit,
  150. single_use: self.single_use,
  151. mints: self.mints,
  152. description: self.description,
  153. transports: self.transports,
  154. nut10: self.nut10,
  155. }
  156. }
  157. }
  158. /// Payment Request
  159. #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
  160. pub struct PaymentRequestPayload {
  161. /// Id
  162. pub id: Option<String>,
  163. /// Memo
  164. pub memo: Option<String>,
  165. /// Mint
  166. pub mint: MintUrl,
  167. /// Unit
  168. pub unit: CurrencyUnit,
  169. /// Proofs
  170. pub proofs: Proofs,
  171. }
  172. #[cfg(test)]
  173. mod tests {
  174. use std::str::FromStr;
  175. use lightning_invoice::Bolt11Invoice;
  176. use super::*;
  177. use crate::nuts::nut10::Kind;
  178. use crate::nuts::SpendingConditions;
  179. use crate::TransportType;
  180. const PAYMENT_REQUEST: &str = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
  181. #[test]
  182. fn test_decode_payment_req() {
  183. let req = PaymentRequest::from_str(PAYMENT_REQUEST).expect("valid payment request");
  184. assert_eq!(&req.payment_id.unwrap(), "b7a90176");
  185. assert_eq!(req.amount.unwrap(), 10.into());
  186. assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
  187. assert_eq!(
  188. req.mints.unwrap(),
  189. vec![MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url")]
  190. );
  191. assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
  192. let transport = req.transports.first().unwrap();
  193. let expected_transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
  194. assert_eq!(transport, &expected_transport);
  195. }
  196. #[test]
  197. fn test_roundtrip_payment_req() {
  198. let transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
  199. let request = PaymentRequest {
  200. payment_id: Some("b7a90176".to_string()),
  201. amount: Some(10.into()),
  202. unit: Some(CurrencyUnit::Sat),
  203. single_use: None,
  204. mints: Some(vec!["https://nofees.testnut.cashu.space"
  205. .parse()
  206. .expect("valid mint url")]),
  207. description: None,
  208. transports: vec![transport.clone()],
  209. nut10: None,
  210. };
  211. let request_str = request.to_string();
  212. let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
  213. assert_eq!(&req.payment_id.unwrap(), "b7a90176");
  214. assert_eq!(req.amount.unwrap(), 10.into());
  215. assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
  216. assert_eq!(
  217. req.mints.unwrap(),
  218. vec![MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url")]
  219. );
  220. assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
  221. let t = req.transports.first().unwrap();
  222. assert_eq!(&transport, t);
  223. }
  224. #[test]
  225. fn test_payment_request_builder() {
  226. let transport = Transport {
  227. _type: TransportType::Nostr,
  228. target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(),
  229. tags: Some(vec![vec!["n".to_string(), "17".to_string()]])
  230. };
  231. let mint_url =
  232. MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url");
  233. // Build a payment request using the builder pattern
  234. let request = PaymentRequest::builder()
  235. .payment_id("b7a90176")
  236. .amount(Amount::from(10))
  237. .unit(CurrencyUnit::Sat)
  238. .add_mint(mint_url.clone())
  239. .add_transport(transport.clone())
  240. .build();
  241. // Verify the built request
  242. assert_eq!(&request.payment_id.clone().unwrap(), "b7a90176");
  243. assert_eq!(request.amount.unwrap(), 10.into());
  244. assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
  245. assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
  246. let t = request.transports.first().unwrap();
  247. assert_eq!(&transport, t);
  248. // Test serialization and deserialization
  249. let request_str = request.to_string();
  250. let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
  251. assert_eq!(req.payment_id, request.payment_id);
  252. assert_eq!(req.amount, request.amount);
  253. assert_eq!(req.unit, request.unit);
  254. }
  255. #[test]
  256. fn test_transport_builder() {
  257. // Build a transport using the builder pattern
  258. let transport = Transport::builder()
  259. .transport_type(TransportType::Nostr)
  260. .target("nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5")
  261. .add_tag(vec!["n".to_string(), "17".to_string()])
  262. .build()
  263. .expect("Valid transport");
  264. // Verify the built transport
  265. assert_eq!(transport._type, TransportType::Nostr);
  266. assert_eq!(transport.target, "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5");
  267. assert_eq!(
  268. transport.tags,
  269. Some(vec![vec!["n".to_string(), "17".to_string()]])
  270. );
  271. // Test error case - missing required fields
  272. let result = crate::nuts::nut18::transport::TransportBuilder::default().build();
  273. assert!(result.is_err());
  274. }
  275. #[test]
  276. fn test_nut10_secret_request() {
  277. use crate::nuts::nut10::Kind;
  278. // Create a Nut10SecretRequest
  279. let secret_request = Nut10SecretRequest::new(
  280. Kind::P2PK,
  281. "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
  282. Some(vec![vec!["key".to_string(), "value".to_string()]]),
  283. );
  284. // Convert to a full Nut10Secret
  285. let full_secret: crate::nuts::Nut10Secret = secret_request.clone().into();
  286. // Check conversion
  287. assert_eq!(full_secret.kind(), Kind::P2PK);
  288. assert_eq!(
  289. full_secret.secret_data().data(),
  290. "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
  291. );
  292. assert_eq!(
  293. full_secret.secret_data().tags().clone(),
  294. Some(vec![vec!["key".to_string(), "value".to_string()]]).as_ref()
  295. );
  296. // Convert back to Nut10SecretRequest
  297. let converted_back = Nut10SecretRequest::from(full_secret);
  298. // Check round-trip conversion
  299. assert_eq!(converted_back.kind, secret_request.kind);
  300. assert_eq!(converted_back.data, secret_request.data);
  301. assert_eq!(converted_back.tags, secret_request.tags);
  302. // Test in PaymentRequest builder
  303. let payment_request = PaymentRequest::builder()
  304. .payment_id("test123")
  305. .amount(Amount::from(100))
  306. .nut10(secret_request.clone())
  307. .build();
  308. assert_eq!(payment_request.nut10, Some(secret_request));
  309. }
  310. #[test]
  311. fn test_nut10_secret_request_multiple_mints() {
  312. let mint_urls = [
  313. "https://8333.space:3338",
  314. "https://mint.minibits.cash/Bitcoin",
  315. "https://antifiat.cash",
  316. "https://mint.macadamia.cash",
  317. ]
  318. .iter()
  319. .map(|m| MintUrl::from_str(m).unwrap())
  320. .collect();
  321. let payment_request = PaymentRequestBuilder::default()
  322. .unit(CurrencyUnit::Sat)
  323. .amount(10)
  324. .mints(mint_urls)
  325. .build();
  326. let payment_request_str = payment_request.to_string();
  327. let r = PaymentRequest::from_str(&payment_request_str).unwrap();
  328. assert_eq!(payment_request, r);
  329. }
  330. #[test]
  331. fn test_nut10_secret_request_htlc() {
  332. let bolt11 = "lnbc100n1p5z3a63pp56854ytysg7e5z9fl3w5mgvrlqjfcytnjv8ff5hm5qt6gl6alxesqdqqcqzzsxqyz5vqsp5p0x0dlhn27s63j4emxnk26p7f94u0lyarnfp5yqmac9gzy4ngdss9qxpqysgqne3v0hnzt2lp0hc69xpzckk0cdcar7glvjhq60lsrfe8gejdm8c564prrnsft6ctxxyrewp4jtezrq3gxxqnfjj0f9tw2qs9y0lslmqpfu7et9";
  333. let bolt11 = Bolt11Invoice::from_str(bolt11).unwrap();
  334. let nut10 = SpendingConditions::HTLCConditions {
  335. data: *bolt11.payment_hash(),
  336. conditions: None,
  337. };
  338. let payment_request = PaymentRequestBuilder::default()
  339. .unit(CurrencyUnit::Sat)
  340. .amount(10)
  341. .nut10(nut10.into())
  342. .build();
  343. let payment_request_str = payment_request.to_string();
  344. let r = PaymentRequest::from_str(&payment_request_str).unwrap();
  345. assert_eq!(payment_request, r);
  346. }
  347. #[test]
  348. fn test_nut10_secret_request_p2pk() {
  349. // Use a public key for P2PK condition
  350. let pubkey_hex = "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198";
  351. // Create P2PK spending conditions
  352. let nut10 = SpendingConditions::P2PKConditions {
  353. data: crate::nuts::PublicKey::from_str(pubkey_hex).unwrap(),
  354. conditions: None,
  355. };
  356. // Build payment request with P2PK condition
  357. let payment_request = PaymentRequestBuilder::default()
  358. .unit(CurrencyUnit::Sat)
  359. .amount(10)
  360. .payment_id("test-p2pk-id")
  361. .description("P2PK locked payment")
  362. .nut10(nut10.into())
  363. .build();
  364. // Convert to string representation
  365. let payment_request_str = payment_request.to_string();
  366. // Parse back from string
  367. let decoded_request = PaymentRequest::from_str(&payment_request_str).unwrap();
  368. // Verify round-trip serialization
  369. assert_eq!(payment_request, decoded_request);
  370. // Verify the P2PK data was preserved correctly
  371. if let Some(nut10_secret) = decoded_request.nut10 {
  372. assert_eq!(nut10_secret.kind, Kind::P2PK);
  373. assert_eq!(nut10_secret.data, pubkey_hex);
  374. } else {
  375. panic!("NUT10 secret data missing in decoded payment request");
  376. }
  377. }
  378. /// Test vectors from NUT-18 specification
  379. /// https://github.com/cashubtc/nuts/blob/main/tests/18-tests.md
  380. #[test]
  381. fn test_basic_payment_request() {
  382. // Basic payment request with required fields
  383. let json = r#"{
  384. "i": "b7a90176",
  385. "a": 10,
  386. "u": "sat",
  387. "m": ["https://8333.space:3338"],
  388. "t": [
  389. {
  390. "t": "nostr",
  391. "a": "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5",
  392. "g": [["n", "17"]]
  393. }
  394. ]
  395. }"#;
  396. let expected_encoded = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF3aHR0cHM6Ly84MzMzLnNwYWNlOjMzMzg=";
  397. // Parse the JSON into a PaymentRequest
  398. let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
  399. let payment_request_cloned = payment_request.clone();
  400. // Verify the payment request fields
  401. assert_eq!(
  402. payment_request_cloned.payment_id.as_ref().unwrap(),
  403. "b7a90176"
  404. );
  405. assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(10));
  406. assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
  407. assert_eq!(
  408. payment_request_cloned.mints.unwrap(),
  409. vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
  410. );
  411. let transport = payment_request.transports.first().unwrap();
  412. assert_eq!(transport._type, TransportType::Nostr);
  413. assert_eq!(transport.target, "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5");
  414. assert_eq!(
  415. transport.tags,
  416. Some(vec![vec!["n".to_string(), "17".to_string()]])
  417. );
  418. // Test encoding - the encoded form should match the expected output
  419. let encoded = payment_request.to_string();
  420. // For now, let's verify it can be decoded back correctly
  421. let decoded = PaymentRequest::from_str(&encoded).unwrap();
  422. assert_eq!(payment_request, decoded);
  423. // Test decoding the expected encoded string
  424. let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
  425. assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "b7a90176");
  426. assert_eq!(decoded_from_spec.amount.unwrap(), Amount::from(10));
  427. assert_eq!(decoded_from_spec.unit.unwrap(), CurrencyUnit::Sat);
  428. assert_eq!(
  429. decoded_from_spec.mints.unwrap(),
  430. vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
  431. );
  432. }
  433. #[test]
  434. fn test_nostr_transport_payment_request() {
  435. // Nostr transport payment request with multiple mints
  436. let json = r#"{
  437. "i": "f92a51b8",
  438. "a": 100,
  439. "u": "sat",
  440. "m": ["https://mint1.example.com", "https://mint2.example.com"],
  441. "t": [
  442. {
  443. "t": "nostr",
  444. "a": "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq28spj3",
  445. "g": [["n", "17"], ["n", "9735"]]
  446. }
  447. ]
  448. }"#;
  449. let expected_encoded = "creqApWF0gaNhdGVub3N0cmFheD9ucHViMXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXEyOHNwajNhZ4KCYW5iMTeCYW5kOTczNWFpaGY5MmE1MWI4YWEYZGF1Y3NhdGFtgngZaHR0cHM6Ly9taW50MS5leGFtcGxlLmNvbXgZaHR0cHM6Ly9taW50Mi5leGFtcGxlLmNvbQ==";
  450. // Parse the JSON into a PaymentRequest
  451. let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
  452. let payment_request_cloned = payment_request.clone();
  453. // Verify the payment request fields
  454. assert_eq!(
  455. payment_request_cloned.payment_id.as_ref().unwrap(),
  456. "f92a51b8"
  457. );
  458. assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(100));
  459. assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
  460. assert_eq!(
  461. payment_request_cloned.mints.unwrap(),
  462. vec![
  463. MintUrl::from_str("https://mint1.example.com").unwrap(),
  464. MintUrl::from_str("https://mint2.example.com").unwrap()
  465. ]
  466. );
  467. let transport = payment_request_cloned.transports.first().unwrap();
  468. assert_eq!(transport._type, TransportType::Nostr);
  469. assert_eq!(
  470. transport.target,
  471. "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq28spj3"
  472. );
  473. assert_eq!(
  474. transport.tags,
  475. Some(vec![
  476. vec!["n".to_string(), "17".to_string()],
  477. vec!["n".to_string(), "9735".to_string()]
  478. ])
  479. );
  480. // Test round-trip serialization
  481. let encoded = payment_request.to_string();
  482. let decoded = PaymentRequest::from_str(&encoded).unwrap();
  483. assert_eq!(payment_request, decoded);
  484. // Test decoding the expected encoded string
  485. let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
  486. assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "f92a51b8");
  487. }
  488. #[test]
  489. fn test_minimal_payment_request() {
  490. // Minimal payment request with only required fields
  491. let json = r#"{
  492. "i": "7f4a2b39",
  493. "u": "sat",
  494. "m": ["https://mint.example.com"]
  495. }"#;
  496. let expected_encoded =
  497. "creqAo2FpaDdmNGEyYjM5YXVjc2F0YW2BeBhodHRwczovL21pbnQuZXhhbXBsZS5jb20=";
  498. // Parse the JSON into a PaymentRequest
  499. let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
  500. let payment_request_cloned = payment_request.clone();
  501. // Verify the payment request fields
  502. assert_eq!(
  503. payment_request_cloned.payment_id.as_ref().unwrap(),
  504. "7f4a2b39"
  505. );
  506. assert_eq!(payment_request_cloned.amount, None);
  507. assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
  508. assert_eq!(
  509. payment_request_cloned.mints.unwrap(),
  510. vec![MintUrl::from_str("https://mint.example.com").unwrap()]
  511. );
  512. assert_eq!(payment_request_cloned.transports, vec![]);
  513. // Test round-trip serialization
  514. let encoded = payment_request.to_string();
  515. let decoded = PaymentRequest::from_str(&encoded).unwrap();
  516. assert_eq!(payment_request, decoded);
  517. // Test decoding the expected encoded string
  518. let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
  519. assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "7f4a2b39");
  520. }
  521. #[test]
  522. fn test_nut10_locking_payment_request() {
  523. // Payment request with NUT-10 P2PK locking
  524. let json = r#"{
  525. "i": "c9e45d2a",
  526. "a": 500,
  527. "u": "sat",
  528. "m": ["https://mint.example.com"],
  529. "nut10": {
  530. "k": "P2PK",
  531. "d": "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331",
  532. "t": [["timeout", "3600"]]
  533. }
  534. }"#;
  535. let expected_encoded = "creqApWFpaGM5ZTQ1ZDJhYWEZAfRhdWNzYXRhbYF4GGh0dHBzOi8vbWludC5leGFtcGxlLmNvbWVudXQxMKNha2RQMlBLYWR4QjAyYzNiNWJiMjdlMzYxNDU3YzkyZDkzZDc4ZGQ3M2QzZDUzNzMyMTEwYjJjZmU4YjUwZmJjMGFiYzYxNWU5YzMzMWF0gYJndGltZW91dGQzNjAw";
  536. // Parse the JSON into a PaymentRequest
  537. let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
  538. let payment_request_cloned = payment_request.clone();
  539. // Verify the payment request fields
  540. assert_eq!(
  541. payment_request_cloned.payment_id.as_ref().unwrap(),
  542. "c9e45d2a"
  543. );
  544. assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(500));
  545. assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
  546. assert_eq!(
  547. payment_request_cloned.mints.unwrap(),
  548. vec![MintUrl::from_str("https://mint.example.com").unwrap()]
  549. );
  550. // Test NUT-10 locking
  551. let nut10 = payment_request_cloned.nut10.unwrap();
  552. assert_eq!(nut10.kind, Kind::P2PK);
  553. assert_eq!(
  554. nut10.data,
  555. "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331"
  556. );
  557. assert_eq!(
  558. nut10.tags,
  559. Some(vec![vec!["timeout".to_string(), "3600".to_string()]])
  560. );
  561. // Test round-trip serialization
  562. let encoded = payment_request.to_string();
  563. let decoded = PaymentRequest::from_str(&encoded).unwrap();
  564. assert_eq!(payment_request, decoded);
  565. // Test decoding the expected encoded string
  566. let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
  567. assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "c9e45d2a");
  568. }
  569. }