feat(wallet): make wallet single mint and unit

feat(wallet): cli use mint with one url and unit

feat(wallet): remove p2pk keys from wallet

feat(wallet): multimint wallet
This commit is contained in:
thesimplekid
2024-06-25 17:19:28 +01:00
parent 54c50c3724
commit 04a463be1f
31 changed files with 931 additions and 838 deletions

View File

@@ -30,7 +30,6 @@ jobs:
-p cdk --no-default-features, -p cdk --no-default-features,
-p cdk --no-default-features --features wallet, -p cdk --no-default-features --features wallet,
-p cdk --no-default-features --features mint, -p cdk --no-default-features --features mint,
-p cdk --no-default-features --features wallet --features nostr,
-p cdk-redb, -p cdk-redb,
-p cdk-sqlite, -p cdk-sqlite,
--bin cdk-cli, --bin cdk-cli,
@@ -68,7 +67,6 @@ jobs:
-p cdk, -p cdk,
-p cdk --no-default-features, -p cdk --no-default-features,
-p cdk --no-default-features --features wallet, -p cdk --no-default-features --features wallet,
-p cdk --no-default-features --features wallet --features nostr,
-p cdk-js -p cdk-js
] ]
steps: steps:

View File

@@ -1,2 +1,2 @@
[language-server.rust-analyzer.config] [language-server.rust-analyzer.config]
cargo = { features = ["wallet", "mint", "nostr"] } cargo = { features = ["wallet", "mint"] }

View File

