1use secp256k1::PublicKey;
14use serde::{Deserialize, Serialize};
15use serde_with::skip_serializing_none;
16use utoipa::ToSchema;
17
18use crate::error::MokshaCoreError;
19
20#[skip_serializing_none]
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
22pub struct Proof {
23 pub amount: u64,
24 #[serde(rename = "id")]
25 pub keyset_id: String, pub secret: String,
27 #[serde(rename = "C")]
28 #[schema(value_type = String)]
29 pub c: PublicKey,
30 pub script: Option<P2SHScript>,
31}
32
33impl Proof {
34 pub const fn new(amount: u64, secret: String, c: PublicKey, id: String) -> Self {
35 Self {
36 amount,
37 secret,
38 c,
39 keyset_id: id,
40 script: None,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
46pub struct P2SHScript;
47
48#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
49pub struct Proofs(pub(super) Vec<Proof>);
50
51impl Proofs {
52 pub fn new(proofs: Vec<Proof>) -> Self {
53 Self(proofs)
54 }
55
56 pub fn with_proof(proof: Proof) -> Self {
57 Self(vec![proof])
58 }
59
60 pub const fn empty() -> Self {
61 Self(vec![])
62 }
63
64 pub fn total_amount(&self) -> u64 {
65 self.0.iter().map(|proof| proof.amount).sum()
66 }
67
68 pub fn proofs(&self) -> Vec<Proof> {
69 self.0.clone()
70 }
71
72 pub fn len(&self) -> usize {
73 self.0.len()
74 }
75
76 pub fn is_empty(&self) -> bool {
77 self.0.is_empty()
78 }
79
80 pub fn proofs_for_amount(&self, amount: u64) -> Result<Self, MokshaCoreError> {
81 let mut all_proofs = self.0.clone();
82 if amount > self.total_amount() {
83 return Err(MokshaCoreError::NotEnoughTokens);
84 }
85
86 all_proofs.sort_by(|a, b| a.amount.cmp(&b.amount));
87
88 let mut selected_proofs = vec![];
89 let mut selected_amount = 0;
90
91 while selected_amount < amount {
92 if all_proofs.is_empty() {
93 break;
94 }
95
96 let proof = all_proofs.pop().expect("proofs is empty");
97 selected_amount += proof.amount;
98 selected_proofs.push(proof);
99 }
100
101 Ok(selected_proofs.into())
102 }
103}
104
105impl From<Vec<Proof>> for Proofs {
106 fn from(from: Vec<Proof>) -> Self {
107 Self(from)
108 }
109}
110
111impl From<Proof> for Proofs {
112 fn from(from: Proof) -> Self {
113 Self(vec![from])
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use serde_json::json;
120
121 use crate::{
122 fixture::read_fixture,
123 proof::{Proof, Proofs},
124 token::TokenV3,
125 };
126 use pretty_assertions::assert_eq;
127
128 #[test]
129 fn test_proofs_for_amount_empty() -> anyhow::Result<()> {
130 let proofs = Proofs::empty();
131
132 let result = proofs.proofs_for_amount(10);
133
134 assert!(result.is_err());
135 assert!(result
136 .err()
137 .unwrap()
138 .to_string()
139 .contains("Not enough tokens"));
140 Ok(())
141 }
142
143 #[test]
144 fn test_proofs_for_amount_valid() -> anyhow::Result<()> {
145 let fixture = read_fixture("token_60.cashu")?; let token: TokenV3 = fixture.try_into()?;
147
148 let result = token.proofs().proofs_for_amount(10)?;
149 assert_eq!(32, result.total_amount());
150 assert_eq!(1, result.len());
151 Ok(())
152 }
153
154 #[test]
155 fn test_proof() -> anyhow::Result<()> {
156 let js = json!(
157 {
158 "id": "DSAl9nvvyfva",
159 "amount": 2,
160 "secret": "EhpennC9qB3iFlW8FZ_pZw",
161 "C": "02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4"
162 }
163 );
164
165 let proof = serde_json::from_value::<Proof>(js)?;
166 assert_eq!(proof.amount, 2);
167 assert_eq!(proof.keyset_id, "DSAl9nvvyfva".to_string());
168 assert_eq!(proof.secret, "EhpennC9qB3iFlW8FZ_pZw".to_string());
169 assert_eq!(
170 proof.c.to_string(),
171 "02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4".to_string()
172 );
173 Ok(())
174 }
175}