mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-21 22:54:49 +01:00
feat: itests
fix: melt change promises amount
This commit is contained in:
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -54,7 +54,6 @@ jobs:
|
|||||||
-p cdk-phoenixd,
|
-p cdk-phoenixd,
|
||||||
-p cdk-strike,
|
-p cdk-strike,
|
||||||
-p cdk-lnbits,
|
-p cdk-lnbits,
|
||||||
-p cdk-integration-tests,
|
|
||||||
-p cdk-fake-wallet,
|
-p cdk-fake-wallet,
|
||||||
--bin cdk-cli,
|
--bin cdk-cli,
|
||||||
--bin cdk-mintd
|
--bin cdk-mintd
|
||||||
@@ -75,6 +74,29 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: nix develop -i -L .#stable --command cargo test ${{ matrix.build-args }}
|
run: nix develop -i -L .#stable --command cargo test ${{ matrix.build-args }}
|
||||||
|
|
||||||
|
itest:
|
||||||
|
name: "Integration tests"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
build-args:
|
||||||
|
[
|
||||||
|
-p cdk-integration-tests,
|
||||||
|
]
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@v11
|
||||||
|
- name: Nix Cache
|
||||||
|
uses: DeterminateSystems/magic-nix-cache-action@v6
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
- name: Clippy
|
||||||
|
run: nix develop -i -L .#stable --command cargo clippy ${{ matrix.build-args }} -- -D warnings
|
||||||
|
- name: Test
|
||||||
|
run: nix develop -i -L .#stable --command just itest
|
||||||
|
|
||||||
msrv-build:
|
msrv-build:
|
||||||
name: "MSRV build"
|
name: "MSRV build"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -63,20 +63,16 @@ pub async fn get_mint_bolt11_quote(
|
|||||||
into_response(Error::UnitUnsupported)
|
into_response(Error::UnitUnsupported)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let amount =
|
|
||||||
to_unit(payload.amount, &payload.unit, &ln.get_settings().unit).map_err(|err| {
|
|
||||||
tracing::error!("Backend does not support unit: {}", err);
|
|
||||||
into_response(Error::UnitUnsupported)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let quote_expiry = unix_time() + state.quote_ttl;
|
let quote_expiry = unix_time() + state.quote_ttl;
|
||||||
|
|
||||||
if payload.description.is_some() && !ln.get_settings().invoice_description {
|
if payload.description.is_some() && !ln.get_settings().invoice_description {
|
||||||
tracing::error!("Backend does not support invoice description");
|
tracing::error!("Backend does not support invoice description");
|
||||||
return Err(into_response(Error::InvoiceDescriptionUnsupported));
|
return Err(into_response(Error::InvoiceDescriptionUnsupported));
|
||||||
}
|
}
|
||||||
|
|
||||||
let create_invoice_response = ln
|
let create_invoice_response = ln
|
||||||
.create_invoice(
|
.create_invoice(
|
||||||
amount,
|
payload.amount,
|
||||||
&payload.unit,
|
&payload.unit,
|
||||||
payload.description.unwrap_or("".to_string()),
|
payload.description.unwrap_or("".to_string()),
|
||||||
quote_expiry,
|
quote_expiry,
|
||||||
@@ -391,8 +387,15 @@ pub async fn post_melt_bolt11(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert from unit of backend to quote unit
|
// Convert from unit of backend to quote unit
|
||||||
let amount_spent = to_unit(pre.total_spent, &pre.unit, "e.unit)
|
let amount_spent = to_unit(pre.total_spent, &pre.unit, "e.unit).map_err(|_| {
|
||||||
.map_err(|_| into_response(Error::UnitUnsupported))?;
|
tracing::error!(
|
||||||
|
"Could not convert from {} to {} in melt.",
|
||||||
|
pre.unit,
|
||||||
|
quote.unit
|
||||||
|
);
|
||||||
|
|
||||||
|
into_response(Error::UnitUnsupported)
|
||||||
|
})?;
|
||||||
|
|
||||||
(pre.payment_preimage, amount_spent)
|
(pre.payment_preimage, amount_spent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,18 @@ rand = "0.8.5"
|
|||||||
bip39 = { version = "2.0", features = ["rand"] }
|
bip39 = { version = "2.0", features = ["rand"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
cdk = { path = "../cdk", version = "0.4.0", features = ["mint", "wallet"] }
|
cdk = { path = "../cdk", version = "0.4.0", features = ["mint", "wallet"] }
|
||||||
|
cdk-cln = { path = "../cdk-cln", version = "0.4.0" }
|
||||||
cdk-axum = { path = "../cdk-axum"}
|
cdk-axum = { path = "../cdk-axum"}
|
||||||
cdk-fake-wallet = { path = "../cdk-fake-wallet" }
|
cdk-fake-wallet = { path = "../cdk-fake-wallet" }
|
||||||
tower-http = { version = "0.4.4", features = ["cors"] }
|
tower-http = { version = "0.4.4", features = ["cors"] }
|
||||||
futures = { version = "0.3.28", default-features = false }
|
futures = { version = "0.3.28", default-features = false, features = ["executor"] }
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
# ln-regtest-rs = { path = "../../../../ln-regtest-rs" }
|
||||||
|
ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "1d88d3d0b" }
|
||||||
|
lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
|
||||||
|
tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
tokio = { version = "1", features = [
|
tokio = { version = "1", features = [
|
||||||
|
|||||||
326
crates/cdk-integration-tests/src/init_regtest.rs
Normal file
326
crates/cdk-integration-tests/src/init_regtest.rs
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
use std::{collections::HashMap, env, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::Router;
|
||||||
|
use bip39::Mnemonic;
|
||||||
|
use cdk::{
|
||||||
|
cdk_database::mint_memory::MintMemoryDatabase,
|
||||||
|
cdk_lightning::MintLightning,
|
||||||
|
mint::{FeeReserve, Mint},
|
||||||
|
nuts::{CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings},
|
||||||
|
};
|
||||||
|
use cdk_axum::LnKey;
|
||||||
|
use cdk_cln::Cln as CdkCln;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use ln_regtest_rs::{
|
||||||
|
bitcoin_client::BitcoinClient, bitcoind::Bitcoind, cln::Clnd, cln_client::ClnClient, lnd::Lnd,
|
||||||
|
lnd_client::LndClient,
|
||||||
|
};
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
const BITCOIND_ADDR: &str = "127.0.0.1:18443";
|
||||||
|
const ZMQ_RAW_BLOCK: &str = "tcp://127.0.0.1:28332";
|
||||||
|
const ZMQ_RAW_TX: &str = "tcp://127.0.0.1:28333";
|
||||||
|
const BITCOIN_RPC_USER: &str = "testuser";
|
||||||
|
const BITCOIN_RPC_PASS: &str = "testpass";
|
||||||
|
const CLN_ADDR: &str = "127.0.0.1:19846";
|
||||||
|
const LND_ADDR: &str = "0.0.0.0:18444";
|
||||||
|
const LND_RPC_ADDR: &str = "https://127.0.0.1:10009";
|
||||||
|
|
||||||
|
const BITCOIN_DIR: &str = "bitcoin";
|
||||||
|
const CLN_DIR: &str = "cln";
|
||||||
|
const LND_DIR: &str = "lnd";
|
||||||
|
|
||||||
|
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");
|
||||||
|
dir.parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mint_url() -> String {
|
||||||
|
format!("http://{}:{}", get_mint_addr(), get_mint_port())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_temp_dir() -> PathBuf {
|
||||||
|
let dir = env::var("cdk_itests").expect("Temp dir set");
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
dir.parse().expect("Valid path buf")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bitcoin_dir() -> PathBuf {
|
||||||
|
let dir = get_temp_dir().join(BITCOIN_DIR);
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
dir
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_bitcoind() -> Bitcoind {
|
||||||
|
Bitcoind::new(
|
||||||
|
get_bitcoin_dir(),
|
||||||
|
BITCOIND_ADDR.parse().unwrap(),
|
||||||
|
BITCOIN_RPC_USER.to_string(),
|
||||||
|
BITCOIN_RPC_PASS.to_string(),
|
||||||
|
ZMQ_RAW_BLOCK.to_string(),
|
||||||
|
ZMQ_RAW_TX.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_bitcoin_client() -> Result<BitcoinClient> {
|
||||||
|
BitcoinClient::new(
|
||||||
|
"wallet".to_string(),
|
||||||
|
BITCOIND_ADDR.into(),
|
||||||
|
None,
|
||||||
|
Some(BITCOIN_RPC_USER.to_string()),
|
||||||
|
Some(BITCOIN_RPC_PASS.to_string()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cln_dir() -> PathBuf {
|
||||||
|
let dir = get_temp_dir().join(CLN_DIR);
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
dir
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_cln() -> Clnd {
|
||||||
|
Clnd::new(
|
||||||
|
get_bitcoin_dir(),
|
||||||
|
get_cln_dir(),
|
||||||
|
CLN_ADDR.to_string().parse().unwrap(),
|
||||||
|
BITCOIN_RPC_USER.to_string(),
|
||||||
|
BITCOIN_RPC_PASS.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init_cln_client() -> Result<ClnClient> {
|
||||||
|
ClnClient::new(get_cln_dir(), None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_lnd_dir() -> PathBuf {
|
||||||
|
let dir = get_temp_dir().join(LND_DIR);
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
dir
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init_lnd() -> Lnd {
|
||||||
|
Lnd::new(
|
||||||
|
get_bitcoin_dir(),
|
||||||
|
get_lnd_dir(),
|
||||||
|
LND_ADDR.parse().unwrap(),
|
||||||
|
BITCOIN_RPC_USER.to_string(),
|
||||||
|
BITCOIN_RPC_PASS.to_string(),
|
||||||
|
ZMQ_RAW_BLOCK.to_string(),
|
||||||
|
ZMQ_RAW_TX.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init_lnd_client() -> Result<LndClient> {
|
||||||
|
let lnd_dir = get_lnd_dir();
|
||||||
|
let cert_file = lnd_dir.join("tls.cert");
|
||||||
|
let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon");
|
||||||
|
LndClient::new(LND_RPC_ADDR.parse().unwrap(), cert_file, macaroon_file).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_cln_backend(cln_client: &ClnClient) -> Result<CdkCln> {
|
||||||
|
let rpc_path = cln_client.rpc_path.clone();
|
||||||
|
|
||||||
|
let fee_reserve = FeeReserve {
|
||||||
|
min_fee_reserve: 1.into(),
|
||||||
|
percent_fee_reserve: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(CdkCln::new(
|
||||||
|
rpc_path,
|
||||||
|
fee_reserve,
|
||||||
|
MintMethodSettings::default(),
|
||||||
|
MeltMethodSettings::default(),
|
||||||
|
)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_mint() -> Result<Mint> {
|
||||||
|
let nuts = cdk::nuts::Nuts::new()
|
||||||
|
.nut07(true)
|
||||||
|
.nut08(true)
|
||||||
|
.nut09(true)
|
||||||
|
.nut10(true)
|
||||||
|
.nut11(true)
|
||||||
|
.nut12(true)
|
||||||
|
.nut14(true);
|
||||||
|
|
||||||
|
let mint_info = MintInfo::new().nuts(nuts);
|
||||||
|
|
||||||
|
let mnemonic = Mnemonic::generate(12)?;
|
||||||
|
|
||||||
|
let mut supported_units: HashMap<CurrencyUnit, (u64, u8)> = HashMap::new();
|
||||||
|
supported_units.insert(CurrencyUnit::Sat, (0, 32));
|
||||||
|
|
||||||
|
let mint = Mint::new(
|
||||||
|
&get_mint_url(),
|
||||||
|
&mnemonic.to_seed_normalized(""),
|
||||||
|
mint_info,
|
||||||
|
Arc::new(MintMemoryDatabase::default()),
|
||||||
|
supported_units,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(mint)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_cln_mint() -> Result<()> {
|
||||||
|
let default_filter = "debug";
|
||||||
|
|
||||||
|
let sqlx_filter = "sqlx=warn";
|
||||||
|
let hyper_filter = "hyper=warn";
|
||||||
|
|
||||||
|
let env_filter = EnvFilter::new(format!(
|
||||||
|
"{},{},{}",
|
||||||
|
default_filter, sqlx_filter, hyper_filter
|
||||||
|
));
|
||||||
|
|
||||||
|
// Parse input
|
||||||
|
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||||
|
|
||||||
|
let mint = create_mint().await?;
|
||||||
|
let cln_client = init_cln_client().await?;
|
||||||
|
|
||||||
|
let cln_backend = create_cln_backend(&cln_client).await?;
|
||||||
|
|
||||||
|
let mut ln_backends: HashMap<
|
||||||
|
LnKey,
|
||||||
|
Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>,
|
||||||
|
> = HashMap::new();
|
||||||
|
|
||||||
|
ln_backends.insert(
|
||||||
|
LnKey::new(CurrencyUnit::Sat, cdk::nuts::PaymentMethod::Bolt11),
|
||||||
|
Arc::new(cln_backend),
|
||||||
|
);
|
||||||
|
|
||||||
|
let quote_ttl = 100000;
|
||||||
|
|
||||||
|
let mint_arc = Arc::new(mint);
|
||||||
|
|
||||||
|
let v1_service = cdk_axum::create_mint_router(
|
||||||
|
&get_mint_url(),
|
||||||
|
Arc::clone(&mint_arc),
|
||||||
|
ln_backends.clone(),
|
||||||
|
quote_ttl,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mint_service = Router::new()
|
||||||
|
.merge(v1_service)
|
||||||
|
.layer(CorsLayer::permissive());
|
||||||
|
|
||||||
|
let mint = Arc::clone(&mint_arc);
|
||||||
|
|
||||||
|
for wallet in ln_backends.values() {
|
||||||
|
let wallet_clone = Arc::clone(wallet);
|
||||||
|
let mint = Arc::clone(&mint);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match wallet_clone.wait_any_invoice().await {
|
||||||
|
Ok(mut stream) => {
|
||||||
|
while let Some(request_lookup_id) = stream.next().await {
|
||||||
|
if let Err(err) =
|
||||||
|
handle_paid_invoice(Arc::clone(&mint), &request_lookup_id).await
|
||||||
|
{
|
||||||
|
// nosemgrep: direct-panic
|
||||||
|
panic!("{:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// nosemgrep: direct-panic
|
||||||
|
panic!("Could not get invoice stream: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
println!("Staring Axum server");
|
||||||
|
axum::Server::bind(
|
||||||
|
&format!("{}:{}", "127.0.0.1", 8085)
|
||||||
|
.as_str()
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.serve(mint_service.into_make_service())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update mint quote when called for a paid invoice
|
||||||
|
async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result<()> {
|
||||||
|
println!("Invoice with lookup id paid: {}", request_lookup_id);
|
||||||
|
if let Ok(Some(mint_quote)) = mint
|
||||||
|
.localstore
|
||||||
|
.get_mint_quote_by_request_lookup_id(request_lookup_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
println!(
|
||||||
|
"Quote {} paid by lookup id {}",
|
||||||
|
mint_quote.id, request_lookup_id
|
||||||
|
);
|
||||||
|
mint.localstore
|
||||||
|
.update_mint_quote_state(&mint_quote.id, cdk::nuts::MintQuoteState::Paid)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fund_ln(
|
||||||
|
bitcoin_client: &BitcoinClient,
|
||||||
|
cln_client: &ClnClient,
|
||||||
|
lnd_client: &LndClient,
|
||||||
|
) -> Result<()> {
|
||||||
|
let lnd_address = lnd_client.get_new_address().await?;
|
||||||
|
|
||||||
|
bitcoin_client.send_to_address(&lnd_address, 2_000_000)?;
|
||||||
|
|
||||||
|
let cln_address = cln_client.get_new_address().await?;
|
||||||
|
bitcoin_client.send_to_address(&cln_address, 2_000_000)?;
|
||||||
|
|
||||||
|
let mining_address = bitcoin_client.get_new_address()?;
|
||||||
|
bitcoin_client.generate_blocks(&mining_address, 200)?;
|
||||||
|
|
||||||
|
cln_client.wait_chain_sync().await?;
|
||||||
|
lnd_client.wait_chain_sync().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn open_channel(
|
||||||
|
bitcoin_client: &BitcoinClient,
|
||||||
|
cln_client: &ClnClient,
|
||||||
|
lnd_client: &LndClient,
|
||||||
|
) -> Result<()> {
|
||||||
|
let cln_info = cln_client.get_info().await?;
|
||||||
|
|
||||||
|
let cln_pubkey = cln_info.id;
|
||||||
|
let cln_address = "127.0.0.1";
|
||||||
|
let cln_port = 19846;
|
||||||
|
|
||||||
|
lnd_client
|
||||||
|
.connect(cln_pubkey.to_string(), cln_address.to_string(), cln_port)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
lnd_client
|
||||||
|
.open_channel(1_500_000, &cln_pubkey.to_string(), Some(750_000))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mine_to_address = bitcoin_client.get_new_address()?;
|
||||||
|
bitcoin_client.generate_blocks(&mine_to_address, 10)?;
|
||||||
|
|
||||||
|
cln_client.wait_chain_sync().await?;
|
||||||
|
lnd_client.wait_chain_sync().await?;
|
||||||
|
|
||||||
|
cln_client.wait_channels_active().await?;
|
||||||
|
lnd_client.wait_channels_active().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -19,12 +19,11 @@ use cdk::{Mint, Wallet};
|
|||||||
use cdk_axum::LnKey;
|
use cdk_axum::LnKey;
|
||||||
use cdk_fake_wallet::FakeWallet;
|
use cdk_fake_wallet::FakeWallet;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use init_regtest::{get_mint_addr, get_mint_port, get_mint_url};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tower_http::cors::CorsLayer;
|
use tower_http::cors::CorsLayer;
|
||||||
|
|
||||||
pub const MINT_URL: &str = "http://127.0.0.1:8088";
|
pub mod init_regtest;
|
||||||
const LISTEN_ADDR: &str = "127.0.0.1";
|
|
||||||
const LISTEN_PORT: u16 = 8088;
|
|
||||||
|
|
||||||
pub fn create_backends_fake_wallet(
|
pub fn create_backends_fake_wallet(
|
||||||
) -> HashMap<LnKey, Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>> {
|
) -> HashMap<LnKey, Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>> {
|
||||||
@@ -70,7 +69,7 @@ pub async fn start_mint(
|
|||||||
let mnemonic = Mnemonic::generate(12)?;
|
let mnemonic = Mnemonic::generate(12)?;
|
||||||
|
|
||||||
let mint = Mint::new(
|
let mint = Mint::new(
|
||||||
MINT_URL,
|
&get_mint_url(),
|
||||||
&mnemonic.to_seed_normalized(""),
|
&mnemonic.to_seed_normalized(""),
|
||||||
mint_info,
|
mint_info,
|
||||||
Arc::new(MintMemoryDatabase::default()),
|
Arc::new(MintMemoryDatabase::default()),
|
||||||
@@ -83,7 +82,7 @@ pub async fn start_mint(
|
|||||||
let mint_arc = Arc::new(mint);
|
let mint_arc = Arc::new(mint);
|
||||||
|
|
||||||
let v1_service = cdk_axum::create_mint_router(
|
let v1_service = cdk_axum::create_mint_router(
|
||||||
MINT_URL,
|
&get_mint_url(),
|
||||||
Arc::clone(&mint_arc),
|
Arc::clone(&mint_arc),
|
||||||
ln_backends.clone(),
|
ln_backends.clone(),
|
||||||
quote_ttl,
|
quote_ttl,
|
||||||
@@ -120,7 +119,7 @@ pub async fn start_mint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
axum::Server::bind(
|
axum::Server::bind(
|
||||||
&format!("{}:{}", LISTEN_ADDR, LISTEN_PORT)
|
&format!("{}:{}", get_mint_addr(), get_mint_port())
|
||||||
.as_str()
|
.as_str()
|
||||||
.parse()?,
|
.parse()?,
|
||||||
)
|
)
|
||||||
|
|||||||
40
crates/cdk-integration-tests/src/main.rs
Normal file
40
crates/cdk-integration-tests/src/main.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use cdk_integration_tests::init_regtest::{
|
||||||
|
fund_ln, init_bitcoin_client, init_bitcoind, init_cln, init_cln_client, init_lnd,
|
||||||
|
init_lnd_client, open_channel, start_cln_mint,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let mut bitcoind = init_bitcoind();
|
||||||
|
bitcoind.start_bitcoind()?;
|
||||||
|
|
||||||
|
let bitcoin_client = init_bitcoin_client()?;
|
||||||
|
bitcoin_client.create_wallet().ok();
|
||||||
|
bitcoin_client.load_wallet()?;
|
||||||
|
|
||||||
|
let new_add = bitcoin_client.get_new_address()?;
|
||||||
|
bitcoin_client.generate_blocks(&new_add, 200).unwrap();
|
||||||
|
|
||||||
|
let mut clnd = init_cln();
|
||||||
|
clnd.start_clnd()?;
|
||||||
|
|
||||||
|
let cln_client = init_cln_client().await?;
|
||||||
|
|
||||||
|
let mut lnd = init_lnd().await;
|
||||||
|
lnd.start_lnd().unwrap();
|
||||||
|
|
||||||
|
let lnd_client = init_lnd_client().await.unwrap();
|
||||||
|
|
||||||
|
fund_ln(&bitcoin_client, &cln_client, &lnd_client)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
open_channel(&bitcoin_client, &cln_client, &lnd_client)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
start_cln_mint().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
255
crates/cdk-integration-tests/tests/regtest.rs
Normal file
255
crates/cdk-integration-tests/tests/regtest.rs
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
use std::{str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use bip39::Mnemonic;
|
||||||
|
use cdk::{
|
||||||
|
amount::{Amount, SplitTarget},
|
||||||
|
cdk_database::WalletMemoryDatabase,
|
||||||
|
nuts::{CurrencyUnit, MeltQuoteState, State},
|
||||||
|
wallet::Wallet,
|
||||||
|
};
|
||||||
|
use cdk_integration_tests::init_regtest::{get_mint_url, init_cln_client, init_lnd_client};
|
||||||
|
use lightning_invoice::Bolt11Invoice;
|
||||||
|
use ln_regtest_rs::InvoiceStatus;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_regtest_mint_melt_round_trip() -> Result<()> {
|
||||||
|
let lnd_client = init_lnd_client().await.unwrap();
|
||||||
|
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
&get_mint_url(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||||
|
|
||||||
|
lnd_client.pay_invoice(mint_quote.request).await?;
|
||||||
|
|
||||||
|
let mint_amount = wallet
|
||||||
|
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert!(mint_amount == 100.into());
|
||||||
|
|
||||||
|
let invoice = lnd_client.create_invoice(50).await?;
|
||||||
|
|
||||||
|
let melt = wallet.melt_quote(invoice, None).await?;
|
||||||
|
|
||||||
|
let melt = wallet.melt(&melt.id).await.unwrap();
|
||||||
|
|
||||||
|
assert!(melt.preimage.is_some());
|
||||||
|
|
||||||
|
assert!(melt.state == MeltQuoteState::Paid);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_regtest_mint_melt() -> Result<()> {
|
||||||
|
let lnd_client = init_lnd_client().await?;
|
||||||
|
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
&get_mint_url(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mint_amount = Amount::from(100);
|
||||||
|
|
||||||
|
let mint_quote = wallet.mint_quote(mint_amount, None).await?;
|
||||||
|
|
||||||
|
assert_eq!(mint_quote.amount, mint_amount);
|
||||||
|
|
||||||
|
lnd_client.pay_invoice(mint_quote.request).await?;
|
||||||
|
|
||||||
|
let mint_amount = wallet
|
||||||
|
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert!(mint_amount == 100.into());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_restore() -> Result<()> {
|
||||||
|
let lnd_client = init_lnd_client().await?;
|
||||||
|
|
||||||
|
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
&get_mint_url(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
|
&seed,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||||
|
|
||||||
|
lnd_client.pay_invoice(mint_quote.request).await?;
|
||||||
|
|
||||||
|
let _mint_amount = wallet
|
||||||
|
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert!(wallet.total_balance().await? == 100.into());
|
||||||
|
|
||||||
|
let wallet_2 = Wallet::new(
|
||||||
|
&get_mint_url(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
|
&seed,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert!(wallet_2.total_balance().await? == 0.into());
|
||||||
|
|
||||||
|
let restored = wallet_2.restore().await?;
|
||||||
|
let proofs = wallet_2.get_proofs().await?;
|
||||||
|
|
||||||
|
wallet_2
|
||||||
|
.swap(None, SplitTarget::default(), proofs, None, false)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert!(restored == 100.into());
|
||||||
|
|
||||||
|
assert!(wallet_2.total_balance().await? == 100.into());
|
||||||
|
|
||||||
|
let proofs = wallet.get_proofs().await?;
|
||||||
|
|
||||||
|
let states = wallet.check_proofs_spent(proofs).await?;
|
||||||
|
|
||||||
|
for state in states {
|
||||||
|
if state.state != State::Spent {
|
||||||
|
bail!("All proofs should be spent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_pay_invoice_twice() -> Result<()> {
|
||||||
|
let lnd_client = init_lnd_client().await?;
|
||||||
|
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
&get_mint_url(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
|
&seed,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||||
|
|
||||||
|
lnd_client.pay_invoice(mint_quote.request).await?;
|
||||||
|
|
||||||
|
let mint_amount = wallet
|
||||||
|
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(mint_amount, 100.into());
|
||||||
|
|
||||||
|
let invoice = lnd_client.create_invoice(10).await?;
|
||||||
|
|
||||||
|
let melt_quote = wallet.melt_quote(invoice.clone(), None).await?;
|
||||||
|
|
||||||
|
let melt = wallet.melt(&melt_quote.id).await.unwrap();
|
||||||
|
|
||||||
|
let melt_two = wallet.melt_quote(invoice, None).await?;
|
||||||
|
|
||||||
|
let melt_two = wallet.melt(&melt_two.id).await;
|
||||||
|
|
||||||
|
match melt_two {
|
||||||
|
Err(err) => match err {
|
||||||
|
cdk::Error::RequestAlreadyPaid => (),
|
||||||
|
_ => {
|
||||||
|
bail!("Wrong invoice already paid");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(_) => {
|
||||||
|
bail!("Should not have allowed second payment");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let balance = wallet.total_balance().await?;
|
||||||
|
|
||||||
|
assert_eq!(balance, (Amount::from(100) - melt.fee_paid - melt.amount));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_internal_payment() -> Result<()> {
|
||||||
|
let lnd_client = init_lnd_client().await?;
|
||||||
|
|
||||||
|
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
&get_mint_url(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
|
&seed,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||||
|
|
||||||
|
lnd_client.pay_invoice(mint_quote.request).await?;
|
||||||
|
|
||||||
|
let _mint_amount = wallet
|
||||||
|
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert!(wallet.total_balance().await? == 100.into());
|
||||||
|
|
||||||
|
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
||||||
|
|
||||||
|
let wallet_2 = Wallet::new(
|
||||||
|
&get_mint_url(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(WalletMemoryDatabase::default()),
|
||||||
|
&seed,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mint_quote = wallet_2.mint_quote(10.into(), None).await?;
|
||||||
|
|
||||||
|
let melt = wallet.melt_quote(mint_quote.request.clone(), None).await?;
|
||||||
|
|
||||||
|
assert_eq!(melt.amount, 10.into());
|
||||||
|
|
||||||
|
let _melted = wallet.melt(&melt.id).await.unwrap();
|
||||||
|
|
||||||
|
let _wallet_2_mint = wallet_2
|
||||||
|
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let cln_client = init_cln_client().await?;
|
||||||
|
let payment_hash = Bolt11Invoice::from_str(&mint_quote.request)?;
|
||||||
|
let check_paid = cln_client
|
||||||
|
.check_incoming_invoice(payment_hash.payment_hash().to_string())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match check_paid {
|
||||||
|
InvoiceStatus::Unpaid => (),
|
||||||
|
_ => {
|
||||||
|
bail!("Invoice has incorrect status: {:?}", check_paid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let wallet_2_balance = wallet_2.total_balance().await?;
|
||||||
|
|
||||||
|
assert!(wallet_2_balance == 10.into());
|
||||||
|
|
||||||
|
let wallet_1_balance = wallet.total_balance().await?;
|
||||||
|
|
||||||
|
assert!(wallet_1_balance == 90.into());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -121,6 +121,12 @@ pub fn construct_proofs(
|
|||||||
keys: &Keys,
|
keys: &Keys,
|
||||||
) -> Result<Proofs, Error> {
|
) -> Result<Proofs, Error> {
|
||||||
if (promises.len() != rs.len()) || (promises.len() != secrets.len()) {
|
if (promises.len() != rs.len()) || (promises.len() != secrets.len()) {
|
||||||
|
tracing::error!(
|
||||||
|
"Promises: {}, RS: {}, secrets:{}",
|
||||||
|
promises.len(),
|
||||||
|
rs.len(),
|
||||||
|
secrets.len()
|
||||||
|
);
|
||||||
return Err(Error::Custom(
|
return Err(Error::Custom(
|
||||||
"Lengths of promises, rs, and secrets must be equal".to_string(),
|
"Lengths of promises, rs, and secrets must be equal".to_string(),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ impl Melted {
|
|||||||
let fee_paid = proofs_amount
|
let fee_paid = proofs_amount
|
||||||
.checked_sub(amount + change_amount)
|
.checked_sub(amount + change_amount)
|
||||||
.ok_or(Error::AmountOverflow)?;
|
.ok_or(Error::AmountOverflow)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
state,
|
state,
|
||||||
preimage,
|
preimage,
|
||||||
|
|||||||
@@ -1442,12 +1442,27 @@ impl Wallet {
|
|||||||
.ok_or(Error::NoActiveKeyset)?;
|
.ok_or(Error::NoActiveKeyset)?;
|
||||||
|
|
||||||
let change_proofs = match melt_response.change {
|
let change_proofs = match melt_response.change {
|
||||||
Some(change) => Some(construct_proofs(
|
Some(change) => {
|
||||||
|
let num_change_proof = change.len();
|
||||||
|
|
||||||
|
let num_change_proof = match (
|
||||||
|
premint_secrets.len() < num_change_proof,
|
||||||
|
premint_secrets.secrets().len() < num_change_proof,
|
||||||
|
) {
|
||||||
|
(true, _) | (_, true) => {
|
||||||
|
tracing::error!("Mismatch in change promises to change");
|
||||||
|
premint_secrets.len()
|
||||||
|
}
|
||||||
|
_ => num_change_proof,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(construct_proofs(
|
||||||
change,
|
change,
|
||||||
premint_secrets.rs(),
|
premint_secrets.rs()[..num_change_proof].to_vec(),
|
||||||
premint_secrets.secrets(),
|
premint_secrets.secrets()[..num_change_proof].to_vec(),
|
||||||
&active_keys,
|
&active_keys,
|
||||||
)?),
|
)?)
|
||||||
|
}
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,9 @@
|
|||||||
nixpkgs-fmt
|
nixpkgs-fmt
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
typos
|
typos
|
||||||
|
lnd
|
||||||
|
clightning
|
||||||
|
bitcoind
|
||||||
] ++ libsDarwin;
|
] ++ libsDarwin;
|
||||||
|
|
||||||
# WASM deps
|
# WASM deps
|
||||||
|
|||||||
1
justfile
1
justfile
@@ -1,4 +1,5 @@
|
|||||||
import "./misc/justfile.custom.just"
|
import "./misc/justfile.custom.just"
|
||||||
|
import "./misc/test.just"
|
||||||
|
|
||||||
alias b := build
|
alias b := build
|
||||||
alias c := check
|
alias c := check
|
||||||
|
|||||||
92
misc/itests.sh
Executable file
92
misc/itests.sh
Executable file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Function to perform cleanup
|
||||||
|
cleanup() {
|
||||||
|
echo "Cleaning up..."
|
||||||
|
|
||||||
|
# Kill the Rust binary process
|
||||||
|
echo "Killing the Rust binary with PID $RUST_BIN_PID"
|
||||||
|
kill $CDK_ITEST_MINT_BIN_PID
|
||||||
|
|
||||||
|
# Wait for the Rust binary to terminate
|
||||||
|
wait $CDK_ITEST_MINT_BIN_PID
|
||||||
|
|
||||||
|
echo "Mint binary terminated"
|
||||||
|
|
||||||
|
# Kill processes
|
||||||
|
lncli --lnddir="$cdk_itests/lnd" --network=regtest stop
|
||||||
|
lightning-cli --regtest --lightning-dir="$cdk_itests/cln/" stop
|
||||||
|
bitcoin-cli --datadir="$cdk_itests/bitcoin" -rpcuser=testuser -rpcpassword=testpass -rpcport=18443 stop
|
||||||
|
|
||||||
|
# Remove the temporary directory
|
||||||
|
rm -rf "$cdk_itests"
|
||||||
|
echo "Temp directory removed: $cdk_itests"
|
||||||
|
unset cdk_itests
|
||||||
|
unset cdk_itests_mint_addr
|
||||||
|
unset cdk_itests_mint_port
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up trap to call cleanup on script exit
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Create a temporary directory
|
||||||
|
export cdk_itests=$(mktemp -d)
|
||||||
|
export cdk_itests_mint_addr="127.0.0.1";
|
||||||
|
export cdk_itests_mint_port=8085;
|
||||||
|
|
||||||
|
URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port/v1/info"
|
||||||
|
# Check if the temporary directory was created successfully
|
||||||
|
if [[ ! -d "$cdk_itests" ]]; then
|
||||||
|
echo "Failed to create temp directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Temp directory created: $cdk_itests"
|
||||||
|
|
||||||
|
cargo build -p cdk-integration-tests
|
||||||
|
cargo build --bin cdk-integration-tests
|
||||||
|
cargo run --bin cdk-integration-tests &
|
||||||
|
# Capture its PID
|
||||||
|
CDK_ITEST_MINT_BIN_PID=$!
|
||||||
|
|
||||||
|
TIMEOUT=100
|
||||||
|
START_TIME=$(date +%s)
|
||||||
|
# Loop until the endpoint returns a 200 OK status or timeout is reached
|
||||||
|
while true; do
|
||||||
|
# Get the current time
|
||||||
|
CURRENT_TIME=$(date +%s)
|
||||||
|
|
||||||
|
# Calculate the elapsed time
|
||||||
|
ELAPSED_TIME=$((CURRENT_TIME - START_TIME))
|
||||||
|
|
||||||
|
# Check if the elapsed time exceeds the timeout
|
||||||
|
if [ $ELAPSED_TIME -ge $TIMEOUT ]; then
|
||||||
|
echo "Timeout of $TIMEOUT seconds reached. Exiting..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make a request to the endpoint and capture the HTTP status code
|
||||||
|
HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" $URL)
|
||||||
|
|
||||||
|
# Check if the HTTP status is 200 OK
|
||||||
|
if [ "$HTTP_STATUS" -eq 200 ]; then
|
||||||
|
echo "Received 200 OK from $URL"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "Waiting for 200 OK response, current status: $HTTP_STATUS"
|
||||||
|
sleep 2 # Wait for 2 seconds before retrying
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
# Run cargo test
|
||||||
|
cargo test -p cdk-integration-tests
|
||||||
|
|
||||||
|
# Capture the exit status of cargo test
|
||||||
|
test_status=$?
|
||||||
|
|
||||||
|
# Source PIDs from file
|
||||||
|
source "$cdk_itests/pids.txt"
|
||||||
|
|
||||||
|
# Exit with the status of the tests
|
||||||
|
exit $test_status
|
||||||
4
misc/test.just
Normal file
4
misc/test.just
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
itest:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
./misc/itests.sh
|
||||||
|
|
||||||
Reference in New Issue
Block a user