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

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
@@ -5,9 +6,9 @@ use std::sync::Arc;
use anyhow::{bail, Result};
use bip39::Mnemonic;
use cdk::cdk_database;
use cdk::cdk_database::WalletDatabase;
use cdk::wallet::Wallet;
use cdk::{cdk_database, UncheckedUrl};
use cdk_redb::RedbWalletDatabase;
use cdk_sqlite::WalletSQLiteDatabase;
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 {
Commands::DecodeToken(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) => {
sub_commands::melt::melt(wallet, sub_command_args).await
sub_commands::melt::melt(wallets, sub_command_args).await
}
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) => {
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) => {
sub_commands::mint_info::mint_info(sub_command_args).await
}
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) => {
sub_commands::burn::burn(wallet, sub_command_args).await
sub_commands::burn::burn(wallets, sub_command_args).await
}
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) => {
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 anyhow::Result;
use cdk::nuts::CurrencyUnit;
use cdk::url::UncheckedUrl;
use cdk::wallet::Wallet;
use cdk::Amount;
pub async fn balance(wallet: Wallet) -> Result<()> {
let _ = mint_balances(&wallet).await;
pub async fn balance(wallets: HashMap<UncheckedUrl, Wallet>) -> Result<()> {
mint_balances(wallets).await?;
Ok(())
}
pub async fn mint_balances(
wallet: &Wallet,
) -> Result<Vec<(UncheckedUrl, HashMap<CurrencyUnit, Amount>)>> {
let mints_amounts: Vec<(UncheckedUrl, HashMap<_, _>)> =
wallet.mint_balances().await?.into_iter().collect();
wallets: HashMap<UncheckedUrl, Wallet>,
) -> Result<Vec<(Wallet, Amount)>> {
let mut wallets_vec: Vec<(Wallet, Amount)> = Vec::with_capacity(wallets.capacity());
for (i, (mint, balance)) in mints_amounts.iter().enumerate() {
println!("{i}: {mint}:");
for (unit, amount) in balance {
println!("- {amount} {unit}");
}
println!("---------");
for (i, (mint_url, wallet)) in wallets.iter().enumerate() {
let mint_url = mint_url.clone();
let amount = wallet.total_balance().await?;
println!("{i}: {mint_url} {amount}");
wallets_vec.push((wallet.clone(), amount));
}
Ok(mints_amounts)
Ok(wallets_vec)
}

View File

@@ -1,18 +1,34 @@
use std::collections::HashMap;
use anyhow::Result;
use cdk::wallet::Wallet;
use cdk::{Amount, UncheckedUrl};
use clap::Args;
#[derive(Args)]
pub struct BurnSubCommand {
/// Mint Url
mint_url: Option<String>,
mint_url: Option<UncheckedUrl>,
}
pub async fn burn(wallet: Wallet, sub_command_args: &BurnSubCommand) -> Result<()> {
let amount_burnt = wallet
.check_all_pending_proofs(sub_command_args.mint_url.clone().map(|u| u.into()), None)
.await?;
pub async fn burn(
wallets: HashMap<UncheckedUrl, Wallet>,
sub_command_args: &BurnSubCommand,
) -> 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(())
}

View File

@@ -1,40 +1,15 @@
use std::collections::HashMap;
use std::io::Write;
use std::{io, println};
use std::println;
use anyhow::{bail, Result};
use anyhow::Result;
use cdk::url::UncheckedUrl;
use cdk::wallet::Wallet;
pub async fn check_spent(wallet: Wallet) -> Result<()> {
let mints_amounts: Vec<(UncheckedUrl, HashMap<_, _>)> =
wallet.mint_balances().await?.into_iter().collect();
pub async fn check_spent(wallets: HashMap<UncheckedUrl, Wallet>) -> Result<()> {
for wallet in wallets.values() {
let amount = wallet.check_all_pending_proofs().await?;
for (i, (mint, amount)) in mints_amounts.iter().enumerate() {
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);
println!("Amount marked as spent: {}", amount);
}
Ok(())

View File

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

View File

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

View File

@@ -1,8 +1,15 @@
use std::collections::HashMap;
use anyhow::Result;
use cdk::wallet::Wallet;
use cdk::{Amount, UncheckedUrl};
pub async fn pending_mints(wallet: Wallet) -> Result<()> {
let amount_claimed = wallet.check_all_mint_quotes().await?;
pub async fn pending_mints(wallets: HashMap<UncheckedUrl, Wallet>) -> Result<()> {
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}");
Ok(())

View File

@@ -1,10 +1,17 @@
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::Arc;
use anyhow::{anyhow, Result};
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::{Amount, UncheckedUrl};
use clap::Args;
use nostr_sdk::nips::nip04;
use nostr_sdk::{Filter, Keys, Kind, Timestamp};
#[derive(Args)]
pub struct ReceiveSubCommand {
@@ -27,54 +34,82 @@ pub struct ReceiveSubCommand {
preimage: Vec<String>,
}
pub async fn receive(wallet: Wallet, sub_command_args: &ReceiveSubCommand) -> Result<()> {
let nostr_key = match sub_command_args.nostr_key.as_ref() {
Some(nostr_key) => {
let secret_key = SecretKey::from_str(nostr_key)?;
wallet.add_p2pk_signing_key(secret_key.clone()).await;
Some(secret_key)
}
None => None,
};
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 signing_keys: Vec<SecretKey> = sub_command_args
let mut s_keys: Vec<SecretKey> = sub_command_args
.signing_key
.iter()
.map(|s| SecretKey::from_str(s).unwrap())
.collect();
for signing_key in signing_keys {
wallet.add_p2pk_signing_key(signing_key).await;
}
signing_keys.append(&mut s_keys);
}
let preimage = match sub_command_args.preimage.is_empty() {
true => None,
false => Some(sub_command_args.preimage.clone()),
};
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?;
wallet
.nostr_receive(nostr_key, sub_command_args.since, SplitTarget::default())
.await?
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
.receive(
sub_command_args
.token
.as_ref()
.ok_or(anyhow!("Token Required"))?,
&SplitTarget::default(),
preimage,
//wallet.add_p2pk_signing_key(nostr_signing_key).await;
let nostr_key = match sub_command_args.nostr_key.as_ref() {
Some(nostr_key) => {
let secret_key = SecretKey::from_str(nostr_key)?;
Some(secret_key)
}
None => None,
};
let nostr_key =
nostr_key.ok_or(anyhow!("Nostr key required if token is not provided"))?;
signing_keys.push(nostr_key.clone());
let relays = sub_command_args.relay.clone();
let since = localstore
.get_nostr_last_checked(&nostr_key.public_key())
.await?;
let tokens = nostr_receive(relays, nostr_key.clone(), since).await?;
let mut total_amount = Amount::ZERO;
for token_str in &tokens {
match receive_token(
token_str,
wallets.clone(),
seed,
&localstore,
&signing_keys,
&sub_command_args.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(())
}
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::wallet::Wallet;
use clap::Args;
@@ -9,10 +11,15 @@ pub struct RestoreSubCommand {
mint_url: UncheckedUrl,
}
pub async fn restore(wallet: Wallet, sub_command_args: &RestoreSubCommand) -> Result<()> {
let mint_url = sub_command_args.mint_url.clone();
pub async fn restore(
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);

View File

@@ -1,12 +1,13 @@
use std::collections::HashMap;
use std::io::Write;
use std::str::FromStr;
use std::{io, println};
use anyhow::{bail, Result};
use cdk::amount::SplitTarget;
use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
use cdk::nuts::{Conditions, PublicKey, SpendingConditions};
use cdk::wallet::Wallet;
use cdk::Amount;
use cdk::{Amount, UncheckedUrl};
use clap::Args;
use crate::sub_commands::balance::mint_balances;
@@ -33,8 +34,11 @@ pub struct SendSubCommand {
refund_keys: Vec<String>,
}
pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<()> {
let mints_amounts = mint_balances(&wallet).await?;
pub async fn send(
wallets: HashMap<UncheckedUrl, Wallet>,
sub_command_args: &SendSubCommand,
) -> Result<()> {
let mints_amounts = mint_balances(wallets).await?;
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");
}
let mint_url = mints_amounts[mint_number].0.clone();
println!("Enter value of token in sats");
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)?;
let token_amount = Amount::from(user_input.trim().parse::<u64>()?);
if token_amount.gt(mints_amounts[mint_number]
.1
.get(&CurrencyUnit::Sat)
.unwrap())
{
if token_amount.gt(&mints_amounts[mint_number].1) {
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
.send(
&mint_url,
CurrencyUnit::Sat,
token_amount,
sub_command_args.memo.clone(),
conditions,

View File

@@ -1,4 +1,6 @@
use anyhow::Result;
use std::collections::HashMap;
use anyhow::{anyhow, Result};
use cdk::url::UncheckedUrl;
use cdk::wallet::Wallet;
use clap::Args;
@@ -12,7 +14,7 @@ pub struct UpdateMintUrlSubCommand {
}
pub async fn update_mint_url(
wallet: Wallet,
wallets: HashMap<UncheckedUrl, Wallet>,
sub_command_args: &UpdateMintUrlSubCommand,
) -> Result<()> {
let UpdateMintUrlSubCommand {
@@ -20,9 +22,12 @@ pub async fn update_mint_url(
new_mint_url,
} = sub_command_args;
wallet
.update_mint_url(old_mint_url.clone(), new_mint_url.clone())
.await?;
let mut wallet = wallets
.get(old_mint_url)
.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);