common.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. //! Types
  2. use serde::{Deserialize, Serialize};
  3. use crate::error::Error;
  4. use crate::mint_url::MintUrl;
  5. use crate::nuts::nut00::ProofsMethods;
  6. use crate::nuts::{
  7. CurrencyUnit, MeltQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SpendingConditions,
  8. State,
  9. };
  10. use crate::Amount;
  11. /// Melt response with proofs
  12. #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
  13. pub struct Melted {
  14. /// State of quote
  15. pub state: MeltQuoteState,
  16. /// Preimage of melt payment
  17. pub preimage: Option<String>,
  18. /// Melt change
  19. pub change: Option<Proofs>,
  20. /// Melt amount
  21. pub amount: Amount,
  22. /// Fee paid
  23. pub fee_paid: Amount,
  24. }
  25. impl Melted {
  26. /// Create new [`Melted`]
  27. pub fn from_proofs(
  28. state: MeltQuoteState,
  29. preimage: Option<String>,
  30. quote_amount: Amount,
  31. proofs: Proofs,
  32. change_proofs: Option<Proofs>,
  33. ) -> Result<Self, Error> {
  34. let proofs_amount = proofs.total_amount()?;
  35. let change_amount = match &change_proofs {
  36. Some(change_proofs) => change_proofs.total_amount()?,
  37. None => Amount::ZERO,
  38. };
  39. tracing::info!(
  40. "Proofs amount: {} Amount: {} Change: {}",
  41. proofs_amount,
  42. quote_amount,
  43. change_amount
  44. );
  45. let fee_paid = proofs_amount
  46. .checked_sub(
  47. quote_amount
  48. .checked_add(change_amount)
  49. .ok_or(Error::AmountOverflow)?,
  50. )
  51. .ok_or(Error::AmountOverflow)?;
  52. Ok(Self {
  53. state,
  54. preimage,
  55. change: change_proofs,
  56. amount: quote_amount,
  57. fee_paid,
  58. })
  59. }
  60. /// Total amount melted
  61. ///
  62. /// # Panics
  63. ///
  64. /// Panics if the sum of `amount` and `fee_paid` overflows. This should not
  65. /// happen as the fee is validated when calculated.
  66. pub fn total_amount(&self) -> Amount {
  67. self.amount
  68. .checked_add(self.fee_paid)
  69. .expect("We check when calc fee paid")
  70. }
  71. }
  72. /// Prooinfo
  73. #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
  74. pub struct ProofInfo {
  75. /// Proof
  76. pub proof: Proof,
  77. /// y
  78. pub y: PublicKey,
  79. /// Mint Url
  80. pub mint_url: MintUrl,
  81. /// Proof State
  82. pub state: State,
  83. /// Proof Spending Conditions
  84. pub spending_condition: Option<SpendingConditions>,
  85. /// Unit
  86. pub unit: CurrencyUnit,
  87. }
  88. impl ProofInfo {
  89. /// Create new [`ProofInfo`]
  90. pub fn new(
  91. proof: Proof,
  92. mint_url: MintUrl,
  93. state: State,
  94. unit: CurrencyUnit,
  95. ) -> Result<Self, Error> {
  96. let y = proof.y()?;
  97. let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
  98. Ok(Self {
  99. proof,
  100. y,
  101. mint_url,
  102. state,
  103. spending_condition,
  104. unit,
  105. })
  106. }
  107. /// Check if [`Proof`] matches conditions
  108. pub fn matches_conditions(
  109. &self,
  110. mint_url: &Option<MintUrl>,
  111. unit: &Option<CurrencyUnit>,
  112. state: &Option<Vec<State>>,
  113. spending_conditions: &Option<Vec<SpendingConditions>>,
  114. ) -> bool {
  115. if let Some(mint_url) = mint_url {
  116. if mint_url.ne(&self.mint_url) {
  117. return false;
  118. }
  119. }
  120. if let Some(unit) = unit {
  121. if unit.ne(&self.unit) {
  122. return false;
  123. }
  124. }
  125. if let Some(state) = state {
  126. if !state.contains(&self.state) {
  127. return false;
  128. }
  129. }
  130. if let Some(spending_conditions) = spending_conditions {
  131. match &self.spending_condition {
  132. None => {
  133. if !spending_conditions.is_empty() {
  134. return false;
  135. }
  136. }
  137. Some(s) => {
  138. if !spending_conditions.contains(s) {
  139. return false;
  140. }
  141. }
  142. }
  143. }
  144. true
  145. }
  146. }
  147. /// Key used in hashmap of ln backends to identify what unit and payment method
  148. /// it is for
  149. #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
  150. pub struct PaymentProcessorKey {
  151. /// Unit of Payment backend
  152. pub unit: CurrencyUnit,
  153. /// Method of payment backend
  154. pub method: PaymentMethod,
  155. }
  156. impl PaymentProcessorKey {
  157. /// Create new [`PaymentProcessorKey`]
  158. pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self {
  159. Self { unit, method }
  160. }
  161. }
  162. /// Seconds quotes are valid
  163. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
  164. pub struct QuoteTTL {
  165. /// Seconds mint quote is valid
  166. pub mint_ttl: u64,
  167. /// Seconds melt quote is valid
  168. pub melt_ttl: u64,
  169. }
  170. impl QuoteTTL {
  171. /// Create new [`QuoteTTL`]
  172. pub fn new(mint_ttl: u64, melt_ttl: u64) -> QuoteTTL {
  173. Self { mint_ttl, melt_ttl }
  174. }
  175. }
  176. impl Default for QuoteTTL {
  177. fn default() -> Self {
  178. Self {
  179. mint_ttl: 60 * 60, // 1 hour
  180. melt_ttl: 60, // 1 minute
  181. }
  182. }
  183. }
  184. #[cfg(test)]
  185. mod tests {
  186. use std::str::FromStr;
  187. use cashu::SecretKey;
  188. use super::{Melted, ProofInfo};
  189. use crate::mint_url::MintUrl;
  190. use crate::nuts::{CurrencyUnit, Id, Proof, PublicKey, SpendingConditions, State};
  191. use crate::secret::Secret;
  192. use crate::Amount;
  193. #[test]
  194. fn test_melted() {
  195. let keyset_id = Id::from_str("00deadbeef123456").unwrap();
  196. let proof = Proof::new(
  197. Amount::from(64),
  198. keyset_id,
  199. Secret::generate(),
  200. PublicKey::from_hex(
  201. "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
  202. )
  203. .unwrap(),
  204. );
  205. let melted = Melted::from_proofs(
  206. super::MeltQuoteState::Paid,
  207. Some("preimage".to_string()),
  208. Amount::from(64),
  209. vec![proof.clone()],
  210. None,
  211. )
  212. .unwrap();
  213. assert_eq!(melted.amount, Amount::from(64));
  214. assert_eq!(melted.fee_paid, Amount::ZERO);
  215. assert_eq!(melted.total_amount(), Amount::from(64));
  216. }
  217. #[test]
  218. fn test_melted_with_change() {
  219. let keyset_id = Id::from_str("00deadbeef123456").unwrap();
  220. let proof = Proof::new(
  221. Amount::from(64),
  222. keyset_id,
  223. Secret::generate(),
  224. PublicKey::from_hex(
  225. "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
  226. )
  227. .unwrap(),
  228. );
  229. let change_proof = Proof::new(
  230. Amount::from(32),
  231. keyset_id,
  232. Secret::generate(),
  233. PublicKey::from_hex(
  234. "03deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
  235. )
  236. .unwrap(),
  237. );
  238. let melted = Melted::from_proofs(
  239. super::MeltQuoteState::Paid,
  240. Some("preimage".to_string()),
  241. Amount::from(31),
  242. vec![proof.clone()],
  243. Some(vec![change_proof.clone()]),
  244. )
  245. .unwrap();
  246. assert_eq!(melted.amount, Amount::from(31));
  247. assert_eq!(melted.fee_paid, Amount::from(1));
  248. assert_eq!(melted.total_amount(), Amount::from(32));
  249. }
  250. #[test]
  251. fn test_matches_conditions() {
  252. let keyset_id = Id::from_str("00deadbeef123456").unwrap();
  253. let proof = Proof::new(
  254. Amount::from(64),
  255. keyset_id,
  256. Secret::new("test_secret"),
  257. PublicKey::from_hex(
  258. "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
  259. )
  260. .unwrap(),
  261. );
  262. let mint_url = MintUrl::from_str("https://example.com").unwrap();
  263. let proof_info =
  264. ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
  265. // Test matching mint_url
  266. assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
  267. assert!(!proof_info.matches_conditions(
  268. &Some(MintUrl::from_str("https://different.com").unwrap()),
  269. &None,
  270. &None,
  271. &None
  272. ));
  273. // Test matching unit
  274. assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
  275. assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
  276. // Test matching state
  277. assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
  278. assert!(proof_info.matches_conditions(
  279. &None,
  280. &None,
  281. &Some(vec![State::Unspent, State::Spent]),
  282. &None
  283. ));
  284. assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
  285. // Test with no conditions (should match)
  286. assert!(proof_info.matches_conditions(&None, &None, &None, &None));
  287. // Test with multiple conditions
  288. assert!(proof_info.matches_conditions(
  289. &Some(mint_url),
  290. &Some(CurrencyUnit::Sat),
  291. &Some(vec![State::Unspent]),
  292. &None
  293. ));
  294. }
  295. #[test]
  296. fn test_matches_conditions_with_spending_conditions() {
  297. // This test would need to be expanded with actual SpendingConditions
  298. // implementation, but we can test the basic case where no spending
  299. // conditions are present
  300. let keyset_id = Id::from_str("00deadbeef123456").unwrap();
  301. let proof = Proof::new(
  302. Amount::from(64),
  303. keyset_id,
  304. Secret::new("test_secret"),
  305. PublicKey::from_hex(
  306. "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
  307. )
  308. .unwrap(),
  309. );
  310. let mint_url = MintUrl::from_str("https://example.com").unwrap();
  311. let proof_info =
  312. ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
  313. // Test with empty spending conditions (should match when proof has none)
  314. assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
  315. // Test with non-empty spending conditions (should not match when proof has none)
  316. let dummy_condition = SpendingConditions::P2PKConditions {
  317. data: SecretKey::generate().public_key(),
  318. conditions: None,
  319. };
  320. assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
  321. }
  322. }
  323. /// Mint Fee Reserve
  324. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
  325. pub struct FeeReserve {
  326. /// Absolute expected min fee
  327. pub min_fee_reserve: Amount,
  328. /// Percentage expected fee
  329. pub percent_fee_reserve: f32,
  330. }