From 800dc26e845fe8f36560e01d7d4b31710ecfbab9 Mon Sep 17 00:00:00 2001 From: ian Date: Tue, 4 Nov 2025 14:11:47 +0800 Subject: [PATCH] feat: multi-hop fiber payments for cch CCH is added before multi-hop payments so it manipulates TLC directly for fiber payments. This PR adds multi-hop payments support to CCH. --- crates/fiber-bin/src/main.rs | 30 +- crates/fiber-lib/src/cch/actor.rs | 468 +++++++++--------- crates/fiber-lib/src/cch/error.rs | 6 +- crates/fiber-lib/src/cch/mod.rs | 4 +- crates/fiber-lib/src/cch/order.rs | 116 ++--- crates/fiber-lib/src/cch/orders_db.rs | 64 +-- crates/fiber-lib/src/fiber/types.rs | 44 +- crates/fiber-lib/src/rpc/README.md | 64 ++- crates/fiber-lib/src/rpc/biscuit.rs | 2 +- crates/fiber-lib/src/rpc/cch.rs | 146 ++---- docs/biscuit-auth.md | 2 +- .../cross-chain-hub/01-add-btc-invoice.bru | 2 +- .../02-create-send-btc-order.bru | 2 +- .../04-node1-open-channel-to-node3.bru | 2 +- .../e2e/cross-chain-hub/07-node1-add-tlc.bru | 50 -- .../cross-chain-hub/07-node1-send-payment.bru | 39 ++ .../09-create-receive-btc-order.bru | 61 --- .../09-node1-add-fiber-invoice.bru | 66 +++ .../10-create-receive-btc-order.bru | 44 ++ .../11-check-receive-btc-order.bru | 34 ++ ...btc-invoice.bru => 12-pay-btc-invoice.bru} | 4 +- .../12-remove-tlc-for-receive-btc-order.bru | 38 -- ...r-tlc-id.bru => 13-get-invoice-status.bru} | 29 +- ...bru => 14-node1-send-shutdown-channel.bru} | 6 +- ...-channel.bru => 15-node3-list-channel.bru} | 6 +- tests/bruno/environments/test.bru | 1 + tests/bruno/environments/xudt-test.bru | 1 + 27 files changed, 643 insertions(+), 688 deletions(-) delete mode 100644 tests/bruno/e2e/cross-chain-hub/07-node1-add-tlc.bru create mode 100644 tests/bruno/e2e/cross-chain-hub/07-node1-send-payment.bru delete mode 100644 tests/bruno/e2e/cross-chain-hub/09-create-receive-btc-order.bru create mode 100644 tests/bruno/e2e/cross-chain-hub/09-node1-add-fiber-invoice.bru create mode 100644 tests/bruno/e2e/cross-chain-hub/10-create-receive-btc-order.bru create mode 100644 tests/bruno/e2e/cross-chain-hub/11-check-receive-btc-order.bru rename tests/bruno/e2e/cross-chain-hub/{10-pay-btc-invoice.bru => 12-pay-btc-invoice.bru} (96%) delete mode 100644 tests/bruno/e2e/cross-chain-hub/12-remove-tlc-for-receive-btc-order.bru rename tests/bruno/e2e/cross-chain-hub/{11-get-receive-btc-order-tlc-id.bru => 13-get-invoice-status.bru} (56%) rename tests/bruno/e2e/cross-chain-hub/{13-node1-send-shutdown-channel.bru => 14-node1-send-shutdown-channel.bru} (89%) rename tests/bruno/e2e/cross-chain-hub/{14-node3-list-channel.bru => 15-node3-list-channel.bru} (92%) diff --git a/crates/fiber-bin/src/main.rs b/crates/fiber-bin/src/main.rs index 9dd3ff2d3..4cc4f1690 100644 --- a/crates/fiber-bin/src/main.rs +++ b/crates/fiber-bin/src/main.rs @@ -2,7 +2,7 @@ use ckb_chain_spec::ChainSpec; use ckb_resource::Resource; use core::default::Default; use fnn::actors::RootActor; -use fnn::cch::CchMessage; +use fnn::cch::{CchArgs, CchMessage}; use fnn::ckb::contracts::TypeIDResolver; #[cfg(debug_assertions)] use fnn::ckb::contracts::{get_cell_deps, Contract}; @@ -308,16 +308,30 @@ pub async fn main() -> Result<(), ExitMessage> { None => (None, None, None), }; - let cch_actor = match config.cch { - Some(cch_config) => { + let cch_actor = match (config.cch, &network_actor) { + (Some(cch_config), Some(network_actor)) => { info!("Starting cch"); let ignore_startup_failure = cch_config.ignore_startup_failure; + let node_keypair = config + .fiber + .as_ref() + .ok_or_else(|| { + ExitMessage( + "failed to read secret key because fiber config is not available" + .to_string(), + ) + })? + .read_or_generate_secret_key() + .map_err(|err| ExitMessage(format!("failed to read secret key: {}", err)))?; match start_cch( - cch_config, - new_tokio_task_tracker(), - new_tokio_cancellation_token(), + CchArgs { + config: cch_config, + tracker: new_tokio_task_tracker(), + token: new_tokio_cancellation_token(), + network_actor: network_actor.clone(), + node_keypair, + }, root_actor.get_cell(), - network_actor.clone(), ) .await { @@ -350,7 +364,7 @@ pub async fn main() -> Result<(), ExitMessage> { } } } - None => None, + _ => None, }; // Start rpc service diff --git a/crates/fiber-lib/src/cch/actor.rs b/crates/fiber-lib/src/cch/actor.rs index 9632f6eca..6cd2f8a51 100644 --- a/crates/fiber-lib/src/cch/actor.rs +++ b/crates/fiber-lib/src/cch/actor.rs @@ -1,65 +1,53 @@ use anyhow::{anyhow, Context, Result}; use futures::StreamExt as _; -use hex::ToHex; use lightning_invoice::Bolt11Invoice; use lnd_grpc_tonic_client::{ create_invoices_client, create_router_client, invoicesrpc, lnrpc, routerrpc, InvoicesClient, RouterClient, Uri, }; - use ractor::{call, RpcReplyPort}; use ractor::{Actor, ActorCell, ActorProcessingErr, ActorRef}; +use secp256k1::{PublicKey, Secp256k1, SecretKey}; use serde::Deserialize; - -use crate::time::{Duration, SystemTime, UNIX_EPOCH}; use std::str::FromStr; +use tentacle::secio::SecioKeyPair; use tokio::{select, time::sleep}; use tokio_util::{sync::CancellationToken, task::TaskTracker}; +use crate::cch::order::CchInvoice; use crate::ckb::contracts::{get_script_by_contract, Contract}; -use crate::fiber::channel::{ - AddTlcCommand, ChannelCommand, ChannelCommandWithId, RemoveTlcCommand, TlcNotification, -}; +use crate::fiber::channel::TlcNotification; use crate::fiber::hash_algorithm::HashAlgorithm; -use crate::fiber::types::{Hash256, RemoveTlcFulfill, RemoveTlcReason, NO_SHARED_SECRET}; +use crate::fiber::network::SendPaymentCommand; +use crate::fiber::payment::PaymentStatus; +use crate::fiber::types::{Hash256, Privkey}; use crate::fiber::{NetworkActorCommand, NetworkActorMessage}; -use crate::invoice::Currency; -use crate::now_timestamp_as_millis_u64; +use crate::invoice::{CkbInvoice, Currency, InvoiceBuilder}; +use crate::time::{Duration, SystemTime, UNIX_EPOCH}; use super::error::CchDbError; -use super::{CchConfig, CchError, CchOrderStatus, CchOrdersDb, ReceiveBTCOrder, SendBTCOrder}; +use super::{CchConfig, CchError, CchOrder, CchOrderStatus, CchOrdersDb}; pub const BTC_PAYMENT_TIMEOUT_SECONDS: i32 = 60; pub const DEFAULT_ORDER_EXPIRY_SECONDS: u64 = 86400; // 24 hours -pub async fn start_cch( - config: CchConfig, - tracker: TaskTracker, - token: CancellationToken, - root_actor: ActorCell, - network_actor: Option>, -) -> Result> { - let (actor, _handle) = Actor::spawn_linked( - Some("cch actor".to_string()), - CchActor::new(config, tracker, token, network_actor), - (), - root_actor, - ) - .await?; +pub async fn start_cch(args: CchArgs, root_actor: ActorCell) -> Result> { + let (actor, _handle) = + Actor::spawn_linked(Some("cch actor".to_string()), CchActor, args, root_actor).await?; Ok(actor) } #[derive(Debug)] pub struct SettleSendBTCOrderEvent { - payment_hash: String, - preimage: Option, + payment_hash: Hash256, + preimage: Option, status: CchOrderStatus, } #[derive(Debug)] pub struct SettleReceiveBTCOrderEvent { - payment_hash: String, - preimage: Option, + payment_hash: Hash256, + preimage: Option, status: CchOrderStatus, } @@ -71,24 +59,14 @@ pub struct SendBTC { #[derive(Clone, Debug, Deserialize)] pub struct ReceiveBTC { - /// Payment hash for the HTLC for both CKB and BTC. - pub payment_hash: String, - - /// Assume that the cross-chain hub already has a channel to the payee and the channel has - /// enough balance to pay the order. - /// TODO: Let the cross-chain hub create a channel to the payee on demand. - pub channel_id: Hash256, - /// Amount required to pay in Satoshis via BTC, including the fee for the cross-chain hub - pub amount_sats: u128, - /// Expiry set for the HTLC for the CKB payment to the payee. - pub final_tlc_expiry: u64, + pub fiber_pay_req: String, } pub enum CchMessage { - SendBTC(SendBTC, RpcReplyPort>), - ReceiveBTC(ReceiveBTC, RpcReplyPort>), + SendBTC(SendBTC, RpcReplyPort>), + ReceiveBTC(ReceiveBTC, RpcReplyPort>), - GetReceiveBTCOrder(String, RpcReplyPort>), + GetCchOrder(Hash256, RpcReplyPort>), SettleSendBTCOrder(SettleSendBTCOrderEvent), SettleReceiveBTCOrder(SettleReceiveBTCOrderEvent), @@ -128,31 +106,40 @@ impl LndConnectionInfo { } } -pub struct CchActor { - config: CchConfig, - tracker: TaskTracker, - token: CancellationToken, - network_actor: Option>, +#[derive(Default)] +pub struct CchActor; + +pub struct CchArgs { + pub config: CchConfig, + pub tracker: TaskTracker, + pub token: CancellationToken, + pub network_actor: ActorRef, + pub node_keypair: crate::fiber::KeyPair, } pub struct CchState { + config: CchConfig, + tracker: TaskTracker, + token: CancellationToken, + network_actor: ActorRef, + node_keypair: (PublicKey, SecretKey), lnd_connection: LndConnectionInfo, orders_db: CchOrdersDb, } -#[cfg_attr(target_arch="wasm32",async_trait::async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] + +#[async_trait::async_trait] impl Actor for CchActor { type Msg = CchMessage; type State = CchState; - type Arguments = (); + type Arguments = CchArgs; async fn pre_start( &self, myself: ActorRef, - _config: Self::Arguments, + args: Self::Arguments, ) -> Result { - let lnd_rpc_url: Uri = self.config.lnd_rpc_url.clone().try_into()?; - let cert = match self.config.resolve_lnd_cert_path() { + let lnd_rpc_url: Uri = args.config.lnd_rpc_url.clone().try_into()?; + let cert = match args.config.resolve_lnd_cert_path() { Some(path) => Some( tokio::fs::read(&path) .await @@ -160,7 +147,7 @@ impl Actor for CchActor { ), None => None, }; - let macaroon = match self.config.resolve_lnd_macaroon_path() { + let macaroon = match args.config.resolve_lnd_macaroon_path() { Some(path) => Some( tokio::fs::read(&path) .await @@ -174,15 +161,36 @@ impl Actor for CchActor { macaroon, }; - let payments_tracker = - LndPaymentsTracker::new(myself.clone(), lnd_connection.clone(), self.token.clone()); - self.tracker - .spawn(async move { payments_tracker.run().await }); + let private_key: Privkey = <[u8; 32]>::try_from(args.node_keypair.as_ref()) + .expect("valid length for key") + .into(); + let secio_kp = SecioKeyPair::from(args.node_keypair); - Ok(CchState { - lnd_connection, + let node_keypair = ( + PublicKey::from_slice(secio_kp.public_key().inner_ref()).expect("valid public key"), + private_key.into(), + ); + + let state = CchState { + config: args.config, + tracker: args.tracker, + token: args.token, + network_actor: args.network_actor, orders_db: Default::default(), - }) + node_keypair, + lnd_connection, + }; + + let payments_tracker = LndPaymentsTracker::new( + myself.clone(), + state.lnd_connection.clone(), + state.token.clone(), + ); + state + .tracker + .spawn(async move { payments_tracker.run().await }); + + Ok(state) } async fn handle( @@ -193,7 +201,7 @@ impl Actor for CchActor { ) -> Result<(), ActorProcessingErr> { match message { CchMessage::SendBTC(send_btc, port) => { - let result = self.send_btc(state, send_btc).await; + let result = state.send_btc(send_btc).await; if !port.is_closed() { // ignore error let _ = port.send(result); @@ -201,17 +209,17 @@ impl Actor for CchActor { Ok(()) } CchMessage::ReceiveBTC(receive_btc, port) => { - let result = self.receive_btc(myself, state, receive_btc).await; + let result = state.receive_btc(myself, receive_btc).await; if !port.is_closed() { // ignore error let _ = port.send(result); } Ok(()) } - CchMessage::GetReceiveBTCOrder(payment_hash, port) => { + CchMessage::GetCchOrder(payment_hash, port) => { let result = state .orders_db - .get_receive_btc_order(&payment_hash) + .get_cch_order(&payment_hash) .await .map_err(Into::into); if !port.is_closed() { @@ -222,21 +230,21 @@ impl Actor for CchActor { } CchMessage::SettleSendBTCOrder(event) => { tracing::debug!("settle_send_btc_order {:?}", event); - if let Err(err) = self.settle_send_btc_order(state, event).await { + if let Err(err) = state.settle_send_btc_order(event).await { tracing::error!("settle_send_btc_order failed: {}", err); } Ok(()) } CchMessage::SettleReceiveBTCOrder(event) => { tracing::debug!("settle_receive_btc_order {:?}", event); - if let Err(err) = self.settle_receive_btc_order(state, event).await { + if let Err(err) = state.settle_receive_btc_order(event).await { tracing::error!("settle_receive_btc_order failed: {}", err); } Ok(()) } CchMessage::PendingReceivedTlcNotification(tlc_notification) => { - if let Err(err) = self - .handle_pending_received_tlc_notification(state, tlc_notification) + if let Err(err) = state + .handle_pending_received_tlc_notification(tlc_notification) .await { tracing::error!("handle_pending_received_tlc_notification failed: {}", err); @@ -244,8 +252,8 @@ impl Actor for CchActor { Ok(()) } CchMessage::SettledTlcNotification(tlc_notification) => { - if let Err(err) = self - .handle_settled_tlc_notification(state, tlc_notification) + if let Err(err) = state + .handle_settled_tlc_notification(tlc_notification) .await { tracing::error!("handle_settled_tlc_notification failed: {}", err); @@ -256,30 +264,13 @@ impl Actor for CchActor { } } -impl CchActor { - pub fn new( - config: CchConfig, - tracker: TaskTracker, - token: CancellationToken, - network_actor: Option>, - ) -> Self { - Self { - config, - tracker, - token, - network_actor, - } - } - - async fn send_btc( - &self, - state: &mut CchState, - send_btc: SendBTC, - ) -> Result { +impl CchState { + async fn send_btc(&mut self, send_btc: SendBTC) -> Result { let duration_since_epoch = SystemTime::now().duration_since(UNIX_EPOCH)?; let invoice = Bolt11Invoice::from_str(&send_btc.btc_pay_req)?; tracing::debug!("BTC invoice: {:?}", invoice); + let payment_hash = (*invoice.payment_hash()).into(); let expiry = invoice .expires_at() @@ -308,25 +299,47 @@ impl CchActor { .as_ref(), ) .into(); - let mut order = SendBTCOrder { - expires_after: expiry, + let invoice_amount_sats = amount_msat.div_ceil(1_000u128) + fee_sats; + + let invoice = InvoiceBuilder::new(send_btc.currency) + .amount(Some(invoice_amount_sats)) + .payment_hash(payment_hash) + .hash_algorithm(HashAlgorithm::Sha256) + .expiry_time(Duration::from_secs(expiry)) + .final_expiry_delta(self.config.ckb_final_tlc_expiry_delta) + .udt_type_script(wrapped_btc_type_script.clone().into()) + .payee_pub_key(self.node_keypair.0) + .build_with_sign(|hash| { + Secp256k1::new().sign_ecdsa_recoverable(hash, &self.node_keypair.1) + })?; + + let message = { + let invoice = invoice.clone(); + move |rpc_reply| -> NetworkActorMessage { + NetworkActorMessage::Command(NetworkActorCommand::AddInvoice( + invoice.clone(), + None, + rpc_reply, + )) + } + }; + call!(self.network_actor, message).expect("call actor")?; + + let order = CchOrder { wrapped_btc_type_script, fee_sats, - currency: send_btc.currency, + payment_hash, + expires_after: expiry, created_at: duration_since_epoch.as_secs(), ckb_final_tlc_expiry_delta: self.config.ckb_final_tlc_expiry_delta, - btc_pay_req: send_btc.btc_pay_req, - ckb_pay_req: Default::default(), - payment_hash: format!("0x{}", invoice.payment_hash().encode_hex::()), + outgoing_pay_req: send_btc.btc_pay_req, + incoming_invoice: CchInvoice::Fiber(invoice), payment_preimage: None, - channel_id: None, - tlc_id: None, - amount_sats: amount_msat.div_ceil(1_000u128) + fee_sats, + amount_sats: invoice_amount_sats, status: CchOrderStatus::Pending, }; - order.generate_ckb_invoice()?; - state.orders_db.insert_send_btc_order(order.clone()).await?; + self.orders_db.insert_cch_order(order.clone()).await?; // TODO(now): save order and invoice into db: store.insert_invoice(invoice.clone()) Ok(order) @@ -334,17 +347,18 @@ impl CchActor { // On receiving new TLC, check whether it matches the SendBTC order async fn handle_pending_received_tlc_notification( - &self, - state: &mut CchState, + &mut self, tlc_notification: TlcNotification, ) -> Result<()> { - let payment_hash = format!("{:#x}", tlc_notification.tlc.payment_hash); + let payment_hash = tlc_notification.tlc.payment_hash; tracing::debug!("[inbounding tlc] payment hash: {}", payment_hash); - let mut order = match state.orders_db.get_send_btc_order(&payment_hash).await { + let mut order = match self.orders_db.get_cch_order(&payment_hash).await { Err(CchDbError::NotFound(_)) => return Ok(()), Err(err) => return Err(err.into()), - Ok(order) => order, + Ok(order) if order.is_from_fiber_to_lightning() => order, + // Ignore if the order is not from fiber to lightning + Ok(_) => return Ok(()), }; if order.status != CchOrderStatus::Pending { @@ -356,18 +370,17 @@ impl CchActor { return Err(CchError::SendBTCReceivedAmountTooSmall.into()); } - order.channel_id = Some(tlc_notification.channel_id); - order.tlc_id = Some(tlc_notification.tlc.tlc_id.into()); - state.orders_db.update_send_btc_order(order.clone()).await?; + order.status = CchOrderStatus::IncomingAccepted; + self.orders_db.update_cch_order(order.clone()).await?; let req = routerrpc::SendPaymentRequest { - payment_request: order.btc_pay_req.clone(), + payment_request: order.outgoing_pay_req.clone(), timeout_seconds: BTC_PAYMENT_TIMEOUT_SECONDS, ..Default::default() }; tracing::debug!("[inbounding tlc] SendPaymentRequest: {:?}", req); - let mut client = state.lnd_connection.create_router_client().await?; + let mut client = self.lnd_connection.create_router_client().await?; // TODO: set a fee let mut stream = client.send_payment_v2(req).await?.into_inner(); // Wait for the first message then quit @@ -376,8 +389,8 @@ impl CchActor { tracing::debug!("[inbounding tlc] payment result: {:?}", payment_result_opt); if let Some(Ok(payment)) = payment_result_opt { order.status = lnrpc::payment::PaymentStatus::try_from(payment.status)?.into(); - state.orders_db - .update_send_btc_order(order) + self.orders_db + .update_cch_order(order) .await?; } } @@ -391,19 +404,18 @@ impl CchActor { } async fn handle_settled_tlc_notification( - &self, - state: &mut CchState, + &mut self, tlc_notification: TlcNotification, ) -> Result<()> { - let payment_hash = format!("{:#x}", tlc_notification.tlc.payment_hash); + let payment_hash = tlc_notification.tlc.payment_hash; tracing::debug!("[settled tlc] payment hash: {}", payment_hash); - match state.orders_db.get_receive_btc_order(&payment_hash).await { + let mut order = match self.orders_db.get_cch_order(&payment_hash).await { Err(CchDbError::NotFound(_)) => return Ok(()), Err(err) => return Err(err.into()), - _ => { - // ignore - } + // Ignore if the order is from fiber to lightning + Ok(order) if order.is_from_fiber_to_lightning() => return Ok(()), + Ok(order) => order, }; let preimage = tlc_notification @@ -413,88 +425,75 @@ impl CchActor { tracing::debug!("[settled tlc] preimage: {:#x}", preimage); + order.status = CchOrderStatus::OutgoingSettled; + order.payment_preimage = Some(preimage); + self.orders_db.update_cch_order(order.clone()).await?; + // settle the lnd invoice let req = invoicesrpc::SettleInvoiceMsg { - preimage: preimage.as_ref().to_vec(), + preimage: preimage.into(), }; tracing::debug!("[settled tlc] SettleInvoiceMsg: {:?}", req); - let mut client = state.lnd_connection.create_invoices_client().await?; + let mut client = self.lnd_connection.create_invoices_client().await?; // TODO: set a fee let resp = client.settle_invoice(req).await?.into_inner(); tracing::debug!("[settled tlc] SettleInvoiceResp: {:?}", resp); + order.status = CchOrderStatus::Succeeded; + self.orders_db.update_cch_order(order.clone()).await?; + Ok(()) } - async fn settle_send_btc_order( - &self, - state: &mut CchState, - event: SettleSendBTCOrderEvent, - ) -> Result<()> { - let mut order = match state - .orders_db - .get_send_btc_order(&event.payment_hash) - .await - { + async fn settle_send_btc_order(&mut self, event: SettleSendBTCOrderEvent) -> Result<()> { + let payment_hash = event.payment_hash; + + let mut order = match self.orders_db.get_cch_order(&event.payment_hash).await { Err(CchDbError::NotFound(_)) => return Ok(()), Err(err) => return Err(err.into()), + Ok(order) if !order.is_from_fiber_to_lightning() => return Ok(()), Ok(order) => order, }; order.status = event.status; - if let (Some(preimage), Some(network_actor), Some(channel_id), Some(tlc_id)) = ( - event.preimage, - &self.network_actor, - order.channel_id, - order.tlc_id, - ) { + if let Some(preimage) = event.preimage { tracing::info!( "SettleSendBTCOrder: payment_hash={}, status={:?}", - event.payment_hash, + payment_hash, event.status ); - order.payment_preimage = Some(preimage.clone()); - - let message = move |rpc_reply| -> NetworkActorMessage { - NetworkActorMessage::Command(NetworkActorCommand::ControlFiberChannel( - ChannelCommandWithId { - channel_id, - command: ChannelCommand::RemoveTlc( - RemoveTlcCommand { - id: tlc_id, - reason: RemoveTlcReason::RemoveTlcFulfill(RemoveTlcFulfill { - payment_preimage: Hash256::from_str(&preimage) - .expect("decode preimage"), - }), - }, - rpc_reply, - ), - }, + order.payment_preimage = Some(preimage); + self.orders_db.update_cch_order(order.clone()).await?; + + let command = move |rpc_reply| -> NetworkActorMessage { + NetworkActorMessage::Command(NetworkActorCommand::SettleInvoice( + payment_hash, + preimage, + rpc_reply, )) }; - - call!(network_actor, message) - .expect("call actor") - .map_err(|msg| anyhow!(msg))?; + call!(self.network_actor, command).expect("call actor")?; + order.status = CchOrderStatus::Succeeded; } - state.orders_db.update_send_btc_order(order).await?; + self.orders_db.update_cch_order(order).await?; Ok(()) } async fn receive_btc( - &self, + &mut self, myself: ActorRef, - state: &mut CchState, receive_btc: ReceiveBTC, - ) -> Result { + ) -> Result { + let invoice = CkbInvoice::from_str(&receive_btc.fiber_pay_req)?; + let payment_hash = *invoice.payment_hash(); + let amount_sats = invoice.amount().ok_or(CchError::CKBInvoiceMissingAmount)?; + let final_tlc_minimum_expiry_delta = + *invoice.final_tlc_minimum_expiry_delta().unwrap_or(&0); let duration_since_epoch = SystemTime::now().duration_since(UNIX_EPOCH)?; - let hash_bin = hex::decode(receive_btc.payment_hash.trim_start_matches("0x")) - .map_err(|_| CchError::HexDecodingError(receive_btc.payment_hash.clone()))?; - let amount_sats = receive_btc.amount_sats; let fee_sats = amount_sats * (self.config.fee_rate_per_million_sats as u128) / 1_000_000u128 + (self.config.base_fee_sats as u128); @@ -505,20 +504,20 @@ impl CchActor { return Err(CchError::ReceiveBTCOrderAmountTooLarge); } - let mut client = state.lnd_connection.create_invoices_client().await?; + let mut client = self.lnd_connection.create_invoices_client().await?; let req = invoicesrpc::AddHoldInvoiceRequest { - hash: hash_bin, + hash: payment_hash.as_ref().to_vec(), value_msat: (amount_sats * 1_000u128) as i64, expiry: DEFAULT_ORDER_EXPIRY_SECONDS as i64, - cltv_expiry: self.config.btc_final_tlc_expiry + receive_btc.final_tlc_expiry, + cltv_expiry: self.config.btc_final_tlc_expiry + final_tlc_minimum_expiry_delta, ..Default::default() }; - let invoice = client + let add_invoice_resp = client .add_hold_invoice(req) .await .map_err(|err| CchError::LndRpcError(err.to_string()))? .into_inner(); - let btc_pay_req = invoice.payment_request; + let incoming_invoice = Bolt11Invoice::from_str(&add_invoice_resp.payment_request)?; let wrapped_btc_type_script: ckb_jsonrpc_types::Script = get_script_by_contract( Contract::SimpleUDT, @@ -533,31 +532,26 @@ impl CchActor { .as_ref(), ) .into(); - let order = ReceiveBTCOrder { + let order = CchOrder { created_at: duration_since_epoch.as_secs(), expires_after: DEFAULT_ORDER_EXPIRY_SECONDS, - ckb_final_tlc_expiry_delta: receive_btc.final_tlc_expiry, - btc_pay_req, - payment_hash: receive_btc.payment_hash.clone(), + ckb_final_tlc_expiry_delta: final_tlc_minimum_expiry_delta, + outgoing_pay_req: receive_btc.fiber_pay_req, + incoming_invoice: CchInvoice::Lightning(incoming_invoice), + payment_hash, payment_preimage: None, amount_sats, fee_sats, status: CchOrderStatus::Pending, wrapped_btc_type_script, - // TODO: check the channel exists and has enough local balance. - channel_id: receive_btc.channel_id, - tlc_id: None, }; - state - .orders_db - .insert_receive_btc_order(order.clone()) - .await?; + self.orders_db.insert_cch_order(order.clone()).await?; let invoice_tracker = LndInvoiceTracker::new( myself, - receive_btc.payment_hash, - state.lnd_connection.clone(), + payment_hash, + self.lnd_connection.clone(), self.token.clone(), ); self.tracker @@ -566,63 +560,42 @@ impl CchActor { Ok(order) } - async fn settle_receive_btc_order( - &self, - state: &mut CchState, - event: SettleReceiveBTCOrderEvent, - ) -> Result<()> { - let mut order = match state - .orders_db - .get_receive_btc_order(&event.payment_hash) - .await - { + async fn settle_receive_btc_order(&mut self, event: SettleReceiveBTCOrderEvent) -> Result<()> { + let mut order = match self.orders_db.get_cch_order(&event.payment_hash).await { Err(CchDbError::NotFound(_)) => return Ok(()), Err(err) => return Err(err.into()), + Ok(order) if order.is_from_fiber_to_lightning() => return Ok(()), Ok(order) => order, }; - if event.status == CchOrderStatus::Accepted && self.network_actor.is_some() { - // AddTlc to initiate the CKB payment + order.status = event.status; + order.payment_preimage = event.preimage; + + if event.status == CchOrderStatus::IncomingAccepted { let message = |rpc_reply| -> NetworkActorMessage { - NetworkActorMessage::Command(NetworkActorCommand::ControlFiberChannel( - ChannelCommandWithId { - channel_id: order.channel_id, - command: ChannelCommand::AddTlc( - AddTlcCommand { - amount: order.amount_sats - order.fee_sats, - payment_hash: Hash256::from_str(&order.payment_hash) - .expect("parse Hash256"), - attempt_id: None, - expiry: now_timestamp_as_millis_u64() - + self.config.ckb_final_tlc_expiry_delta, - hash_algorithm: HashAlgorithm::Sha256, - onion_packet: None, - shared_secret: NO_SHARED_SECRET, - previous_tlc: None, - }, - rpc_reply, - ), + NetworkActorMessage::Command(NetworkActorCommand::SendPayment( + SendPaymentCommand { + invoice: Some(order.outgoing_pay_req.clone()), + ..Default::default() }, + rpc_reply, )) }; - let tlc_response = call!( - self.network_actor - .as_ref() - .expect("CCH requires network actor"), - message - ) - .expect("call actor") - .map_err(|msg| anyhow!(msg))?; - order.tlc_id = Some(tlc_response.tlc_id); - } - order.status = event.status; - order.payment_preimage = event.preimage.clone(); + let payment_status = call!(self.network_actor, message) + .expect("call actor") + .map_err(|err| anyhow!("{}", err))? + .status; + + let mut order = order.clone(); + if payment_status == PaymentStatus::Failed { + order.status = CchOrderStatus::Failed; + } else { + order.status = CchOrderStatus::OutgoingInFlight; + } + } - state - .orders_db - .update_receive_btc_order(order.clone()) - .await?; + self.orders_db.update_cch_order(order.clone()).await?; Ok(()) } } @@ -715,13 +688,18 @@ impl LndPaymentsTracker { async fn on_payment(&self, payment: lnrpc::Payment) -> Result<()> { tracing::debug!(target: "fnn::cch::actor::tracker::lnd_payments", "payment: {:?}", payment); + let preimage = if !payment.payment_preimage.is_empty() { + Some(Hash256::from_str(&payment.payment_preimage)?) + } else { + None + }; + let event = CchMessage::SettleSendBTCOrder(SettleSendBTCOrderEvent { - payment_hash: format!("0x{}", payment.payment_hash), - preimage: (!payment.payment_preimage.is_empty()) - .then(|| format!("0x{}", payment.payment_preimage)), + payment_hash: Hash256::from_str(&payment.payment_hash)?, + preimage, status: lnrpc::payment::PaymentStatus::try_from(payment.status) .map(Into::into) - .unwrap_or(CchOrderStatus::InFlight), + .unwrap_or(CchOrderStatus::OutgoingInFlight), }); self.cch_actor.cast(event).map_err(Into::into) } @@ -734,7 +712,7 @@ impl LndPaymentsTracker { /// struct LndInvoiceTracker { cch_actor: ActorRef, - payment_hash: String, + payment_hash: Hash256, lnd_connection: LndConnectionInfo, token: CancellationToken, } @@ -742,7 +720,7 @@ struct LndInvoiceTracker { impl LndInvoiceTracker { fn new( cch_actor: ActorRef, - payment_hash: String, + payment_hash: Hash256, lnd_connection: LndConnectionInfo, token: CancellationToken, ) -> Self { @@ -798,7 +776,7 @@ impl LndInvoiceTracker { // TODO: clean up expired orders let mut stream = client .subscribe_single_invoice(invoicesrpc::SubscribeSingleInvoiceRequest { - r_hash: hex::decode(self.payment_hash.trim_start_matches("0x"))?, + r_hash: self.payment_hash.into(), }) .await? .into_inner(); @@ -828,10 +806,14 @@ impl LndInvoiceTracker { let status = lnrpc::invoice::InvoiceState::try_from(invoice.state) .map(Into::into) .unwrap_or(CchOrderStatus::Pending); + let preimage = if !invoice.r_preimage.is_empty() { + Some(Hash256::try_from(invoice.r_preimage.as_slice())?) + } else { + None + }; let event = CchMessage::SettleReceiveBTCOrder(SettleReceiveBTCOrderEvent { - payment_hash: format!("0x{}", hex::encode(invoice.r_hash)), - preimage: (!invoice.r_preimage.is_empty()) - .then(|| format!("0x{}", hex::encode(invoice.r_preimage))), + payment_hash: Hash256::try_from(invoice.r_hash.as_slice())?, + preimage, status, }); self.cch_actor.cast(event)?; diff --git a/crates/fiber-lib/src/cch/error.rs b/crates/fiber-lib/src/cch/error.rs index c86d8c808..fab440947 100644 --- a/crates/fiber-lib/src/cch/error.rs +++ b/crates/fiber-lib/src/cch/error.rs @@ -1,4 +1,4 @@ -use crate::time::SystemTimeError; +use crate::{invoice::SettleInvoiceError, time::SystemTimeError}; use jsonrpsee::types::{error::CALL_EXECUTION_FAILED_CODE, ErrorObjectOwned}; use thiserror::Error; @@ -24,6 +24,10 @@ pub enum CchError { BTCInvoiceMissingAmount, #[error("CKB invoice error: {0}")] CKBInvoiceError(#[from] crate::invoice::InvoiceError), + #[error("CKB invoice missing amount")] + CKBInvoiceMissingAmount, + #[error("Fail to settle CKB invoice: {0}")] + CKBSettleInvoiceError(#[from] SettleInvoiceError), #[error("SendBTC order already paid")] SendBTCOrderAlreadyPaid, #[error("SendBTC received payment amount is too small")] diff --git a/crates/fiber-lib/src/cch/mod.rs b/crates/fiber-lib/src/cch/mod.rs index 4f0733dd9..517b396d9 100644 --- a/crates/fiber-lib/src/cch/mod.rs +++ b/crates/fiber-lib/src/cch/mod.rs @@ -1,5 +1,5 @@ mod actor; -pub use actor::{start_cch, CchActor, CchMessage, ReceiveBTC, SendBTC}; +pub use actor::{start_cch, CchActor, CchArgs, CchMessage, ReceiveBTC, SendBTC}; mod error; pub use error::{CchError, CchResult}; @@ -11,7 +11,7 @@ pub use config::{ }; mod order; -pub use order::{CchOrderStatus, ReceiveBTCOrder, SendBTCOrder}; +pub use order::{CchInvoice, CchOrder, CchOrderStatus}; mod orders_db; pub use orders_db::CchOrdersDb; diff --git a/crates/fiber-lib/src/cch/order.rs b/crates/fiber-lib/src/cch/order.rs index 17c57c877..476adb19f 100644 --- a/crates/fiber-lib/src/cch/order.rs +++ b/crates/fiber-lib/src/cch/order.rs @@ -1,15 +1,14 @@ -use super::CchError; +use lightning_invoice::Bolt11Invoice; use lnd_grpc_tonic_client::lnrpc; use serde::{Deserialize, Serialize}; -use serde_with::serde_as; -use std::{str::FromStr as _, time::Duration}; +use serde_with::{serde_as, DisplayFromStr}; use crate::{ fiber::{ serde_utils::{U128Hex, U64Hex}, types::Hash256, }, - invoice::{Currency, InvoiceBuilder}, + invoice::CkbInvoice, }; /// The status of a cross-chain hub order, will update as the order progresses. @@ -18,35 +17,36 @@ use crate::{ pub enum CchOrderStatus { /// Order is created and has not send out payments yet. Pending = 0, - /// HTLC in the first half is accepted. - Accepted = 1, - /// There's an outgoing payment in flight for the second half. - InFlight = 2, - /// Order is settled. - Succeeded = 3, + /// HTLC in the incoming payment is accepted. + IncomingAccepted = 1, + /// There's an outgoing payment in flight. + OutgoingInFlight = 2, + /// The outgoing payment is settled. + OutgoingSettled = 3, + /// Both payments are settled and the order succeeds. + Succeeded = 4, /// Order is failed. - Failed = 4, + Failed = 5, } -/// lnd payment is the second half of SendBTCOrder +/// Lnd payment is the outgoing part of a CCHOrder to send BTC from Fiber to Lightning impl From for CchOrderStatus { fn from(status: lnrpc::payment::PaymentStatus) -> Self { use lnrpc::payment::PaymentStatus; match status { - PaymentStatus::Succeeded => CchOrderStatus::Succeeded, + PaymentStatus::Succeeded => CchOrderStatus::OutgoingSettled, PaymentStatus::Failed => CchOrderStatus::Failed, - _ => CchOrderStatus::InFlight, + _ => CchOrderStatus::OutgoingInFlight, } } } -/// lnd invoice is the first half of ReceiveBTCOrder +/// Lnd invoice is the incoming part of a CCHOrder to receive BTC from Lightning to Fiber impl From for CchOrderStatus { fn from(state: lnrpc::invoice::InvoiceState) -> Self { use lnrpc::invoice::InvoiceState; - // Set to InFlight only when a CKB HTLC is created match state { - InvoiceState::Accepted => CchOrderStatus::Accepted, + InvoiceState::Accepted => CchOrderStatus::IncomingAccepted, InvoiceState::Canceled => CchOrderStatus::Failed, InvoiceState::Settled => CchOrderStatus::Succeeded, _ => CchOrderStatus::Pending, @@ -54,81 +54,43 @@ impl From for CchOrderStatus { } } +/// The generated proxy invoice for the incoming payment. +/// +/// The JSON representation: +/// +/// ```text +/// { "Fiber": String } | { "Lightning": String } +/// ``` #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SendBTCOrder { - // Seconds since epoch when the order is created - #[serde_as(as = "U64Hex")] - pub created_at: u64, - // Seconds after timestamp that the order expires - #[serde_as(as = "U64Hex")] - pub expires_after: u64, - // The minimal expiry delta in milliseconds of the final TLC hop in the CKB network - #[serde_as(as = "U64Hex")] - pub ckb_final_tlc_expiry_delta: u64, - - pub currency: Currency, - pub wrapped_btc_type_script: ckb_jsonrpc_types::Script, - - pub btc_pay_req: String, - pub ckb_pay_req: String, - pub payment_hash: String, - pub payment_preimage: Option, - pub channel_id: Option, - #[serde_as(as = "Option")] - pub tlc_id: Option, - - #[serde_as(as = "U128Hex")] - /// Amount required to pay in Satoshis via wrapped BTC, including the fee for the cross-chain hub - pub amount_sats: u128, - #[serde_as(as = "U128Hex")] - pub fee_sats: u128, - - pub status: CchOrderStatus, -} - -impl SendBTCOrder { - pub fn generate_ckb_invoice(&mut self) -> Result<(), CchError> { - let invoice_builder = InvoiceBuilder::new(self.currency) - .amount(Some(self.amount_sats)) - .payment_hash( - Hash256::from_str(&self.payment_hash) - .map_err(|_| CchError::HexDecodingError(self.payment_hash.clone()))?, - ) - .expiry_time(Duration::from_secs(self.expires_after)) - .final_expiry_delta(self.ckb_final_tlc_expiry_delta) - .udt_type_script(self.wrapped_btc_type_script.clone().into()); - - let invoice = invoice_builder.build()?; - self.ckb_pay_req = invoice.to_string(); - - Ok(()) - } +pub enum CchInvoice { + /// Fiber invoice that once paid, the hub will send the outgoing payment to Lightning + Fiber(#[serde_as(as = "DisplayFromStr")] CkbInvoice), + /// Lightning invoice that once paid, the hub will send the outgoing payment to Fiber + Lightning(#[serde_as(as = "DisplayFromStr")] Bolt11Invoice), } #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReceiveBTCOrder { +pub struct CchOrder { // Seconds since epoch when the order is created #[serde_as(as = "U64Hex")] pub created_at: u64, // Seconds after timestamp that the order expires #[serde_as(as = "U64Hex")] pub expires_after: u64, - // The minimal expiry in seconds of the final TLC in the CKB network + // The minimal expiry delta in milliseconds of the final TLC hop in the CKB network #[serde_as(as = "U64Hex")] pub ckb_final_tlc_expiry_delta: u64, pub wrapped_btc_type_script: ckb_jsonrpc_types::Script, - pub btc_pay_req: String, - pub payment_hash: String, - pub payment_preimage: Option, - pub channel_id: Hash256, - #[serde_as(as = "Option")] - pub tlc_id: Option, + pub outgoing_pay_req: String, + pub incoming_invoice: CchInvoice, + pub payment_hash: Hash256, + pub payment_preimage: Option, - /// Amount required to pay in Satoshis via BTC, including the fee for the cross-chain hub + /// Amount required to pay in Satoshis via BTC or wrapped BTC, including the fee for the cross-chain hub #[serde_as(as = "U128Hex")] pub amount_sats: u128, #[serde_as(as = "U128Hex")] @@ -136,3 +98,9 @@ pub struct ReceiveBTCOrder { pub status: CchOrderStatus, } + +impl CchOrder { + pub fn is_from_fiber_to_lightning(&self) -> bool { + matches!(self.incoming_invoice, CchInvoice::Fiber(_)) + } +} diff --git a/crates/fiber-lib/src/cch/orders_db.rs b/crates/fiber-lib/src/cch/orders_db.rs index c6573f04f..38117a43e 100644 --- a/crates/fiber-lib/src/cch/orders_db.rs +++ b/crates/fiber-lib/src/cch/orders_db.rs @@ -1,71 +1,37 @@ use std::collections::HashMap; -use super::{error::CchDbError, ReceiveBTCOrder, SendBTCOrder}; +use crate::fiber::types::Hash256; + +use super::{error::CchDbError, CchOrder}; // TODO: persist orders #[derive(Default)] pub struct CchOrdersDb { - /// SendBTCOrder map by payment hash - send_btc_orders: HashMap, - receive_btc_orders: HashMap, + /// Orders by payment hash + orders: HashMap, } impl CchOrdersDb { - pub async fn insert_send_btc_order(&mut self, order: SendBTCOrder) -> Result<(), CchDbError> { - let key = order.payment_hash.clone(); - match self.send_btc_orders.insert(key.clone(), order) { - Some(_) => Err(CchDbError::Duplicated(key)), - None => Ok(()), - } - } - - pub async fn get_send_btc_order( - &mut self, - payment_hash: &str, - ) -> Result { - self.send_btc_orders - .get(payment_hash) - .ok_or_else(|| CchDbError::NotFound(payment_hash.to_string())) - .cloned() - } - - pub async fn update_send_btc_order(&mut self, order: SendBTCOrder) -> Result<(), CchDbError> { - let key = order.payment_hash.clone(); - match self.send_btc_orders.insert(key.clone(), order) { - Some(_) => Ok(()), - None => Err(CchDbError::NotFound(key)), - } - } - - pub async fn insert_receive_btc_order( - &mut self, - order: ReceiveBTCOrder, - ) -> Result<(), CchDbError> { - let key = order.payment_hash.clone(); - match self.receive_btc_orders.insert(key.clone(), order) { - Some(_) => Err(CchDbError::Duplicated(key)), + pub async fn insert_cch_order(&mut self, order: CchOrder) -> Result<(), CchDbError> { + let key = order.payment_hash; + match self.orders.insert(key, order) { + Some(_) => Err(CchDbError::Duplicated(key.to_string())), None => Ok(()), } } - pub async fn get_receive_btc_order( - &mut self, - payment_hash: &str, - ) -> Result { - self.receive_btc_orders + pub async fn get_cch_order(&mut self, payment_hash: &Hash256) -> Result { + self.orders .get(payment_hash) .ok_or_else(|| CchDbError::NotFound(payment_hash.to_string())) .cloned() } - pub async fn update_receive_btc_order( - &mut self, - order: ReceiveBTCOrder, - ) -> Result<(), CchDbError> { - let key = order.payment_hash.clone(); - match self.receive_btc_orders.insert(key.clone(), order) { + pub async fn update_cch_order(&mut self, order: CchOrder) -> Result<(), CchDbError> { + let key = order.payment_hash; + match self.orders.insert(key, order) { Some(_) => Ok(()), - None => Err(CchDbError::NotFound(key)), + None => Err(CchDbError::NotFound(key.to_string())), } } } diff --git a/crates/fiber-lib/src/fiber/types.rs b/crates/fiber-lib/src/fiber/types.rs index eabb30583..db716bda0 100644 --- a/crates/fiber-lib/src/fiber/types.rs +++ b/crates/fiber-lib/src/fiber/types.rs @@ -13,14 +13,11 @@ use super::serde_utils::{EntityHex, PubNonceAsBytes, SliceBase58, SliceHex}; use crate::ckb::config::{UdtArgInfo, UdtCellDep, UdtCfgInfos, UdtDep, UdtScript}; use crate::ckb::contracts::get_udt_whitelist; use crate::fiber::network::USER_CUSTOM_RECORDS_MAX_INDEX; -use ckb_jsonrpc_types::CellOutput; -use ckb_types::H256; -use num_enum::IntoPrimitive; -use num_enum::TryFromPrimitive; -use std::convert::TryFrom; -use std::fmt::Debug; use anyhow::anyhow; +use bitcoin::hashes::Hash; +use ckb_jsonrpc_types::CellOutput; +use ckb_types::H256; use ckb_types::{ core::FeeRate, packed::{Byte32 as MByte32, BytesVec, OutPoint, Script, Transaction}, @@ -31,6 +28,8 @@ use fiber_sphinx::{OnionErrorPacket, SphinxError}; use molecule::prelude::{Builder, Byte, Entity}; use musig2::secp::{Point, Scalar}; use musig2::{BinaryEncoding, PartialSignature, PubNonce}; +use num_enum::IntoPrimitive; +use num_enum::TryFromPrimitive; use once_cell::sync::OnceCell; use ractor::concurrency::Duration; use secp256k1::{ @@ -41,6 +40,8 @@ use secp256k1::{Verification, XOnlyPublicKey}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use std::cmp::Ordering; +use std::convert::TryFrom; +use std::fmt::Debug; use std::fmt::Display; use std::str::FromStr; use strum::{AsRefStr, EnumString}; @@ -202,6 +203,31 @@ impl From for Hash256 { } } +impl From for Hash256 { + fn from(value: lightning_invoice::Sha256) -> Self { + Hash256(value.0.to_byte_array()) + } +} + +impl From for Hash256 { + fn from(value: bitcoin::hashes::sha256::Hash) -> Self { + Hash256(value.to_byte_array()) + } +} + +impl TryFrom<&[u8]> for Hash256 { + type Error = anyhow::Error; + + fn try_from(value: &[u8]) -> Result { + if value.len() != 32 { + return Err(anyhow!("Invalid hash length")); + } + let mut data = [0u8; 32]; + data.copy_from_slice(value); + Ok(Hash256(data)) + } +} + fn u8_32_as_byte_32(value: &[u8; 32]) -> MByte32 { MByte32::from_slice(value.as_slice()).expect("[u8; 32] to Byte32") } @@ -243,6 +269,12 @@ impl FromStr for Hash256 { } } +impl From for Vec { + fn from(val: Hash256) -> Self { + val.0.to_vec() + } +} + impl Privkey { pub fn from_slice(key: &[u8]) -> Self { SecretKey::from_slice(key) diff --git a/crates/fiber-lib/src/rpc/README.md b/crates/fiber-lib/src/rpc/README.md index 4e1ab2c64..748b1d82a 100644 --- a/crates/fiber-lib/src/rpc/README.md +++ b/crates/fiber-lib/src/rpc/README.md @@ -17,7 +17,7 @@ You may refer to the e2e test cases in the `tests/bruno/e2e` directory for examp * [Module Cch](#module-cch) * [Method `send_btc`](#cch-send_btc) * [Method `receive_btc`](#cch-receive_btc) - * [Method `get_receive_btc_order`](#cch-get_receive_btc_order) + * [Method `get_cch_order`](#cch-get_cch_order) * [Module Channel](#module-channel) * [Method `open_channel`](#channel-open_channel) * [Method `accept_channel`](#channel-accept_channel) @@ -63,6 +63,7 @@ You may refer to the e2e test cases in the `tests/bruno/e2e` directory for examp * [RPC Types](#rpc-types) * [Type `Attribute`](#type-attribute) + * [Type `CchInvoice`](#type-cchinvoice) * [Type `CchOrderStatus`](#type-cchorderstatus) * [Type `Channel`](#type-channel) * [Type `ChannelInfo`](#type-channelinfo) @@ -119,11 +120,10 @@ Send BTC to a address. * `timestamp` - `u64`, Seconds since epoch when the order is created * `expiry` - `u64`, Seconds after timestamp that the order expires * `ckb_final_tlc_expiry_delta` - `u64`, The minimal expiry in seconds of the final TLC in the CKB network -* `currency` - [Currency](#type-currency), Request currency * `wrapped_btc_type_script` - `ckb_jsonrpc_types::Script`, Wrapped BTC type script -* `btc_pay_req` - `String`, Payment request for BTC -* `ckb_pay_req` - `String`, Payment request for CKB -* `payment_hash` - `String`, Payment hash for the HTLC for both CKB and BTC. +* `incoming_invoice` - [CchInvoice](#type-cchinvoice), Generated invoice for the incoming payment +* `outgoing_pay_req` - `String`, The final payee to accept the payment. It has the different network with incoming invoice. +* `payment_hash` - [Hash256](#type-hash256), Payment hash for the HTLC for both CKB and BTC. * `amount_sats` - `u128`, Amount required to pay in Satoshis, including fee * `fee_sats` - `u128`, Fee in Satoshis * `status` - [CchOrderStatus](#type-cchorderstatus), Order status @@ -139,10 +139,7 @@ Receive BTC from a payment hash. ##### Params -* `payment_hash` - `String`, Payment hash for the HTLC for both CKB and BTC. -* `channel_id` - [Hash256](#type-hash256), Channel ID for the CKB payment. -* `amount_sats` - `u128`, How many satoshis to receive, excluding cross-chain hub fee. -* `final_tlc_expiry` - `u64`, Expiry set for the HTLC for the CKB payment to the payee. +* `fiber_pay_req` - `String`, Fiber payment request string ##### Returns @@ -150,11 +147,10 @@ Receive BTC from a payment hash. * `expiry` - `u64`, Seconds after timestamp that the order expires * `ckb_final_tlc_expiry_delta` - `u64`, The minimal expiry in seconds of the final TLC in the CKB network * `wrapped_btc_type_script` - `ckb_jsonrpc_types::Script`, Wrapped BTC type script -* `btc_pay_req` - `String`, Payment request for BTC -* `payment_hash` - `String`, Payment hash for the HTLC for both CKB and BTC. -* `channel_id` - [Hash256](#type-hash256), Channel ID for the CKB payment. -* `tlc_id` - `Option`, TLC ID for the CKB payment. -* `amount_sats` - `u128`, Amount will be received by the payee +* `incoming_invoice` - [CchInvoice](#type-cchinvoice), Generated invoice for the incoming payment +* `outgoing_pay_req` - `String`, The final payee to accept the payment. It has the different network with incoming invoice. +* `payment_hash` - [Hash256](#type-hash256), Payment hash for the HTLC for both CKB and BTC. +* `amount_sats` - `u128`, Amount required to pay in Satoshis, including fee * `fee_sats` - `u128`, Fee in Satoshis * `status` - [CchOrderStatus](#type-cchorderstatus), Order status @@ -162,14 +158,14 @@ Receive BTC from a payment hash. - -#### Method `get_receive_btc_order` + +#### Method `get_cch_order` Get receive BTC order by payment hash. ##### Params -* `payment_hash` - `String`, Payment hash for the HTLC for both CKB and BTC. +* `payment_hash` - [Hash256](#type-hash256), Payment hash for the HTLC for both CKB and BTC. ##### Returns @@ -177,11 +173,10 @@ Get receive BTC order by payment hash. * `expiry` - `u64`, Seconds after timestamp that the order expires * `ckb_final_tlc_expiry_delta` - `u64`, The minimal expiry in seconds of the final TLC in the CKB network * `wrapped_btc_type_script` - `ckb_jsonrpc_types::Script`, Wrapped BTC type script -* `btc_pay_req` - `String`, Payment request for BTC -* `payment_hash` - `String`, Payment hash for the HTLC for both CKB and BTC. -* `channel_id` - [Hash256](#type-hash256), Channel ID for the CKB payment. -* `tlc_id` - `Option`, TLC ID for the CKB payment. -* `amount_sats` - `u128`, Amount will be received by the payee +* `incoming_invoice` - [CchInvoice](#type-cchinvoice), Generated invoice for the incoming payment +* `outgoing_pay_req` - `String`, The final payee to accept the payment. It has the different network with incoming invoice. +* `payment_hash` - [Hash256](#type-hash256), Payment hash for the HTLC for both CKB and BTC. +* `amount_sats` - `u128`, Amount required to pay in Satoshis, including fee * `fee_sats` - `u128`, Fee in Satoshis * `status` - [CchOrderStatus](#type-cchorderstatus), Order status @@ -1038,6 +1033,24 @@ The attributes of the invoice * `PaymentSecret` - [Hash256](#type-hash256), The payment secret of the invoice --- + +### Type `CchInvoice` + +The generated proxy invoice for the incoming payment. + + The JSON representation: + + ```text + { "Fiber": String } | { "Lightning": String } + ``` + + +#### Enum with values of + +* `Fiber` - [CkbInvoice](#type-ckbinvoice), Fiber invoice that once paid, the hub will send the outgoing payment to Lightning +* `Lightning` - `Bolt11Invoice`, Lightning invoice that once paid, the hub will send the outgoing payment to Fiber +--- + ### Type `CchOrderStatus` @@ -1047,9 +1060,10 @@ The status of a cross-chain hub order, will update as the order progresses. #### Enum with values of * `Pending` - Order is created and has not send out payments yet. -* `Accepted` - HTLC in the first half is accepted. -* `InFlight` - There's an outgoing payment in flight for the second half. -* `Succeeded` - Order is settled. +* `IncomingAccepted` - HTLC in the incoming payment is accepted. +* `OutgoingInFlight` - There's an outgoing payment in flight. +* `OutgoingSettled` - The outgoing payment is settled. +* `Succeeded` - Both payments are settled and the order succeeds. * `Failed` - Order is failed. --- diff --git a/crates/fiber-lib/src/rpc/biscuit.rs b/crates/fiber-lib/src/rpc/biscuit.rs index 5e2187f6a..324e6989b 100644 --- a/crates/fiber-lib/src/rpc/biscuit.rs +++ b/crates/fiber-lib/src/rpc/biscuit.rs @@ -77,7 +77,7 @@ fn build_rules() -> HashMap<&'static str, AuthRule> { // Cch b.rule("send_btc", r#"allow if write("cch");"#); b.rule("receive_btc", r#"allow if read("cch");"#); - b.rule("get_receive_btc_order", r#"allow if read("cch");"#); + b.rule("get_cch_order", r#"allow if read("cch");"#); // channels b.rule("open_channel", r#"allow if write("channels");"#); b.rule("accept_channel", r#"allow if write("channels");"#); diff --git a/crates/fiber-lib/src/rpc/cch.rs b/crates/fiber-lib/src/rpc/cch.rs index eed329d44..aa2138994 100644 --- a/crates/fiber-lib/src/rpc/cch.rs +++ b/crates/fiber-lib/src/rpc/cch.rs @@ -1,6 +1,7 @@ #[cfg(not(target_arch = "wasm32"))] -use crate::cch::{CchMessage, CchOrderStatus, ReceiveBTCOrder}; +use crate::cch::{CchMessage, CchOrder, CchOrderStatus}; use crate::{ + cch::CchInvoice, fiber::{ serde_utils::{U128Hex, U64Hex}, types::Hash256, @@ -17,7 +18,7 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; #[derive(Serialize, Deserialize)] -pub struct SendBtcParams { +pub struct SendBTCParams { /// Bitcoin payment request string pub btc_pay_req: String, /// Request currency @@ -26,7 +27,7 @@ pub struct SendBtcParams { #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SendBTCResponse { +pub struct CchOrderResponse { /// Seconds since epoch when the order is created #[serde_as(as = "U64Hex")] pub timestamp: u64, @@ -37,17 +38,15 @@ pub struct SendBTCResponse { #[serde_as(as = "U64Hex")] pub ckb_final_tlc_expiry_delta: u64, - /// Request currency - pub currency: Currency, /// Wrapped BTC type script pub wrapped_btc_type_script: ckb_jsonrpc_types::Script, - /// Payment request for BTC - pub btc_pay_req: String, - /// Payment request for CKB - pub ckb_pay_req: String, + /// Generated invoice for the incoming payment + pub incoming_invoice: CchInvoice, + /// The final payee to accept the payment. It has the different network with incoming invoice. + pub outgoing_pay_req: String, /// Payment hash for the HTLC for both CKB and BTC. - pub payment_hash: String, + pub payment_hash: Hash256, /// Amount required to pay in Satoshis, including fee #[serde_as(as = "U128Hex")] pub amount_sats: u128, @@ -60,60 +59,15 @@ pub struct SendBTCResponse { #[serde_as] #[derive(Serialize, Deserialize)] -pub struct ReceiveBtcParams { - /// Payment hash for the HTLC for both CKB and BTC. - pub payment_hash: String, - /// Channel ID for the CKB payment. - pub channel_id: Hash256, - /// How many satoshis to receive, excluding cross-chain hub fee. - #[serde_as(as = "U128Hex")] - pub amount_sats: u128, - /// Expiry set for the HTLC for the CKB payment to the payee. - #[serde_as(as = "U64Hex")] - pub final_tlc_expiry: u64, +pub struct ReceiveBTCParams { + /// Fiber payment request string + pub fiber_pay_req: String, } #[derive(Serialize, Deserialize)] -pub struct GetReceiveBtcOrderParams { - /// Payment hash for the HTLC for both CKB and BTC. - pub payment_hash: String, -} - -#[serde_as] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReceiveBTCResponse { - /// Seconds since epoch when the order is created - #[serde_as(as = "U64Hex")] - pub timestamp: u64, - /// Seconds after timestamp that the order expires - #[serde_as(as = "U64Hex")] - pub expiry: u64, - /// The minimal expiry in seconds of the final TLC in the CKB network - #[serde_as(as = "U64Hex")] - pub ckb_final_tlc_expiry_delta: u64, - - /// Wrapped BTC type script - pub wrapped_btc_type_script: ckb_jsonrpc_types::Script, - - /// Payment request for BTC - pub btc_pay_req: String, +pub struct GetCchOrderParams { /// Payment hash for the HTLC for both CKB and BTC. - pub payment_hash: String, - /// Channel ID for the CKB payment. - pub channel_id: Hash256, - /// TLC ID for the CKB payment. - #[serde_as(as = "Option")] - pub tlc_id: Option, - - /// Amount will be received by the payee - #[serde_as(as = "U128Hex")] - pub amount_sats: u128, - /// Fee in Satoshis - #[serde_as(as = "U128Hex")] - pub fee_sats: u128, - - /// Order status - pub status: CchOrderStatus, + pub payment_hash: Hash256, } /// RPC module for cross chain hub demonstration. @@ -123,21 +77,21 @@ pub struct ReceiveBTCResponse { trait CchRpc { /// Send BTC to a address. #[method(name = "send_btc")] - async fn send_btc(&self, params: SendBtcParams) -> Result; + async fn send_btc(&self, params: SendBTCParams) -> Result; /// Receive BTC from a payment hash. #[method(name = "receive_btc")] async fn receive_btc( &self, - params: ReceiveBtcParams, - ) -> Result; + params: ReceiveBTCParams, + ) -> Result; /// Get receive BTC order by payment hash. - #[method(name = "get_receive_btc_order")] - async fn get_receive_btc_order( + #[method(name = "get_cch_order")] + async fn get_cch_order( &self, - params: GetReceiveBtcOrderParams, - ) -> Result; + params: GetCchOrderParams, + ) -> Result; } pub struct CchRpcServerImpl { @@ -155,7 +109,7 @@ const TIMEOUT: u64 = 1000; #[async_trait::async_trait] impl CchRpcServer for CchRpcServerImpl { /// Send BTC to a address. - async fn send_btc(&self, params: SendBtcParams) -> Result { + async fn send_btc(&self, params: SendBTCParams) -> Result { // ::send_btc(self, params).await self.send_btc(params).await } @@ -163,23 +117,23 @@ impl CchRpcServer for CchRpcServerImpl { /// Receive BTC from a payment hash. async fn receive_btc( &self, - params: ReceiveBtcParams, - ) -> Result { + params: ReceiveBTCParams, + ) -> Result { self.receive_btc(params).await } /// Get receive BTC order by payment hash. - async fn get_receive_btc_order( + async fn get_cch_order( &self, - params: GetReceiveBtcOrderParams, - ) -> Result { - self.get_receive_btc_order(params).await + params: GetCchOrderParams, + ) -> Result { + self.get_cch_order(params).await } } // #[async_trait::async_trait(?Send)] impl CchRpcServerImpl { - async fn send_btc(&self, params: SendBtcParams) -> Result { + async fn send_btc(&self, params: SendBTCParams) -> Result { let result = call_t!( self.cch_actor, CchMessage::SendBTC, @@ -197,36 +151,19 @@ impl CchRpcServerImpl { ) })?; - result - .map(|order| SendBTCResponse { - timestamp: order.created_at, - expiry: order.expires_after, - ckb_final_tlc_expiry_delta: order.ckb_final_tlc_expiry_delta, - currency: order.currency, - wrapped_btc_type_script: order.wrapped_btc_type_script, - btc_pay_req: order.btc_pay_req, - ckb_pay_req: order.ckb_pay_req, - payment_hash: order.payment_hash, - amount_sats: order.amount_sats, - fee_sats: order.fee_sats, - status: order.status, - }) - .map_err(Into::into) + result.map(Into::into).map_err(Into::into) } async fn receive_btc( &self, - params: ReceiveBtcParams, - ) -> Result { + params: ReceiveBTCParams, + ) -> Result { let result = call_t!( self.cch_actor, CchMessage::ReceiveBTC, TIMEOUT, crate::cch::ReceiveBTC { - payment_hash: params.payment_hash, - channel_id: params.channel_id, - amount_sats: params.amount_sats, - final_tlc_expiry: params.final_tlc_expiry, + fiber_pay_req: params.fiber_pay_req, } ) .map_err(|ractor_error| { @@ -240,13 +177,13 @@ impl CchRpcServerImpl { result.map(Into::into).map_err(Into::into) } - async fn get_receive_btc_order( + async fn get_cch_order( &self, - params: GetReceiveBtcOrderParams, - ) -> Result { + params: GetCchOrderParams, + ) -> Result { let result = call_t!( self.cch_actor, - CchMessage::GetReceiveBTCOrder, + CchMessage::GetCchOrder, TIMEOUT, params.payment_hash ) @@ -262,17 +199,16 @@ impl CchRpcServerImpl { } } -impl From for ReceiveBTCResponse { - fn from(value: ReceiveBTCOrder) -> Self { +impl From for CchOrderResponse { + fn from(value: CchOrder) -> Self { Self { timestamp: value.created_at, expiry: value.expires_after, ckb_final_tlc_expiry_delta: value.ckb_final_tlc_expiry_delta, wrapped_btc_type_script: value.wrapped_btc_type_script, - btc_pay_req: value.btc_pay_req, + outgoing_pay_req: value.outgoing_pay_req, + incoming_invoice: value.incoming_invoice, payment_hash: value.payment_hash, - channel_id: value.channel_id, - tlc_id: value.tlc_id, amount_sats: value.amount_sats, fee_sats: value.fee_sats, status: value.status, diff --git a/docs/biscuit-auth.md b/docs/biscuit-auth.md index 28d847dfd..991f2b55f 100644 --- a/docs/biscuit-auth.md +++ b/docs/biscuit-auth.md @@ -37,7 +37,7 @@ The current rules for each RPC methods: // Cch rule("send_btc", r#"allow if write("cch");"#); rule("receive_btc", r#"allow if read("cch");"#); -rule("get_receive_btc_order", r#"allow if read("cch");"#); +rule("get_cch_order", r#"allow if read("cch");"#); // channels rule("open_channel", r#"allow if write("channels");"#); rule("accept_channel", r#"allow if write("channels");"#); diff --git a/tests/bruno/e2e/cross-chain-hub/01-add-btc-invoice.bru b/tests/bruno/e2e/cross-chain-hub/01-add-btc-invoice.bru index 381292e23..4427847be 100644 --- a/tests/bruno/e2e/cross-chain-hub/01-add-btc-invoice.bru +++ b/tests/bruno/e2e/cross-chain-hub/01-add-btc-invoice.bru @@ -11,7 +11,7 @@ post { } body:json { - {"value":20000} + {"value":100000} } assert { diff --git a/tests/bruno/e2e/cross-chain-hub/02-create-send-btc-order.bru b/tests/bruno/e2e/cross-chain-hub/02-create-send-btc-order.bru index 3ba61a553..b3b3c717f 100644 --- a/tests/bruno/e2e/cross-chain-hub/02-create-send-btc-order.bru +++ b/tests/bruno/e2e/cross-chain-hub/02-create-send-btc-order.bru @@ -36,7 +36,7 @@ assert { script:post-response { if (res.body.result) { - bru.setVar("CKB_PAY_REQ", res.body.result.ckb_pay_req); + bru.setVar("FIBER_PAY_REQ", res.body.result.incoming_invoice.Fiber); console.log(res.body.result.payment_hash); } } diff --git a/tests/bruno/e2e/cross-chain-hub/04-node1-open-channel-to-node3.bru b/tests/bruno/e2e/cross-chain-hub/04-node1-open-channel-to-node3.bru index 8301ef0e7..862f704ea 100644 --- a/tests/bruno/e2e/cross-chain-hub/04-node1-open-channel-to-node3.bru +++ b/tests/bruno/e2e/cross-chain-hub/04-node1-open-channel-to-node3.bru @@ -23,7 +23,7 @@ body:json { "params": [ { "peer_id": "{{NODE3_PEERID}}", - "funding_amount": "0xc350", + "funding_amount": "0x30d40", "funding_udt_type_script": { "code_hash": "{{UDT_CODE_HASH}}", "hash_type": "data1", diff --git a/tests/bruno/e2e/cross-chain-hub/07-node1-add-tlc.bru b/tests/bruno/e2e/cross-chain-hub/07-node1-add-tlc.bru deleted file mode 100644 index da62b44e4..000000000 --- a/tests/bruno/e2e/cross-chain-hub/07-node1-add-tlc.bru +++ /dev/null @@ -1,50 +0,0 @@ -meta { - name: 07-node1-add-tlc - type: http - seq: 7 -} - -post { - url: {{NODE1_RPC_URL}} - body: json - auth: none -} - -headers { - Content-Type: application/json - Accept: application/json -} - -body:json { - { - "id": "42", - "jsonrpc": "2.0", - "method": "add_tlc", - "params": [ - { - "channel_id": "{{N1N3_CHANNEL_ID}}", - "amount": "0x4e20", - "payment_hash": "{{PAYMENT_HASH}}", - "expiry": "{{expiry}}", - "hash_algorithm": "sha256" - } - ] - } -} - -assert { - res.body.error: isUndefined - res.body.result.tlc_id: isDefined -} - -script:pre-request { - let expiry = "0x" + (Date.now() + 1000 * 60 * 60 * 24).toString(16); - bru.setVar("expiry", expiry); -} - -script:post-response { - // Sleep for sometime to make sure current operation finishes before next request starts. - await new Promise(r => setTimeout(r, 100)); - console.log("response from node1 AddTlc:", res.body); - bru.setVar("N1N3_TLC_ID1", res.body.result.tlc_id); -} diff --git a/tests/bruno/e2e/cross-chain-hub/07-node1-send-payment.bru b/tests/bruno/e2e/cross-chain-hub/07-node1-send-payment.bru new file mode 100644 index 000000000..7dad7d5d7 --- /dev/null +++ b/tests/bruno/e2e/cross-chain-hub/07-node1-send-payment.bru @@ -0,0 +1,39 @@ +meta { + name: 07-node1-send-payment + type: http + seq: 7 +} + +post { + url: {{NODE1_RPC_URL}} + body: json + auth: none +} + +headers { + Content-Type: application/json + Accept: application/json +} + +body:json { + { + "id": "42", + "jsonrpc": "2.0", + "method": "send_payment", + "params": [ + { + "invoice": "{{FIBER_PAY_REQ}}" + } + ] + } +} + +assert { + res.body.error: isUndefined +} + +script:post-response { + // Sleep for sometime to make sure current operation finishes before next request starts. + await new Promise(r => setTimeout(r, 100)); + console.log("response from node1 send_payment:", res.body); +} diff --git a/tests/bruno/e2e/cross-chain-hub/09-create-receive-btc-order.bru b/tests/bruno/e2e/cross-chain-hub/09-create-receive-btc-order.bru deleted file mode 100644 index 1644615fc..000000000 --- a/tests/bruno/e2e/cross-chain-hub/09-create-receive-btc-order.bru +++ /dev/null @@ -1,61 +0,0 @@ -meta { - name: 09-create-receive-btc-order - type: http - seq: 9 -} - -post { - url: {{NODE3_RPC_URL}} - body: json - auth: none -} - -headers { - Content-Type: application/json - Accept: application/json -} - -body:json { - { - "id": "42", - "jsonrpc": "2.0", - "method": "receive_btc", - "params": [ - { - "payment_hash": "{{PAYMENT_HASH}}", - "channel_id": "{{N1N3_CHANNEL_ID}}", - "amount_sats": "0x1", - "final_tlc_expiry": "0x3c" - } - ] - } -} - -assert { - res.status: eq 200 - res.body.error: isUndefined -} - -script:pre-request { - const uuid = require('uuid'); - const CryptoJS = require("crypto-js"); - - const preimage = CryptoJS.SHA256(uuid.v4()); - const hash = CryptoJS.SHA256(preimage); - console.log(preimage.toString(CryptoJS.enc.Hex)); - console.log(hash.toString(CryptoJS.enc.Hex)); - - bru.setVar("PAYMENT_HASH", `0x${hash.toString(CryptoJS.enc.Hex)}`); - bru.setVar("PAYMENT_PREIMAGE", `0x${preimage.toString(CryptoJS.enc.Hex)}`); -} - -script:post-response { - if (res.body.result) { - bru.setVar("BTC_PAY_REQ", res.body.result.btc_pay_req); - console.log(res.body.result.payment_hash); - } -} - -docs { - CKB user requests a BTC invoice to receive BTC from Bitcoin user. -} diff --git a/tests/bruno/e2e/cross-chain-hub/09-node1-add-fiber-invoice.bru b/tests/bruno/e2e/cross-chain-hub/09-node1-add-fiber-invoice.bru new file mode 100644 index 000000000..3d9ccdad4 --- /dev/null +++ b/tests/bruno/e2e/cross-chain-hub/09-node1-add-fiber-invoice.bru @@ -0,0 +1,66 @@ +meta { + name: 09-node1-add-fiber-invoice + type: http + seq: 9 +} + +post { + url: {{NODE1_RPC_URL}} + body: json + auth: none +} + +headers { + Content-Type: application/json + Accept: application/json +} + +body:json { + { + "id": "42", + "jsonrpc": "2.0", + "method": "new_invoice", + "params": [ + { + "amount": "0xc350", + "currency": "Fibd", + "udt_type_script": { + "args": "{{UDT_SCRIPT_ARGS}}", + "code_hash": "0xe1e354d6d643ad42724d40967e334984534e0367405c5ae42a9d7d63d77df419", + "hash_type": "data1" + }, + "description": "test invoice", + "payment_preimage": "{{payment_preimage}}" + } + ] + } +} + +assert { + res.status: eq 200 + res.body.error: isUndefined + res.body.result.invoice_address: isDefined + res.body.result.invoice.data.payment_hash: isDefined +} + +script:pre-request { + // generate random preimage + function generateRandomPreimage() { + let hash = '0x'; + for (let i = 0; i < 64; i++) { + hash += Math.floor(Math.random() * 16).toString(16); + } + return hash; + } + const payment_preimage = generateRandomPreimage(); + bru.setVar("payment_preimage", payment_preimage); +} + +script:post-response { + bru.setVar("FIBER_PAY_REQ", res.body.result.invoice_address); + bru.setVar("PAYMENT_HASH", res.body.result.invoice.data.payment_hash); +} + +docs { + Create a fiber invoice to receive money +} diff --git a/tests/bruno/e2e/cross-chain-hub/10-create-receive-btc-order.bru b/tests/bruno/e2e/cross-chain-hub/10-create-receive-btc-order.bru new file mode 100644 index 000000000..70b4c459d --- /dev/null +++ b/tests/bruno/e2e/cross-chain-hub/10-create-receive-btc-order.bru @@ -0,0 +1,44 @@ +meta { + name: 10-create-receive-btc-order + type: http + seq: 10 +} + +post { + url: {{NODE3_RPC_URL}} + body: json + auth: none +} + +headers { + Content-Type: application/json + Accept: application/json +} + +body:json { + { + "id": "42", + "jsonrpc": "2.0", + "method": "receive_btc", + "params": [ + { + "fiber_pay_req": "{{FIBER_PAY_REQ}}" + } + ] + } +} + +assert { + res.status: eq 200 + res.body.error: isUndefined + res.body.result.incoming_invoice.Lightning: isDefined +} + +script:post-response { + bru.setVar("BTC_PAY_REQ", res.body.result.incoming_invoice.Lightning); + console.log(res.body.result.incoming_invoice.Lightning); +} + +docs { + CKB user requests a BTC invoice to receive BTC from Bitcoin user. +} diff --git a/tests/bruno/e2e/cross-chain-hub/11-check-receive-btc-order.bru b/tests/bruno/e2e/cross-chain-hub/11-check-receive-btc-order.bru new file mode 100644 index 000000000..4464f1837 --- /dev/null +++ b/tests/bruno/e2e/cross-chain-hub/11-check-receive-btc-order.bru @@ -0,0 +1,34 @@ +meta { + name: 11-check-receive-btc-order + type: http + seq: 11 +} + +post { + url: {{NODE3_RPC_URL}} + body: json + auth: none +} + +headers { + Content-Type: application/json + Accept: application/json +} + +body:json { + { + "id": "42", + "jsonrpc": "2.0", + "method": "get_cch_order", + "params": [ + { + "payment_hash": "{{PAYMENT_HASH}}" + } + ] + } +} + +assert { + res.body.error: isUndefined + res.status: eq 200 +} diff --git a/tests/bruno/e2e/cross-chain-hub/10-pay-btc-invoice.bru b/tests/bruno/e2e/cross-chain-hub/12-pay-btc-invoice.bru similarity index 96% rename from tests/bruno/e2e/cross-chain-hub/10-pay-btc-invoice.bru rename to tests/bruno/e2e/cross-chain-hub/12-pay-btc-invoice.bru index a1c0e77e6..bcd038ca3 100644 --- a/tests/bruno/e2e/cross-chain-hub/10-pay-btc-invoice.bru +++ b/tests/bruno/e2e/cross-chain-hub/12-pay-btc-invoice.bru @@ -1,7 +1,7 @@ meta { - name: 10-pay-btc-invoice + name: 12-pay-btc-invoice type: http - seq: 10 + seq: 12 } post { diff --git a/tests/bruno/e2e/cross-chain-hub/12-remove-tlc-for-receive-btc-order.bru b/tests/bruno/e2e/cross-chain-hub/12-remove-tlc-for-receive-btc-order.bru deleted file mode 100644 index a01fe13de..000000000 --- a/tests/bruno/e2e/cross-chain-hub/12-remove-tlc-for-receive-btc-order.bru +++ /dev/null @@ -1,38 +0,0 @@ -meta { - name: 12-remove-tlc-for-receive-btc-order - type: http - seq: 12 -} - -post { - url: {{NODE1_RPC_URL}} - body: json - auth: none -} - -headers { - Content-Type: application/json - Accept: application/json -} - -body:json { - { - "id": "42", - "jsonrpc": "2.0", - "method": "remove_tlc", - "params": [ - { - "channel_id": "{{N1N3_CHANNEL_ID}}", - "tlc_id": "{{N3N1_TLC_ID1}}", - "reason": { - "payment_preimage": "{{PAYMENT_PREIMAGE}}" - } - } - ] - } -} - -assert { - res.body.error: isUndefined - res.body.result: isNull -} diff --git a/tests/bruno/e2e/cross-chain-hub/11-get-receive-btc-order-tlc-id.bru b/tests/bruno/e2e/cross-chain-hub/13-get-invoice-status.bru similarity index 56% rename from tests/bruno/e2e/cross-chain-hub/11-get-receive-btc-order-tlc-id.bru rename to tests/bruno/e2e/cross-chain-hub/13-get-invoice-status.bru index 83ec1d65b..76325f069 100644 --- a/tests/bruno/e2e/cross-chain-hub/11-get-receive-btc-order-tlc-id.bru +++ b/tests/bruno/e2e/cross-chain-hub/13-get-invoice-status.bru @@ -1,11 +1,11 @@ meta { - name: 11-get-receive-btc-order-tlc-id + name: 13-get-invoice-status type: http - seq: 11 + seq: 13 } post { - url: {{NODE3_RPC_URL}} + url: {{NODE1_RPC_URL}} body: json auth: none } @@ -19,7 +19,7 @@ body:json { { "id": "42", "jsonrpc": "2.0", - "method": "get_receive_btc_order", + "method": "get_invoice", "params": [ { "payment_hash": "{{PAYMENT_HASH}}" @@ -30,34 +30,37 @@ body:json { assert { res.body.error: isUndefined - res.status: eq 200 + res.body.result.status: isDefined } + script:pre-request { if(bru.getVar("iteration") === undefined){ bru.setVar("iteration", 0); } } +vars:post-response { + max_iterations: 20 +} + script:post-response { const i = bru.getVar("iteration"); const n = bru.getVar("max_iterations"); if (i < n) { console.log(`Try ${i+1}/${n}`); + console.log(res.body) } - if (res.body.result.tlc_id !== null) { - bru.setVar("N3N1_TLC_ID1", res.body.result.tlc_id); - console.log(`Node 3 has sent a pending tlc: ${res.body.result.tlc_id}`); + if (res.body.result.status == "Paid") { + console.log("Invoicee is paid"); bru.setVar("iteration", 0); - // wait for confirmation - await new Promise(r => setTimeout(r, 500)); } else if (i+1 < n) { - await new Promise(r => setTimeout(r, 10)); + await new Promise(r => setTimeout(r, 100)); bru.setVar("iteration", i + 1); - bru.setNextRequest("11-get-receive-btc-order-tlc-id"); + bru.setNextRequest("13-get-invoice-status"); } else { bru.setVar("iteration", 0); - throw new Error("Node 3 has not sent a pending tlc"); + throw new Error("Alice has not received the payment"); } } diff --git a/tests/bruno/e2e/cross-chain-hub/13-node1-send-shutdown-channel.bru b/tests/bruno/e2e/cross-chain-hub/14-node1-send-shutdown-channel.bru similarity index 89% rename from tests/bruno/e2e/cross-chain-hub/13-node1-send-shutdown-channel.bru rename to tests/bruno/e2e/cross-chain-hub/14-node1-send-shutdown-channel.bru index a706c2efa..78a17950a 100644 --- a/tests/bruno/e2e/cross-chain-hub/13-node1-send-shutdown-channel.bru +++ b/tests/bruno/e2e/cross-chain-hub/14-node1-send-shutdown-channel.bru @@ -1,7 +1,7 @@ meta { - name: 13-node1-send-shutdown-channel + name: 14-node1-send-shutdown-channel type: http - seq: 13 + seq: 14 } post { @@ -35,7 +35,7 @@ body:json { } script:pre-request { - await new Promise(r => setTimeout(r, 1000)); + await new Promise(r => setTimeout(r, 5000)); } assert { diff --git a/tests/bruno/e2e/cross-chain-hub/14-node3-list-channel.bru b/tests/bruno/e2e/cross-chain-hub/15-node3-list-channel.bru similarity index 92% rename from tests/bruno/e2e/cross-chain-hub/14-node3-list-channel.bru rename to tests/bruno/e2e/cross-chain-hub/15-node3-list-channel.bru index aadd17ac0..51255fda8 100644 --- a/tests/bruno/e2e/cross-chain-hub/14-node3-list-channel.bru +++ b/tests/bruno/e2e/cross-chain-hub/15-node3-list-channel.bru @@ -1,7 +1,7 @@ meta { - name: 14-node3-list-channel + name: 15-node3-list-channel type: http - seq: 14 + seq: 16 } post { @@ -32,4 +32,4 @@ assert { res.body.error: isUndefined res.body.result.channels: isDefined res.body.result.channels.map(channel => channel.channel_id): notContains {{N1N3_CHANNEL_ID}} -} \ No newline at end of file +} diff --git a/tests/bruno/environments/test.bru b/tests/bruno/environments/test.bru index 8d2121e0e..4c1553ec8 100644 --- a/tests/bruno/environments/test.bru +++ b/tests/bruno/environments/test.bru @@ -10,6 +10,7 @@ vars { NODE2_PEERID: QmSRcPqUn4aQrKHXyCDjGn2qBVf43tWBDS2Wj9QDUZXtZp NODE3_PEERID: QmaFDJb9CkMrXy7nhTWBY5y9mvuykre3EzzRsCJUAVXprZ UDT_CODE_HASH: 0xe1e354d6d643ad42724d40967e334984534e0367405c5ae42a9d7d63d77df419 + UDT_SCRIPT_ARGS: 0x32e555f3ff8e135cece1351a6a2971518392c1e30375c1e006ad0ce8eac07947 LND_BOB_RPC_URL: http://127.0.0.1:8180 LND_INGRID_RPC_URL: http://127.0.0.1:8080 } diff --git a/tests/bruno/environments/xudt-test.bru b/tests/bruno/environments/xudt-test.bru index f5cc31133..61ded3018 100644 --- a/tests/bruno/environments/xudt-test.bru +++ b/tests/bruno/environments/xudt-test.bru @@ -10,6 +10,7 @@ vars { NODE2_PEERID: QmSRcPqUn4aQrKHXyCDjGn2qBVf43tWBDS2Wj9QDUZXtZp NODE3_PEERID: QmaFDJb9CkMrXy7nhTWBY5y9mvuykre3EzzRsCJUAVXprZ UDT_CODE_HASH: 0x50bd8d6680b8b9cf98b73f3c08faf8b2a21914311954118ad6609be6e78a1b95 + UDT_SCRIPT_ARGS: 0x32e555f3ff8e135cece1351a6a2971518392c1e30375c1e006ad0ce8eac07947 LND_BOB_RPC_URL: http://127.0.0.1:8180 LND_INGRID_RPC_URL: http://127.0.0.1:8080 }