Skip to content

Commit 548b23a

Browse files
authored
implement keyset info and minting (#529)
1 parent 5e24bd0 commit 548b23a

File tree

11 files changed

+303
-63
lines changed

11 files changed

+303
-63
lines changed

.github/workflows/wasm_release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
- name: Install Rust toolchain
5050
uses: actions-rust-lang/setup-rust-toolchain@v1
5151
with:
52-
toolchain: 1.86.0
52+
toolchain: 1.87.0
5353

5454
- name: Install wasm-pack
5555
run: |

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* Call mint endpoint for cancelling
77
* Use mint nostr relays from network and fall back to identity ones
88
* Add endpoints to accept, or reject an offer from a mint
9+
* Add logic to check keyset info, mint and create proofs
910

1011
# 0.3.13
1112

crates/bcr-ebill-api/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ bcr-ebill-transport = { path = "../bcr-ebill-transport" }
3131
tokio.workspace = true
3232
tokio_with_wasm.workspace = true
3333
secp256k1.workspace = true
34-
bcr-wdc-webapi = { git = "https://github.com/BitcreditProtocol/wildcat", rev = "8a07e1d5012255282acde35982762f18e80322fb" }
35-
bcr-wdc-quote-client = { git = "https://github.com/BitcreditProtocol/wildcat", rev = "8a07e1d5012255282acde35982762f18e80322fb" }
34+
bcr-wdc-webapi = { git = "https://github.com/BitcreditProtocol/wildcat", rev = "4437c3b809105df69ed459a9698cac8deb8d9210" }
35+
bcr-wdc-quote-client = { git = "https://github.com/BitcreditProtocol/wildcat", rev = "4437c3b809105df69ed459a9698cac8deb8d9210" }
36+
bcr-wdc-key-client = { git = "https://github.com/BitcreditProtocol/wildcat", rev = "4437c3b809105df69ed459a9698cac8deb8d9210" }
3637
cashu = { version = "0.9", default-features = false }
38+
rand = { version = "0.8" }
3739

3840
[target.'cfg(target_arch = "wasm32")'.dependencies]
3941
reqwest = { workspace = true, features = ["json"] }

crates/bcr-ebill-api/src/constants.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ pub const VALID_FILE_MIME_TYPES: [&str; 3] = ["image/jpeg", "image/png", "applic
55

66
// When subscribing events we subtract this from the last received event time
77
pub const NOSTR_EVENT_TIME_SLACK: u64 = 3600; // 1 hour
8+
pub const CURRENCY_CRSAT: &str = "crsat";
9+
pub const CURRENCY_SAT: &str = "sat";

crates/bcr-ebill-api/src/external/mint.rs

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ use bcr_ebill_core::{
77
contact::{BillAnonParticipant, BillIdentParticipant, BillParticipant, ContactType},
88
util::{BcrKeys, date::DateTimeUtc},
99
};
10+
use bcr_wdc_key_client::KeyClient;
1011
use bcr_wdc_quote_client::QuoteClient;
1112
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};
1314
use thiserror::Error;
1415

1516
/// Generic result type
@@ -24,6 +25,9 @@ pub enum Error {
2425
/// all errors originating from parsing public keys
2526
#[error("External Mint Public Key Error")]
2627
PubKey,
28+
/// all errors originating from parsing private keys
29+
#[error("External Mint Private Key Error")]
30+
PrivateKey,
2731
/// all errors originating from creating signatures
2832
#[error("External Mint Signature Error")]
2933
Signature,
@@ -36,20 +40,40 @@ pub enum Error {
3640
/// all errors originating from invalid mint request ids
3741
#[error("External Mint Invalid Mint Request Id Error")]
3842
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,
3949
/// all errors originating from the quote client
4050
#[error("External Mint Quote Client Error")]
4151
QuoteClient,
52+
/// all errors originating from the key client
53+
#[error("External Mint Key Client Error")]
54+
KeyClient,
4255
}
4356

4457
#[cfg(test)]
4558
use mockall::automock;
4659

47-
use crate::util;
60+
use crate::{constants::CURRENCY_CRSAT, util};
4861

4962
#[cfg_attr(test, automock)]
5063
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
5164
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
5265
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>;
5377
/// Request to mint a bill with a given mint
5478
async fn enquire_mint_quote(
5579
&self,
@@ -94,11 +118,81 @@ impl MintClient {
94118
);
95119
Ok(quote_client)
96120
}
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+
}
97128
}
98129

99130
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
100131
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
101132
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+
102196
async fn enquire_mint_quote(
103197
&self,
104198
mint_url: &str,
@@ -180,6 +274,38 @@ impl MintClientApi for MintClient {
180274
}
181275
}
182276

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+
183309
#[derive(Debug, Clone)]
184310
pub enum ResolveMintOffer {
185311
Accept,

0 commit comments

Comments
 (0)