mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 05:35:18 +01:00
feat: bolt12
This commit is contained in:
@@ -11,6 +11,8 @@ rust-version.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[features]
|
||||
default = ["bip353"]
|
||||
bip353 = ["dep:trust-dns-resolver"]
|
||||
sqlcipher = ["cdk-sqlite/sqlcipher"]
|
||||
# MSRV is not tracked with redb enabled
|
||||
redb = ["dep:cdk-redb"]
|
||||
@@ -37,3 +39,5 @@ nostr-sdk = { version = "0.41.0", default-features = false, features = [
|
||||
reqwest.workspace = true
|
||||
url.workspace = true
|
||||
serde_with.workspace = true
|
||||
lightning.workspace = true
|
||||
trust-dns-resolver = { version = "0.23.2", optional = true }
|
||||
|
||||
132
crates/cdk-cli/src/bip353.rs
Normal file
132
crates/cdk-cli/src/bip353.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
|
||||
use trust_dns_resolver::TokioAsyncResolver;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Bip353Address {
|
||||
pub user: String,
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
impl Bip353Address {
|
||||
/// Resolve a human-readable Bitcoin address
|
||||
pub async fn resolve(self) -> Result<PaymentInstruction> {
|
||||
// Construct DNS name
|
||||
let dns_name = format!("{}.user._bitcoin-payment.{}", self.user, self.domain);
|
||||
|
||||
// Create a new resolver with DNSSEC validation
|
||||
let mut opts = ResolverOpts::default();
|
||||
opts.validate = true; // Enable DNSSEC validation
|
||||
|
||||
let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), opts);
|
||||
|
||||
// Query TXT records - with opts.validate=true, this will fail if DNSSEC validation fails
|
||||
let response = resolver.txt_lookup(&dns_name).await?;
|
||||
|
||||
// Extract and concatenate TXT record strings
|
||||
let mut bitcoin_uris = Vec::new();
|
||||
|
||||
for txt in response.iter() {
|
||||
let txt_data: Vec<String> = txt
|
||||
.txt_data()
|
||||
.iter()
|
||||
.map(|bytes| String::from_utf8_lossy(bytes).into_owned())
|
||||
.collect();
|
||||
|
||||
let concatenated = txt_data.join("");
|
||||
|
||||
if concatenated.to_lowercase().starts_with("bitcoin:") {
|
||||
bitcoin_uris.push(concatenated);
|
||||
}
|
||||
}
|
||||
|
||||
// BIP-353 requires exactly one Bitcoin URI
|
||||
match bitcoin_uris.len() {
|
||||
0 => bail!("No Bitcoin URI found"),
|
||||
1 => PaymentInstruction::from_uri(&bitcoin_uris[0]),
|
||||
_ => bail!("Multiple Bitcoin URIs found"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Bip353Address {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
/// Parse a human-readable Bitcoin address
|
||||
fn from_str(address: &str) -> Result<Self, Self::Err> {
|
||||
let addr = address.trim();
|
||||
|
||||
// Remove Bitcoin prefix if present
|
||||
let addr = addr.strip_prefix("₿").unwrap_or(addr);
|
||||
|
||||
// Split by @
|
||||
let parts: Vec<&str> = addr.split('@').collect();
|
||||
if parts.len() != 2 {
|
||||
bail!("Address is not formatted correctly")
|
||||
}
|
||||
|
||||
let user = parts[0].trim();
|
||||
let domain = parts[1].trim();
|
||||
|
||||
if user.is_empty() || domain.is_empty() {
|
||||
bail!("User name and domain must not be empty")
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
user: user.to_string(),
|
||||
domain: domain.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Payment instruction type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum PaymentType {
|
||||
OnChain,
|
||||
LightningOffer,
|
||||
}
|
||||
|
||||
/// BIP-353 payment instruction
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PaymentInstruction {
|
||||
pub parameters: HashMap<PaymentType, String>,
|
||||
}
|
||||
|
||||
impl PaymentInstruction {
|
||||
/// Parse a payment instruction from a Bitcoin URI
|
||||
pub fn from_uri(uri: &str) -> Result<Self> {
|
||||
if !uri.to_lowercase().starts_with("bitcoin:") {
|
||||
bail!("URI must start with 'bitcoin:'")
|
||||
}
|
||||
|
||||
let mut parameters = HashMap::new();
|
||||
|
||||
// Parse URI parameters
|
||||
if let Some(query_start) = uri.find('?') {
|
||||
let query = &uri[query_start + 1..];
|
||||
for pair in query.split('&') {
|
||||
if let Some(eq_pos) = pair.find('=') {
|
||||
let key = pair[..eq_pos].to_string();
|
||||
let value = pair[eq_pos + 1..].to_string();
|
||||
let payment_type;
|
||||
// Determine payment type
|
||||
if key.contains("lno") {
|
||||
payment_type = PaymentType::LightningOffer;
|
||||
} else if !uri[8..].contains('?') && uri.len() > 8 {
|
||||
// Simple on-chain address
|
||||
payment_type = PaymentType::OnChain;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
parameters.insert(payment_type, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(PaymentInstruction { parameters })
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ use tracing::Level;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(feature = "bip353")]
|
||||
mod bip353;
|
||||
mod nostr_storage;
|
||||
mod sub_commands;
|
||||
mod token_storage;
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use cdk::amount::MSAT_IN_SAT;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use cdk::amount::{amount_for_offer, Amount, MSAT_IN_SAT};
|
||||
use cdk::mint_url::MintUrl;
|
||||
use cdk::nuts::{CurrencyUnit, MeltOptions};
|
||||
use cdk::wallet::multi_mint_wallet::MultiMintWallet;
|
||||
use cdk::wallet::types::WalletKey;
|
||||
use cdk::wallet::{MeltQuote, Wallet};
|
||||
use cdk::Bolt11Invoice;
|
||||
use clap::Args;
|
||||
use clap::{Args, ValueEnum};
|
||||
use lightning::offers::offer::Offer;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::bip353::{Bip353Address, PaymentType as Bip353PaymentType};
|
||||
use crate::sub_commands::balance::mint_balances;
|
||||
use crate::utils::{
|
||||
get_number_input, get_user_input, get_wallet_by_index, get_wallet_by_mint_url,
|
||||
validate_mint_number,
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
pub enum PaymentType {
|
||||
/// BOLT11 invoice
|
||||
Bolt11,
|
||||
/// BOLT12 offer
|
||||
Bolt12,
|
||||
/// Bip353
|
||||
Bip353,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct MeltSubCommand {
|
||||
/// Currency unit e.g. sat
|
||||
@@ -26,6 +40,56 @@ pub struct MeltSubCommand {
|
||||
/// Mint URL to use for melting
|
||||
#[arg(long, conflicts_with = "mpp")]
|
||||
mint_url: Option<String>,
|
||||
/// Payment method (bolt11 or bolt12)
|
||||
#[arg(long, default_value = "bolt11")]
|
||||
method: PaymentType,
|
||||
}
|
||||
|
||||
/// Helper function to process a melt quote and execute the payment
|
||||
async fn process_payment(wallet: &Wallet, quote: MeltQuote) -> Result<()> {
|
||||
// Display quote information
|
||||
println!("Quote ID: {}", quote.id);
|
||||
println!("Amount: {}", quote.amount);
|
||||
println!("Fee Reserve: {}", quote.fee_reserve);
|
||||
println!("State: {}", quote.state);
|
||||
println!("Expiry: {}", quote.expiry);
|
||||
|
||||
// Execute the payment
|
||||
let melt = wallet.melt("e.id).await?;
|
||||
println!("Paid: {}", melt.state);
|
||||
|
||||
if let Some(preimage) = melt.preimage {
|
||||
println!("Payment preimage: {preimage}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to check if there are enough funds and create appropriate MeltOptions
|
||||
fn create_melt_options(
|
||||
available_funds: u64,
|
||||
payment_amount: Option<u64>,
|
||||
prompt: &str,
|
||||
) -> Result<Option<MeltOptions>> {
|
||||
match payment_amount {
|
||||
Some(amount) => {
|
||||
// Payment has a specified amount
|
||||
if amount > available_funds {
|
||||
bail!("Not enough funds; payment requires {} msats", amount);
|
||||
}
|
||||
Ok(None) // Use default options
|
||||
}
|
||||
None => {
|
||||
// Payment doesn't have an amount, ask user for it
|
||||
let user_amount = get_number_input::<u64>(prompt)? * MSAT_IN_SAT;
|
||||
|
||||
if user_amount > available_funds {
|
||||
bail!("Not enough funds");
|
||||
}
|
||||
|
||||
Ok(Some(MeltOptions::new_amountless(user_amount)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn pay(
|
||||
@@ -35,123 +99,31 @@ pub async fn pay(
|
||||
let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
|
||||
let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
|
||||
|
||||
let mut mints = vec![];
|
||||
let mut mint_amounts = vec![];
|
||||
if sub_command_args.mpp {
|
||||
// MPP functionality expects multiple mints, so mint_url flag doesn't fully apply here,
|
||||
// but we can offer to use the specified mint as the first one if provided
|
||||
if let Some(mint_url) = &sub_command_args.mint_url {
|
||||
println!("Using mint URL {mint_url} as the first mint for MPP payment.");
|
||||
|
||||
// Check if the mint exists
|
||||
if let Ok(_wallet) =
|
||||
get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit.clone()).await
|
||||
{
|
||||
// Find the index of this mint in the mints_amounts list
|
||||
if let Some(mint_index) = mints_amounts
|
||||
.iter()
|
||||
.position(|(url, _)| url.to_string() == *mint_url)
|
||||
{
|
||||
mints.push(mint_index);
|
||||
let melt_amount: u64 =
|
||||
get_number_input("Enter amount to mint from this mint in sats.")?;
|
||||
mint_amounts.push(melt_amount);
|
||||
} else {
|
||||
println!("Warning: Mint URL exists but no balance found. Continuing with manual selection.");
|
||||
}
|
||||
} else {
|
||||
println!("Warning: Could not find wallet for the specified mint URL. Continuing with manual selection.");
|
||||
}
|
||||
}
|
||||
loop {
|
||||
let mint_number: String =
|
||||
get_user_input("Enter mint number to melt from and -1 when done.")?;
|
||||
|
||||
if mint_number == "-1" || mint_number.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mint_number: usize = mint_number.parse()?;
|
||||
validate_mint_number(mint_number, mints_amounts.len())?;
|
||||
|
||||
mints.push(mint_number);
|
||||
let melt_amount: u64 =
|
||||
get_number_input("Enter amount to mint from this mint in sats.")?;
|
||||
mint_amounts.push(melt_amount);
|
||||
// MPP logic only works with BOLT11 currently
|
||||
if !matches!(sub_command_args.method, PaymentType::Bolt11) {
|
||||
bail!("MPP is only supported for BOLT11 invoices");
|
||||
}
|
||||
|
||||
// Collect mint numbers and amounts for MPP
|
||||
let (mints, mint_amounts) = collect_mpp_inputs(&mints_amounts, &sub_command_args.mint_url)?;
|
||||
|
||||
// Process BOLT11 MPP payment
|
||||
let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
|
||||
|
||||
let mut quotes = JoinSet::new();
|
||||
// Get quotes from all mints
|
||||
let quotes = get_mpp_quotes(
|
||||
multi_mint_wallet,
|
||||
&mints_amounts,
|
||||
&mints,
|
||||
&mint_amounts,
|
||||
&unit,
|
||||
&bolt11,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for (mint, amount) in mints.iter().zip(mint_amounts) {
|
||||
let wallet = mints_amounts[*mint].0.clone();
|
||||
|
||||
let wallet = multi_mint_wallet
|
||||
.get_wallet(&WalletKey::new(wallet, unit.clone()))
|
||||
.await
|
||||
.expect("Known wallet");
|
||||
let options = MeltOptions::new_mpp(amount * 1000);
|
||||
|
||||
let bolt11_clone = bolt11.clone();
|
||||
|
||||
quotes.spawn(async move {
|
||||
let quote = wallet
|
||||
.melt_quote(bolt11_clone.to_string(), Some(options))
|
||||
.await;
|
||||
|
||||
(wallet, quote)
|
||||
});
|
||||
}
|
||||
|
||||
let quotes = quotes.join_all().await;
|
||||
|
||||
for (wallet, quote) in quotes.iter() {
|
||||
if let Err(quote) = quote {
|
||||
tracing::error!("Could not get quote for {}: {:?}", wallet.mint_url, quote);
|
||||
bail!("Could not get melt quote for {}", wallet.mint_url);
|
||||
} else {
|
||||
let quote = quote.as_ref().unwrap();
|
||||
println!(
|
||||
"Melt quote {} for mint {} of amount {} with fee {}.",
|
||||
quote.id, wallet.mint_url, quote.amount, quote.fee_reserve
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut melts = JoinSet::new();
|
||||
|
||||
for (wallet, quote) in quotes {
|
||||
let quote = quote.expect("Errors checked above");
|
||||
|
||||
melts.spawn(async move {
|
||||
let melt = wallet.melt("e.id).await;
|
||||
(wallet, melt)
|
||||
});
|
||||
}
|
||||
|
||||
let melts = melts.join_all().await;
|
||||
|
||||
let mut error = false;
|
||||
|
||||
for (wallet, melt) in melts {
|
||||
match melt {
|
||||
Ok(melt) => {
|
||||
println!(
|
||||
"Melt for {} paid {} with fee of {} ",
|
||||
wallet.mint_url, melt.amount, melt.fee_paid
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Melt for {} failed with {}", wallet.mint_url, err);
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if error {
|
||||
bail!("Could not complete all melts");
|
||||
}
|
||||
// Execute all melts
|
||||
execute_mpp_melts(quotes).await?;
|
||||
} else {
|
||||
// Get wallet either by mint URL or by index
|
||||
let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
|
||||
@@ -174,47 +146,207 @@ pub async fn pay(
|
||||
|
||||
let available_funds = <cdk::Amount as Into<u64>>::into(mint_amount) * MSAT_IN_SAT;
|
||||
|
||||
let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
|
||||
// Process payment based on payment method
|
||||
match sub_command_args.method {
|
||||
PaymentType::Bolt11 => {
|
||||
// Process BOLT11 payment
|
||||
let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice")?)?;
|
||||
|
||||
// Determine payment amount and options
|
||||
let options = if bolt11.amount_milli_satoshis().is_none() {
|
||||
// Get user input for amount
|
||||
let prompt = format!(
|
||||
"Enter the amount you would like to pay in sats for a {} payment.",
|
||||
if sub_command_args.mpp {
|
||||
"MPP"
|
||||
} else {
|
||||
"amountless invoice"
|
||||
}
|
||||
);
|
||||
// Determine payment amount and options
|
||||
let prompt =
|
||||
"Enter the amount you would like to pay in sats for this amountless invoice.";
|
||||
let options =
|
||||
create_melt_options(available_funds, bolt11.amount_milli_satoshis(), prompt)?;
|
||||
|
||||
let user_amount = get_number_input::<u64>(&prompt)? * MSAT_IN_SAT;
|
||||
|
||||
if user_amount > available_funds {
|
||||
bail!("Not enough funds");
|
||||
// Process payment
|
||||
let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
|
||||
process_payment(&wallet, quote).await?;
|
||||
}
|
||||
PaymentType::Bolt12 => {
|
||||
// Process BOLT12 payment (offer)
|
||||
let offer_str = get_user_input("Enter BOLT12 offer")?;
|
||||
let offer = Offer::from_str(&offer_str)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid BOLT12 offer: {:?}", e))?;
|
||||
|
||||
Some(MeltOptions::new_amountless(user_amount))
|
||||
} else {
|
||||
// Check if invoice amount exceeds available funds
|
||||
let invoice_amount = bolt11.amount_milli_satoshis().unwrap();
|
||||
if invoice_amount > available_funds {
|
||||
bail!("Not enough funds");
|
||||
// Determine if offer has an amount
|
||||
let prompt =
|
||||
"Enter the amount you would like to pay in sats for this amountless offer:";
|
||||
let amount_msat = match amount_for_offer(&offer, &CurrencyUnit::Msat) {
|
||||
Ok(amount) => Some(u64::from(amount)),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
let options = create_melt_options(available_funds, amount_msat, prompt)?;
|
||||
|
||||
// Get melt quote for BOLT12
|
||||
let quote = wallet.melt_bolt12_quote(offer_str, options).await?;
|
||||
process_payment(&wallet, quote).await?;
|
||||
}
|
||||
None
|
||||
};
|
||||
PaymentType::Bip353 => {
|
||||
let bip353_addr = get_user_input("Enter Bip353 address.")?;
|
||||
let bip353_addr = Bip353Address::from_str(&bip353_addr)?;
|
||||
|
||||
// Process payment
|
||||
let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
|
||||
println!("{quote:?}");
|
||||
let payment_instructions = bip353_addr.resolve().await?;
|
||||
|
||||
let melt = wallet.melt("e.id).await?;
|
||||
println!("Paid invoice: {}", melt.state);
|
||||
let offer = payment_instructions
|
||||
.parameters
|
||||
.get(&Bip353PaymentType::LightningOffer)
|
||||
.ok_or(anyhow!("Offer not defined"))?;
|
||||
|
||||
if let Some(preimage) = melt.preimage {
|
||||
println!("Payment preimage: {preimage}");
|
||||
let prompt =
|
||||
"Enter the amount you would like to pay in sats for this amountless offer:";
|
||||
// BIP353 payments are always amountless for now
|
||||
let options = create_melt_options(available_funds, None, prompt)?;
|
||||
|
||||
// Get melt quote for BOLT12
|
||||
let quote = wallet.melt_bolt12_quote(offer.to_string(), options).await?;
|
||||
process_payment(&wallet, quote).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Collect mint numbers and amounts for MPP payments
|
||||
fn collect_mpp_inputs(
|
||||
mints_amounts: &[(MintUrl, Amount)],
|
||||
mint_url_opt: &Option<String>,
|
||||
) -> Result<(Vec<usize>, Vec<u64>)> {
|
||||
let mut mints = Vec::new();
|
||||
let mut mint_amounts = Vec::new();
|
||||
|
||||
// If a specific mint URL was provided, try to use it as the first mint
|
||||
if let Some(mint_url) = mint_url_opt {
|
||||
println!("Using mint URL {mint_url} as the first mint for MPP payment.");
|
||||
|
||||
// Find the index of this mint in the mints_amounts list
|
||||
if let Some(mint_index) = mints_amounts
|
||||
.iter()
|
||||
.position(|(url, _)| url.to_string() == *mint_url)
|
||||
{
|
||||
mints.push(mint_index);
|
||||
let melt_amount: u64 =
|
||||
get_number_input("Enter amount to mint from this mint in sats.")?;
|
||||
mint_amounts.push(melt_amount);
|
||||
} else {
|
||||
println!(
|
||||
"Warning: Mint URL not found or no balance. Continuing with manual selection."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with regular mint selection
|
||||
loop {
|
||||
let mint_number: String =
|
||||
get_user_input("Enter mint number to melt from and -1 when done.")?;
|
||||
|
||||
if mint_number == "-1" || mint_number.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mint_number: usize = mint_number.parse()?;
|
||||
validate_mint_number(mint_number, mints_amounts.len())?;
|
||||
|
||||
mints.push(mint_number);
|
||||
let melt_amount: u64 = get_number_input("Enter amount to mint from this mint in sats.")?;
|
||||
mint_amounts.push(melt_amount);
|
||||
}
|
||||
|
||||
if mints.is_empty() {
|
||||
bail!("No mints selected for MPP payment");
|
||||
}
|
||||
|
||||
Ok((mints, mint_amounts))
|
||||
}
|
||||
|
||||
/// Get quotes from all mints for MPP payment
|
||||
async fn get_mpp_quotes(
|
||||
multi_mint_wallet: &MultiMintWallet,
|
||||
mints_amounts: &[(MintUrl, Amount)],
|
||||
mints: &[usize],
|
||||
mint_amounts: &[u64],
|
||||
unit: &CurrencyUnit,
|
||||
bolt11: &Bolt11Invoice,
|
||||
) -> Result<Vec<(Wallet, MeltQuote)>> {
|
||||
let mut quotes = JoinSet::new();
|
||||
|
||||
for (mint, amount) in mints.iter().zip(mint_amounts) {
|
||||
let wallet = mints_amounts[*mint].0.clone();
|
||||
|
||||
let wallet = multi_mint_wallet
|
||||
.get_wallet(&WalletKey::new(wallet, unit.clone()))
|
||||
.await
|
||||
.expect("Known wallet");
|
||||
let options = MeltOptions::new_mpp(*amount * 1000);
|
||||
|
||||
let bolt11_clone = bolt11.clone();
|
||||
|
||||
quotes.spawn(async move {
|
||||
let quote = wallet
|
||||
.melt_quote(bolt11_clone.to_string(), Some(options))
|
||||
.await;
|
||||
|
||||
(wallet, quote)
|
||||
});
|
||||
}
|
||||
|
||||
let quotes_results = quotes.join_all().await;
|
||||
|
||||
// Validate all quotes succeeded
|
||||
let mut valid_quotes = Vec::new();
|
||||
for (wallet, quote_result) in quotes_results {
|
||||
match quote_result {
|
||||
Ok(quote) => {
|
||||
println!(
|
||||
"Melt quote {} for mint {} of amount {} with fee {}.",
|
||||
quote.id, wallet.mint_url, quote.amount, quote.fee_reserve
|
||||
);
|
||||
valid_quotes.push((wallet, quote));
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Could not get quote for {}: {:?}", wallet.mint_url, err);
|
||||
bail!("Could not get melt quote for {}", wallet.mint_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(valid_quotes)
|
||||
}
|
||||
|
||||
/// Execute all melts for MPP payment
|
||||
async fn execute_mpp_melts(quotes: Vec<(Wallet, MeltQuote)>) -> Result<()> {
|
||||
let mut melts = JoinSet::new();
|
||||
|
||||
for (wallet, quote) in quotes {
|
||||
melts.spawn(async move {
|
||||
let melt = wallet.melt("e.id).await;
|
||||
(wallet, melt)
|
||||
});
|
||||
}
|
||||
|
||||
let melts = melts.join_all().await;
|
||||
|
||||
let mut error = false;
|
||||
|
||||
for (wallet, melt) in melts {
|
||||
match melt {
|
||||
Ok(melt) => {
|
||||
println!(
|
||||
"Melt for {} paid {} with fee of {} ",
|
||||
wallet.mint_url, melt.amount, melt.fee_paid
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Melt for {} failed with {}", wallet.mint_url, err);
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if error {
|
||||
bail!("Could not complete all melts");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::{anyhow, Result};
|
||||
use cdk::amount::SplitTarget;
|
||||
use cdk::mint_url::MintUrl;
|
||||
use cdk::nuts::nut00::ProofsMethods;
|
||||
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload};
|
||||
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload, PaymentMethod};
|
||||
use cdk::wallet::{MultiMintWallet, WalletSubscription};
|
||||
use cdk::Amount;
|
||||
use clap::Args;
|
||||
@@ -27,6 +27,15 @@ pub struct MintSubCommand {
|
||||
/// Quote Id
|
||||
#[arg(short, long)]
|
||||
quote_id: Option<String>,
|
||||
/// Payment method
|
||||
#[arg(long, default_value = "bolt11")]
|
||||
method: String,
|
||||
/// Expiry
|
||||
#[arg(short, long)]
|
||||
expiry: Option<u64>,
|
||||
/// Expiry
|
||||
#[arg(short, long)]
|
||||
single_use: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn mint(
|
||||
@@ -39,36 +48,104 @@ pub async fn mint(
|
||||
|
||||
let wallet = get_or_create_wallet(multi_mint_wallet, &mint_url, unit).await?;
|
||||
|
||||
let mut payment_method = PaymentMethod::from_str(&sub_command_args.method)?;
|
||||
|
||||
let quote_id = match &sub_command_args.quote_id {
|
||||
None => {
|
||||
let amount = sub_command_args
|
||||
.amount
|
||||
.ok_or(anyhow!("Amount must be defined"))?;
|
||||
let quote = wallet.mint_quote(Amount::from(amount), description).await?;
|
||||
None => match payment_method {
|
||||
PaymentMethod::Bolt11 => {
|
||||
let amount = sub_command_args
|
||||
.amount
|
||||
.ok_or(anyhow!("Amount must be defined"))?;
|
||||
let quote = wallet.mint_quote(Amount::from(amount), description).await?;
|
||||
|
||||
println!("Quote: {quote:#?}");
|
||||
println!("Quote: {quote:#?}");
|
||||
|
||||
println!("Please pay: {}", quote.request);
|
||||
println!("Please pay: {}", quote.request);
|
||||
|
||||
let mut subscription = wallet
|
||||
.subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote
|
||||
.id
|
||||
.clone()]))
|
||||
.await;
|
||||
let mut subscription = wallet
|
||||
.subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote
|
||||
.id
|
||||
.clone()]))
|
||||
.await;
|
||||
|
||||
while let Some(msg) = subscription.recv().await {
|
||||
if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
|
||||
if response.state == MintQuoteState::Paid {
|
||||
break;
|
||||
while let Some(msg) = subscription.recv().await {
|
||||
if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
|
||||
if response.state == MintQuoteState::Paid {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
quote.id
|
||||
}
|
||||
quote.id
|
||||
PaymentMethod::Bolt12 => {
|
||||
let amount = sub_command_args.amount;
|
||||
println!("{:?}", sub_command_args.single_use);
|
||||
let quote = wallet
|
||||
.mint_bolt12_quote(amount.map(|a| a.into()), description)
|
||||
.await?;
|
||||
|
||||
println!("Quote: {quote:#?}");
|
||||
|
||||
println!("Please pay: {}", quote.request);
|
||||
|
||||
let mut subscription = wallet
|
||||
.subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote
|
||||
.id
|
||||
.clone()]))
|
||||
.await;
|
||||
|
||||
while let Some(msg) = subscription.recv().await {
|
||||
if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
|
||||
if response.state == MintQuoteState::Paid {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
quote.id
|
||||
}
|
||||
_ => {
|
||||
todo!()
|
||||
}
|
||||
},
|
||||
Some(quote_id) => {
|
||||
let quote = wallet
|
||||
.localstore
|
||||
.get_mint_quote(quote_id)
|
||||
.await?
|
||||
.ok_or(anyhow!("Unknown quote"))?;
|
||||
|
||||
payment_method = quote.payment_method;
|
||||
quote_id.to_string()
|
||||
}
|
||||
Some(quote_id) => quote_id.to_string(),
|
||||
};
|
||||
|
||||
let proofs = wallet.mint("e_id, SplitTarget::default(), None).await?;
|
||||
tracing::debug!("Attempting mint for: {}", payment_method);
|
||||
|
||||
let proofs = match payment_method {
|
||||
PaymentMethod::Bolt11 => wallet.mint("e_id, SplitTarget::default(), None).await?,
|
||||
PaymentMethod::Bolt12 => {
|
||||
let response = wallet.mint_bolt12_quote_state("e_id).await?;
|
||||
|
||||
let amount_mintable = response.amount_paid - response.amount_issued;
|
||||
|
||||
if amount_mintable == Amount::ZERO {
|
||||
println!("Mint quote does not have amount that can be minted.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
wallet
|
||||
.mint_bolt12(
|
||||
"e_id,
|
||||
Some(amount_mintable),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
_ => {
|
||||
todo!()
|
||||
}
|
||||
};
|
||||
|
||||
let receive_amount = proofs.total_amount()?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user