nut18.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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 thiserror::Error;
  10. use super::{CurrencyUnit, Proofs};
  11. use crate::mint_url::MintUrl;
  12. use crate::Amount;
  13. const PAYMENT_REQUEST_PREFIX: &str = "creqA";
  14. /// NUT18 Error
  15. #[derive(Debug, Error)]
  16. pub enum Error {
  17. /// Invalid Prefix
  18. #[error("Invalid Prefix")]
  19. InvalidPrefix,
  20. /// Ciborium error
  21. #[error(transparent)]
  22. CiboriumError(#[from] ciborium::de::Error<std::io::Error>),
  23. /// Base64 error
  24. #[error(transparent)]
  25. Base64Error(#[from] bitcoin::base64::DecodeError),
  26. }
  27. /// Transport Type
  28. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
  29. pub enum TransportType {
  30. /// Nostr
  31. #[serde(rename = "nostr")]
  32. Nostr,
  33. /// Http post
  34. #[serde(rename = "post")]
  35. HttpPost,
  36. }
  37. impl fmt::Display for TransportType {
  38. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  39. use serde::ser::Error;
  40. let t = serde_json::to_string(self).map_err(|e| fmt::Error::custom(e.to_string()))?;
  41. write!(f, "{}", t)
  42. }
  43. }
  44. impl FromStr for TransportType {
  45. type Err = Error;
  46. fn from_str(s: &str) -> Result<Self, Self::Err> {
  47. match s.to_lowercase().as_str() {
  48. "nostr" => Ok(Self::Nostr),
  49. "post" => Ok(Self::HttpPost),
  50. _ => Err(Error::InvalidPrefix),
  51. }
  52. }
  53. }
  54. impl FromStr for Transport {
  55. type Err = Error;
  56. fn from_str(s: &str) -> Result<Self, Self::Err> {
  57. let decode_config = general_purpose::GeneralPurposeConfig::new()
  58. .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
  59. let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
  60. Ok(ciborium::from_reader(&decoded[..])?)
  61. }
  62. }
  63. /// Transport
  64. #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
  65. pub struct Transport {
  66. /// Type
  67. #[serde(rename = "t")]
  68. pub _type: TransportType,
  69. /// Target
  70. #[serde(rename = "a")]
  71. pub target: String,
  72. /// Tags
  73. #[serde(rename = "g")]
  74. pub tags: Option<Vec<Vec<String>>>,
  75. }
  76. impl Transport {
  77. /// Create a new TransportBuilder
  78. pub fn builder() -> TransportBuilder {
  79. TransportBuilder::default()
  80. }
  81. }
  82. /// Builder for Transport
  83. #[derive(Debug, Default, Clone)]
  84. pub struct TransportBuilder {
  85. _type: Option<TransportType>,
  86. target: Option<String>,
  87. tags: Option<Vec<Vec<String>>>,
  88. }
  89. impl TransportBuilder {
  90. /// Set transport type
  91. pub fn transport_type(mut self, transport_type: TransportType) -> Self {
  92. self._type = Some(transport_type);
  93. self
  94. }
  95. /// Set target
  96. pub fn target<S: Into<String>>(mut self, target: S) -> Self {
  97. self.target = Some(target.into());
  98. self
  99. }
  100. /// Add a tag
  101. pub fn add_tag(mut self, tag: Vec<String>) -> Self {
  102. if let Some(ref mut tags) = self.tags {
  103. tags.push(tag);
  104. } else {
  105. self.tags = Some(vec![tag]);
  106. }
  107. self
  108. }
  109. /// Set tags
  110. pub fn tags(mut self, tags: Vec<Vec<String>>) -> Self {
  111. self.tags = Some(tags);
  112. self
  113. }
  114. /// Build the Transport
  115. pub fn build(self) -> Result<Transport, &'static str> {
  116. let _type = self._type.ok_or("Transport type is required")?;
  117. let target = self.target.ok_or("Target is required")?;
  118. Ok(Transport {
  119. _type,
  120. target,
  121. tags: self.tags,
  122. })
  123. }
  124. }
  125. impl AsRef<String> for Transport {
  126. fn as_ref(&self) -> &String {
  127. &self.target
  128. }
  129. }
  130. /// Payment Request
  131. #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
  132. pub struct PaymentRequest {
  133. /// `Payment id`
  134. #[serde(rename = "i")]
  135. pub payment_id: Option<String>,
  136. /// Amount
  137. #[serde(rename = "a")]
  138. pub amount: Option<Amount>,
  139. /// Unit
  140. #[serde(rename = "u")]
  141. pub unit: Option<CurrencyUnit>,
  142. /// Single use
  143. #[serde(rename = "s")]
  144. pub single_use: Option<bool>,
  145. /// Mints
  146. #[serde(rename = "m")]
  147. pub mints: Option<Vec<MintUrl>>,
  148. /// Description
  149. #[serde(rename = "d")]
  150. pub description: Option<String>,
  151. /// Transport
  152. #[serde(rename = "t")]
  153. pub transports: Vec<Transport>,
  154. }
  155. impl PaymentRequest {
  156. /// Create a new PaymentRequestBuilder
  157. pub fn builder() -> PaymentRequestBuilder {
  158. PaymentRequestBuilder::default()
  159. }
  160. }
  161. /// Builder for PaymentRequest
  162. #[derive(Debug, Default, Clone)]
  163. pub struct PaymentRequestBuilder {
  164. payment_id: Option<String>,
  165. amount: Option<Amount>,
  166. unit: Option<CurrencyUnit>,
  167. single_use: Option<bool>,
  168. mints: Option<Vec<MintUrl>>,
  169. description: Option<String>,
  170. transports: Vec<Transport>,
  171. }
  172. impl PaymentRequestBuilder {
  173. /// Set payment ID
  174. pub fn payment_id<S>(mut self, payment_id: S) -> Self
  175. where
  176. S: Into<String>,
  177. {
  178. self.payment_id = Some(payment_id.into());
  179. self
  180. }
  181. /// Set amount
  182. pub fn amount<A>(mut self, amount: A) -> Self
  183. where
  184. A: Into<Amount>,
  185. {
  186. self.amount = Some(amount.into());
  187. self
  188. }
  189. /// Set unit
  190. pub fn unit(mut self, unit: CurrencyUnit) -> Self {
  191. self.unit = Some(unit);
  192. self
  193. }
  194. /// Set single use flag
  195. pub fn single_use(mut self, single_use: bool) -> Self {
  196. self.single_use = Some(single_use);
  197. self
  198. }
  199. /// Add a mint URL
  200. pub fn add_mint(mut self, mint_url: MintUrl) -> Self {
  201. if let Some(ref mut mints) = self.mints {
  202. mints.push(mint_url);
  203. } else {
  204. self.mints = Some(vec![mint_url]);
  205. }
  206. self
  207. }
  208. /// Set mints
  209. pub fn mints(mut self, mints: Vec<MintUrl>) -> Self {
  210. self.mints = Some(mints);
  211. self
  212. }
  213. /// Set description
  214. pub fn description<S: Into<String>>(mut self, description: S) -> Self {
  215. self.description = Some(description.into());
  216. self
  217. }
  218. /// Add a transport
  219. pub fn add_transport(mut self, transport: Transport) -> Self {
  220. self.transports.push(transport);
  221. self
  222. }
  223. /// Set transports
  224. pub fn transports(mut self, transports: Vec<Transport>) -> Self {
  225. self.transports = transports;
  226. self
  227. }
  228. /// Build the PaymentRequest
  229. pub fn build(self) -> PaymentRequest {
  230. PaymentRequest {
  231. payment_id: self.payment_id,
  232. amount: self.amount,
  233. unit: self.unit,
  234. single_use: self.single_use,
  235. mints: self.mints,
  236. description: self.description,
  237. transports: self.transports,
  238. }
  239. }
  240. }
  241. impl AsRef<Option<String>> for PaymentRequest {
  242. fn as_ref(&self) -> &Option<String> {
  243. &self.payment_id
  244. }
  245. }
  246. impl fmt::Display for PaymentRequest {
  247. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  248. use serde::ser::Error;
  249. let mut data = Vec::new();
  250. ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
  251. let encoded = general_purpose::URL_SAFE.encode(data);
  252. write!(f, "{}{}", PAYMENT_REQUEST_PREFIX, encoded)
  253. }
  254. }
  255. impl FromStr for PaymentRequest {
  256. type Err = Error;
  257. fn from_str(s: &str) -> Result<Self, Self::Err> {
  258. let s = s
  259. .strip_prefix(PAYMENT_REQUEST_PREFIX)
  260. .ok_or(Error::InvalidPrefix)?;
  261. let decode_config = general_purpose::GeneralPurposeConfig::new()
  262. .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
  263. let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
  264. Ok(ciborium::from_reader(&decoded[..])?)
  265. }
  266. }
  267. /// Payment Request
  268. #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
  269. pub struct PaymentRequestPayload {
  270. /// Id
  271. pub id: Option<String>,
  272. /// Memo
  273. pub memo: Option<String>,
  274. /// Mint
  275. pub mint: MintUrl,
  276. /// Unit
  277. pub unit: CurrencyUnit,
  278. /// Proofs
  279. pub proofs: Proofs,
  280. }
  281. #[cfg(test)]
  282. mod tests {
  283. use std::str::FromStr;
  284. use super::*;
  285. const PAYMENT_REQUEST: &str = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
  286. #[test]
  287. fn test_decode_payment_req() {
  288. let req = PaymentRequest::from_str(PAYMENT_REQUEST).expect("valid payment request");
  289. assert_eq!(&req.payment_id.unwrap(), "b7a90176");
  290. assert_eq!(req.amount.unwrap(), 10.into());
  291. assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
  292. assert_eq!(
  293. req.mints.unwrap(),
  294. vec![MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url")]
  295. );
  296. assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
  297. let transport = req.transports.first().unwrap();
  298. let expected_transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
  299. assert_eq!(transport, &expected_transport);
  300. }
  301. #[test]
  302. fn test_roundtrip_payment_req() {
  303. let transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
  304. let request = PaymentRequest {
  305. payment_id: Some("b7a90176".to_string()),
  306. amount: Some(10.into()),
  307. unit: Some(CurrencyUnit::Sat),
  308. single_use: None,
  309. mints: Some(vec!["https://nofees.testnut.cashu.space"
  310. .parse()
  311. .expect("valid mint url")]),
  312. description: None,
  313. transports: vec![transport.clone()],
  314. };
  315. let request_str = request.to_string();
  316. let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
  317. assert_eq!(&req.payment_id.unwrap(), "b7a90176");
  318. assert_eq!(req.amount.unwrap(), 10.into());
  319. assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
  320. assert_eq!(
  321. req.mints.unwrap(),
  322. vec![MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url")]
  323. );
  324. assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
  325. let t = req.transports.first().unwrap();
  326. assert_eq!(&transport, t);
  327. }
  328. #[test]
  329. fn test_payment_request_builder() {
  330. let transport = Transport {
  331. _type: TransportType::Nostr,
  332. target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(),
  333. tags: Some(vec![vec!["n".to_string(), "17".to_string()]])
  334. };
  335. let mint_url =
  336. MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url");
  337. // Build a payment request using the builder pattern
  338. let request = PaymentRequest::builder()
  339. .payment_id("b7a90176")
  340. .amount(Amount::from(10))
  341. .unit(CurrencyUnit::Sat)
  342. .add_mint(mint_url.clone())
  343. .add_transport(transport.clone())
  344. .build();
  345. // Verify the built request
  346. assert_eq!(&request.payment_id.clone().unwrap(), "b7a90176");
  347. assert_eq!(request.amount.unwrap(), 10.into());
  348. assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
  349. assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
  350. let t = request.transports.first().unwrap();
  351. assert_eq!(&transport, t);
  352. // Test serialization and deserialization
  353. let request_str = request.to_string();
  354. let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
  355. assert_eq!(req.payment_id, request.payment_id);
  356. assert_eq!(req.amount, request.amount);
  357. assert_eq!(req.unit, request.unit);
  358. }
  359. #[test]
  360. fn test_transport_builder() {
  361. // Build a transport using the builder pattern
  362. let transport = Transport::builder()
  363. .transport_type(TransportType::Nostr)
  364. .target("nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5")
  365. .add_tag(vec!["n".to_string(), "17".to_string()])
  366. .build()
  367. .expect("Valid transport");
  368. // Verify the built transport
  369. assert_eq!(transport._type, TransportType::Nostr);
  370. assert_eq!(transport.target, "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5");
  371. assert_eq!(
  372. transport.tags,
  373. Some(vec![vec!["n".to_string(), "17".to_string()]])
  374. );
  375. // Test error case - missing required fields
  376. let result = TransportBuilder::default().build();
  377. assert!(result.is_err());
  378. }
  379. }