mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-23 07:35:03 +01:00
[NUT-15] LND Support for MPP Payments (#536)
--------- Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
@@ -241,9 +241,14 @@ impl MintLightning for Cln {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let amount_msat = melt_quote
|
let amount_msat = partial_amount
|
||||||
|
.is_none()
|
||||||
|
.then(|| {
|
||||||
|
melt_quote
|
||||||
.msat_to_pay
|
.msat_to_pay
|
||||||
.map(|a| CLN_Amount::from_msat(a.into()));
|
.map(|a| CLN_Amount::from_msat(a.into()))
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
|
||||||
let mut cln_client = self.cln_client.lock().await;
|
let mut cln_client = self.cln_client.lock().await;
|
||||||
let cln_response = cln_client
|
let cln_response = cln_client
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ axum = "0.6.20"
|
|||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
bip39 = { version = "2.0", features = ["rand"] }
|
bip39 = { version = "2.0", features = ["rand"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
cashu = { path = "../cashu", features = ["mint", "wallet"] }
|
||||||
cdk = { path = "../cdk", features = ["mint", "wallet"] }
|
cdk = { path = "../cdk", features = ["mint", "wallet"] }
|
||||||
cdk-cln = { path = "../cdk-cln" }
|
cdk-cln = { path = "../cdk-cln" }
|
||||||
cdk-lnd = { path = "../cdk-lnd" }
|
cdk-lnd = { path = "../cdk-lnd" }
|
||||||
|
|||||||
@@ -36,17 +36,17 @@ pub fn get_mint_addr() -> String {
|
|||||||
env::var("cdk_itests_mint_addr").expect("Temp dir set")
|
env::var("cdk_itests_mint_addr").expect("Temp dir set")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mint_port() -> u16 {
|
pub fn get_mint_port(which: &str) -> u16 {
|
||||||
let dir = env::var("cdk_itests_mint_port").expect("Temp dir set");
|
let dir = env::var(format!("cdk_itests_mint_port_{}", which)).expect("Temp dir set");
|
||||||
dir.parse().unwrap()
|
dir.parse().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mint_url() -> String {
|
pub fn get_mint_url(which: &str) -> String {
|
||||||
format!("http://{}:{}", get_mint_addr(), get_mint_port())
|
format!("http://{}:{}", get_mint_addr(), get_mint_port(which))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mint_ws_url() -> String {
|
pub fn get_mint_ws_url(which: &str) -> String {
|
||||||
format!("ws://{}:{}/v1/ws", get_mint_addr(), get_mint_port())
|
format!("ws://{}:{}/v1/ws", get_mint_addr(), get_mint_port(which))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_temp_dir() -> PathBuf {
|
pub fn get_temp_dir() -> PathBuf {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use bip39::Mnemonic;
|
use bip39::Mnemonic;
|
||||||
|
use cashu::{MeltOptions, Mpp};
|
||||||
use cdk::amount::{Amount, SplitTarget};
|
use cdk::amount::{Amount, SplitTarget};
|
||||||
use cdk::cdk_database::WalletMemoryDatabase;
|
use cdk::cdk_database::WalletMemoryDatabase;
|
||||||
use cdk::nuts::nut00::ProofsMethods;
|
use cdk::nuts::nut00::ProofsMethods;
|
||||||
@@ -19,7 +20,7 @@ use cdk_integration_tests::init_regtest::{
|
|||||||
get_mint_url, get_mint_ws_url, LND_RPC_ADDR, LND_TWO_RPC_ADDR,
|
get_mint_url, get_mint_ws_url, LND_RPC_ADDR, LND_TWO_RPC_ADDR,
|
||||||
};
|
};
|
||||||
use cdk_integration_tests::wait_for_mint_to_be_paid;
|
use cdk_integration_tests::wait_for_mint_to_be_paid;
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{join, SinkExt, StreamExt};
|
||||||
use lightning_invoice::Bolt11Invoice;
|
use lightning_invoice::Bolt11Invoice;
|
||||||
use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient};
|
use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient};
|
||||||
use ln_regtest_rs::InvoiceStatus;
|
use ln_regtest_rs::InvoiceStatus;
|
||||||
@@ -79,14 +80,14 @@ async fn test_regtest_mint_melt_round_trip() -> Result<()> {
|
|||||||
let lnd_client = init_lnd_client().await;
|
let lnd_client = init_lnd_client().await;
|
||||||
|
|
||||||
let wallet = Wallet::new(
|
let wallet = Wallet::new(
|
||||||
&get_mint_url(),
|
&get_mint_url("0"),
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
Arc::new(WalletMemoryDatabase::default()),
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let (ws_stream, _) = connect_async(get_mint_ws_url())
|
let (ws_stream, _) = connect_async(get_mint_ws_url("0"))
|
||||||
.await
|
.await
|
||||||
.expect("Failed to connect");
|
.expect("Failed to connect");
|
||||||
let (mut write, mut reader) = ws_stream.split();
|
let (mut write, mut reader) = ws_stream.split();
|
||||||
@@ -164,7 +165,7 @@ async fn test_regtest_mint_melt() -> Result<()> {
|
|||||||
let lnd_client = init_lnd_client().await;
|
let lnd_client = init_lnd_client().await;
|
||||||
|
|
||||||
let wallet = Wallet::new(
|
let wallet = Wallet::new(
|
||||||
&get_mint_url(),
|
&get_mint_url("0"),
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
Arc::new(WalletMemoryDatabase::default()),
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
@@ -198,7 +199,7 @@ async fn test_restore() -> Result<()> {
|
|||||||
|
|
||||||
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
||||||
let wallet = Wallet::new(
|
let wallet = Wallet::new(
|
||||||
&get_mint_url(),
|
&get_mint_url("0"),
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
Arc::new(WalletMemoryDatabase::default()),
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
&seed,
|
&seed,
|
||||||
@@ -218,7 +219,7 @@ async fn test_restore() -> Result<()> {
|
|||||||
assert!(wallet.total_balance().await? == 100.into());
|
assert!(wallet.total_balance().await? == 100.into());
|
||||||
|
|
||||||
let wallet_2 = Wallet::new(
|
let wallet_2 = Wallet::new(
|
||||||
&get_mint_url(),
|
&get_mint_url("0"),
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
Arc::new(WalletMemoryDatabase::default()),
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
&seed,
|
&seed,
|
||||||
@@ -257,7 +258,7 @@ async fn test_pay_invoice_twice() -> Result<()> {
|
|||||||
|
|
||||||
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
||||||
let wallet = Wallet::new(
|
let wallet = Wallet::new(
|
||||||
&get_mint_url(),
|
&get_mint_url("0"),
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
Arc::new(WalletMemoryDatabase::default()),
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
&seed,
|
&seed,
|
||||||
@@ -316,7 +317,7 @@ async fn test_internal_payment() -> Result<()> {
|
|||||||
|
|
||||||
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
||||||
let wallet = Wallet::new(
|
let wallet = Wallet::new(
|
||||||
&get_mint_url(),
|
&get_mint_url("0"),
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
Arc::new(WalletMemoryDatabase::default()),
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
&seed,
|
&seed,
|
||||||
@@ -338,7 +339,7 @@ async fn test_internal_payment() -> Result<()> {
|
|||||||
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
||||||
|
|
||||||
let wallet_2 = Wallet::new(
|
let wallet_2 = Wallet::new(
|
||||||
&get_mint_url(),
|
&get_mint_url("0"),
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
Arc::new(WalletMemoryDatabase::default()),
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
&seed,
|
&seed,
|
||||||
@@ -360,7 +361,7 @@ async fn test_internal_payment() -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let check_paid = match get_mint_port() {
|
let check_paid = match get_mint_port("0") {
|
||||||
8085 => {
|
8085 => {
|
||||||
let cln_one_dir = get_cln_dir("one");
|
let cln_one_dir = get_cln_dir("one");
|
||||||
let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
|
let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
|
||||||
@@ -411,7 +412,7 @@ async fn test_cached_mint() -> Result<()> {
|
|||||||
let lnd_client = init_lnd_client().await;
|
let lnd_client = init_lnd_client().await;
|
||||||
|
|
||||||
let wallet = Wallet::new(
|
let wallet = Wallet::new(
|
||||||
&get_mint_url(),
|
&get_mint_url("0"),
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
Arc::new(WalletMemoryDatabase::default()),
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
@@ -438,7 +439,7 @@ async fn test_cached_mint() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
|
let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
|
||||||
let http_client = HttpClient::new(get_mint_url().as_str().parse()?);
|
let http_client = HttpClient::new(get_mint_url("0").as_str().parse()?);
|
||||||
let premint_secrets =
|
let premint_secrets =
|
||||||
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
|
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
|
||||||
|
|
||||||
@@ -458,3 +459,84 @@ async fn test_cached_mint() -> Result<()> {
|
|||||||
assert!(response == response1);
|
assert!(response == response1);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_multimint_melt() -> Result<()> {
|
||||||
|
let lnd_client = init_lnd_client().await;
|
||||||
|
|
||||||
|
let wallet1 = Wallet::new(
|
||||||
|
&get_mint_url("0"),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
let wallet2 = Wallet::new(
|
||||||
|
&get_mint_url("1"),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mint_amount = Amount::from(100);
|
||||||
|
|
||||||
|
// Fund the wallets
|
||||||
|
let quote = wallet1.mint_quote(mint_amount, None).await?;
|
||||||
|
lnd_client.pay_invoice(quote.request.clone()).await?;
|
||||||
|
loop {
|
||||||
|
let quote_status = wallet1.mint_quote_state("e.id).await?;
|
||||||
|
if quote_status.state == MintQuoteState::Paid {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tracing::debug!("Quote not yet paid");
|
||||||
|
}
|
||||||
|
wallet1
|
||||||
|
.mint("e.id, SplitTarget::default(), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let quote = wallet2.mint_quote(mint_amount, None).await?;
|
||||||
|
lnd_client.pay_invoice(quote.request.clone()).await?;
|
||||||
|
loop {
|
||||||
|
let quote_status = wallet2.mint_quote_state("e.id).await?;
|
||||||
|
if quote_status.state == MintQuoteState::Paid {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tracing::debug!("Quote not yet paid");
|
||||||
|
}
|
||||||
|
wallet2
|
||||||
|
.mint("e.id, SplitTarget::default(), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Get an invoice
|
||||||
|
let invoice = lnd_client.create_invoice(Some(50)).await?;
|
||||||
|
|
||||||
|
// Get multi-part melt quotes
|
||||||
|
let melt_options = MeltOptions::Mpp {
|
||||||
|
mpp: Mpp {
|
||||||
|
amount: Amount::from(25000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let quote_1 = wallet1
|
||||||
|
.melt_quote(invoice.clone(), Some(melt_options))
|
||||||
|
.await
|
||||||
|
.expect("Could not get melt quote");
|
||||||
|
let quote_2 = wallet2
|
||||||
|
.melt_quote(invoice.clone(), Some(melt_options))
|
||||||
|
.await
|
||||||
|
.expect("Could not get melt quote");
|
||||||
|
|
||||||
|
// Multimint pay invoice
|
||||||
|
let result1 = wallet1.melt("e_1.id);
|
||||||
|
let result2 = wallet2.melt("e_2.id);
|
||||||
|
let result = join!(result1, result2);
|
||||||
|
|
||||||
|
// Unpack results
|
||||||
|
let result1 = result.0.unwrap();
|
||||||
|
let result2 = result.1.unwrap();
|
||||||
|
|
||||||
|
// Check
|
||||||
|
assert!(result1.state == result2.state);
|
||||||
|
assert!(result1.state == MeltQuoteState::Paid);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! LND Errors
|
//! LND Errors
|
||||||
|
|
||||||
|
use fedimint_tonic_lnd::tonic::Status;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// LND Error
|
/// LND Error
|
||||||
@@ -23,6 +24,12 @@ pub enum Error {
|
|||||||
/// Unknown payment status
|
/// Unknown payment status
|
||||||
#[error("LND unknown payment status")]
|
#[error("LND unknown payment status")]
|
||||||
UnknownPaymentStatus,
|
UnknownPaymentStatus,
|
||||||
|
/// Missing last hop in route
|
||||||
|
#[error("LND missing last hop in route")]
|
||||||
|
MissingLastHop,
|
||||||
|
/// Errors coming from the backend
|
||||||
|
#[error("LND error: `{0}`")]
|
||||||
|
LndError(Status),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Error> for cdk::cdk_lightning::Error {
|
impl From<Error> for cdk::cdk_lightning::Error {
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ use cdk::cdk_lightning::{
|
|||||||
};
|
};
|
||||||
use cdk::mint::FeeReserve;
|
use cdk::mint::FeeReserve;
|
||||||
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
|
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
|
||||||
|
use cdk::secp256k1::hashes::Hash;
|
||||||
use cdk::util::{hex, unix_time};
|
use cdk::util::{hex, unix_time};
|
||||||
use cdk::{mint, Bolt11Invoice};
|
use cdk::{mint, Bolt11Invoice};
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use fedimint_tonic_lnd::lnrpc::fee_limit::Limit;
|
use fedimint_tonic_lnd::lnrpc::fee_limit::Limit;
|
||||||
use fedimint_tonic_lnd::lnrpc::payment::PaymentStatus;
|
use fedimint_tonic_lnd::lnrpc::payment::PaymentStatus;
|
||||||
use fedimint_tonic_lnd::lnrpc::FeeLimit;
|
use fedimint_tonic_lnd::lnrpc::{FeeLimit, Hop, HtlcAttempt, MppRecord};
|
||||||
use fedimint_tonic_lnd::tonic::Code;
|
use fedimint_tonic_lnd::tonic::Code;
|
||||||
use fedimint_tonic_lnd::Client;
|
use fedimint_tonic_lnd::Client;
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
@@ -80,7 +81,7 @@ impl MintLightning for Lnd {
|
|||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
fn get_settings(&self) -> Settings {
|
fn get_settings(&self) -> Settings {
|
||||||
Settings {
|
Settings {
|
||||||
mpp: false,
|
mpp: true,
|
||||||
unit: CurrencyUnit::Msat,
|
unit: CurrencyUnit::Msat,
|
||||||
invoice_description: true,
|
invoice_description: true,
|
||||||
}
|
}
|
||||||
@@ -200,7 +201,7 @@ impl MintLightning for Lnd {
|
|||||||
async fn pay_invoice(
|
async fn pay_invoice(
|
||||||
&self,
|
&self,
|
||||||
melt_quote: mint::MeltQuote,
|
melt_quote: mint::MeltQuote,
|
||||||
_partial_amount: Option<Amount>,
|
partial_amount: Option<Amount>,
|
||||||
max_fee: Option<Amount>,
|
max_fee: Option<Amount>,
|
||||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||||
let payment_request = melt_quote.request;
|
let payment_request = melt_quote.request;
|
||||||
@@ -222,16 +223,113 @@ impl MintLightning for Lnd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let amount_msat: u64 = match melt_quote.msat_to_pay {
|
|
||||||
Some(amount_msat) => amount_msat.into(),
|
|
||||||
None => {
|
|
||||||
let bolt11 = Bolt11Invoice::from_str(&payment_request)?;
|
let bolt11 = Bolt11Invoice::from_str(&payment_request)?;
|
||||||
bolt11
|
let amount_msat: u64 = match bolt11.amount_milli_satoshis() {
|
||||||
.amount_milli_satoshis()
|
Some(amount_msat) => amount_msat,
|
||||||
|
None => melt_quote
|
||||||
|
.msat_to_pay
|
||||||
.ok_or(Error::UnknownInvoiceAmount)?
|
.ok_or(Error::UnknownInvoiceAmount)?
|
||||||
}
|
.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Detect partial payments
|
||||||
|
match partial_amount {
|
||||||
|
Some(part_amt) => {
|
||||||
|
let partial_amount_msat = to_unit(part_amt, &melt_quote.unit, &CurrencyUnit::Msat)?;
|
||||||
|
let invoice = Bolt11Invoice::from_str(&payment_request)?;
|
||||||
|
|
||||||
|
// Extract information from invoice
|
||||||
|
let pub_key = invoice.get_payee_pub_key();
|
||||||
|
let payer_addr = invoice.payment_secret().0.to_vec();
|
||||||
|
let payment_hash = invoice.payment_hash();
|
||||||
|
|
||||||
|
// Create a request for the routes
|
||||||
|
let route_req = fedimint_tonic_lnd::lnrpc::QueryRoutesRequest {
|
||||||
|
pub_key: hex::encode(pub_key.serialize()),
|
||||||
|
amt_msat: u64::from(partial_amount_msat) as i64,
|
||||||
|
fee_limit: max_fee.map(|f| {
|
||||||
|
let limit = Limit::Fixed(u64::from(f) as i64);
|
||||||
|
FeeLimit { limit: Some(limit) }
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Query the routes
|
||||||
|
let routes_response: fedimint_tonic_lnd::lnrpc::QueryRoutesResponse = self
|
||||||
|
.client
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.lightning()
|
||||||
|
.query_routes(route_req)
|
||||||
|
.await
|
||||||
|
.map_err(Error::LndError)?
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
|
let mut payment_response: HtlcAttempt = HtlcAttempt {
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// For each route:
|
||||||
|
// update its MPP record,
|
||||||
|
// attempt it and check the result
|
||||||
|
for mut route in routes_response.routes.into_iter() {
|
||||||
|
let last_hop: &mut Hop = route.hops.last_mut().ok_or(Error::MissingLastHop)?;
|
||||||
|
let mpp_record = MppRecord {
|
||||||
|
payment_addr: payer_addr.clone(),
|
||||||
|
total_amt_msat: amount_msat as i64,
|
||||||
|
};
|
||||||
|
last_hop.mpp_record = Some(mpp_record);
|
||||||
|
tracing::debug!("sendToRouteV2 needle");
|
||||||
|
payment_response = self
|
||||||
|
.client
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.router()
|
||||||
|
.send_to_route_v2(fedimint_tonic_lnd::routerrpc::SendToRouteRequest {
|
||||||
|
payment_hash: payment_hash.to_byte_array().to_vec(),
|
||||||
|
route: Some(route),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(Error::LndError)?
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
|
if let Some(failure) = payment_response.failure {
|
||||||
|
if failure.code == 15 {
|
||||||
|
// Try a different route
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get status and maybe the preimage
|
||||||
|
let (status, payment_preimage) = match payment_response.status {
|
||||||
|
0 => (MeltQuoteState::Pending, None),
|
||||||
|
1 => (
|
||||||
|
MeltQuoteState::Paid,
|
||||||
|
Some(hex::encode(payment_response.preimage)),
|
||||||
|
),
|
||||||
|
2 => (MeltQuoteState::Unpaid, None),
|
||||||
|
_ => (MeltQuoteState::Unknown, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the actual amount paid in sats
|
||||||
|
let mut total_amt: u64 = 0;
|
||||||
|
if let Some(route) = payment_response.route {
|
||||||
|
total_amt = (route.total_amt_msat / 1000) as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PayInvoiceResponse {
|
||||||
|
payment_lookup_id: hex::encode(payment_hash),
|
||||||
|
payment_preimage,
|
||||||
|
status,
|
||||||
|
total_spent: total_amt.into(),
|
||||||
|
unit: CurrencyUnit::Sat,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => {
|
||||||
let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest {
|
let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest {
|
||||||
payment_request,
|
payment_request,
|
||||||
fee_limit: max_fee.map(|f| {
|
fee_limit: max_fee.map(|f| {
|
||||||
@@ -277,6 +375,8 @@ impl MintLightning for Lnd {
|
|||||||
unit: CurrencyUnit::Sat,
|
unit: CurrencyUnit::Sat,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, description))]
|
#[instrument(skip(self, description))]
|
||||||
async fn create_invoice(
|
async fn create_invoice(
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ impl Mint {
|
|||||||
.get_settings(&unit, &method)
|
.get_settings(&unit, &method)
|
||||||
.ok_or(Error::UnsupportedUnit)?;
|
.ok_or(Error::UnsupportedUnit)?;
|
||||||
|
|
||||||
if matches!(options, Some(MeltOptions::Mpp { mpp: _ })) {
|
let amount = match options {
|
||||||
|
Some(MeltOptions::Mpp { mpp: _ }) => {
|
||||||
// Verify there is no corresponding mint quote.
|
// Verify there is no corresponding mint quote.
|
||||||
// Otherwise a wallet is trying to pay someone internally, but
|
// Otherwise a wallet is trying to pay someone internally, but
|
||||||
// with a multi-part quote. And that's just not possible.
|
// with a multi-part quote. And that's just not possible.
|
||||||
@@ -58,15 +59,29 @@ impl Mint {
|
|||||||
{
|
{
|
||||||
return Err(Error::MppUnitMethodNotSupported(unit, method));
|
return Err(Error::MppUnitMethodNotSupported(unit, method));
|
||||||
}
|
}
|
||||||
|
// Assign `amount`
|
||||||
|
// because should have already been converted to the partial amount
|
||||||
|
amount
|
||||||
}
|
}
|
||||||
|
None => amount,
|
||||||
|
};
|
||||||
|
|
||||||
let is_above_max = matches!(settings.max_amount, Some(max) if amount > max);
|
let is_above_max = matches!(settings.max_amount, Some(max) if amount > max);
|
||||||
let is_below_min = matches!(settings.min_amount, Some(min) if amount < min);
|
let is_below_min = matches!(settings.min_amount, Some(min) if amount < min);
|
||||||
match is_above_max || is_below_min {
|
match is_above_max || is_below_min {
|
||||||
true => Err(Error::AmountOutofLimitRange(
|
true => {
|
||||||
|
tracing::error!(
|
||||||
|
"Melt amount out of range: {} is not within {} and {}",
|
||||||
|
amount,
|
||||||
|
settings.min_amount.unwrap_or_default(),
|
||||||
|
settings.max_amount.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
Err(Error::AmountOutofLimitRange(
|
||||||
settings.min_amount.unwrap_or_default(),
|
settings.min_amount.unwrap_or_default(),
|
||||||
settings.max_amount.unwrap_or_default(),
|
settings.max_amount.unwrap_or_default(),
|
||||||
amount,
|
amount,
|
||||||
)),
|
))
|
||||||
|
}
|
||||||
false => Ok(()),
|
false => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,6 +225,13 @@ impl Mint {
|
|||||||
let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat)
|
let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat)
|
||||||
.expect("Quote unit is checked above that it can convert to msat");
|
.expect("Quote unit is checked above that it can convert to msat");
|
||||||
|
|
||||||
|
let invoice_amount_msats: Amount = match invoice.amount_milli_satoshis() {
|
||||||
|
Some(amt) => amt.into(),
|
||||||
|
None => melt_quote
|
||||||
|
.msat_to_pay
|
||||||
|
.ok_or(Error::InvoiceAmountUndefined)?,
|
||||||
|
};
|
||||||
|
/*
|
||||||
let invoice_amount_msats: Amount = match melt_quote.msat_to_pay {
|
let invoice_amount_msats: Amount = match melt_quote.msat_to_pay {
|
||||||
Some(amount) => amount,
|
Some(amount) => amount,
|
||||||
None => invoice
|
None => invoice
|
||||||
@@ -217,11 +239,11 @@ impl Mint {
|
|||||||
.ok_or(Error::InvoiceAmountUndefined)?
|
.ok_or(Error::InvoiceAmountUndefined)?
|
||||||
.into(),
|
.into(),
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
let partial_amount = match invoice_amount_msats > quote_msats {
|
let partial_amount = match invoice_amount_msats > quote_msats {
|
||||||
true => {
|
true => {
|
||||||
let partial_msats = invoice_amount_msats - quote_msats;
|
let partial_msats = invoice_amount_msats - quote_msats;
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
to_unit(partial_msats, &CurrencyUnit::Msat, &melt_quote.unit)
|
to_unit(partial_msats, &CurrencyUnit::Msat, &melt_quote.unit)
|
||||||
.map_err(|_| Error::UnsupportedUnit)?,
|
.map_err(|_| Error::UnsupportedUnit)?,
|
||||||
@@ -491,6 +513,7 @@ impl Mint {
|
|||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
tracing::debug!("partial_amount: {:?}", partial_amount);
|
||||||
let ln = match self
|
let ln = match self
|
||||||
.ln
|
.ln
|
||||||
.get(&LnKey::new(quote.unit.clone(), PaymentMethod::Bolt11))
|
.get(&LnKey::new(quote.unit.clone(), PaymentMethod::Bolt11))
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ trap cleanup EXIT
|
|||||||
# Create a temporary directory
|
# Create a temporary directory
|
||||||
export cdk_itests=$(mktemp -d)
|
export cdk_itests=$(mktemp -d)
|
||||||
export cdk_itests_mint_addr="127.0.0.1";
|
export cdk_itests_mint_addr="127.0.0.1";
|
||||||
export cdk_itests_mint_port=8085;
|
export cdk_itests_mint_port_0=8085;
|
||||||
|
export cdk_itests_mint_port_1=8087;
|
||||||
|
|
||||||
URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port/v1/info"
|
URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port_0/v1/info"
|
||||||
# Check if the temporary directory was created successfully
|
# Check if the temporary directory was created successfully
|
||||||
if [[ ! -d "$cdk_itests" ]]; then
|
if [[ ! -d "$cdk_itests" ]]; then
|
||||||
echo "Failed to create temp directory"
|
echo "Failed to create temp directory"
|
||||||
@@ -90,8 +91,9 @@ cargo test -p cdk-integration-tests --test regtest
|
|||||||
# # Run cargo test with the http_subscription feature
|
# # Run cargo test with the http_subscription feature
|
||||||
cargo test -p cdk-integration-tests --test regtest --features http_subscription
|
cargo test -p cdk-integration-tests --test regtest --features http_subscription
|
||||||
|
|
||||||
# Run tests with lnd mint
|
# Switch Mints: Run tests with LND mint
|
||||||
export cdk_itests_mint_port=8087;
|
export cdk_itests_mint_port_0=8087;
|
||||||
|
export cdk_itests_mint_port_1=8085;
|
||||||
cargo test -p cdk-integration-tests --test regtest
|
cargo test -p cdk-integration-tests --test regtest
|
||||||
|
|
||||||
# Capture the exit status of cargo test
|
# Capture the exit status of cargo test
|
||||||
|
|||||||
Reference in New Issue
Block a user