Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = { git = "https://github.com/benthecarman/bitcoin-payment-instructions.git", branch = "orange-fork", features = ["http"] }
lightning = { git = "https://github.com/tnull/rust-lightning", branch = "2025-08-bump-electrum-client-0.1" }
lightning-invoice = { git = "https://github.com/tnull/rust-lightning", branch = "2025-08-bump-electrum-client-0.1" }
bitcoin-payment-instructions = { version = "0.6.0" }
lightning = { version = "0.2.0" }
lightning-invoice = { version = "0.34.0" }

[profile.release]
panic = "abort"
Expand Down
31 changes: 17 additions & 14 deletions examples/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::runtime::Runtime;
use tokio::signal;

const NETWORK: Network = Network::Bitcoin; // Supports Bitcoin and Regtest
Expand Down Expand Up @@ -58,7 +57,6 @@ enum Commands {

struct WalletState {
wallet: Wallet,
_runtime: Arc<Runtime>, // Keep runtime alive
shutdown: Arc<AtomicBool>,
}

Expand Down Expand Up @@ -128,20 +126,20 @@ fn get_config(network: Network) -> Result<WalletConfig> {
}

impl WalletState {
async fn new(runtime: Arc<Runtime>) -> Result<Self> {
async fn new() -> Result<Self> {
let shutdown = Arc::new(AtomicBool::new(false));
let config = get_config(NETWORK)
.with_context(|| format!("Failed to get wallet config for network: {NETWORK:?}"))?;

println!("{} Initializing wallet...", "⚡".bright_yellow());

match Wallet::new_with_runtime(runtime.clone(), config).await {
match Wallet::new(config).await {
Ok(wallet) => {
println!("{} Wallet initialized successfully!", "✅".bright_green());
println!("Network: {}", NETWORK.to_string().bright_cyan());

let w = wallet.clone();
runtime.spawn(async move {
tokio::spawn(async move {
let event = w.next_event_async().await;
match event {
Event::PaymentSuccessful { payment_id, .. } => {
Expand Down Expand Up @@ -193,12 +191,19 @@ impl WalletState {
fee_msat
);
},
Event::SplicePending { new_funding_txo, .. } => {
println!(
"{} Splice pending: {}",
"🔄".bright_yellow(),
new_funding_txo
);
},
}

w.event_handled().unwrap();
});

Ok(WalletState { wallet, _runtime: runtime, shutdown })
Ok(WalletState { wallet, shutdown })
},
Err(e) => Err(anyhow::anyhow!("Failed to initialize wallet: {:?}", e)),
}
Expand All @@ -213,23 +218,21 @@ impl WalletState {
}
}

fn main() -> Result<()> {
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
let cli = Cli::parse();

println!("{}", "🟠 Orange CLI Wallet".bright_yellow().bold());
println!("{}", "Type 'help' for available commands or 'exit' to quit".dimmed());
println!();

// Create runtime outside async context to avoid drop issues
let runtime = Arc::new(Runtime::new().context("Failed to create tokio runtime")?);

// Initialize wallet once at startup
let mut state = runtime.block_on(WalletState::new(runtime.clone()))?;
let mut state = WalletState::new().await?;

// Set up signal handling for graceful shutdown
let shutdown_state = state.shutdown.clone();
let shutdown_wallet = state.wallet.clone();
runtime.spawn(async move {
tokio::task::spawn(async move {
if let Ok(()) = signal::ctrl_c().await {
println!("\n{} Shutdown signal received, stopping wallet...", "⏹️".bright_yellow());
shutdown_state.store(true, Ordering::Relaxed);
Expand All @@ -241,12 +244,12 @@ fn main() -> Result<()> {

// If a command was provided via command line, execute it and start interactive mode
if let Some(command) = cli.command {
runtime.block_on(execute_command(command, &mut state))?;
execute_command(command, &mut state).await?;
println!();
}

// Start interactive mode
runtime.block_on(start_interactive_mode(state))
start_interactive_mode(state).await
}

async fn start_interactive_mode(mut state: WalletState) -> Result<()> {
Expand Down
112 changes: 77 additions & 35 deletions graduated-rebalancer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ pub trait LightningWallet: Send + Sync {
&self, payment_hash: [u8; 32],
) -> Pin<Box<dyn Future<Output = Option<ReceivedLightningPayment>> + Send + '_>>;

/// Check if we already have a channel with the LSP
fn has_channel_with_lsp(&self) -> bool;

/// Open a channel with the LSP using on-chain funds
fn open_channel_with_lsp(
&self, amt: Amount,
Expand All @@ -117,6 +120,16 @@ pub trait LightningWallet: Send + Sync {
fn await_channel_pending(
&self, channel_id: u128,
) -> Pin<Box<dyn Future<Output = OutPoint> + Send + '_>>;

/// Splice funds from on-chain to an existing channel with the LSP
fn splice_to_lsp_channel(
&self, amt: Amount,
) -> Pin<Box<dyn Future<Output = Result<u128, Self::Error>> + Send + '_>>;

/// Wait for a splice pending notification, returns the splice outpoint
fn await_splice_pending(
&self, channel_id: u128,
) -> Pin<Box<dyn Future<Output = OutPoint> + Send + '_>>;
}

/// Represents a payment from the lightning wallet
Expand Down Expand Up @@ -176,15 +189,19 @@ pub enum RebalancerEvent {
/// Trait for handling rebalancer events
pub trait EventHandler: Send + Sync {
/// Handle a rebalancer event
fn handle_event(&self, event: RebalancerEvent);
fn handle_event(&self, event: RebalancerEvent)
-> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
}

/// A no-op event handler that discards all events
#[derive(Debug, Copy, Clone, Default)]
pub struct IgnoringEventHandler;

impl EventHandler for IgnoringEventHandler {
fn handle_event(&self, _event: RebalancerEvent) {
fn handle_event(
&self, _event: RebalancerEvent,
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {})
// Do nothing
}
}
Expand Down Expand Up @@ -260,11 +277,13 @@ where
rebalance_id.as_hex()
);

self.event_handler.handle_event(RebalancerEvent::RebalanceInitiated {
trigger_id: params.id,
trusted_rebalance_payment_id: rebalance_id,
amount_msat: transfer_amt.milli_sats(),
});
self.event_handler
.handle_event(RebalancerEvent::RebalanceInitiated {
trigger_id: params.id,
trusted_rebalance_payment_id: rebalance_id,
amount_msat: transfer_amt.milli_sats(),
})
.await;

let ln_payment = match self
.ln_wallet
Expand Down Expand Up @@ -297,14 +316,16 @@ where
ln_payment.id.as_hex(),
);

self.event_handler.handle_event(RebalancerEvent::RebalanceSuccessful {
trigger_id: params.id,
trusted_rebalance_payment_id: rebalance_id,
ln_rebalance_payment_id: ln_payment.id,
amount_msat: transfer_amt.milli_sats(),
fee_msat: ln_payment.fee_paid_msat.unwrap_or_default()
+ trusted_payment.fee_paid_msat.unwrap_or_default(),
});
self.event_handler
.handle_event(RebalancerEvent::RebalanceSuccessful {
trigger_id: params.id,
trusted_rebalance_payment_id: rebalance_id,
ln_rebalance_payment_id: ln_payment.id,
amount_msat: transfer_amt.milli_sats(),
fee_msat: ln_payment.fee_paid_msat.unwrap_or_default()
+ trusted_payment.fee_paid_msat.unwrap_or_default(),
})
.await;
},
Err(e) => {
log_info!(self.logger, "Rebalance trusted transaction failed with {e:?}",);
Expand All @@ -313,34 +334,55 @@ where
}
}

/// Perform on-chain to lightning rebalance by opening a channel
/// Perform on-chain to lightning rebalance by opening a channel or splicing into an existing one
async fn do_onchain_rebalance(&self, params: TriggerParams) {
// This should open a channel with the LSP using available on-chain funds

let _ = self.balance_mutex.lock().await;

log_info!(self.logger, "Opening channel with LSP with on-chain funds");
let (channel_outpoint, user_channel_id) = if self.ln_wallet.has_channel_with_lsp() {
log_info!(self.logger, "Splicing into channel with LSP with on-chain funds");

// todo for now we can only open a channel, eventually move to splicing
let user_chan_id = match self.ln_wallet.open_channel_with_lsp(params.amount).await {
Ok(chan_id) => chan_id,
Err(e) => {
log_error!(self.logger, "Failed to open channel with LSP: {e:?}");
return;
},
};
let user_chan_id = match self.ln_wallet.splice_to_lsp_channel(params.amount).await {
Ok(chan_id) => chan_id,
Err(e) => {
log_error!(self.logger, "Failed to open channel with LSP: {e:?}");
return;
},
};

log_info!(self.logger, "Initiated splice opened with LSP");

let channel_outpoint = self.ln_wallet.await_splice_pending(user_chan_id).await;

log_info!(self.logger, "Splice initiated at: {channel_outpoint}");

(channel_outpoint, user_chan_id)
} else {
log_info!(self.logger, "Opening channel with LSP with on-chain funds");

log_info!(self.logger, "Initiated channel opened with LSP");
let user_chan_id = match self.ln_wallet.open_channel_with_lsp(params.amount).await {
Ok(chan_id) => chan_id,
Err(e) => {
log_error!(self.logger, "Failed to open channel with LSP: {e:?}");
return;
},
};

log_info!(self.logger, "Initiated channel opened with LSP");

let channel_outpoint = self.ln_wallet.await_channel_pending(user_chan_id).await;
let channel_outpoint = self.ln_wallet.await_channel_pending(user_chan_id).await;

log_info!(self.logger, "Channel open succeeded at: {channel_outpoint}",);
log_info!(self.logger, "Channel open succeeded at: {channel_outpoint}");

(channel_outpoint, user_chan_id)
};

self.event_handler.handle_event(RebalancerEvent::OnChainRebalanceInitiated {
trigger_id: params.id,
user_channel_id: user_chan_id,
channel_outpoint,
});
self.event_handler
.handle_event(RebalancerEvent::OnChainRebalanceInitiated {
trigger_id: params.id,
user_channel_id,
channel_outpoint,
})
.await;
}

/// Stops the rebalancer, waits for any active rebalances to complete
Expand Down
10 changes: 8 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ default:
@just --list

test *args:
cargo test {{ args }} --features _test-utils -p orange-sdk
#!/usr/bin/env bash
THREADS=$(($(nproc) / 2))
if [ $THREADS -lt 1 ]; then THREADS=1; fi
cargo test {{ args }} --features _test-utils -p orange-sdk -- --test-threads=$THREADS

test-cashu *args:
cargo test {{ args }} --features _cashu-tests -p orange-sdk
#!/usr/bin/env bash
THREADS=$(($(nproc) / 2))
if [ $THREADS -lt 1 ]; then THREADS=1; fi
cargo test {{ args }} --features _cashu-tests -p orange-sdk -- --test-threads=$THREADS

cli:
cd examples/cli && cargo run
Expand Down
22 changes: 13 additions & 9 deletions orange-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,33 @@ default = ["spark"]
uniffi = ["dep:uniffi", "spark", "cashu"]
spark = ["breez-sdk-spark", "uuid", "serde_json"]
cashu = ["cdk", "serde_json"]
_test-utils = ["corepc-node", "cashu", "uuid/v7", "rand"]
_test-utils = ["corepc-node", 'electrsd', "cashu", "uuid/v7", "rand"]
_cashu-tests = ["_test-utils", "cdk-ldk-node", "cdk/mint", "cdk-sqlite", "cdk-axum", "axum"]

[dependencies]
graduated-rebalancer = { path = "../graduated-rebalancer", version = "0.1.0" }

ldk-node = { git = "https://github.com/benthecarman/ldk-node.git", branch = "esplora-auth" }
bitcoin-payment-instructions = { workspace = true }
ldk-node = { version = "0.7.0" }
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 }
reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls"] }
breez-sdk-spark = { git = "https://github.com/breez/spark-sdk.git", rev = "1f2e9995230cd582d6b4aa7d06d76b99defb635e", default-features = false, features = ["rustls-tls"], optional = true }
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "sync"] }
uuid = { version = "1.0", default-features = false, optional = true }
cdk = { git = "https://github.com/benthecarman/cdk.git", rev = "39c1206a4a1dda2adc1f3e23628136ef645f6c6b", default-features = false, features = ["wallet"], optional = true }
cdk = { version = "0.14.2", 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.8.0", features = ["29_0", "download"], optional = true }
cdk-ldk-node = { git = "https://github.com/benthecarman/cdk.git", rev = "39c1206a4a1dda2adc1f3e23628136ef645f6c6b", optional = true }
cdk-sqlite = { git = "https://github.com/benthecarman/cdk.git", rev = "39c1206a4a1dda2adc1f3e23628136ef645f6c6b", optional = true }
cdk-axum = { git = "https://github.com/benthecarman/cdk.git", rev = "39c1206a4a1dda2adc1f3e23628136ef645f6c6b", optional = true }
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.14.2", optional = true }
cdk-sqlite = { version = "0.14.2", optional = true }
cdk-axum = { version = "0.14.2", optional = true }
axum = { version = "0.8.1", optional = true }

uniffi = { version = "0.29", features = ["cli", "tokio"], optional = true }

[dev-dependencies]
test-log = "0.2.18"
Loading
Loading