Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NUT-15] LND Support for MPP Payments #536

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions crates/cdk-cln/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,14 @@ impl MintLightning for Cln {
}
}

let amount_msat = melt_quote
.msat_to_pay
.map(|a| CLN_Amount::from_msat(a.into()));
let amount_msat = partial_amount
.is_none()
.then(|| {
melt_quote
.msat_to_pay
.map(|a| CLN_Amount::from_msat(a.into()))
})
.flatten();

let mut cln_client = self.cln_client.lock().await;
let cln_response = cln_client
Expand Down
1 change: 1 addition & 0 deletions crates/cdk-integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ axum = "0.6.20"
rand = "0.8.5"
bip39 = { version = "2.0", features = ["rand"] }
anyhow = "1"
cashu = { path = "../cashu", features = ["mint", "wallet"] }
cdk = { path = "../cdk", features = ["mint", "wallet"] }
cdk-cln = { path = "../cdk-cln" }
cdk-lnd = { path = "../cdk-lnd" }
Expand Down
12 changes: 6 additions & 6 deletions crates/cdk-integration-tests/src/init_regtest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ pub fn get_mint_addr() -> String {
env::var("cdk_itests_mint_addr").expect("Temp dir set")
}

pub fn get_mint_port() -> u16 {
let dir = env::var("cdk_itests_mint_port").expect("Temp dir set");
pub fn get_mint_port(which: &str) -> u16 {
let dir = env::var(format!("cdk_itests_mint_port_{}", which)).expect("Temp dir set");
dir.parse().unwrap()
}

pub fn get_mint_url() -> String {
format!("http://{}:{}", get_mint_addr(), get_mint_port())
pub fn get_mint_url(which: &str) -> String {
format!("http://{}:{}", get_mint_addr(), get_mint_port(which))
}

pub fn get_mint_ws_url() -> String {
format!("ws://{}:{}/v1/ws", get_mint_addr(), get_mint_port())
pub fn get_mint_ws_url(which: &str) -> String {
format!("ws://{}:{}/v1/ws", get_mint_addr(), get_mint_port(which))
}

pub fn get_temp_dir() -> PathBuf {
Expand Down
106 changes: 94 additions & 12 deletions crates/cdk-integration-tests/tests/regtest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::time::Duration;

use anyhow::{bail, Result};
use bip39::Mnemonic;
use cashu::{MeltOptions, Mpp};
use cdk::amount::{Amount, SplitTarget};
use cdk::cdk_database::WalletMemoryDatabase;
use cdk::nuts::nut00::ProofsMethods;
Expand All @@ -19,7 +20,7 @@ use cdk_integration_tests::init_regtest::{
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 futures::{SinkExt, StreamExt};
use futures::{join, SinkExt, StreamExt};
use lightning_invoice::Bolt11Invoice;
use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient};
use ln_regtest_rs::InvoiceStatus;
Expand Down Expand Up @@ -79,14 +80,14 @@ async fn test_regtest_mint_melt_round_trip() -> Result<()> {
let lnd_client = init_lnd_client().await;

let wallet = Wallet::new(
&get_mint_url(),
&get_mint_url("0"),
CurrencyUnit::Sat,
Arc::new(WalletMemoryDatabase::default()),
&Mnemonic::generate(12)?.to_seed_normalized(""),
None,
)?;

let (ws_stream, _) = connect_async(get_mint_ws_url())
let (ws_stream, _) = connect_async(get_mint_ws_url("0"))
.await
.expect("Failed to connect");
let (mut write, mut reader) = ws_stream.split();
Expand Down Expand Up @@ -164,7 +165,7 @@ async fn test_regtest_mint_melt() -> Result<()> {
let lnd_client = init_lnd_client().await;

let wallet = Wallet::new(
&get_mint_url(),
&get_mint_url("0"),
CurrencyUnit::Sat,
Arc::new(WalletMemoryDatabase::default()),
&Mnemonic::generate(12)?.to_seed_normalized(""),
Expand Down Expand Up @@ -198,7 +199,7 @@ async fn test_restore() -> Result<()> {

let seed = Mnemonic::generate(12)?.to_seed_normalized("");
let wallet = Wallet::new(
&get_mint_url(),
&get_mint_url("0"),
CurrencyUnit::Sat,
Arc::new(WalletMemoryDatabase::default()),
&seed,
Expand All @@ -218,7 +219,7 @@ async fn test_restore() -> Result<()> {
assert!(wallet.total_balance().await? == 100.into());

let wallet_2 = Wallet::new(
&get_mint_url(),
&get_mint_url("0"),
CurrencyUnit::Sat,
Arc::new(WalletMemoryDatabase::default()),
&seed,
Expand Down Expand Up @@ -257,7 +258,7 @@ async fn test_pay_invoice_twice() -> Result<()> {

let seed = Mnemonic::generate(12)?.to_seed_normalized("");
let wallet = Wallet::new(
&get_mint_url(),
&get_mint_url("0"),
CurrencyUnit::Sat,
Arc::new(WalletMemoryDatabase::default()),
&seed,
Expand Down Expand Up @@ -316,7 +317,7 @@ async fn test_internal_payment() -> Result<()> {

let seed = Mnemonic::generate(12)?.to_seed_normalized("");
let wallet = Wallet::new(
&get_mint_url(),
&get_mint_url("0"),
CurrencyUnit::Sat,
Arc::new(WalletMemoryDatabase::default()),
&seed,
Expand All @@ -338,7 +339,7 @@ async fn test_internal_payment() -> Result<()> {
let seed = Mnemonic::generate(12)?.to_seed_normalized("");

let wallet_2 = Wallet::new(
&get_mint_url(),
&get_mint_url("0"),
CurrencyUnit::Sat,
Arc::new(WalletMemoryDatabase::default()),
&seed,
Expand All @@ -360,7 +361,7 @@ async fn test_internal_payment() -> Result<()> {
.await
.unwrap();

let check_paid = match get_mint_port() {
let check_paid = match get_mint_port("0") {
8085 => {
let cln_one_dir = get_cln_dir("one");
let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
Expand Down Expand Up @@ -411,7 +412,7 @@ async fn test_cached_mint() -> Result<()> {
let lnd_client = init_lnd_client().await;

let wallet = Wallet::new(
&get_mint_url(),
&get_mint_url("0"),
CurrencyUnit::Sat,
Arc::new(WalletMemoryDatabase::default()),
&Mnemonic::generate(12)?.to_seed_normalized(""),
Expand All @@ -438,7 +439,7 @@ async fn test_cached_mint() -> Result<()> {
}

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 =
PreMintSecrets::random(active_keyset_id, 31.into(), &SplitTarget::default()).unwrap();

Expand All @@ -458,3 +459,84 @@ async fn test_cached_mint() -> Result<()> {
assert!(response == response1);
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(&quote.id).await?;
if quote_status.state == MintQuoteState::Paid {
break;
}
tracing::debug!("Quote not yet paid");
}
wallet1
.mint(&quote.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(&quote.id).await?;
if quote_status.state == MintQuoteState::Paid {
break;
}
tracing::debug!("Quote not yet paid");
}
wallet2
.mint(&quote.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(25),
},
};
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(&quote_1.id);
let result2 = wallet2.melt(&quote_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(())
}
7 changes: 7 additions & 0 deletions crates/cdk-lnd/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! LND Errors

use fedimint_tonic_lnd::tonic::Status;
use thiserror::Error;

/// LND Error
Expand All @@ -23,6 +24,12 @@ pub enum Error {
/// Unknown payment status
#[error("LND unknown payment status")]
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 {
Expand Down
Loading
Loading