1use cdk_common::common::IssuerVersion;
10use cdk_common::error::Error;
11use cdk_common::mint::MintKeySetInfo;
12use cdk_common::nuts::nut02::KeySetVersion;
13use cdk_common::{
14 BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, Keys, MintKeySet, Proof, PublicKey,
15};
16
17#[derive(Debug, Clone)]
21pub struct RotateKeyArguments {
22 pub unit: CurrencyUnit,
24 pub amounts: Vec<u64>,
26 pub input_fee_ppk: u64,
28 pub keyset_id_type: KeySetVersion,
30 pub final_expiry: Option<u64>,
32}
33
34#[derive(Debug, Clone)]
35pub struct SignatoryKeysets {
37 pub pubkey: PublicKey,
39 pub keysets: Vec<SignatoryKeySet>,
41}
42
43#[derive(Debug, Clone)]
44pub struct SignatoryKeySet {
49 pub id: Id,
51 pub unit: CurrencyUnit,
53 pub active: bool,
55 pub keys: Keys,
57 pub amounts: Vec<u64>,
59 pub input_fee_ppk: u64,
61 pub final_expiry: Option<u64>,
63 pub issuer_version: Option<IssuerVersion>,
65 pub version: u32,
67}
68
69impl SignatoryKeySet {
70 pub fn is_expired(&self) -> bool {
72 self.final_expiry
73 .is_some_and(|expiry| expiry < cdk_common::util::unix_time())
74 }
75}
76
77impl From<&SignatoryKeySet> for KeySet {
78 fn from(val: &SignatoryKeySet) -> Self {
79 val.to_owned().into()
80 }
81}
82
83impl From<SignatoryKeySet> for KeySet {
84 fn from(val: SignatoryKeySet) -> Self {
85 KeySet {
86 id: val.id,
87 unit: val.unit,
88 active: Some(val.active),
89 keys: val.keys,
90 input_fee_ppk: val.input_fee_ppk,
91 final_expiry: val.final_expiry,
92 }
93 }
94}
95
96impl From<&SignatoryKeySet> for MintKeySetInfo {
97 fn from(val: &SignatoryKeySet) -> Self {
98 val.to_owned().into()
99 }
100}
101
102impl From<SignatoryKeySet> for MintKeySetInfo {
103 fn from(val: SignatoryKeySet) -> Self {
104 MintKeySetInfo {
105 id: val.id,
106 unit: val.unit,
107 active: val.active,
108 input_fee_ppk: val.input_fee_ppk,
109 derivation_path: Default::default(),
110 derivation_path_index: Default::default(),
111 amounts: val.amounts,
112 final_expiry: val.final_expiry,
113 issuer_version: val.issuer_version,
114 valid_from: 0,
115 }
116 }
117}
118
119impl From<&(MintKeySetInfo, MintKeySet)> for SignatoryKeySet {
123 fn from((info, key): &(MintKeySetInfo, MintKeySet)) -> Self {
124 Self {
125 id: info.id,
126 unit: info.unit.clone(),
127 active: info.active,
128 input_fee_ppk: info.input_fee_ppk,
129 amounts: info.amounts.clone(),
130 keys: key.keys.clone().into(),
131 version: info.derivation_path_index.unwrap_or(1),
132 final_expiry: info.final_expiry,
133 issuer_version: info.issuer_version.clone(),
134 }
135 }
136}
137
138#[async_trait::async_trait]
139pub trait Signatory {
141 fn name(&self) -> String;
144
145 async fn blind_sign(
149 &self,
150 blinded_messages: Vec<BlindedMessage>,
151 ) -> Result<Vec<BlindSignature>, Error>;
152
153 async fn verify_proofs(&self, proofs: Vec<Proof>) -> Result<(), Error>;
155
156 async fn keysets(&self) -> Result<SignatoryKeysets, Error>;
158
159 async fn subscribe_keysets(
166 &self,
167 ) -> Result<tokio::sync::watch::Receiver<SignatoryKeysets>, Error>;
168
169 async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result<SignatoryKeySet, Error>;
172}
173
174#[cfg(test)]
175mod tests {
176 use std::collections::BTreeMap;
177 use std::str::FromStr;
178
179 use bitcoin::bip32::DerivationPath;
180 use bitcoin::secp256k1::Secp256k1;
181 use cdk_common::nuts::nut01::Keys;
182 use cdk_common::util::unix_time;
183 use cdk_common::{CurrencyUnit, Id};
184
185 use super::*;
186
187 fn dummy_signatory_keyset(final_expiry: Option<u64>) -> SignatoryKeySet {
188 SignatoryKeySet {
189 id: Id::from_str("009a1f293253e41e").unwrap(),
190 unit: CurrencyUnit::Sat,
191 active: true,
192 keys: Keys::new(BTreeMap::new()),
193 amounts: vec![1, 2, 4, 8, 16, 32, 64, 128, 256, 512],
194 input_fee_ppk: 0,
195 final_expiry,
196 issuer_version: None,
197 version: 0,
198 }
199 }
200
201 #[test]
202 fn test_is_expired_none() {
203 let ks = dummy_signatory_keyset(None);
204 assert!(!ks.is_expired());
205 }
206
207 #[test]
208 fn test_is_expired_far_future() {
209 let ks = dummy_signatory_keyset(Some(unix_time() + 1_000_000));
210 assert!(!ks.is_expired());
211 }
212
213 #[test]
214 fn test_is_expired_exactly_now_is_not_expired() {
215 let ks = dummy_signatory_keyset(Some(unix_time()));
217 assert!(!ks.is_expired());
218 }
219
220 #[test]
221 fn test_is_expired_one_second_ago() {
222 let ks = dummy_signatory_keyset(Some(unix_time() - 1));
223 assert!(ks.is_expired());
224 }
225
226 #[test]
227 fn test_is_expired_zero() {
228 let ks = dummy_signatory_keyset(Some(0));
229 assert!(ks.is_expired());
230 }
231
232 #[test]
233 fn conversion_prefers_info_over_cached_key() {
234 let path = DerivationPath::from_str("m/0'/0'/0'").unwrap();
235 let key = MintKeySet::generate_from_seed(
236 &Secp256k1::new(),
237 b"test-seed-conversion",
238 &[1, 2, 4, 8],
239 CurrencyUnit::Usd,
240 path.clone(),
241 0,
242 Some(222),
243 KeySetVersion::Version00,
244 );
245
246 let info = MintKeySetInfo {
247 id: key.id,
248 unit: CurrencyUnit::Sat,
249 active: true,
250 valid_from: 0,
251 derivation_path: path,
252 derivation_path_index: Some(1),
253 amounts: vec![1, 2, 4, 8],
254 input_fee_ppk: 0,
255 final_expiry: Some(111),
256 issuer_version: None,
257 };
258
259 let keyset: SignatoryKeySet = (&(info, key)).into();
260
261 assert_eq!(
262 keyset.unit,
263 CurrencyUnit::Sat,
264 "unit must come from the keyset info, not the cached key"
265 );
266 assert_eq!(
267 keyset.final_expiry,
268 Some(111),
269 "final_expiry must come from the keyset info, not the cached key"
270 );
271 }
272}