1use bitcoin::secp256k1::schnorr::Signature;
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8
9use super::nut01::Error as Nut01Error;
10use super::{PublicKey, SecretKey};
11use crate::{Amount, BlindedMessage};
12
13#[derive(Debug, thiserror::Error)]
15pub enum Error {
16 #[error(transparent)]
18 Signature(#[from] super::nut20::Error),
19 #[error(transparent)]
21 Nut01(#[from] Nut01Error),
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
26pub struct Settings {
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub max_batch_size: Option<u64>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub methods: Option<Vec<String>>,
33}
34
35impl Settings {
36 pub fn new(max_batch_size: Option<u64>, methods: Option<Vec<String>>) -> Self {
38 Self {
39 max_batch_size,
40 methods,
41 }
42 }
43
44 pub fn is_empty(&self) -> bool {
46 self.max_batch_size.is_none() && self.methods.is_none()
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(bound = "Q: Serialize + DeserializeOwned")]
55pub struct BatchCheckMintQuoteRequest<Q> {
56 pub quotes: Vec<Q>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(bound = "Q: Serialize + DeserializeOwned")]
63pub struct BatchMintRequest<Q> {
64 pub quotes: Vec<Q>,
66 pub quote_amounts: Option<Vec<Amount>>,
68 pub outputs: Vec<BlindedMessage>,
70 pub signatures: Option<Vec<Option<String>>>,
72}
73
74impl<Q> BatchMintRequest<Q>
75where
76 Q: ToString,
77{
78 pub fn msg_to_sign(&self, quote: &Q) -> Vec<u8> {
87 let quote_id = quote.to_string();
88 let capacity = quote_id.len() + (self.outputs.len() * 66);
89 let mut msg = Vec::with_capacity(capacity);
90
91 msg.extend_from_slice(quote_id.as_bytes());
92
93 for output in &self.outputs {
94 msg.extend_from_slice(output.blinded_secret.to_hex().as_bytes());
95 }
96
97 msg
98 }
99
100 pub fn sign_quote(&self, quote: &Q, secret_key: &SecretKey) -> Result<String, Error> {
102 let msg = self.msg_to_sign(quote);
103 let signature: Signature = secret_key.sign(&msg)?;
104 Ok(signature.to_string())
105 }
106
107 pub fn verify_quote_signature(
109 &self,
110 quote: &Q,
111 signature: &str,
112 pubkey: &PublicKey,
113 ) -> Result<(), Error> {
114 let signature = signature
115 .parse::<Signature>()
116 .map_err(|_| super::nut20::Error::InvalidSignature)?;
117 let msg = self.msg_to_sign(quote);
118
119 pubkey
120 .verify(&msg, &signature)
121 .map_err(|_| super::nut20::Error::InvalidSignature)?;
122
123 Ok(())
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use std::str::FromStr;
130
131 use super::*;
132 use crate::Id;
133
134 fn dummy_blinded_message() -> BlindedMessage {
135 let secret_key = SecretKey::generate();
136 BlindedMessage {
137 amount: Amount::from(1),
138 keyset_id: Id::from_str("009a1f293253e41e").unwrap(),
139 blinded_secret: secret_key.public_key(),
140 witness: None,
141 }
142 }
143
144 #[test]
145 fn test_sign_and_verify_batch_quote_roundtrip() {
146 let secret_key = SecretKey::generate();
147 let pubkey = secret_key.public_key();
148 let quote_id = "test-quote-id-123".to_string();
149 let outputs = vec![dummy_blinded_message(), dummy_blinded_message()];
150 let request = BatchMintRequest {
151 quotes: vec![quote_id.clone()],
152 quote_amounts: None,
153 outputs,
154 signatures: None,
155 };
156
157 let signature = request
158 .sign_quote("e_id, &secret_key)
159 .expect("signing should succeed");
160
161 request
162 .verify_quote_signature("e_id, &signature, &pubkey)
163 .expect("verification should succeed with correct key");
164 }
165
166 #[test]
167 fn test_verify_batch_quote_wrong_key() {
168 let signing_key = SecretKey::generate();
169 let wrong_key = SecretKey::generate();
170 let quote_id = "test-quote-wrong-key".to_string();
171 let outputs = vec![dummy_blinded_message()];
172 let request = BatchMintRequest {
173 quotes: vec![quote_id.clone()],
174 quote_amounts: None,
175 outputs,
176 signatures: None,
177 };
178
179 let signature = request
180 .sign_quote("e_id, &signing_key)
181 .expect("signing should succeed");
182
183 let result = request.verify_quote_signature("e_id, &signature, &wrong_key.public_key());
184 assert!(result.is_err(), "verification should fail with wrong key");
185 }
186
187 #[test]
188 fn test_verify_batch_quote_tampered_outputs() {
189 let secret_key = SecretKey::generate();
190 let pubkey = secret_key.public_key();
191 let quote_id = "test-quote-tampered".to_string();
192 let outputs = vec![dummy_blinded_message(), dummy_blinded_message()];
193 let request = BatchMintRequest {
194 quotes: vec![quote_id.clone()],
195 quote_amounts: None,
196 outputs,
197 signatures: None,
198 };
199
200 let signature = request
201 .sign_quote("e_id, &secret_key)
202 .expect("signing should succeed");
203
204 let tampered_request = BatchMintRequest {
205 quotes: vec![quote_id.clone()],
206 quote_amounts: None,
207 outputs: vec![dummy_blinded_message(), dummy_blinded_message()],
208 signatures: None,
209 };
210
211 let result = tampered_request.verify_quote_signature("e_id, &signature, &pubkey);
212 assert!(
213 result.is_err(),
214 "verification should fail with tampered outputs"
215 );
216 }
217
218 #[test]
219 fn test_verify_batch_quote_wrong_quote_id() {
220 let secret_key = SecretKey::generate();
221 let pubkey = secret_key.public_key();
222 let quote_id = "original-quote-id".to_string();
223 let outputs = vec![dummy_blinded_message()];
224 let request = BatchMintRequest {
225 quotes: vec![quote_id.clone()],
226 quote_amounts: None,
227 outputs,
228 signatures: None,
229 };
230
231 let signature = request
232 .sign_quote("e_id, &secret_key)
233 .expect("signing should succeed");
234
235 let different_quote_id = "different-quote-id".to_string();
236 let result = request.verify_quote_signature(&different_quote_id, &signature, &pubkey);
237 assert!(
238 result.is_err(),
239 "verification should fail with different quote ID"
240 );
241 }
242
243 #[test]
244 fn test_sign_batch_quote_empty_outputs() {
245 let secret_key = SecretKey::generate();
246 let pubkey = secret_key.public_key();
247 let quote_id = "test-empty-outputs".to_string();
248 let request = BatchMintRequest {
249 quotes: vec![quote_id.clone()],
250 quote_amounts: None,
251 outputs: vec![],
252 signatures: None,
253 };
254
255 let signature = request
256 .sign_quote("e_id, &secret_key)
257 .expect("signing should succeed");
258
259 request
260 .verify_quote_signature("e_id, &signature, &pubkey)
261 .expect("verification should succeed even with empty outputs");
262 }
263
264 #[test]
265 fn test_sign_batch_quote_multiple_quotes_different_sigs() {
266 let secret_key = SecretKey::generate();
267 let outputs = vec![dummy_blinded_message(), dummy_blinded_message()];
268 let quote_1 = "quote-1".to_string();
269 let quote_2 = "quote-2".to_string();
270 let request = BatchMintRequest {
271 quotes: vec![quote_1.clone(), quote_2.clone()],
272 quote_amounts: None,
273 outputs,
274 signatures: None,
275 };
276
277 let sig1 = request
278 .sign_quote("e_1, &secret_key)
279 .expect("signing should succeed");
280 let sig2 = request
281 .sign_quote("e_2, &secret_key)
282 .expect("signing should succeed");
283
284 assert_ne!(
285 sig1, sig2,
286 "signatures for different quote IDs should differ"
287 );
288 }
289}