From b333eefee1225c92c7e9409055795f5b390503f2 Mon Sep 17 00:00:00 2001 From: michael1011 Date: Tue, 20 Aug 2024 16:27:30 +0200 Subject: [PATCH 1/3] test: add unit tests --- .github/workflows/ci.yml | 3 + Cargo.lock | 78 ++++++++ Cargo.toml | 4 + src/commands/cancel.rs | 4 +- src/commands/invoice.rs | 5 +- src/commands/list.rs | 4 +- src/commands/settle.rs | 4 +- src/database/model.rs | 239 +++++++++++++++++------ src/encoder.rs | 31 +-- src/grpc/server.rs | 182 +++++++++++++++++- src/grpc/service.rs | 14 +- src/grpc/tls.rs | 94 +++++++++ src/grpc/transformers.rs | 2 +- src/handler.rs | 404 +++++++++++++++++++++++++++++++++++++++ src/hooks/mod.rs | 10 +- src/main.rs | 10 +- 16 files changed, 999 insertions(+), 89 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 907506b..29c2ddb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,3 +35,6 @@ jobs: - name: Clippy run: cargo clippy + + - name: Unit tests + run: cargo test diff --git a/Cargo.lock b/Cargo.lock index c659ad1..06cd0b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + [[package]] name = "anyhow" version = "1.0.86" @@ -477,6 +483,12 @@ dependencies = [ "syn", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "dsl_auto_type" version = "0.1.2" @@ -550,6 +562,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "futures" version = "0.3.30" @@ -745,12 +763,14 @@ dependencies = [ "hex", "lightning-invoice", "log", + "mockall", "prost", "rcgen", "secp256k1", "serde", "serde_json", "tokio", + "tokio-util", "tonic", "tonic-build", ] @@ -1112,6 +1132,32 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "multimap" version = "0.10.0" @@ -1313,6 +1359,32 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettyplease" version = "0.2.20" @@ -1782,6 +1854,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.63" diff --git a/Cargo.toml b/Cargo.toml index 9ddae77..fac0471 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,11 @@ bitcoin = { version = "0.30.2", features = ["rand-std"] } secp256k1 = "0.27.0" cln-rpc = "0.1.9" hex = "0.4.3" +tokio-util = "0.7.11" [build-dependencies] built = { version = "0.7.4", features = ["git2"] } tonic-build = "0.12.1" + +[dev-dependencies] +mockall = "0.13.0" diff --git a/src/commands/cancel.rs b/src/commands/cancel.rs index 9353ddf..535e8de 100644 --- a/src/commands/cancel.rs +++ b/src/commands/cancel.rs @@ -1,5 +1,6 @@ use crate::commands::structs::{parse_args, FromArr, ParamsError}; use crate::database::helpers::invoice_helper::InvoiceHelper; +use crate::encoder::InvoiceEncoder; use crate::State; use cln_plugin::Plugin; use serde::{Deserialize, Serialize}; @@ -25,9 +26,10 @@ impl FromArr for CancelRequest { #[derive(Debug, Serialize)] struct CancelResponse {} -pub async fn cancel(plugin: Plugin>, args: Value) -> anyhow::Result +pub async fn cancel(plugin: Plugin>, args: Value) -> anyhow::Result where T: InvoiceHelper + Sync + Send + Clone, + E: InvoiceEncoder + Sync + Send + Clone, { let params = parse_args::(args)?; let payment_hash = hex::decode(params.payment_hash)?; diff --git a/src/commands/invoice.rs b/src/commands/invoice.rs index 47c6e57..0ce9bdb 100644 --- a/src/commands/invoice.rs +++ b/src/commands/invoice.rs @@ -1,7 +1,7 @@ use crate::commands::structs::{parse_args, FromArr, ParamsError}; use crate::database::helpers::invoice_helper::InvoiceHelper; use crate::database::model::{InvoiceInsertable, InvoiceState}; -use crate::encoder::InvoiceBuilder; +use crate::encoder::{InvoiceBuilder, InvoiceEncoder}; use crate::State; use anyhow::Result; use cln_plugin::Plugin; @@ -34,9 +34,10 @@ struct InvoiceResponse { bolt11: String, } -pub async fn invoice(plugin: Plugin>, args: Value) -> Result +pub async fn invoice(plugin: Plugin>, args: Value) -> Result where T: InvoiceHelper + Sync + Send + Clone, + E: InvoiceEncoder + Sync + Send + Clone, { let params = parse_args::(args)?; let payment_hash = hex::decode(params.payment_hash)?; diff --git a/src/commands/list.rs b/src/commands/list.rs index 718cfe4..17c2a2b 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,6 +1,7 @@ use crate::commands::structs::{parse_args, FromArr, ParamsError}; use crate::database::helpers::invoice_helper::InvoiceHelper; use crate::database::model::{HoldInvoice, Htlc}; +use crate::encoder::InvoiceEncoder; use crate::State; use cln_plugin::Plugin; use lightning_invoice::Bolt11Invoice; @@ -64,9 +65,10 @@ struct ListInvoicesResponse { holdinvoices: Vec, } -pub async fn list_invoices(plugin: Plugin>, args: Value) -> anyhow::Result +pub async fn list_invoices(plugin: Plugin>, args: Value) -> anyhow::Result where T: InvoiceHelper + Sync + Send + Clone, + E: InvoiceEncoder + Sync + Send + Clone, { let params = parse_args::(args)?; if params.bolt11.is_some() && params.payment_hash.is_some() { diff --git a/src/commands/settle.rs b/src/commands/settle.rs index f441487..8252fdc 100644 --- a/src/commands/settle.rs +++ b/src/commands/settle.rs @@ -1,5 +1,6 @@ use crate::commands::structs::{parse_args, FromArr, ParamsError}; use crate::database::helpers::invoice_helper::InvoiceHelper; +use crate::encoder::InvoiceEncoder; use crate::State; use bitcoin::hashes::{sha256, Hash}; use cln_plugin::Plugin; @@ -26,9 +27,10 @@ impl FromArr for SettleRequest { #[derive(Debug, Serialize)] struct SettleResponse {} -pub async fn settle(plugin: Plugin>, args: Value) -> anyhow::Result +pub async fn settle(plugin: Plugin>, args: Value) -> anyhow::Result where T: InvoiceHelper + Sync + Send + Clone, + E: InvoiceEncoder + Sync + Send + Clone, { let params = parse_args::(args)?; let preimage = hex::decode(params.preimage)?; diff --git a/src/database/model.rs b/src/database/model.rs index 99f2c2d..23d2dae 100644 --- a/src/database/model.rs +++ b/src/database/model.rs @@ -3,6 +3,59 @@ use diesel::{AsChangeset, Associations, Identifiable, Insertable, Queryable, Sel use serde::Serialize; use std::fmt::{Display, Formatter}; +#[derive(Queryable, Identifiable, Selectable, AsChangeset, Serialize, Debug, PartialEq, Clone)] +#[diesel(table_name = crate::database::schema::invoices)] +pub struct Invoice { + pub id: i64, + pub payment_hash: Vec, + pub preimage: Option>, + pub bolt11: String, + pub state: String, + pub created_at: chrono::NaiveDateTime, +} + +#[derive(Insertable, Debug, PartialEq, Clone)] +#[diesel(table_name = crate::database::schema::invoices)] +pub struct InvoiceInsertable { + pub payment_hash: Vec, + pub bolt11: String, + pub state: String, +} + +#[derive( + Queryable, + Identifiable, + Selectable, + Associations, + Insertable, + AsChangeset, + Serialize, + Debug, + PartialEq, + Clone, +)] +#[diesel(belongs_to(Invoice))] +#[diesel(table_name = crate::database::schema::htlcs)] +pub struct Htlc { + pub id: i64, + pub invoice_id: i64, + pub state: String, + pub scid: String, + pub channel_id: i64, + pub msat: i64, + pub created_at: chrono::NaiveDateTime, +} + +#[derive(Insertable, Debug, PartialEq, Clone)] +#[diesel(table_name = crate::database::schema::htlcs)] +pub struct HtlcInsertable { + pub invoice_id: i64, + pub state: String, + pub scid: String, + pub channel_id: i64, + pub msat: i64, +} + #[derive(Debug, PartialEq, Clone, Copy)] pub enum InvoiceState { Paid = 0, @@ -17,12 +70,6 @@ impl Display for InvoiceState { } } -impl InvoiceState { - pub fn is_final(&self) -> bool { - *self == InvoiceState::Paid || *self == InvoiceState::Cancelled - } -} - impl From for String { fn from(value: InvoiceState) -> Self { match value { @@ -49,6 +96,12 @@ impl TryFrom<&str> for InvoiceState { } } +impl InvoiceState { + pub fn is_final(&self) -> bool { + *self == InvoiceState::Paid || *self == InvoiceState::Cancelled + } +} + #[derive(Serialize, Clone, Debug)] pub struct HoldInvoice { pub invoice: Invoice, @@ -79,55 +132,133 @@ impl HoldInvoice { } } -#[derive(Queryable, Identifiable, Selectable, AsChangeset, Serialize, Debug, PartialEq, Clone)] -#[diesel(table_name = crate::database::schema::invoices)] -pub struct Invoice { - pub id: i64, - pub payment_hash: Vec, - pub preimage: Option>, - pub bolt11: String, - pub state: String, - pub created_at: chrono::NaiveDateTime, -} +#[cfg(test)] +mod test { + use crate::database::model::{HoldInvoice, Htlc, Invoice, InvoiceState}; -#[derive(Insertable, Debug, PartialEq, Clone)] -#[diesel(table_name = crate::database::schema::invoices)] -pub struct InvoiceInsertable { - pub payment_hash: Vec, - pub bolt11: String, - pub state: String, -} + #[test] + fn invoice_state_to_string() { + assert_eq!(InvoiceState::Paid.to_string(), "paid"); + assert_eq!(InvoiceState::Unpaid.to_string(), "unpaid"); + assert_eq!(InvoiceState::Accepted.to_string(), "accepted"); + assert_eq!(InvoiceState::Cancelled.to_string(), "cancelled"); + } -#[derive( - Queryable, - Identifiable, - Selectable, - Associations, - Insertable, - AsChangeset, - Serialize, - Debug, - PartialEq, - Clone, -)] -#[diesel(belongs_to(Invoice))] -#[diesel(table_name = crate::database::schema::htlcs)] -pub struct Htlc { - pub id: i64, - pub invoice_id: i64, - pub state: String, - pub scid: String, - pub channel_id: i64, - pub msat: i64, - pub created_at: chrono::NaiveDateTime, -} + #[test] + fn invoice_state_from_str() { + assert_eq!(InvoiceState::try_from("paid").unwrap(), InvoiceState::Paid); + assert_eq!( + InvoiceState::try_from("unpaid").unwrap(), + InvoiceState::Unpaid + ); + assert_eq!( + InvoiceState::try_from("accepted").unwrap(), + InvoiceState::Accepted + ); + assert_eq!( + InvoiceState::try_from("cancelled").unwrap(), + InvoiceState::Cancelled + ); -#[derive(Insertable, Debug, PartialEq, Clone)] -#[diesel(table_name = crate::database::schema::htlcs)] -pub struct HtlcInsertable { - pub invoice_id: i64, - pub state: String, - pub scid: String, - pub channel_id: i64, - pub msat: i64, + assert_eq!( + InvoiceState::try_from("invalid").err().unwrap(), + "unknown state invariant" + ); + } + + #[test] + fn invoice_state_is_final() { + assert!(InvoiceState::Paid.is_final()); + assert!(InvoiceState::Cancelled.is_final()); + + assert!(!InvoiceState::Unpaid.is_final()); + assert!(!InvoiceState::Accepted.is_final()); + } + + #[test] + fn hold_invoice_amount_paid_msat() { + let mut invoice = HoldInvoice::new( + Invoice { + id: 0, + payment_hash: vec![], + preimage: None, + bolt11: "".to_string(), + state: "".to_string(), + created_at: Default::default(), + }, + vec![], + ); + assert_eq!(invoice.amount_paid_msat(), 0); + + invoice.htlcs.push(Htlc { + id: 0, + invoice_id: 0, + state: InvoiceState::Cancelled.to_string(), + scid: "".to_string(), + channel_id: 0, + msat: 21_000, + created_at: Default::default(), + }); + assert_eq!(invoice.amount_paid_msat(), 0); + + invoice.htlcs.push(Htlc { + id: 0, + invoice_id: 0, + state: InvoiceState::Accepted.to_string(), + scid: "".to_string(), + channel_id: 0, + msat: 10_000, + created_at: Default::default(), + }); + assert_eq!(invoice.amount_paid_msat(), 10_000); + + invoice.htlcs.push(Htlc { + id: 0, + invoice_id: 0, + state: InvoiceState::Paid.to_string(), + scid: "".to_string(), + channel_id: 0, + msat: 10_000, + created_at: Default::default(), + }); + assert_eq!(invoice.amount_paid_msat(), 20_000); + } + + #[test] + fn hold_invoice_htlc_is_known() { + let invoice = HoldInvoice::new( + Invoice { + id: 0, + payment_hash: vec![], + preimage: None, + bolt11: "".to_string(), + state: "".to_string(), + created_at: Default::default(), + }, + vec![ + Htlc { + id: 0, + invoice_id: 0, + state: InvoiceState::Accepted.to_string(), + scid: "asdf".to_string(), + channel_id: 123, + msat: 0, + created_at: Default::default(), + }, + Htlc { + id: 0, + invoice_id: 0, + state: InvoiceState::Accepted.to_string(), + scid: "some channel".to_string(), + channel_id: 21, + msat: 0, + created_at: Default::default(), + }, + ], + ); + + assert!(invoice.htlc_is_known("asdf", 123)); + assert!(invoice.htlc_is_known("some channel", 21)); + assert!(!invoice.htlc_is_known("not found", 42)); + } } diff --git a/src/encoder.rs b/src/encoder.rs index 787b425..460878d 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -10,6 +10,7 @@ use std::fmt::{Display, Formatter}; use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; +use tonic::async_trait; const DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA: u64 = 80; @@ -88,6 +89,11 @@ impl InvoiceBuilder { } } +#[async_trait] +pub trait InvoiceEncoder { + async fn encode(&self, invoice_builder: InvoiceBuilder) -> Result; +} + #[derive(Clone)] pub struct Encoder { network: Currency, @@ -104,7 +110,20 @@ impl Encoder { }) } - pub async fn encode(&self, invoice_builder: InvoiceBuilder) -> Result { + fn parse_network(network: &str) -> Result { + match network { + "bitcoin" => Ok(Currency::Bitcoin), + "testnet" => Ok(Currency::BitcoinTestnet), + "signet" => Ok(Currency::Signet), + "regtest" => Ok(Currency::Regtest), + _ => Err(NetworkError::InvalidNetwork.into()), + } + } +} + +#[async_trait] +impl InvoiceEncoder for Encoder { + async fn encode(&self, invoice_builder: InvoiceBuilder) -> Result { let payment_hash: sha256::Hash = Hash::from_slice(&invoice_builder.payment_hash)?; let payment_secret = PaymentSecret(match invoice_builder.payment_secret { Some(secret) => secret.as_slice().try_into()?, @@ -170,14 +189,4 @@ impl Encoder { Ok(signed.bolt11) } - - fn parse_network(network: &str) -> Result { - match network { - "bitcoin" => Ok(Currency::Bitcoin), - "testnet" => Ok(Currency::BitcoinTestnet), - "signet" => Ok(Currency::Signet), - "regtest" => Ok(Currency::Regtest), - _ => Err(NetworkError::InvalidNetwork.into()), - } - } } diff --git a/src/grpc/server.rs b/src/grpc/server.rs index 8ac8bed..10006d1 100644 --- a/src/grpc/server.rs +++ b/src/grpc/server.rs @@ -1,5 +1,5 @@ use crate::database::helpers::invoice_helper::InvoiceHelper; -use crate::encoder::Encoder; +use crate::encoder::InvoiceEncoder; use crate::grpc::service::hold::hold_server::HoldServer; use crate::grpc::service::HoldService; use crate::grpc::tls::load_certificates; @@ -9,28 +9,32 @@ use log::info; use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; use std::str::FromStr; +use tokio_util::sync::CancellationToken; use tonic::transport::ServerTlsConfig; -pub struct Server { +pub struct Server { host: String, port: i64, directory: PathBuf, + cancellation_token: CancellationToken, - encoder: Encoder, + encoder: E, invoice_helper: T, settler: Settler, } -impl Server +impl Server where T: InvoiceHelper + Sync + Send + Clone + 'static, + E: InvoiceEncoder + Sync + Send + Clone + 'static, { pub fn new( host: &str, port: i64, + cancellation_token: CancellationToken, directory: PathBuf, invoice_helper: T, - encoder: Encoder, + encoder: E, settler: Settler, ) -> Self { Self { @@ -39,6 +43,7 @@ where encoder, directory, invoice_helper, + cancellation_token, host: host.to_string(), } } @@ -61,7 +66,172 @@ where self.encoder.clone(), self.settler.clone(), ))) - .serve(socket_addr) + .serve_with_shutdown(socket_addr, async move { + self.cancellation_token.cancelled().await; + info!("Shutting down gRPC server"); + }) .await?) } } + +#[cfg(test)] +mod test { + use crate::database::helpers::invoice_helper::InvoiceHelper; + use crate::database::model::*; + use crate::encoder::{InvoiceBuilder, InvoiceEncoder}; + use crate::grpc::server::Server; + use crate::grpc::service::hold::hold_client::HoldClient; + use crate::grpc::service::hold::GetInfoRequest; + use crate::settler::Settler; + use anyhow::Result; + use mockall::mock; + use std::fs; + use std::path::{Path, PathBuf}; + use std::time::Duration; + use tokio::task::JoinHandle; + use tokio_util::sync::CancellationToken; + use tonic::async_trait; + use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; + + mock! { + InvoiceHelper {} + + impl Clone for InvoiceHelper { + fn clone(&self) -> Self; + } + + impl InvoiceHelper for InvoiceHelper { + fn insert(&self, invoice: &InvoiceInsertable) -> Result; + fn insert_htlc(&self, htlc: &HtlcInsertable) -> Result; + fn set_invoice_state(&self, id: i64, state: InvoiceState) -> Result; + fn set_invoice_preimage(&self, id: i64, preimage: &[u8]) -> Result; + fn set_htlc_state_by_id(&self, htlc_id: i64, state: InvoiceState) -> Result; + fn set_htlc_states_by_invoice(&self, invoice_id: i64, state: InvoiceState) -> Result; + fn get_all(&self) -> Result>; + fn get_paginated(&self, index_start: i64, limit: u64) -> Result>; + fn get_by_payment_hash(&self, payment_hash: &[u8]) -> Result>; + } + } + + mock! { + InvoiceEncoder {} + + impl Clone for InvoiceEncoder { + fn clone(&self) -> Self; + } + + #[async_trait] + impl InvoiceEncoder for InvoiceEncoder { + async fn encode(&self, invoice_builder: InvoiceBuilder) -> Result; + } + } + + #[tokio::test] + async fn connect() { + let port = 9124; + let (certs_dir, token, server_thread) = start_server_tls(port).await; + + let tls = ClientTlsConfig::new() + .domain_name("hold") + .ca_certificate(Certificate::from_pem( + fs::read_to_string(certs_dir.clone().join("ca.pem")).unwrap(), + )) + .identity(Identity::from_pem( + fs::read_to_string(certs_dir.clone().join("client.pem")).unwrap(), + fs::read_to_string(certs_dir.clone().join("client-key.pem")).unwrap(), + )); + + let channel = Channel::from_shared(format!("https://127.0.0.1:{}", port)) + .unwrap() + .tls_config(tls) + .unwrap() + .connect() + .await + .unwrap(); + + let mut client = HoldClient::new(channel); + + let res = client.get_info(GetInfoRequest {}).await.unwrap(); + assert_eq!( + res.into_inner().version, + crate::utils::built_info::PKG_VERSION + ); + + token.cancel(); + server_thread.await.unwrap(); + + fs::remove_dir_all(certs_dir).unwrap() + } + + #[tokio::test] + async fn connect_invalid_client_certificate() { + let port = 9125; + let (certs_dir, token, server_thread) = start_server_tls(port).await; + + let tls = ClientTlsConfig::new() + .domain_name("hold") + .ca_certificate(Certificate::from_pem( + fs::read_to_string(certs_dir.clone().join("ca.pem")).unwrap(), + )); + + let channel = Channel::from_shared(format!("https://127.0.0.1:{}", port)) + .unwrap() + .tls_config(tls) + .unwrap() + .connect() + .await + .unwrap(); + + let mut client = HoldClient::new(channel); + + let res = client.get_info(GetInfoRequest {}).await; + assert_eq!(res.err().unwrap().message(), "transport error"); + + token.cancel(); + server_thread.await.unwrap(); + + fs::remove_dir_all(certs_dir).unwrap() + } + + async fn start_server_tls(port: i64) -> (PathBuf, CancellationToken, JoinHandle<()>) { + let certs_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("test-certs-{}", port)); + + let token = CancellationToken::new(); + + let server = Server::new( + "127.0.0.1", + port, + token.clone(), + certs_dir.clone(), + make_mock_invoice_helper(), + make_mock_invoice_encoder(), + Settler::new(make_mock_invoice_helper(), 60), + ); + + let server_thread = tokio::spawn(async move { + server.start().await.unwrap(); + () + }); + tokio::time::sleep(Duration::from_millis(50)).await; + + (certs_dir, token, server_thread) + } + + fn make_mock_invoice_helper() -> MockInvoiceHelper { + let mut hook_helper = MockInvoiceHelper::new(); + hook_helper + .expect_clone() + .returning(make_mock_invoice_helper); + + hook_helper + } + + fn make_mock_invoice_encoder() -> MockInvoiceEncoder { + let mut invoice_encoder = MockInvoiceEncoder::new(); + invoice_encoder + .expect_clone() + .returning(make_mock_invoice_encoder); + + invoice_encoder + } +} diff --git a/src/grpc/service.rs b/src/grpc/service.rs index 1ad7590..b2f8df0 100644 --- a/src/grpc/service.rs +++ b/src/grpc/service.rs @@ -1,6 +1,6 @@ use crate::database::helpers::invoice_helper::InvoiceHelper; use crate::database::model::{InvoiceInsertable, InvoiceState}; -use crate::encoder::{Encoder, InvoiceBuilder, InvoiceDescription}; +use crate::encoder::{InvoiceBuilder, InvoiceDescription, InvoiceEncoder}; use crate::grpc::service::hold::hold_server::Hold; use crate::grpc::service::hold::invoice_request::Description; use crate::grpc::service::hold::list_request::Constraint; @@ -23,17 +23,18 @@ pub mod hold { tonic::include_proto!("hold"); } -pub struct HoldService { - encoder: Encoder, +pub struct HoldService { + encoder: E, invoice_helper: T, settler: Settler, } -impl HoldService +impl HoldService where T: InvoiceHelper + Send + Sync + Clone + 'static, + E: InvoiceEncoder + Send + Sync + Clone + 'static, { - pub fn new(invoice_helper: T, encoder: Encoder, settler: Settler) -> Self { + pub fn new(invoice_helper: T, encoder: E, settler: Settler) -> Self { HoldService { encoder, settler, @@ -43,9 +44,10 @@ where } #[async_trait] -impl Hold for HoldService +impl Hold for HoldService where T: InvoiceHelper + Send + Sync + Clone + 'static, + E: InvoiceEncoder + Send + Sync + Clone + 'static, { async fn get_info( &self, diff --git a/src/grpc/tls.rs b/src/grpc/tls.rs index cd2b5d0..628e329 100644 --- a/src/grpc/tls.rs +++ b/src/grpc/tls.rs @@ -95,3 +95,97 @@ fn generate_certificate( Vec::from(cert.pem().as_bytes()), )) } + +#[cfg(test)] +mod test { + use crate::grpc::tls::{generate_certificate, generate_or_load_certificate, load_certificates}; + use rcgen::{CertificateParams, KeyPair}; + use std::fs; + use std::path::Path; + + #[test] + fn test_load_certificates() { + let certs_dir = "test-certs-all"; + assert_eq!(Path::new(certs_dir).exists(), false); + + let (_, cert) = load_certificates(certs_dir.into()).unwrap(); + assert_eq!(Path::new(certs_dir).exists(), true); + + for file in vec!["ca", "client", "server"] + .iter() + .flat_map(|entry| vec![format!("{}.pem", entry), format!("{}-key.pem", entry)]) + { + assert_eq!(Path::new(certs_dir).join(file).exists(), true); + } + + let (_, cert_loaded) = load_certificates(certs_dir.into()).unwrap(); + assert_eq!(cert.into_inner(), cert_loaded.into_inner()); + + fs::remove_dir_all(certs_dir).unwrap(); + } + + #[test] + fn test_generate_or_load_certificate() { + let certs_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("test-certs-load".to_string()); + fs::create_dir(certs_dir.clone()).unwrap(); + + let (created_key, created_cert) = + generate_or_load_certificate("test", Path::new(&certs_dir), "ca", None).unwrap(); + let (loaded_key, loaded_cert) = + generate_or_load_certificate("test", Path::new(&certs_dir), "ca", None).unwrap(); + + assert_eq!(created_key, loaded_key); + assert_eq!(created_cert, loaded_cert); + + fs::remove_dir_all(certs_dir).unwrap(); + } + + #[test] + fn test_generate_certificate_ca() { + let certs_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("test-certs1".to_string()); + fs::create_dir(certs_dir.clone()).unwrap(); + + let key_path = certs_dir.clone().join("key.pem"); + let cert_path = certs_dir.clone().join("cert.pem"); + let (key, cert) = + generate_certificate("test", key_path.clone(), cert_path.clone(), None).unwrap(); + + assert_eq!(key, fs::read(key_path).unwrap()); + assert_eq!(cert, fs::read(cert_path).unwrap()); + + fs::remove_dir_all(certs_dir).unwrap(); + } + + #[test] + fn test_generate_certificate() { + let certs_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("test-certs2".to_string()); + fs::create_dir(certs_dir.clone()).unwrap(); + + let (ca_key, ca_cert) = generate_certificate( + "test", + certs_dir.clone().join("ca-key.pem"), + certs_dir.clone().join("ca.pem"), + None, + ) + .unwrap(); + + let ca_keypair = KeyPair::from_pem(&String::from_utf8_lossy(&ca_key)).unwrap(); + let ca = ( + &ca_keypair, + &CertificateParams::from_ca_cert_pem(&String::from_utf8_lossy(&ca_cert.clone())) + .unwrap() + .self_signed(&ca_keypair) + .unwrap(), + ); + + let key_path = certs_dir.clone().join("client-key.pem"); + let cert_path = certs_dir.clone().join("client.pem"); + let (client_key, client_cert) = + generate_certificate("test", key_path.clone(), cert_path.clone(), Some(ca)).unwrap(); + + assert_eq!(client_key, fs::read(key_path).unwrap()); + assert_eq!(client_cert, fs::read(cert_path).unwrap()); + + fs::remove_dir_all(certs_dir).unwrap(); + } +} diff --git a/src/grpc/transformers.rs b/src/grpc/transformers.rs index f277712..19a8767 100644 --- a/src/grpc/transformers.rs +++ b/src/grpc/transformers.rs @@ -20,7 +20,7 @@ impl From for hold::Invoice { fn from(value: HoldInvoice) -> Self { hold::Invoice { id: value.invoice.id, - payment_hash: vec![], + payment_hash: value.invoice.payment_hash, preimage: value.invoice.preimage, bolt11: value.invoice.bolt11, state: transform_invoice_state( diff --git a/src/handler.rs b/src/handler.rs index 9467481..9528f88 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -11,6 +11,7 @@ use tokio::sync::Mutex; const OVERPAYMENT_FACTOR: u64 = 2; +#[derive(Debug)] pub enum Resolution { Resolution(HtlcCallbackResponse), Resolver(Resolver), @@ -203,3 +204,406 @@ where } } } + +#[cfg(test)] +mod test { + use crate::database::helpers::invoice_helper::InvoiceHelper; + use crate::database::model::{ + HoldInvoice, HtlcInsertable, Invoice, InvoiceInsertable, InvoiceState, + }; + use crate::handler::{Handler, Resolution}; + use crate::hooks::{FailureMessage, Htlc, HtlcCallbackRequest, HtlcCallbackResponse, Onion}; + use crate::settler::Settler; + use anyhow::Result; + use lightning_invoice::Bolt11Invoice; + use mockall::mock; + use std::str::FromStr; + + const INVOICE: &str = "lnbc10n1pnvfs4vsp57npt9tx2glnkx29ng98cmc0lt0as8se4x4776rtwqp3gr3hj807qpp5ysnte2hh3nv4z0jd4pfe5wla956zxxg3rmxs5ux4v0xfwplvlm8sdqdw3jhxar5v4ehgxqyjw5qcqpjrzjq2rnwvp7zt9cgeparuqcrqft2kd9dm6a0z6vg0gucrqurutaezrjyrze2uqq2wcqqyqqqqdyqqqqqpqqvs9qxpqysgqjkdwjjuzfy5ek4k9xgsv0ysrc3lg349caqqh3yearxmv4zgyqhqyuntk4gyjpvpezcc66v5lyzxm240wdfgp6cqkwt7fv2nngjjnlrspmaakpk"; + + mock! { + InvoiceHelper {} + + impl Clone for InvoiceHelper { + fn clone(&self) -> Self; + } + + impl InvoiceHelper for InvoiceHelper { + fn insert(&self, invoice: &InvoiceInsertable) -> Result; + fn insert_htlc(&self, htlc: &HtlcInsertable) -> Result; + fn set_invoice_state(&self, id: i64, state: InvoiceState) -> Result; + fn set_invoice_preimage(&self, id: i64, preimage: &[u8]) -> Result; + fn set_htlc_state_by_id(&self, htlc_id: i64, state: InvoiceState) -> Result; + fn set_htlc_states_by_invoice(&self, invoice_id: i64, state: InvoiceState) -> Result; + fn get_all(&self) -> Result>; + fn get_paginated(&self, index_start: i64, limit: u64) -> Result>; + fn get_by_payment_hash(&self, payment_hash: &[u8]) -> Result>; + } + } + + #[tokio::test] + async fn no_invoice() { + let mut helper = MockInvoiceHelper::new(); + helper.expect_get_by_payment_hash().returning(|_| Ok(None)); + + let mut handler = Handler::new(helper, Settler::new(MockInvoiceHelper::new(), 0)); + + let res = handler + .htlc_accepted(HtlcCallbackRequest { + onion: Onion::default(), + htlc: Htlc { + short_channel_id: "".to_string(), + id: 0, + amount_msat: 0, + cltv_expiry: 0, + cltv_expiry_relative: 0, + payment_hash: "00".to_string(), + }, + forward_to: None, + }) + .await; + + match res { + Resolution::Resolution(res) => { + assert_eq!(res, HtlcCallbackResponse::Continue); + } + Resolution::Resolver(_) => { + assert!(false); + } + }; + } + + #[tokio::test] + async fn invoice_not_unpaid() { + let mut helper = MockInvoiceHelper::new(); + helper.expect_get_by_payment_hash().returning(|_| { + Ok(Some(HoldInvoice { + invoice: Invoice { + id: 0, + payment_hash: vec![], + preimage: None, + bolt11: "".to_string(), + state: InvoiceState::Paid.to_string(), + created_at: Default::default(), + }, + htlcs: vec![], + })) + }); + helper.expect_insert_htlc().returning(|_| Ok(0)); + + let mut handler = Handler::new(helper, Settler::new(MockInvoiceHelper::new(), 0)); + + let res = handler + .htlc_accepted(HtlcCallbackRequest { + onion: Onion::default(), + htlc: Htlc { + short_channel_id: "".to_string(), + id: 0, + amount_msat: 0, + cltv_expiry: 0, + cltv_expiry_relative: 0, + payment_hash: "00".to_string(), + }, + forward_to: None, + }) + .await; + + match res { + Resolution::Resolution(res) => { + assert_eq!( + res, + HtlcCallbackResponse::Fail { + failure_message: FailureMessage::IncorrectPaymentDetails + } + ); + } + Resolution::Resolver(_) => { + assert!(false); + } + }; + } + + #[tokio::test] + async fn invoice_incorrect_payment_secret() { + let mut helper = MockInvoiceHelper::new(); + helper.expect_get_by_payment_hash().returning(|_| { + Ok(Some(HoldInvoice { + invoice: Invoice { + id: 0, + payment_hash: vec![], + preimage: None, + bolt11: INVOICE.to_string(), + state: InvoiceState::Unpaid.to_string(), + created_at: Default::default(), + }, + htlcs: vec![], + })) + }); + helper.expect_insert_htlc().returning(|_| Ok(0)); + + let mut handler = Handler::new(helper, Settler::new(MockInvoiceHelper::new(), 0)); + + let res = handler + .htlc_accepted(HtlcCallbackRequest { + onion: Onion { + payload: "".to_string(), + type_field: "".to_string(), + forward_msat: 0, + outgoing_cltv_value: 0, + total_msat: None, + next_onion: "".to_string(), + shared_secret: None, + payment_secret: None, + }, + htlc: Htlc { + short_channel_id: "".to_string(), + id: 0, + amount_msat: 0, + cltv_expiry: 0, + cltv_expiry_relative: 0, + payment_hash: "00".to_string(), + }, + forward_to: None, + }) + .await; + + match res { + Resolution::Resolution(res) => { + assert_eq!( + res, + HtlcCallbackResponse::Fail { + failure_message: FailureMessage::IncorrectPaymentDetails + } + ); + } + Resolution::Resolver(_) => { + assert!(false); + } + }; + } + + #[tokio::test] + async fn invoice_too_little_cltv() { + let mut helper = MockInvoiceHelper::new(); + helper.expect_get_by_payment_hash().returning(|_| { + Ok(Some(HoldInvoice { + invoice: Invoice { + id: 0, + payment_hash: vec![], + preimage: None, + bolt11: INVOICE.to_string(), + state: InvoiceState::Unpaid.to_string(), + created_at: Default::default(), + }, + htlcs: vec![], + })) + }); + helper.expect_insert_htlc().returning(|_| Ok(0)); + + let mut handler = Handler::new(helper, Settler::new(MockInvoiceHelper::new(), 0)); + + let res = handler + .htlc_accepted(HtlcCallbackRequest { + onion: Onion { + payload: "".to_string(), + type_field: "".to_string(), + forward_msat: 0, + outgoing_cltv_value: 0, + total_msat: None, + next_onion: "".to_string(), + shared_secret: None, + payment_secret: Some( + "f4c2b2acca47e76328b3414f8de1ff5bfb03c335357ded0d6e006281c6f23bfc" + .to_string(), + ), + }, + htlc: Htlc { + short_channel_id: "".to_string(), + id: 0, + amount_msat: 0, + cltv_expiry: 0, + cltv_expiry_relative: 2, + payment_hash: "00".to_string(), + }, + forward_to: None, + }) + .await; + + match res { + Resolution::Resolution(res) => { + assert_eq!( + res, + HtlcCallbackResponse::Fail { + failure_message: FailureMessage::IncorrectPaymentDetails + } + ); + } + Resolution::Resolver(_) => { + assert!(false); + } + }; + } + + #[tokio::test] + async fn overpayment_rejection() { + let mut helper = MockInvoiceHelper::new(); + helper.expect_get_by_payment_hash().returning(|_| { + Ok(Some(HoldInvoice { + invoice: Invoice { + id: 0, + payment_hash: vec![], + preimage: None, + bolt11: INVOICE.to_string(), + state: InvoiceState::Unpaid.to_string(), + created_at: Default::default(), + }, + htlcs: vec![], + })) + }); + helper.expect_insert_htlc().returning(|_| Ok(0)); + + let mut handler = Handler::new(helper, Settler::new(MockInvoiceHelper::new(), 0)); + + let res = handler + .htlc_accepted(HtlcCallbackRequest { + onion: Onion { + payload: "".to_string(), + type_field: "".to_string(), + forward_msat: 0, + outgoing_cltv_value: 0, + total_msat: None, + next_onion: "".to_string(), + shared_secret: None, + payment_secret: Some( + "f4c2b2acca47e76328b3414f8de1ff5bfb03c335357ded0d6e006281c6f23bfc" + .to_string(), + ), + }, + htlc: Htlc { + short_channel_id: "".to_string(), + id: 0, + amount_msat: 21_000, + cltv_expiry: 0, + cltv_expiry_relative: 18, + payment_hash: "00".to_string(), + }, + forward_to: None, + }) + .await; + + match res { + Resolution::Resolution(res) => { + assert_eq!( + res, + HtlcCallbackResponse::Fail { + failure_message: FailureMessage::IncorrectPaymentDetails + } + ); + } + Resolution::Resolver(_) => { + assert!(false); + } + }; + } + + #[tokio::test] + async fn accept_full_amount() { + let invoice_decoded = Bolt11Invoice::from_str(INVOICE).unwrap(); + let payment_hash = invoice_decoded.payment_hash()[..].to_vec(); + let payment_hash_cp = payment_hash.clone(); + + let mut helper = MockInvoiceHelper::new(); + helper.expect_get_by_payment_hash().returning(move |_| { + Ok(Some(HoldInvoice { + invoice: Invoice { + id: 0, + preimage: None, + bolt11: INVOICE.to_string(), + created_at: Default::default(), + payment_hash: payment_hash_cp.clone(), + state: InvoiceState::Unpaid.to_string(), + }, + htlcs: vec![], + })) + }); + helper.expect_insert_htlc().returning(|_| Ok(0)); + + let payment_hash_cp_settler = payment_hash.clone(); + + let mut helper_settler = MockInvoiceHelper::new(); + helper_settler + .expect_get_by_payment_hash() + .returning(move |_| { + Ok(Some(HoldInvoice { + invoice: Invoice { + id: 0, + preimage: None, + bolt11: INVOICE.to_string(), + created_at: Default::default(), + state: InvoiceState::Unpaid.to_string(), + payment_hash: payment_hash_cp_settler.clone(), + }, + htlcs: vec![], + })) + }); + helper_settler + .expect_set_htlc_states_by_invoice() + .returning(|_, _| Ok(0)); + helper_settler + .expect_set_invoice_state() + .returning(|_, _| Ok(0)); + helper_settler + .expect_set_invoice_preimage() + .returning(|_, _| Ok(0)); + + let mut handler = Handler::new(helper, Settler::new(helper_settler, 0)); + + let res = handler + .htlc_accepted(HtlcCallbackRequest { + onion: Onion { + payload: "".to_string(), + type_field: "".to_string(), + forward_msat: 0, + outgoing_cltv_value: 0, + total_msat: None, + next_onion: "".to_string(), + shared_secret: None, + payment_secret: Some( + "f4c2b2acca47e76328b3414f8de1ff5bfb03c335357ded0d6e006281c6f23bfc" + .to_string(), + ), + }, + htlc: Htlc { + short_channel_id: "".to_string(), + id: 0, + amount_msat: 1_000, + cltv_expiry: 0, + cltv_expiry_relative: 18, + payment_hash: hex::encode(payment_hash.clone()), + }, + forward_to: None, + }) + .await; + + match res { + Resolution::Resolution(_) => { + assert!(false); + } + Resolution::Resolver(res) => { + let preimage = &hex::decode("0011").unwrap(); + handler + .settler + .settle(&payment_hash, preimage) + .await + .unwrap(); + + assert_eq!( + res.await.unwrap(), + HtlcCallbackResponse::Resolve { + payment_key: hex::encode(preimage) + } + ); + } + }; + } +} diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index 2f3ff85..52a8375 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -1,4 +1,5 @@ use crate::database::helpers::invoice_helper::InvoiceHelper; +use crate::encoder::InvoiceEncoder; use crate::handler::Resolution; use crate::State; use anyhow::Result; @@ -15,7 +16,7 @@ pub struct HtlcCallbackRequest { } #[allow(dead_code)] -#[derive(Debug, Deserialize)] +#[derive(Default, Debug, Deserialize)] pub struct Onion { pub payload: String, #[serde(rename = "type")] @@ -39,7 +40,7 @@ pub struct Htlc { pub payment_hash: String, } -#[derive(Debug, Serialize)] +#[derive(Debug, PartialEq, Serialize)] pub enum FailureMessage { #[serde(rename = "0017")] MppTimeout, @@ -47,7 +48,7 @@ pub enum FailureMessage { IncorrectPaymentDetails, } -#[derive(Debug, Serialize)] +#[derive(Debug, PartialEq, Serialize)] #[serde(tag = "result")] pub enum HtlcCallbackResponse { #[serde(rename = "continue")] @@ -58,9 +59,10 @@ pub enum HtlcCallbackResponse { Resolve { payment_key: String }, } -pub async fn htlc_accepted(plugin: Plugin>, request: Value) -> Result +pub async fn htlc_accepted(plugin: Plugin>, request: Value) -> Result where T: InvoiceHelper + Sync + Send + Clone, + E: InvoiceEncoder + Sync + Send + Clone, { let args = match serde_json::from_value::(request) { Ok(args) => args, diff --git a/src/main.rs b/src/main.rs index df9e3a6..5e037ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use crate::settler::Settler; use anyhow::Result; use cln_plugin::{Builder, RpcMethodBuilder}; use log::{debug, error, info}; +use tokio_util::sync::CancellationToken; mod commands; mod config; @@ -17,10 +18,10 @@ mod settler; mod utils; #[derive(Clone)] -struct State { +struct State { handler: Handler, settler: Settler, - encoder: Encoder, + encoder: E, invoice_helper: T, } @@ -149,9 +150,12 @@ async fn main() -> Result<()> { }) .await?; + let cancellation_token = CancellationToken::new(); + let grpc_server = grpc::server::Server::new( &grpc_host, grpc_port, + cancellation_token.clone(), std::env::current_dir()?.join(utils::built_info::PKG_NAME), invoice_helper, encoder, @@ -173,6 +177,8 @@ async fn main() -> Result<()> { } } + cancellation_token.cancel(); + info!("Stopped plugin"); Ok(()) } From 8368516bdaab31564b28c705ed3695d6cc42d358 Mon Sep 17 00:00:00 2001 From: michael1011 Date: Tue, 20 Aug 2024 23:56:21 +0200 Subject: [PATCH 2/3] test: RPC command integration tests --- .github/workflows/ci.yml | 25 ++++++++ .gitignore | 2 + .gitmodules | 3 + Makefile | 39 ++++++++++++ db-start.sh | 1 - db-stop.sh | 1 - regtest | 1 + src/config.rs | 2 +- src/grpc/tls.rs | 2 +- src/main.rs | 10 ++- tests/poetry.lock | 128 +++++++++++++++++++++++++++++++++++++++ tests/pyproject.toml | 20 ++++++ tests/test_rpc.py | 117 +++++++++++++++++++++++++++++++++++ tests/utils.py | 66 ++++++++++++++++++++ 14 files changed, 412 insertions(+), 5 deletions(-) create mode 100644 .gitmodules create mode 100644 Makefile delete mode 100755 db-start.sh delete mode 100755 db-stop.sh create mode 160000 regtest create mode 100644 tests/poetry.lock create mode 100644 tests/pyproject.toml create mode 100644 tests/test_rpc.py create mode 100644 tests/utils.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29c2ddb..65f0d9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ jobs: strategy: matrix: platform: [ ubuntu-latest ] + python-version: [ '3.10' ] rust-version: [ stable, nightly ] runs-on: ${{ matrix.platform }} @@ -38,3 +39,27 @@ jobs: - name: Unit tests run: cargo test + + - name: Install Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: abatilo/actions-poetry@v3 + + - name: Install Python dependencies + run: make python-install + + - name: Python lint + run: make python-lint + + - name: Python formatting + run: poetry run ruff format --check + working-directory: ./tests + + - name: Regtest startup + run: make regtest-start + + - name: Integration tests + run: make integration-tests diff --git a/.gitignore b/.gitignore index 5a8bfa6..cb4f54e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ target/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ + +**/__pycache__ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9c27e0c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "regtest"] + path = regtest + url = https://github.com/BoltzExchange/regtest diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0b35eca --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +build: + cargo build + +build-release: + cargo build --release + +python-install: + cd tests && poetry install + +python-lint: + cd tests && poetry run ruff check + +python-format: + cd tests && poetry run ruff format + +regtest-start: + git submodule init + git submodule update + chmod -R 777 regtest + cd regtest && COMPOSE_PROFILES=ci ./start.sh + cd .. + mkdir regtest/data/cln1/plugins + cp target/debug/hold regtest/data/cln1/plugins/ + docker exec boltz-cln-1 lightning-cli --regtest plugin stop /root/hold.sh + rm -rf regtest/data/cln1/regtest/hold/ + docker exec boltz-cln-1 lightning-cli --regtest plugin start /root/.lightning/plugins/hold + +regtest-stop: + cd regtest && ./stop.sh + +db-start: + docker run --name hold-db --rm -e POSTGRES_DB=hold -e POSTGRES_USER=hold -e POSTGRES_PASSWORD=hold \ + -d -p 5433:5432 postgres:14-alpine + +db-stop: + docker stop hold-db + +integration-tests: + cd tests && poetry run pytest diff --git a/db-start.sh b/db-start.sh deleted file mode 100755 index 927b3cd..0000000 --- a/db-start.sh +++ /dev/null @@ -1 +0,0 @@ -docker run --name hold-db --rm -e POSTGRES_DB=hold -e POSTGRES_USER=hold -e POSTGRES_PASSWORD=hold -d -p 5433:5432 postgres:14-alpine diff --git a/db-stop.sh b/db-stop.sh deleted file mode 100755 index b0ab725..0000000 --- a/db-stop.sh +++ /dev/null @@ -1 +0,0 @@ -docker stop hold-db diff --git a/regtest b/regtest new file mode 160000 index 0000000..dd1b4f3 --- /dev/null +++ b/regtest @@ -0,0 +1 @@ +Subproject commit dd1b4f399cf024e0f13a4e667cfc1feae18090f4 diff --git a/src/config.rs b/src/config.rs index a626f74..7ac0f18 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use cln_plugin::options; pub const OPTION_DATABASE: options::DefaultStringConfigOption = options::ConfigOption::new_str_with_default( "hold-database", - "sqlite://./hold/hold.sqlite", + "sqlite://./hold/hold.sqlite3", "hold database", ); diff --git a/src/grpc/tls.rs b/src/grpc/tls.rs index 628e329..29d705f 100644 --- a/src/grpc/tls.rs +++ b/src/grpc/tls.rs @@ -28,7 +28,7 @@ pub fn load_certificates(base_path: PathBuf) -> Result<(Identity, Certificate)> generate_or_load_certificate("Hold gRPC server", base, "server", Some(ca))?; generate_or_load_certificate("Hold gRPC client", base, "client", Some(ca))?; - trace!("Loaded certificates"); + debug!("Loaded certificates"); Ok(( Identity::from_pem(server_cert, server_key), Certificate::from_pem(ca_cert), diff --git a/src/main.rs b/src/main.rs index 5e037ca..cd314f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,8 @@ use crate::settler::Settler; use anyhow::Result; use cln_plugin::{Builder, RpcMethodBuilder}; use log::{debug, error, info}; +use std::fs; +use std::path::Path; use tokio_util::sync::CancellationToken; mod commands; @@ -117,6 +119,13 @@ async fn main() -> Result<()> { } }; + let config = plugin.configuration(); + + let plugin_dir = Path::new(config.lightning_dir.as_str()).join("hold"); + if !plugin_dir.exists() { + fs::create_dir(plugin_dir)?; + } + let db = match database::connect(&db_url) { Ok(db) => db, Err(err) => { @@ -127,7 +136,6 @@ async fn main() -> Result<()> { } }; - let config = plugin.configuration(); let encoder = match Encoder::new(&config.rpc_file, &config.network).await { Ok(res) => res, Err(err) => { diff --git a/tests/poetry.lock b/tests/poetry.lock new file mode 100644 index 0000000..85049b9 --- /dev/null +++ b/tests/poetry.lock @@ -0,0 +1,128 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "ruff" +version = "0.6.1" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:b4bb7de6a24169dc023f992718a9417380301b0c2da0fe85919f47264fb8add9"}, + {file = "ruff-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45efaae53b360c81043e311cdec8a7696420b3d3e8935202c2846e7a97d4edae"}, + {file = "ruff-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bc60c7d71b732c8fa73cf995efc0c836a2fd8b9810e115be8babb24ae87e0850"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c7477c3b9da822e2db0b4e0b59e61b8a23e87886e727b327e7dcaf06213c5cf"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a0af7ab3f86e3dc9f157a928e08e26c4b40707d0612b01cd577cc84b8905cc9"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392688dbb50fecf1bf7126731c90c11a9df1c3a4cdc3f481b53e851da5634fa5"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5278d3e095ccc8c30430bcc9bc550f778790acc211865520f3041910a28d0024"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6d5f65d6f276ee7a0fc50a0cecaccb362d30ef98a110f99cac1c7872df2f18"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e0dd11e2ae553ee5c92a81731d88a9883af8db7408db47fc81887c1f8b672e"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d812615525a34ecfc07fd93f906ef5b93656be01dfae9a819e31caa6cfe758a1"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faaa4060f4064c3b7aaaa27328080c932fa142786f8142aff095b42b6a2eb631"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99d7ae0df47c62729d58765c593ea54c2546d5de213f2af2a19442d50a10cec9"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9eb18dfd7b613eec000e3738b3f0e4398bf0153cb80bfa3e351b3c1c2f6d7b15"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c62bc04c6723a81e25e71715aa59489f15034d69bf641df88cb38bdc32fd1dbb"}, + {file = "ruff-0.6.1-py3-none-win32.whl", hash = "sha256:9fb4c4e8b83f19c9477a8745e56d2eeef07a7ff50b68a6998f7d9e2e3887bdc4"}, + {file = "ruff-0.6.1-py3-none-win_amd64.whl", hash = "sha256:c2ebfc8f51ef4aca05dad4552bbcf6fe8d1f75b2f6af546cc47cc1c1ca916b5b"}, + {file = "ruff-0.6.1-py3-none-win_arm64.whl", hash = "sha256:3bc81074971b0ffad1bd0c52284b22411f02a11a012082a76ac6da153536e014"}, + {file = "ruff-0.6.1.tar.gz", hash = "sha256:af3ffd8c6563acb8848d33cd19a69b9bfe943667f0419ca083f8ebe4224a3436"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "1d452508104c22c9af0db099221f49dd2ca3cea667098d160502c4bd53846cdb" diff --git a/tests/pyproject.toml b/tests/pyproject.toml new file mode 100644 index 0000000..3735120 --- /dev/null +++ b/tests/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "tests" +authors = ["Boltz"] +description = "Test for CLN hold invoice plugin" +version = "0.1.0" +license = "MIT" +package-mode = false + +[tool.poetry.dependencies] +python = "^3.10" +pytest = "^8.3.2" +ruff = "^0.6.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff.lint] +select = ["ALL"] +ignore = ["D100", "D101", "D102", "D103", "D107", "D211", "D212", "S605", "D203", "ISC001", "COM812", "S101"] diff --git a/tests/test_rpc.py b/tests/test_rpc.py new file mode 100644 index 0000000..d5ed457 --- /dev/null +++ b/tests/test_rpc.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import time +from typing import Any + +from utils import LndPay, lightning, new_preimage + + +def check_unpaid_invoice( + entry: dict[str, Any], payment_hash: str, invoice: str +) -> None: + assert entry is not None + assert entry["payment_hash"] == payment_hash + assert entry["preimage"] is None + assert entry["bolt11"] == invoice + assert entry["state"] == "unpaid" + assert len(entry["htlcs"]) == 0 + + +class TestRpc: + def test_invoice(self) -> None: + amount = 2112 + (_, payment_hash) = new_preimage() + + invoice = lightning("holdinvoice", payment_hash, f"{amount}") + decoded = lightning("decode", invoice["bolt11"]) + + assert decoded["valid"] + assert decoded["amount_msat"] == amount + assert decoded["payment_hash"] == payment_hash + assert decoded["payee"] == lightning("getinfo")["id"] + assert "payment_secret" in decoded + + def test_list(self) -> None: + (_, payment_hash) = new_preimage() + invoice = lightning("holdinvoice", payment_hash, "1")["bolt11"] + + list_all = lightning("listholdinvoices")["holdinvoices"] + + assert len(list_all) > 1 + + entry = next(e for e in list_all if e["bolt11"] == invoice) + assert entry is not None + check_unpaid_invoice(entry, payment_hash, invoice) + + def test_list_payment_hash(self) -> None: + (_, payment_hash) = new_preimage() + invoice = lightning("holdinvoice", payment_hash, "1")["bolt11"] + + list_entries = lightning("listholdinvoices", payment_hash)["holdinvoices"] + assert len(list_entries) == 1 + check_unpaid_invoice(list_entries[0], payment_hash, invoice) + + def test_list_invoice(self) -> None: + (_, payment_hash) = new_preimage() + invoice = lightning("holdinvoice", payment_hash, "1")["bolt11"] + + list_entries = lightning("listholdinvoices", "null", invoice)["holdinvoices"] + assert len(list_entries) == 1 + check_unpaid_invoice(list_entries[0], payment_hash, invoice) + + def test_settle(self) -> None: + amount = 1_000 + (preimage, payment_hash) = new_preimage() + + invoice = lightning("holdinvoice", payment_hash, f"{amount}")["bolt11"] + + payer = LndPay(1, invoice) + payer.start() + time.sleep(2) + + data = lightning("listholdinvoices", payment_hash)["holdinvoices"][0] + assert data["state"] == "accepted" + + htlcs = data["htlcs"] + assert len(htlcs) == 1 + assert htlcs[0]["state"] == "accepted" + assert htlcs[0]["msat"] == amount + + lightning("settleholdinvoice", preimage) + + payer.join() + assert payer.res["status"] == "SUCCEEDED" + + data = lightning("listholdinvoices", payment_hash)["holdinvoices"][0] + assert data["state"] == "paid" + + htlcs = data["htlcs"] + assert len(htlcs) == 1 + assert htlcs[0]["state"] == "paid" + + def test_cancel(self) -> None: + (_, payment_hash) = new_preimage() + invoice = lightning("holdinvoice", payment_hash, "1000")["bolt11"] + + payer = LndPay(1, invoice) + payer.start() + time.sleep(2) + + data = lightning("listholdinvoices", payment_hash)["holdinvoices"][0] + assert data["state"] == "accepted" + + htlcs = data["htlcs"] + assert len(htlcs) == 1 + assert htlcs[0]["state"] == "accepted" + + lightning("cancelholdinvoice", payment_hash) + + payer.join() + assert payer.res["status"] == "FAILED" + + data = lightning("listholdinvoices", payment_hash)["holdinvoices"][0] + assert data["state"] == "cancelled" + + htlcs = data["htlcs"] + assert len(htlcs) == 1 + assert htlcs[0]["state"] == "cancelled" diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..e742081 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import json +import os +from hashlib import sha256 +from threading import Thread +from typing import Any + + +def lightning(*args: str, node: int = 1) -> dict[str, Any]: + return json.load( + os.popen( + f"docker exec boltz-cln-{node} lightning-cli --regtest {' '.join(args)}", + ), + ) + + +def lnd(*args: str, node: int = 1) -> dict[str, Any]: + return json.loads(lnd_raw(*args, node=node)) + + +def lnd_raw(*args: str, node: int = 1) -> str: + return os.popen( + f"docker exec boltz-lnd-{node} lncli -n regtest {' '.join(args)}" + ).read() + + +def new_preimage() -> tuple[str, str]: + preimage = os.urandom(32) + return preimage.hex(), sha256(preimage).hexdigest() + + +class LndPay(Thread): + res: dict[str, Any] = None + + def __init__( + self, + node: int, + invoice: str, + max_shard_size: int | None = None, + outgoing_chan_id: str | None = None, + timeout: int | None = None, + ) -> None: + Thread.__init__(self) + + self.node = node + self.timeout = timeout + self.invoice = invoice + self.max_shard_size = max_shard_size + self.outgoing_chan_id = outgoing_chan_id + + def run(self) -> None: + cmd = "payinvoice --force --json" + + if self.outgoing_chan_id is not None: + cmd += f" --outgoing_chan_id {self.outgoing_chan_id}" + + if self.max_shard_size is not None: + cmd += f" --max_shard_size_sat {self.max_shard_size}" + + if self.timeout is not None: + cmd += f" --timeout {self.timeout}s" + + res = lnd_raw(f"{cmd} {self.invoice} 2> /dev/null", node=self.node) + res = res[res.find("{") :] + self.res = json.loads(res) From 67b7437ec0b79ff9915370b168128861de4b3fd9 Mon Sep 17 00:00:00 2001 From: michael1011 Date: Wed, 21 Aug 2024 02:26:39 +0200 Subject: [PATCH 3/3] test: gRPC integration tests --- .gitignore | 2 + Makefile | 25 +- src/commands/invoice.rs | 10 +- src/database/helpers/invoice_helper.rs | 3 +- src/grpc/server.rs | 16 +- src/grpc/service.rs | 49 +++- src/main.rs | 1 + src/settler.rs | 30 +- tests/hold/__init__.py | 0 tests/hold/protos/__init__.py | 4 + tests/hold/test_grpc.py | 392 +++++++++++++++++++++++++ tests/{ => hold}/test_rpc.py | 8 +- tests/{ => hold}/utils.py | 22 +- tests/poetry.lock | 156 +++++++++- tests/pyproject.toml | 13 +- 15 files changed, 683 insertions(+), 48 deletions(-) create mode 100644 tests/hold/__init__.py create mode 100644 tests/hold/protos/__init__.py create mode 100644 tests/hold/test_grpc.py rename tests/{ => hold}/test_rpc.py (96%) rename tests/{ => hold}/utils.py (84%) diff --git a/.gitignore b/.gitignore index cb4f54e..8ae67c1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ target/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ +tests/hold/protos/* +!tests/hold/protos/__init__.py **/__pycache__ diff --git a/Makefile b/Makefile index 0b35eca..8247333 100644 --- a/Makefile +++ b/Makefile @@ -13,17 +13,28 @@ python-lint: python-format: cd tests && poetry run ruff format +python-protos: + cd tests && poetry run python -m grpc_tools.protoc -I ../protos \ + --python_out=hold/protos \ + --pyi_out=hold/protos \ + --grpc_python_out=hold/protos \ + ../protos/hold.proto + regtest-start: git submodule init git submodule update - chmod -R 777 regtest + chmod -R 777 regtest 2> /dev/null || true cd regtest && COMPOSE_PROFILES=ci ./start.sh - cd .. - mkdir regtest/data/cln1/plugins - cp target/debug/hold regtest/data/cln1/plugins/ - docker exec boltz-cln-1 lightning-cli --regtest plugin stop /root/hold.sh - rm -rf regtest/data/cln1/regtest/hold/ - docker exec boltz-cln-1 lightning-cli --regtest plugin start /root/.lightning/plugins/hold + mkdir regtest/data/cln2/plugins + cp target/debug/hold regtest/data/cln2/plugins/ + docker exec boltz-cln-2 lightning-cli --regtest plugin stop /root/hold.sh + rm -rf regtest/data/cln2/regtest/hold/ + docker exec boltz-cln-2 lightning-cli --regtest plugin start /root/.lightning/plugins/hold + + sleep 1 + docker exec boltz-cln-2 chmod 777 -R /root/.lightning/regtest/hold + + make python-protos regtest-stop: cd regtest && ./stop.sh diff --git a/src/commands/invoice.rs b/src/commands/invoice.rs index 0ce9bdb..64a5ff6 100644 --- a/src/commands/invoice.rs +++ b/src/commands/invoice.rs @@ -5,7 +5,6 @@ use crate::encoder::{InvoiceBuilder, InvoiceEncoder}; use crate::State; use anyhow::Result; use cln_plugin::Plugin; -use log::info; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::fmt::Debug; @@ -52,11 +51,10 @@ where payment_hash: payment_hash.clone(), state: InvoiceState::Unpaid.into(), })?; - info!( - "Added hold invoice {} for {}msat", - hex::encode(payment_hash), - params.amount - ); + plugin + .state() + .settler + .new_invoice(invoice.clone(), payment_hash, params.amount); Ok(serde_json::to_value(&InvoiceResponse { bolt11: invoice })?) } diff --git a/src/database/helpers/invoice_helper.rs b/src/database/helpers/invoice_helper.rs index edfc313..f44edd9 100644 --- a/src/database/helpers/invoice_helper.rs +++ b/src/database/helpers/invoice_helper.rs @@ -76,6 +76,7 @@ impl InvoiceHelper for InvoiceHelperDatabase { let invoices = invoices::dsl::invoices .select(Invoice::as_select()) + .order_by(invoices::dsl::id) .load(&mut con)?; let htlcs = Htlc::belonging_to(&invoices) .select(Htlc::as_select()) @@ -95,7 +96,7 @@ impl InvoiceHelper for InvoiceHelperDatabase { let invoices = invoices::dsl::invoices .select(Invoice::as_select()) .filter(invoices::dsl::id.ge(index_start)) - .order_by(invoices::dsl::id.desc()) + .order_by(invoices::dsl::id) .limit(limit as i64) .load(&mut con)?; let htlcs = Htlc::belonging_to(&invoices) diff --git a/src/grpc/server.rs b/src/grpc/server.rs index 10006d1..5aa422a 100644 --- a/src/grpc/server.rs +++ b/src/grpc/server.rs @@ -15,6 +15,8 @@ use tonic::transport::ServerTlsConfig; pub struct Server { host: String, port: i64, + is_regtest: bool, + directory: PathBuf, cancellation_token: CancellationToken, @@ -28,9 +30,11 @@ where T: InvoiceHelper + Sync + Send + Clone + 'static, E: InvoiceEncoder + Sync + Send + Clone + 'static, { + #[allow(clippy::too_many_arguments)] pub fn new( host: &str, port: i64, + is_regtest: bool, cancellation_token: CancellationToken, directory: PathBuf, invoice_helper: T, @@ -42,6 +46,7 @@ where settler, encoder, directory, + is_regtest, invoice_helper, cancellation_token, host: host.to_string(), @@ -49,7 +54,15 @@ where } pub async fn start(&self) -> Result<()> { - let socket_addr = SocketAddr::new(IpAddr::from_str(self.host.as_str())?, self.port as u16); + // Always listen to all interfaces on regtest + let socket_addr = SocketAddr::new( + IpAddr::from_str(if !self.is_regtest { + self.host.as_str() + } else { + "0.0.0.0" + })?, + self.port as u16, + ); info!("Starting gRPC server on: {}", socket_addr); let (identity, ca) = load_certificates(self.directory.clone())?; @@ -201,6 +214,7 @@ mod test { let server = Server::new( "127.0.0.1", port, + false, token.clone(), certs_dir.clone(), make_mock_invoice_helper(), diff --git a/src/grpc/service.rs b/src/grpc/service.rs index b2f8df0..a9276d3 100644 --- a/src/grpc/service.rs +++ b/src/grpc/service.rs @@ -12,7 +12,7 @@ use crate::grpc::service::hold::{ use crate::grpc::transformers::{transform_invoice_state, transform_route_hints}; use crate::settler::Settler; use bitcoin::hashes::{sha256, Hash}; -use log::{error, info}; +use log::{debug, error}; use std::pin::Pin; use tokio::sync::mpsc; use tonic::codegen::tokio_stream::wrappers::ReceiverStream; @@ -114,11 +114,8 @@ where )); } - info!( - "Added hold invoice {} for {}msat", - hex::encode(params.payment_hash), - params.amount_msat - ); + self.settler + .new_invoice(invoice.clone(), params.payment_hash, params.amount_msat); Ok(Response::new(InvoiceResponse { bolt11: invoice })) } @@ -206,6 +203,36 @@ where let mut state_rx = self.settler.state_rx(); + match self + .invoice_helper + .get_by_payment_hash(¶ms.payment_hash) + { + Ok(res) => { + if let Some(res) = res { + if let Ok(state) = InvoiceState::try_from(res.invoice.state.as_str()) { + if let Err(err) = tx + .send(Ok(TrackResponse { + state: transform_invoice_state(state), + })) + .await + { + error!("Could not send invoice state update: {}", err); + return Err(Status::new( + Code::Internal, + format!("could not send initial invoice state: {}", err), + )); + } + } + } + } + Err(err) => { + return Err(Status::new( + Code::Internal, + format!("could not fetch invoice state from database: {}", err), + )); + } + }; + tokio::spawn(async move { loop { match state_rx.recv().await { @@ -220,7 +247,7 @@ where })) .await { - error!("Could not send invoice state update: {}", err); + debug!("Could not send invoice state update: {}", err); break; }; @@ -261,16 +288,12 @@ where })) .await { - error!("Could not send invoice state update: {}", err); + debug!("Could not send all invoices state update: {}", err); break; }; - - if update.state.is_final() { - break; - } } Err(err) => { - error!("Waiting for invoice state updates failed: {}", err); + error!("Waiting for all invoices state updates failed: {}", err); break; } } diff --git a/src/main.rs b/src/main.rs index cd314f2..05e1c39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -163,6 +163,7 @@ async fn main() -> Result<()> { let grpc_server = grpc::server::Server::new( &grpc_host, grpc_port, + config.network == "regtest", cancellation_token.clone(), std::env::current_dir()?.join(utils::built_info::PKG_NAME), invoice_helper, diff --git a/src/settler.rs b/src/settler.rs index f87bdf0..90628a8 100644 --- a/src/settler.rs +++ b/src/settler.rs @@ -63,6 +63,8 @@ pub struct Settler { pending_htlcs: Arc, Vec>>>, } +// TODO: only allow valid state transitions + impl Settler where T: InvoiceHelper + Sync + Send + Clone, @@ -81,6 +83,20 @@ where self.state_tx.subscribe() } + pub fn new_invoice(&self, invoice: String, payment_hash: Vec, amount_msat: u64) { + info!( + "Added hold invoice {} for {}msat", + hex::encode(payment_hash.clone()), + amount_msat + ); + + let _ = self.state_tx.send(StateUpdate { + payment_hash, + bolt11: invoice, + state: InvoiceState::Unpaid, + }); + } + pub fn set_accepted(&self, invoice: &Invoice, num_htlcs: usize) -> Result<()> { info!( "Accepted hold invoice {} with {} HTLCs", @@ -90,7 +106,7 @@ where self.invoice_helper .set_invoice_state(invoice.id, InvoiceState::Accepted)?; let _ = self.state_tx.send(StateUpdate { - state: InvoiceState::Paid, + state: InvoiceState::Accepted, bolt11: invoice.bolt11.clone(), payment_hash: invoice.payment_hash.clone(), }); @@ -161,10 +177,12 @@ where } pub async fn cancel(&mut self, payment_hash: &Vec) -> Result<()> { - let htlcs = match self.pending_htlcs.lock().await.remove(payment_hash) { - Some(res) => res, - None => return Err(SettleError::InvoiceNotFound.into()), - }; + let htlcs = self + .pending_htlcs + .lock() + .await + .remove(payment_hash) + .unwrap_or_else(Vec::new); let htlc_count = htlcs.len(); for htlc in htlcs { @@ -180,7 +198,7 @@ where payment_hash: payment_hash.clone(), }); info!( - "Cancelled hold invoice {} with {} HTLCs", + "Cancelled hold invoice {} with pending {} HTLCs", hex::encode(payment_hash), htlc_count ); diff --git a/tests/hold/__init__.py b/tests/hold/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/hold/protos/__init__.py b/tests/hold/protos/__init__.py new file mode 100644 index 0000000..1da1dcf --- /dev/null +++ b/tests/hold/protos/__init__.py @@ -0,0 +1,4 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) diff --git a/tests/hold/test_grpc.py b/tests/hold/test_grpc.py new file mode 100644 index 0000000..a5e62e2 --- /dev/null +++ b/tests/hold/test_grpc.py @@ -0,0 +1,392 @@ +from __future__ import annotations + +import concurrent.futures +import time +from pathlib import Path + +import grpc +import pytest + +from hold.protos.hold_pb2 import ( + CancelRequest, + GetInfoRequest, + GetInfoResponse, + Hop, + Invoice, + InvoiceRequest, + InvoiceResponse, + InvoiceState, + ListRequest, + ListResponse, + RoutingHint, + SettleRequest, + TrackAllRequest, + TrackRequest, +) +from hold.protos.hold_pb2_grpc import HoldStub +from hold.utils import LndPay, lightning, new_preimage_bytes, time_now + + +class TestGrpc: + @pytest.fixture(scope="class", autouse=True) + def cl(self) -> HoldStub: + cert_path = Path("../regtest/data/cln2/regtest/hold") + creds = grpc.ssl_channel_credentials( + root_certificates=cert_path.joinpath("ca.pem").read_bytes(), + private_key=cert_path.joinpath("client-key.pem").read_bytes(), + certificate_chain=cert_path.joinpath("client.pem").read_bytes(), + ) + channel = grpc.secure_channel( + "127.0.0.1:9738", + creds, + options=(("grpc.ssl_target_name_override", "hold"),), + ) + client = HoldStub(channel) + + yield client + + channel.close() + + def test_get_info(self, cl: HoldStub) -> None: + info: GetInfoResponse = cl.GetInfo(GetInfoRequest()) + assert info.version != "" + + def test_invoice_defaults(self, cl: HoldStub) -> None: + amount = 21_000 + (_, payment_hash) = new_preimage_bytes() + + invoice: InvoiceResponse = cl.Invoice( + InvoiceRequest(payment_hash=payment_hash, amount_msat=amount) + ) + decoded = lightning("decode", invoice.bolt11) + + assert decoded["currency"] == "bcrt" + assert decoded["created_at"] - int(time_now().timestamp()) < 2 + assert decoded["expiry"] == 3_600 + assert decoded["payee"] == lightning("getinfo")["id"] + assert decoded["amount_msat"] == amount + assert decoded["description"] == "" + assert decoded["min_final_cltv_expiry"] == 80 + assert "payment_secret" in decoded + assert decoded["features"] == "024100" + assert decoded["payment_hash"] == payment_hash.hex() + assert decoded["valid"] + + @pytest.mark.parametrize( + "memo", + [ + "some", + "text", + "Send to BTC address", + "some way longer text with so many chars", + ], + ) + def test_invoice_memo(self, cl: HoldStub, memo: str) -> None: + (_, payment_hash) = new_preimage_bytes() + invoice: InvoiceResponse = cl.Invoice( + InvoiceRequest(payment_hash=payment_hash, amount_msat=1, memo=memo) + ) + decoded = lightning("decode", invoice.bolt11) + + assert decoded["description"] == memo + + def test_invoice_description_hash(self, cl: HoldStub) -> None: + (preimage, payment_hash) = new_preimage_bytes() + invoice: InvoiceResponse = cl.Invoice( + InvoiceRequest(payment_hash=payment_hash, amount_msat=1, hash=preimage) + ) + decoded = lightning("decode", invoice.bolt11) + + assert decoded["description_hash"] == preimage.hex() + + @pytest.mark.parametrize( + "expiry", + [ + 1, + 2, + 3_600, + 7_200, + 10_000, + ], + ) + def test_invoice_expiry(self, cl: HoldStub, expiry: int) -> None: + (_, payment_hash) = new_preimage_bytes() + invoice: InvoiceResponse = cl.Invoice( + InvoiceRequest(payment_hash=payment_hash, amount_msat=1, expiry=expiry) + ) + decoded = lightning("decode", invoice.bolt11) + + assert decoded["expiry"] == expiry + + @pytest.mark.parametrize( + "expiry", + [ + 1, + 2, + 80, + 144, + 288, + ], + ) + def test_invoice_min_final_cltv_expiry(self, cl: HoldStub, expiry: int) -> None: + (_, payment_hash) = new_preimage_bytes() + invoice: InvoiceResponse = cl.Invoice( + InvoiceRequest( + payment_hash=payment_hash, amount_msat=1, min_final_cltv_expiry=expiry + ) + ) + decoded = lightning("decode", invoice.bolt11) + + assert decoded["min_final_cltv_expiry"] == expiry + + def test_invoice_routing_hints(self, cl: HoldStub) -> None: + (_, payment_hash) = new_preimage_bytes() + + hints = [ + RoutingHint( + hops=[ + Hop( + public_key=bytes.fromhex( + "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2" + ), + short_channel_id=123, + base_fee=1, + ppm_fee=2, + cltv_expiry_delta=23, + ), + Hop( + public_key=bytes.fromhex( + "02d96eadea3d780104449aca5c93461ce67c1564e2e1d73225fa67dd3b997a6018" + ), + short_channel_id=321, + base_fee=2, + ppm_fee=21, + cltv_expiry_delta=26, + ), + ] + ), + RoutingHint( + hops=[ + Hop( + public_key=bytes.fromhex( + "027a7666ec63448bacaec5b00398dd263522755e95bcded7b52b2c9dc4533d34f1" + ), + short_channel_id=121, + base_fee=1_000, + ppm_fee=2_500, + cltv_expiry_delta=80, + ) + ] + ), + ] + + invoice: InvoiceResponse = cl.Invoice( + InvoiceRequest( + payment_hash=payment_hash, + amount_msat=1, + routing_hints=hints, + ) + ) + decoded = lightning("decode", invoice.bolt11) + routes = decoded["routes"] + + assert len(routes) == 2 + + assert len(routes[0]) == 2 + assert len(routes[1]) == 1 + + for i in range(len(routes)): + for j in range(len(routes[i])): + decoded_hop = routes[i][j] + hint = hints[i].hops[j] + + assert decoded_hop["pubkey"] == hint.public_key.hex() + assert decoded_hop["short_channel_id"] == f"0x0x{hint.short_channel_id}" + assert decoded_hop["fee_base_msat"] == hint.base_fee + assert decoded_hop["fee_proportional_millionths"] == hint.ppm_fee + assert decoded_hop["cltv_expiry_delta"] == hint.cltv_expiry_delta + + def test_list_all(self, cl: HoldStub) -> None: + cl.Invoice(InvoiceRequest(payment_hash=new_preimage_bytes()[1], amount_msat=1)) + + hold_list: ListResponse = cl.List(ListRequest()) + assert len(hold_list.invoices) > 0 + + def test_list_payment_hash(self, cl: HoldStub) -> None: + (_, payment_hash) = new_preimage_bytes() + invoice: InvoiceResponse = cl.Invoice( + InvoiceRequest(payment_hash=payment_hash, amount_msat=1) + ) + + hold_list: ListResponse = cl.List(ListRequest(payment_hash=payment_hash)) + assert len(hold_list.invoices) == 1 + + assert hold_list.invoices[0].bolt11 == invoice.bolt11 + assert hold_list.invoices[0].payment_hash == payment_hash + + def test_list_payment_hash_not_found(self, cl: HoldStub) -> None: + (_, payment_hash) = new_preimage_bytes() + + hold_list: ListResponse = cl.List(ListRequest(payment_hash=payment_hash)) + assert len(hold_list.invoices) == 0 + + def test_list_pagination(self, cl: HoldStub) -> None: + for _ in range(10): + (_, payment_hash) = new_preimage_bytes() + cl.Invoice(InvoiceRequest(payment_hash=payment_hash, amount_msat=1)) + + page: ListResponse = cl.List( + ListRequest(pagination=ListRequest.Pagination(index_start=0, limit=2)) + ) + assert len(page.invoices) == 2 + assert page.invoices[0].id == 1 + assert page.invoices[1].id == 2 + + page: ListResponse = cl.List( + ListRequest(pagination=ListRequest.Pagination(index_start=2, limit=1)) + ) + assert len(page.invoices) == 1 + assert page.invoices[0].id == 2 + + page: ListResponse = cl.List( + ListRequest(pagination=ListRequest.Pagination(index_start=3, limit=5)) + ) + assert len(page.invoices) == 5 + assert page.invoices[0].id == 3 + + def test_track_settle(self, cl: HoldStub) -> None: + (preimage, payment_hash) = new_preimage_bytes() + invoice: InvoiceResponse = cl.Invoice( + InvoiceRequest(payment_hash=payment_hash, amount_msat=1_000) + ) + + def track_states() -> list[InvoiceState]: + return [ + update.state + for update in cl.Track(TrackRequest(payment_hash=payment_hash)) + ] + + with concurrent.futures.ThreadPoolExecutor() as pool: + fut = pool.submit(track_states) + + pay = LndPay(1, invoice.bolt11) + pay.start() + time.sleep(1) + + invoice_state: Invoice = cl.List( + ListRequest(payment_hash=payment_hash) + ).invoices[0] + assert invoice_state.state == InvoiceState.ACCEPTED + assert len(invoice_state.htlcs) == 1 + assert invoice_state.htlcs[0].state == InvoiceState.ACCEPTED + + cl.Settle(SettleRequest(payment_preimage=preimage)) + pay.join() + + assert fut.result() == [ + InvoiceState.UNPAID, + InvoiceState.ACCEPTED, + InvoiceState.PAID, + ] + + invoice_state = cl.List(ListRequest(payment_hash=payment_hash)).invoices[0] + assert invoice_state.state == InvoiceState.PAID + assert len(invoice_state.htlcs) == 1 + assert invoice_state.htlcs[0].state == InvoiceState.PAID + + def test_track_cancel(self, cl: HoldStub) -> None: + (_, payment_hash) = new_preimage_bytes() + invoice: InvoiceResponse = cl.Invoice( + InvoiceRequest(payment_hash=payment_hash, amount_msat=1_000) + ) + + def track_states() -> list[InvoiceState]: + return [ + update.state + for update in cl.Track(TrackRequest(payment_hash=payment_hash)) + ] + + with concurrent.futures.ThreadPoolExecutor() as pool: + fut = pool.submit(track_states) + + pay = LndPay(1, invoice.bolt11) + pay.start() + time.sleep(1) + + invoice_state: Invoice = cl.List( + ListRequest(payment_hash=payment_hash) + ).invoices[0] + assert invoice_state.state == InvoiceState.ACCEPTED + assert len(invoice_state.htlcs) == 1 + assert invoice_state.htlcs[0].state == InvoiceState.ACCEPTED + + cl.Cancel(CancelRequest(payment_hash=payment_hash)) + pay.join() + + assert fut.result() == [ + InvoiceState.UNPAID, + InvoiceState.ACCEPTED, + InvoiceState.CANCELLED, + ] + + invoice_state = cl.List(ListRequest(payment_hash=payment_hash)).invoices[0] + assert invoice_state.state == InvoiceState.CANCELLED + assert len(invoice_state.htlcs) == 1 + assert invoice_state.htlcs[0].state == InvoiceState.CANCELLED + + def test_track_all(self, cl: HoldStub) -> None: + expected_events = 6 + + def track_states() -> list[tuple[bytes, str, str]]: + evs = [] + + sub = cl.TrackAll(TrackAllRequest()) + for ev in sub: + evs.append((ev.payment_hash, ev.bolt11, ev.state)) + if len(evs) == expected_events: + sub.cancel() + break + + return evs + + with concurrent.futures.ThreadPoolExecutor() as pool: + fut = pool.submit(track_states) + + (_, payment_hash_created) = new_preimage_bytes() + invoice_created: InvoiceResponse = cl.Invoice( + InvoiceRequest(payment_hash=payment_hash_created, amount_msat=1_000) + ) + + (_, payment_hash_cancelled) = new_preimage_bytes() + invoice_cancelled: InvoiceResponse = cl.Invoice( + InvoiceRequest(payment_hash=payment_hash_cancelled, amount_msat=1_000) + ) + + (preimage_settled, payment_hash_settled) = new_preimage_bytes() + invoice_settled: InvoiceResponse = cl.Invoice( + InvoiceRequest(payment_hash=payment_hash_settled, amount_msat=1_000) + ) + + cl.Cancel(CancelRequest(payment_hash=payment_hash_cancelled)) + + pay = LndPay(1, invoice_settled.bolt11) + pay.start() + time.sleep(1) + + cl.Settle(SettleRequest(payment_preimage=preimage_settled)) + pay.join() + + res = fut.result() + assert len(res) == expected_events + assert res == [ + (payment_hash_created, invoice_created.bolt11, InvoiceState.UNPAID), + (payment_hash_cancelled, invoice_cancelled.bolt11, InvoiceState.UNPAID), + (payment_hash_settled, invoice_settled.bolt11, InvoiceState.UNPAID), + ( + payment_hash_cancelled, + invoice_cancelled.bolt11, + InvoiceState.CANCELLED, + ), + (payment_hash_settled, invoice_settled.bolt11, InvoiceState.ACCEPTED), + (payment_hash_settled, invoice_settled.bolt11, InvoiceState.PAID), + ] diff --git a/tests/test_rpc.py b/tests/hold/test_rpc.py similarity index 96% rename from tests/test_rpc.py rename to tests/hold/test_rpc.py index d5ed457..6201e2d 100644 --- a/tests/test_rpc.py +++ b/tests/hold/test_rpc.py @@ -3,7 +3,7 @@ import time from typing import Any -from utils import LndPay, lightning, new_preimage +from hold.utils import LndPay, lightning, new_preimage def check_unpaid_invoice( @@ -19,7 +19,7 @@ def check_unpaid_invoice( class TestRpc: def test_invoice(self) -> None: - amount = 2112 + amount = 2_112 (_, payment_hash) = new_preimage() invoice = lightning("holdinvoice", payment_hash, f"{amount}") @@ -67,7 +67,7 @@ def test_settle(self) -> None: payer = LndPay(1, invoice) payer.start() - time.sleep(2) + time.sleep(1) data = lightning("listholdinvoices", payment_hash)["holdinvoices"][0] assert data["state"] == "accepted" @@ -95,7 +95,7 @@ def test_cancel(self) -> None: payer = LndPay(1, invoice) payer.start() - time.sleep(2) + time.sleep(1) data = lightning("listholdinvoices", payment_hash)["holdinvoices"][0] assert data["state"] == "accepted" diff --git a/tests/utils.py b/tests/hold/utils.py similarity index 84% rename from tests/utils.py rename to tests/hold/utils.py index e742081..3857b1a 100644 --- a/tests/utils.py +++ b/tests/hold/utils.py @@ -2,12 +2,27 @@ import json import os +from datetime import datetime, timezone from hashlib import sha256 from threading import Thread from typing import Any -def lightning(*args: str, node: int = 1) -> dict[str, Any]: +def time_now() -> datetime: + return datetime.now(tz=timezone.utc) + + +def new_preimage_bytes() -> tuple[bytes, bytes]: + preimage = os.urandom(32) + return preimage, sha256(preimage).digest() + + +def new_preimage() -> tuple[str, str]: + preimage = os.urandom(32) + return preimage.hex(), sha256(preimage).hexdigest() + + +def lightning(*args: str, node: int = 2) -> dict[str, Any]: return json.load( os.popen( f"docker exec boltz-cln-{node} lightning-cli --regtest {' '.join(args)}", @@ -25,11 +40,6 @@ def lnd_raw(*args: str, node: int = 1) -> str: ).read() -def new_preimage() -> tuple[str, str]: - preimage = os.urandom(32) - return preimage.hex(), sha256(preimage).hexdigest() - - class LndPay(Thread): res: dict[str, Any] = None diff --git a/tests/poetry.lock b/tests/poetry.lock index 85049b9..5f2e11b 100644 --- a/tests/poetry.lock +++ b/tests/poetry.lock @@ -25,6 +25,124 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "grpcio" +version = "1.65.5" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.8" +files = [ + {file = "grpcio-1.65.5-cp310-cp310-linux_armv7l.whl", hash = "sha256:b67d450f1e008fedcd81e097a3a400a711d8be1a8b20f852a7b8a73fead50fe3"}, + {file = "grpcio-1.65.5-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:a70a20eed87bba647a38bedd93b3ce7db64b3f0e8e0952315237f7f5ca97b02d"}, + {file = "grpcio-1.65.5-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:f79c87c114bf37adf408026b9e2e333fe9ff31dfc9648f6f80776c513145c813"}, + {file = "grpcio-1.65.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17f9fa2d947dbfaca01b3ab2c62eefa8240131fdc67b924eb42ce6032e3e5c1"}, + {file = "grpcio-1.65.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32d60e18ff7c34fe3f6db3d35ad5c6dc99f5b43ff3982cb26fad4174462d10b1"}, + {file = "grpcio-1.65.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fe6505376f5b00bb008e4e1418152e3ad3d954b629da286c7913ff3cfc0ff740"}, + {file = "grpcio-1.65.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:33158e56c6378063923c417e9fbdb28660b6e0e2835af42e67f5a7793f587af7"}, + {file = "grpcio-1.65.5-cp310-cp310-win32.whl", hash = "sha256:1cbc208edb9acf1cc339396a1a36b83796939be52f34e591c90292045b579fbf"}, + {file = "grpcio-1.65.5-cp310-cp310-win_amd64.whl", hash = "sha256:bc74f3f745c37e2c5685c9d2a2d5a94de00f286963f5213f763ae137bf4f2358"}, + {file = "grpcio-1.65.5-cp311-cp311-linux_armv7l.whl", hash = "sha256:3207ae60d07e5282c134b6e02f9271a2cb523c6d7a346c6315211fe2bf8d61ed"}, + {file = "grpcio-1.65.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2f80510f99f82d4eb825849c486df703f50652cea21c189eacc2b84f2bde764"}, + {file = "grpcio-1.65.5-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a80e9a5e3f93c54f5eb82a3825ea1fc4965b2fa0026db2abfecb139a5c4ecdf1"}, + {file = "grpcio-1.65.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b2944390a496567de9e70418f3742b477d85d8ca065afa90432edc91b4bb8ad"}, + {file = "grpcio-1.65.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3655139d7be213c32c79ef6fb2367cae28e56ef68e39b1961c43214b457f257"}, + {file = "grpcio-1.65.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05f02d68fc720e085f061b704ee653b181e6d5abfe315daef085719728d3d1fd"}, + {file = "grpcio-1.65.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1c4caafe71aef4dabf53274bbf4affd6df651e9f80beedd6b8e08ff438ed3260"}, + {file = "grpcio-1.65.5-cp311-cp311-win32.whl", hash = "sha256:84c901cdec16a092099f251ef3360d15e29ef59772150fa261d94573612539b5"}, + {file = "grpcio-1.65.5-cp311-cp311-win_amd64.whl", hash = "sha256:11f8b16121768c1cb99d7dcb84e01510e60e6a206bf9123e134118802486f035"}, + {file = "grpcio-1.65.5-cp312-cp312-linux_armv7l.whl", hash = "sha256:ee6ed64a27588a2c94e8fa84fe8f3b5c89427d4d69c37690903d428ec61ca7e4"}, + {file = "grpcio-1.65.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:76991b7a6fb98630a3328839755181ce7c1aa2b1842aa085fd4198f0e5198960"}, + {file = "grpcio-1.65.5-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:89c00a18801b1ed9cc441e29b521c354725d4af38c127981f2c950c796a09b6e"}, + {file = "grpcio-1.65.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:078038e150a897e5e402ed3d57f1d31ebf604cbed80f595bd281b5da40762a92"}, + {file = "grpcio-1.65.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97962720489ef31b5ad8a916e22bc31bba3664e063fb9f6702dce056d4aa61b"}, + {file = "grpcio-1.65.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b8270b15b99781461b244f5c81d5c2bc9696ab9189fb5ff86c841417fb3b39fe"}, + {file = "grpcio-1.65.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8e5c4c15ac3fe1eb68e46bc51e66ad29be887479f231f8237cf8416058bf0cc1"}, + {file = "grpcio-1.65.5-cp312-cp312-win32.whl", hash = "sha256:f5b5970341359341d0e4c789da7568264b2a89cd976c05ea476036852b5950cd"}, + {file = "grpcio-1.65.5-cp312-cp312-win_amd64.whl", hash = "sha256:238a625f391a1b9f5f069bdc5930f4fd71b74426bea52196fc7b83f51fa97d34"}, + {file = "grpcio-1.65.5-cp38-cp38-linux_armv7l.whl", hash = "sha256:6c4e62bcf297a1568f627f39576dbfc27f1e5338a691c6dd5dd6b3979da51d1c"}, + {file = "grpcio-1.65.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d7df567b67d16d4177835a68d3f767bbcbad04da9dfb52cbd19171f430c898bd"}, + {file = "grpcio-1.65.5-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:b7ca419f1462390851eec395b2089aad1e49546b52d4e2c972ceb76da69b10f8"}, + {file = "grpcio-1.65.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa36dd8496d3af0d40165252a669fa4f6fd2db4b4026b9a9411cbf060b9d6a15"}, + {file = "grpcio-1.65.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a101696f9ece90a0829988ff72f1b1ea2358f3df035bdf6d675dd8b60c2c0894"}, + {file = "grpcio-1.65.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2a6d8169812932feac514b420daffae8ab8e36f90f3122b94ae767e633296b17"}, + {file = "grpcio-1.65.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:47d0aaaab82823f0aa6adea5184350b46e2252e13a42a942db84da5b733f2e05"}, + {file = "grpcio-1.65.5-cp38-cp38-win32.whl", hash = "sha256:85ae8f8517d5bcc21fb07dbf791e94ed84cc28f84c903cdc2bd7eaeb437c8f45"}, + {file = "grpcio-1.65.5-cp38-cp38-win_amd64.whl", hash = "sha256:770bd4bd721961f6dd8049bc27338564ba8739913f77c0f381a9815e465ff965"}, + {file = "grpcio-1.65.5-cp39-cp39-linux_armv7l.whl", hash = "sha256:ab5ec837d8cee8dbce9ef6386125f119b231e4333cc6b6d57b6c5c7c82a72331"}, + {file = "grpcio-1.65.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cabd706183ee08d8026a015af5819a0b3a8959bdc9d1f6fdacd1810f09200f2a"}, + {file = "grpcio-1.65.5-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:ec71fc5b39821ad7d80db7473c8f8c2910f3382f0ddadfbcfc2c6c437107eb67"}, + {file = "grpcio-1.65.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a9e35bcb045e39d7cac30464c285389b9a816ac2067e4884ad2c02e709ef8e"}, + {file = "grpcio-1.65.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d750e9330eb14236ca11b78d0c494eed13d6a95eb55472298f0e547c165ee324"}, + {file = "grpcio-1.65.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2b91ce647b6307f25650872454a4d02a2801f26a475f90d0b91ed8110baae589"}, + {file = "grpcio-1.65.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8da58ff80bc4556cf29bc03f5fff1f03b8387d6aaa7b852af9eb65b2cf833be4"}, + {file = "grpcio-1.65.5-cp39-cp39-win32.whl", hash = "sha256:7a412959aa5f08c5ac04aa7b7c3c041f5e4298cadd4fcc2acff195b56d185ebc"}, + {file = "grpcio-1.65.5-cp39-cp39-win_amd64.whl", hash = "sha256:55714ea852396ec9568f45f487639945ab674de83c12bea19d5ddbc3ae41ada3"}, + {file = "grpcio-1.65.5.tar.gz", hash = "sha256:ec6f219fb5d677a522b0deaf43cea6697b16f338cb68d009e30930c4aa0d2209"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.65.5)"] + +[[package]] +name = "grpcio-tools" +version = "1.65.5" +description = "Protobuf code generator for gRPC" +optional = false +python-versions = ">=3.8" +files = [ + {file = "grpcio_tools-1.65.5-cp310-cp310-linux_armv7l.whl", hash = "sha256:f141f247a93e4c7faf33ac683a9cab93bb6570946a219260d33e2e62079db6e8"}, + {file = "grpcio_tools-1.65.5-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:a6d05950c62024ac54dfb7b7987fd45e22e832143aa88768439aa12073e9d035"}, + {file = "grpcio_tools-1.65.5-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:675df59961e2ab7808a3c0222ad995d8886bbbb7e77000fba1059214c9ce3e09"}, + {file = "grpcio_tools-1.65.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5a21e4970cc2555066ba37c7c743749ccd0bd056d4262e97678927c586def8"}, + {file = "grpcio_tools-1.65.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d0d7d34b4b3fba78075a923de2f962b33bcc04926569966c00219d5f41f2589"}, + {file = "grpcio_tools-1.65.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:474d5905ee0700662b42f71ce2fc5901786c88d5a54c08749fa5bccae1db27af"}, + {file = "grpcio_tools-1.65.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0f698f34be22a89426f986310ee866b8faa812355aab5d241fdaf742b546c36c"}, + {file = "grpcio_tools-1.65.5-cp310-cp310-win32.whl", hash = "sha256:3d8cee4c1f0bca80115cfa99f25ab6e6c6797b4443b1f0d5fa949bf2e9ac5af9"}, + {file = "grpcio_tools-1.65.5-cp310-cp310-win_amd64.whl", hash = "sha256:ac013d5d118dfafc887c3da1649dbd5087a7161d969dab236050e54c55fa0725"}, + {file = "grpcio_tools-1.65.5-cp311-cp311-linux_armv7l.whl", hash = "sha256:553b3f406a681719f6c11e70c993fe77383ab6adead9173ad1c6a611e5aaaf48"}, + {file = "grpcio_tools-1.65.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a520fbb9be5a05b5a0cdb5c5d481f63fea5db2f048f47f19b613685009890f2"}, + {file = "grpcio_tools-1.65.5-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:eca7be231ba6de3ac38556dcba1f94c05422e7cc62341bc2787ac9881aed3026"}, + {file = "grpcio_tools-1.65.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd1e9134e266fefdd49e1c9989d1bdf74578a9f237d7d9df01d871d898deda9b"}, + {file = "grpcio_tools-1.65.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:777243e4f7152da9d226d9cc1e6d7c2b94335e267c618260e6255a063bb7dfcb"}, + {file = "grpcio_tools-1.65.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a2e63bf9b6444f28ec684faf3c5fc8394b035fe221842186c3b9ff0121c20534"}, + {file = "grpcio_tools-1.65.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:969c0b5079beb08ae0a22237652289bfc0e34602403e040bab419f46cb775e50"}, + {file = "grpcio_tools-1.65.5-cp311-cp311-win32.whl", hash = "sha256:b9aefd9dc742c20bc5fb16f497f6d04b4f4f5c7d44cc86654a334ce7ea9c8021"}, + {file = "grpcio_tools-1.65.5-cp311-cp311-win_amd64.whl", hash = "sha256:ba27d67421dad33cbb42cdcd144dabed0516f0a5ee48d37250dd1b37c97cca72"}, + {file = "grpcio_tools-1.65.5-cp312-cp312-linux_armv7l.whl", hash = "sha256:b48943492a7c00a3ce6d7159c37761d006085f7dcd4a13931dcc74ecb8a24b56"}, + {file = "grpcio_tools-1.65.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ef44822eee4834158eb03cd432e4cf7e716d7d03051cc8314be4956ee9e9da3f"}, + {file = "grpcio_tools-1.65.5-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:6077a87bb3028797175dd437e08ff42b559045f9588a14eb9c943dd8bde32dcc"}, + {file = "grpcio_tools-1.65.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ee220c430f87378c598b7217c8c32ce7aeab3d8a93bc92cee92ce6940d870dd"}, + {file = "grpcio_tools-1.65.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c86a003bfcbf98b6261a89c2aad97197672c99d057fe441440210f052c9b54f1"}, + {file = "grpcio_tools-1.65.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:475ef5e8d91cbcf9bd9edbf51ac135931853d1c2fe6f8ae0c496b9ef422b41e4"}, + {file = "grpcio_tools-1.65.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e680b32e90c42d08363a02e9971e690bcf2509cb7bf647e232113b3e777eac9a"}, + {file = "grpcio_tools-1.65.5-cp312-cp312-win32.whl", hash = "sha256:cc6b010bc26566ca35e858a94daa18992a02e7b70f688a78f3308dada54fc063"}, + {file = "grpcio_tools-1.65.5-cp312-cp312-win_amd64.whl", hash = "sha256:3ddce72654ce415cbe36561b5e124fc0fcb461582e829016b7aa726824bcadc9"}, + {file = "grpcio_tools-1.65.5-cp38-cp38-linux_armv7l.whl", hash = "sha256:e5ae4a000c3344c32c1fa63e137ef42e65eae9adb5576dab636e3bc092653ae6"}, + {file = "grpcio_tools-1.65.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:23bce4fcee7cad2e085923fdfd65ed2bd2173bfc298c8c8964d3dddaef1f49ae"}, + {file = "grpcio_tools-1.65.5-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:56617905a4e478132b3732fd9dda71e35f1e7adedd34c92248c9a04a3892cb01"}, + {file = "grpcio_tools-1.65.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c38a8dc81900b7211fc5b1a14ace7f4ffd8cfbfd17e504f40044f0918b99825"}, + {file = "grpcio_tools-1.65.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b4f00f66a3f024e9bfaf535e2be8a373ada199eb928507945685208bf29536"}, + {file = "grpcio_tools-1.65.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b2072ad56bec624d0190e605c6b56205a6336f31a35617b90d927791c14aa4ad"}, + {file = "grpcio_tools-1.65.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c8f8241b859413b8f0c5c8cfd4d9021862d29cf090e60fb8b30968737b575b52"}, + {file = "grpcio_tools-1.65.5-cp38-cp38-win32.whl", hash = "sha256:e099bff2328931064aef565e811a7ce6ecbe7359c4d377534eee12dc6c35deb8"}, + {file = "grpcio_tools-1.65.5-cp38-cp38-win_amd64.whl", hash = "sha256:bf78ed1cfc9304dca4d1a5ec578a91b65a5946bf4ee923358a721fb47e35ffdf"}, + {file = "grpcio_tools-1.65.5-cp39-cp39-linux_armv7l.whl", hash = "sha256:02ed771ce6aea1a5620d818ae41380a7fcf65c6d499c53d1ddaf6ded882640a9"}, + {file = "grpcio_tools-1.65.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5b6a50253f950fd02caff90a021d6564731a86ffad38b7c0a76423f6ed58e779"}, + {file = "grpcio_tools-1.65.5-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:8848d509b88631be77b4c40119c02a37d0e884d10b10f0ddb1e3e551d7023b0d"}, + {file = "grpcio_tools-1.65.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:853ebfa33ed5336b51d0fa5d068bd5b42cb84d09077670ffa6b2dc7980f000cd"}, + {file = "grpcio_tools-1.65.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:221fd8f4c3f54ced15d9dac2b8800fd1b254bf9cd29414d500ce6f7ddb59be25"}, + {file = "grpcio_tools-1.65.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b6b33e23bfc6919c71329dabec632e7693de62efbed24b3e34616c09827909d8"}, + {file = "grpcio_tools-1.65.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:21122fa43c48e15ff0d656258f942fdf7c3ed2b7ab1530c7d37d3027b71a5872"}, + {file = "grpcio_tools-1.65.5-cp39-cp39-win32.whl", hash = "sha256:0e092c51089251f41e6e2c03519311509162be3aba2c71a91983d7d86ed300f3"}, + {file = "grpcio_tools-1.65.5-cp39-cp39-win_amd64.whl", hash = "sha256:2819a3a50c61306074cc95938db97e365acfca873b2cce986ad2d1f519d51f2f"}, + {file = "grpcio_tools-1.65.5.tar.gz", hash = "sha256:7c3a47ad0070bc907c7818caf55aa1948e9282d24e27afd21015872a25594bc7"}, +] + +[package.dependencies] +grpcio = ">=1.65.5" +protobuf = ">=5.26.1,<6.0dev" +setuptools = "*" + [[package]] name = "iniconfig" version = "2.0.0" @@ -62,6 +180,26 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "protobuf" +version = "5.27.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-5.27.3-cp310-abi3-win32.whl", hash = "sha256:dcb307cd4ef8fec0cf52cb9105a03d06fbb5275ce6d84a6ae33bc6cf84e0a07b"}, + {file = "protobuf-5.27.3-cp310-abi3-win_amd64.whl", hash = "sha256:16ddf3f8c6c41e1e803da7abea17b1793a97ef079a912e42351eabb19b2cffe7"}, + {file = "protobuf-5.27.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:68248c60d53f6168f565a8c76dc58ba4fa2ade31c2d1ebdae6d80f969cdc2d4f"}, + {file = "protobuf-5.27.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b8a994fb3d1c11156e7d1e427186662b64694a62b55936b2b9348f0a7c6625ce"}, + {file = "protobuf-5.27.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:a55c48f2a2092d8e213bd143474df33a6ae751b781dd1d1f4d953c128a415b25"}, + {file = "protobuf-5.27.3-cp38-cp38-win32.whl", hash = "sha256:043853dcb55cc262bf2e116215ad43fa0859caab79bb0b2d31b708f128ece035"}, + {file = "protobuf-5.27.3-cp38-cp38-win_amd64.whl", hash = "sha256:c2a105c24f08b1e53d6c7ffe69cb09d0031512f0b72f812dd4005b8112dbe91e"}, + {file = "protobuf-5.27.3-cp39-cp39-win32.whl", hash = "sha256:c84eee2c71ed83704f1afbf1a85c3171eab0fd1ade3b399b3fad0884cbcca8bf"}, + {file = "protobuf-5.27.3-cp39-cp39-win_amd64.whl", hash = "sha256:af7c0b7cfbbb649ad26132e53faa348580f844d9ca46fd3ec7ca48a1ea5db8a1"}, + {file = "protobuf-5.27.3-py3-none-any.whl", hash = "sha256:8572c6533e544ebf6899c360e91d6bcbbee2549251643d32c52cf8a5de295ba5"}, + {file = "protobuf-5.27.3.tar.gz", hash = "sha256:82460903e640f2b7e34ee81a947fdaad89de796d324bcbc38ff5430bcdead82c"}, +] + [[package]] name = "pytest" version = "8.3.2" @@ -111,6 +249,22 @@ files = [ {file = "ruff-0.6.1.tar.gz", hash = "sha256:af3ffd8c6563acb8848d33cd19a69b9bfe943667f0419ca083f8ebe4224a3436"}, ] +[[package]] +name = "setuptools" +version = "73.0.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-73.0.1-py3-none-any.whl", hash = "sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e"}, + {file = "setuptools-73.0.1.tar.gz", hash = "sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193"}, +] + +[package.extras] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] + [[package]] name = "tomli" version = "2.0.1" @@ -125,4 +279,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "1d452508104c22c9af0db099221f49dd2ca3cea667098d160502c4bd53846cdb" +content-hash = "af0e063a00ac6f3479fccf071d82a107f21eb9cae18e87188565a90d353cbc2c" diff --git a/tests/pyproject.toml b/tests/pyproject.toml index 3735120..6f979a8 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -1,20 +1,27 @@ [tool.poetry] -name = "tests" +name = "hold" authors = ["Boltz"] description = "Test for CLN hold invoice plugin" version = "0.1.0" license = "MIT" -package-mode = false [tool.poetry.dependencies] python = "^3.10" pytest = "^8.3.2" ruff = "^0.6.1" +grpcio = "^1.65.5" +grpcio-tools = "^1.65.5" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" +[tool.ruff] +exclude = ["hold/protos"] + [tool.ruff.lint] select = ["ALL"] -ignore = ["D100", "D101", "D102", "D103", "D107", "D211", "D212", "S605", "D203", "ISC001", "COM812", "S101"] +ignore = [ + "D100", "D101", "D102", "D103", "D104", "D107", "D211", "D212", "S605", "D203", "ISC001", "COM812", "S101", + "PLR2004" +]