mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-23 07:35:03 +01:00
feat(NUT02): add input_fee_ppk
chore: instrument log on mint fns
This commit is contained in:
@@ -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]).
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE keyset ADD input_fee_ppk INTEGER;
|
||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE keyset ADD input_fee_ppk INTEGER;
|
||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
61
crates/cdk/examples/proof_selection.rs
Normal file
61
crates/cdk/examples/proof_selection.rs
Normal 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("e.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("e.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user