1use cdk::amount::SplitTarget;
2use cdk::mint_url::MintUrl;
3use cdk::nuts::nut00::ProofsMethods;
4use cdk::nuts::{CurrencyUnit, MintQuoteState};
5use cdk::wallet::multi_mint_wallet::MultiMintWallet;
6use cdk::wallet::types::WalletKey;
7use cdk::wallet::{SendOptions, Wallet, WalletBuilder};
8use cdk::Amount;
9use cdk_common::database::WalletDatabase;
10use cdk_redb::WalletRedbDatabase;
11use seedstore::{ChildSpecifier, SeedStore, SeedStoreCreator};
15
16use std::collections::BTreeMap;
17use std::fmt;
18use std::str::FromStr;
19use std::sync::Arc;
20use std::time::Duration;
21
22const KEY_DERIVATION_PATH: &str = "m/84'/0'/0'/0/0";
23
24pub struct PKApp {
26 seedstore: SeedStore,
28 unit: CurrencyUnit,
29 store: Arc<WalletRedbDatabase>,
31 multi_mint_wallet: MultiMintWallet,
33 selected_mint: Option<MintUrl>,
35}
36
37#[derive(Clone, Debug, Default)]
39pub enum MintsSummary {
40 #[default]
41 None,
42 Single(String),
43 Multiple(usize),
44}
45
46#[derive(Clone, Debug, Default)]
48pub struct WalletInfo {
49 pub is_inititalized: bool,
50 pub mint_count: usize,
51 pub mints_summary: MintsSummary,
52 pub selected_mint_url: String,
53}
54
55#[derive(Clone, Debug, Default)]
56pub struct BalanceInfo(pub u64);
57
58#[derive(Clone, Debug)]
59pub struct MintInfo {
60 pub url: String,
61 pub balance: u64,
62}
63
64#[derive(Debug, PartialEq, Eq)]
66pub struct StringError(String);
67
68impl std::error::Error for StringError {}
69
70impl fmt::Display for StringError {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(f, "{}", self.0)
73 }
74}
75
76#[derive(Clone, Debug)]
78pub struct MintFromLnIntermediaryResult {
79 mint_quote: cdk::wallet::MintQuote,
80 pub paid_result: Option<Result<u64, String>>,
82}
83
84impl PKApp {
85 pub async fn new() -> Result<PKApp, String> {
87 let secret_seed_file_name = "./parakesh.secret";
89 let seed_encryption_password = "Parakesh+Password1337";
91 let seedstore = match SeedStore::new_from_encrypted_file(
92 secret_seed_file_name,
93 &seed_encryption_password.to_owned(),
94 None,
95 ) {
96 Ok(seedstore) => seedstore,
97 Err(_e) => {
98 let secret_key = [43u8; 16].to_vec();
102 match SeedStoreCreator::new_from_data(&secret_key, None, None) {
103 Ok(seedstore) => {
104 let _res = SeedStoreCreator::write_to_file(
105 &seedstore,
106 secret_seed_file_name,
107 seed_encryption_password,
108 None, )?;
110 println!("Seed written to secret file {}", secret_seed_file_name);
111 match SeedStore::new_from_encrypted_file(
113 secret_seed_file_name,
114 &seed_encryption_password.to_owned(),
115 None,
116 ) {
117 Ok(seedstore) => seedstore,
118 Err(e2) => {
119 return Err(format!(
120 "Could not read seed from freshly-generated secret file ({})",
121 e2
122 ));
123 }
124 }
125 }
126 Err(e3) => {
127 return Err(format!("Could not read seed from secret file, and could not save newly-generated seed ({})", e3));
128 }
129 }
130 }
131 };
132
133 let unit = CurrencyUnit::Sat;
134
135 let path = std::path::Path::new("./parakesh_data.dedb");
139 let store = Arc::new(WalletRedbDatabase::new(&path).unwrap());
140
141 let mut wallets: Vec<Wallet> = Vec::new();
143 let db_mints = store.get_mints().await.map_err(|e| e.to_string())?;
144 let seed_privkey = seedstore.get_secret_child_private_key(&ChildSpecifier::Derivation(
145 KEY_DERIVATION_PATH.into(),
146 ))?;
147 for (mint_url, _) in db_mints {
148 let builder = WalletBuilder::new()
149 .mint_url(mint_url.clone())
150 .unit(unit.clone())
151 .localstore(store.clone())
152 .seed(seed_privkey.as_ref());
153 let wallet = builder.build().map_err(|e| e.to_string())?;
154 wallets.push(wallet);
155 }
156
157 let wallets_len = wallets.len();
158 let multi_mint_wallet = MultiMintWallet::new(wallets);
159
160 let mut app = PKApp {
161 seedstore,
162 unit,
163 store,
164 multi_mint_wallet,
165 selected_mint: None, };
167
168 if wallets_len > 0 {
170 let _res = app
171 .select_mint_by_index(1)
172 .await
173 .map_err(|e| e.to_string())?;
174 }
175
176 Ok(app)
177 }
178
179 pub async fn get_wallet_info(&self) -> Result<WalletInfo, String> {
180 let wallets = self.multi_mint_wallet.get_wallets().await;
181 let mint_count = wallets.len();
182 let selected_mint_url = if let Some(mint) = &self.selected_mint {
183 mint.to_string()
184 } else {
185 "".to_string()
186 };
187 let mints_summary = match wallets.len() {
188 0 => MintsSummary::None,
189 1 => MintsSummary::Single(wallets[0].mint_url.to_string()),
190 _ => MintsSummary::Multiple(wallets.len()),
191 };
192 Ok(WalletInfo {
193 is_inititalized: true,
194 mint_count,
195 mints_summary,
196 selected_mint_url,
197 })
198 }
199
200 pub async fn get_balance(&self) -> Result<BalanceInfo, String> {
201 let wallet_balances: BTreeMap<MintUrl, Amount> = self
202 .multi_mint_wallet
203 .get_balances(&CurrencyUnit::Sat)
204 .await
205 .map_err(|e| e.to_string())?;
206 let total_balance: u64 = wallet_balances
207 .iter()
208 .map(|(_url, a)| {
209 let u: u64 = (*a).into();
210 u
211 })
212 .sum();
213 Ok(BalanceInfo(total_balance))
214 }
215
216 async fn get_mint_wallet(
217 &self,
218 mint_url: MintUrl,
219 ) -> Result<Wallet, Box<dyn std::error::Error>> {
220 let wallet_key = WalletKey::new(mint_url.clone(), self.unit.clone());
221 match self.multi_mint_wallet.get_wallet(&wallet_key).await {
222 Some(wallet) => Ok(wallet.clone()),
223 None => {
224 return Err(Box::new(StringError(format!(
225 "Mint not found, {}",
226 mint_url.to_string()
227 ))))
228 }
229 }
230 }
231
232 fn get_seed(&self) -> Result<[u8; 32], String> {
241 let seed_privkey = self
242 .seedstore
243 .get_secret_child_private_key(&ChildSpecifier::Derivation(KEY_DERIVATION_PATH.into()))
244 .map_err(|err| err.to_string())?;
245 Ok(seed_privkey.as_ref().clone())
246 }
247
248 pub async fn add_mint(&mut self, mint_url_str: &str) -> Result<(), String> {
249 let wallet = Wallet::new(
250 &mint_url_str.to_string(),
251 self.unit.clone(),
252 self.store.clone(),
253 &self.get_seed()?,
254 None,
255 )
256 .map_err(|err| err.to_string())?;
257 let mint_info = wallet
259 .get_mint_info()
260 .await
261 .map_err(|err| err.to_string())?;
262 if let Some(_info) = mint_info {
263 } else {
264 return Err(format!("Could not obtain mint info for {}", mint_url_str));
265 }
266 self.multi_mint_wallet.add_wallet(wallet).await;
267 let _res = self.select_mint(mint_url_str).await?;
268 Ok(())
269 }
270
271 pub fn selected_mint(&self) -> String {
272 match &self.selected_mint {
273 Some(mint) => mint.to_string(),
274 None => "(none)".to_string(),
275 }
276 }
277
278 pub async fn select_mint(&mut self, mint_url_str: &str) -> Result<String, String> {
279 let mint_url = MintUrl::from_str(mint_url_str).map_err(|e| e.to_string())?;
280 let _wallet = self
281 .get_mint_wallet(mint_url.clone())
282 .await
283 .map_err(|e| e.to_string())?;
284 self.selected_mint = Some(mint_url);
285 Ok(mint_url_str.to_owned())
286 }
287
288 pub async fn select_mint_by_index(
289 &mut self,
290 mint_index_1_based: usize,
291 ) -> Result<usize, String> {
292 let wallets = &self.multi_mint_wallet.get_wallets().await;
293 if mint_index_1_based == 0 {
294 return Err(format!(
295 "Invalid mint index {}, the first is 1",
296 mint_index_1_based
297 ));
298 }
299 let max_index = wallets.len();
300 if mint_index_1_based > max_index {
301 return Err(format!(
302 "Invalid mint index {}, maximum is {}",
303 mint_index_1_based, max_index
304 ));
305 }
306 let mint_url = &wallets[mint_index_1_based - 1].mint_url;
307 self.selected_mint = Some(mint_url.clone());
308 Ok(mint_index_1_based)
309 }
310
311 pub async fn get_mints_info(&self) -> Result<Vec<MintInfo>, String> {
312 let wallets = &self.multi_mint_wallet.get_wallets().await;
313 let mut info = Vec::new();
314 for wallet in wallets.iter() {
315 let balance: u64 = wallet.total_balance().await.unwrap_or_default().into();
316 info.push(MintInfo {
317 url: wallet.mint_url.to_string(),
318 balance,
319 });
320 }
321 Ok(info)
322 }
323
324 pub async fn receive_ecash(&mut self, token: &str) -> Result<u64, String> {
325 if let Some(sel_mint) = &self.selected_mint {
326 let wallet = self
327 .get_mint_wallet(sel_mint.clone())
328 .await
329 .map_err(|e| e.to_string())?;
330
331 let received = wallet
333 .receive(token, SplitTarget::default(), &[], &[])
334 .await
335 .map_err(|e| e.to_string())?;
336 Ok(received.into())
337 } else {
338 Err("No selected mint!".to_owned())
339 }
340 }
341
342 pub async fn send_ecash(&mut self, amount_sats: u64) -> Result<(u64, String), String> {
343 if let Some(sel_mint) = &self.selected_mint {
344 let wallet = self
345 .get_mint_wallet(sel_mint.clone())
346 .await
347 .map_err(|e| e.to_string())?;
348 let prepared_send = wallet
350 .prepare_send(Amount::from(amount_sats), SendOptions::default())
351 .await
352 .map_err(|e| e.to_string())?;
353 let token = wallet
354 .send(prepared_send, None)
355 .await
356 .map_err(|e| e.to_string())?;
357
358 Ok((amount_sats, token.to_v3_string()))
359 } else {
360 Err("No selected mint!".to_string())
361 }
362 }
363
364 pub async fn mint_from_ln<F: FnOnce(&str)>(
368 &mut self,
369 amount_sats: u64,
370 callback: F,
371 ) -> Result<u64, String> {
372 let (invoice, intermediary_res) = self.mint_from_ln_start(amount_sats).await?;
373 (callback)(invoice.as_str());
374 self.mint_from_ln_wait(intermediary_res).await
375 }
376
377 pub async fn mint_from_ln_start(
381 &mut self,
382 amount_sats: u64,
383 ) -> Result<(String, MintFromLnIntermediaryResult), String> {
384 if let Some(sel_mint) = &self.selected_mint {
385 let wallet = self
386 .get_mint_wallet(sel_mint.clone())
387 .await
388 .map_err(|e| e.to_string())?;
389
390 let mint_quote = wallet
392 .mint_quote(Amount::from(amount_sats), None)
393 .await
394 .map_err(|e| e.to_string())?;
395
396 let invoice_to_be_paid = mint_quote.request.clone();
398 Ok((
399 invoice_to_be_paid,
400 MintFromLnIntermediaryResult {
401 mint_quote,
402 paid_result: None,
403 },
404 ))
405 } else {
406 Err("No selected mint!".to_string())
407 }
408 }
409
410 pub async fn mint_from_ln_check(
413 &mut self,
414 intermediary_result: MintFromLnIntermediaryResult,
415 ) -> Result<MintFromLnIntermediaryResult, String> {
416 println!("Polling for mint result...");
417 if intermediary_result.paid_result.is_some() {
418 return Ok(intermediary_result);
419 }
420 if let Some(sel_mint) = &self.selected_mint {
421 let wallet = self
422 .get_mint_wallet(sel_mint.clone())
423 .await
424 .map_err(|e| e.to_string())?;
425
426 let quote_id = &intermediary_result.mint_quote.id;
427 let status = wallet
428 .mint_quote_state(quote_id)
429 .await
430 .map_err(|e| e.to_string())?;
431 if status.state == MintQuoteState::Paid || status.state == MintQuoteState::Issued {
432 let proofs = wallet
434 .mint(quote_id, SplitTarget::default(), None)
435 .await
436 .map_err(|e| e.to_string())?;
437 let receive_amount = proofs.total_amount().map_err(|e| e.to_string())?;
438 let mut res2 = intermediary_result;
439 res2.paid_result = Some(Ok(receive_amount.into()));
440 return Ok(res2);
441 }
442 println!("Quote state: {}", status.state);
444
445 let res2 = intermediary_result;
446 Ok(res2)
447 } else {
448 Err("No selected mint!".to_string())
449 }
450 }
451
452 pub async fn mint_from_ln_wait(
457 &mut self,
458 intermediary_result: MintFromLnIntermediaryResult,
459 ) -> Result<u64, String> {
460 let mut int_res = intermediary_result;
462 loop {
463 let res2 = self.mint_from_ln_check(int_res).await?;
464 if let Some(res) = res2.paid_result {
465 return res;
466 }
467 int_res = res2;
469 tokio::time::sleep(Duration::from_secs(2)).await;
471 }
472 }
473
474 pub async fn melt_to_ln(&mut self, ln_invoice: &str) -> Result<u64, String> {
475 if let Some(sel_mint) = &self.selected_mint {
476 let wallet = self
477 .get_mint_wallet(sel_mint.clone())
478 .await
479 .map_err(|e| e.to_string())?;
480
481 println!("About to melt_quote...");
482 let quote = wallet
484 .melt_quote(ln_invoice.to_string(), None)
485 .await
486 .map_err(|e| e.to_string())?;
487 println!("Melt quote: {} {} {:?}", quote.amount, quote.state, quote,);
488
489 let melted = wallet.melt("e.id).await.map_err(|e| e.to_string())?;
512 Ok(melted.amount.into())
513 } else {
514 Err("No selected mint!".to_owned())
515 }
516 }
517}
518
519impl MintFromLnIntermediaryResult {
520 pub fn id(&self) -> String {
521 self.mint_quote.id.clone()
522 }
523}