@@ -7,9 +7,10 @@ use bcr_ebill_core::{
7
7
contact:: { BillAnonParticipant , BillIdentParticipant , BillParticipant , ContactType } ,
8
8
util:: { BcrKeys , date:: DateTimeUtc } ,
9
9
} ;
10
+ use bcr_wdc_key_client:: KeyClient ;
10
11
use bcr_wdc_quote_client:: QuoteClient ;
11
12
use bcr_wdc_webapi:: quotes:: { BillInfo , ResolveOffer , StatusReply } ;
12
- use cashu:: { nut01 as cdk01, nut02 as cdk02} ;
13
+ use cashu:: { nut00 as cdk00 , nut01 as cdk01, nut02 as cdk02} ;
13
14
use thiserror:: Error ;
14
15
15
16
/// Generic result type
@@ -24,6 +25,9 @@ pub enum Error {
24
25
/// all errors originating from parsing public keys
25
26
#[ error( "External Mint Public Key Error" ) ]
26
27
PubKey ,
28
+ /// all errors originating from parsing private keys
29
+ #[ error( "External Mint Private Key Error" ) ]
30
+ PrivateKey ,
27
31
/// all errors originating from creating signatures
28
32
#[ error( "External Mint Signature Error" ) ]
29
33
Signature ,
@@ -36,20 +40,40 @@ pub enum Error {
36
40
/// all errors originating from invalid mint request ids
37
41
#[ error( "External Mint Invalid Mint Request Id Error" ) ]
38
42
InvalidMintRequestId ,
43
+ /// all errors originating from invalid keyset ids
44
+ #[ error( "External Mint Invalid KeySet Id Error" ) ]
45
+ InvalidKeySetId ,
46
+ /// all errors originating from blind message generation
47
+ #[ error( "External Mint BlindMessage Error" ) ]
48
+ BlindMessage ,
39
49
/// all errors originating from the quote client
40
50
#[ error( "External Mint Quote Client Error" ) ]
41
51
QuoteClient ,
52
+ /// all errors originating from the key client
53
+ #[ error( "External Mint Key Client Error" ) ]
54
+ KeyClient ,
42
55
}
43
56
44
57
#[ cfg( test) ]
45
58
use mockall:: automock;
46
59
47
- use crate :: util;
60
+ use crate :: { constants :: CURRENCY_CRSAT , util} ;
48
61
49
62
#[ cfg_attr( test, automock) ]
50
63
#[ cfg_attr( target_arch = "wasm32" , async_trait( ?Send ) ) ]
51
64
#[ cfg_attr( not( target_arch = "wasm32" ) , async_trait) ]
52
65
pub trait MintClientApi : ServiceTraitBounds {
66
+ /// Mint and return encoded token
67
+ async fn mint (
68
+ & self ,
69
+ mint_url : & str ,
70
+ keyset : cdk02:: KeySet ,
71
+ discounted_amount : u64 ,
72
+ quote_id : & str ,
73
+ private_key : & str ,
74
+ ) -> Result < String > ;
75
+ /// Check keyset info for a given keyset id with a given mint
76
+ async fn get_keyset_info ( & self , mint_url : & str , keyset_id : & str ) -> Result < cdk02:: KeySet > ;
53
77
/// Request to mint a bill with a given mint
54
78
async fn enquire_mint_quote (
55
79
& self ,
@@ -94,11 +118,81 @@ impl MintClient {
94
118
) ;
95
119
Ok ( quote_client)
96
120
}
121
+
122
+ pub fn key_client ( & self , mint_url : & str ) -> Result < KeyClient > {
123
+ let key_client = bcr_wdc_key_client:: KeyClient :: new (
124
+ reqwest:: Url :: parse ( mint_url) . map_err ( |_| Error :: InvalidMintUrl ) ?,
125
+ ) ;
126
+ Ok ( key_client)
127
+ }
97
128
}
98
129
99
130
#[ cfg_attr( target_arch = "wasm32" , async_trait( ?Send ) ) ]
100
131
#[ cfg_attr( not( target_arch = "wasm32" ) , async_trait) ]
101
132
impl MintClientApi for MintClient {
133
+ async fn mint (
134
+ & self ,
135
+ mint_url : & str ,
136
+ keyset : cdk02:: KeySet ,
137
+ discounted_amount : u64 ,
138
+ quote_id : & str ,
139
+ private_key : & str ,
140
+ ) -> Result < String > {
141
+ let secret_key = cdk01:: SecretKey :: from_hex ( private_key) . map_err ( |_| Error :: PrivateKey ) ?;
142
+ let qid = uuid:: Uuid :: from_str ( quote_id) . map_err ( |_| Error :: InvalidMintRequestId ) ?;
143
+
144
+ // create blinded messages
145
+ let amounts: Vec < cashu:: Amount > = cashu:: Amount :: from ( discounted_amount) . split ( ) ;
146
+ let blinds = generate_blinds ( keyset. id , & amounts) ?;
147
+ let blinded_messages = blinds. iter ( ) . map ( |b| b. 0 . clone ( ) ) . collect :: < Vec < _ > > ( ) ;
148
+
149
+ // mint
150
+ let blinded_signatures = self
151
+ . key_client ( mint_url) ?
152
+ . mint ( qid, blinded_messages, secret_key)
153
+ . await
154
+ . map_err ( |e| {
155
+ log:: error!( "Error minting at mint {mint_url}: {e}" ) ;
156
+ Error :: KeyClient
157
+ } ) ?;
158
+
159
+ // create proofs
160
+ let secrets = blinds. iter ( ) . map ( |b| b. 1 . clone ( ) ) . collect :: < Vec < _ > > ( ) ;
161
+ let rs = blinds. iter ( ) . map ( |b| b. 2 . clone ( ) ) . collect :: < Vec < _ > > ( ) ;
162
+ let proofs =
163
+ cashu:: dhke:: construct_proofs ( blinded_signatures, rs, secrets, & keyset. keys ) . unwrap ( ) ;
164
+
165
+ // generate token from proofs
166
+ let mint_url = cashu:: MintUrl :: from_str ( mint_url) . map_err ( |_| Error :: InvalidMintUrl ) ?;
167
+ let token = cdk00:: Token :: new (
168
+ mint_url,
169
+ proofs,
170
+ None ,
171
+ cashu:: CurrencyUnit :: Custom ( CURRENCY_CRSAT . into ( ) ) ,
172
+ ) ;
173
+
174
+ Ok ( token. to_v3_string ( ) )
175
+ }
176
+
177
+ async fn get_keyset_info ( & self , mint_url : & str , keyset_id : & str ) -> Result < cdk02:: KeySet > {
178
+ let base = reqwest:: Url :: parse ( mint_url) . map_err ( |_| Error :: InvalidMintUrl ) ?;
179
+ let url = base
180
+ . join ( & format ! ( "/v1/keys/{}" , keyset_id) )
181
+ . expect ( "keys relative path" ) ;
182
+ let res = reqwest:: Client :: new ( ) . get ( url) . send ( ) . await . map_err ( |e| {
183
+ log:: error!( "Error getting keyset info from mint {mint_url}: {e}" ) ;
184
+ Error :: KeyClient
185
+ } ) ?;
186
+ let json: cdk01:: KeysResponse = res. json ( ) . await . map_err ( |e| {
187
+ log:: error!( "Error deserializing keyset info: {e}" ) ;
188
+ Error :: KeyClient
189
+ } ) ?;
190
+ json. keysets . first ( ) . map ( |k| k. to_owned ( ) ) . ok_or_else ( || {
191
+ log:: error!( "Empty keyset" ) ;
192
+ Error :: KeyClient . into ( )
193
+ } )
194
+ }
195
+
102
196
async fn enquire_mint_quote (
103
197
& self ,
104
198
mint_url : & str ,
@@ -180,6 +274,38 @@ impl MintClientApi for MintClient {
180
274
}
181
275
}
182
276
277
+ pub fn generate_blinds (
278
+ keyset_id : cashu:: Id ,
279
+ amounts : & [ cashu:: Amount ] ,
280
+ ) -> Result <
281
+ Vec < (
282
+ cashu:: BlindedMessage ,
283
+ cashu:: secret:: Secret ,
284
+ cashu:: SecretKey ,
285
+ ) > ,
286
+ > {
287
+ let mut blinds = Vec :: new ( ) ;
288
+ for amount in amounts {
289
+ let blind = generate_blind ( keyset_id, * amount) ?;
290
+ blinds. push ( blind) ;
291
+ }
292
+ Ok ( blinds)
293
+ }
294
+
295
+ pub fn generate_blind (
296
+ kid : cashu:: Id ,
297
+ amount : cashu:: Amount ,
298
+ ) -> Result < (
299
+ cashu:: BlindedMessage ,
300
+ cashu:: secret:: Secret ,
301
+ cashu:: SecretKey ,
302
+ ) > {
303
+ let secret = cashu:: secret:: Secret :: new ( rand:: random :: < u64 > ( ) . to_string ( ) ) ;
304
+ let ( b_, r) =
305
+ cashu:: dhke:: blind_message ( secret. as_bytes ( ) , None ) . map_err ( |_| Error :: BlindMessage ) ?;
306
+ Ok ( ( cashu:: BlindedMessage :: new ( amount, kid, b_) , secret, r) )
307
+ }
308
+
183
309
#[ derive( Debug , Clone ) ]
184
310
pub enum ResolveMintOffer {
185
311
Accept ,
0 commit comments