@@ -1,12 +1,10 @@
//! Wallet Js Bindings //! Wallet Js Bindings
use std::ops::Deref; use std::ops::Deref;
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use cdk::amount::SplitTarget; use cdk::amount::SplitTarget;
use cdk::nuts::Proofs; use cdk::nuts::{Proofs, SecretKey};
use cdk::url::UncheckedUrl;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::Amount; use cdk::Amount;
use cdk_rexie::RexieWalletDatabase; use cdk_rexie::RexieWalletDatabase;
@@ -43,38 +41,10 @@ impl From<Wallet> for JsWallet {
#[wasm_bindgen(js_class = Wallet)] #[wasm_bindgen(js_class = Wallet)]
impl JsWallet { impl JsWallet {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub async fn new(seed: Vec<u8>, p2pk_signing_keys: Vec<JsSecretKey>) -> Self { pub async fn new(mints_url: String, unit: JsCurrencyUnit, seed: Vec<u8>) -> Self {
let db = RexieWalletDatabase::new().await.unwrap(); let db = RexieWalletDatabase::new().await.unwrap();
Wallet::new( Wallet::new(&mints_url, unit.into(), Arc::new(db), &seed).into()
Arc::new(db),
&seed,
p2pk_signing_keys
.into_iter()
.map(|s| s.deref().clone())
.collect(),
)
.into()
}
#[wasm_bindgen(js_name = unitBalance)]
pub async fn unit_balance(&self, unit: JsCurrencyUnit) -> Result<JsAmount> {
Ok(self
.inner
.unit_balance(unit.into())
.await
.map_err(into_err)?
.into())
}
#[wasm_bindgen(js_name = pendingUnitBalance)]
pub async fn pending_unit_balance(&self, unit: JsCurrencyUnit) -> Result<JsAmount> {
Ok(self
.inner
.pending_unit_balance(unit.into())
.await
.map_err(into_err)?
.into())
} }
#[wasm_bindgen(js_name = totalBalance)] #[wasm_bindgen(js_name = totalBalance)]
@@ -92,64 +62,36 @@ impl JsWallet {
} }
#[wasm_bindgen(js_name = checkAllPendingProofs)] #[wasm_bindgen(js_name = checkAllPendingProofs)]
pub async fn check_all_pending_proofs( pub async fn check_all_pending_proofs(&self) -> Result<JsAmount> {
&self,
mint_url: Option<String>,
unit: Option<JsCurrencyUnit>,
) -> Result<JsAmount> {
let mint_url = match mint_url {
Some(url) => Some(UncheckedUrl::from_str(&url).map_err(into_err)?),
None => None,
};
Ok(self Ok(self
.inner .inner
.check_all_pending_proofs(mint_url, unit.map(|u| u.into())) .check_all_pending_proofs()
.await .await
.map_err(into_err)? .map_err(into_err)?
.into()) .into())
} }
#[wasm_bindgen(js_name = mintBalances)] #[wasm_bindgen(js_name = getMintInfo)]
pub async fn mint_balances(&self) -> Result<JsValue> { pub async fn get_mint_info(&self) -> Result<Option<JsMintInfo>> {
let mint_balances = self.inner.mint_balances().await.map_err(into_err)?;
Ok(serde_wasm_bindgen::to_value(&mint_balances)?)
}
#[wasm_bindgen(js_name = addMint)]
pub async fn add_mint(&self, mint_url: String) -> Result<Option<JsMintInfo>> {
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
Ok(self Ok(self
.inner .inner
.add_mint(mint_url) .get_mint_info()
.await .await
.map_err(into_err)? .map_err(into_err)?
.map(|i| i.into())) .map(|i| i.into()))
} }
#[wasm_bindgen(js_name = refreshMint)] #[wasm_bindgen(js_name = refreshMint)]
pub async fn refresh_mint_keys(&self, mint_url: String) -> Result<()> { pub async fn refresh_mint_keys(&self) -> Result<()> {
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?; self.inner.refresh_mint_keys().await.map_err(into_err)?;
self.inner
.refresh_mint_keys(&mint_url)
.await
.map_err(into_err)?;
Ok(()) Ok(())
} }
#[wasm_bindgen(js_name = mintQuote)] #[wasm_bindgen(js_name = mintQuote)]
pub async fn mint_quote( pub async fn mint_quote(&mut self, amount: u64) -> Result<JsMintQuote> {
&mut self,
mint_url: String,
amount: u64,
unit: JsCurrencyUnit,
) -> Result<JsMintQuote> {
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
let quote = self let quote = self
.inner .inner
.mint_quote(mint_url, unit.into(), amount.into()) .mint_quote(amount.into())
.await .await
.map_err(into_err)?; .map_err(into_err)?;
@@ -157,16 +99,10 @@ impl JsWallet {
} }
#[wasm_bindgen(js_name = mintQuoteStatus)] #[wasm_bindgen(js_name = mintQuoteStatus)]
pub async fn mint_quote_status( pub async fn mint_quote_status(&self, quote_id: String) -> Result<JsMintQuoteBolt11Response> {
&self,
mint_url: String,
quote_id: String,
) -> Result<JsMintQuoteBolt11Response> {
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
let quote = self let quote = self
.inner .inner
.mint_quote_status(mint_url, &quote_id) .mint_quote_status(&quote_id)
.await .await
.map_err(into_err)?; .map_err(into_err)?;
@@ -183,7 +119,6 @@ impl JsWallet {
#[wasm_bindgen(js_name = mint)] #[wasm_bindgen(js_name = mint)]
pub async fn mint( pub async fn mint(
&mut self, &mut self,
mint_url: String,
quote_id: String, quote_id: String,
p2pk_condition: Option<JsP2PKSpendingConditions>, p2pk_condition: Option<JsP2PKSpendingConditions>,
htlc_condition: Option<JsHTLCSpendingConditions>, htlc_condition: Option<JsHTLCSpendingConditions>,
@@ -192,7 +127,6 @@ impl JsWallet {
let target = split_target_amount let target = split_target_amount
.map(|a| SplitTarget::Value(*a.deref())) .map(|a| SplitTarget::Value(*a.deref()))
.unwrap_or_default(); .unwrap_or_default();
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
let conditions = match (p2pk_condition, htlc_condition) { let conditions = match (p2pk_condition, htlc_condition) {
(Some(_), Some(_)) => { (Some(_), Some(_)) => {
return Err(JsValue::from_str( return Err(JsValue::from_str(
@@ -206,7 +140,7 @@ impl JsWallet {
Ok(self Ok(self
.inner .inner
.mint(mint_url, &quote_id, target, conditions) .mint(&quote_id, target, conditions)
.await .await
.map_err(into_err)? .map_err(into_err)?
.into()) .into())
@@ -215,20 +149,12 @@ impl JsWallet {
#[wasm_bindgen(js_name = meltQuote)] #[wasm_bindgen(js_name = meltQuote)]
pub async fn melt_quote( pub async fn melt_quote(
&mut self, &mut self,
mint_url: String,
unit: JsCurrencyUnit,
request: String, request: String,
mpp_amount: Option<JsAmount>, mpp_amount: Option<JsAmount>,
) -> Result<JsMeltQuote> { ) -> Result<JsMeltQuote> {
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
let melt_quote = self let melt_quote = self
.inner .inner
.melt_quote( .melt_quote(request, mpp_amount.map(|a| *a.deref()))
mint_url,
unit.into(),
request,
mpp_amount.map(|a| *a.deref()),
)
.await .await
.map_err(into_err)?; .map_err(into_err)?;
@@ -236,16 +162,10 @@ impl JsWallet {
} }
#[wasm_bindgen(js_name = meltQuoteStatus)] #[wasm_bindgen(js_name = meltQuoteStatus)]
pub async fn melt_quote_status( pub async fn melt_quote_status(&self, quote_id: String) -> Result<JsMeltQuoteBolt11Response> {
&self,
mint_url: String,
quote_id: String,
) -> Result<JsMeltQuoteBolt11Response> {
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
let quote = self let quote = self
.inner .inner
.melt_quote_status(mint_url, &quote_id) .melt_quote_status(&quote_id)
.await .await
.map_err(into_err)?; .map_err(into_err)?;
@@ -255,31 +175,35 @@ impl JsWallet {
#[wasm_bindgen(js_name = melt)] #[wasm_bindgen(js_name = melt)]
pub async fn melt( pub async fn melt(
&mut self, &mut self,
mint_url: String,
quote_id: String, quote_id: String,
split_target_amount: Option<JsAmount>, split_target_amount: Option<JsAmount>,
) -> Result<JsMelted> { ) -> Result<JsMelted> {
let target = split_target_amount let target = split_target_amount
.map(|a| SplitTarget::Value(*a.deref())) .map(|a| SplitTarget::Value(*a.deref()))
.unwrap_or_default(); .unwrap_or_default();
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
let melted = self let melted = self.inner.melt(&quote_id, target).await.map_err(into_err)?;
.inner
.melt(&mint_url, &quote_id, target)
.await
.map_err(into_err)?;
Ok(melted.into()) Ok(melted.into())
} }
#[wasm_bindgen(js_name = receive)] #[wasm_bindgen(js_name = receive)]
pub async fn receive(&mut self, encoded_token: String, preimages: JsValue) -> Result<JsAmount> { pub async fn receive(
let preimages: Option<Vec<String>> = serde_wasm_bindgen::from_value(preimages)?; &mut self,
encoded_token: String,
signing_keys: Vec<JsSecretKey>,
preimages: Vec<String>,
) -> Result<JsAmount> {
let signing_keys: Vec<SecretKey> = signing_keys.iter().map(|s| s.deref().clone()).collect();
Ok(self Ok(self
.inner .inner
.receive(&encoded_token, &SplitTarget::default(), preimages) .receive(
&encoded_token,
&SplitTarget::default(),
&signing_keys,
&preimages,
)
.await .await
.map_err(into_err)? .map_err(into_err)?
.into()) .into())
@@ -289,8 +213,6 @@ impl JsWallet {
#[wasm_bindgen(js_name = send)] #[wasm_bindgen(js_name = send)]
pub async fn send( pub async fn send(
&mut self, &mut self,
mint_url: String,
unit: JsCurrencyUnit,
memo: Option<String>, memo: Option<String>,
amount: u64, amount: u64,
p2pk_condition: Option<JsP2PKSpendingConditions>, p2pk_condition: Option<JsP2PKSpendingConditions>,
@@ -308,20 +230,11 @@ impl JsWallet {
(None, None) => None, (None, None) => None,
}; };
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
let target = split_target_amount let target = split_target_amount
.map(|a| SplitTarget::Value(*a.deref())) .map(|a| SplitTarget::Value(*a.deref()))
.unwrap_or_default(); .unwrap_or_default();
self.inner self.inner
.send( .send(Amount::from(amount), memo, conditions, &target)
&mint_url,
unit.into(),
Amount::from(amount),
memo,
conditions,
&target,
)
.await .await
.map_err(into_err) .map_err(into_err)
} }
@@ -330,8 +243,6 @@ impl JsWallet {
#[wasm_bindgen(js_name = swap)] #[wasm_bindgen(js_name = swap)]
pub async fn swap( pub async fn swap(
&mut self, &mut self,
mint_url: String,
unit: JsCurrencyUnit,
amount: u64, amount: u64,
input_proofs: Vec<JsProof>, input_proofs: Vec<JsProof>,
p2pk_condition: Option<JsP2PKSpendingConditions>, p2pk_condition: Option<JsP2PKSpendingConditions>,
@@ -349,8 +260,6 @@ impl JsWallet {
(None, None) => None, (None, None) => None,
}; };
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
let proofs: Proofs = input_proofs.iter().map(|p| p.deref()).cloned().collect(); let proofs: Proofs = input_proofs.iter().map(|p| p.deref()).cloned().collect();
let target = split_target_amount let target = split_target_amount
@@ -358,14 +267,7 @@ impl JsWallet {
.unwrap_or_default(); .unwrap_or_default();
let post_swap_proofs = self let post_swap_proofs = self
.inner .inner
.swap( .swap(Some(Amount::from(amount)), &target, proofs, conditions)
&mint_url,
&unit.into(),
Some(Amount::from(amount)),
&target,
proofs,
conditions,
)
.await .await
.map_err(into_err)?; .map_err(into_err)?;

View File

@@ -13,9 +13,9 @@ license.workspace = true
[dependencies] [dependencies]
anyhow = "1.0.75" anyhow = "1.0.75"
bip39.workspace = true bip39.workspace = true
cdk = { workspace = true, default-features = false, features = ["wallet", "nostr"] } cdk = { workspace = true, default-features = false, features = ["wallet"] }
cdk-redb = { workspace = true, default-features = false, features = ["wallet", "nostr"] } cdk-redb = { workspace = true, default-features = false, features = ["wallet"] }
cdk-sqlite = { workspace = true, default-features = false, features = ["wallet", "nostr"] } cdk-sqlite = { workspace = true, default-features = false, features = ["wallet"] }
clap = { version = "4.4.8", features = ["derive", "env"] } clap = { version = "4.4.8", features = ["derive", "env"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
@@ -24,3 +24,7 @@ tracing.workspace = true
tracing-subscriber = "0.3.18" tracing-subscriber = "0.3.18"
rand = "0.8.5" rand = "0.8.5"
home = "0.5.9" home = "0.5.9"
nostr-sdk = { version = "0.31.0", default-features = false, features = [
"nip04",
"nip44"
]}

View File

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
@@ -5,9 +6,9 @@ use std::sync::Arc;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use bip39::Mnemonic; use bip39::Mnemonic;
use cdk::cdk_database;
use cdk::cdk_database::WalletDatabase; use cdk::cdk_database::WalletDatabase;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::{cdk_database, UncheckedUrl};
use cdk_redb::RedbWalletDatabase; use cdk_redb::RedbWalletDatabase;
use cdk_sqlite::WalletSQLiteDatabase; use cdk_sqlite::WalletSQLiteDatabase;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
@@ -117,38 +118,63 @@ async fn main() -> Result<()> {
} }
}; };
let wallet = Wallet::new(localstore, &mnemonic.to_seed_normalized(""), vec![]); let mut wallets: HashMap<UncheckedUrl, Wallet> = HashMap::new();
let mints = localstore.get_mints().await?;
for (mint, _) in mints {
let wallet = Wallet::new(
&mint.to_string(),
cdk::nuts::CurrencyUnit::Sat,
localstore.clone(),
&mnemonic.to_seed_normalized(""),
);
wallets.insert(mint, wallet);
}
match &args.command { match &args.command {
Commands::DecodeToken(sub_command_args) => { Commands::DecodeToken(sub_command_args) => {
sub_commands::decode_token::decode_token(sub_command_args) sub_commands::decode_token::decode_token(sub_command_args)
} }
Commands::Balance => sub_commands::balance::balance(wallet).await, Commands::Balance => sub_commands::balance::balance(wallets).await,
Commands::Melt(sub_command_args) => { Commands::Melt(sub_command_args) => {
sub_commands::melt::melt(wallet, sub_command_args).await sub_commands::melt::melt(wallets, sub_command_args).await
} }
Commands::Receive(sub_command_args) => { Commands::Receive(sub_command_args) => {
sub_commands::receive::receive(wallet, sub_command_args).await sub_commands::receive::receive(
wallets,
&mnemonic.to_seed_normalized(""),
localstore,
sub_command_args,
)
.await
} }
Commands::Send(sub_command_args) => { Commands::Send(sub_command_args) => {
sub_commands::send::send(wallet, sub_command_args).await sub_commands::send::send(wallets, sub_command_args).await
} }
Commands::CheckSpendable => sub_commands::check_spent::check_spent(wallet).await, Commands::CheckSpendable => sub_commands::check_spent::check_spent(wallets).await,
Commands::MintInfo(sub_command_args) => { Commands::MintInfo(sub_command_args) => {
sub_commands::mint_info::mint_info(sub_command_args).await sub_commands::mint_info::mint_info(sub_command_args).await
} }
Commands::Mint(sub_command_args) => { Commands::Mint(sub_command_args) => {
sub_commands::mint::mint(wallet, sub_command_args).await sub_commands::mint::mint(
wallets,
&mnemonic.to_seed_normalized(""),
localstore,
sub_command_args,
)
.await
} }
Commands::PendingMint => sub_commands::pending_mints::pending_mints(wallet).await, Commands::PendingMint => sub_commands::pending_mints::pending_mints(wallets).await,
Commands::Burn(sub_command_args) => { Commands::Burn(sub_command_args) => {
sub_commands::burn::burn(wallet, sub_command_args).await sub_commands::burn::burn(wallets, sub_command_args).await
} }
Commands::Restore(sub_command_args) => { Commands::Restore(sub_command_args) => {
sub_commands::restore::restore(wallet, sub_command_args).await sub_commands::restore::restore(wallets, sub_command_args).await
} }
Commands::UpdateMintUrl(sub_command_args) => { Commands::UpdateMintUrl(sub_command_args) => {
sub_commands::update_mint_url::update_mint_url(wallet, sub_command_args).await sub_commands::update_mint_url::update_mint_url(wallets, sub_command_args).await
} }
} }
} }

View File

@@ -1,29 +1,25 @@
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::Result; use anyhow::Result;
use cdk::nuts::CurrencyUnit;
use cdk::url::UncheckedUrl; use cdk::url::UncheckedUrl;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::Amount; use cdk::Amount;
pub async fn balance(wallet: Wallet) -> Result<()> { pub async fn balance(wallets: HashMap<UncheckedUrl, Wallet>) -> Result<()> {
let _ = mint_balances(&wallet).await; mint_balances(wallets).await?;
Ok(()) Ok(())
} }
pub async fn mint_balances( pub async fn mint_balances(
wallet: &Wallet, wallets: HashMap<UncheckedUrl, Wallet>,
) -> Result<Vec<(UncheckedUrl, HashMap<CurrencyUnit, Amount>)>> { ) -> Result<Vec<(Wallet, Amount)>> {
let mints_amounts: Vec<(UncheckedUrl, HashMap<_, _>)> = let mut wallets_vec: Vec<(Wallet, Amount)> = Vec::with_capacity(wallets.capacity());
wallet.mint_balances().await?.into_iter().collect();
for (i, (mint, balance)) in mints_amounts.iter().enumerate() { for (i, (mint_url, wallet)) in wallets.iter().enumerate() {
println!("{i}: {mint}:"); let mint_url = mint_url.clone();
for (unit, amount) in balance { let amount = wallet.total_balance().await?;
println!("- {amount} {unit}"); println!("{i}: {mint_url} {amount}");
wallets_vec.push((wallet.clone(), amount));
} }
println!("---------"); Ok(wallets_vec)
}
Ok(mints_amounts)
} }

View File

@@ -1,18 +1,34 @@
use std::collections::HashMap;
use anyhow::Result; use anyhow::Result;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::{Amount, UncheckedUrl};
use clap::Args; use clap::Args;
#[derive(Args)] #[derive(Args)]
pub struct BurnSubCommand { pub struct BurnSubCommand {
/// Mint Url /// Mint Url
mint_url: Option<String>, mint_url: Option<UncheckedUrl>,
} }
pub async fn burn(wallet: Wallet, sub_command_args: &BurnSubCommand) -> Result<()> { pub async fn burn(
let amount_burnt = wallet wallets: HashMap<UncheckedUrl, Wallet>,
.check_all_pending_proofs(sub_command_args.mint_url.clone().map(|u| u.into()), None) sub_command_args: &BurnSubCommand,
.await?; ) -> Result<()> {
let mut total_burnt = Amount::ZERO;
match &sub_command_args.mint_url {
Some(mint_url) => {
let wallet = wallets.get(mint_url).unwrap();
total_burnt = wallet.check_all_pending_proofs().await?;
}
None => {
for wallet in wallets.values() {
let amount_burnt = wallet.check_all_pending_proofs().await?;
total_burnt += amount_burnt;
}
}
}
println!("{amount_burnt} burned"); println!("{total_burnt} burned");
Ok(()) Ok(())
} }

View File

@@ -1,40 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Write; use std::println;
use std::{io, println};
use anyhow::{bail, Result}; use anyhow::Result;
use cdk::url::UncheckedUrl; use cdk::url::UncheckedUrl;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
pub async fn check_spent(wallet: Wallet) -> Result<()> { pub async fn check_spent(wallets: HashMap<UncheckedUrl, Wallet>) -> Result<()> {
let mints_amounts: Vec<(UncheckedUrl, HashMap<_, _>)> = for wallet in wallets.values() {
wallet.mint_balances().await?.into_iter().collect(); let amount = wallet.check_all_pending_proofs().await?;
for (i, (mint, amount)) in mints_amounts.iter().enumerate() { println!("Amount marked as spent: {}", amount);
println!("{}: {}, {:?} sats", i, mint, amount);
}
println!("Enter mint number to create token");
let mut user_input = String::new();
let stdin = io::stdin();
io::stdout().flush().unwrap();
stdin.read_line(&mut user_input)?;
let mint_number: usize = user_input.trim().parse()?;
if mint_number.gt(&(mints_amounts.len() - 1)) {
bail!("Invalid mint number");
}
let mint_url = mints_amounts[mint_number].0.clone();
let proofs = wallet.get_proofs(mint_url.clone()).await?.unwrap();
let send_proofs = wallet.check_proofs_spent(mint_url, proofs.to_vec()).await?;
for proof in send_proofs {
println!("{:#?}", proof);
} }
Ok(()) Ok(())

View File

@@ -1,12 +1,12 @@
use std::collections::HashMap;
use std::io::Write; use std::io::Write;
use std::str::FromStr; use std::str::FromStr;
use std::{io, println}; use std::{io, println};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use cdk::amount::SplitTarget; use cdk::amount::SplitTarget;
use cdk::nuts::CurrencyUnit;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::Bolt11Invoice; use cdk::{Bolt11Invoice, UncheckedUrl};
use clap::Args; use clap::Args;
use crate::sub_commands::balance::mint_balances; use crate::sub_commands::balance::mint_balances;
@@ -14,8 +14,11 @@ use crate::sub_commands::balance::mint_balances;
#[derive(Args)] #[derive(Args)]
pub struct MeltSubCommand {} pub struct MeltSubCommand {}
pub async fn melt(wallet: Wallet, _sub_command_args: &MeltSubCommand) -> Result<()> { pub async fn melt(
let mints_amounts = mint_balances(&wallet).await?; wallets: HashMap<UncheckedUrl, Wallet>,
_sub_command_args: &MeltSubCommand,
) -> Result<()> {
let mints_amounts = mint_balances(wallets).await?;
println!("Enter mint number to create token"); println!("Enter mint number to create token");
@@ -30,7 +33,7 @@ pub async fn melt(wallet: Wallet, _sub_command_args: &MeltSubCommand) -> Result<
bail!("Invalid mint number"); bail!("Invalid mint number");
} }
let mint_url = mints_amounts[mint_number].0.clone(); let wallet = mints_amounts[mint_number].0.clone();
println!("Enter bolt11 invoice request"); println!("Enter bolt11 invoice request");
@@ -43,26 +46,14 @@ pub async fn melt(wallet: Wallet, _sub_command_args: &MeltSubCommand) -> Result<
if bolt11 if bolt11
.amount_milli_satoshis() .amount_milli_satoshis()
.unwrap() .unwrap()
.gt(&(<cdk::Amount as Into<u64>>::into( .gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * 1000_u64))
*mints_amounts[mint_number]
.1
.get(&CurrencyUnit::Sat)
.unwrap(),
) * 1000_u64))
{ {
bail!("Not enough funds"); bail!("Not enough funds");
} }
let quote = wallet let quote = wallet.melt_quote(bolt11.to_string(), None).await?;
.melt_quote(
mint_url.clone(),
cdk::nuts::CurrencyUnit::Sat,
bolt11.to_string(),
None,
)
.await?;
let melt = wallet let melt = wallet
.melt(&mint_url, &quote.id, SplitTarget::default()) .melt(&quote.id, SplitTarget::default())
.await .await
.unwrap(); .unwrap();

View File

@@ -1,7 +1,10 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use cdk::amount::SplitTarget; use cdk::amount::SplitTarget;
use cdk::cdk_database::{Error, WalletDatabase};
use cdk::nuts::CurrencyUnit; use cdk::nuts::CurrencyUnit;
use cdk::url::UncheckedUrl; use cdk::url::UncheckedUrl;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
@@ -19,15 +22,20 @@ pub struct MintSubCommand {
unit: String, unit: String,
} }
pub async fn mint(wallet: Wallet, sub_command_args: &MintSubCommand) -> Result<()> { pub async fn mint(
wallets: HashMap<UncheckedUrl, Wallet>,
seed: &[u8],
localstore: Arc<dyn WalletDatabase<Err = Error> + Sync + Send>,
sub_command_args: &MintSubCommand,
) -> Result<()> {
let mint_url = sub_command_args.mint_url.clone(); let mint_url = sub_command_args.mint_url.clone();
let wallet = match wallets.get(&mint_url) {
Some(wallet) => wallet.clone(),
None => Wallet::new(&mint_url.to_string(), CurrencyUnit::Sat, localstore, seed),
};
let quote = wallet let quote = wallet
.mint_quote( .mint_quote(Amount::from(sub_command_args.amount))
mint_url.clone(),
CurrencyUnit::from(&sub_command_args.unit),
Amount::from(sub_command_args.amount),
)
.await?; .await?;
println!("Quote: {:#?}", quote); println!("Quote: {:#?}", quote);
@@ -35,9 +43,7 @@ pub async fn mint(wallet: Wallet, sub_command_args: &MintSubCommand) -> Result<(
println!("Please pay: {}", quote.request); println!("Please pay: {}", quote.request);
loop { loop {
let status = wallet let status = wallet.mint_quote_status(&quote.id).await?;
.mint_quote_status(mint_url.clone(), &quote.id)
.await?;
if status.paid { if status.paid {
break; break;
@@ -46,9 +52,7 @@ pub async fn mint(wallet: Wallet, sub_command_args: &MintSubCommand) -> Result<(
sleep(Duration::from_secs(2)).await; sleep(Duration::from_secs(2)).await;
} }
let receive_amount = wallet let receive_amount = wallet.mint(&quote.id, SplitTarget::default(), None).await?;
.mint(mint_url.clone(), &quote.id, SplitTarget::default(), None)
.await?;
println!("Received {receive_amount} from mint {mint_url}"); println!("Received {receive_amount} from mint {mint_url}");

View File

@@ -1,8 +1,15 @@
use std::collections::HashMap;
use anyhow::Result; use anyhow::Result;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::{Amount, UncheckedUrl};
pub async fn pending_mints(wallet: Wallet) -> Result<()> { pub async fn pending_mints(wallets: HashMap<UncheckedUrl, Wallet>) -> Result<()> {
let amount_claimed = wallet.check_all_mint_quotes().await?; let mut amount_claimed = Amount::ZERO;
for wallet in wallets.values() {
let claimed = wallet.check_all_mint_quotes().await?;
amount_claimed += claimed;
}
println!("Amount minted: {amount_claimed}"); println!("Amount minted: {amount_claimed}");
Ok(()) Ok(())

View File

@@ -1,10 +1,17 @@
use std::collections::{HashMap, HashSet};
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use cdk::amount::SplitTarget; use cdk::amount::SplitTarget;
use cdk::nuts::SecretKey; use cdk::cdk_database::{Error, WalletDatabase};
use cdk::nuts::{CurrencyUnit, SecretKey, Token};
use cdk::util::unix_time;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::{Amount, UncheckedUrl};
use clap::Args; use clap::Args;
use nostr_sdk::nips::nip04;
use nostr_sdk::{Filter, Keys, Kind, Timestamp};
#[derive(Args)] #[derive(Args)]
pub struct ReceiveSubCommand { pub struct ReceiveSubCommand {
@@ -27,54 +34,82 @@ pub struct ReceiveSubCommand {
preimage: Vec<String>, preimage: Vec<String>,
} }
pub async fn receive(wallet: Wallet, sub_command_args: &ReceiveSubCommand) -> Result<()> { pub async fn receive(
wallets: HashMap<UncheckedUrl, Wallet>,
seed: &[u8],
localstore: Arc<dyn WalletDatabase<Err = Error> + Sync + Send>,
sub_command_args: &ReceiveSubCommand,
) -> Result<()> {
let mut signing_keys = Vec::new();
if !sub_command_args.signing_key.is_empty() {
let mut s_keys: Vec<SecretKey> = sub_command_args
.signing_key
.iter()
.map(|s| SecretKey::from_str(s).unwrap())
.collect();
signing_keys.append(&mut s_keys);
}
let amount = match &sub_command_args.token {
Some(token_str) => {
receive_token(
token_str,
wallets,
seed,
&localstore,
&signing_keys,
&sub_command_args.preimage,
)
.await?
}
None => {
//wallet.add_p2pk_signing_key(nostr_signing_key).await;
let nostr_key = match sub_command_args.nostr_key.as_ref() { let nostr_key = match sub_command_args.nostr_key.as_ref() {
Some(nostr_key) => { Some(nostr_key) => {
let secret_key = SecretKey::from_str(nostr_key)?; let secret_key = SecretKey::from_str(nostr_key)?;
wallet.add_p2pk_signing_key(secret_key.clone()).await;
Some(secret_key) Some(secret_key)
} }
None => None, None => None,
}; };
if !sub_command_args.signing_key.is_empty() { let nostr_key =
let signing_keys: Vec<SecretKey> = sub_command_args nostr_key.ok_or(anyhow!("Nostr key required if token is not provided"))?;
.signing_key
.iter()
.map(|s| SecretKey::from_str(s).unwrap())
.collect();
for signing_key in signing_keys { signing_keys.push(nostr_key.clone());
wallet.add_p2pk_signing_key(signing_key).await;
}
}
let preimage = match sub_command_args.preimage.is_empty() { let relays = sub_command_args.relay.clone();
true => None, let since = localstore
false => Some(sub_command_args.preimage.clone()), .get_nostr_last_checked(&nostr_key.public_key())
};
let amount = match nostr_key {
Some(nostr_key) => {
assert!(!sub_command_args.relay.is_empty());
wallet
.add_nostr_relays(sub_command_args.relay.clone())
.await?; .await?;
wallet
.nostr_receive(nostr_key, sub_command_args.since, SplitTarget::default()) let tokens = nostr_receive(relays, nostr_key.clone(), since).await?;
.await?
} let mut total_amount = Amount::ZERO;
None => { for token_str in &tokens {
wallet match receive_token(
.receive( token_str,
sub_command_args wallets.clone(),
.token seed,
.as_ref() &localstore,
.ok_or(anyhow!("Token Required"))?, &signing_keys,
&SplitTarget::default(), &sub_command_args.preimage,
preimage,
) )
.await? .await
{
Ok(amount) => {
total_amount += amount;
}
Err(err) => {
println!("{}", err);
}
}
}
localstore
.add_nostr_last_checked(nostr_key.public_key(), unix_time() as u32)
.await?;
total_amount
} }
}; };
@@ -82,3 +117,82 @@ pub async fn receive(wallet: Wallet, sub_command_args: &ReceiveSubCommand) -> Re
Ok(()) Ok(())
} }
async fn receive_token(
token_str: &str,
wallets: HashMap<UncheckedUrl, Wallet>,
seed: &[u8],
localstore: &Arc<dyn WalletDatabase<Err = Error> + Sync + Send>,
signing_keys: &[SecretKey],
preimage: &[String],
) -> Result<Amount> {
let token = Token::from_str(token_str)?;
let mint_url = token.token.first().unwrap().mint.clone();
let wallet = match wallets.get(&mint_url) {
Some(wallet) => wallet.clone(),
None => Wallet::new(
&mint_url.to_string(),
CurrencyUnit::Sat,
Arc::clone(localstore),
seed,
),
};
let amount = wallet
.receive(token_str, &SplitTarget::default(), signing_keys, preimage)
.await?;
Ok(amount)
}
/// Receive tokens sent to nostr pubkey via dm
async fn nostr_receive(
relays: Vec<String>,
nostr_signing_key: SecretKey,
since: Option<u32>,
) -> Result<HashSet<String>> {
let verifying_key = nostr_signing_key.public_key();
let x_only_pubkey = verifying_key.x_only_public_key();
let nostr_pubkey = nostr_sdk::PublicKey::from_hex(x_only_pubkey.to_string())?;
let since = since.map(|s| Timestamp::from(s as u64));
let filter = match since {
Some(since) => Filter::new()
.pubkey(nostr_pubkey)
.kind(Kind::EncryptedDirectMessage)
.since(since),
None => Filter::new()
.pubkey(nostr_pubkey)
.kind(Kind::EncryptedDirectMessage),
};
let client = nostr_sdk::Client::default();
client.add_relays(relays).await?;
client.connect().await;
let events = client.get_events_of(vec![filter], None).await?;
let mut tokens: HashSet<String> = HashSet::new();
let keys = Keys::from_str(&(nostr_signing_key).to_secret_hex())?;
for event in events {
if event.kind() == Kind::EncryptedDirectMessage {
if let Ok(msg) = nip04::decrypt(keys.secret_key()?, event.author_ref(), event.content())
{
if let Some(token) = cdk::wallet::util::token_from_text(&msg) {
tokens.insert(token.to_string());
}
} else {
tracing::error!("Impossible to decrypt direct message");
}
}
}
Ok(tokens)
}

View File

@@ -1,4 +1,6 @@
use anyhow::Result; use std::collections::HashMap;
use anyhow::{anyhow, Result};
use cdk::url::UncheckedUrl; use cdk::url::UncheckedUrl;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use clap::Args; use clap::Args;
@@ -9,10 +11,15 @@ pub struct RestoreSubCommand {
mint_url: UncheckedUrl, mint_url: UncheckedUrl,
} }
pub async fn restore(wallet: Wallet, sub_command_args: &RestoreSubCommand) -> Result<()> { pub async fn restore(
let mint_url = sub_command_args.mint_url.clone(); wallets: HashMap<UncheckedUrl, Wallet>,
sub_command_args: &RestoreSubCommand,
) -> Result<()> {
let wallet = wallets
.get(&sub_command_args.mint_url)
.ok_or(anyhow!("Unknown mint url"))?;
let amount = wallet.restore(mint_url).await?; let amount = wallet.restore().await?;
println!("Restored {}", amount); println!("Restored {}", amount);

View File

@@ -1,12 +1,13 @@
use std::collections::HashMap;
use std::io::Write; use std::io::Write;
use std::str::FromStr; use std::str::FromStr;
use std::{io, println}; use std::{io, println};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use cdk::amount::SplitTarget; use cdk::amount::SplitTarget;
use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions}; use cdk::nuts::{Conditions, PublicKey, SpendingConditions};
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::Amount; use cdk::{Amount, UncheckedUrl};
use clap::Args; use clap::Args;
use crate::sub_commands::balance::mint_balances; use crate::sub_commands::balance::mint_balances;
@@ -33,8 +34,11 @@ pub struct SendSubCommand {
refund_keys: Vec<String>, refund_keys: Vec<String>,
} }
pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<()> { pub async fn send(
let mints_amounts = mint_balances(&wallet).await?; wallets: HashMap<UncheckedUrl, Wallet>,
sub_command_args: &SendSubCommand,
) -> Result<()> {
let mints_amounts = mint_balances(wallets).await?;
println!("Enter mint number to create token"); println!("Enter mint number to create token");
@@ -49,8 +53,6 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<(
bail!("Invalid mint number"); bail!("Invalid mint number");
} }
let mint_url = mints_amounts[mint_number].0.clone();
println!("Enter value of token in sats"); println!("Enter value of token in sats");
let mut user_input = String::new(); let mut user_input = String::new();
@@ -59,11 +61,7 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<(
stdin.read_line(&mut user_input)?; stdin.read_line(&mut user_input)?;
let token_amount = Amount::from(user_input.trim().parse::<u64>()?); let token_amount = Amount::from(user_input.trim().parse::<u64>()?);
if token_amount.gt(mints_amounts[mint_number] if token_amount.gt(&mints_amounts[mint_number].1) {
.1
.get(&CurrencyUnit::Sat)
.unwrap())
{
bail!("Not enough funds"); bail!("Not enough funds");
} }
@@ -145,10 +143,10 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<(
}, },
}; };
let wallet = mints_amounts[mint_number].0.clone();
let token = wallet let token = wallet
.send( .send(
&mint_url,
CurrencyUnit::Sat,
token_amount, token_amount,
sub_command_args.memo.clone(), sub_command_args.memo.clone(),
conditions, conditions,

View File

@@ -1,4 +1,6 @@
use anyhow::Result; use std::collections::HashMap;
use anyhow::{anyhow, Result};
use cdk::url::UncheckedUrl; use cdk::url::UncheckedUrl;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use clap::Args; use clap::Args;
@@ -12,7 +14,7 @@ pub struct UpdateMintUrlSubCommand {
} }
pub async fn update_mint_url( pub async fn update_mint_url(
wallet: Wallet, wallets: HashMap<UncheckedUrl, Wallet>,
sub_command_args: &UpdateMintUrlSubCommand, sub_command_args: &UpdateMintUrlSubCommand,
) -> Result<()> { ) -> Result<()> {
let UpdateMintUrlSubCommand { let UpdateMintUrlSubCommand {
@@ -20,9 +22,12 @@ pub async fn update_mint_url(
new_mint_url, new_mint_url,
} = sub_command_args; } = sub_command_args;
wallet let mut wallet = wallets
.update_mint_url(old_mint_url.clone(), new_mint_url.clone()) .get(old_mint_url)
.await?; .ok_or(anyhow!("Unknown mint url"))?
.clone();
wallet.update_mint_url(new_mint_url.clone()).await?;
println!("Mint Url changed from {} to {}", old_mint_url, new_mint_url); println!("Mint Url changed from {} to {}", old_mint_url, new_mint_url);

View File

@@ -12,7 +12,6 @@ rust-version.workspace = true
default = ["mint", "wallet"] default = ["mint", "wallet"]
mint = ["cdk/mint"] mint = ["cdk/mint"]
wallet = ["cdk/wallet"] wallet = ["cdk/wallet"]
nostr = ["cdk/nostr"]
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true

View File

@@ -31,7 +31,6 @@ const MINT_KEYS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_
const PROOFS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("proofs"); const PROOFS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("proofs");
const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config"); const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config");
const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter"); const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
#[cfg(feature = "nostr")]
const NOSTR_LAST_CHECKED: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter"); const NOSTR_LAST_CHECKED: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
const DATABASE_VERSION: u32 = 0; const DATABASE_VERSION: u32 = 0;
@@ -73,7 +72,6 @@ impl RedbWalletDatabase {
let _ = write_txn.open_table(MINT_KEYS_TABLE)?; let _ = write_txn.open_table(MINT_KEYS_TABLE)?;
let _ = write_txn.open_table(PROOFS_TABLE)?; let _ = write_txn.open_table(PROOFS_TABLE)?;
let _ = write_txn.open_table(KEYSET_COUNTER)?; let _ = write_txn.open_table(KEYSET_COUNTER)?;
#[cfg(feature = "nostr")]
let _ = write_txn.open_table(NOSTR_LAST_CHECKED)?; let _ = write_txn.open_table(NOSTR_LAST_CHECKED)?;
table.insert("db_version", "0")?; table.insert("db_version", "0")?;
} }
@@ -655,7 +653,6 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(counter.map(|c| c.value())) Ok(counter.map(|c| c.value()))
} }
#[cfg(feature = "nostr")]
#[instrument(skip(self))] #[instrument(skip(self))]
async fn get_nostr_last_checked( async fn get_nostr_last_checked(
&self, &self,
@@ -673,7 +670,6 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(last_checked.map(|c| c.value())) Ok(last_checked.map(|c| c.value()))
} }
#[cfg(feature = "nostr")]
#[instrument(skip(self))] #[instrument(skip(self))]
async fn add_nostr_last_checked( async fn add_nostr_last_checked(
&self, &self,

View File

@@ -24,8 +24,9 @@ const MELT_QUOTES: &str = "melt_quotes";
const PROOFS: &str = "proofs"; const PROOFS: &str = "proofs";
const CONFIG: &str = "config"; const CONFIG: &str = "config";
const KEYSET_COUNTER: &str = "keyset_counter"; const KEYSET_COUNTER: &str = "keyset_counter";
const NOSTR_LAST_CHECKED: &str = "nostr_last_check";
const DATABASE_VERSION: u32 = 2; const DATABASE_VERSION: u32 = 3;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
@@ -87,6 +88,7 @@ impl RexieWalletDatabase {
.add_object_store(ObjectStore::new(MELT_QUOTES)) .add_object_store(ObjectStore::new(MELT_QUOTES))
.add_object_store(ObjectStore::new(CONFIG)) .add_object_store(ObjectStore::new(CONFIG))
.add_object_store(ObjectStore::new(KEYSET_COUNTER)) .add_object_store(ObjectStore::new(KEYSET_COUNTER))
.add_object_store(ObjectStore::new(NOSTR_LAST_CHECKED))
// Build the database // Build the database
.build() .build()
.await .await
@@ -712,4 +714,55 @@ impl WalletDatabase for RexieWalletDatabase {
Ok(current_count) Ok(current_count)
} }
async fn add_nostr_last_checked(
&self,
verifying_key: PublicKey,
last_checked: u32,
) -> Result<(), Self::Err> {
let rexie = self.db.lock().await;
let transaction = rexie
.transaction(&[NOSTR_LAST_CHECKED], TransactionMode::ReadWrite)
.map_err(Error::from)?;
let counter_store = transaction.store(NOSTR_LAST_CHECKED).map_err(Error::from)?;
let verifying_key = serde_wasm_bindgen::to_value(&verifying_key).map_err(Error::from)?;
let last_checked = serde_wasm_bindgen::to_value(&last_checked).map_err(Error::from)?;
counter_store
.put(&last_checked, Some(&verifying_key))
.await
.map_err(Error::from)?;
transaction.done().await.map_err(Error::from)?;
Ok(())
}
async fn get_nostr_last_checked(
&self,
verifying_key: &PublicKey,
) -> Result<Option<u32>, Self::Err> {
let rexie = self.db.lock().await;
let transaction = rexie
.transaction(&[NOSTR_LAST_CHECKED], TransactionMode::ReadOnly)
.map_err(Error::from)?;
let nostr_last_check_store = transaction.store(NOSTR_LAST_CHECKED).map_err(Error::from)?;
let verifying_key = serde_wasm_bindgen::to_value(verifying_key).map_err(Error::from)?;
let last_checked = nostr_last_check_store
.get(&verifying_key)
.await
.map_err(Error::from)?;
let last_checked: Option<u32> =
serde_wasm_bindgen::from_value(last_checked).map_err(Error::from)?;
Ok(last_checked)
}
} }

View File

@@ -12,7 +12,6 @@ rust-version.workspace = true
default = ["mint", "wallet"] default = ["mint", "wallet"]
mint = ["cdk/mint"] mint = ["cdk/mint"]
wallet = ["cdk/wallet"] wallet = ["cdk/wallet"]
nostr = ["cdk/nostr"]
[dependencies] [dependencies]
bitcoin.workspace = true bitcoin.workspace = true

View File

@@ -627,7 +627,6 @@ WHERE id=?;
Ok(count) Ok(count)
} }
#[cfg(feature = "nostr")]
async fn get_nostr_last_checked( async fn get_nostr_last_checked(
&self, &self,
verifying_key: &PublicKey, verifying_key: &PublicKey,
@@ -656,7 +655,6 @@ WHERE key=?;
Ok(count) Ok(count)
} }
#[cfg(feature = "nostr")]
async fn add_nostr_last_checked( async fn add_nostr_last_checked(
&self, &self,
verifying_key: PublicKey, verifying_key: PublicKey,

View File

@@ -13,7 +13,6 @@ license.workspace = true
default = ["mint", "wallet"] default = ["mint", "wallet"]
mint = [] mint = []
wallet = ["dep:reqwest"] wallet = ["dep:reqwest"]
nostr = ["dep:nostr-sdk"]
[dependencies] [dependencies]
@@ -42,10 +41,6 @@ tracing = { version = "0.1", default-features = false, features = [
thiserror = "1" thiserror = "1"
url = "2.3" url = "2.3"
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
nostr-sdk = { version = "0.31.0", default-features = false, features = [
"nip04",
"nip44"
], optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { workspace = true, features = [ tokio = { workspace = true, features = [

View File

@@ -1,4 +1,3 @@
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -7,7 +6,7 @@ use cdk::cdk_database::WalletMemoryDatabase;
use cdk::error::Error; use cdk::error::Error;
use cdk::nuts::CurrencyUnit; use cdk::nuts::CurrencyUnit;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::{Amount, UncheckedUrl}; use cdk::Amount;
use rand::Rng; use rand::Rng;
use tokio::time::sleep; use tokio::time::sleep;
@@ -16,24 +15,18 @@ async fn main() -> Result<(), Error> {
let localstore = WalletMemoryDatabase::default(); let localstore = WalletMemoryDatabase::default();
let seed = rand::thread_rng().gen::<[u8; 32]>(); let seed = rand::thread_rng().gen::<[u8; 32]>();
let mint_url = UncheckedUrl::from_str("https://testnut.cashu.space").unwrap(); let mint_url = "https://testnut.cashu.space";
let unit = CurrencyUnit::Sat; let unit = CurrencyUnit::Sat;
let amount = Amount::from(10); let amount = Amount::from(10);
let wallet = Wallet::new(Arc::new(localstore), &seed, vec![]); let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed);
let quote = wallet let quote = wallet.mint_quote(amount).await.unwrap();
.mint_quote(mint_url.clone(), unit.clone(), amount)
.await
.unwrap();
println!("Quote: {:#?}", quote); println!("Quote: {:#?}", quote);
loop { loop {
let status = wallet let status = wallet.mint_quote_status(&quote.id).await.unwrap();
.mint_quote_status(mint_url.clone(), &quote.id)
.await
.unwrap();
println!("Quote status: {}", status.paid); println!("Quote status: {}", status.paid);
@@ -45,14 +38,14 @@ async fn main() -> Result<(), Error> {
} }
let receive_amount = wallet let receive_amount = wallet
.mint(mint_url.clone(), &quote.id, SplitTarget::default(), None) .mint(&quote.id, SplitTarget::default(), None)
.await .await
.unwrap(); .unwrap();
println!("Received {receive_amount} from mint {mint_url}"); println!("Received {receive_amount} from mint {mint_url}");
let token = wallet let token = wallet
.send(&mint_url, unit, amount, None, None, &SplitTarget::default()) .send(amount, None, None, &SplitTarget::default())
.await .await
.unwrap(); .unwrap();

View File

@@ -1,4 +1,3 @@
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -7,7 +6,7 @@ use cdk::cdk_database::WalletMemoryDatabase;
use cdk::error::Error; use cdk::error::Error;
use cdk::nuts::{CurrencyUnit, SecretKey, SpendingConditions}; use cdk::nuts::{CurrencyUnit, SecretKey, SpendingConditions};
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::{Amount, UncheckedUrl}; use cdk::Amount;
use rand::Rng; use rand::Rng;
use tokio::time::sleep; use tokio::time::sleep;
@@ -16,24 +15,18 @@ async fn main() -> Result<(), Error> {
let localstore = WalletMemoryDatabase::default(); let localstore = WalletMemoryDatabase::default();
let seed = rand::thread_rng().gen::<[u8; 32]>(); let seed = rand::thread_rng().gen::<[u8; 32]>();
let mint_url = UncheckedUrl::from_str("https://testnut.cashu.space").unwrap(); let mint_url = "https://testnut.cashu.space";
let unit = CurrencyUnit::Sat; let unit = CurrencyUnit::Sat;
let amount = Amount::from(10); let amount = Amount::from(10);
let wallet = Wallet::new(Arc::new(localstore), &seed, vec![]); let wallet = Wallet::new(mint_url, unit.clone(), Arc::new(localstore), &seed);
let quote = wallet let quote = wallet.mint_quote(amount).await.unwrap();
.mint_quote(mint_url.clone(), unit.clone(), amount)
.await
.unwrap();
println!("Minting nuts ..."); println!("Minting nuts ...");
loop { loop {
let status = wallet let status = wallet.mint_quote_status(&quote.id).await.unwrap();
.mint_quote_status(mint_url.clone(), &quote.id)
.await
.unwrap();
println!("Quote status: {}", status.paid); println!("Quote status: {}", status.paid);
@@ -45,7 +38,7 @@ async fn main() -> Result<(), Error> {
} }
let _receive_amount = wallet let _receive_amount = wallet
.mint(mint_url.clone(), &quote.id, SplitTarget::default(), None) .mint(&quote.id, SplitTarget::default(), None)
.await .await
.unwrap(); .unwrap();
@@ -54,24 +47,15 @@ async fn main() -> Result<(), Error> {
let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None); let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None);
let token = wallet let token = wallet
.send( .send(amount, None, Some(spending_conditions), &SplitTarget::None)
&mint_url,
unit,
amount,
None,
Some(spending_conditions),
&SplitTarget::None,
)
.await .await
.unwrap(); .unwrap();
println!("Created token locked to pubkey: {}", secret.public_key()); println!("Created token locked to pubkey: {}", secret.public_key());
println!("{}", token); println!("{}", token);
wallet.add_p2pk_signing_key(secret).await;
let amount = wallet let amount = wallet
.receive(&token, &SplitTarget::default(), None) .receive(&token, &SplitTarget::default(), &[secret], &[])
.await .await
.unwrap(); .unwrap();

View File

@@ -104,12 +104,10 @@ pub trait WalletDatabase: Debug {
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>; async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err>; async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err>;
#[cfg(feature = "nostr")]
async fn get_nostr_last_checked( async fn get_nostr_last_checked(
&self, &self,
verifying_key: &PublicKey, verifying_key: &PublicKey,
) -> Result<Option<u32>, Self::Err>; ) -> Result<Option<u32>, Self::Err>;
#[cfg(feature = "nostr")]
async fn add_nostr_last_checked( async fn add_nostr_last_checked(
&self, &self,
verifying_key: PublicKey, verifying_key: PublicKey,

View File

@@ -25,7 +25,6 @@ pub struct WalletMemoryDatabase {
mint_keys: Arc<RwLock<HashMap<Id, Keys>>>, mint_keys: Arc<RwLock<HashMap<Id, Keys>>>,
proofs: Arc<RwLock<HashMap<PublicKey, ProofInfo>>>, proofs: Arc<RwLock<HashMap<PublicKey, ProofInfo>>>,
keyset_counter: Arc<RwLock<HashMap<Id, u32>>>, keyset_counter: Arc<RwLock<HashMap<Id, u32>>>,
#[cfg(feature = "nostr")]
nostr_last_checked: Arc<RwLock<HashMap<PublicKey, u32>>>, nostr_last_checked: Arc<RwLock<HashMap<PublicKey, u32>>>,
} }
@@ -35,7 +34,7 @@ impl WalletMemoryDatabase {
melt_quotes: Vec<MeltQuote>, melt_quotes: Vec<MeltQuote>,
mint_keys: Vec<Keys>, mint_keys: Vec<Keys>,
keyset_counter: HashMap<Id, u32>, keyset_counter: HashMap<Id, u32>,
#[cfg(feature = "nostr")] nostr_last_checked: HashMap<PublicKey, u32>, nostr_last_checked: HashMap<PublicKey, u32>,
) -> Self { ) -> Self {
Self { Self {
mints: Arc::new(RwLock::new(HashMap::new())), mints: Arc::new(RwLock::new(HashMap::new())),
@@ -52,7 +51,6 @@ impl WalletMemoryDatabase {
)), )),
proofs: Arc::new(RwLock::new(HashMap::new())), proofs: Arc::new(RwLock::new(HashMap::new())),
keyset_counter: Arc::new(RwLock::new(keyset_counter)), keyset_counter: Arc::new(RwLock::new(keyset_counter)),
#[cfg(feature = "nostr")]
nostr_last_checked: Arc::new(RwLock::new(nostr_last_checked)), nostr_last_checked: Arc::new(RwLock::new(nostr_last_checked)),
} }
} }
@@ -321,7 +319,6 @@ impl WalletDatabase for WalletMemoryDatabase {
Ok(self.keyset_counter.read().await.get(id).cloned()) Ok(self.keyset_counter.read().await.get(id).cloned())
} }
#[cfg(feature = "nostr")]
async fn get_nostr_last_checked( async fn get_nostr_last_checked(
&self, &self,
verifying_key: &PublicKey, verifying_key: &PublicKey,
@@ -333,7 +330,6 @@ impl WalletDatabase for WalletMemoryDatabase {
.get(verifying_key) .get(verifying_key)
.cloned()) .cloned())
} }
#[cfg(feature = "nostr")]
async fn add_nostr_last_checked( async fn add_nostr_last_checked(
&self, &self,
verifying_key: PublicKey, verifying_key: PublicKey,

View File

@@ -63,6 +63,15 @@ pub enum Error {
/// Unknown error response /// Unknown error response
#[error("Unknown Error response: `{0}`")] #[error("Unknown Error response: `{0}`")]
UnknownErrorResponse(String), UnknownErrorResponse(String),
/// Unknown Wallet
#[error("Unknown Wallet: `{0}`")]
UnknownWallet(String),
/// Unknown Wallet
#[error("Unknown Wallet: `{0}`")]
IncorrectWallet(String),
/// Max Fee Ecxeded
#[error("Max fee exceeded")]
MaxFeeExceeded,
/// CDK Error /// CDK Error
#[error(transparent)] #[error(transparent)]
Cashu(#[from] crate::error::Error), Cashu(#[from] crate::error::Error),

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,299 @@
//! MultiMint Wallet
//!
//! Wrapper around core [`Wallet`] that enables the use of multiple mint unit pairs
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use tracing::instrument;
use super::Error;
use crate::amount::SplitTarget;
use crate::nuts::{CurrencyUnit, SecretKey, SpendingConditions, Token};
use crate::types::{Melted, MintQuote};
use crate::{Amount, UncheckedUrl, Wallet};
#[derive(Debug, Clone)]
pub struct MultiMintWallet {
pub wallets: Arc<Mutex<HashMap<WalletKey, Wallet>>>,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct WalletKey {
mint_url: UncheckedUrl,
unit: CurrencyUnit,
}
impl fmt::Display for WalletKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
}
}
impl WalletKey {
pub fn new(mint_url: UncheckedUrl, unit: CurrencyUnit) -> Self {
Self { mint_url, unit }
}
}
impl MultiMintWallet {
/// New Multimint wallet
pub fn new(wallets: Vec<Wallet>) -> Self {
Self {
wallets: Arc::new(Mutex::new(
wallets
.into_iter()
.map(|w| (WalletKey::new(w.mint_url.clone(), w.unit.clone()), w))
.collect(),
)),
}
}
/// Add wallet to MultiMintWallet
#[instrument(skip(self, wallet))]
pub async fn add_wallet(&self, wallet: Wallet) {
let wallet_key = WalletKey::new(wallet.mint_url.clone(), wallet.unit.clone());
let mut wallets = self.wallets.lock().await;
wallets.insert(wallet_key, wallet);
}
/// Remove Wallet from MultiMintWallet
#[instrument(skip(self))]
pub async fn remove_wallet(&self, wallet_key: &WalletKey) {
let mut wallets = self.wallets.lock().await;
wallets.remove(wallet_key);
}
/// Get Wallets from MultiMintWallet
#[instrument(skip(self))]
pub async fn get_wallets(&self) -> Vec<Wallet> {
self.wallets.lock().await.values().cloned().collect()
}
/// Get Wallet from MultiMintWallet
#[instrument(skip(self))]
pub async fn get_wallet(&self, wallet_key: &WalletKey) -> Option<Wallet> {
let wallets = self.wallets.lock().await;
wallets.get(wallet_key).cloned()
}
/// Check if mint unit pair is in wallet
#[instrument(skip(self))]
pub async fn has(&self, wallet_key: &WalletKey) -> bool {
self.wallets.lock().await.contains_key(wallet_key)
}
/// Get wallet balances
#[instrument(skip(self))]
pub async fn get_balances(
&self,
unit: &CurrencyUnit,
) -> Result<HashMap<UncheckedUrl, Amount>, Error> {
let mut balances = HashMap::new();
for (WalletKey { mint_url, unit: u }, wallet) in self.wallets.lock().await.iter() {
if unit == u {
let wallet_balance = wallet.total_balance().await?;
balances.insert(mint_url.clone(), wallet_balance);
}
}
Ok(balances)
}
/// Create cashu token
#[instrument(skip(self))]
pub async fn send(
&self,
wallet_key: &WalletKey,
amount: Amount,
memo: Option<String>,
conditions: Option<SpendingConditions>,
) -> Result<String, Error> {
let wallet = self
.get_wallet(wallet_key)
.await
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
wallet
.send(amount, memo, conditions, &SplitTarget::default())
.await
}
/// Mint quote for wallet
#[instrument(skip(self))]
pub async fn mint_quote(
&self,
wallet_key: &WalletKey,
amount: Amount,
) -> Result<MintQuote, Error> {
let wallet = self
.get_wallet(wallet_key)
.await
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
wallet.mint_quote(amount).await
}
/// Check all mint quotes
/// If quote is paid, wallet will mint
#[instrument(skip(self))]
pub async fn check_all_mint_quotes(
&self,
wallet_key: Option<WalletKey>,
) -> Result<HashMap<CurrencyUnit, Amount>, Error> {
let mut amount_minted = HashMap::new();
match wallet_key {
Some(wallet_key) => {
let wallet = self
.get_wallet(&wallet_key)
.await
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
let amount = wallet.check_all_mint_quotes().await?;
amount_minted.insert(wallet.unit.clone(), amount);
}
None => {
for (_, wallet) in self.wallets.lock().await.iter() {
let amount = wallet.check_all_mint_quotes().await?;
amount_minted
.entry(wallet.unit.clone())
.and_modify(|b| *b += amount)
.or_insert(amount);
}
}
}
Ok(amount_minted)
}
/// Mint a specific quote
#[instrument(skip(self))]
pub async fn mint(
&self,
wallet_key: &WalletKey,
quote_id: &str,
conditions: Option<SpendingConditions>,
) -> Result<Amount, Error> {
let wallet = self
.get_wallet(wallet_key)
.await
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
wallet
.mint(quote_id, SplitTarget::default(), conditions)
.await
}
/// Receive token
/// Wallet must be already added to multimintwallet
#[instrument(skip_all)]
pub async fn receive(
&self,
encoded_token: &str,
p2pk_signing_keys: &[SecretKey],
preimages: &[String],
) -> Result<Amount, Error> {
let token_data = Token::from_str(encoded_token)?;
let unit = token_data.unit.unwrap_or_default();
let mint_url = token_data.token.first().unwrap().mint.clone();
let mints: HashSet<&UncheckedUrl> = token_data.token.iter().map(|d| &d.mint).collect();
// Check that all mints in tokes have wallets
for mint in mints {
let wallet_key = WalletKey::new(mint.clone(), unit.clone());
if !self.has(&wallet_key).await {
return Err(Error::UnknownWallet(wallet_key.to_string()));
}
}
let wallet_key = WalletKey::new(mint_url, unit);
let wallet = self
.get_wallet(&wallet_key)
.await
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
wallet
.receive(
encoded_token,
&SplitTarget::default(),
p2pk_signing_keys,
preimages,
)
.await
}
/// Pay an bolt11 invoice from specific wallet
#[instrument(skip(self, bolt11))]
pub async fn pay_invoice_for_wallet(
&self,
bolt11: &str,
wallet_key: &WalletKey,
max_fee: Option<Amount>,
) -> Result<Melted, Error> {
let wallet = self
.get_wallet(wallet_key)
.await
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
let quote = wallet.melt_quote(bolt11.to_string(), None).await?;
if let Some(max_fee) = max_fee {
if quote.fee_reserve > max_fee {
return Err(Error::MaxFeeExceeded);
}
}
wallet.melt(&quote.id, SplitTarget::default()).await
}
// Restore
#[instrument(skip(self))]
pub async fn restore(&self, wallet_key: &WalletKey) -> Result<Amount, Error> {
let wallet = self
.get_wallet(wallet_key)
.await
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
wallet.restore().await
}
/// Verify token matches p2pk conditions
#[instrument(skip(self, token))]
pub async fn verify_token_p2pk(
&self,
wallet_key: &WalletKey,
token: &Token,
conditions: SpendingConditions,
) -> Result<(), Error> {
let wallet = self
.get_wallet(wallet_key)
.await
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
wallet.verify_token_p2pk(token, conditions)
}
/// Verifys all proofs in toke have valid dleq proof
#[instrument(skip(self, token))]
pub async fn verify_token_dleq(
&self,
wallet_key: &WalletKey,
token: &Token,
) -> Result<(), Error> {
let wallet = self
.get_wallet(wallet_key)
.await
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
wallet.verify_token_dleq(token).await
}
}

View File

@@ -1,118 +0,0 @@
//! Wallet Nostr functions
use std::collections::HashSet;
use std::str::FromStr;
use nostr_sdk::nips::nip04;
use nostr_sdk::{Filter, Timestamp};
use tracing::instrument;
use super::error::Error;
use super::{util, Wallet};
use crate::amount::{Amount, SplitTarget};
use crate::nuts::SecretKey;
impl Wallet {
/// Add nostr relays to client
#[instrument(skip(self))]
pub async fn add_nostr_relays(&self, relays: Vec<String>) -> Result<(), Error> {
self.nostr_client.add_relays(relays).await?;
Ok(())
}
/// Remove nostr relays to client
#[instrument(skip(self))]
pub async fn remove_nostr_relays(&self, relay: String) -> Result<(), Error> {
self.nostr_client.remove_relay(relay).await?;
Ok(())
}
/// Nostr relays
#[instrument(skip(self))]
pub async fn nostr_relays(&self) -> Vec<String> {
self.nostr_client
.relays()
.await
.keys()
.map(|url| url.to_string())
.collect()
}
/// Receive tokens sent to nostr pubkey via dm
#[instrument(skip_all)]
pub async fn nostr_receive(
&self,
nostr_signing_key: SecretKey,
since: Option<u64>,
amount_split_target: SplitTarget,
) -> Result<Amount, Error> {
use nostr_sdk::{Keys, Kind};
use crate::util::unix_time;
use crate::Amount;
let verifying_key = nostr_signing_key.public_key();
let x_only_pubkey = verifying_key.x_only_public_key();
let nostr_pubkey = nostr_sdk::PublicKey::from_hex(x_only_pubkey.to_string())?;
let keys = Keys::from_str(&(nostr_signing_key).to_secret_hex())?;
self.add_p2pk_signing_key(nostr_signing_key).await;
let since = match since {
Some(since) => Some(Timestamp::from(since)),
None => self
.localstore
.get_nostr_last_checked(&verifying_key)
.await?
.map(|s| Timestamp::from(s as u64)),
};
let filter = match since {
Some(since) => Filter::new()
.pubkey(nostr_pubkey)
.kind(Kind::EncryptedDirectMessage)
.since(since),
None => Filter::new()
.pubkey(nostr_pubkey)
.kind(Kind::EncryptedDirectMessage),
};
self.nostr_client.connect().await;
let events = self.nostr_client.get_events_of(vec![filter], None).await?;
let mut tokens: HashSet<String> = HashSet::new();
for event in events {
if event.kind() == Kind::EncryptedDirectMessage {
if let Ok(msg) =
nip04::decrypt(keys.secret_key()?, event.author_ref(), event.content())
{
if let Some(token) = util::token_from_text(&msg) {
tokens.insert(token.to_string());
}
} else {
tracing::error!("Impossible to decrypt direct message");
}
}
}
let mut total_received = Amount::ZERO;
for token in tokens.iter() {
match self.receive(token, &amount_split_target, None).await {
Ok(amount) => total_received += amount,
Err(err) => {
tracing::error!("Could not receive token: {}", err);
}
}
}
self.localstore
.add_nostr_last_checked(verifying_key, unix_time() as u32)
.await?;
Ok(total_received)
}
}

View File

@@ -5,8 +5,7 @@ use crate::nuts::{CurrencyUnit, Proofs, Token};
use crate::UncheckedUrl; use crate::UncheckedUrl;
/// Extract token from text /// Extract token from text
#[cfg(feature = "nostr")] pub fn token_from_text(text: &str) -> Option<&str> {
pub(crate) fn token_from_text(text: &str) -> Option<&str> {
let text = text.trim(); let text = text.trim();
if let Some(start) = text.find("cashu") { if let Some(start) = text.find("cashu") {
match text[start..].find(' ') { match text[start..].find(' ') {

View File

@@ -26,11 +26,9 @@ buildargs=(
"-p cdk" "-p cdk"
"-p cdk --no-default-features" "-p cdk --no-default-features"
"-p cdk --no-default-features --features wallet" "-p cdk --no-default-features --features wallet"
"-p cdk --no-default-features --features wallet --features nostr"
"-p cdk --no-default-features --features mint" "-p cdk --no-default-features --features mint"
"-p cdk-redb" "-p cdk-redb"
"-p cdk-redb --no-default-features --features wallet" "-p cdk-redb --no-default-features --features wallet"
"-p cdk-redb --no-default-features --features wallet --features nostr"
"-p cdk-redb --no-default-features --features mint" "-p cdk-redb --no-default-features --features mint"
"-p cdk-sqlite --no-default-features --features mint" "-p cdk-sqlite --no-default-features --features mint"
"-p cdk-sqlite --no-default-features --features wallet" "-p cdk-sqlite --no-default-features --features wallet"