diff --git a/.gitignore b/.gitignore index 0251689..f290d55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ /target /.sensei-tests -./senseicore/.sensei-tests +/senseicore/.sensei-tests .DS_Store *_pb2.py *_pb2_grpc.py diff --git a/Cargo.lock b/Cargo.lock index 579bb17..0b3a99c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1052,12 +1052,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "futures" version = "0.3.21" @@ -2229,19 +2223,6 @@ dependencies = [ "nibble_vec", ] -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - [[package]] name = "rand" version = "0.7.3" @@ -2286,21 +2267,6 @@ dependencies = [ "rand_core 0.6.3", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.5.1" @@ -2328,15 +2294,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.2.13" @@ -2765,12 +2722,13 @@ dependencies = [ "portpicker", "prost", "public-ip", - "rand 0.4.6", + "rand 0.8.5", "rusqlite", "rust-embed", "senseicore", "serde", "serde_json", + "serial_test", "tindercrypt", "tokio", "tonic", @@ -2815,10 +2773,11 @@ dependencies = [ "pin-project", "portpicker", "public-ip", - "rand 0.4.6", + "rand 0.8.5", "rust-embed", "serde", "serde_json", + "serial_test", "tindercrypt", "tokio", "tower", @@ -2869,6 +2828,30 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bcc41d18f7a1d50525d080fd3e953be87c4f9f1a974f3c21798ca00d54ec15" +dependencies = [ + "lazy_static", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2881bccd7d60fb32dfa3d7b3136385312f8ad75e2674aab2852867a09790cae8" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "sha-1" version = "0.9.8" diff --git a/Cargo.toml b/Cargo.toml index e13bcf7..a754f53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ bitcoin-bech32 = "0.12" bech32 = "0.8" futures = "0.3" chrono = "0.4" -rand = "0.4" +rand = "0.8.4" axum = { version = "0.4.2", features = ["headers"] } http = "0.2" tower = { version = "0.4", features = ["full"] } @@ -64,6 +64,7 @@ tonic-build = "0.6" [dev-dependencies] bitcoind = { version = "0.26", features = [ "22_0" ] } +serial_test = "0.6.0" [[test]] name = "senseicore" diff --git a/proto/sensei.proto b/proto/sensei.proto index a4ca079..aa95aa6 100644 --- a/proto/sensei.proto +++ b/proto/sensei.proto @@ -20,7 +20,7 @@ service Node { rpc StopNode (StopNodeRequest) returns (StopNodeResponse); rpc GetUnusedAddress (GetUnusedAddressRequest) returns (GetUnusedAddressResponse); rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse); - rpc OpenChannel (OpenChannelRequest) returns (OpenChannelResponse); + rpc OpenChannels (OpenChannelsRequest) returns (OpenChannelsResponse); rpc PayInvoice (PayInvoiceRequest) returns (PayInvoiceResponse); rpc DecodeInvoice (DecodeInvoiceRequest) returns (DecodeInvoiceResponse); rpc Keysend (KeysendRequest) returns (KeysendResponse); @@ -189,13 +189,24 @@ message GetBalanceResponse { uint64 balance_satoshis = 1; } -message OpenChannelRequest { +message OpenChannelInfo { string node_connection_string = 1; uint64 amt_satoshis = 2; bool public = 3; } -message OpenChannelResponse { - string temp_channel_id = 1; + +message OpenChannelResult { + bool error = 1; + optional string error_message = 2; + optional string temp_channel_id = 3; +} + +message OpenChannelsRequest { + repeated OpenChannelInfo channels = 1; +} +message OpenChannelsResponse { + repeated OpenChannelInfo channels = 1; + repeated OpenChannelResult results = 2; } message PayInvoiceRequest { diff --git a/senseicore/Cargo.toml b/senseicore/Cargo.toml index cf48519..d9098fc 100644 --- a/senseicore/Cargo.toml +++ b/senseicore/Cargo.toml @@ -21,7 +21,7 @@ bitcoin-bech32 = "0.12" bech32 = "0.8" futures = "0.3" chrono = "0.4" -rand = "0.4" +rand = "0.8.4" axum = { version = "0.4.2", features = ["headers"] } http = "0.2" tower = { version = "0.4", features = ["full"] } @@ -49,3 +49,4 @@ migration = { path = "../migration" } [dev-dependencies] bitcoind = { version = "0.26", features = [ "22_0" ] } +serial_test = "0.6.0" diff --git a/senseicore/src/chain/broadcaster.rs b/senseicore/src/chain/broadcaster.rs index 0b26506..853ff4c 100644 --- a/senseicore/src/chain/broadcaster.rs +++ b/senseicore/src/chain/broadcaster.rs @@ -1,21 +1,45 @@ -use std::sync::{Arc, Mutex}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; use crate::events::SenseiEvent; use super::database::WalletDatabase; -use bitcoin::Transaction; +use bitcoin::{Transaction, Txid}; use lightning::chain::chaininterface::BroadcasterInterface; use tokio::sync::broadcast; pub struct SenseiBroadcaster { + pub debounce: Mutex>, pub node_id: String, pub broadcaster: Arc, pub wallet_database: Arc>, pub event_sender: broadcast::Sender, } -impl BroadcasterInterface for SenseiBroadcaster { - fn broadcast_transaction(&self, tx: &Transaction) { +impl SenseiBroadcaster { + pub fn new( + node_id: String, + broadcaster: Arc, + wallet_database: Arc>, + event_sender: broadcast::Sender, + ) -> Self { + Self { + node_id, + broadcaster, + wallet_database, + event_sender, + debounce: Mutex::new(HashMap::new()), + } + } + + pub fn set_debounce(&self, txid: Txid, count: usize) { + let mut debounce = self.debounce.lock().unwrap(); + debounce.insert(txid, count); + } + + pub fn broadcast(&self, tx: &Transaction) { self.broadcaster.broadcast_transaction(tx); // TODO: there's a bug here if the broadcast fails @@ -31,3 +55,23 @@ impl BroadcasterInterface for SenseiBroadcaster { .unwrap_or_default(); } } + +impl BroadcasterInterface for SenseiBroadcaster { + fn broadcast_transaction(&self, tx: &Transaction) { + let txid = tx.txid(); + + let mut debounce = self.debounce.lock().unwrap(); + + let can_broadcast = match debounce.get_mut(&txid) { + Some(count) => { + *count -= 1; + *count == 0 + } + None => true, + }; + + if can_broadcast { + self.broadcast(tx); + } + } +} diff --git a/senseicore/src/channels.rs b/senseicore/src/channels.rs new file mode 100644 index 0000000..b032f08 --- /dev/null +++ b/senseicore/src/channels.rs @@ -0,0 +1,256 @@ +use crate::chain::broadcaster::SenseiBroadcaster; +use crate::chain::manager::SenseiChainManager; +use crate::error::Error; +use crate::{chain::database::WalletDatabase, events::SenseiEvent, node::ChannelManager}; +use bdk::{FeeRate, SignOptions}; +use bitcoin::secp256k1::PublicKey; +use lightning::chain::chaininterface::ConfirmationTarget; +use lightning::util::config::{ChannelConfig, ChannelHandshakeLimits, UserConfig}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tokio::sync::broadcast; + +pub struct EventFilter +where + F: Fn(SenseiEvent) -> bool, +{ + pub f: F, +} + +pub struct ChannelOpenRequest { + pub node_connection_string: String, + pub peer_pubkey: PublicKey, + pub channel_amt_sat: u64, + pub push_amt_msat: u64, + pub custom_id: u64, + pub announced_channel: bool, +} + +pub struct ChannelOpener { + node_id: String, + channel_manager: Arc, + wallet: Arc>>, + chain_manager: Arc, + event_receiver: broadcast::Receiver, + broadcaster: Arc, +} + +impl ChannelOpener { + pub fn new( + node_id: String, + channel_manager: Arc, + chain_manager: Arc, + wallet: Arc>>, + event_receiver: broadcast::Receiver, + broadcaster: Arc, + ) -> Self { + Self { + node_id, + channel_manager, + chain_manager, + wallet, + event_receiver, + broadcaster, + } + } + + async fn wait_for_events bool>( + &mut self, + mut filters: Vec>, + timeout_ms: u64, + interval_ms: u64, + ) -> Vec { + let mut events = vec![]; + let mut current_ms = 0; + while current_ms < timeout_ms { + while let Ok(event) = self.event_receiver.try_recv() { + let filter_index = filters + .iter() + .enumerate() + .find(|(_index, filter)| (filter.f)(event.clone())) + .map(|(index, _filter)| index); + + if let Some(index) = filter_index { + events.push(event); + filters.swap_remove(index); + } + + if filters.is_empty() { + return events; + } + } + tokio::time::sleep(Duration::from_millis(interval_ms)).await; + current_ms += interval_ms; + } + events + } + + pub async fn open_batch( + &mut self, + requests: Vec, + ) -> Vec<(ChannelOpenRequest, Result<[u8; 32], Error>)> { + let mut requests_with_results = requests + .into_iter() + .map(|request| { + let result = self.initiate_channel_open(&request); + + (request, result) + }) + .collect::>(); + + let filters = requests_with_results + .iter() + .filter(|(_request, result)| result.is_ok()) + .map(|(request, _result)| { + let filter_node_id = self.node_id.clone(); + let request_user_channel_id = request.custom_id; + let filter = move |event| { + if let SenseiEvent::FundingGenerationReady { + node_id, + user_channel_id, + .. + } = event + { + if *node_id == filter_node_id && user_channel_id == request_user_channel_id + { + return true; + } + } + false + }; + EventFilter { f: filter } + }) + .collect::>>(); + + // TODO: is this appropriate timeout? maybe should accept as param + let events = self.wait_for_events(filters, 15000, 500).await; + + // set error state for requests we didn't get an event for + let requests_with_results = requests_with_results + .drain(..) + .map(|(request, result)| { + if result.is_ok() { + let mut channel_counterparty_node_id = None; + let event = events.iter().find(|event| { + if let SenseiEvent::FundingGenerationReady { + user_channel_id, + counterparty_node_id, + .. + } = event + { + if *user_channel_id == request.custom_id { + channel_counterparty_node_id = Some(*counterparty_node_id); + return true; + } + } + false + }); + + if event.is_none() { + (request, Err(Error::FundingGenerationNeverHappened), None) + } else { + (request, result, channel_counterparty_node_id) + } + } else { + (request, result, None) + } + }) + .collect::>(); + + // build a tx with these events and requests + let wallet = self.wallet.lock().unwrap(); + + let mut tx_builder = wallet.build_tx(); + let fee_sats_per_1000_wu = self + .chain_manager + .fee_estimator + .get_est_sat_per_1000_weight(ConfirmationTarget::Normal); + + // TODO: is this the correct conversion?? + let sat_per_vb = match fee_sats_per_1000_wu { + 253 => 1.0, + _ => fee_sats_per_1000_wu as f32 / 250.0, + } as f32; + + let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb); + + events.iter().for_each(|event| { + if let SenseiEvent::FundingGenerationReady { + channel_value_satoshis, + output_script, + .. + } = event + { + tx_builder.add_recipient(output_script.clone(), *channel_value_satoshis); + } + }); + + tx_builder.fee_rate(fee_rate).enable_rbf(); + let (mut psbt, _tx_details) = tx_builder.finish().unwrap(); + let _finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); + let funding_tx = psbt.extract_tx(); + + let channels_to_open = requests_with_results + .iter() + .filter(|(_request, result, _counterparty_node_id)| result.is_ok()) + .count(); + + self.broadcaster + .set_debounce(funding_tx.txid(), channels_to_open); + + requests_with_results + .into_iter() + .map(|(request, result, counterparty_node_id)| { + if let Ok(tcid) = result { + let counterparty_node_id = counterparty_node_id.unwrap(); + match self.channel_manager.funding_transaction_generated( + &tcid, + &counterparty_node_id, + funding_tx.clone(), + ) { + Ok(()) => (request, result), + Err(e) => (request, Err(Error::LdkApi(e))), + } + } else { + (request, result) + } + }) + .collect() + } + + fn initiate_channel_open(&self, request: &ChannelOpenRequest) -> Result<[u8; 32], Error> { + let config = UserConfig { + peer_channel_config_limits: ChannelHandshakeLimits { + // lnd's max to_self_delay is 2016, so we want to be compatible. + their_to_self_delay: 2016, + ..Default::default() + }, + channel_options: ChannelConfig { + announced_channel: request.announced_channel, + ..Default::default() + }, + ..Default::default() + }; + + // TODO: want to be logging channels in db for matching forwarded payments + match self.channel_manager.create_channel( + request.peer_pubkey, + request.channel_amt_sat, + request.push_amt_msat, + request.custom_id, + Some(config), + ) { + Ok(short_channel_id) => { + println!( + "EVENT: initiated channel with peer {}. ", + request.peer_pubkey + ); + Ok(short_channel_id) + } + Err(e) => { + println!("ERROR: failed to open channel: {:?}", e); + Err(e.into()) + } + } + } +} diff --git a/senseicore/src/database.rs b/senseicore/src/database.rs index 5280603..13a68ab 100644 --- a/senseicore/src/database.rs +++ b/senseicore/src/database.rs @@ -22,7 +22,7 @@ use entity::sea_orm::QueryOrder; use migration::Condition; use migration::Expr; use rand::thread_rng; -use rand::Rng; +use rand::RngCore; use sea_orm::entity::EntityTrait; use sea_orm::{prelude::*, DatabaseConnection}; use serde::Deserialize; @@ -114,6 +114,15 @@ impl SenseiDatabase { .await?) } + pub async fn list_ports_in_use(&self) -> Result, Error> { + Ok(Node::find() + .all(&self.connection) + .await? + .into_iter() + .map(|node| node.listen_port as u16) + .collect()) + } + pub async fn list_nodes( &self, pagination: PaginationRequest, diff --git a/senseicore/src/error.rs b/senseicore/src/error.rs index f784caf..5a7e43e 100644 --- a/senseicore/src/error.rs +++ b/senseicore/src/error.rs @@ -32,6 +32,7 @@ pub enum Error { InvalidMacaroon, AdminNodeNotStarted, AdminNodeNotCreated, + FundingGenerationNeverHappened, } impl Display for Error { @@ -55,6 +56,9 @@ impl Display for Error { Error::InvalidMacaroon => String::from("invalid macaroon"), Error::AdminNodeNotCreated => String::from("admin node not created"), Error::AdminNodeNotStarted => String::from("admin node not started"), + Error::FundingGenerationNeverHappened => { + String::from("funding generation for request never happened") + } }; write!(f, "{}", str) } diff --git a/senseicore/src/event_handler.rs b/senseicore/src/event_handler.rs index c04c606..8c5c326 100644 --- a/senseicore/src/event_handler.rs +++ b/senseicore/src/event_handler.rs @@ -17,7 +17,6 @@ use crate::hex_utils; use crate::node::{ChannelManager, HTLCStatus, PaymentOrigin}; use bdk::wallet::AddressIndex; -use bdk::{FeeRate, SignOptions}; use bitcoin::{secp256k1::Secp256k1, Network}; use bitcoin_bech32::WitnessProgram; use entity::sea_orm::ActiveValue; @@ -55,8 +54,8 @@ impl EventHandler for LightningNodeEventHandler { temporary_channel_id, channel_value_satoshis, output_script, - user_channel_id: _, counterparty_node_id, + user_channel_id, } => { // Construct the raw transaction with one output, that is paid the amount of the // channel. @@ -72,48 +71,17 @@ impl EventHandler for LightningNodeEventHandler { .expect("Lightning funding tx should always be to a SegWit output") .to_address(); - // Have wallet put the inputs into the transaction such that the output - // is satisfied and then sign the funding transaction - let wallet = self.wallet.lock().unwrap(); - - let mut tx_builder = wallet.build_tx(); - let fee_sats_per_1000_wu = self - .chain_manager - .fee_estimator - .get_est_sat_per_1000_weight(ConfirmationTarget::Normal); - - // TODO: is this the correct conversion?? - let sat_per_vb = match fee_sats_per_1000_wu { - 253 => 1.0, - _ => fee_sats_per_1000_wu as f32 / 250.0, - } as f32; - - let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb); - - tx_builder - .add_recipient(output_script.clone(), *channel_value_satoshis) - .fee_rate(fee_rate) - .enable_rbf(); - - let (mut psbt, _tx_details) = tx_builder.finish().unwrap(); - - let _finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); - - let funding_tx = psbt.extract_tx(); - - // Give the funding transaction back to LDK for opening the channel. - if self - .channel_manager - .funding_transaction_generated( - temporary_channel_id, - counterparty_node_id, - funding_tx, - ) - .is_err() - { - println!( - "\nERROR: Channel went away before we could fund it. The peer disconnected or refused the channel."); - } + let _res = self + .event_sender + .send(SenseiEvent::FundingGenerationReady { + node_id: self.node_id.clone(), + temporary_channel_id: *temporary_channel_id, + channel_value_satoshis: *channel_value_satoshis, + output_script: output_script.clone(), + user_channel_id: *user_channel_id, + counterparty_node_id: *counterparty_node_id, + }) + .unwrap(); } Event::PaymentReceived { payment_hash, @@ -289,7 +257,7 @@ impl EventHandler for LightningNodeEventHandler { let forwarding_channel_manager = self.channel_manager.clone(); let min = time_forwardable.as_millis() as u64; self.tokio_handle.spawn(async move { - let millis_to_sleep = thread_rng().gen_range(min, min * 5) as u64; + let millis_to_sleep = thread_rng().gen_range(min..(min * 5)) as u64; tokio::time::sleep(Duration::from_millis(millis_to_sleep)).await; forwarding_channel_manager.process_pending_htlc_forwards(); }); diff --git a/senseicore/src/events.rs b/senseicore/src/events.rs index e56755c..2d2fc0f 100644 --- a/senseicore/src/events.rs +++ b/senseicore/src/events.rs @@ -1,7 +1,18 @@ -use bitcoin::Txid; +use bitcoin::{secp256k1::PublicKey, Script, Txid}; use serde::Serialize; #[derive(Clone, Debug, Serialize)] pub enum SenseiEvent { - TransactionBroadcast { node_id: String, txid: Txid }, + TransactionBroadcast { + node_id: String, + txid: Txid, + }, + FundingGenerationReady { + node_id: String, + temporary_channel_id: [u8; 32], + channel_value_satoshis: u64, + output_script: Script, + user_channel_id: u64, + counterparty_node_id: PublicKey, + }, } diff --git a/senseicore/src/lib.rs b/senseicore/src/lib.rs index 16f7e8c..a9cd00f 100644 --- a/senseicore/src/lib.rs +++ b/senseicore/src/lib.rs @@ -1,4 +1,5 @@ pub mod chain; +pub mod channels; pub mod config; pub mod database; pub mod disk; diff --git a/senseicore/src/node.rs b/senseicore/src/node.rs index c94e768..665055e 100644 --- a/senseicore/src/node.rs +++ b/senseicore/src/node.rs @@ -11,6 +11,7 @@ use crate::chain::broadcaster::SenseiBroadcaster; use crate::chain::database::WalletDatabase; use crate::chain::fee_estimator::SenseiFeeEstimator; use crate::chain::manager::SenseiChainManager; +use crate::channels::{ChannelOpenRequest, ChannelOpener}; use crate::config::SenseiConfig; use crate::database::SenseiDatabase; use crate::disk::FilesystemLogger; @@ -19,7 +20,9 @@ use crate::event_handler::LightningNodeEventHandler; use crate::events::SenseiEvent; use crate::network_graph::OptionalNetworkGraphMsgHandler; use crate::persist::{AnyKVStore, DatabaseStore, SenseiPersister}; -use crate::services::node::{Channel, NodeInfo, NodeRequest, NodeRequestError, NodeResponse, Peer}; +use crate::services::node::{ + Channel, NodeInfo, NodeRequest, NodeRequestError, NodeResponse, OpenChannelResult, Peer, +}; use crate::services::{PaginationRequest, PaginationResponse, PaymentsFilter}; use crate::utils::PagedVec; use crate::{hex_utils, version}; @@ -55,14 +58,14 @@ use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning::routing::network_graph::{NetGraphMsgHandler, NetworkGraph, NodeId, RoutingFees}; use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning::routing::scoring::ProbabilisticScorer; -use lightning::util::config::{ChannelConfig, ChannelHandshakeLimits, UserConfig}; +use lightning::util::config::UserConfig; use lightning::util::ser::ReadableArgs; use lightning_background_processor::BackgroundProcessor; use lightning_invoice::utils::DefaultRouter; use lightning_invoice::{payment, utils, Currency, Invoice, InvoiceDescription}; use lightning_net_tokio::SocketDescriptor; use macaroon::Macaroon; -use rand::{thread_rng, Rng}; +use rand::{thread_rng, Rng, RngCore}; use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer}; use std::fmt::Display; use std::fs::File; @@ -407,6 +410,7 @@ pub struct LightningNode { pub stop_listen: Arc, pub persister: Arc, pub event_sender: broadcast::Sender, + pub broadcaster: Arc, } impl LightningNode { @@ -634,12 +638,12 @@ impl LightningNode { fee_estimator: chain_manager.fee_estimator.clone(), }); - let broadcaster = Arc::new(SenseiBroadcaster { - node_id: id.clone(), - broadcaster: chain_manager.broadcaster.clone(), - wallet_database: Arc::new(Mutex::new(wallet_database.clone())), - event_sender: event_sender.clone(), - }); + let broadcaster = Arc::new(SenseiBroadcaster::new( + id.clone(), + chain_manager.broadcaster.clone(), + Arc::new(Mutex::new(wallet_database.clone())), + event_sender.clone(), + )); let persistence_store = AnyKVStore::Database(DatabaseStore::new(database.clone(), id.clone())); @@ -880,6 +884,7 @@ impl LightningNode { stop_listen, persister, event_sender, + broadcaster, }) } @@ -1021,46 +1026,28 @@ impl LightningNode { (handles, background_processor) } - // `custom_id` will be user_channel_id in FundingGenerated event - // allows use to tie the create_channel call with the event - pub fn open_channel( + pub async fn open_channels( &self, - peer_pubkey: PublicKey, - channel_amt_sat: u64, - push_amt_msat: u64, - custom_id: u64, - announced_channel: bool, - ) -> Result<[u8; 32], Error> { - let config = UserConfig { - peer_channel_config_limits: ChannelHandshakeLimits { - // lnd's max to_self_delay is 2016, so we want to be compatible. - their_to_self_delay: 2016, - ..Default::default() - }, - channel_options: ChannelConfig { - announced_channel, - ..Default::default() - }, - ..Default::default() - }; + requests: Vec, + ) -> Vec<(ChannelOpenRequest, Result<[u8; 32], Error>)> { + let mut opener = ChannelOpener::new( + self.id.clone(), + self.channel_manager.clone(), + self.chain_manager.clone(), + self.wallet.clone(), + self.event_sender.subscribe(), + self.broadcaster.clone(), + ); + opener.open_batch(requests).await + } - // TODO: want to be logging channels in db for matching forwarded payments - match self.channel_manager.create_channel( - peer_pubkey, - channel_amt_sat, - push_amt_msat, - custom_id, - Some(config), - ) { - Ok(short_channel_id) => { - println!("EVENT: initiated channel with peer {}. ", peer_pubkey); - Ok(short_channel_id) - } - Err(e) => { - println!("ERROR: failed to open channel: {:?}", e); - Err(e.into()) - } - } + // `custom_id` will be user_channel_id in FundingGenerated event + // allows use to tie the create_channel call with the event + pub async fn open_channel(&self, request: ChannelOpenRequest) -> Result<[u8; 32], Error> { + let requests = vec![request]; + let mut responses = self.open_channels(requests).await; + let (_request, result) = responses.pop().unwrap(); + result } pub async fn connect_to_peer(&self, pubkey: PublicKey, addr: SocketAddr) -> Result<(), Error> { @@ -1470,37 +1457,59 @@ impl LightningNode { balance_satoshis: balance, }) } - NodeRequest::OpenChannel { - node_connection_string, - amt_satoshis, - public, - } => { - let (pubkey, addr) = parse_peer_info(node_connection_string.clone()).await?; - - let found_peer = self - .peer_manager - .get_peer_node_ids() - .into_iter() - .find(|node_pubkey| *node_pubkey == pubkey); - - if found_peer.is_none() { - self.connect_to_peer(pubkey, addr).await?; + NodeRequest::OpenChannels { channels } => { + let mut requests = vec![]; + + // TODO: i guess we need to filter out peers we couldn't connect to instead of unwrap() + for channel in &channels { + let (pubkey, addr) = parse_peer_info(channel.node_connection_string.clone()) + .await + .unwrap_or_else(|_| { + panic!( + "failed to parse connection string: {}", + channel.node_connection_string + ) + }); + connect_peer_if_necessary(pubkey, addr, self.peer_manager.clone()) + .await + .unwrap_or_else(|_| { + panic!("failed to connect to peer {}@{}", pubkey, addr) + }); + requests.push(ChannelOpenRequest { + node_connection_string: channel.node_connection_string.clone(), + peer_pubkey: pubkey, + channel_amt_sat: channel.amt_satoshis, + push_amt_msat: 0, + custom_id: thread_rng().gen_range(1..u64::MAX), + announced_channel: channel.public, + }); } - let res = self.open_channel(pubkey, amt_satoshis, 0, 0, public); + let responses = self.open_channels(requests).await; - match res { - Ok(temp_channel_id) => { - let _ = self.persister.persist_channel_peer(&node_connection_string); - Ok(NodeResponse::OpenChannel { - temp_channel_id: hex_utils::hex_str(&temp_channel_id), + Ok(NodeResponse::OpenChannels { + channels, + results: responses + .into_iter() + .map(|(request, result)| match result { + Ok(temp_channel_id) => { + let _ = self + .persister + .persist_channel_peer(&request.node_connection_string); + OpenChannelResult { + error: false, + error_message: None, + temp_channel_id: Some(hex_utils::hex_str(&temp_channel_id)), + } + } + Err(e) => OpenChannelResult { + error: true, + error_message: Some(e.to_string()), + temp_channel_id: None, + }, }) - } - Err(e) => Ok(NodeResponse::Error(NodeRequestError::Sensei(format!( - "Failed to open channel: {:?}", - e - )))), - } + .collect::>(), + }) } NodeRequest::SendPayment { invoice } => { let invoice = self.get_invoice_from_str(&invoice)?; diff --git a/senseicore/src/services/admin.rs b/senseicore/src/services/admin.rs index a9082d1..74ec2c9 100644 --- a/senseicore/src/services/admin.rs +++ b/senseicore/src/services/admin.rs @@ -20,7 +20,7 @@ use entity::sea_orm::{ActiveModelTrait, ActiveValue}; use lightning_background_processor::BackgroundProcessor; use macaroon::Macaroon; use serde::Serialize; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet, VecDeque}; use std::sync::atomic::Ordering; use std::{collections::hash_map::Entry, fs, sync::Arc}; use tokio::sync::{broadcast, Mutex}; @@ -141,6 +141,7 @@ pub struct AdminService { pub database: Arc, pub chain_manager: Arc, pub event_sender: broadcast::Sender, + pub available_ports: Arc>>, } impl AdminService { @@ -151,6 +152,23 @@ impl AdminService { chain_manager: Arc, event_sender: broadcast::Sender, ) -> Self { + let mut used_ports = HashSet::new(); + let mut available_ports = VecDeque::new(); + database + .list_ports_in_use() + .await + .unwrap() + .into_iter() + .for_each(|port| { + used_ports.insert(port); + }); + + for port in config.port_range_min..config.port_range_max { + if !used_ports.contains(&port) { + available_ports.push_back(port); + } + } + Self { data_dir: String::from(data_dir), config: Arc::new(config), @@ -158,6 +176,7 @@ impl AdminService { database: Arc::new(database), chain_manager, event_sender, + available_ports: Arc::new(Mutex::new(available_ports)), } } } @@ -400,34 +419,31 @@ impl AdminService { role: node::NodeRole, ) -> Result<(node::Model, Macaroon), crate::error::Error> { let listen_addr = public_ip::addr().await.unwrap().to_string(); + let listen_port: i32 = match role { node::NodeRole::Root => 9735, node::NodeRole::Default => { - let mut port = self.config.port_range_min; - let mut port_used_by_system = !portpicker::is_free(port); - let mut port_used_by_sensei = - self.database.port_in_use(&listen_addr, port.into()).await?; - - while port <= self.config.port_range_max - && (port_used_by_system || port_used_by_sensei) - { - port += 1; - port_used_by_system = !portpicker::is_free(port); - port_used_by_sensei = - self.database.port_in_use(&listen_addr, port.into()).await?; - } - - port.into() + let mut available_ports = self.available_ports.lock().await; + available_ports.pop_front().unwrap().into() } }; let node_id = Uuid::new_v4().to_string(); - let (node_pubkey, node_macaroon) = LightningNode::get_node_pubkey_and_macaroon( + + let result = LightningNode::get_node_pubkey_and_macaroon( node_id.clone(), passphrase, self.database.clone(), ) - .await?; + .await; + + if let Err(e) = result { + let mut available_ports = self.available_ports.lock().await; + available_ports.push_front(listen_port.try_into().unwrap()); + return Err(e); + } + + let (node_pubkey, macaroon) = result.unwrap(); let node = entity::node::ActiveModel { id: ActiveValue::Set(node_id), @@ -442,12 +458,22 @@ impl AdminService { ..Default::default() }; - let node = node.insert(self.database.get_connection()).await.unwrap(); + let result = node.insert(self.database.get_connection()).await; + + if let Err(e) = result { + let mut available_ports = self.available_ports.lock().await; + available_ports.push_front(listen_port.try_into().unwrap()); + return Err(e.into()); + } + + let node = result.unwrap(); - Ok((node, node_macaroon)) + Ok((node, macaroon)) } // note: please be sure to stop the node first? maybe? + // TODO: this was never updated with the DB rewrite + // need to release the port and actually delete the node async fn delete_node(&self, node: node::Model) -> Result<(), crate::error::Error> { let data_dir = format!("{}/{}/{}", self.data_dir, self.config.network, node.id); Ok(fs::remove_dir_all(&data_dir)?) diff --git a/senseicore/src/services/node.rs b/senseicore/src/services/node.rs index 465b224..1ba3127 100644 --- a/senseicore/src/services/node.rs +++ b/senseicore/src/services/node.rs @@ -17,7 +17,7 @@ use tower::Service; use crate::hex_utils; use lightning::ln::channelmanager::ChannelDetails; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use super::{PaginationRequest, PaginationResponse, PaymentsFilter}; @@ -115,6 +115,20 @@ impl From for Channel { } } +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct OpenChannelInfo { + pub node_connection_string: String, + pub amt_satoshis: u64, + pub public: bool, +} + +#[derive(Serialize, Clone, Debug)] +pub struct OpenChannelResult { + pub error: bool, + pub error_message: Option, + pub temp_channel_id: Option, +} + pub enum NodeRequest { StartNode { passphrase: String, @@ -122,10 +136,8 @@ pub enum NodeRequest { StopNode {}, GetUnusedAddress {}, GetBalance {}, - OpenChannel { - node_connection_string: String, - amt_satoshis: u64, - public: bool, + OpenChannels { + channels: Vec, }, SendPayment { invoice: String, @@ -187,8 +199,9 @@ pub enum NodeResponse { GetBalance { balance_satoshis: u64, }, - OpenChannel { - temp_channel_id: String, + OpenChannels { + channels: Vec, + results: Vec, }, SendPayment {}, DecodeInvoice { diff --git a/senseicore/tests/smoke_test.rs b/senseicore/tests/smoke_test.rs index 6bf7f41..4de670a 100644 --- a/senseicore/tests/smoke_test.rs +++ b/senseicore/tests/smoke_test.rs @@ -8,8 +8,9 @@ mod test { use migration::{Migrator, MigratorTrait}; use senseicore::events::SenseiEvent; use senseicore::node::{HTLCStatus, LightningNode}; - use senseicore::services::node::Channel; + use senseicore::services::node::{Channel, OpenChannelInfo}; use senseicore::services::{PaginationRequest, PaymentsFilter}; + use serial_test::serial; use std::{str::FromStr, sync::Arc, time::Duration}; use tokio::runtime::{Builder, Handle}; use tokio::sync::broadcast; @@ -283,6 +284,122 @@ mod test { .unwrap() } + async fn open_channels( + bitcoind: &BitcoinD, + from: Arc, + to: Vec>, + amt_sat: u64, + ) -> Vec<(Channel, Arc)> { + let miner_address = bitcoind.client.get_new_address(None, None).unwrap(); + + let channel_infos = to + .iter() + .map(|to| { + let node_connection_string = format!( + "{}@{}:{}", + to.get_pubkey(), + to.listen_addresses.first().unwrap(), + to.listen_port + ); + + OpenChannelInfo { + node_connection_string, + amt_satoshis: amt_sat, + public: true, + } + }) + .collect::>(); + + let mut event_receiver = from.event_sender.subscribe(); + + from.call(NodeRequest::OpenChannels { + channels: channel_infos, + }) + .await + .unwrap(); + + let from_node_id = from.id.clone(); + let filter = move |event| { + if let SenseiEvent::TransactionBroadcast { node_id, .. } = event { + if *node_id == from_node_id { + return true; + } + } + return false; + }; + + let event = wait_for_event(&mut event_receiver, filter, 15000, 250).await; + assert!(event.is_some()); + + let funding_txid = match event.unwrap() { + SenseiEvent::TransactionBroadcast { txid, .. } => Some(txid), + _ => None, + } + .unwrap(); + + bitcoind + .client + .generate_to_address(10, &miner_address) + .unwrap(); + + let usable_from = from.clone(); + let expected_channels = to.len(); + let has_usable_channels = move || { + let usable_channels = usable_from + .list_channels(PaginationRequest { + page: 0, + take: 5, + query: None, + }) + .unwrap() + .0 + .into_iter() + .filter(|channel| channel.is_usable) + .collect::>(); + + usable_channels.len() >= expected_channels + }; + assert!(wait_until(Box::new(has_usable_channels), 15000, 250).await); + + for to_node in &to { + let usable_to = to_node.clone(); + + let has_usable_channels = move || { + let usable_channels = usable_to + .list_channels(PaginationRequest { + page: 0, + take: 5, + query: None, + }) + .unwrap() + .0 + .into_iter() + .filter(|channel| channel.is_usable) + .collect::>(); + + usable_channels.len() == 1 + }; + assert!(wait_until(Box::new(has_usable_channels), 15000, 250).await); + } + + from.list_channels(PaginationRequest { + page: 0, + take: to.len() as u32, + query: None, + }) + .unwrap() + .0 + .into_iter() + .map(|channel| { + let counterparty = to + .iter() + .find(|to| to.node_info().unwrap().node_pubkey == channel.counterparty_pubkey) + .unwrap(); + (channel, counterparty.clone()) + }) + .collect() + } + async fn open_channel( bitcoind: &BitcoinD, from: Arc, @@ -299,10 +416,12 @@ mod test { let mut event_receiver = from.event_sender.subscribe(); - from.call(NodeRequest::OpenChannel { - node_connection_string: node_connection_string, - amt_satoshis: amt_sat, - public: true, + from.call(NodeRequest::OpenChannels { + channels: vec![OpenChannelInfo { + node_connection_string: node_connection_string, + amt_satoshis: amt_sat, + public: true, + }], }) .await .unwrap(); @@ -484,7 +603,7 @@ mod test { .await } - fn run_test(test: fn(BitcoinD, AdminService) -> F) -> F::Output + fn run_test(name: &str, test: fn(BitcoinD, AdminService) -> F) -> F::Output where F: Future, { @@ -504,7 +623,7 @@ mod test { .build() .unwrap() .block_on(async move { - let sensei_dir = String::from("./.sensei-tests"); + let sensei_dir = format!("./.sensei-tests/{}", name); let bitcoind = setup_bitcoind(); let admin_service = setup_sensei(&sensei_dir, &bitcoind, persistence_runtime_handle).await; @@ -621,8 +740,75 @@ mod test { )); } + async fn batch_open_channels_test(bitcoind: BitcoinD, admin_service: AdminService) { + let alice = create_root_node(&admin_service, "alice", "alice", true).await; + let bob = create_node(&admin_service, "bob", "bob", true).await; + let charlie = create_node(&admin_service, "charlie", "charlie", true).await; + let doug = create_node(&admin_service, "doug", "doug", true).await; + fund_node(&bitcoind, alice.clone()).await; + + let alice_channels_with_counterparties = open_channels( + &bitcoind, + alice.clone(), + vec![bob.clone(), charlie.clone(), doug.clone()], + 1_000_000, + ) + .await; + + let num_invoices = 5; + let invoice_amt = 3500; + let mut bob_invoices = batch_create_invoices(bob.clone(), invoice_amt, num_invoices).await; + let mut charlie_invoices = + batch_create_invoices(charlie.clone(), invoice_amt, num_invoices).await; + let mut doug_invoices = + batch_create_invoices(doug.clone(), invoice_amt, num_invoices).await; + let mut invoices = vec![]; + invoices.append(&mut bob_invoices); + invoices.append(&mut charlie_invoices); + invoices.append(&mut doug_invoices); + future::try_join_all( + invoices + .into_iter() + .map(|invoice| pay_invoice(alice.clone(), invoice)) + .map(tokio::spawn), + ) + .await + .unwrap(); + + let alice_test = alice.clone(); + let has_payments = move || { + let pagination = PaginationRequest { + page: 0, + take: 1, + query: None, + }; + let filter = PaymentsFilter { + status: Some(HTLCStatus::Succeeded.to_string()), + origin: None, + }; + let (_payments, pagination) = alice_test + .database + .list_payments_sync(alice_test.id.clone(), pagination, filter) + .unwrap(); + pagination.total == (num_invoices * 3) as u64 + }; + + assert!(wait_until(has_payments, 60000, 500).await); + + for (channel, counterparty) in alice_channels_with_counterparties { + close_channel(&bitcoind, alice.clone(), counterparty, channel, false).await + } + } + + #[test] + #[serial] + fn run_batch_open_channel_test() { + run_test("batch_open_channels", batch_open_channels_test) + } + #[test] + #[serial] fn run_smoke_test() { - run_test(smoke_test) + run_test("smoke_test", smoke_test) } } diff --git a/src/cli.rs b/src/cli.rs index df37a02..cab307f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,8 +21,8 @@ use tonic::{metadata::MetadataValue, transport::Channel, Request}; use crate::sensei::{ CloseChannelRequest, ConnectPeerRequest, CreateAdminRequest, CreateInvoiceRequest, CreateNodeRequest, GetUnusedAddressRequest, InfoRequest, KeysendRequest, ListChannelsRequest, - ListNodesRequest, ListPaymentsRequest, ListPeersRequest, OpenChannelRequest, PayInvoiceRequest, - SignMessageRequest, StartAdminRequest, StartNodeRequest, + ListNodesRequest, ListPaymentsRequest, ListPeersRequest, OpenChannelInfo, OpenChannelsRequest, + PayInvoiceRequest, SignMessageRequest, StartAdminRequest, StartNodeRequest, }; pub mod sensei { @@ -340,13 +340,15 @@ async fn main() -> Result<(), Box> { .parse() .expect("public must be true or false"); - let request = tonic::Request::new(OpenChannelRequest { - node_connection_string: node_connection_string.to_string(), - amt_satoshis, - public, + let request = tonic::Request::new(OpenChannelsRequest { + channels: vec![OpenChannelInfo { + node_connection_string: node_connection_string.to_string(), + amt_satoshis, + public, + }], }); - let response = client.open_channel(request).await?; + let response = client.open_channels(request).await?; println!("{:?}", response.into_inner()); } "closechannel" => { diff --git a/src/grpc/adaptor.rs b/src/grpc/adaptor.rs index 99c88ad..2758713 100644 --- a/src/grpc/adaptor.rs +++ b/src/grpc/adaptor.rs @@ -8,10 +8,11 @@ // licenses. use super::sensei::{ - Channel as ChannelMessage, DeletePaymentRequest, DeletePaymentResponse, Info as InfoMessage, - LabelPaymentRequest, LabelPaymentResponse, PaginationRequest, PaginationResponse, - Payment as PaymentMessage, PaymentsFilter, Peer as PeerMessage, StartNodeRequest, - StartNodeResponse, StopNodeRequest, StopNodeResponse, + self, Channel as ChannelMessage, DeletePaymentRequest, DeletePaymentResponse, + Info as InfoMessage, LabelPaymentRequest, LabelPaymentResponse, OpenChannelsRequest, + OpenChannelsResponse, PaginationRequest, PaginationResponse, Payment as PaymentMessage, + PaymentsFilter, Peer as PeerMessage, StartNodeRequest, StartNodeResponse, StopNodeRequest, + StopNodeResponse, }; use super::sensei::{ @@ -20,9 +21,8 @@ use super::sensei::{ GetBalanceRequest, GetBalanceResponse, GetUnusedAddressRequest, GetUnusedAddressResponse, InfoRequest, InfoResponse, KeysendRequest, KeysendResponse, ListChannelsRequest, ListChannelsResponse, ListPaymentsRequest, ListPaymentsResponse, ListPeersRequest, - ListPeersResponse, OpenChannelRequest, OpenChannelResponse, PayInvoiceRequest, - PayInvoiceResponse, SignMessageRequest, SignMessageResponse, VerifyMessageRequest, - VerifyMessageResponse, + ListPeersResponse, PayInvoiceRequest, PayInvoiceResponse, SignMessageRequest, + SignMessageResponse, VerifyMessageRequest, VerifyMessageResponse, }; use senseicore::services::{ @@ -189,22 +189,45 @@ impl TryFrom for GetBalanceResponse { } } -impl From for NodeRequest { - fn from(req: OpenChannelRequest) -> Self { - NodeRequest::OpenChannel { - node_connection_string: req.node_connection_string, - amt_satoshis: req.amt_satoshis, - public: req.public, +impl From for NodeRequest { + fn from(req: OpenChannelsRequest) -> Self { + NodeRequest::OpenChannels { + channels: req + .channels + .into_iter() + .map(|channel| senseicore::services::node::OpenChannelInfo { + node_connection_string: channel.node_connection_string, + amt_satoshis: channel.amt_satoshis, + public: channel.public, + }) + .collect::>(), } } } -impl TryFrom for OpenChannelResponse { +impl TryFrom for OpenChannelsResponse { type Error = String; fn try_from(res: NodeResponse) -> Result { match res { - NodeResponse::OpenChannel { temp_channel_id } => Ok(Self { temp_channel_id }), + NodeResponse::OpenChannels { channels, results } => Ok(Self { + channels: channels + .into_iter() + .map(|channel| sensei::OpenChannelInfo { + node_connection_string: channel.node_connection_string, + amt_satoshis: channel.amt_satoshis, + public: channel.public, + }) + .collect::>(), + results: results + .into_iter() + .map(|result| sensei::OpenChannelResult { + error: result.error, + error_message: result.error_message, + temp_channel_id: result.temp_channel_id, + }) + .collect::>(), + }), _ => Err("impossible".to_string()), } } diff --git a/src/grpc/node.rs b/src/grpc/node.rs index 77d70ca..f20eeae 100644 --- a/src/grpc/node.rs +++ b/src/grpc/node.rs @@ -19,7 +19,7 @@ use super::{ GetUnusedAddressRequest, GetUnusedAddressResponse, InfoRequest, InfoResponse, KeysendRequest, KeysendResponse, LabelPaymentRequest, LabelPaymentResponse, ListChannelsRequest, ListChannelsResponse, ListPaymentsRequest, ListPaymentsResponse, - ListPeersRequest, ListPeersResponse, OpenChannelRequest, OpenChannelResponse, + ListPeersRequest, ListPeersResponse, OpenChannelsRequest, OpenChannelsResponse, PayInvoiceRequest, PayInvoiceResponse, SignMessageRequest, SignMessageResponse, StartNodeRequest, StartNodeResponse, StopNodeRequest, StopNodeResponse, VerifyMessageRequest, VerifyMessageResponse, @@ -143,10 +143,10 @@ impl Node for NodeService { .map(Response::new) .map_err(|_e| Status::unknown("unknown error")) } - async fn open_channel( + async fn open_channels( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { self.authenticated_request(request.metadata().clone(), request.into_inner().into()) .await? .try_into() diff --git a/src/http/admin.rs b/src/http/admin.rs index 977bf3b..378c4cd 100644 --- a/src/http/admin.rs +++ b/src/http/admin.rs @@ -372,13 +372,16 @@ pub async fn login( .http_only(true) .finish(); cookies.add(macaroon_cookie); - let token_cookie = Cookie::build("token", token).http_only(true).finish(); + let token_cookie = Cookie::build("token", token.clone()) + .http_only(true) + .finish(); cookies.add(token_cookie); Ok(Json(json!({ "pubkey": node.pubkey, "alias": node.alias, "macaroon": macaroon, "role": node.role as u16, + "token": token }))) } _ => Err(StatusCode::UNPROCESSABLE_ENTITY), diff --git a/src/http/node.rs b/src/http/node.rs index 311509a..8127184 100644 --- a/src/http/node.rs +++ b/src/http/node.rs @@ -16,7 +16,7 @@ use axum::routing::{get, post}; use axum::Router; use http::{HeaderValue, StatusCode}; use senseicore::services::admin::AdminRequest; -use senseicore::services::node::{NodeRequest, NodeRequestError, NodeResponse}; +use senseicore::services::node::{NodeRequest, NodeRequestError, NodeResponse, OpenChannelInfo}; use senseicore::services::{ListChannelsParams, ListPaymentsParams, ListTransactionsParams}; use senseicore::utils; use serde::Deserialize; @@ -69,18 +69,14 @@ impl From for NodeRequest { } #[derive(Deserialize)] -pub struct OpenChannelParams { - pub node_connection_string: String, - pub amt_satoshis: u64, - pub public: bool, +pub struct BatchOpenChannelParams { + channels: Vec, } -impl From for NodeRequest { - fn from(params: OpenChannelParams) -> Self { - Self::OpenChannel { - node_connection_string: params.node_connection_string, - amt_satoshis: params.amt_satoshis, - public: params.public, +impl From for NodeRequest { + fn from(params: BatchOpenChannelParams) -> Self { + Self::OpenChannels { + channels: params.channels, } } } @@ -211,7 +207,7 @@ pub fn add_routes(router: Router) -> Router { .route("/v1/node/invoices/decode", post(decode_invoice)) .route("/v1/node/payments/label", post(label_payment)) .route("/v1/node/payments/delete", post(delete_payment)) - .route("/v1/node/channels/open", post(open_channel)) + .route("/v1/node/channels/open", post(open_channels)) .route("/v1/node/channels/close", post(close_channel)) .route("/v1/node/keysend", post(keysend)) .route("/v1/node/peers/connect", post(connect_peer)) @@ -461,14 +457,14 @@ pub async fn decode_invoice( handle_authenticated_request(admin_service, request, macaroon, cookies).await } -pub async fn open_channel( +pub async fn open_channels( Extension(admin_service): Extension>, Json(payload): Json, AuthHeader { macaroon, token: _ }: AuthHeader, cookies: Cookies, ) -> Result, StatusCode> { let request = { - let params: Result = serde_json::from_value(payload); + let params: Result = serde_json::from_value(payload); match params { Ok(params) => Ok(params.into()), Err(_) => Err(StatusCode::UNPROCESSABLE_ENTITY), diff --git a/system-tests/system-test.py b/system-tests/system-test.py index 9e66de4..5e80ac9 100755 --- a/system-tests/system-test.py +++ b/system-tests/system-test.py @@ -19,7 +19,7 @@ from sensei_pb2 import ( CloseChannelRequest, GetStatusRequest, CreateNodeRequest, GetUnusedAddressRequest, GetBalanceRequest, - ListPaymentsRequest, OpenChannelRequest, ListChannelsRequest, CreateInvoiceRequest, + ListPaymentsRequest, OpenChannelsRequest, OpenChannelInfo, ListChannelsRequest, CreateInvoiceRequest, PaginationRequest, PayInvoiceRequest ) @@ -148,15 +148,17 @@ def run(): bob, meta_b, id_b = fund_node(btc, metadata, senseid, 1) print('Create channel alice -> bob') - alice.OpenChannel(OpenChannelRequest(node_connection_string=f"{id_b}@127.0.0.1:10000", amt_satoshis=CHANNEL_VALUE_SAT, public=True), + oc_res = alice.OpenChannels(OpenChannelsRequest(channels=[OpenChannelInfo(node_connection_string=f"{id_b}@127.0.0.1:10000", amt_satoshis=CHANNEL_VALUE_SAT, public=True)]), metadata=meta_a) + + print(oc_res) wait_until('channel at bob', lambda: bob.ListChannels(ListChannelsRequest(), metadata=meta_b).channels[0]) assert not bob.ListChannels(ListChannelsRequest(), metadata=meta_b).channels[0].is_usable charlie, meta_c, id_c = fund_node(btc, metadata, senseid, 2) print('Create channel bob -> charlie') - bob.OpenChannel(OpenChannelRequest(node_connection_string=f"{id_c}@127.0.0.1:10001", amt_satoshis=CHANNEL_VALUE_SAT, public=True), + bob.OpenChannels(OpenChannelsRequest(channels=[OpenChannelInfo(node_connection_string=f"{id_c}@127.0.0.1:10001", amt_satoshis=CHANNEL_VALUE_SAT, public=True)]), metadata=meta_b) wait_until('channel at charlie', lambda: charlie.ListChannels(ListChannelsRequest(), metadata=meta_c).channels[0]) assert not charlie.ListChannels(ListChannelsRequest(), metadata=meta_c).channels[0].is_usable diff --git a/web-admin/package-lock.json b/web-admin/package-lock.json index f4fa3c8..74bc649 100644 --- a/web-admin/package-lock.json +++ b/web-admin/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@headlessui/react": "^1.4.2", "@heroicons/react": "^1.0.5", - "@l2-technology/sensei-client": "^0.1.8", + "@l2-technology/sensei-client": "^0.1.14", "@tailwindcss/aspect-ratio": "^0.4.0", "@tailwindcss/forms": "^0.4.0", "@tailwindcss/typography": "^0.5.0", @@ -3189,12 +3189,9 @@ } }, "node_modules/@l2-technology/sensei-client": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@l2-technology/sensei-client/-/sensei-client-0.1.8.tgz", - "integrity": "sha512-nxtn0G7tS0MlCT/16HPssxiqCAucycc3msR+P78+gZ4sTIg/fnmgefg0HAYWBfjUr37VD9VjVN6pUwYXPuHlew==", - "dependencies": { - "cross-fetch": "^3.1.5" - } + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/@l2-technology/sensei-client/-/sensei-client-0.1.14.tgz", + "integrity": "sha512-d9D0rXjSLiwcwLgO1JU6JvZ5/Plo7H/QNYuEzsONudWvpy9j5KYTUCE9tkioxom/LloddMQkU8ilH4BsJcGEWw==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -8158,14 +8155,6 @@ "yarn": ">=1" } }, - "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dependencies": { - "node-fetch": "2.6.7" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -16733,6 +16722,7 @@ "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -16751,17 +16741,20 @@ "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "peer": true }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "peer": true }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -25929,12 +25922,9 @@ } }, "@l2-technology/sensei-client": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@l2-technology/sensei-client/-/sensei-client-0.1.8.tgz", - "integrity": "sha512-nxtn0G7tS0MlCT/16HPssxiqCAucycc3msR+P78+gZ4sTIg/fnmgefg0HAYWBfjUr37VD9VjVN6pUwYXPuHlew==", - "requires": { - "cross-fetch": "^3.1.5" - } + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/@l2-technology/sensei-client/-/sensei-client-0.1.14.tgz", + "integrity": "sha512-d9D0rXjSLiwcwLgO1JU6JvZ5/Plo7H/QNYuEzsONudWvpy9j5KYTUCE9tkioxom/LloddMQkU8ilH4BsJcGEWw==" }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -29708,14 +29698,6 @@ "cross-spawn": "^7.0.1" } }, - "cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "requires": { - "node-fetch": "2.6.7" - } - }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -36160,6 +36142,7 @@ "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "peer": true, "requires": { "whatwg-url": "^5.0.0" }, @@ -36167,17 +36150,20 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "peer": true }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "peer": true }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "peer": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/web-admin/package.json b/web-admin/package.json index 7a1f7f0..7952e3c 100644 --- a/web-admin/package.json +++ b/web-admin/package.json @@ -6,7 +6,7 @@ "dependencies": { "@headlessui/react": "^1.4.2", "@heroicons/react": "^1.0.5", - "@l2-technology/sensei-client": "^0.1.8", + "@l2-technology/sensei-client": "^0.1.14", "@tailwindcss/aspect-ratio": "^0.4.0", "@tailwindcss/forms": "^0.4.0", "@tailwindcss/typography": "^0.5.0",