Skip to main content

parakesh_common/
pk_app.rs

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;
11// use cdk_sqlite::wallet::memory;
12// use cdk_sqlite::WalletSqliteDatabase;
13
14use 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
24/// Parakesh application, based on CDK.
25pub struct PKApp {
26    /// Stores the seed
27    seedstore: SeedStore,
28    unit: CurrencyUnit,
29    /// CDK ecash store
30    store: Arc<WalletRedbDatabase>,
31    /// CDK multi-mint wallet
32    multi_mint_wallet: MultiMintWallet,
33    /// Current mint, to use with operations
34    selected_mint: Option<MintUrl>,
35}
36
37/// Summary info about the mints
38#[derive(Clone, Debug, Default)]
39pub enum MintsSummary {
40    #[default]
41    None,
42    Single(String),
43    Multiple(usize),
44}
45
46/// Ecash wallet struct
47#[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/// Used with CDK error
65#[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/// Intermediary result used in `mint_from_ln_start` and `mint_from_ln_wait`.
77#[derive(Clone, Debug)]
78pub struct MintFromLnIntermediaryResult {
79    mint_quote: cdk::wallet::MintQuote,
80    /// Set if complete (paid)
81    pub paid_result: Option<Result<u64, String>>,
82}
83
84impl PKApp {
85    /// Create new app instance
86    pub async fn new() -> Result<PKApp, String> {
87        // TODO should be in config dir
88        let secret_seed_file_name = "./parakesh.secret";
89        // TODO should be user input
90        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                // Could not read seed, generate a new one
99                // TODO do this with init, read PW, etc.
100                // TODO Should be random!
101                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, // allow weak pw
109                        )?;
110                        println!("Seed written to secret file {}", secret_seed_file_name);
111                        // Try to open again
112                        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        // Initialize the memory store
136        // let store = memory::empty().await?;
137        // TODO should be in config dir
138        let path = std::path::Path::new("./parakesh_data.dedb");
139        let store = Arc::new(WalletRedbDatabase::new(&path).unwrap());
140
141        // read the wallets, create Wallet instances
142        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, // set below
166        };
167
168        // Select first wallet
169        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    // async fn get_mint_wallet_str(
233    //     &mut self,
234    //     mint_url_str: &str,
235    // ) -> Result<Wallet, Box<dyn std::error::Error>> {
236    //     let mint_url = MintUrl::from_str(mint_url_str)?;
237    //     self.get_mint_wallet(mint_url).await
238    // }
239
240    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        // This is needed to store the mint in the store
258        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            // Receive the token
332            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            // Send the token
349            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    /// Run `mint_from_ln_start` and `mint_from_ln_wait` in sequence.
365    /// Return the invoice in a callback.
366    /// - `callback`: This callback is called with the invoice to be paid.
367    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    /// Receive Lightning: perform Mint from Lightning invoice.
378    /// First part, generate the invoice to be paid, and return it, alongside an
379    /// intermediary result with which `mint_from_ln_wait` should be invoked.
380    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            // Request a mint quote from the wallet
391            let mint_quote = wallet
392                .mint_quote(Amount::from(amount_sats), None)
393                .await
394                .map_err(|e| e.to_string())?;
395
396            // println!("Pay request: {}", quote.request);
397            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    /// Check for the status once
411    /// Returns an intermediary result, or the amount if paid
412    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                // Mint the received amount
433                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            // not paid yet
443            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    /// Second part of `mint_from_ln_start`, should be invoked with the intermediary result.
453    /// Polls for result, waits until a result is available (invoice had been paid), or timeout.
454    /// Returns the amount received.
455    /// Warning: Returns in a long time (waits until user action)
456    pub async fn mint_from_ln_wait(
457        &mut self,
458        intermediary_result: MintFromLnIntermediaryResult,
459    ) -> Result<u64, String> {
460        // Check the quote state in a loop with a timeout
461        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            // not paid, wait some more
468            int_res = res2;
469            // sleep(to_wait).await;
470            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            // Request a melt quote from the wallet
483            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            /*
490            // Check the quote state in a loop with a timeout
491            let timeout = Duration::from_secs(60); // Set a timeout duration
492            let start = std::time::Instant::now();
493
494            loop {
495                let status = wallet.melt_quote_status(&quote.id).await?;
496                println!("status {:?}", status);
497
498                if status.state == MeltQuoteState::Paid {
499                    break;
500                }
501                if start.elapsed() >= timeout {
502                    return Err("Timeout while waiting for mint quote to be paid".into());
503                }
504
505                println!("Quote state: {}", status.state);
506                sleep(Duration::from_millis(1500)).await;
507            }
508            */
509
510            // Melt the sent amount
511            let melted = wallet.melt(&quote.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}