Skip to content

Commit eff8d3f

Browse files
committed
WIP: splice out
1 parent 02b6f29 commit eff8d3f

3 files changed

Lines changed: 171 additions & 11 deletions

File tree

orange-sdk/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1114,7 +1114,9 @@ impl Wallet {
11141114
let mut pay_lightning = async |method, ty: fn() -> PaymentType| {
11151115
let typ = ty();
11161116
let balance = if matches!(typ, PaymentType::OutgoingOnChain { .. }) {
1117-
ln_balance.onchain
1117+
// if we are paying on-chain, we can either use the on-chain balance or the
1118+
// lightning balance with a splice. Use the larger of the two.
1119+
ln_balance.onchain.max(ln_balance.lightning)
11181120
} else {
11191121
ln_balance.lightning
11201122
};

orange-sdk/src/lightning_wallet.rs

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,66 @@ impl LightningWallet {
298298
.ldk_node
299299
.bolt12_payment()
300300
.send_using_amount(offer, amount.milli_sats(), None, None),
301-
PaymentMethod::OnChain(address) => self
302-
.inner
303-
.ldk_node
304-
.onchain_payment()
305-
.send_to_address(address, amount.sats_rounding_up(), None)
306-
.map(|txid| PaymentId(*txid.as_ref())),
301+
PaymentMethod::OnChain(address) => {
302+
let balance = self.inner.ldk_node.list_balances();
303+
304+
// if we have enough onchain balance, send onchain
305+
if balance.spendable_onchain_balance_sats > amount.sats_rounding_up() {
306+
self.inner
307+
.ldk_node
308+
.onchain_payment()
309+
.send_to_address(address, amount.sats_rounding_up(), None)
310+
.map(|txid| PaymentId(*txid.as_ref()))
311+
} else {
312+
// otherwise try to pay via splice out
313+
314+
// find existing channel to splice out of
315+
let channels = self.inner.ldk_node.list_channels();
316+
let channel =
317+
channels.iter().find(|c| c.counterparty_node_id == self.inner.lsp_node_id);
318+
319+
match channel {
320+
None => {
321+
log_error!(self.inner.logger, "No existing channel to splice out of");
322+
Err(NodeError::InsufficientFunds)
323+
},
324+
Some(chan) => {
325+
self.inner.ldk_node.splice_out(
326+
&chan.user_channel_id,
327+
chan.counterparty_node_id,
328+
address.clone(),
329+
amount.sats_rounding_up(),
330+
)?;
331+
332+
loop {
333+
self.await_splice_pending(chan.user_channel_id.0).await;
334+
let channels = self.inner.ldk_node.list_channels();
335+
let new_chan = channels
336+
.iter()
337+
.find(|c| c.user_channel_id == chan.user_channel_id);
338+
match new_chan {
339+
Some(c) => {
340+
if c.funding_txo
341+
.is_some_and(|f| f != chan.funding_txo.unwrap())
342+
{
343+
return Ok(PaymentId(
344+
*c.funding_txo.unwrap().txid.as_ref(),
345+
));
346+
}
347+
},
348+
None => {
349+
log_error!(
350+
self.inner.logger,
351+
"Channel disappeared while awaiting splice out"
352+
);
353+
return Err(NodeError::WalletOperationFailed);
354+
},
355+
}
356+
}
357+
},
358+
}
359+
}
360+
},
307361
}
308362
}
309363

orange-sdk/tests/integration_tests.rs

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -565,10 +565,10 @@ async fn test_receive_to_onchain_with_channel() {
565565
);
566566
assert_ne!(tx.time_since_epoch, Duration::ZERO, "Time should be set");
567567
assert_eq!(tx.amount, Some(recv_amt), "Amount should equal received amount");
568-
// fixme assert!(
569-
// tx.fee.unwrap() > Amount::ZERO,
570-
// "On-chain receive should have rebalance fees after channel opening"
571-
// );
568+
assert!(
569+
tx.fee.unwrap() > Amount::ZERO,
570+
"On-chain receive should have rebalance fees after channel opening"
571+
);
572572

573573
// Validate fee is reasonable (should be less than 5% of received amount for rebalance)
574574
let fee_ratio = tx.fee.unwrap().milli_sats() as f64 / recv_amt.milli_sats() as f64;
@@ -894,6 +894,110 @@ async fn test_pay_onchain_from_self_custody() {
894894
.await;
895895
}
896896

897+
#[tokio::test(flavor = "multi_thread")]
898+
async fn test_pay_onchain_from_channel() {
899+
test_utils::run_test(|params| async move {
900+
let wallet = Arc::clone(&params.wallet);
901+
let bitcoind = Arc::clone(&params.bitcoind);
902+
let third_party = Arc::clone(&params.third_party);
903+
904+
// get a channel so we can make a payment
905+
let recv = open_channel_from_lsp(&wallet, Arc::clone(&third_party)).await;
906+
907+
let starting_bal = wallet.get_balance().await.unwrap();
908+
assert_eq!(
909+
starting_bal.available_balance(),
910+
recv.saturating_sub(Amount::from_sats(2_000).unwrap())
911+
);
912+
assert_eq!(starting_bal.pending_balance, Amount::ZERO);
913+
914+
// wait for sync
915+
generate_blocks(&bitcoind, 6);
916+
test_utils::wait_for_condition("wallet sync after channel open", || async {
917+
wallet.channels().iter().any(|a| a.confirmations.is_some_and(|c| c > 0) && a.is_usable)
918+
})
919+
.await;
920+
921+
// get address from third party node
922+
let addr = third_party.onchain_payment().new_address().unwrap();
923+
let send_amount = Amount::from_sats(10_000).unwrap();
924+
925+
let instr = wallet.parse_payment_instructions(addr.to_string().as_str()).await.unwrap();
926+
let info = PaymentInfo::build(instr, Some(send_amount)).unwrap();
927+
wallet.pay(&info).await.unwrap();
928+
929+
// sleep for a second to wait for proper broadcast
930+
tokio::time::sleep(Duration::from_secs(1)).await;
931+
932+
// confirm the tx
933+
generate_blocks(&bitcoind, 6);
934+
935+
// sleep for a second to wait for sync
936+
tokio::time::sleep(Duration::from_secs(1)).await;
937+
938+
// wait for payment to complete
939+
test_utils::wait_for_condition("on-chain payment completion", || async {
940+
let payments = wallet.list_transactions().await.unwrap();
941+
let payment = payments.into_iter().find(|p| p.outbound);
942+
if payment.as_ref().is_some_and(|p| p.status == TxStatus::Failed) {
943+
panic!("Payment failed");
944+
}
945+
payment.is_some_and(|p| p.status == TxStatus::Completed)
946+
})
947+
.await;
948+
949+
// check the payment is correct
950+
let payments = wallet.list_transactions().await.unwrap();
951+
let payment = payments.into_iter().find(|p| p.outbound).unwrap();
952+
953+
// Comprehensive validation for outgoing on-chain payment
954+
assert_eq!(payment.amount, Some(send_amount), "Amount should equal sent amount");
955+
assert!(
956+
payment.fee.is_some_and(|f| f > Amount::ZERO),
957+
"On-chain payment should have non-zero fees"
958+
);
959+
assert!(payment.outbound, "Outgoing payment should be outbound");
960+
assert!(
961+
matches!(payment.payment_type, PaymentType::OutgoingOnChain { .. }),
962+
"Payment type should be OutgoingOnChain"
963+
);
964+
assert_eq!(payment.status, TxStatus::Completed, "Payment should be completed");
965+
assert_ne!(payment.time_since_epoch, Duration::ZERO, "Time should be set");
966+
967+
// Validate fee is reasonable for on-chain (should be less than 1% of sent amount)
968+
let fee_ratio = payment.fee.unwrap().milli_sats() as f64 / send_amount.milli_sats() as f64;
969+
assert!(
970+
fee_ratio < 0.01,
971+
"On-chain fee should be less than 1% of sent amount, got {:.2}%",
972+
fee_ratio * 100.0
973+
);
974+
975+
// Check that payment_type contains txid for completed payments
976+
if let PaymentType::OutgoingOnChain { txid } = &payment.payment_type {
977+
assert!(txid.is_some(), "Completed on-chain payment should have txid");
978+
}
979+
980+
// check balance left our wallet
981+
let bal = wallet.get_balance().await.unwrap();
982+
assert_eq!(
983+
bal.pending_balance,
984+
recv.saturating_sub(send_amount).saturating_sub(payment.fee.unwrap())
985+
);
986+
987+
// Wait for third party node to receive it
988+
test_utils::wait_for_condition("on-chain payment received", || async {
989+
let payments = third_party.list_payments();
990+
payments.iter().any(|p| {
991+
p.status == PaymentStatus::Succeeded
992+
&& p.direction == PaymentDirection::Inbound
993+
&& p.amount_msat == Some(send_amount.milli_sats())
994+
})
995+
})
996+
.await;
997+
})
998+
.await;
999+
}
1000+
8971001
#[tokio::test(flavor = "multi_thread")]
8981002
async fn test_force_close_handling() {
8991003
test_utils::run_test(|params| async move {

0 commit comments

Comments
 (0)