moksha_core/
proof.rs

1//! This module defines the `Proof`, `P2SHScript`, and `Proofs` structs, which are used for representing proofs in the Moksha Core library as described in [Nut-00](https://github.com/cashubtc/nuts/blob/main/00.md)
2//!
3//! The `Proof` struct represents a proof, with an `amount` field for the amount in satoshis, a `secret` field for the secret string, a `c` field for the public key of the blinding factor, an `id` field for the ID of the proof, and an optional `script` field for the P2SH script.
4//!
5//! The `Proof` struct provides a `new` method for creating a new proof from its constituent fields.
6//!
7//! The `P2SHScript` struct represents a P2SH script, and is currently not implemented.
8//!
9//! The `Proofs` struct represents a collection of proofs, with a `Vec<Proof>` field for the proofs.
10//!
11//! Both the `Proof` and `Proofs` structs are serializable and deserializable using serde.
12
13use 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, // FIXME use keysetID as specific type
26    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")?; // 60 tokens (4,8,16,32)
146        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}