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