diff --git a/Cargo.toml b/Cargo.toml index 8d8bee99d..1ca122c18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,10 @@ description = "A ready-to-go node implementation based on LDK." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["staticlib", "cdylib"] +name = "ldk_node" + [dependencies] #lightning = { version = "0.0.112", features = ["max_level_trace", "std"] } #lightning-invoice = { version = "0.20" } @@ -47,12 +51,17 @@ chrono = "0.4" futures = "0.3" serde_json = { version = "1.0" } tokio = { version = "1", features = [ "full" ] } +uniffi = { version = "0.21.0", features = ["builtin-bindgen"] } +uniffi_macros = { version = "0.21.0", features = ["builtin-bindgen"] } [dev-dependencies] electrsd = { version = "0.22.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_23_0"] } electrum-client = "0.12.0" once_cell = "1.16.0" +[build-dependencies] +uniffi_build = "0.21.0" + [profile.release] panic = "abort" diff --git a/build.rs b/build.rs new file mode 100644 index 000000000..2bd473cd6 --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi_build::generate_scaffolding("uniffi/ldk_node.udl").unwrap(); +} diff --git a/src/error.rs b/src/error.rs index d0d38b6c9..2c6c18da1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,12 +11,20 @@ pub enum Error { FundingTxCreationFailed, /// A network connection has been closed. ConnectionFailed, + /// The given address is invalid. + AddressInvalid, + /// The given public key is invalid. + PublicKeyInvalid, + /// The given payment hash is invalid. + PaymentHashInvalid, /// Payment of the given invoice has already been intiated. NonUniquePaymentHash, /// The given invoice is invalid. InvoiceInvalid, /// Invoice creation failed. InvoiceCreationFailed, + /// The given channel ID is invalid. + ChannelIdInvalid, /// No route for the given target could be found. RoutingFailed, /// A given peer info could not be parsed. @@ -40,13 +48,15 @@ impl fmt::Display for Error { match *self { Self::AlreadyRunning => write!(f, "Node is already running."), Self::NotRunning => write!(f, "Node is not running."), - Self::FundingTxCreationFailed => { - write!(f, "Funding transaction could not be created.") - } + Self::FundingTxCreationFailed => write!(f, "Funding transaction could not be created."), Self::ConnectionFailed => write!(f, "Network connection closed."), + Self::AddressInvalid => write!(f, "The given address is invalid."), + Self::PublicKeyInvalid => write!(f, "The given public key is invalid."), + Self::PaymentHashInvalid => write!(f, "The given payment hash is invalid."), Self::NonUniquePaymentHash => write!(f, "An invoice must not get payed twice."), Self::InvoiceInvalid => write!(f, "The given invoice is invalid."), Self::InvoiceCreationFailed => write!(f, "Failed to create invoice."), + Self::ChannelIdInvalid => write!(f, "The given channel ID is invalid."), Self::RoutingFailed => write!(f, "Failed to find route."), Self::PeerInfoParseFailed => write!(f, "Failed to parse the given peer information."), Self::ChannelCreationFailed => write!(f, "Failed to create channel."), diff --git a/src/event.rs b/src/event.rs index 5f679b66c..4f19ca893 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,6 +1,6 @@ use crate::{ - hex_utils, ChannelManager, Config, Error, KeysManager, NetworkGraph, PaymentInfo, - PaymentInfoStorage, PaymentStatus, Wallet, + hex_utils, ChannelId, ChannelManager, Config, Error, KeysManager, NetworkGraph, PaymentInfo, + PaymentInfoStorage, PaymentStatus, UserChannelId, Wallet, }; use crate::logger::{log_error, log_given_level, log_info, log_internal, Logger}; @@ -50,16 +50,16 @@ pub enum Event { /// A channel is ready to be used. ChannelReady { /// The `channel_id` of the channel. - channel_id: [u8; 32], + channel_id: ChannelId, /// The `user_channel_id` of the channel. - user_channel_id: u128, + user_channel_id: UserChannelId, }, /// A channel has been closed. ChannelClosed { /// The `channel_id` of the channel. - channel_id: [u8; 32], + channel_id: ChannelId, /// The `user_channel_id` of the channel. - user_channel_id: u128, + user_channel_id: UserChannelId, }, } @@ -83,13 +83,13 @@ impl Readable for Event { Ok(Self::PaymentReceived { payment_hash, amount_msat }) } 3u8 => { - let channel_id: [u8; 32] = Readable::read(reader)?; - let user_channel_id: u128 = Readable::read(reader)?; + let channel_id = ChannelId(Readable::read(reader)?); + let user_channel_id = UserChannelId(Readable::read(reader)?); Ok(Self::ChannelReady { channel_id, user_channel_id }) } 4u8 => { - let channel_id: [u8; 32] = Readable::read(reader)?; - let user_channel_id: u128 = Readable::read(reader)?; + let channel_id = ChannelId(Readable::read(reader)?); + let user_channel_id = UserChannelId(Readable::read(reader)?); Ok(Self::ChannelClosed { channel_id, user_channel_id }) } _ => Err(lightning::ln::msgs::DecodeError::InvalidValue), @@ -118,14 +118,14 @@ impl Writeable for Event { } Self::ChannelReady { channel_id, user_channel_id } => { 3u8.write(writer)?; - channel_id.write(writer)?; - user_channel_id.write(writer)?; + channel_id.0.write(writer)?; + user_channel_id.0.write(writer)?; Ok(()) } Self::ChannelClosed { channel_id, user_channel_id } => { 4u8.write(writer)?; - channel_id.write(writer)?; - user_channel_id.write(writer)?; + channel_id.0.write(writer)?; + user_channel_id.0.write(writer)?; Ok(()) } } @@ -562,7 +562,10 @@ where counterparty_node_id, ); self.event_queue - .add_event(Event::ChannelReady { channel_id, user_channel_id }) + .add_event(Event::ChannelReady { + channel_id: ChannelId(channel_id), + user_channel_id: UserChannelId(user_channel_id), + }) .expect("Failed to push to event queue"); } LdkEvent::ChannelClosed { channel_id, reason, user_channel_id } => { @@ -573,7 +576,10 @@ where reason ); self.event_queue - .add_event(Event::ChannelClosed { channel_id, user_channel_id }) + .add_event(Event::ChannelClosed { + channel_id: ChannelId(channel_id), + user_channel_id: UserChannelId(user_channel_id), + }) .expect("Failed to push to event queue"); } LdkEvent::DiscardFunding { .. } => {} @@ -592,7 +598,10 @@ mod tests { let test_persister = Arc::new(TestPersister::new()); let event_queue = EventQueue::new(Arc::clone(&test_persister)); - let expected_event = Event::ChannelReady { channel_id: [23u8; 32], user_channel_id: 2323 }; + let expected_event = Event::ChannelReady { + channel_id: ChannelId([23u8; 32]), + user_channel_id: UserChannelId(2323), + }; event_queue.add_event(expected_event.clone()).unwrap(); assert!(test_persister.get_and_clear_pending_persist()); diff --git a/src/lib.rs b/src/lib.rs index 40f2cb741..502d511d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,6 @@ //! - Wallet and channel states are persisted to disk. //! - Gossip is retrieved over the P2P network. -#![deny(missing_docs)] #![deny(broken_intra_doc_links)] #![deny(private_intra_doc_links)] #![allow(bare_trait_objects)] @@ -35,7 +34,8 @@ mod tests; mod types; mod wallet; -pub use error::Error; +pub use error::Error as NodeError; +use error::Error; pub use event::Event; use event::{EventHandler, EventQueue}; use peer_store::{PeerInfo, PeerInfoStorage}; @@ -43,7 +43,7 @@ use types::{ ChainMonitor, ChannelManager, GossipSync, InvoicePayer, KeysManager, NetworkGraph, OnionMessenger, PaymentInfoStorage, PeerManager, Router, Scorer, }; -pub use types::{PaymentInfo, PaymentStatus}; +pub use types::{ChannelId, PaymentInfo, PaymentStatus, UserChannelId}; use wallet::Wallet; use logger::{log_error, log_given_level, log_info, log_internal, FilesystemLogger, Logger}; @@ -76,7 +76,7 @@ use bdk::template::Bip84; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; -use bitcoin::BlockHash; +use bitcoin::{Address, BlockHash}; use rand::Rng; @@ -89,6 +89,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, Instant, SystemTime}; +uniffi_macros::include_scaffolding!("ldk_node"); + // The used 'stop gap' parameter used by BDK's wallet sync. This seems to configure the threshold // number of blocks after which BDK stops looking for scripts belonging to the wallet. const BDK_CLIENT_STOP_GAP: usize = 20; @@ -195,8 +197,8 @@ impl Builder { self } - /// Builds an [`Node`] instance according to the options previously configured. - pub fn build(&self) -> Node { + /// Builds a [`Node`] instance according to the options previously configured. + pub fn build(&self) -> Arc { let config = Arc::new(self.config.clone()); let ldk_data_dir = format!("{}/ldk", &config.storage_dir_path.clone()); @@ -410,7 +412,7 @@ impl Builder { let running = RwLock::new(None); - Node { + Arc::new(Node { running, config, wallet, @@ -429,7 +431,7 @@ impl Builder { inbound_payments, outbound_payments, peer_store, - } + }) } } @@ -472,7 +474,7 @@ impl Node { /// Starts the necessary background tasks, such as handling events coming from user input, /// LDK/BDK, and the peer-to-peer network. After this returns, the [`Node`] instance can be /// controlled via the provided API methods in a thread-safe manner. - pub fn start(&mut self) -> Result<(), Error> { + pub fn start(&self) -> Result<(), Error> { // Acquire a run lock and hold it until we're setup. let mut run_lock = self.running.write().unwrap(); if run_lock.is_some() { @@ -486,7 +488,7 @@ impl Node { } /// Disconnects all peers, stops all running background tasks, and shuts down [`Node`]. - pub fn stop(&mut self) -> Result<(), Error> { + pub fn stop(&self) -> Result<(), Error> { let mut run_lock = self.running.write().unwrap(); if run_lock.is_none() { return Err(Error::NotRunning); @@ -704,7 +706,7 @@ impl Node { } /// Retrieve a new on-chain/funding address. - pub fn new_funding_address(&mut self) -> Result { + pub fn new_funding_address(&self) -> Result { let funding_address = self.wallet.get_new_address()?; log_info!(self.logger, "Generated new funding address: {}", funding_address); Ok(funding_address) @@ -712,7 +714,7 @@ impl Node { #[cfg(test)] /// Retrieve the current on-chain balance. - pub fn on_chain_balance(&mut self) -> Result { + pub fn on_chain_balance(&self) -> Result { self.wallet.get_balance() } diff --git a/src/tests/functional_tests.rs b/src/tests/functional_tests.rs index e77a3abbd..d66f8cd19 100644 --- a/src/tests/functional_tests.rs +++ b/src/tests/functional_tests.rs @@ -124,13 +124,13 @@ fn rand_config() -> Config { fn channel_full_cycle() { println!("== Node A =="); let config_a = rand_config(); - let mut node_a = Builder::from_config(config_a).build(); + let node_a = Builder::from_config(config_a).build(); node_a.start().unwrap(); let addr_a = node_a.new_funding_address().unwrap(); println!("\n== Node B =="); let config_b = rand_config(); - let mut node_b = Builder::from_config(config_b).build(); + let node_b = Builder::from_config(config_b).build(); node_b.start().unwrap(); let addr_b = node_b.new_funding_address().unwrap(); @@ -161,10 +161,10 @@ fn channel_full_cycle() { expect_event!(node_a, ChannelReady); let channel_id = match node_b.next_event() { - ref e @ Event::ChannelReady { channel_id, .. } => { + ref e @ Event::ChannelReady { ref channel_id, .. } => { println!("{} got event {:?}", std::stringify!(node_b), e); node_b.event_handled(); - channel_id + channel_id.clone() } ref e => { panic!("{} got unexpected event!: {:?}", std::stringify!(node_b), e); @@ -180,7 +180,7 @@ fn channel_full_cycle() { expect_event!(node_a, PaymentSuccessful); expect_event!(node_b, PaymentReceived); - node_b.close_channel(&channel_id, &node_a.node_id().unwrap()).unwrap(); + node_b.close_channel(&channel_id.0, &node_a.node_id().unwrap()).unwrap(); expect_event!(node_a, ChannelClosed); expect_event!(node_b, ChannelClosed); diff --git a/src/types.rs b/src/types.rs index b05763f46..fe1939377 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,8 @@ +use crate::error::Error; +use crate::hex_utils; use crate::logger::FilesystemLogger; use crate::wallet::{Wallet, WalletKeysManager}; +use crate::UniffiCustomTypeConverter; use lightning::chain::keysinterface::InMemorySigner; use lightning::chain::{chainmonitor, Access}; @@ -9,12 +12,18 @@ use lightning::routing::gossip; use lightning::routing::gossip::P2PGossipSync; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::ProbabilisticScorer; -use lightning_invoice::payment; +use lightning_invoice::{payment, Invoice, SignedRawInvoice}; use lightning_net_tokio::SocketDescriptor; use lightning_persister::FilesystemPersister; use lightning_transaction_sync::EsploraSyncClient; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::PublicKey; +use bitcoin::Address; + use std::collections::HashMap; +use std::str::FromStr; use std::sync::{Arc, Mutex}; // Structs wrapping the particular information which should easily be @@ -95,3 +104,106 @@ pub(crate) type OnionMessenger = lightning::onion_message::OnionMessenger< Arc, IgnoringMessageHandler, >; + +impl UniffiCustomTypeConverter for PublicKey { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(key) = PublicKey::from_str(&val) { + return Ok(key); + } + + Err(Error::PublicKeyInvalid.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for Address { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(addr) = Address::from_str(&val) { + return Ok(addr); + } + + Err(Error::AddressInvalid.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for Invoice { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(signed) = val.parse::() { + if let Ok(invoice) = Invoice::from_signed(signed) { + return Ok(invoice); + } + } + + Err(Error::InvoiceInvalid.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for PaymentHash { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(hash) = Sha256::from_str(&val) { + Ok(PaymentHash(hash.into_inner())) + } else { + Err(Error::PaymentHashInvalid.into()) + } + } + + fn from_custom(obj: Self) -> Self::Builtin { + Sha256::from_slice(&obj.0).unwrap().to_string() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChannelId(pub [u8; 32]); + +impl UniffiCustomTypeConverter for ChannelId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(hex_vec) = hex_utils::to_vec(&val) { + if hex_vec.len() == 32 { + let mut channel_id = [0u8; 32]; + channel_id.copy_from_slice(&hex_vec[..]); + return Ok(Self(channel_id)); + } + } + Err(Error::ChannelIdInvalid.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UserChannelId(pub u128); + +impl UniffiCustomTypeConverter for UserChannelId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(UserChannelId(u128::from_str(&val).map_err(|_| Error::ChannelIdInvalid)?)) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.0.to_string() + } +} diff --git a/uniffi/ldk_node.udl b/uniffi/ldk_node.udl new file mode 100644 index 000000000..77246e4b9 --- /dev/null +++ b/uniffi/ldk_node.udl @@ -0,0 +1,79 @@ +namespace ldk_node { +}; + +interface Builder { + constructor(); + Node build(); +}; + +interface Node { + [Throws=NodeError] + void start(); + [Throws=NodeError] + void stop(); + Event next_event(); + void event_handled(); + [Throws=NodeError] + PublicKey node_id(); + [Throws=NodeError] + Address new_funding_address(); + [Throws=NodeError] + void connect_open_channel([ByRef]string node_pubkey_and_address, u64 channel_amount_sats, boolean announce_channel); + [Throws=NodeError] + PaymentHash send_payment(Invoice invoice); + [Throws=NodeError] + PaymentHash send_spontaneous_payment(u64 amount_msat, [ByRef]string node_id); + [Throws=NodeError] + Invoice receive_payment(u64? amount_msat, [ByRef]string description, u32 expiry_secs); + // TODO: payment_info() +}; + +[Error] +enum NodeError { + "AlreadyRunning", + "NotRunning", + "FundingTxCreationFailed", + "ConnectionFailed", + "AddressInvalid", + "PublicKeyInvalid", + "PaymentHashInvalid", + "NonUniquePaymentHash", + "InvoiceInvalid", + "InvoiceCreationFailed", + "ChannelIdInvalid", + "RoutingFailed", + "PeerInfoParseFailed", + "ChannelCreationFailed", + "ChannelClosingFailed", + "PersistenceFailed", + "WalletOperationFailed", + "WalletSigningFailed", + "TxSyncFailed", +}; + +[Enum] +interface Event { + PaymentSuccessful( PaymentHash payment_hash ); + PaymentFailed( PaymentHash payment_hash ); + PaymentReceived( PaymentHash payment_hash, u64 amount_msat); + ChannelReady ( ChannelId channel_id, UserChannelId user_channel_id ); + ChannelClosed ( ChannelId channel_id, UserChannelId user_channel_id ); +}; + +[Custom] +typedef string PublicKey; + +[Custom] +typedef string Address; + +[Custom] +typedef string Invoice; + +[Custom] +typedef string PaymentHash; + +[Custom] +typedef string ChannelId; + +[Custom] +typedef string UserChannelId; diff --git a/uniffi_bindgen_generate.sh b/uniffi_bindgen_generate.sh new file mode 100644 index 000000000..bf912723e --- /dev/null +++ b/uniffi_bindgen_generate.sh @@ -0,0 +1,8 @@ +#!/bin/bash +uniffi-bindgen generate uniffi/ldk_node.udl --language python +uniffi-bindgen generate uniffi/ldk_node.udl --language kotlin + +uniffi-bindgen generate uniffi/ldk_node.udl --language swift +#swiftc -module-name ldk_node -emit-library -o libldk_node.dylib -emit-module -emit-module-path ./uniffi -parse-as-library -L ./target/release/ -lldk_node -Xcc -fmodule-map-file=./uniffi/ldk_nodeFFI.modulemap ./uniffi/ldk_node.swift -v +#swiftc -module-name ldk_node -emit-library -o libldk_node.dylib -emit-module -emit-module-path ./uniffi -parse-as-library -L ./target/debug/ -lldk_node -Xcc -fmodule-map-file=./uniffi/ldk_nodeFFI.modulemap ./uniffi/ldk_node.swift -v +#swiftc -module-name ldk_node -emit-library -o libldk_node.dylib -emit-module -emit-module-path ./uniffi -parse-as-library -L ./target/x86_64-apple-darwin/release/ -lldk_node -Xcc -fmodule-map-file=./uniffi/ldk_nodeFFI.modulemap ./uniffi/ldk_node.swift -v