feat(NUT02): add input_fee_ppk

chore: instrument log on mint fns
This commit is contained in:
thesimplekid
2024-05-30 00:19:19 +01:00
parent 8477dc7a1d
commit 17263b07f5
33 changed files with 1009 additions and 514 deletions

View File

@@ -27,7 +27,7 @@
### Changed ### Changed
cdk(wallet): `wallet:receive` will not claim `proofs` from a mint other then the wallet's mint ([thesimplekid]). cdk(wallet): `wallet:receive` will not claim `proofs` from a mint other then the wallet's mint ([thesimplekid]).
cdk(NUT00): `Token` is changed from a struct to enum of either `TokenV4` or `Tokenv3` ([thesimplekid]). cdk(NUT00): `Token` is changed from a `struct` to `enum` of either `TokenV4` or `Tokenv3` ([thesimplekid]).
cdk(NUT00): Rename `MintProofs` to `TokenV3Token` ([thesimplekid]). cdk(NUT00): Rename `MintProofs` to `TokenV3Token` ([thesimplekid]).
@@ -40,6 +40,8 @@ cdk-mintd: Mint binary ([thesimplekid]).
cdk-cln: cln backend for mint ([thesimplekid]). cdk-cln: cln backend for mint ([thesimplekid]).
cdk-axum: Mint axum server ([thesimplekid]). cdk-axum: Mint axum server ([thesimplekid]).
cdk: NUT06 `MintInfo` and `NUTs` builder ([thesimplekid]). cdk: NUT06 `MintInfo` and `NUTs` builder ([thesimplekid]).
cdk: NUT00 `PreMintSecret` added Keyset id ([thesimplekid])
cdk: NUT02 Support fees ([thesimplekid])
### Fixed ### Fixed
cdk: NUT06 deseralize `MintInfo` ([thesimplekid]). cdk: NUT06 deseralize `MintInfo` ([thesimplekid]).

View File

