diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f218c07..abceb56 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,19 +7,20 @@ on: branches: [ main, master ] workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: rust_tests: name: Rust Checks runs-on: self-hosted - timeout-minutes: 60 + timeout-minutes: 120 steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: stable components: clippy, rustfmt - override: true - profile: minimal - name: Install Protoc uses: arduino/setup-protoc@v3 @@ -27,16 +28,7 @@ jobs: version: "30.2" repo-token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: cargo-${{ runner.os }}-rust-tests-${{ hashFiles('**/Cargo.toml') }} - restore-keys: | - cargo-${{ runner.os }}-rust-tests- - cargo-${{ runner.os }}- + - uses: Swatinem/rust-cache@v2.7.8 - name: Check formatting run: cargo fmt --check diff --git a/Cargo.toml b/Cargo.toml index 493717e..d90585b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,9 @@ codegen-units = 1 # Reduce number of codegen units to increase optimizations. panic = 'abort' # Abort on panic [workspace.dependencies] -bitcoin-payment-instructions = { version = "0.6.0" } -lightning = { version = "0.2.0" } -lightning-invoice = { version = "0.34.0" } +bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "679dac50cc0d81ec4d31da94b93d467e5308f16a" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } [profile.release] panic = "abort" diff --git a/graduated-rebalancer/src/lib.rs b/graduated-rebalancer/src/lib.rs index fa65da8..2afe6e3 100644 --- a/graduated-rebalancer/src/lib.rs +++ b/graduated-rebalancer/src/lib.rs @@ -9,7 +9,6 @@ use bitcoin_payment_instructions::amount::Amount; use bitcoin_payment_instructions::PaymentMethod; -use lightning::bitcoin::hashes::Hash; use lightning::bitcoin::hex::DisplayHex; use lightning::bitcoin::OutPoint; use lightning::util::logger::Logger; @@ -278,7 +277,7 @@ where self.logger, "Attempting to pay invoice {inv} to rebalance for {transfer_amt:?}", ); - let expected_hash = *inv.payment_hash(); + let expected_hash = inv.payment_hash(); match self.trusted.pay(PaymentMethod::LightningBolt11(inv), transfer_amt).await { Ok(rebalance_id) => { log_debug!( @@ -295,29 +294,23 @@ where }) .await; - let ln_payment = match self - .ln_wallet - .await_payment_receipt(expected_hash.to_byte_array()) - .await - { - Some(receipt) => receipt, - None => { - log_error!(self.logger, "Failed to receive rebalance payment!"); - return; - }, - }; - - let trusted_payment = match self - .trusted - .await_payment_success(expected_hash.to_byte_array()) - .await - { - Some(success) => success, - None => { - log_error!(self.logger, "Failed to send rebalance payment!"); - return; - }, - }; + let ln_payment = + match self.ln_wallet.await_payment_receipt(expected_hash.0).await { + Some(receipt) => receipt, + None => { + log_error!(self.logger, "Failed to receive rebalance payment!"); + return; + }, + }; + + let trusted_payment = + match self.trusted.await_payment_success(expected_hash.0).await { + Some(success) => success, + None => { + log_error!(self.logger, "Failed to send rebalance payment!"); + return; + }, + }; log_info!( self.logger, diff --git a/orange-sdk/Cargo.toml b/orange-sdk/Cargo.toml index 15c9bef..1852672 100644 --- a/orange-sdk/Cargo.toml +++ b/orange-sdk/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" authors = ["benthecarman "] documentation = "https://docs.rs/orange-sdk/" license = "MIT OR Apache-2.0" -keywords = [ "lightning", "bitcoin", "spark", "cashu" ] +keywords = ["lightning", "bitcoin", "spark", "cashu"] readme = "../README.md" [lib] @@ -23,24 +23,24 @@ _cashu-tests = ["_test-utils", "cdk-ldk-node", "cdk/mint", "cdk-sqlite", "cdk-ax [dependencies] graduated-rebalancer = { path = "../graduated-rebalancer", version = "0.1.0" } -ldk-node = { version = "0.7.0" } +ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "109978de1f57fc3b3f23f241f238b97d9deaa56a" } lightning-macros = "0.2.0" bitcoin-payment-instructions = { workspace = true, features = ["http"] } chrono = { version = "0.4", default-features = false } rand = { version = "0.8.5", optional = true } -breez-sdk-spark = { git = "https://github.com/breez/spark-sdk.git", rev = "1000f276d2f16592b5f6eb8fce29228f34ff88bf", default-features = false, optional = true } +breez-sdk-spark = { git = "https://github.com/breez/spark-sdk.git", rev = "0d8db793618a4f9eaf8098d759baefe58ea2da49", default-features = false, optional = true } tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "sync", "macros"] } uuid = { version = "1.0", default-features = false, optional = true } -cdk = { version = "0.15.1", default-features = false, features = ["wallet"], optional = true } +cdk = { version = "0.16.0", default-features = false, features = ["wallet"], optional = true } serde_json = { version = "1.0", optional = true } async-trait = "0.1" log = "0.4.28" corepc-node = { version = "0.10.1", features = ["29_0", "download"], optional = true } electrsd = { version = "0.36.1", default-features = false, features = ["esplora_a33e97e1", "corepc-node_29_0"], optional = true } -cdk-ldk-node = { version = "0.15.1", optional = true } -cdk-sqlite = { version = "0.15.1", optional = true } -cdk-axum = { version = "0.15.1", optional = true } +cdk-ldk-node = { version = "0.16.0", optional = true } +cdk-sqlite = { version = "0.16.0", optional = true } +cdk-axum = { version = "0.16.0", optional = true } axum = { version = "0.8.1", optional = true } uniffi = { version = "0.29", default-features = false, features = ["cli"], optional = true } diff --git a/orange-sdk/src/dyn_store.rs b/orange-sdk/src/dyn_store.rs new file mode 100644 index 0000000..7501f57 --- /dev/null +++ b/orange-sdk/src/dyn_store.rs @@ -0,0 +1,192 @@ +//! Object-safe wrapper around ldk-node's `KVStore` + `KVStoreSync` traits. +//! +//! `lightning`'s `KVStore` returns `impl Future` from its methods, which makes the trait not +//! object-safe — orange-sdk can't share a backend across components as `Arc`. The +//! supertrait `SyncAndAsyncKVStore` that ldk-node exposes inherits the same problem, and even +//! if it didn't, no `Deref` blanket impl exists for `KVStoreSync`, so `Arc<...>` doesn't satisfy +//! it on its own. +//! +//! This module defines `DynStore`, an object-safe trait covering both sync and async kv +//! methods (async ones return boxed futures), with a blanket impl over any concrete type that +//! implements `KVStore + KVStoreSync`. The whole crate stores backends as +//! `Arc`; conversion to a value ldk-node accepts happens at the call site +//! through a thin newtype that delegates both traits back to `DynStore`. + +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use ldk_node::lightning::io; +use ldk_node::lightning::util::persist::{KVStore, KVStoreSync}; + +/// Object-safe view of a `KVStore + KVStoreSync` backend. Async methods return boxed +/// futures so the trait can be used through `dyn`. +pub(crate) trait DynStore: Send + Sync + 'static { + fn read_async( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Pin, io::Error>> + Send + 'static>>; + + fn write_async( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> Pin> + Send + 'static>>; + + fn remove_async( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> Pin> + Send + 'static>>; + + fn list_async( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Pin, io::Error>> + Send + 'static>>; + + fn read_sync( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Result, io::Error>; + + fn write_sync( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> Result<(), io::Error>; + + fn remove_sync( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> Result<(), io::Error>; + + fn list_sync( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Result, io::Error>; +} + +impl DynStore for T +where + T: KVStore + KVStoreSync + Send + Sync + 'static, +{ + fn read_async( + &self, p: &str, s: &str, k: &str, + ) -> Pin, io::Error>> + Send + 'static>> { + Box::pin(::read(self, p, s, k)) + } + + fn write_async( + &self, p: &str, s: &str, k: &str, buf: Vec, + ) -> Pin> + Send + 'static>> { + Box::pin(::write(self, p, s, k, buf)) + } + + fn remove_async( + &self, p: &str, s: &str, k: &str, lazy: bool, + ) -> Pin> + Send + 'static>> { + Box::pin(::remove(self, p, s, k, lazy)) + } + + fn list_async( + &self, p: &str, s: &str, + ) -> Pin, io::Error>> + Send + 'static>> { + Box::pin(::list(self, p, s)) + } + + fn read_sync(&self, p: &str, s: &str, k: &str) -> Result, io::Error> { + ::read(self, p, s, k) + } + + fn write_sync(&self, p: &str, s: &str, k: &str, buf: Vec) -> Result<(), io::Error> { + ::write(self, p, s, k, buf) + } + + fn remove_sync(&self, p: &str, s: &str, k: &str, lazy: bool) -> Result<(), io::Error> { + ::remove(self, p, s, k, lazy) + } + + fn list_sync(&self, p: &str, s: &str) -> Result, io::Error> { + ::list(self, p, s) + } +} + +// Make `Arc` itself implement `KVStore` + `KVStoreSync` so the same handle +// orange-sdk shares internally can be handed to ldk-node's `build_with_store`. We give it +// `KVStore::read` etc. by forwarding to the boxed-future variants on the trait, and the +// sync trait by forwarding to the sync variants. +// +// Note: we impl on `dyn DynStore` (which is local), not `Arc` directly — that gives us +// `&dyn DynStore: KVStore + KVStoreSync` and, via lightning's `Deref` blanket impl for +// `KVStore`, `Arc: KVStore`. For `KVStoreSync` (no `Deref` blanket exists) +// callers wrap the `Arc` in `LdkNodeStore` below before handing it to ldk-node. +impl KVStore for dyn DynStore { + fn read( + &self, p: &str, s: &str, k: &str, + ) -> impl Future, io::Error>> + Send + 'static { + self.read_async(p, s, k) + } + fn write( + &self, p: &str, s: &str, k: &str, buf: Vec, + ) -> impl Future> + Send + 'static { + self.write_async(p, s, k, buf) + } + fn remove( + &self, p: &str, s: &str, k: &str, lazy: bool, + ) -> impl Future> + Send + 'static { + self.remove_async(p, s, k, lazy) + } + fn list( + &self, p: &str, s: &str, + ) -> impl Future, io::Error>> + Send + 'static { + self.list_async(p, s) + } +} + +impl KVStoreSync for dyn DynStore { + fn read(&self, p: &str, s: &str, k: &str) -> Result, io::Error> { + self.read_sync(p, s, k) + } + fn write(&self, p: &str, s: &str, k: &str, buf: Vec) -> Result<(), io::Error> { + self.write_sync(p, s, k, buf) + } + fn remove(&self, p: &str, s: &str, k: &str, lazy: bool) -> Result<(), io::Error> { + self.remove_sync(p, s, k, lazy) + } + fn list(&self, p: &str, s: &str) -> Result, io::Error> { + self.list_sync(p, s) + } +} + +/// Cloneable handle wrapping `Arc` that satisfies ldk-node's +/// `SyncAndAsyncKVStore + Send + Sync + 'static` bound on `build_with_store`. Both trait +/// impls just forward to the underlying `dyn DynStore`. +#[derive(Clone)] +pub(crate) struct LdkNodeStore(pub(crate) Arc); + +impl KVStore for LdkNodeStore { + fn read( + &self, p: &str, s: &str, k: &str, + ) -> impl Future, io::Error>> + Send + 'static { + self.0.read_async(p, s, k) + } + fn write( + &self, p: &str, s: &str, k: &str, buf: Vec, + ) -> impl Future> + Send + 'static { + self.0.write_async(p, s, k, buf) + } + fn remove( + &self, p: &str, s: &str, k: &str, lazy: bool, + ) -> impl Future> + Send + 'static { + self.0.remove_async(p, s, k, lazy) + } + fn list( + &self, p: &str, s: &str, + ) -> impl Future, io::Error>> + Send + 'static { + self.0.list_async(p, s) + } +} + +impl KVStoreSync for LdkNodeStore { + fn read(&self, p: &str, s: &str, k: &str) -> Result, io::Error> { + self.0.read_sync(p, s, k) + } + fn write(&self, p: &str, s: &str, k: &str, buf: Vec) -> Result<(), io::Error> { + self.0.write_sync(p, s, k, buf) + } + fn remove(&self, p: &str, s: &str, k: &str, lazy: bool) -> Result<(), io::Error> { + self.0.remove_sync(p, s, k, lazy) + } + fn list(&self, p: &str, s: &str) -> Result, io::Error> { + self.0.list_sync(p, s) + } +} diff --git a/orange-sdk/src/event.rs b/orange-sdk/src/event.rs index 87d179d..4c3a66e 100644 --- a/orange-sdk/src/event.rs +++ b/orange-sdk/src/event.rs @@ -1,6 +1,7 @@ use crate::logging::Logger; use crate::store::{self, PaymentId}; +use crate::dyn_store::DynStore; use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::{OutPoint, Txid}; use ldk_node::lightning::events::{ClosureReason, PaymentFailureReason}; @@ -11,7 +12,7 @@ use ldk_node::lightning::util::ser::{Writeable, Writer}; use ldk_node::lightning::{impl_writeable_tlv_based_enum, log_debug, log_error, log_warn}; use ldk_node::lightning_types::payment::{PaymentHash, PaymentPreimage}; use ldk_node::payment::{ConfirmationStatus, PaymentKind}; -use ldk_node::{CustomTlvRecord, DynStore, UserChannelId}; +use ldk_node::{CustomTlvRecord, UserChannelId}; use std::collections::VecDeque; use std::sync::Arc; @@ -207,12 +208,12 @@ impl_writeable_tlv_based_enum!(Event, pub struct EventQueue { queue: Arc>>, waker: Arc>>, - kv_store: Arc, + kv_store: Arc, logger: Arc, } impl EventQueue { - pub(crate) fn new(kv_store: Arc, logger: Arc) -> Self { + pub(crate) fn new(kv_store: Arc, logger: Arc) -> Self { let queue = Arc::new(Mutex::new(VecDeque::new())); let waker = Arc::new(Mutex::new(None)); Self { queue, waker, kv_store, logger } @@ -335,6 +336,7 @@ impl LdkEventHandler { payment_hash, payment_preimage, fee_paid_msat, + bolt12_invoice: _, } => { let preimage = payment_preimage.unwrap(); // safe let payment_id = PaymentId::SelfCustodial(payment_id.unwrap().0); // safe diff --git a/orange-sdk/src/lib.rs b/orange-sdk/src/lib.rs index 3388a60..aa58b74 100644 --- a/orange-sdk/src/lib.rs +++ b/orange-sdk/src/lib.rs @@ -27,13 +27,16 @@ use ldk_node::lightning::util::logger::Logger as _; use ldk_node::lightning::{log_debug, log_error, log_info, log_trace, log_warn}; use ldk_node::lightning_invoice::Bolt11Invoice; use ldk_node::payment::{PaymentDirection, PaymentKind}; -use ldk_node::{BuildError, ChannelDetails, DynStore, NodeError}; +use ldk_node::{BuildError, ChannelDetails, NodeError}; + +use crate::dyn_store::DynStore; use std::collections::HashMap; use std::fmt::{self, Debug, Write}; use std::sync::Arc; use std::time::{Duration, SystemTime}; +mod dyn_store; mod event; #[cfg(feature = "uniffi")] mod ffi; @@ -112,7 +115,7 @@ struct WalletImpl { /// Metadata store for tracking transactions. tx_metadata: TxMetadataStore, /// Key-value store for persistent storage. - store: Arc, + store: Arc, /// Logger for logging wallet operations. logger: Arc, /// The Tokio runtime for asynchronous operations. @@ -519,7 +522,7 @@ impl Wallet { BuildError::RuntimeSetupFailed })?); - let store: Arc = match &config.storage_config { + let store: Arc = match &config.storage_config { StorageConfig::LocalSQLite(path) => { Arc::new(SqliteStore::new(path.into(), Some("orange.sqlite".to_owned()), None)?) }, diff --git a/orange-sdk/src/lightning_wallet.rs b/orange-sdk/src/lightning_wallet.rs index c54314b..3d7a10b 100644 --- a/orange-sdk/src/lightning_wallet.rs +++ b/orange-sdk/src/lightning_wallet.rs @@ -1,5 +1,6 @@ use crate::bitcoin::OutPoint; use crate::bitcoin::hashes::Hash; +use crate::dyn_store::{DynStore, LdkNodeStore}; use crate::event::{EventQueue, LdkEventHandler}; use crate::logging::Logger; use crate::runtime::Runtime; @@ -13,7 +14,8 @@ use ldk_node::bitcoin::base64::Engine; use ldk_node::bitcoin::base64::prelude::BASE64_STANDARD; use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::{Address, Network}; -use ldk_node::config::{AsyncPaymentsRole, BackgroundSyncConfig}; +use ldk_node::config::{AsyncPaymentsRole, BackgroundSyncConfig, SyncTimeoutsConfig}; +use ldk_node::entropy::NodeEntropy; use ldk_node::lightning::ln::channelmanager::PaymentId; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning::util::logger::Logger as _; @@ -22,7 +24,7 @@ use ldk_node::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Descr use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, }; -use ldk_node::{DynStore, NodeError, UserChannelId}; +use ldk_node::{NodeError, UserChannelId}; use graduated_rebalancer::{LightningBalance, ReceivedLightningPayment}; @@ -42,7 +44,7 @@ pub(crate) struct LightningWalletBalance { pub(crate) struct LightningWalletImpl { pub(crate) ldk_node: Arc, logger: Arc, - store: Arc, + store: Arc, payment_receipt_flag: watch::Receiver<()>, channel_pending_receipt_flag: watch::Receiver, splice_pending_receipt_flag: watch::Receiver, @@ -58,7 +60,7 @@ const DEFAULT_INVOICE_EXPIRY_SECS: u32 = 86_400; // 24 hours impl LightningWallet { pub(super) async fn init( - runtime: Arc, config: WalletConfig, store: Arc, + runtime: Arc, config: WalletConfig, store: Arc, event_queue: Arc, tx_metadata: TxMetadataStore, logger: Arc, ) -> Result { log_info!(logger, "Creating LDK node..."); @@ -72,14 +74,12 @@ impl LightningWallet { }; let mut builder = ldk_node::Builder::from_config(ldk_node_config); builder.set_network(config.network); - match config.seed { - Seed::Seed64(seed) => { - builder.set_entropy_seed_bytes(seed); - }, + let node_entropy = match config.seed { + Seed::Seed64(seed) => NodeEntropy::from_seed_bytes(seed), Seed::Mnemonic { mnemonic, passphrase } => { - builder.set_entropy_bip39_mnemonic(mnemonic, passphrase); + NodeEntropy::from_bip39_mnemonic(mnemonic, passphrase) }, - } + }; match config.rgs_url { Some(url) => { @@ -118,6 +118,7 @@ impl LightningWallet { lightning_wallet_sync_interval_secs: 2, fee_rate_cache_update_interval_secs: 30, }), + timeouts_config: SyncTimeoutsConfig::default(), } } else { ldk_node::config::EsploraSyncConfig::default() @@ -150,6 +151,7 @@ impl LightningWallet { ChainSource::Electrum(url) => { let sync_config = if config.network == Network::Regtest { Some(ldk_node::config::ElectrumSyncConfig { + timeouts_config: SyncTimeoutsConfig::default(), background_sync_config: Some(BackgroundSyncConfig { onchain_wallet_sync_interval_secs: 2, lightning_wallet_sync_interval_secs: 2, @@ -176,7 +178,8 @@ impl LightningWallet { builder.set_pathfinding_scores_source(url); } - let ldk_node = Arc::new(builder.build_with_store(Arc::clone(&store))?); + let ldk_node = + Arc::new(builder.build_with_store(node_entropy, LdkNodeStore(Arc::clone(&store)))?); let (payment_receipt_sender, payment_receipt_flag) = watch::channel(()); let (channel_pending_sender, channel_pending_receipt_flag) = watch::channel(0); let (splice_pending_sender, splice_pending_receipt_flag) = watch::channel(0); diff --git a/orange-sdk/src/rebalancer.rs b/orange-sdk/src/rebalancer.rs index 335717c..5b459b3 100644 --- a/orange-sdk/src/rebalancer.rs +++ b/orange-sdk/src/rebalancer.rs @@ -1,6 +1,7 @@ use crate::bitcoin::Txid; use crate::bitcoin::hashes::Hash; use crate::bitcoin::hex::DisplayHex; +use crate::dyn_store::DynStore; use crate::lightning_wallet::LightningWallet; use crate::logging::Logger; use crate::store::{PaymentId, TxMetadata, TxMetadataStore, TxType}; @@ -8,7 +9,6 @@ use crate::trusted_wallet::DynTrustedWalletInterface; use crate::{Event, EventQueue, PaymentType, Tunables, store}; use bitcoin_payment_instructions::amount::Amount; use graduated_rebalancer::{RebalanceTrigger, RebalancerEvent, TriggerParams}; -use ldk_node::DynStore; use ldk_node::lightning::util::logger::Logger as _; use ldk_node::lightning::{log_error, log_info, log_trace, log_warn}; use ldk_node::payment::{ConfirmationStatus, PaymentDirection, PaymentKind, PaymentStatus}; @@ -30,7 +30,7 @@ pub(crate) struct OrangeTrigger { /// The event handler for processing wallet events. event_queue: Arc, /// Key-value store for persistent storage. - store: Arc, + store: Arc, /// Time of the last on-chain sync, used to determine when to trigger rebalances. onchain_sync_time: AtomicU64, /// Logger for logging events and errors. @@ -42,7 +42,7 @@ impl OrangeTrigger { pub(crate) fn new( ln_wallet: Arc, trusted: Arc>, tunables: Tunables, tx_metadata: TxMetadataStore, event_queue: Arc, - store: Arc, logger: Arc, + store: Arc, logger: Arc, ) -> Self { let start = ln_wallet.inner.ldk_node.status().latest_onchain_wallet_sync_timestamp.unwrap_or(0); diff --git a/orange-sdk/src/store.rs b/orange-sdk/src/store.rs index f6eb15a..58ce72e 100644 --- a/orange-sdk/src/store.rs +++ b/orange-sdk/src/store.rs @@ -13,7 +13,7 @@ use bitcoin_payment_instructions::amount::Amount; -use ldk_node::DynStore; +use crate::dyn_store::DynStore; use ldk_node::bitcoin::Txid; use ldk_node::bitcoin::hex::{DisplayHex, FromHex}; use ldk_node::lightning::io; @@ -295,11 +295,11 @@ impl_writeable_tlv_based!(TxMetadata, { (0, ty, required), (2, time, required) } #[derive(Clone)] pub(crate) struct TxMetadataStore { tx_metadata: Arc>>, - store: Arc, + store: Arc, } impl TxMetadataStore { - pub async fn new(store: Arc) -> TxMetadataStore { + pub async fn new(store: Arc) -> TxMetadataStore { let keys = KVStore::list(store.as_ref(), STORE_PRIMARY_KEY, STORE_SECONDARY_KEY) .await .expect("We do not allow reads to fail"); @@ -429,7 +429,7 @@ impl TxMetadataStore { const REBALANCE_ENABLED_KEY: &str = "rebalance_enabled"; -pub(crate) async fn get_rebalance_enabled(store: &DynStore) -> bool { +pub(crate) async fn get_rebalance_enabled(store: &dyn DynStore) -> bool { match KVStore::read(store, STORE_PRIMARY_KEY, "", REBALANCE_ENABLED_KEY).await { Ok(bytes) => Readable::read(&mut &bytes[..]).expect("Invalid data in rebalance_enabled"), Err(e) if e.kind() == io::ErrorKind::NotFound => { @@ -445,14 +445,14 @@ pub(crate) async fn get_rebalance_enabled(store: &DynStore) -> bool { } } -pub(crate) async fn set_rebalance_enabled(store: &DynStore, enabled: bool) { +pub(crate) async fn set_rebalance_enabled(store: &dyn DynStore, enabled: bool) { let bytes = enabled.encode(); KVStore::write(store, STORE_PRIMARY_KEY, "", REBALANCE_ENABLED_KEY, bytes) .await .expect("Failed to write rebalance_enabled"); } -pub(crate) async fn write_splice_out(store: &DynStore, details: &PaymentDetails) { +pub(crate) async fn write_splice_out(store: &dyn DynStore, details: &PaymentDetails) { KVStore::write( store, STORE_PRIMARY_KEY, @@ -464,7 +464,7 @@ pub(crate) async fn write_splice_out(store: &DynStore, details: &PaymentDetails) .expect("Failed to write splice out txid"); } -pub(crate) async fn read_splice_outs(store: &DynStore) -> Vec { +pub(crate) async fn read_splice_outs(store: &dyn DynStore) -> Vec { let keys = KVStore::list(store, STORE_PRIMARY_KEY, SPLICE_OUT_SECONDARY_KEY) .await .expect("We do not allow reads to fail"); diff --git a/orange-sdk/src/trusted_wallet/cashu/cashu_store.rs b/orange-sdk/src/trusted_wallet/cashu/cashu_store.rs index bdb797e..e5f8f3e 100644 --- a/orange-sdk/src/trusted_wallet/cashu/cashu_store.rs +++ b/orange-sdk/src/trusted_wallet/cashu/cashu_store.rs @@ -3,10 +3,10 @@ use std::fmt::Debug; use std::str::FromStr; use std::sync::{Arc, RwLock}; +use crate::dyn_store::DynStore; use async_trait::async_trait; use cdk::cdk_database::WalletDatabase; use cdk::wallet::types::WalletSaga; -use ldk_node::DynStore; use ldk_node::lightning::io; use ldk_node::lightning::util::persist::KVStore; @@ -113,7 +113,7 @@ impl From for cdk::cdk_database::Error { /// A KV store-based implementation of the Cashu WalletDatabase trait pub struct CashuKvDatabase { - store: Arc, + store: Arc, // In-memory caches for frequently accessed data mints_cache: Arc>>>, proofs_cache: Arc>>, @@ -143,7 +143,7 @@ impl CashuKvDatabase { /// /// Returns a `Result` containing the initialized database or a `DatabaseError` if /// initialization fails. - pub async fn new(store: Arc) -> Result { + pub(crate) async fn new(store: Arc) -> Result { let database = Self { store, mints_cache: Arc::new(RwLock::new(HashMap::new())), @@ -1201,9 +1201,41 @@ impl WalletDatabase for CashuKvDatabase { .await .map_err(|e| DatabaseError::Io(e).into()) } + + // P2PK signing keys are not used by orange-sdk. The stubs below trip a debug assert if + // anything (orange-sdk or cdk internals) ever exercises them, so we notice before silently + // losing key material; release builds keep the safe defaults. + async fn add_p2pk_key( + &self, _pubkey: &PublicKey, _derivation_path: ldk_node::bitcoin::bip32::DerivationPath, + _derivation_index: u32, + ) -> Result<(), cdk::cdk_database::Error> { + debug_assert!(false, "orange-sdk does not support P2PK keys: add_p2pk_key called"); + Ok(()) + } + + async fn get_p2pk_key( + &self, _pubkey: &PublicKey, + ) -> Result, cdk::cdk_database::Error> { + debug_assert!(false, "orange-sdk does not support P2PK keys: get_p2pk_key called"); + Ok(None) + } + + async fn list_p2pk_keys( + &self, + ) -> Result, cdk::cdk_database::Error> { + debug_assert!(false, "orange-sdk does not support P2PK keys: list_p2pk_keys called"); + Ok(Vec::new()) + } + + async fn latest_p2pk( + &self, + ) -> Result, cdk::cdk_database::Error> { + debug_assert!(false, "orange-sdk does not support P2PK keys: latest_p2pk called"); + Ok(None) + } } -pub(super) async fn read_has_recovered(store: &Arc) -> Result { +pub(super) async fn read_has_recovered(store: &Arc) -> Result { match KVStore::read(store.as_ref(), CASHU_PRIMARY_KEY, "", HAS_RECOVERED_KEY).await { Ok(data) => { if data.is_empty() { @@ -1217,7 +1249,7 @@ pub(super) async fn read_has_recovered(store: &Arc) -> Result, has_recovered: bool, + store: &Arc, has_recovered: bool, ) -> Result<(), TrustedError> { let data = vec![if has_recovered { 1 } else { 0 }]; diff --git a/orange-sdk/src/trusted_wallet/cashu/mod.rs b/orange-sdk/src/trusted_wallet/cashu/mod.rs index 404791e..f522d55 100644 --- a/orange-sdk/src/trusted_wallet/cashu/mod.rs +++ b/orange-sdk/src/trusted_wallet/cashu/mod.rs @@ -7,7 +7,7 @@ use crate::store::{PaymentId, TxMetadataStore, TxStatus}; use crate::trusted_wallet::{Payment, TrustedError, TrustedWalletInterface}; use crate::{Event, EventQueue, InitFailure, Seed, WalletConfig}; -use ldk_node::DynStore; +use crate::dyn_store::DynStore; use ldk_node::bitcoin::hashes::Hash; use ldk_node::bitcoin::hashes::sha256::Hash as Sha256; use ldk_node::bitcoin::hex::FromHex; @@ -251,7 +251,7 @@ impl TrustedWalletInterface for Cashu { let quote = match method { PaymentMethod::LightningBolt11(invoice) => { - payment_hash = Some(PaymentHash(invoice.payment_hash().to_byte_array())); + payment_hash = Some(invoice.payment_hash()); // if we have an active quote for this invoice, use it // otherwise create a new one @@ -539,7 +539,7 @@ const PAYMENT_HASH_METADATA_KEY: &str = "payment_hash"; impl Cashu { pub(crate) async fn init( - config: &WalletConfig, cashu_config: CashuConfig, store: Arc, + config: &WalletConfig, cashu_config: CashuConfig, store: Arc, event_queue: Arc, tx_metadata: TxMetadataStore, logger: Arc, runtime: Arc, ) -> Result { @@ -837,7 +837,7 @@ impl Cashu { event_queue .add_event(Event::PaymentReceived { payment_id: PaymentId::Trusted(payment_id), - payment_hash: PaymentHash(hash.to_byte_array()), + payment_hash: hash, amount_msat: u64::from(mint_quote.amount.unwrap_or_default()) * 1_000, /* convert to msats */ custom_records: vec![], lsp_fee_msats: None, diff --git a/orange-sdk/src/trusted_wallet/dummy.rs b/orange-sdk/src/trusted_wallet/dummy.rs index b34cb11..e19e8da 100644 --- a/orange-sdk/src/trusted_wallet/dummy.rs +++ b/orange-sdk/src/trusted_wallet/dummy.rs @@ -56,7 +56,7 @@ impl DummyTrustedWallet { builder.set_network(Network::Regtest); let mut seed: [u8; 64] = [0; 64]; rand::thread_rng().fill_bytes(&mut seed); - builder.set_entropy_seed_bytes(seed); + let node_entropy = ldk_node::entropy::NodeEntropy::from_seed_bytes(seed); builder.set_gossip_source_p2p(); let cookie = bitcoind.params.get_cookie_values().unwrap().unwrap(); @@ -74,7 +74,7 @@ impl DummyTrustedWallet { let socket_addr = SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port }; builder.set_listening_addresses(vec![socket_addr.clone()]).unwrap(); - let ldk_node = Arc::new(builder.build().unwrap()); + let ldk_node = Arc::new(builder.build(node_entropy).unwrap()); ldk_node.start().unwrap(); @@ -95,6 +95,7 @@ impl DummyTrustedWallet { fee_paid_msat, payment_hash, payment_preimage, + bolt12_invoice: _, } => { // convert id let id = mangle_payment_id(payment_id.unwrap().0); diff --git a/orange-sdk/src/trusted_wallet/spark/mod.rs b/orange-sdk/src/trusted_wallet/spark/mod.rs index 5010599..b0314cc 100644 --- a/orange-sdk/src/trusted_wallet/spark/mod.rs +++ b/orange-sdk/src/trusted_wallet/spark/mod.rs @@ -9,7 +9,7 @@ use crate::store::{PaymentId, TxMetadataStore, TxStatus}; use crate::trusted_wallet::{Payment, TrustedError, TrustedWalletInterface}; use crate::{Event, EventQueue, InitFailure, Seed, WalletConfig}; -use ldk_node::DynStore; +use crate::dyn_store::DynStore; use ldk_node::lightning::util::logger::Logger as _; use ldk_node::lightning::{log_debug, log_error, log_info, log_warn}; use ldk_node::lightning_invoice::Bolt11Invoice; @@ -87,7 +87,7 @@ impl SparkWalletConfig { optimization_config: OptimizationConfig { auto_enabled: true, multiplicity: 1 }, stable_balance_config: None, max_concurrent_claims: 4, - support_lnurl_verify: false, + spark_config: None, }) } } @@ -318,7 +318,7 @@ impl TrustedWalletInterface for Spark { impl Spark { /// Initialize a new Spark wallet instance with the given configuration. pub(crate) async fn init( - config: &WalletConfig, spark_config: SparkWalletConfig, store: Arc, + config: &WalletConfig, spark_config: SparkWalletConfig, store: Arc, event_queue: Arc, tx_metadata: TxMetadataStore, logger: Arc, runtime: Arc, ) -> Result { @@ -416,6 +416,12 @@ impl EventListener for SparkEventHandler { SdkEvent::Optimization { optimization_event } => { log_debug!(self.logger, "Spark optimization event: {optimization_event:?}"); }, + SdkEvent::LightningAddressChanged { lightning_address } => { + log_debug!(self.logger, "Spark lightning address changed: {lightning_address:?}"); + }, + SdkEvent::NewDeposits { new_deposits } => { + log_info!(self.logger, "Spark wallet new deposits: {new_deposits:?}"); + }, } } } diff --git a/orange-sdk/src/trusted_wallet/spark/spark_store.rs b/orange-sdk/src/trusted_wallet/spark/spark_store.rs index 4e42e77..9b93e20 100644 --- a/orange-sdk/src/trusted_wallet/spark/spark_store.rs +++ b/orange-sdk/src/trusted_wallet/spark/spark_store.rs @@ -5,14 +5,14 @@ use std::sync::Arc; use crate::io; +use crate::dyn_store::DynStore; use breez_sdk_spark::sync_storage::{ IncomingChange, OutgoingChange, Record, RecordChange, RecordId, UnversionedRecordChange, }; use breez_sdk_spark::{ - DepositInfo, Payment, PaymentDetails, PaymentMetadata, SetLnurlMetadataItem, StorageError, - StorageListPaymentsRequest, UpdateDepositPayload, + Contact, DepositInfo, ListContactsRequest, Payment, PaymentDetails, PaymentMetadata, + SetLnurlMetadataItem, StorageError, StorageListPaymentsRequest, UpdateDepositPayload, }; -use ldk_node::DynStore; use ldk_node::lightning::util::persist::KVSTORE_NAMESPACE_KEY_MAX_LEN; use ldk_node::lightning::util::persist::KVStore; @@ -32,7 +32,7 @@ const REVISION_KEY: &str = "revision"; const LOCAL_REVISION_KEY: &str = "local_revision"; #[derive(Clone)] -pub(crate) struct SparkStore(pub(crate) Arc); +pub(crate) struct SparkStore(pub(crate) Arc); /// The Spark sdk can produce keys that are too long, we just truncate them here fn sanitize_key(key: String) -> String { @@ -298,6 +298,7 @@ impl breez_sdk_spark::Storage for SparkStore { lnurl_withdraw_info: metadata.lnurl_withdraw_info.or(existing.lnurl_withdraw_info), lnurl_description: metadata.lnurl_description.or(existing.lnurl_description), conversion_info: metadata.conversion_info.or(existing.conversion_info), + conversion_status: metadata.conversion_status.or(existing.conversion_status), } } else { metadata @@ -407,13 +408,14 @@ impl breez_sdk_spark::Storage for SparkStore { } async fn add_deposit( - &self, txid: String, vout: u32, amount_sats: u64, + &self, txid: String, vout: u32, amount_sats: u64, is_mature: bool, ) -> Result<(), StorageError> { let id = format!("{txid}:{vout}"); let info = DepositInfo { txid, vout, amount_sats, + is_mature, refund_tx: None, refund_tx_id: None, claim_error: None, @@ -534,6 +536,28 @@ impl breez_sdk_spark::Storage for SparkStore { Ok(()) } + async fn list_contacts( + &self, _request: ListContactsRequest, + ) -> Result, StorageError> { + // Contacts are not used by orange-sdk + Ok(Vec::new()) + } + + async fn get_contact(&self, _id: String) -> Result { + // Contacts are not used by orange-sdk + Err(StorageError::Implementation("contacts are not supported".to_string())) + } + + async fn insert_contact(&self, _contact: Contact) -> Result<(), StorageError> { + // Contacts are not used by orange-sdk + Ok(()) + } + + async fn delete_contact(&self, _id: String) -> Result<(), StorageError> { + // Contacts are not used by orange-sdk + Ok(()) + } + async fn add_outgoing_change( &self, record: UnversionedRecordChange, ) -> Result { diff --git a/orange-sdk/tests/integration_tests.rs b/orange-sdk/tests/integration_tests.rs index bd39347..5a654ae 100644 --- a/orange-sdk/tests/integration_tests.rs +++ b/orange-sdk/tests/integration_tests.rs @@ -137,7 +137,7 @@ async fn test_pay_from_trusted() { match event { Event::PaymentSuccessful { payment_hash, fee_paid_msat, .. } => { assert!(fee_paid_msat.is_some()); - assert_eq!(payment_hash.0, invoice.payment_hash().to_byte_array()); + assert_eq!(payment_hash, invoice.payment_hash()); }, e => panic!("Expected PaymentSuccessful event, got {e:?}"), } @@ -1482,8 +1482,8 @@ async fn test_payment_with_expired_invoice() { let invoice = third_party.bolt11_payment().receive(payment_amount.milli_sats(), &desc, 1).unwrap(); // 1 second expiry - // Wait longer to ensure invoice expires - tokio::time::sleep(Duration::from_secs(5)).await; + // Wait long enough to ensure the 1s-expiry invoice has expired. + tokio::time::sleep(Duration::from_secs(2)).await; // Try to parse and pay the expired invoice - it should either fail to parse or fail to pay let parse_result = wallet.parse_payment_instructions(invoice.to_string().as_str()).await; diff --git a/orange-sdk/tests/test_utils.rs b/orange-sdk/tests/test_utils.rs index 6942807..fb7de82 100644 --- a/orange-sdk/tests/test_utils.rs +++ b/orange-sdk/tests/test_utils.rs @@ -93,12 +93,9 @@ async fn create_bitcoind(uuid: Uuid) -> (Arc, Arc) { ElectrsD::with_conf(electrsd::downloaded_exe_path().unwrap(), &bitcoind, &electrsd_conf) .unwrap_or_else(|_| panic!("Failed to start electrsd for test {uuid}")); - // mine 101 blocks to get some spendable funds, split it up into batches - // to avoid potentially hitting RPC timeouts on slower CI systems + // mine 101 blocks to get some spendable funds let address = bitcoind.client.new_address().unwrap(); - for batch_size in [50, 51] { - let _block_hashes = bitcoind.client.generate_to_address(batch_size, &address).unwrap(); - } + let _block_hashes = bitcoind.client.generate_to_address(101, &address).unwrap(); wait_for_block(&electrsd.client, 101).await; @@ -145,7 +142,7 @@ fn create_lsp(uuid: Uuid, bitcoind: &Bitcoind) -> Arc { builder.set_network(Network::Regtest); let mut seed: [u8; 64] = [0; 64]; rand::thread_rng().fill_bytes(&mut seed); - builder.set_entropy_seed_bytes(seed); + let node_entropy = ldk_node::entropy::NodeEntropy::from_seed_bytes(seed); builder.set_gossip_source_p2p(); builder.set_node_alias("LSP".to_string()).unwrap(); @@ -172,15 +169,15 @@ fn create_lsp(uuid: Uuid, bitcoind: &Bitcoind) -> Arc { min_channel_opening_fee_msat: 0, max_client_to_self_delay: 1024, client_trusts_lsp: true, + disable_client_reserve: false, }; builder.set_liquidity_provider_lsps2(lsps2_service_config); let port = get_available_port().unwrap(); let addr = SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port }; - builder.set_listening_addresses(vec![addr.clone()]).unwrap(); builder.set_listening_addresses(vec![addr]).unwrap(); - let ldk_node = Arc::new(builder.build().unwrap()); + let ldk_node = Arc::new(builder.build(node_entropy).unwrap()); ldk_node.start().unwrap(); @@ -203,7 +200,7 @@ fn create_third_party(uuid: Uuid, bitcoind: &Bitcoind) -> Arc { builder.set_network(Network::Regtest); let mut seed: [u8; 64] = [0; 64]; rand::thread_rng().fill_bytes(&mut seed); - builder.set_entropy_seed_bytes(seed); + let node_entropy = ldk_node::entropy::NodeEntropy::from_seed_bytes(seed); builder.set_gossip_source_p2p(); builder.set_node_alias("third-party".to_string()).unwrap(); @@ -221,10 +218,9 @@ fn create_third_party(uuid: Uuid, bitcoind: &Bitcoind) -> Arc { let port = get_available_port().unwrap(); let addr = SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port }; - builder.set_listening_addresses(vec![addr.clone()]).unwrap(); builder.set_listening_addresses(vec![addr]).unwrap(); - let ldk_node = Arc::new(builder.build().unwrap()); + let ldk_node = Arc::new(builder.build(node_entropy).unwrap()); ldk_node.start().unwrap(); @@ -242,11 +238,20 @@ fn create_third_party(uuid: Uuid, bitcoind: &Bitcoind) -> Arc { ldk_node } -async fn fund_node(node: &Node, bitcoind: &Bitcoind, electrsd: &ElectrsD) { - let addr = node.onchain_payment().new_address().unwrap(); - let res = - bitcoind.client.send_to_address(&addr, bitcoin::Amount::from_btc(1.0).unwrap()).unwrap(); - wait_for_tx(&electrsd.client, res.txid().unwrap()).await; +/// Funds two nodes in one round so the 6-block confirmation only happens once. +async fn fund_two_nodes(a: &Node, b: &Node, bitcoind: &Bitcoind, electrsd: &ElectrsD) { + let addr_a = a.onchain_payment().new_address().unwrap(); + let addr_b = b.onchain_payment().new_address().unwrap(); + + let one_btc = bitcoin::Amount::from_btc(1.0).unwrap(); + let tx_a = bitcoind.client.send_to_address(&addr_a, one_btc).unwrap(); + let tx_b = bitcoind.client.send_to_address(&addr_b, one_btc).unwrap(); + + tokio::join!( + wait_for_tx(&electrsd.client, tx_a.txid().unwrap()), + wait_for_tx(&electrsd.client, tx_b.txid().unwrap()), + ); + generate_blocks(bitcoind, electrsd, 6).await; } @@ -300,10 +305,9 @@ async fn build_test_nodes() -> TestParams { let (bitcoind, electrsd) = create_bitcoind(test_id).await; let lsp = create_lsp(test_id, &bitcoind); - fund_node(&lsp, &bitcoind, &electrsd).await; let third_party = create_third_party(test_id, &bitcoind); let start_bal = third_party.list_balances().total_onchain_balance_sats; - fund_node(&third_party, &bitcoind, &electrsd).await; + fund_two_nodes(&lsp, &third_party, &bitcoind, &electrsd).await; // wait for node to sync (needs blocking wait as we are not in async context here) let third = Arc::clone(&third_party); @@ -550,7 +554,7 @@ pub async fn open_channel_from_lsp(wallet: &orange_sdk::Wallet, payer: Arc orange_sdk::Event::PaymentReceived { payment_hash, amount_msat, lsp_fee_msats, .. } => { assert!(lsp_fee_msats.is_some()); // we expect a fee to be paid for opening a channel assert_eq!(recv_amt.milli_sats(), amount_msat + lsp_fee_msats.unwrap_or(0)); // the fee will be deducted from the amount received - assert_eq!(payment_hash.0, uri.invoice.payment_hash().to_byte_array()); + assert_eq!(payment_hash, uri.invoice.payment_hash()); }, _ => panic!("Expected PaymentReceived event"), }