@@ -34,6 +34,7 @@ cdk-axum = { version = "0.1", path = "./crates/cdk-axum", default-features = fal
tokio = { version = "1", default-features = false } tokio = { version = "1", default-features = false }
thiserror = "1" thiserror = "1"
tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
serde = { version = "1", default-features = false, features = ["derive"] } serde = { version = "1", default-features = false, features = ["derive"] }
serde_json = "1" serde_json = "1"
serde-wasm-bindgen = "0.6.5" serde-wasm-bindgen = "0.6.5"

View File

@@ -5,7 +5,7 @@ use std::sync::Arc;
use cdk::amount::SplitTarget; use cdk::amount::SplitTarget;
use cdk::nuts::{Proofs, SecretKey}; use cdk::nuts::{Proofs, SecretKey};
use cdk::wallet::Wallet; use cdk::wallet::{SendKind, Wallet};
use cdk::Amount; use cdk::Amount;
use cdk_rexie::WalletRexieDatabase; use cdk_rexie::WalletRexieDatabase;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@@ -44,7 +44,7 @@ impl JsWallet {
pub async fn new(mints_url: String, unit: JsCurrencyUnit, seed: Vec<u8>) -> Self { pub async fn new(mints_url: String, unit: JsCurrencyUnit, seed: Vec<u8>) -> Self {
let db = WalletRexieDatabase::new().await.unwrap(); let db = WalletRexieDatabase::new().await.unwrap();
Wallet::new(&mints_url, unit.into(), Arc::new(db), &seed).into() Wallet::new(&mints_url, unit.into(), Arc::new(db), &seed, None).into()
} }
#[wasm_bindgen(js_name = totalBalance)] #[wasm_bindgen(js_name = totalBalance)]
@@ -81,12 +81,6 @@ impl JsWallet {
.map(|i| i.into())) .map(|i| i.into()))
} }
#[wasm_bindgen(js_name = refreshMint)]
pub async fn refresh_mint_keys(&self) -> Result<()> {
self.inner.refresh_mint_keys().await.map_err(into_err)?;
Ok(())
}
#[wasm_bindgen(js_name = mintQuote)] #[wasm_bindgen(js_name = mintQuote)]
pub async fn mint_quote(&mut self, amount: u64) -> Result<JsMintQuote> { pub async fn mint_quote(&mut self, amount: u64) -> Result<JsMintQuote> {
let quote = self let quote = self
@@ -200,7 +194,7 @@ impl JsWallet {
.inner .inner
.receive( .receive(
&encoded_token, &encoded_token,
&SplitTarget::default(), SplitTarget::default(),
&signing_keys, &signing_keys,
&preimages, &preimages,
) )
@@ -234,7 +228,14 @@ impl JsWallet {
.map(|a| SplitTarget::Value(*a.deref())) .map(|a| SplitTarget::Value(*a.deref()))
.unwrap_or_default(); .unwrap_or_default();
self.inner self.inner
.send(Amount::from(amount), memo, conditions, &target) .send(
Amount::from(amount),
memo,
conditions,
&target,
&SendKind::default(),
false,
)
.await .await
.map_err(into_err) .map_err(into_err)
} }
@@ -267,7 +268,13 @@ impl JsWallet {
.unwrap_or_default(); .unwrap_or_default();
let post_swap_proofs = self let post_swap_proofs = self
.inner .inner
.swap(Some(Amount::from(amount)), &target, proofs, conditions) .swap(
Some(Amount::from(amount)),
target,
proofs,
conditions,
false,
)
.await .await
.map_err(into_err)?; .map_err(into_err)?;

View File

@@ -22,7 +22,7 @@ serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
tokio.workspace = true tokio.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber = "0.3.18" tracing-subscriber.workspace = true
rand = "0.8.5" rand = "0.8.5"
home.workspace = true home.workspace = true
nostr-sdk = { version = "0.32.0", default-features = false, features = [ nostr-sdk = { version = "0.32.0", default-features = false, features = [

View File

@@ -14,6 +14,7 @@ use cdk_sqlite::WalletSqliteDatabase;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use rand::Rng; use rand::Rng;
use tracing::Level; use tracing::Level;
use tracing_subscriber::EnvFilter;
mod sub_commands; mod sub_commands;
@@ -69,11 +70,15 @@ enum Commands {
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
// Parse input
let args: Cli = Cli::parse(); let args: Cli = Cli::parse();
tracing_subscriber::fmt() let default_filter = args.log_level;
.with_max_level(args.log_level)
.init(); let sqlx_filter = "sqlx=warn";
let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter));
// Parse input
tracing_subscriber::fmt().with_env_filter(env_filter).init();
let work_dir = match &args.work_dir { let work_dir = match &args.work_dir {
Some(work_dir) => work_dir.clone(), Some(work_dir) => work_dir.clone(),
@@ -131,6 +136,7 @@ async fn main() -> Result<()> {
cdk::nuts::CurrencyUnit::Sat, cdk::nuts::CurrencyUnit::Sat,
localstore.clone(), localstore.clone(),
&mnemonic.to_seed_normalized(""), &mnemonic.to_seed_normalized(""),
None,
); );
wallets.insert(mint, wallet); wallets.insert(mint, wallet);

View File

@@ -32,7 +32,13 @@ pub async fn mint(
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) { let wallet = match wallets.get(&mint_url) {
Some(wallet) => wallet.clone(), Some(wallet) => wallet.clone(),
None => Wallet::new(&mint_url.to_string(), CurrencyUnit::Sat, localstore, seed), None => Wallet::new(
&mint_url.to_string(),
CurrencyUnit::Sat,
localstore,
seed,
None,
),
}; };
let quote = wallet let quote = wallet

View File

@@ -136,11 +136,12 @@ async fn receive_token(
CurrencyUnit::Sat, CurrencyUnit::Sat,
Arc::clone(localstore), Arc::clone(localstore),
seed, seed,
None,
), ),
}; };
let amount = wallet let amount = wallet
.receive(token_str, &SplitTarget::default(), signing_keys, preimage) .receive(token_str, SplitTarget::default(), signing_keys, preimage)
.await?; .await?;
Ok(amount) Ok(amount)
} }

View File

@@ -6,6 +6,7 @@ use std::str::FromStr;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use cdk::amount::SplitTarget; use cdk::amount::SplitTarget;
use cdk::nuts::{Conditions, PublicKey, SpendingConditions, Token}; use cdk::nuts::{Conditions, PublicKey, SpendingConditions, Token};
use cdk::wallet::types::SendKind;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::{Amount, UncheckedUrl}; use cdk::{Amount, UncheckedUrl};
use clap::Args; use clap::Args;
@@ -35,6 +36,15 @@ pub struct SendSubCommand {
/// Token as V3 token /// Token as V3 token
#[arg(short, long)] #[arg(short, long)]
v3: bool, v3: bool,
/// Should the send be offline only
#[arg(short, long)]
offline: bool,
/// Include fee to redeam in token
#[arg(short, long)]
include_fee: bool,
/// Amount willing to overpay to avoid a swap
#[arg(short, long)]
tolerance: Option<u64>,
} }
pub async fn send( pub async fn send(
@@ -146,12 +156,21 @@ pub async fn send(
let wallet = mints_amounts[mint_number].0.clone(); let wallet = mints_amounts[mint_number].0.clone();
let send_kind = match (sub_command_args.offline, sub_command_args.tolerance) {
(true, Some(amount)) => SendKind::OfflineTolerance(Amount::from(amount)),
(true, None) => SendKind::OfflineExact,
(false, Some(amount)) => SendKind::OnlineTolerance(Amount::from(amount)),
(false, None) => SendKind::OnlineExact,
};
let token = wallet let token = wallet
.send( .send(
token_amount, token_amount,
sub_command_args.memo.clone(), sub_command_args.memo.clone(),
conditions, conditions,
&SplitTarget::default(), &SplitTarget::default(),
&send_kind,
sub_command_args.include_fee,
) )
.await?; .await?;

View File

@@ -21,7 +21,7 @@ config = { version = "0.13.3", features = ["toml"] }
clap = { version = "4.4.8", features = ["derive", "env", "default"] } clap = { version = "4.4.8", features = ["derive", "env", "default"] }
tokio.workspace = true tokio.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber = "0.3.18" tracing-subscriber.workspace = true
futures = "0.3.28" futures = "0.3.28"
serde.workspace = true serde.workspace = true
bip39.workspace = true bip39.workspace = true

View File

@@ -3,6 +3,7 @@ url = "https://mint.thesimplekid.dev/"
listen_host = "127.0.0.1" listen_host = "127.0.0.1"
listen_port = 8085 listen_port = 8085
mnemonic = "" mnemonic = ""
# input_fee_ppk = 0

View File

@@ -13,6 +13,7 @@ pub struct Info {
pub listen_port: u16, pub listen_port: u16,
pub mnemonic: String, pub mnemonic: String,
pub seconds_quote_is_valid_for: Option<u64>, pub seconds_quote_is_valid_for: Option<u64>,
pub input_fee_ppk: Option<u64>,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]

View File

@@ -28,6 +28,7 @@ use cli::CLIArgs;
use config::{DatabaseEngine, LnBackend}; use config::{DatabaseEngine, LnBackend};
use futures::StreamExt; use futures::StreamExt;
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
use tracing_subscriber::EnvFilter;
mod cli; mod cli;
mod config; mod config;
@@ -37,9 +38,13 @@ const DEFAULT_QUOTE_TTL_SECS: u64 = 1800;
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt() let default_filter = "debug";
.with_max_level(tracing::Level::DEBUG)
.init(); let sqlx_filter = "sqlx=warn";
let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter));
tracing_subscriber::fmt().with_env_filter(env_filter).init();
let args = CLIArgs::parse(); let args = CLIArgs::parse();
@@ -206,6 +211,8 @@ async fn main() -> anyhow::Result<()> {
let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?; let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?;
let input_fee_ppk = settings.info.input_fee_ppk.unwrap_or(0);
let mint = Mint::new( let mint = Mint::new(
&settings.info.url, &settings.info.url,
&mnemonic.to_seed_normalized(""), &mnemonic.to_seed_normalized(""),
@@ -213,6 +220,7 @@ async fn main() -> anyhow::Result<()> {
localstore, localstore,
absolute_ln_fee_reserve, absolute_ln_fee_reserve,
relative_ln_fee, relative_ln_fee,
input_fee_ppk,
) )
.await?; .await?;

View File

@@ -288,6 +288,7 @@ impl WalletDatabase for WalletRedbDatabase {
let mut table = write_txn let mut table = write_txn
.open_multimap_table(MINT_KEYSETS_TABLE) .open_multimap_table(MINT_KEYSETS_TABLE)
.map_err(Error::from)?; .map_err(Error::from)?;
let mut keysets_table = write_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
for keyset in keysets { for keyset in keysets {
table table
@@ -296,6 +297,15 @@ impl WalletDatabase for WalletRedbDatabase {
keyset.id.to_bytes().as_slice(), keyset.id.to_bytes().as_slice(),
) )
.map_err(Error::from)?; .map_err(Error::from)?;
keysets_table
.insert(
keyset.id.to_bytes().as_slice(),
serde_json::to_string(&keyset)
.map_err(Error::from)?
.as_str(),
)
.map_err(Error::from)?;
} }
} }
write_txn.commit().map_err(Error::from)?; write_txn.commit().map_err(Error::from)?;

View File

@@ -0,0 +1 @@
ALTER TABLE keyset ADD input_fee_ppk INTEGER;

View File

@@ -407,9 +407,9 @@ WHERE id=?
async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> { async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> {
sqlx::query( sqlx::query(
r#" r#"
INSERT INTO keyset INSERT OR REPLACE INTO keyset
(id, unit, active, valid_from, valid_to, derivation_path, max_order) (id, unit, active, valid_from, valid_to, derivation_path, max_order, input_fee_ppk)
VALUES (?, ?, ?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?, ?, ?);
"#, "#,
) )
.bind(keyset.id.to_string()) .bind(keyset.id.to_string())
@@ -419,6 +419,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
.bind(keyset.valid_to.map(|v| v as i64)) .bind(keyset.valid_to.map(|v| v as i64))
.bind(keyset.derivation_path.to_string()) .bind(keyset.derivation_path.to_string())
.bind(keyset.max_order) .bind(keyset.max_order)
.bind(keyset.input_fee_ppk as i64)
.execute(&self.pool) .execute(&self.pool)
.await .await
.map_err(Error::from)?; .map_err(Error::from)?;
@@ -714,6 +715,7 @@ fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result<MintKeySetInfo, Error> {
let row_valid_to: Option<i64> = row.try_get("valid_to").map_err(Error::from)?; let row_valid_to: Option<i64> = row.try_get("valid_to").map_err(Error::from)?;
let row_derivation_path: String = row.try_get("derivation_path").map_err(Error::from)?; let row_derivation_path: String = row.try_get("derivation_path").map_err(Error::from)?;
let row_max_order: u8 = row.try_get("max_order").map_err(Error::from)?; let row_max_order: u8 = row.try_get("max_order").map_err(Error::from)?;
let row_keyset_ppk: Option<i64> = row.try_get("input_fee_ppk").map_err(Error::from)?;
Ok(MintKeySetInfo { Ok(MintKeySetInfo {
id: Id::from_str(&row_id).map_err(Error::from)?, id: Id::from_str(&row_id).map_err(Error::from)?,
@@ -723,6 +725,7 @@ fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result<MintKeySetInfo, Error> {
valid_to: row_valid_to.map(|v| v as u64), valid_to: row_valid_to.map(|v| v as u64),
derivation_path: DerivationPath::from_str(&row_derivation_path).map_err(Error::from)?, derivation_path: DerivationPath::from_str(&row_derivation_path).map_err(Error::from)?,
max_order: row_max_order, max_order: row_max_order,
input_fee_ppk: row_keyset_ppk.unwrap_or(0) as u64,
}) })
} }

View File

@@ -0,0 +1 @@
ALTER TABLE keyset ADD input_fee_ppk INTEGER;

View File

@@ -211,14 +211,15 @@ FROM mint
sqlx::query( sqlx::query(
r#" r#"
INSERT OR REPLACE INTO keyset INSERT OR REPLACE INTO keyset
(mint_url, id, unit, active) (mint_url, id, unit, active, input_fee_ppk)
VALUES (?, ?, ?, ?); VALUES (?, ?, ?, ?, ?);
"#, "#,
) )
.bind(mint_url.to_string()) .bind(mint_url.to_string())
.bind(keyset.id.to_string()) .bind(keyset.id.to_string())
.bind(keyset.unit.to_string()) .bind(keyset.unit.to_string())
.bind(keyset.active) .bind(keyset.active)
.bind(keyset.input_fee_ppk as i64)
.execute(&self.pool) .execute(&self.pool)
.await .await
.map_err(Error::from)?; .map_err(Error::from)?;
@@ -708,11 +709,13 @@ fn sqlite_row_to_keyset(row: &SqliteRow) -> Result<KeySetInfo, Error> {
let row_id: String = row.try_get("id").map_err(Error::from)?; let row_id: String = row.try_get("id").map_err(Error::from)?;
let row_unit: String = row.try_get("unit").map_err(Error::from)?; let row_unit: String = row.try_get("unit").map_err(Error::from)?;
let active: bool = row.try_get("active").map_err(Error::from)?; let active: bool = row.try_get("active").map_err(Error::from)?;
let row_keyset_ppk: Option<i64> = row.try_get("input_fee_ppk").map_err(Error::from)?;
Ok(KeySetInfo { Ok(KeySetInfo {
id: Id::from_str(&row_id)?, id: Id::from_str(&row_id)?,
unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
active, active,
input_fee_ppk: row_keyset_ppk.unwrap_or(0) as u64,
}) })
} }

View File

@@ -69,6 +69,10 @@ required-features = ["wallet"]
name = "wallet" name = "wallet"
required-features = ["wallet"] required-features = ["wallet"]
[[example]]
name = "proof_selection"
required-features = ["wallet"]
[dev-dependencies] [dev-dependencies]
rand = "0.8.5" rand = "0.8.5"
bip39.workspace = true bip39.workspace = true

View File

@@ -5,6 +5,7 @@ use cdk::amount::SplitTarget;
use cdk::cdk_database::WalletMemoryDatabase; use cdk::cdk_database::WalletMemoryDatabase;
use cdk::error::Error; use cdk::error::Error;
use cdk::nuts::{CurrencyUnit, MintQuoteState}; use cdk::nuts::{CurrencyUnit, MintQuoteState};
use cdk::wallet::types::SendKind;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::Amount; use cdk::Amount;
use rand::Rng; use rand::Rng;
@@ -19,7 +20,7 @@ async fn main() -> Result<(), Error> {
let unit = CurrencyUnit::Sat; let unit = CurrencyUnit::Sat;
let amount = Amount::from(10); let amount = Amount::from(10);
let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed); let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None);
let quote = wallet.mint_quote(amount).await.unwrap(); let quote = wallet.mint_quote(amount).await.unwrap();
@@ -45,7 +46,14 @@ async fn main() -> Result<(), Error> {
println!("Received {receive_amount} from mint {mint_url}"); println!("Received {receive_amount} from mint {mint_url}");
let token = wallet let token = wallet
.send(amount, None, None, &SplitTarget::default()) .send(
amount,
None,
None,
&SplitTarget::default(),
&SendKind::OnlineExact,
false,
)
.await .await
.unwrap(); .unwrap();

View File

@@ -5,6 +5,7 @@ use cdk::amount::SplitTarget;
use cdk::cdk_database::WalletMemoryDatabase; use cdk::cdk_database::WalletMemoryDatabase;
use cdk::error::Error; use cdk::error::Error;
use cdk::nuts::{CurrencyUnit, MintQuoteState, SecretKey, SpendingConditions}; use cdk::nuts::{CurrencyUnit, MintQuoteState, SecretKey, SpendingConditions};
use cdk::wallet::types::SendKind;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::Amount; use cdk::Amount;
use rand::Rng; use rand::Rng;
@@ -19,7 +20,7 @@ async fn main() -> Result<(), Error> {
let unit = CurrencyUnit::Sat; let unit = CurrencyUnit::Sat;
let amount = Amount::from(10); let amount = Amount::from(10);
let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed); let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None);
let quote = wallet.mint_quote(amount).await.unwrap(); let quote = wallet.mint_quote(amount).await.unwrap();
@@ -47,7 +48,14 @@ 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(amount, None, Some(spending_conditions), &SplitTarget::None) .send(
amount,
None,
Some(spending_conditions),
&SplitTarget::None,
&SendKind::default(),
false,
)
.await .await
.unwrap(); .unwrap();
@@ -55,7 +63,7 @@ async fn main() -> Result<(), Error> {
println!("{}", token); println!("{}", token);
let amount = wallet let amount = wallet
.receive(&token, &SplitTarget::default(), &[secret], &[]) .receive(&token, SplitTarget::default(), &[secret], &[])
.await .await
.unwrap(); .unwrap();

View File

@@ -0,0 +1,61 @@
//! Wallet example with memory store
use std::sync::Arc;
use std::time::Duration;
use cdk::amount::SplitTarget;
use cdk::cdk_database::WalletMemoryDatabase;
use cdk::nuts::{CurrencyUnit, MintQuoteState};
use cdk::wallet::Wallet;
use cdk::Amount;
use rand::Rng;
use tokio::time::sleep;
#[tokio::main]
async fn main() {
let seed = rand::thread_rng().gen::<[u8; 32]>();
let mint_url = "https://testnut.cashu.space";
let unit = CurrencyUnit::Sat;
let localstore = WalletMemoryDatabase::default();
let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None);
for amount in [64] {
let amount = Amount::from(amount);
let quote = wallet.mint_quote(amount).await.unwrap();
println!("Pay request: {}", quote.request);
loop {
let status = wallet.mint_quote_state(&quote.id).await.unwrap();
if status.state == MintQuoteState::Paid {
break;
}
println!("Quote state: {}", status.state);
sleep(Duration::from_secs(5)).await;
}
let receive_amount = wallet
.mint(&quote.id, SplitTarget::default(), None)
.await
.unwrap();
println!("Minted {}", receive_amount);
}
let proofs = wallet.get_proofs().await.unwrap();
let selected = wallet
.select_proofs_to_send(Amount::from(65), proofs, false)
.await
.unwrap();
for (i, proof) in selected.iter().enumerate() {
println!("{}: {}", i, proof.amount);
}
}

View File

@@ -6,6 +6,7 @@ use std::time::Duration;
use cdk::amount::SplitTarget; use cdk::amount::SplitTarget;
use cdk::cdk_database::WalletMemoryDatabase; use cdk::cdk_database::WalletMemoryDatabase;
use cdk::nuts::{CurrencyUnit, MintQuoteState}; use cdk::nuts::{CurrencyUnit, MintQuoteState};
use cdk::wallet::types::SendKind;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk::Amount; use cdk::Amount;
use rand::Rng; use rand::Rng;
@@ -21,7 +22,7 @@ async fn main() {
let localstore = WalletMemoryDatabase::default(); let localstore = WalletMemoryDatabase::default();
let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed); let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None);
let quote = wallet.mint_quote(amount).await.unwrap(); let quote = wallet.mint_quote(amount).await.unwrap();
@@ -47,7 +48,14 @@ async fn main() {
println!("Minted {}", receive_amount); println!("Minted {}", receive_amount);
let token = wallet let token = wallet
.send(amount, None, None, &SplitTarget::None) .send(
amount,
None,
None,
&SplitTarget::None,
&SendKind::default(),
false,
)
.await .await
.unwrap(); .unwrap();

View File

@@ -2,10 +2,13 @@
//! //!
//! Is any unit and will be treated as the unit of the wallet //! Is any unit and will be treated as the unit of the wallet
use std::cmp::Ordering;
use std::fmt; use std::fmt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::error::Error;
/// Amount can be any unit /// Amount can be any unit
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)] #[serde(transparent)]
@@ -28,12 +31,12 @@ impl Amount {
} }
/// Split into parts that are powers of two by target /// Split into parts that are powers of two by target
pub fn split_targeted(&self, target: &SplitTarget) -> Vec<Self> { pub fn split_targeted(&self, target: &SplitTarget) -> Result<Vec<Self>, Error> {
let mut parts = match *target { let mut parts = match target {
SplitTarget::None => self.split(), SplitTarget::None => self.split(),
SplitTarget::Value(amount) => { SplitTarget::Value(amount) => {
if self.le(&amount) { if self.le(amount) {
return self.split(); return Ok(self.split());
} }
let mut parts_total = Amount::ZERO; let mut parts_total = Amount::ZERO;
@@ -61,10 +64,28 @@ impl Amount {
parts parts
} }
SplitTarget::Values(values) => {
let values_total: Amount = values.clone().into_iter().sum();
match self.cmp(&values_total) {
Ordering::Equal => values.clone(),
Ordering::Less => {
return Err(Error::SplitValuesGreater);
}
Ordering::Greater => {
let extra = *self - values_total;
let mut extra_amount = extra.split();
let mut values = values.clone();
values.append(&mut extra_amount);
values
}
}
}
}; };
parts.sort(); parts.sort();
parts Ok(parts)
} }
} }
@@ -162,15 +183,15 @@ impl core::iter::Sum for Amount {
} }
/// Kinds of targeting that are supported /// Kinds of targeting that are supported
#[derive( #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
)]
pub enum SplitTarget { pub enum SplitTarget {
/// Default target; least amount of proofs /// Default target; least amount of proofs
#[default] #[default]
None, None,
/// Target amount for wallet to have most proofs that add up to value /// Target amount for wallet to have most proofs that add up to value
Value(Amount), Value(Amount),
/// Specific amounts to split into **must** equal amount being split
Values(Vec<Amount>),
} }
#[cfg(test)] #[cfg(test)]
@@ -198,12 +219,16 @@ mod tests {
fn test_split_target_amount() { fn test_split_target_amount() {
let amount = Amount(65); let amount = Amount(65);
let split = amount.split_targeted(&SplitTarget::Value(Amount(32))); let split = amount
.split_targeted(&SplitTarget::Value(Amount(32)))
.unwrap();
assert_eq!(vec![Amount(1), Amount(32), Amount(32)], split); assert_eq!(vec![Amount(1), Amount(32), Amount(32)], split);
let amount = Amount(150); let amount = Amount(150);
let split = amount.split_targeted(&SplitTarget::Value(Amount::from(50))); let split = amount
.split_targeted(&SplitTarget::Value(Amount::from(50)))
.unwrap();
assert_eq!( assert_eq!(
vec![ vec![
Amount(2), Amount(2),
@@ -221,7 +246,9 @@ mod tests {
let amount = Amount::from(63); let amount = Amount::from(63);
let split = amount.split_targeted(&SplitTarget::Value(Amount::from(32))); let split = amount
.split_targeted(&SplitTarget::Value(Amount::from(32)))
.unwrap();
assert_eq!( assert_eq!(
vec![ vec![
Amount(1), Amount(1),
@@ -234,4 +261,31 @@ mod tests {
split split
); );
} }
#[test]
fn test_split_values() {
let amount = Amount(10);
let target = vec![Amount(2), Amount(4), Amount(4)];
let split_target = SplitTarget::Values(target.clone());
let values = amount.split_targeted(&split_target).unwrap();
assert_eq!(target, values);
let target = vec![Amount(2), Amount(4), Amount(4)];
let split_target = SplitTarget::Values(vec![Amount(2), Amount(4)]);
let values = amount.split_targeted(&split_target).unwrap();
assert_eq!(target, values);
let split_target = SplitTarget::Values(vec![Amount(2), Amount(10)]);
let values = amount.split_targeted(&split_target);
assert!(values.is_err())
}
} }

View File

@@ -54,6 +54,9 @@ pub enum Error {
/// No valid point on curve /// No valid point on curve
#[error("No valid point found")] #[error("No valid point found")]
NoValidPoint, NoValidPoint,
/// Split Values must be less then or equal to amount
#[error("Split Values must be less then or equal to amount")]
SplitValuesGreater,
/// Secp256k1 error /// Secp256k1 error
#[error(transparent)] #[error(transparent)]
Secp256k1(#[from] bitcoin::secp256k1::Error), Secp256k1(#[from] bitcoin::secp256k1::Error),

View File

@@ -20,6 +20,9 @@ pub enum Error {
/// Amount is not what is expected /// Amount is not what is expected
#[error("Amount")] #[error("Amount")]
Amount, Amount,
/// Not engough inputs provided
#[error("Inputs: `{0}`, Outputs: `{0}`, Fee: `{0}`")]
InsufficientInputs(u64, u64, u64),
/// Duplicate proofs provided /// Duplicate proofs provided
#[error("Duplicate proofs")] #[error("Duplicate proofs")]
DuplicateProofs, DuplicateProofs,

View File

@@ -8,6 +8,7 @@ use bitcoin::secp256k1::{self, Secp256k1};
use error::Error; use error::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing::instrument;
use self::nut05::QuoteState; use self::nut05::QuoteState;
use crate::cdk_database::{self, MintDatabase}; use crate::cdk_database::{self, MintDatabase};
@@ -47,6 +48,7 @@ impl Mint {
localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>, localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>,
min_fee_reserve: Amount, min_fee_reserve: Amount,
percent_fee_reserve: f32, percent_fee_reserve: f32,
input_fee_ppk: u64,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let secp_ctx = Secp256k1::new(); let secp_ctx = Secp256k1::new();
let xpriv = let xpriv =
@@ -58,6 +60,9 @@ impl Mint {
match keysets_infos.is_empty() { match keysets_infos.is_empty() {
false => { false => {
for keyset_info in keysets_infos { for keyset_info in keysets_infos {
let mut keyset_info = keyset_info;
keyset_info.input_fee_ppk = input_fee_ppk;
localstore.add_keyset_info(keyset_info.clone()).await?;
if keyset_info.active { if keyset_info.active {
let id = keyset_info.id; let id = keyset_info.id;
let keyset = MintKeySet::generate_from_xpriv(&secp_ctx, xpriv, keyset_info); let keyset = MintKeySet::generate_from_xpriv(&secp_ctx, xpriv, keyset_info);
@@ -69,8 +74,14 @@ impl Mint {
let derivation_path = DerivationPath::from(vec![ let derivation_path = DerivationPath::from(vec![
ChildNumber::from_hardened_idx(0).expect("0 is a valid index") ChildNumber::from_hardened_idx(0).expect("0 is a valid index")
]); ]);
let (keyset, keyset_info) = let (keyset, keyset_info) = create_new_keyset(
create_new_keyset(&secp_ctx, xpriv, derivation_path, CurrencyUnit::Sat, 64); &secp_ctx,
xpriv,
derivation_path,
CurrencyUnit::Sat,
64,
input_fee_ppk,
);
let id = keyset_info.id; let id = keyset_info.id;
localstore.add_keyset_info(keyset_info).await?; localstore.add_keyset_info(keyset_info).await?;
localstore.add_active_keyset(CurrencyUnit::Sat, id).await?; localstore.add_active_keyset(CurrencyUnit::Sat, id).await?;
@@ -95,26 +106,31 @@ impl Mint {
} }
/// Set Mint Url /// Set Mint Url
#[instrument(skip_all)]
pub fn set_mint_url(&mut self, mint_url: UncheckedUrl) { pub fn set_mint_url(&mut self, mint_url: UncheckedUrl) {
self.mint_url = mint_url; self.mint_url = mint_url;
} }
/// Get Mint Url /// Get Mint Url
#[instrument(skip_all)]
pub fn get_mint_url(&self) -> &UncheckedUrl { pub fn get_mint_url(&self) -> &UncheckedUrl {
&self.mint_url &self.mint_url
} }
/// Set Mint Info /// Set Mint Info
#[instrument(skip_all)]
pub fn set_mint_info(&mut self, mint_info: MintInfo) { pub fn set_mint_info(&mut self, mint_info: MintInfo) {
self.mint_info = mint_info; self.mint_info = mint_info;
} }
/// Get Mint Info /// Get Mint Info
#[instrument(skip_all)]
pub fn mint_info(&self) -> &MintInfo { pub fn mint_info(&self) -> &MintInfo {
&self.mint_info &self.mint_info
} }
/// New mint quote /// New mint quote
#[instrument(skip_all)]
pub async fn new_mint_quote( pub async fn new_mint_quote(
&self, &self,
mint_url: UncheckedUrl, mint_url: UncheckedUrl,
@@ -139,6 +155,7 @@ impl Mint {
} }
/// Check mint quote /// Check mint quote
#[instrument(skip(self))]
pub async fn check_mint_quote(&self, quote_id: &str) -> Result<MintQuoteBolt11Response, Error> { pub async fn check_mint_quote(&self, quote_id: &str) -> Result<MintQuoteBolt11Response, Error> {
let quote = self let quote = self
.localstore .localstore
@@ -165,18 +182,21 @@ impl Mint {
} }
/// Update mint quote /// Update mint quote
#[instrument(skip_all)]
pub async fn update_mint_quote(&self, quote: MintQuote) -> Result<(), Error> { pub async fn update_mint_quote(&self, quote: MintQuote) -> Result<(), Error> {
self.localstore.add_mint_quote(quote).await?; self.localstore.add_mint_quote(quote).await?;
Ok(()) Ok(())
} }
/// Get mint quotes /// Get mint quotes
#[instrument(skip_all)]
pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> { pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
let quotes = self.localstore.get_mint_quotes().await?; let quotes = self.localstore.get_mint_quotes().await?;
Ok(quotes) Ok(quotes)
} }
/// Get pending mint quotes /// Get pending mint quotes
#[instrument(skip_all)]
pub async fn get_pending_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> { pub async fn get_pending_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
let mint_quotes = self.localstore.get_mint_quotes().await?; let mint_quotes = self.localstore.get_mint_quotes().await?;
@@ -187,6 +207,7 @@ impl Mint {
} }
/// Remove mint quote /// Remove mint quote
#[instrument(skip_all)]
pub async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> { pub async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> {
self.localstore.remove_mint_quote(quote_id).await?; self.localstore.remove_mint_quote(quote_id).await?;
@@ -194,6 +215,7 @@ impl Mint {
} }
/// New melt quote /// New melt quote
#[instrument(skip_all)]
pub async fn new_melt_quote( pub async fn new_melt_quote(
&self, &self,
request: String, request: String,
@@ -217,7 +239,28 @@ impl Mint {
Ok(quote) Ok(quote)
} }
/// Fee required for proof set
#[instrument(skip_all)]
pub async fn get_proofs_fee(&self, proofs: &Proofs) -> Result<Amount, Error> {
let mut sum_fee = 0;
for proof in proofs {
let input_fee_ppk = self
.localstore
.get_keyset_info(&proof.keyset_id)
.await?
.ok_or(Error::UnknownKeySet)?;
sum_fee += input_fee_ppk.input_fee_ppk;
}
let fee = (sum_fee + 999) / 1000;
Ok(Amount::from(fee))
}
/// Check melt quote status /// Check melt quote status
#[instrument(skip(self))]
pub async fn check_melt_quote(&self, quote_id: &str) -> Result<MeltQuoteBolt11Response, Error> { pub async fn check_melt_quote(&self, quote_id: &str) -> Result<MeltQuoteBolt11Response, Error> {
let quote = self let quote = self
.localstore .localstore
@@ -238,26 +281,29 @@ impl Mint {
} }
/// Update melt quote /// Update melt quote
#[instrument(skip_all)]
pub async fn update_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> { pub async fn update_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> {
self.localstore.add_melt_quote(quote).await?; self.localstore.add_melt_quote(quote).await?;
Ok(()) Ok(())
} }
/// Get melt quotes /// Get melt quotes
#[instrument(skip_all)]
pub async fn melt_quotes(&self) -> Result<Vec<MeltQuote>, Error> { pub async fn melt_quotes(&self) -> Result<Vec<MeltQuote>, Error> {
let quotes = self.localstore.get_melt_quotes().await?; let quotes = self.localstore.get_melt_quotes().await?;
Ok(quotes) Ok(quotes)
} }
/// Remove melt quote /// Remove melt quote
#[instrument(skip(self))]
pub async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error> { pub async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error> {
self.localstore.remove_melt_quote(quote_id).await?; self.localstore.remove_melt_quote(quote_id).await?;
Ok(()) Ok(())
} }
/// Retrieve the public keys of the active keyset for distribution to /// Retrieve the public keys of the active keyset for distribution to wallet clients
/// wallet clients #[instrument(skip(self))]
pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result<KeysResponse, Error> { pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result<KeysResponse, Error> {
self.ensure_keyset_loaded(keyset_id).await?; self.ensure_keyset_loaded(keyset_id).await?;
let keysets = self.keysets.read().await; let keysets = self.keysets.read().await;
@@ -267,8 +313,8 @@ impl Mint {
}) })
} }
/// Retrieve the public keys of the active keyset for distribution to /// Retrieve the public keys of the active keyset for distribution to wallet clients
/// wallet clients #[instrument(skip_all)]
pub async fn pubkeys(&self) -> Result<KeysResponse, Error> { pub async fn pubkeys(&self) -> Result<KeysResponse, Error> {
let keyset_infos = self.localstore.get_keyset_infos().await?; let keyset_infos = self.localstore.get_keyset_infos().await?;
for keyset_info in keyset_infos { for keyset_info in keyset_infos {
@@ -281,6 +327,7 @@ impl Mint {
} }
/// Return a list of all supported keysets /// Return a list of all supported keysets
#[instrument(skip_all)]
pub async fn keysets(&self) -> Result<KeysetResponse, Error> { pub async fn keysets(&self) -> Result<KeysetResponse, Error> {
let keysets = self.localstore.get_keyset_infos().await?; let keysets = self.localstore.get_keyset_infos().await?;
let active_keysets: HashSet<Id> = self let active_keysets: HashSet<Id> = self
@@ -297,6 +344,7 @@ impl Mint {
id: k.id, id: k.id,
unit: k.unit, unit: k.unit,
active: active_keysets.contains(&k.id), active: active_keysets.contains(&k.id),
input_fee_ppk: k.input_fee_ppk,
}) })
.collect(); .collect();
@@ -304,6 +352,7 @@ impl Mint {
} }
/// Get keysets /// Get keysets
#[instrument(skip(self))]
pub async fn keyset(&self, id: &Id) -> Result<Option<KeySet>, Error> { pub async fn keyset(&self, id: &Id) -> Result<Option<KeySet>, Error> {
self.ensure_keyset_loaded(id).await?; self.ensure_keyset_loaded(id).await?;
let keysets = self.keysets.read().await; let keysets = self.keysets.read().await;
@@ -313,14 +362,22 @@ impl Mint {
/// Add current keyset to inactive keysets /// Add current keyset to inactive keysets
/// Generate new keyset /// Generate new keyset
#[instrument(skip(self))]
pub async fn rotate_keyset( pub async fn rotate_keyset(
&self, &self,
unit: CurrencyUnit, unit: CurrencyUnit,
derivation_path: DerivationPath, derivation_path: DerivationPath,
max_order: u8, max_order: u8,
input_fee_ppk: u64,
) -> Result<(), Error> { ) -> Result<(), Error> {
let (keyset, keyset_info) = let (keyset, keyset_info) = create_new_keyset(
create_new_keyset(&self.secp_ctx, self.xpriv, derivation_path, unit, max_order); &self.secp_ctx,
self.xpriv,
derivation_path,
unit,
max_order,
input_fee_ppk,
);
let id = keyset_info.id; let id = keyset_info.id;
self.localstore.add_keyset_info(keyset_info).await?; self.localstore.add_keyset_info(keyset_info).await?;
self.localstore.add_active_keyset(unit, id).await?; self.localstore.add_active_keyset(unit, id).await?;
@@ -332,6 +389,7 @@ impl Mint {
} }
/// Process mint request /// Process mint request
#[instrument(skip_all)]
pub async fn process_mint_request( pub async fn process_mint_request(
&self, &self,
mint_request: nut04::MintBolt11Request, mint_request: nut04::MintBolt11Request,
@@ -397,6 +455,7 @@ impl Mint {
} }
/// Blind Sign /// Blind Sign
#[instrument(skip_all)]
pub async fn blind_sign( pub async fn blind_sign(
&self, &self,
blinded_message: &BlindedMessage, blinded_message: &BlindedMessage,
@@ -447,6 +506,7 @@ impl Mint {
} }
/// Process Swap /// Process Swap
#[instrument(skip_all)]
pub async fn process_swap_request( pub async fn process_swap_request(
&self, &self,
swap_request: SwapRequest, swap_request: SwapRequest,
@@ -470,8 +530,20 @@ impl Mint {
let output_total = swap_request.output_amount(); let output_total = swap_request.output_amount();
if proofs_total != output_total { let fee = self.get_proofs_fee(&swap_request.inputs).await?;
return Err(Error::Amount);
if proofs_total < output_total + fee {
tracing::info!(
"Swap request without enough inputs: {}, outputs {}, fee {}",
proofs_total,
output_total,
fee
);
return Err(Error::InsufficientInputs(
proofs_total.into(),
output_total.into(),
fee.into(),
));
} }
let proof_count = swap_request.inputs.len(); let proof_count = swap_request.inputs.len();
@@ -554,6 +626,7 @@ impl Mint {
} }
/// Verify [`Proof`] meets conditions and is signed /// Verify [`Proof`] meets conditions and is signed
#[instrument(skip_all)]
pub async fn verify_proof(&self, proof: &Proof) -> Result<(), Error> { pub async fn verify_proof(&self, proof: &Proof) -> Result<(), Error> {
// Check if secret is a nut10 secret with conditions // Check if secret is a nut10 secret with conditions
if let Ok(secret) = if let Ok(secret) =
@@ -597,6 +670,7 @@ impl Mint {
} }
/// Check state /// Check state
#[instrument(skip_all)]
pub async fn check_state( pub async fn check_state(
&self, &self,
check_state: &CheckStateRequest, check_state: &CheckStateRequest,
@@ -622,6 +696,7 @@ impl Mint {
} }
/// Verify melt request is valid /// Verify melt request is valid
#[instrument(skip_all)]
pub async fn verify_melt_request( pub async fn verify_melt_request(
&self, &self,
melt_request: &MeltBolt11Request, melt_request: &MeltBolt11Request,
@@ -653,15 +728,23 @@ impl Mint {
let proofs_total = melt_request.proofs_amount(); let proofs_total = melt_request.proofs_amount();
let required_total = quote.amount + quote.fee_reserve; let fee = self.get_proofs_fee(&melt_request.inputs).await?;
let required_total = quote.amount + quote.fee_reserve + fee;
if proofs_total < required_total { if proofs_total < required_total {
tracing::debug!( tracing::info!(
"Insufficient Proofs: Got: {}, Required: {}", "Swap request without enough inputs: {}, quote amount {}, fee_reserve: {} fee {}",
proofs_total, proofs_total,
required_total quote.amount,
quote.fee_reserve,
fee
); );
return Err(Error::Amount); return Err(Error::InsufficientInputs(
proofs_total.into(),
(quote.amount + quote.fee_reserve).into(),
fee.into(),
));
} }
let input_keyset_ids: HashSet<Id> = let input_keyset_ids: HashSet<Id> =
@@ -740,6 +823,7 @@ impl Mint {
/// Process unpaid melt request /// Process unpaid melt request
/// In the event that a melt request fails and the lighthing payment is not made /// In the event that a melt request fails and the lighthing payment is not made
/// The [`Proofs`] should be returned to an unspent state and the quote should be unpaid /// The [`Proofs`] should be returned to an unspent state and the quote should be unpaid
#[instrument(skip_all)]
pub async fn process_unpaid_melt(&self, melt_request: &MeltBolt11Request) -> Result<(), Error> { pub async fn process_unpaid_melt(&self, melt_request: &MeltBolt11Request) -> Result<(), Error> {
self.localstore self.localstore
.remove_pending_proofs(melt_request.inputs.iter().map(|p| &p.secret).collect()) .remove_pending_proofs(melt_request.inputs.iter().map(|p| &p.secret).collect())
@@ -754,6 +838,7 @@ impl Mint {
/// Process melt request marking [`Proofs`] as spent /// Process melt request marking [`Proofs`] as spent
/// The melt request must be verifyed using [`Self::verify_melt_request`] before calling [`Self::process_melt_request`] /// The melt request must be verifyed using [`Self::verify_melt_request`] before calling [`Self::process_melt_request`]
#[instrument(skip_all)]
pub async fn process_melt_request( pub async fn process_melt_request(
&self, &self,
melt_request: &MeltBolt11Request, melt_request: &MeltBolt11Request,
@@ -851,6 +936,7 @@ impl Mint {
} }
/// Restore /// Restore
#[instrument(skip_all)]
pub async fn restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> { pub async fn restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
let output_len = request.outputs.len(); let output_len = request.outputs.len();
@@ -883,6 +969,7 @@ impl Mint {
} }
/// Ensure Keyset is loaded in mint /// Ensure Keyset is loaded in mint
#[instrument(skip(self))]
pub async fn ensure_keyset_loaded(&self, id: &Id) -> Result<(), Error> { pub async fn ensure_keyset_loaded(&self, id: &Id) -> Result<(), Error> {
let keysets = self.keysets.read().await; let keysets = self.keysets.read().await;
if keysets.contains_key(id) { if keysets.contains_key(id) {
@@ -902,6 +989,7 @@ impl Mint {
} }
/// Generate [`MintKeySet`] from [`MintKeySetInfo`] /// Generate [`MintKeySet`] from [`MintKeySetInfo`]
#[instrument(skip_all)]
pub fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet { pub fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet {
MintKeySet::generate_from_xpriv(&self.secp_ctx, self.xpriv, keyset_info) MintKeySet::generate_from_xpriv(&self.secp_ctx, self.xpriv, keyset_info)
} }
@@ -935,6 +1023,13 @@ pub struct MintKeySetInfo {
pub derivation_path: DerivationPath, pub derivation_path: DerivationPath,
/// Max order of keyset /// Max order of keyset
pub max_order: u8, pub max_order: u8,
/// Input Fee ppk
#[serde(default = "default_fee")]
pub input_fee_ppk: u64,
}
fn default_fee() -> u64 {
0
} }
impl From<MintKeySetInfo> for KeySetInfo { impl From<MintKeySetInfo> for KeySetInfo {
@@ -943,17 +1038,20 @@ impl From<MintKeySetInfo> for KeySetInfo {
id: keyset_info.id, id: keyset_info.id,
unit: keyset_info.unit, unit: keyset_info.unit,
active: keyset_info.active, active: keyset_info.active,
input_fee_ppk: keyset_info.input_fee_ppk,
} }
} }
} }
/// Generate new [`MintKeySetInfo`] from path /// Generate new [`MintKeySetInfo`] from path
#[instrument(skip_all)]
fn create_new_keyset<C: secp256k1::Signing>( fn create_new_keyset<C: secp256k1::Signing>(
secp: &secp256k1::Secp256k1<C>, secp: &secp256k1::Secp256k1<C>,
xpriv: ExtendedPrivKey, xpriv: ExtendedPrivKey,
derivation_path: DerivationPath, derivation_path: DerivationPath,
unit: CurrencyUnit, unit: CurrencyUnit,
max_order: u8, max_order: u8,
input_fee_ppk: u64,
) -> (MintKeySet, MintKeySetInfo) { ) -> (MintKeySet, MintKeySetInfo) {
let keyset = MintKeySet::generate( let keyset = MintKeySet::generate(
secp, secp,
@@ -971,6 +1069,7 @@ fn create_new_keyset<C: secp256k1::Signing>(
valid_to: None, valid_to: None,
derivation_path, derivation_path,
max_order, max_order,
input_fee_ppk,
}; };
(keyset, keyset_info) (keyset, keyset_info)
} }

View File

@@ -444,20 +444,30 @@ impl PartialOrd for PreMint {
} }
/// Premint Secrets /// Premint Secrets
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PreMintSecrets { pub struct PreMintSecrets {
/// Secrets /// Secrets
pub secrets: Vec<PreMint>, pub secrets: Vec<PreMint>,
/// Keyset Id
pub keyset_id: Id,
} }
impl PreMintSecrets { impl PreMintSecrets {
/// Create new [`PreMintSecrets`]
pub fn new(keyset_id: Id) -> Self {
Self {
secrets: Vec::new(),
keyset_id,
}
}
/// Outputs for speceifed amount with random secret /// Outputs for speceifed amount with random secret
pub fn random( pub fn random(
keyset_id: Id, keyset_id: Id,
amount: Amount, amount: Amount,
amount_split_target: &SplitTarget, amount_split_target: &SplitTarget,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let amount_split = amount.split_targeted(amount_split_target); let amount_split = amount.split_targeted(amount_split_target)?;
let mut output = Vec::with_capacity(amount_split.len()); let mut output = Vec::with_capacity(amount_split.len());
@@ -475,7 +485,10 @@ impl PreMintSecrets {
}); });
} }
Ok(PreMintSecrets { secrets: output }) Ok(PreMintSecrets {
secrets: output,
keyset_id,
})
} }
/// Outputs from pre defined secrets /// Outputs from pre defined secrets
@@ -499,7 +512,10 @@ impl PreMintSecrets {
}); });
} }
Ok(PreMintSecrets { secrets: output }) Ok(PreMintSecrets {
secrets: output,
keyset_id,
})
} }
/// Blank Outputs used for NUT-08 change /// Blank Outputs used for NUT-08 change
@@ -522,7 +538,10 @@ impl PreMintSecrets {
}) })
} }
Ok(PreMintSecrets { secrets: output }) Ok(PreMintSecrets {
secrets: output,
keyset_id,
})
} }
/// Outputs with specific spending conditions /// Outputs with specific spending conditions
@@ -532,7 +551,7 @@ impl PreMintSecrets {
amount_split_target: &SplitTarget, amount_split_target: &SplitTarget,
conditions: &SpendingConditions, conditions: &SpendingConditions,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let amount_split = amount.split_targeted(amount_split_target); let amount_split = amount.split_targeted(amount_split_target)?;
let mut output = Vec::with_capacity(amount_split.len()); let mut output = Vec::with_capacity(amount_split.len());
@@ -552,7 +571,10 @@ impl PreMintSecrets {
}); });
} }
Ok(PreMintSecrets { secrets: output }) Ok(PreMintSecrets {
secrets: output,
keyset_id,
})
} }
/// Iterate over secrets /// Iterate over secrets

View File

@@ -230,15 +230,6 @@ pub struct KeysetResponse {
pub keysets: Vec<KeySetInfo>, pub keysets: Vec<KeySetInfo>,
} }
impl KeysetResponse {
/// Create new [`KeysetResponse`]
pub fn new(keysets: Vec<KeySet>) -> Self {
Self {
keysets: keysets.into_iter().map(|keyset| keyset.into()).collect(),
}
}
}
/// Keyset /// Keyset
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct KeySet { pub struct KeySet {
@@ -271,16 +262,13 @@ pub struct KeySetInfo {
/// Keyset state /// Keyset state
/// Mint will only sign from an active keyset /// Mint will only sign from an active keyset
pub active: bool, pub active: bool,
/// Input Fee PPK
#[serde(default = "default_input_fee_ppk")]
pub input_fee_ppk: u64,
} }
impl From<KeySet> for KeySetInfo { fn default_input_fee_ppk() -> u64 {
fn from(keyset: KeySet) -> KeySetInfo { 0
Self {
id: keyset.id,
unit: keyset.unit,
active: false,
}
}
} }
/// MintKeyset /// MintKeyset
@@ -504,7 +492,7 @@ mod test {
#[test] #[test]
fn test_deserialization_of_keyset_response() { fn test_deserialization_of_keyset_response() {
let h = r#"{"keysets":[{"id":"009a1f293253e41e","unit":"sat","active":true},{"id":"eGnEWtdJ0PIM","unit":"sat","active":true},{"id":"003dfdf4e5e35487","unit":"sat","active":true},{"id":"0066ad1a4b6fc57c","unit":"sat","active":true},{"id":"00f7ca24d44c3e5e","unit":"sat","active":true},{"id":"001fcea2931f2d85","unit":"sat","active":true},{"id":"00d095959d940edb","unit":"sat","active":true},{"id":"000d7f730d657125","unit":"sat","active":true},{"id":"0007208d861d7295","unit":"sat","active":true},{"id":"00bfdf8889b719dd","unit":"sat","active":true},{"id":"00ca9b17da045f21","unit":"sat","active":true}]}"#; let h = r#"{"keysets":[{"id":"009a1f293253e41e","unit":"sat","active":true, "input_fee_ppk": 100},{"id":"eGnEWtdJ0PIM","unit":"sat","active":true},{"id":"003dfdf4e5e35487","unit":"sat","active":true},{"id":"0066ad1a4b6fc57c","unit":"sat","active":true},{"id":"00f7ca24d44c3e5e","unit":"sat","active":true},{"id":"001fcea2931f2d85","unit":"sat","active":true},{"id":"00d095959d940edb","unit":"sat","active":true},{"id":"000d7f730d657125","unit":"sat","active":true},{"id":"0007208d861d7295","unit":"sat","active":true},{"id":"00bfdf8889b719dd","unit":"sat","active":true},{"id":"00ca9b17da045f21","unit":"sat","active":true}]}"#;
let _keyset_response: KeysetResponse = serde_json::from_str(h).unwrap(); let _keyset_response: KeysetResponse = serde_json::from_str(h).unwrap();
} }

View File

@@ -16,6 +16,8 @@ pub struct PreSwap {
pub swap_request: SwapRequest, pub swap_request: SwapRequest,
/// Amount to increment keyset counter by /// Amount to increment keyset counter by
pub derived_secret_count: u32, pub derived_secret_count: u32,
/// Fee amount
pub fee: Amount,
} }
/// Split Request [NUT-06] /// Split Request [NUT-06]

View File

@@ -3,6 +3,7 @@
//! <https://github.com/cashubtc/nuts/blob/main/13.md> //! <https://github.com/cashubtc/nuts/blob/main/13.md>
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}; use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey};
use tracing::instrument;
use super::nut00::{BlindedMessage, PreMint, PreMintSecrets}; use super::nut00::{BlindedMessage, PreMint, PreMintSecrets};
use super::nut01::SecretKey; use super::nut01::SecretKey;
@@ -43,6 +44,7 @@ impl SecretKey {
impl PreMintSecrets { impl PreMintSecrets {
/// Generate blinded messages from predetermined secrets and blindings /// Generate blinded messages from predetermined secrets and blindings
/// factor /// factor
#[instrument(skip(xpriv))]
pub fn from_xpriv( pub fn from_xpriv(
keyset_id: Id, keyset_id: Id,
counter: u32, counter: u32,
@@ -50,11 +52,11 @@ impl PreMintSecrets {
amount: Amount, amount: Amount,
amount_split_target: &SplitTarget, amount_split_target: &SplitTarget,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let mut pre_mint_secrets = PreMintSecrets::default(); let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
let mut counter = counter; let mut counter = counter;
for amount in amount.split_targeted(amount_split_target) { for amount in amount.split_targeted(amount_split_target)? {
let secret = Secret::from_xpriv(xpriv, keyset_id, counter)?; let secret = Secret::from_xpriv(xpriv, keyset_id, counter)?;
let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, counter)?; let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, counter)?;
@@ -84,10 +86,10 @@ impl PreMintSecrets {
amount: Amount, amount: Amount,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
if amount <= Amount::ZERO { if amount <= Amount::ZERO {
return Ok(PreMintSecrets::default()); return Ok(PreMintSecrets::new(keyset_id));
} }
let count = ((u64::from(amount) as f64).log2().ceil() as u64).max(1); let count = ((u64::from(amount) as f64).log2().ceil() as u64).max(1);
let mut pre_mint_secrets = PreMintSecrets::default(); let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
let mut counter = counter; let mut counter = counter;
@@ -115,15 +117,14 @@ impl PreMintSecrets {
Ok(pre_mint_secrets) Ok(pre_mint_secrets)
} }
/// Generate blinded messages from predetermined secrets and blindings /// Generate blinded messages from predetermined secrets and blindings factor
/// factor
pub fn restore_batch( pub fn restore_batch(
keyset_id: Id, keyset_id: Id,
xpriv: ExtendedPrivKey, xpriv: ExtendedPrivKey,
start_count: u32, start_count: u32,
end_count: u32, end_count: u32,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let mut pre_mint_secrets = PreMintSecrets::default(); let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
for i in start_count..=end_count { for i in start_count..=end_count {
let secret = Secret::from_xpriv(xpriv, keyset_id, i)?; let secret = Secret::from_xpriv(xpriv, keyset_id, i)?;

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::instrument; use tracing::instrument;
use super::types::SendKind;
use super::Error; use super::Error;
use crate::amount::SplitTarget; use crate::amount::SplitTarget;
use crate::nuts::{CurrencyUnit, SecretKey, SpendingConditions, Token}; use crate::nuts::{CurrencyUnit, SecretKey, SpendingConditions, Token};
@@ -122,6 +123,8 @@ impl MultiMintWallet {
amount: Amount, amount: Amount,
memo: Option<String>, memo: Option<String>,
conditions: Option<SpendingConditions>, conditions: Option<SpendingConditions>,
send_kind: SendKind,
include_fees: bool,
) -> Result<String, Error> { ) -> Result<String, Error> {
let wallet = self let wallet = self
.get_wallet(wallet_key) .get_wallet(wallet_key)
@@ -129,7 +132,14 @@ impl MultiMintWallet {
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?; .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
wallet wallet
.send(amount, memo, conditions, &SplitTarget::default()) .send(
amount,
memo,
conditions,
&SplitTarget::default(),
&send_kind,
include_fees,
)
.await .await
} }
@@ -228,12 +238,7 @@ impl MultiMintWallet {
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?; .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
let amount = wallet let amount = wallet
.receive_proofs( .receive_proofs(proofs, SplitTarget::default(), p2pk_signing_keys, preimages)
proofs,
&SplitTarget::default(),
p2pk_signing_keys,
preimages,
)
.await?; .await?;
amount_received += amount; amount_received += amount;

View File

@@ -44,3 +44,17 @@ pub struct MeltQuote {
/// Payment preimage /// Payment preimage
pub payment_preimage: Option<String>, pub payment_preimage: Option<String>,
} }
/// Send Kind
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum SendKind {
#[default]
/// Allow online swap before send if wallet does not have exact amount
OnlineExact,
/// Prefer offline send if difference is less then tolerance
OnlineTolerance(Amount),
/// Wallet cannot do an online swap and selectedp proof must be exactly send amount
OfflineExact,
/// Wallet must remain offline but can over pay if below tolerance
OfflineTolerance(Amount),
}