├── .github └── workflows │ ├── build.yml │ └── integration-tests-events-rabbitmq.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── ldk-server-cli ├── Cargo.toml └── src │ └── main.rs ├── ldk-server-client ├── Cargo.toml └── src │ ├── client.rs │ ├── error.rs │ └── lib.rs ├── ldk-server-protos ├── Cargo.toml ├── build.rs └── src │ ├── api.rs │ ├── error.rs │ ├── events.rs │ ├── lib.rs │ ├── proto │ ├── api.proto │ ├── error.proto │ ├── events.proto │ └── types.proto │ └── types.rs ├── ldk-server ├── Cargo.toml ├── ldk-server-config.toml └── src │ ├── api │ ├── bolt11_receive.rs │ ├── bolt11_send.rs │ ├── bolt12_receive.rs │ ├── bolt12_send.rs │ ├── close_channel.rs │ ├── error.rs │ ├── get_balances.rs │ ├── get_node_info.rs │ ├── get_payment_details.rs │ ├── list_channels.rs │ ├── list_forwarded_payments.rs │ ├── list_payments.rs │ ├── mod.rs │ ├── onchain_receive.rs │ ├── onchain_send.rs │ ├── open_channel.rs │ └── update_channel_config.rs │ ├── io │ ├── events │ │ ├── event_publisher.rs │ │ ├── mod.rs │ │ └── rabbitmq │ │ │ └── mod.rs │ ├── mod.rs │ ├── persist │ │ ├── mod.rs │ │ ├── paginated_kv_store.rs │ │ └── sqlite_store │ │ │ └── mod.rs │ └── utils.rs │ ├── main.rs │ ├── service.rs │ └── util │ ├── config.rs │ ├── mod.rs │ └── proto_adapter.rs └── rustfmt.toml /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration Checks 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | platform: [ 10 | ubuntu-latest, 11 | macos-latest, 12 | ] 13 | toolchain: 14 | [ stable, 15 | beta, 16 | 1.75.0, # MSRV 17 | ] 18 | include: 19 | - toolchain: stable 20 | check-fmt: true 21 | - toolchain: 1.75.0 22 | msrv: true 23 | runs-on: ${{ matrix.platform }} 24 | steps: 25 | - name: Checkout source code 26 | uses: actions/checkout@v3 27 | - name: Install Rust ${{ matrix.toolchain }} toolchain 28 | run: | 29 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ matrix.toolchain }} 30 | rustup override set ${{ matrix.toolchain }} 31 | - name: Check formatting 32 | if: matrix.check-fmt 33 | run: rustup component add rustfmt && cargo fmt --all -- --check 34 | - name: Pin packages to allow for MSRV 35 | if: matrix.msrv 36 | run: | 37 | cargo update -p home --precise "0.5.9" --verbose # home v0.5.11 requires rustc 1.81 or newer 38 | - name: Build on Rust ${{ matrix.toolchain }} 39 | run: cargo build --verbose --color always 40 | - name: Test on Rust ${{ matrix.toolchain }} 41 | run: cargo test 42 | - name: Cargo check release on Rust ${{ matrix.toolchain }} 43 | run: cargo check --release 44 | - name: Cargo check doc on Rust ${{ matrix.toolchain }} 45 | run: cargo doc --release 46 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests-events-rabbitmq.yml: -------------------------------------------------------------------------------- 1 | name: RabbitMQ Integration Tests 2 | 3 | on: [ push, pull_request ] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | integration-tests: 11 | runs-on: ubuntu-latest 12 | 13 | services: 14 | rabbitmq: 15 | image: rabbitmq:3 16 | env: 17 | RABBITMQ_DEFAULT_USER: guest 18 | RABBITMQ_DEFAULT_PASS: guest 19 | ports: 20 | - 5672:5672 21 | options: >- 22 | --health-cmd "rabbitmqctl node_health_check" 23 | --health-interval 10s 24 | --health-timeout 5s 25 | --health-retries 5 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | - name: Run RabbitMQ integration tests 32 | run: cargo test --features integration-tests-events-rabbitmq --verbose --color=always -- --nocapture 33 | env: 34 | RUST_BACKTRACE: 1 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ "ldk-server-cli", "ldk-server-client", "ldk-server-protos", "ldk-server"] 4 | 5 | [profile.release] 6 | panic = "abort" 7 | opt-level = 3 8 | lto = true 9 | 10 | [profile.dev] 11 | panic = "abort" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LDK Server 2 | 3 | **LDK Server** is a fully-functional Lightning node in daemon form, built on top of 4 | [LDK Node](https://github.com/lightningdevkit/ldk-node), which itself provides a powerful abstraction over the 5 | [Lightning Development Kit (LDK)](https://github.com/lightningdevkit/rust-lightning) and uses a built-in 6 | [Bitcoin Development Kit (BDK)](https://bitcoindevkit.org/) wallet. 7 | 8 | The primary goal of LDK Server is to provide an efficient, stable, and API-first solution for deploying and managing 9 | a Lightning Network node. With its streamlined setup, LDK Server enables users to easily set up, configure, and run 10 | a Lightning node while exposing a robust, language-agnostic API via [Protocol Buffers (Protobuf)](https://protobuf.dev/). 11 | 12 | ### Features 13 | 14 | - **Out-of-the-Box Lightning Node**: 15 | - Deploy a Lightning Network node with minimal configuration, no coding required. 16 | 17 | - **API-First Design**: 18 | - Exposes a well-defined API using Protobuf, allowing seamless integration with HTTP-clients or applications. 19 | 20 | - **Powered by LDK**: 21 | - Built on top of LDK-Node, leveraging the modular, reliable, and high-performance architecture of LDK. 22 | 23 | - **Effortless Integration**: 24 | - Ideal for embedding Lightning functionality into payment processors, self-hosted nodes, custodial wallets, or other Lightning-enabled 25 | applications. 26 | 27 | ### Project Status 28 | 29 | 🚧 **Work in Progress**: 30 | - **APIs Under Development**: Expect breaking changes as the project evolves. 31 | - **Potential Bugs and Inconsistencies**: While progress is being made toward stability, unexpected behavior may occur. 32 | - **Improved Logging and Error Handling Coming Soon**: Current error handling is rudimentary (specially for CLI), and usability improvements are actively being worked on. 33 | - **Pending Testing**: Not tested, hence don't use it for production! 34 | 35 | We welcome your feedback and contributions to help shape the future of LDK Server! 36 | 37 | 38 | ### Configuration 39 | Refer `./ldk-server/ldk-server-config.toml` to see available configuration options. 40 | 41 | ### Building 42 | ``` 43 | git clone https://github.com/lightningdevkit/ldk-server.git 44 | cargo build 45 | ``` 46 | 47 | ### Running 48 | ``` 49 | cargo run --bin ldk-server ./ldk-server/ldk-server.config 50 | ``` 51 | 52 | Interact with the node using CLI: 53 | ``` 54 | ./target/debug/ldk-server-cli -b localhost:3002 onchain-receive # To generate onchain-receive address. 55 | ./target/debug/ldk-server-cli -b localhost:3002 help # To print help/available commands. 56 | ``` 57 | -------------------------------------------------------------------------------- /ldk-server-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ldk-server-cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ldk-server-client = { path = "../ldk-server-client" } 8 | clap = { version = "4.0.5", default-features = false, features = ["derive", "std", "error-context", "suggestions", "help"] } 9 | tokio = { version = "1.38.0", default-features = false, features = ["rt-multi-thread", "macros"] } 10 | prost = { version = "0.11.6", default-features = false} 11 | -------------------------------------------------------------------------------- /ldk-server-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use ldk_server_client::client::LdkServerClient; 3 | use ldk_server_client::error::LdkServerError; 4 | use ldk_server_client::error::LdkServerErrorCode::{ 5 | AuthError, InternalError, InternalServerError, InvalidRequestError, LightningError, 6 | }; 7 | use ldk_server_client::ldk_server_protos::api::{ 8 | Bolt11ReceiveRequest, Bolt11SendRequest, Bolt12ReceiveRequest, Bolt12SendRequest, 9 | GetBalancesRequest, GetNodeInfoRequest, ListChannelsRequest, ListPaymentsRequest, 10 | OnchainReceiveRequest, OnchainSendRequest, OpenChannelRequest, 11 | }; 12 | use ldk_server_client::ldk_server_protos::types::{ 13 | bolt11_invoice_description, Bolt11InvoiceDescription, PageToken, Payment, 14 | }; 15 | use std::fmt::Debug; 16 | 17 | #[derive(Parser, Debug)] 18 | #[command(version, about, long_about = None)] 19 | struct Cli { 20 | #[arg(short, long, default_value = "localhost:3000")] 21 | base_url: String, 22 | 23 | #[command(subcommand)] 24 | command: Commands, 25 | } 26 | 27 | #[derive(Subcommand, Debug)] 28 | enum Commands { 29 | GetNodeInfo, 30 | GetBalances, 31 | OnchainReceive, 32 | OnchainSend { 33 | #[arg(short, long)] 34 | address: String, 35 | #[arg(long)] 36 | amount_sats: Option, 37 | #[arg(long)] 38 | send_all: Option, 39 | #[arg(long)] 40 | fee_rate_sat_per_vb: Option, 41 | }, 42 | Bolt11Receive { 43 | #[arg(short, long)] 44 | description: Option, 45 | #[arg(long)] 46 | description_hash: Option, 47 | #[arg(short, long)] 48 | expiry_secs: u32, 49 | #[arg(long)] 50 | amount_msat: Option, 51 | }, 52 | Bolt11Send { 53 | #[arg(short, long)] 54 | invoice: String, 55 | #[arg(long)] 56 | amount_msat: Option, 57 | }, 58 | Bolt12Receive { 59 | #[arg(short, long)] 60 | description: String, 61 | #[arg(long)] 62 | amount_msat: Option, 63 | #[arg(long)] 64 | expiry_secs: Option, 65 | #[arg(long)] 66 | quantity: Option, 67 | }, 68 | Bolt12Send { 69 | #[arg(short, long)] 70 | offer: String, 71 | #[arg(long)] 72 | amount_msat: Option, 73 | #[arg(short, long)] 74 | quantity: Option, 75 | #[arg(short, long)] 76 | payer_note: Option, 77 | }, 78 | OpenChannel { 79 | #[arg(short, long)] 80 | node_pubkey: String, 81 | #[arg(short, long)] 82 | address: String, 83 | #[arg(long)] 84 | channel_amount_sats: u64, 85 | #[arg(long)] 86 | push_to_counterparty_msat: Option, 87 | #[arg(long)] 88 | announce_channel: bool, 89 | }, 90 | ListChannels, 91 | ListPayments { 92 | #[arg(short, long)] 93 | #[arg( 94 | help = "Minimum number of payments to return. If not provided, only the first page of the paginated list is returned." 95 | )] 96 | number_of_payments: Option, 97 | }, 98 | } 99 | 100 | #[tokio::main] 101 | async fn main() { 102 | let cli = Cli::parse(); 103 | let client = LdkServerClient::new(cli.base_url); 104 | 105 | match cli.command { 106 | Commands::GetNodeInfo => { 107 | handle_response_result(client.get_node_info(GetNodeInfoRequest {}).await); 108 | }, 109 | Commands::GetBalances => { 110 | handle_response_result(client.get_balances(GetBalancesRequest {}).await); 111 | }, 112 | Commands::OnchainReceive => { 113 | handle_response_result(client.onchain_receive(OnchainReceiveRequest {}).await); 114 | }, 115 | Commands::OnchainSend { address, amount_sats, send_all, fee_rate_sat_per_vb } => { 116 | handle_response_result( 117 | client 118 | .onchain_send(OnchainSendRequest { 119 | address, 120 | amount_sats, 121 | send_all, 122 | fee_rate_sat_per_vb, 123 | }) 124 | .await, 125 | ); 126 | }, 127 | Commands::Bolt11Receive { description, description_hash, expiry_secs, amount_msat } => { 128 | let invoice_description = match (description, description_hash) { 129 | (Some(desc), None) => Some(Bolt11InvoiceDescription { 130 | kind: Some(bolt11_invoice_description::Kind::Direct(desc)), 131 | }), 132 | (None, Some(hash)) => Some(Bolt11InvoiceDescription { 133 | kind: Some(bolt11_invoice_description::Kind::Hash(hash)), 134 | }), 135 | (Some(_), Some(_)) => { 136 | handle_error(LdkServerError::new( 137 | InternalError, 138 | "Only one of description or description_hash can be set.".to_string(), 139 | )); 140 | }, 141 | (None, None) => None, 142 | }; 143 | 144 | let request = 145 | Bolt11ReceiveRequest { description: invoice_description, expiry_secs, amount_msat }; 146 | 147 | handle_response_result(client.bolt11_receive(request).await); 148 | }, 149 | Commands::Bolt11Send { invoice, amount_msat } => { 150 | handle_response_result( 151 | client.bolt11_send(Bolt11SendRequest { invoice, amount_msat }).await, 152 | ); 153 | }, 154 | Commands::Bolt12Receive { description, amount_msat, expiry_secs, quantity } => { 155 | handle_response_result( 156 | client 157 | .bolt12_receive(Bolt12ReceiveRequest { 158 | description, 159 | amount_msat, 160 | expiry_secs, 161 | quantity, 162 | }) 163 | .await, 164 | ); 165 | }, 166 | Commands::Bolt12Send { offer, amount_msat, quantity, payer_note } => { 167 | handle_response_result( 168 | client 169 | .bolt12_send(Bolt12SendRequest { offer, amount_msat, quantity, payer_note }) 170 | .await, 171 | ); 172 | }, 173 | Commands::OpenChannel { 174 | node_pubkey, 175 | address, 176 | channel_amount_sats, 177 | push_to_counterparty_msat, 178 | announce_channel, 179 | } => { 180 | handle_response_result( 181 | client 182 | .open_channel(OpenChannelRequest { 183 | node_pubkey, 184 | address, 185 | channel_amount_sats, 186 | push_to_counterparty_msat, 187 | channel_config: None, 188 | announce_channel, 189 | }) 190 | .await, 191 | ); 192 | }, 193 | Commands::ListChannels => { 194 | handle_response_result(client.list_channels(ListChannelsRequest {}).await); 195 | }, 196 | Commands::ListPayments { number_of_payments } => { 197 | handle_response_result(list_n_payments(client, number_of_payments).await); 198 | }, 199 | } 200 | } 201 | 202 | async fn list_n_payments( 203 | client: LdkServerClient, number_of_payments: Option, 204 | ) -> Result, LdkServerError> { 205 | let mut payments = Vec::new(); 206 | let mut page_token: Option = None; 207 | // If no count is specified, just list the first page. 208 | let target_count = number_of_payments.unwrap_or(0); 209 | 210 | loop { 211 | let response = client.list_payments(ListPaymentsRequest { page_token }).await?; 212 | 213 | payments.extend(response.payments); 214 | if payments.len() >= target_count as usize || response.next_page_token.is_none() { 215 | break; 216 | } 217 | page_token = response.next_page_token; 218 | } 219 | Ok(payments) 220 | } 221 | 222 | fn handle_response_result(response: Result) { 223 | match response { 224 | Ok(response) => { 225 | println!("Success: {:?}", response); 226 | }, 227 | Err(e) => { 228 | handle_error(e); 229 | }, 230 | } 231 | } 232 | 233 | fn handle_error(e: LdkServerError) -> ! { 234 | let error_type = match e.error_code { 235 | InvalidRequestError => "Invalid Request", 236 | AuthError => "Authentication Error", 237 | LightningError => "Lightning Error", 238 | InternalServerError => "Internal Server Error", 239 | InternalError => "Internal Error", 240 | }; 241 | eprintln!("Error ({}): {}", error_type, e.message); 242 | std::process::exit(1); // Exit with status code 1 on error. 243 | } 244 | -------------------------------------------------------------------------------- /ldk-server-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ldk-server-client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ldk-server-protos = { path = "../ldk-server-protos" } 8 | reqwest = { version = "0.11.13", default-features = false, features = ["rustls-tls"] } 9 | prost = { version = "0.11.6", default-features = false, features = ["std", "prost-derive"] } 10 | -------------------------------------------------------------------------------- /ldk-server-client/src/client.rs: -------------------------------------------------------------------------------- 1 | use prost::Message; 2 | 3 | use crate::error::LdkServerError; 4 | use crate::error::LdkServerErrorCode::{ 5 | AuthError, InternalError, InternalServerError, InvalidRequestError, LightningError, 6 | }; 7 | use ldk_server_protos::api::{ 8 | Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse, 9 | Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse, 10 | CloseChannelRequest, CloseChannelResponse, GetBalancesRequest, GetBalancesResponse, 11 | GetNodeInfoRequest, GetNodeInfoResponse, ListChannelsRequest, ListChannelsResponse, 12 | ListPaymentsRequest, ListPaymentsResponse, OnchainReceiveRequest, OnchainReceiveResponse, 13 | OnchainSendRequest, OnchainSendResponse, OpenChannelRequest, OpenChannelResponse, 14 | }; 15 | use ldk_server_protos::error::{ErrorCode, ErrorResponse}; 16 | use reqwest::header::CONTENT_TYPE; 17 | use reqwest::Client; 18 | 19 | const APPLICATION_OCTET_STREAM: &str = "application/octet-stream"; 20 | 21 | const GET_NODE_INFO_PATH: &str = "GetNodeInfo"; 22 | const GET_BALANCES_PATH: &str = "GetBalances"; 23 | const ONCHAIN_RECEIVE_PATH: &str = "OnchainReceive"; 24 | const ONCHAIN_SEND_PATH: &str = "OnchainSend"; 25 | const BOLT11_RECEIVE_PATH: &str = "Bolt11Receive"; 26 | const BOLT11_SEND_PATH: &str = "Bolt11Send"; 27 | const BOLT12_RECEIVE_PATH: &str = "Bolt12Receive"; 28 | const BOLT12_SEND_PATH: &str = "Bolt12Send"; 29 | const OPEN_CHANNEL_PATH: &str = "OpenChannel"; 30 | const CLOSE_CHANNEL_PATH: &str = "CloseChannel"; 31 | const LIST_CHANNELS_PATH: &str = "ListChannels"; 32 | const LIST_PAYMENTS_PATH: &str = "ListPayments"; 33 | 34 | /// Client to access a hosted instance of LDK Server. 35 | #[derive(Clone)] 36 | pub struct LdkServerClient { 37 | base_url: String, 38 | client: Client, 39 | } 40 | 41 | impl LdkServerClient { 42 | /// Constructs a [`LdkServerClient`] using `base_url` as the ldk-server endpoint. 43 | pub fn new(base_url: String) -> Self { 44 | Self { base_url, client: Client::new() } 45 | } 46 | 47 | /// Retrieve the latest node info like `node_id`, `current_best_block` etc. 48 | /// For API contract/usage, refer to docs for [`GetNodeInfoRequest`] and [`GetNodeInfoResponse`]. 49 | pub async fn get_node_info( 50 | &self, request: GetNodeInfoRequest, 51 | ) -> Result { 52 | let url = format!("http://{}/{GET_NODE_INFO_PATH}", self.base_url); 53 | self.post_request(&request, &url).await 54 | } 55 | 56 | /// Retrieves an overview of all known balances. 57 | /// For API contract/usage, refer to docs for [`GetBalancesRequest`] and [`GetBalancesResponse`]. 58 | pub async fn get_balances( 59 | &self, request: GetBalancesRequest, 60 | ) -> Result { 61 | let url = format!("http://{}/{GET_BALANCES_PATH}", self.base_url); 62 | self.post_request(&request, &url).await 63 | } 64 | 65 | /// Retrieve a new on-chain funding address. 66 | /// For API contract/usage, refer to docs for [`OnchainReceiveRequest`] and [`OnchainReceiveResponse`]. 67 | pub async fn onchain_receive( 68 | &self, request: OnchainReceiveRequest, 69 | ) -> Result { 70 | let url = format!("http://{}/{ONCHAIN_RECEIVE_PATH}", self.base_url); 71 | self.post_request(&request, &url).await 72 | } 73 | 74 | /// Send an on-chain payment to the given address. 75 | /// For API contract/usage, refer to docs for [`OnchainSendRequest`] and [`OnchainSendResponse`]. 76 | pub async fn onchain_send( 77 | &self, request: OnchainSendRequest, 78 | ) -> Result { 79 | let url = format!("http://{}/{ONCHAIN_SEND_PATH}", self.base_url); 80 | self.post_request(&request, &url).await 81 | } 82 | 83 | /// Retrieve a new BOLT11 payable invoice. 84 | /// For API contract/usage, refer to docs for [`Bolt11ReceiveRequest`] and [`Bolt11ReceiveResponse`]. 85 | pub async fn bolt11_receive( 86 | &self, request: Bolt11ReceiveRequest, 87 | ) -> Result { 88 | let url = format!("http://{}/{BOLT11_RECEIVE_PATH}", self.base_url); 89 | self.post_request(&request, &url).await 90 | } 91 | 92 | /// Send a payment for a BOLT11 invoice. 93 | /// For API contract/usage, refer to docs for [`Bolt11SendRequest`] and [`Bolt11SendResponse`]. 94 | pub async fn bolt11_send( 95 | &self, request: Bolt11SendRequest, 96 | ) -> Result { 97 | let url = format!("http://{}/{BOLT11_SEND_PATH}", self.base_url); 98 | self.post_request(&request, &url).await 99 | } 100 | 101 | /// Retrieve a new BOLT11 payable offer. 102 | /// For API contract/usage, refer to docs for [`Bolt12ReceiveRequest`] and [`Bolt12ReceiveResponse`]. 103 | pub async fn bolt12_receive( 104 | &self, request: Bolt12ReceiveRequest, 105 | ) -> Result { 106 | let url = format!("http://{}/{BOLT12_RECEIVE_PATH}", self.base_url); 107 | self.post_request(&request, &url).await 108 | } 109 | 110 | /// Send a payment for a BOLT12 offer. 111 | /// For API contract/usage, refer to docs for [`Bolt12SendRequest`] and [`Bolt12SendResponse`]. 112 | pub async fn bolt12_send( 113 | &self, request: Bolt12SendRequest, 114 | ) -> Result { 115 | let url = format!("http://{}/{BOLT12_SEND_PATH}", self.base_url); 116 | self.post_request(&request, &url).await 117 | } 118 | 119 | /// Creates a new outbound channel. 120 | /// For API contract/usage, refer to docs for [`OpenChannelRequest`] and [`OpenChannelResponse`]. 121 | pub async fn open_channel( 122 | &self, request: OpenChannelRequest, 123 | ) -> Result { 124 | let url = format!("http://{}/{OPEN_CHANNEL_PATH}", self.base_url); 125 | self.post_request(&request, &url).await 126 | } 127 | 128 | /// Closes the channel specified by given request. 129 | /// For API contract/usage, refer to docs for [`CloseChannelRequest`] and [`CloseChannelResponse`]. 130 | pub async fn close_channel( 131 | &self, request: CloseChannelRequest, 132 | ) -> Result { 133 | let url = format!("http://{}/{CLOSE_CHANNEL_PATH}", self.base_url); 134 | self.post_request(&request, &url).await 135 | } 136 | 137 | /// Retrieves list of known channels. 138 | /// For API contract/usage, refer to docs for [`ListChannelsRequest`] and [`ListChannelsResponse`]. 139 | pub async fn list_channels( 140 | &self, request: ListChannelsRequest, 141 | ) -> Result { 142 | let url = format!("http://{}/{LIST_CHANNELS_PATH}", self.base_url); 143 | self.post_request(&request, &url).await 144 | } 145 | 146 | /// Retrieves list of all payments sent or received by us. 147 | /// For API contract/usage, refer to docs for [`ListPaymentsRequest`] and [`ListPaymentsResponse`]. 148 | pub async fn list_payments( 149 | &self, request: ListPaymentsRequest, 150 | ) -> Result { 151 | let url = format!("http://{}/{LIST_PAYMENTS_PATH}", self.base_url); 152 | self.post_request(&request, &url).await 153 | } 154 | 155 | async fn post_request( 156 | &self, request: &Rq, url: &str, 157 | ) -> Result { 158 | let request_body = request.encode_to_vec(); 159 | let response_raw = self 160 | .client 161 | .post(url) 162 | .header(CONTENT_TYPE, APPLICATION_OCTET_STREAM) 163 | .body(request_body) 164 | .send() 165 | .await 166 | .map_err(|e| { 167 | LdkServerError::new(InternalError, format!("HTTP request failed: {}", e)) 168 | })?; 169 | 170 | let status = response_raw.status(); 171 | let payload = response_raw.bytes().await.map_err(|e| { 172 | LdkServerError::new(InternalError, format!("Failed to read response body: {}", e)) 173 | })?; 174 | 175 | if status.is_success() { 176 | Ok(Rs::decode(&payload[..]).map_err(|e| { 177 | LdkServerError::new( 178 | InternalError, 179 | format!("Failed to decode success response: {}", e), 180 | ) 181 | })?) 182 | } else { 183 | let error_response = ErrorResponse::decode(&payload[..]).map_err(|e| { 184 | LdkServerError::new( 185 | InternalError, 186 | format!("Failed to decode error response (status {}): {}", status, e), 187 | ) 188 | })?; 189 | 190 | let error_code = match ErrorCode::from_i32(error_response.error_code) { 191 | Some(ErrorCode::InvalidRequestError) => InvalidRequestError, 192 | Some(ErrorCode::AuthError) => AuthError, 193 | Some(ErrorCode::LightningError) => LightningError, 194 | Some(ErrorCode::InternalServerError) => InternalServerError, 195 | Some(ErrorCode::UnknownError) | None => InternalError, 196 | }; 197 | 198 | Err(LdkServerError::new(error_code, error_response.message)) 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /ldk-server-client/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// Represents an error returned by the LDK server. 4 | #[derive(Clone, Debug, PartialEq, Eq)] 5 | pub struct LdkServerError { 6 | /// The error message containing a generic description of the error condition in English. 7 | /// It is intended for a human audience only and should not be parsed to extract any information 8 | /// programmatically. Client-side code may use it for logging only. 9 | pub message: String, 10 | 11 | /// The error code uniquely identifying an error condition. 12 | /// It is meant to be read and understood programmatically by code that detects/handles errors by 13 | /// type. 14 | pub error_code: LdkServerErrorCode, 15 | } 16 | 17 | impl LdkServerError { 18 | /// Creates a new [`LdkServerError`] with the given error code and message. 19 | pub fn new(error_code: LdkServerErrorCode, message: impl Into) -> Self { 20 | Self { error_code, message: message.into() } 21 | } 22 | } 23 | 24 | impl std::error::Error for LdkServerError {} 25 | 26 | impl fmt::Display for LdkServerError { 27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 28 | write!(f, "Error: [{}]: {}", self.error_code, self.message) 29 | } 30 | } 31 | 32 | /// Defines error codes for categorizing LDK server errors. 33 | #[derive(Clone, Debug, PartialEq, Eq)] 34 | pub enum LdkServerErrorCode { 35 | /// Please refer to [`ldk_server_protos::error::ErrorCode::InvalidRequestError`]. 36 | InvalidRequestError, 37 | 38 | /// Please refer to [`ldk_server_protos::error::ErrorCode::AuthError`]. 39 | AuthError, 40 | 41 | /// Please refer to [`ldk_server_protos::error::ErrorCode::LightningError`]. 42 | LightningError, 43 | 44 | /// Please refer to [`ldk_server_protos::error::ErrorCode::InternalServerError`]. 45 | InternalServerError, 46 | 47 | /// There is an unknown error, it could be a client-side bug, unrecognized error-code, network error 48 | /// or something else. 49 | InternalError, 50 | } 51 | 52 | impl fmt::Display for LdkServerErrorCode { 53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 54 | match self { 55 | LdkServerErrorCode::InvalidRequestError => write!(f, "InvalidRequestError"), 56 | LdkServerErrorCode::AuthError => write!(f, "AuthError"), 57 | LdkServerErrorCode::LightningError => write!(f, "LightningError"), 58 | LdkServerErrorCode::InternalServerError => write!(f, "InternalServerError"), 59 | LdkServerErrorCode::InternalError => write!(f, "InternalError"), 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ldk-server-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Client-side library to interact with LDK Server. 2 | 3 | #![deny(rustdoc::broken_intra_doc_links)] 4 | #![deny(rustdoc::private_intra_doc_links)] 5 | #![deny(missing_docs)] 6 | 7 | /// Implements a ldk-ldk-server-client ([`client::LdkServerClient`]) to access a hosted instance of LDK Server. 8 | pub mod client; 9 | 10 | /// Implements the error type ([`error::LdkServerError`]) returned on interacting with [`client::LdkServerClient`] 11 | pub mod error; 12 | 13 | /// Request/Response structs required for interacting with the ldk-ldk-server-client. 14 | pub use ldk_server_protos; 15 | -------------------------------------------------------------------------------- /ldk-server-protos/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ldk-server-protos" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | build = "build.rs" 7 | 8 | [dependencies] 9 | prost = { version = "0.11.6", default-features = false, features = ["std", "prost-derive"] } 10 | 11 | [target.'cfg(genproto)'.build-dependencies] 12 | prost-build = { version = "0.11.6" , default-features = false} 13 | -------------------------------------------------------------------------------- /ldk-server-protos/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(genproto)] 2 | extern crate prost_build; 3 | 4 | #[cfg(genproto)] 5 | use std::{env, fs, path::Path}; 6 | 7 | /// To generate updated proto objects, run `RUSTFLAGS="--cfg genproto" cargo build` 8 | fn main() { 9 | #[cfg(genproto)] 10 | generate_protos(); 11 | } 12 | 13 | #[cfg(genproto)] 14 | fn generate_protos() { 15 | prost_build::Config::new() 16 | .bytes(&["."]) 17 | .compile_protos( 18 | &[ 19 | "src/proto/api.proto", 20 | "src/proto/types.proto", 21 | "src/proto/events.proto", 22 | "src/proto/error.proto", 23 | ], 24 | &["src/proto/"], 25 | ) 26 | .expect("protobuf compilation failed"); 27 | println!("OUT_DIR: {}", &env::var("OUT_DIR").unwrap()); 28 | let from_path = Path::new(&env::var("OUT_DIR").unwrap()).join("api.rs"); 29 | fs::copy(from_path, "src/api.rs").unwrap(); 30 | let from_path = Path::new(&env::var("OUT_DIR").unwrap()).join("types.rs"); 31 | fs::copy(from_path, "src/types.rs").unwrap(); 32 | let from_path = Path::new(&env::var("OUT_DIR").unwrap()).join("events.rs"); 33 | fs::copy(from_path, "src/events.rs").unwrap(); 34 | let from_path = Path::new(&env::var("OUT_DIR").unwrap()).join("error.rs"); 35 | fs::copy(from_path, "src/error.rs").unwrap(); 36 | } 37 | -------------------------------------------------------------------------------- /ldk-server-protos/src/api.rs: -------------------------------------------------------------------------------- 1 | /// Retrieve the latest node info like `node_id`, `current_best_block` etc. 2 | /// See more: 3 | /// - 4 | /// - 5 | #[allow(clippy::derive_partial_eq_without_eq)] 6 | #[derive(Clone, PartialEq, ::prost::Message)] 7 | pub struct GetNodeInfoRequest {} 8 | /// The response `content` for the `GetNodeInfo` API, when HttpStatusCode is OK (200). 9 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 10 | #[allow(clippy::derive_partial_eq_without_eq)] 11 | #[derive(Clone, PartialEq, ::prost::Message)] 12 | pub struct GetNodeInfoResponse { 13 | /// The hex-encoded `node-id` or public key for our own lightning node. 14 | #[prost(string, tag = "1")] 15 | pub node_id: ::prost::alloc::string::String, 16 | /// The best block to which our Lightning wallet is currently synced. 17 | /// 18 | /// Should be always set, will never be `None`. 19 | #[prost(message, optional, tag = "3")] 20 | pub current_best_block: ::core::option::Option, 21 | /// The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced our Lightning wallet to 22 | /// the chain tip. 23 | /// 24 | /// Will be `None` if the wallet hasn't been synced yet. 25 | #[prost(uint64, optional, tag = "4")] 26 | pub latest_lightning_wallet_sync_timestamp: ::core::option::Option, 27 | /// The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced our on-chain 28 | /// wallet to the chain tip. 29 | /// 30 | /// Will be `None` if the wallet hasn’t been synced since the node was initialized. 31 | #[prost(uint64, optional, tag = "5")] 32 | pub latest_onchain_wallet_sync_timestamp: ::core::option::Option, 33 | /// The timestamp, in seconds since start of the UNIX epoch, when we last successfully update our fee rate cache. 34 | /// 35 | /// Will be `None` if the cache hasn’t been updated since the node was initialized. 36 | #[prost(uint64, optional, tag = "6")] 37 | pub latest_fee_rate_cache_update_timestamp: ::core::option::Option, 38 | /// The timestamp, in seconds since start of the UNIX epoch, when the last rapid gossip sync (RGS) snapshot we 39 | /// successfully applied was generated. 40 | /// 41 | /// Will be `None` if RGS isn’t configured or the snapshot hasn’t been updated since the node was initialized. 42 | #[prost(uint64, optional, tag = "7")] 43 | pub latest_rgs_snapshot_timestamp: ::core::option::Option, 44 | /// The timestamp, in seconds since start of the UNIX epoch, when we last broadcasted a node announcement. 45 | /// 46 | /// Will be `None` if we have no public channels or we haven’t broadcasted since the node was initialized. 47 | #[prost(uint64, optional, tag = "8")] 48 | pub latest_node_announcement_broadcast_timestamp: ::core::option::Option, 49 | } 50 | /// Retrieve a new on-chain funding address. 51 | /// See more: 52 | #[allow(clippy::derive_partial_eq_without_eq)] 53 | #[derive(Clone, PartialEq, ::prost::Message)] 54 | pub struct OnchainReceiveRequest {} 55 | /// The response `content` for the `OnchainReceive` API, when HttpStatusCode is OK (200). 56 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.. 57 | #[allow(clippy::derive_partial_eq_without_eq)] 58 | #[derive(Clone, PartialEq, ::prost::Message)] 59 | pub struct OnchainReceiveResponse { 60 | /// A Bitcoin on-chain address. 61 | #[prost(string, tag = "1")] 62 | pub address: ::prost::alloc::string::String, 63 | } 64 | /// Send an on-chain payment to the given address. 65 | #[allow(clippy::derive_partial_eq_without_eq)] 66 | #[derive(Clone, PartialEq, ::prost::Message)] 67 | pub struct OnchainSendRequest { 68 | /// The address to send coins to. 69 | #[prost(string, tag = "1")] 70 | pub address: ::prost::alloc::string::String, 71 | /// The amount in satoshis to send. 72 | /// While sending the specified amount, we will respect any on-chain reserve we need to keep, 73 | /// i.e., won't allow to cut into `total_anchor_channels_reserve_sats`. 74 | /// See more: 75 | #[prost(uint64, optional, tag = "2")] 76 | pub amount_sats: ::core::option::Option, 77 | /// If set, the amount_sats field should be unset. 78 | /// It indicates that node will send full balance to the specified address. 79 | /// 80 | /// Please note that when send_all is used this operation will **not** retain any on-chain reserves, 81 | /// which might be potentially dangerous if you have open Anchor channels for which you can't trust 82 | /// the counterparty to spend the Anchor output after channel closure. 83 | /// See more: 84 | #[prost(bool, optional, tag = "3")] 85 | pub send_all: ::core::option::Option, 86 | /// If `fee_rate_sat_per_vb` is set it will be used on the resulting transaction. Otherwise we'll retrieve 87 | /// a reasonable estimate from BitcoinD. 88 | #[prost(uint64, optional, tag = "4")] 89 | pub fee_rate_sat_per_vb: ::core::option::Option, 90 | } 91 | /// The response `content` for the `OnchainSend` API, when HttpStatusCode is OK (200). 92 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 93 | #[allow(clippy::derive_partial_eq_without_eq)] 94 | #[derive(Clone, PartialEq, ::prost::Message)] 95 | pub struct OnchainSendResponse { 96 | /// The transaction ID of the broadcasted transaction. 97 | #[prost(string, tag = "1")] 98 | pub txid: ::prost::alloc::string::String, 99 | } 100 | /// Return a BOLT11 payable invoice that can be used to request and receive a payment 101 | /// for the given amount, if specified. 102 | /// The inbound payment will be automatically claimed upon arrival. 103 | /// See more: 104 | /// - 105 | /// - 106 | #[allow(clippy::derive_partial_eq_without_eq)] 107 | #[derive(Clone, PartialEq, ::prost::Message)] 108 | pub struct Bolt11ReceiveRequest { 109 | /// The amount in millisatoshi to send. If unset, a "zero-amount" or variable-amount invoice is returned. 110 | #[prost(uint64, optional, tag = "1")] 111 | pub amount_msat: ::core::option::Option, 112 | /// An optional description to attach along with the invoice. 113 | /// Will be set in the description field of the encoded payment request. 114 | #[prost(message, optional, tag = "2")] 115 | pub description: ::core::option::Option, 116 | /// Invoice expiry time in seconds. 117 | #[prost(uint32, tag = "3")] 118 | pub expiry_secs: u32, 119 | } 120 | /// The response `content` for the `Bolt11Receive` API, when HttpStatusCode is OK (200). 121 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 122 | #[allow(clippy::derive_partial_eq_without_eq)] 123 | #[derive(Clone, PartialEq, ::prost::Message)] 124 | pub struct Bolt11ReceiveResponse { 125 | /// An invoice for a payment within the Lightning Network. 126 | /// With the details of the invoice, the sender has all the data necessary to send a payment 127 | /// to the recipient. 128 | #[prost(string, tag = "1")] 129 | pub invoice: ::prost::alloc::string::String, 130 | } 131 | /// Send a payment for a BOLT11 invoice. 132 | /// See more: 133 | #[allow(clippy::derive_partial_eq_without_eq)] 134 | #[derive(Clone, PartialEq, ::prost::Message)] 135 | pub struct Bolt11SendRequest { 136 | /// An invoice for a payment within the Lightning Network. 137 | #[prost(string, tag = "1")] 138 | pub invoice: ::prost::alloc::string::String, 139 | /// Set this field when paying a so-called "zero-amount" invoice, i.e., an invoice that leaves the 140 | /// amount paid to be determined by the user. 141 | /// This operation will fail if the amount specified is less than the value required by the given invoice. 142 | #[prost(uint64, optional, tag = "2")] 143 | pub amount_msat: ::core::option::Option, 144 | } 145 | /// The response `content` for the `Bolt11Send` API, when HttpStatusCode is OK (200). 146 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 147 | #[allow(clippy::derive_partial_eq_without_eq)] 148 | #[derive(Clone, PartialEq, ::prost::Message)] 149 | pub struct Bolt11SendResponse { 150 | /// An identifier used to uniquely identify a payment in hex-encoded form. 151 | #[prost(string, tag = "1")] 152 | pub payment_id: ::prost::alloc::string::String, 153 | } 154 | /// Returns a BOLT12 offer for the given amount, if specified. 155 | /// 156 | /// See more: 157 | /// - 158 | /// - 159 | #[allow(clippy::derive_partial_eq_without_eq)] 160 | #[derive(Clone, PartialEq, ::prost::Message)] 161 | pub struct Bolt12ReceiveRequest { 162 | /// An optional description to attach along with the offer. 163 | /// Will be set in the description field of the encoded offer. 164 | #[prost(string, tag = "1")] 165 | pub description: ::prost::alloc::string::String, 166 | /// The amount in millisatoshi to send. If unset, a "zero-amount" or variable-amount offer is returned. 167 | #[prost(uint64, optional, tag = "2")] 168 | pub amount_msat: ::core::option::Option, 169 | /// Offer expiry time in seconds. 170 | #[prost(uint32, optional, tag = "3")] 171 | pub expiry_secs: ::core::option::Option, 172 | /// If set, it represents the number of items requested, can only be set for fixed-amount offers. 173 | #[prost(uint64, optional, tag = "4")] 174 | pub quantity: ::core::option::Option, 175 | } 176 | /// The response `content` for the `Bolt12Receive` API, when HttpStatusCode is OK (200). 177 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 178 | #[allow(clippy::derive_partial_eq_without_eq)] 179 | #[derive(Clone, PartialEq, ::prost::Message)] 180 | pub struct Bolt12ReceiveResponse { 181 | /// An offer for a payment within the Lightning Network. 182 | /// With the details of the offer, the sender has all the data necessary to send a payment 183 | /// to the recipient. 184 | #[prost(string, tag = "1")] 185 | pub offer: ::prost::alloc::string::String, 186 | } 187 | /// Send a payment for a BOLT12 offer. 188 | /// See more: 189 | /// - 190 | /// - 191 | #[allow(clippy::derive_partial_eq_without_eq)] 192 | #[derive(Clone, PartialEq, ::prost::Message)] 193 | pub struct Bolt12SendRequest { 194 | /// An offer for a payment within the Lightning Network. 195 | #[prost(string, tag = "1")] 196 | pub offer: ::prost::alloc::string::String, 197 | /// Set this field when paying a so-called "zero-amount" offer, i.e., an offer that leaves the 198 | /// amount paid to be determined by the user. 199 | /// This operation will fail if the amount specified is less than the value required by the given offer. 200 | #[prost(uint64, optional, tag = "2")] 201 | pub amount_msat: ::core::option::Option, 202 | /// If set, it represents the number of items requested. 203 | #[prost(uint64, optional, tag = "3")] 204 | pub quantity: ::core::option::Option, 205 | /// If set, it will be seen by the recipient and reflected back in the invoice. 206 | #[prost(string, optional, tag = "4")] 207 | pub payer_note: ::core::option::Option<::prost::alloc::string::String>, 208 | } 209 | /// The response `content` for the `Bolt12Send` API, when HttpStatusCode is OK (200). 210 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 211 | #[allow(clippy::derive_partial_eq_without_eq)] 212 | #[derive(Clone, PartialEq, ::prost::Message)] 213 | pub struct Bolt12SendResponse { 214 | /// An identifier used to uniquely identify a payment in hex-encoded form. 215 | #[prost(string, tag = "1")] 216 | pub payment_id: ::prost::alloc::string::String, 217 | } 218 | /// Creates a new outbound channel to the given remote node. 219 | /// See more: 220 | #[allow(clippy::derive_partial_eq_without_eq)] 221 | #[derive(Clone, PartialEq, ::prost::Message)] 222 | pub struct OpenChannelRequest { 223 | /// The hex-encoded public key of the node to open a channel with. 224 | #[prost(string, tag = "1")] 225 | pub node_pubkey: ::prost::alloc::string::String, 226 | /// An address which can be used to connect to a remote peer. 227 | /// It can be of type IPv4:port, IPv6:port, OnionV3:port or hostname:port 228 | #[prost(string, tag = "2")] 229 | pub address: ::prost::alloc::string::String, 230 | /// The amount of satoshis the caller is willing to commit to the channel. 231 | #[prost(uint64, tag = "3")] 232 | pub channel_amount_sats: u64, 233 | /// The amount of satoshis to push to the remote side as part of the initial commitment state. 234 | #[prost(uint64, optional, tag = "4")] 235 | pub push_to_counterparty_msat: ::core::option::Option, 236 | /// The channel configuration to be used for opening this channel. If unset, default ChannelConfig is used. 237 | #[prost(message, optional, tag = "5")] 238 | pub channel_config: ::core::option::Option, 239 | /// Whether the channel should be public. 240 | #[prost(bool, tag = "6")] 241 | pub announce_channel: bool, 242 | } 243 | /// The response `content` for the `OpenChannel` API, when HttpStatusCode is OK (200). 244 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 245 | #[allow(clippy::derive_partial_eq_without_eq)] 246 | #[derive(Clone, PartialEq, ::prost::Message)] 247 | pub struct OpenChannelResponse { 248 | /// The local channel id of the created channel that user can use to refer to channel. 249 | #[prost(string, tag = "1")] 250 | pub user_channel_id: ::prost::alloc::string::String, 251 | } 252 | /// Update the config for a previously opened channel. 253 | /// See more: 254 | #[allow(clippy::derive_partial_eq_without_eq)] 255 | #[derive(Clone, PartialEq, ::prost::Message)] 256 | pub struct UpdateChannelConfigRequest { 257 | /// The local `user_channel_id` of this channel. 258 | #[prost(string, tag = "1")] 259 | pub user_channel_id: ::prost::alloc::string::String, 260 | /// The hex-encoded public key of the counterparty node to update channel config with. 261 | #[prost(string, tag = "2")] 262 | pub counterparty_node_id: ::prost::alloc::string::String, 263 | /// The updated channel configuration settings for a channel. 264 | #[prost(message, optional, tag = "3")] 265 | pub channel_config: ::core::option::Option, 266 | } 267 | /// The response `content` for the `UpdateChannelConfig` API, when HttpStatusCode is OK (200). 268 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 269 | #[allow(clippy::derive_partial_eq_without_eq)] 270 | #[derive(Clone, PartialEq, ::prost::Message)] 271 | pub struct UpdateChannelConfigResponse {} 272 | /// Closes the channel specified by given request. 273 | /// See more: 274 | /// - 275 | /// - 276 | #[allow(clippy::derive_partial_eq_without_eq)] 277 | #[derive(Clone, PartialEq, ::prost::Message)] 278 | pub struct CloseChannelRequest { 279 | /// The local `user_channel_id` of this channel. 280 | #[prost(string, tag = "1")] 281 | pub user_channel_id: ::prost::alloc::string::String, 282 | /// The hex-encoded public key of the node to close a channel with. 283 | #[prost(string, tag = "2")] 284 | pub counterparty_node_id: ::prost::alloc::string::String, 285 | /// Whether to force close the specified channel. 286 | #[prost(bool, optional, tag = "3")] 287 | pub force_close: ::core::option::Option, 288 | /// The reason for force-closing, can only be set while force closing a channel. 289 | #[prost(string, optional, tag = "4")] 290 | pub force_close_reason: ::core::option::Option<::prost::alloc::string::String>, 291 | } 292 | /// The response `content` for the `CloseChannel` API, when HttpStatusCode is OK (200). 293 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 294 | #[allow(clippy::derive_partial_eq_without_eq)] 295 | #[derive(Clone, PartialEq, ::prost::Message)] 296 | pub struct CloseChannelResponse {} 297 | /// Returns a list of known channels. 298 | /// See more: 299 | #[allow(clippy::derive_partial_eq_without_eq)] 300 | #[derive(Clone, PartialEq, ::prost::Message)] 301 | pub struct ListChannelsRequest {} 302 | /// The response `content` for the `ListChannels` API, when HttpStatusCode is OK (200). 303 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 304 | #[allow(clippy::derive_partial_eq_without_eq)] 305 | #[derive(Clone, PartialEq, ::prost::Message)] 306 | pub struct ListChannelsResponse { 307 | /// List of channels. 308 | #[prost(message, repeated, tag = "1")] 309 | pub channels: ::prost::alloc::vec::Vec, 310 | } 311 | /// Returns payment details for a given payment_id. 312 | /// See more: 313 | #[allow(clippy::derive_partial_eq_without_eq)] 314 | #[derive(Clone, PartialEq, ::prost::Message)] 315 | pub struct GetPaymentDetailsRequest { 316 | /// An identifier used to uniquely identify a payment in hex-encoded form. 317 | #[prost(string, tag = "1")] 318 | pub payment_id: ::prost::alloc::string::String, 319 | } 320 | /// The response `content` for the `GetPaymentDetails` API, when HttpStatusCode is OK (200). 321 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 322 | #[allow(clippy::derive_partial_eq_without_eq)] 323 | #[derive(Clone, PartialEq, ::prost::Message)] 324 | pub struct GetPaymentDetailsResponse { 325 | /// Represents a payment. 326 | /// Will be `None` if payment doesn't exist. 327 | #[prost(message, optional, tag = "1")] 328 | pub payment: ::core::option::Option, 329 | } 330 | /// Retrieves list of all payments. 331 | /// See more: 332 | #[allow(clippy::derive_partial_eq_without_eq)] 333 | #[derive(Clone, PartialEq, ::prost::Message)] 334 | pub struct ListPaymentsRequest { 335 | /// `page_token` is a pagination token. 336 | /// 337 | /// To query for the first page, `page_token` must not be specified. 338 | /// 339 | /// For subsequent pages, use the value that was returned as `next_page_token` in the previous 340 | /// page's response. 341 | #[prost(message, optional, tag = "1")] 342 | pub page_token: ::core::option::Option, 343 | } 344 | /// The response `content` for the `ListPayments` API, when HttpStatusCode is OK (200). 345 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 346 | #[allow(clippy::derive_partial_eq_without_eq)] 347 | #[derive(Clone, PartialEq, ::prost::Message)] 348 | pub struct ListPaymentsResponse { 349 | /// List of payments. 350 | #[prost(message, repeated, tag = "1")] 351 | pub payments: ::prost::alloc::vec::Vec, 352 | /// `next_page_token` is a pagination token, used to retrieve the next page of results. 353 | /// Use this value to query for next-page of paginated operation, by specifying 354 | /// this value as the `page_token` in the next request. 355 | /// 356 | /// If `next_page_token` is `None`, then the "last page" of results has been processed and 357 | /// there is no more data to be retrieved. 358 | /// 359 | /// If `next_page_token` is not `None`, it does not necessarily mean that there is more data in the 360 | /// result set. The only way to know when you have reached the end of the result set is when 361 | /// `next_page_token` is `None`. 362 | /// 363 | /// **Caution**: Clients must not assume a specific number of records to be present in a page for 364 | /// paginated response. 365 | #[prost(message, optional, tag = "2")] 366 | pub next_page_token: ::core::option::Option, 367 | } 368 | /// Retrieves list of all forwarded payments. 369 | /// See more: 370 | #[allow(clippy::derive_partial_eq_without_eq)] 371 | #[derive(Clone, PartialEq, ::prost::Message)] 372 | pub struct ListForwardedPaymentsRequest { 373 | /// `page_token` is a pagination token. 374 | /// 375 | /// To query for the first page, `page_token` must not be specified. 376 | /// 377 | /// For subsequent pages, use the value that was returned as `next_page_token` in the previous 378 | /// page's response. 379 | #[prost(message, optional, tag = "1")] 380 | pub page_token: ::core::option::Option, 381 | } 382 | /// The response `content` for the `ListForwardedPayments` API, when HttpStatusCode is OK (200). 383 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 384 | #[allow(clippy::derive_partial_eq_without_eq)] 385 | #[derive(Clone, PartialEq, ::prost::Message)] 386 | pub struct ListForwardedPaymentsResponse { 387 | /// List of forwarded payments. 388 | #[prost(message, repeated, tag = "1")] 389 | pub forwarded_payments: ::prost::alloc::vec::Vec, 390 | /// `next_page_token` is a pagination token, used to retrieve the next page of results. 391 | /// Use this value to query for next-page of paginated operation, by specifying 392 | /// this value as the `page_token` in the next request. 393 | /// 394 | /// If `next_page_token` is `None`, then the "last page" of results has been processed and 395 | /// there is no more data to be retrieved. 396 | /// 397 | /// If `next_page_token` is not `None`, it does not necessarily mean that there is more data in the 398 | /// result set. The only way to know when you have reached the end of the result set is when 399 | /// `next_page_token` is `None`. 400 | /// 401 | /// **Caution**: Clients must not assume a specific number of records to be present in a page for 402 | /// paginated response. 403 | #[prost(message, optional, tag = "2")] 404 | pub next_page_token: ::core::option::Option, 405 | } 406 | /// Retrieves an overview of all known balances. 407 | /// See more: 408 | #[allow(clippy::derive_partial_eq_without_eq)] 409 | #[derive(Clone, PartialEq, ::prost::Message)] 410 | pub struct GetBalancesRequest {} 411 | /// The response `content` for the `GetBalances` API, when HttpStatusCode is OK (200). 412 | /// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 413 | #[allow(clippy::derive_partial_eq_without_eq)] 414 | #[derive(Clone, PartialEq, ::prost::Message)] 415 | pub struct GetBalancesResponse { 416 | /// The total balance of our on-chain wallet. 417 | #[prost(uint64, tag = "1")] 418 | pub total_onchain_balance_sats: u64, 419 | /// The currently spendable balance of our on-chain wallet. 420 | /// 421 | /// This includes any sufficiently confirmed funds, minus `total_anchor_channels_reserve_sats`. 422 | #[prost(uint64, tag = "2")] 423 | pub spendable_onchain_balance_sats: u64, 424 | /// The share of our total balance that we retain as an emergency reserve to (hopefully) be 425 | /// able to spend the Anchor outputs when one of our channels is closed. 426 | #[prost(uint64, tag = "3")] 427 | pub total_anchor_channels_reserve_sats: u64, 428 | /// The total balance that we would be able to claim across all our Lightning channels. 429 | /// 430 | /// Note this excludes balances that we are unsure if we are able to claim (e.g., as we are 431 | /// waiting for a preimage or for a timeout to expire). These balances will however be included 432 | /// as `MaybePreimageClaimableHTLC` and `MaybeTimeoutClaimableHTLC` in `lightning_balances`. 433 | #[prost(uint64, tag = "4")] 434 | pub total_lightning_balance_sats: u64, 435 | /// A detailed list of all known Lightning balances that would be claimable on channel closure. 436 | /// 437 | /// Note that less than the listed amounts are spendable over lightning as further reserve 438 | /// restrictions apply. Please refer to `Channel::outbound_capacity_msat` and 439 | /// Channel::next_outbound_htlc_limit_msat as returned by `ListChannels` 440 | /// for a better approximation of the spendable amounts. 441 | #[prost(message, repeated, tag = "5")] 442 | pub lightning_balances: ::prost::alloc::vec::Vec, 443 | /// A detailed list of balances currently being swept from the Lightning to the on-chain 444 | /// wallet. 445 | /// 446 | /// These are balances resulting from channel closures that may have been encumbered by a 447 | /// delay, but are now being claimed and useable once sufficiently confirmed on-chain. 448 | /// 449 | /// Note that, depending on the sync status of the wallets, swept balances listed here might or 450 | /// might not already be accounted for in `total_onchain_balance_sats`. 451 | #[prost(message, repeated, tag = "6")] 452 | pub pending_balances_from_channel_closures: 453 | ::prost::alloc::vec::Vec, 454 | } 455 | -------------------------------------------------------------------------------- /ldk-server-protos/src/error.rs: -------------------------------------------------------------------------------- 1 | /// When HttpStatusCode is not ok (200), the response `content` contains a serialized `ErrorResponse` 2 | /// with the relevant ErrorCode and `message` 3 | #[allow(clippy::derive_partial_eq_without_eq)] 4 | #[derive(Clone, PartialEq, ::prost::Message)] 5 | pub struct ErrorResponse { 6 | /// The error message containing a generic description of the error condition in English. 7 | /// It is intended for a human audience only and should not be parsed to extract any information 8 | /// programmatically. Client-side code may use it for logging only. 9 | #[prost(string, tag = "1")] 10 | pub message: ::prost::alloc::string::String, 11 | /// The error code uniquely identifying an error condition. 12 | /// It is meant to be read and understood programmatically by code that detects/handles errors by 13 | /// type. 14 | /// 15 | /// **Caution**: If a new type of `error_code` is introduced in the `ErrorCode` enum, `error_code` field will be set to 16 | /// `UnknownError`. 17 | #[prost(enumeration = "ErrorCode", tag = "2")] 18 | pub error_code: i32, 19 | } 20 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] 21 | #[repr(i32)] 22 | pub enum ErrorCode { 23 | /// Will never be used as `error_code` by server. 24 | /// 25 | /// **Caution**: If a new type of `error_code` is introduced in the `ErrorCode` enum, `error_code` field will be set to 26 | /// `UnknownError`. 27 | UnknownError = 0, 28 | /// Used in the following cases: 29 | /// - The request was missing a required argument. 30 | /// - The specified argument was invalid, incomplete or in the wrong format. 31 | /// - The request body of api cannot be deserialized into corresponding protobuf object. 32 | /// - The request does not follow api contract. 33 | InvalidRequestError = 1, 34 | /// Used when authentication fails or in case of an unauthorized request. 35 | AuthError = 2, 36 | /// Used to represent an error while doing a Lightning operation. 37 | LightningError = 3, 38 | /// Used when an internal server error occurred. The client is probably at no fault. 39 | InternalServerError = 4, 40 | } 41 | impl ErrorCode { 42 | /// String value of the enum field names used in the ProtoBuf definition. 43 | /// 44 | /// The values are not transformed in any way and thus are considered stable 45 | /// (if the ProtoBuf definition does not change) and safe for programmatic use. 46 | pub fn as_str_name(&self) -> &'static str { 47 | match self { 48 | ErrorCode::UnknownError => "UNKNOWN_ERROR", 49 | ErrorCode::InvalidRequestError => "INVALID_REQUEST_ERROR", 50 | ErrorCode::AuthError => "AUTH_ERROR", 51 | ErrorCode::LightningError => "LIGHTNING_ERROR", 52 | ErrorCode::InternalServerError => "INTERNAL_SERVER_ERROR", 53 | } 54 | } 55 | /// Creates an enum from field names used in the ProtoBuf definition. 56 | pub fn from_str_name(value: &str) -> ::core::option::Option { 57 | match value { 58 | "UNKNOWN_ERROR" => Some(Self::UnknownError), 59 | "INVALID_REQUEST_ERROR" => Some(Self::InvalidRequestError), 60 | "AUTH_ERROR" => Some(Self::AuthError), 61 | "LIGHTNING_ERROR" => Some(Self::LightningError), 62 | "INTERNAL_SERVER_ERROR" => Some(Self::InternalServerError), 63 | _ => None, 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ldk-server-protos/src/events.rs: -------------------------------------------------------------------------------- 1 | /// EventEnvelope wraps different event types in a single message to be used by EventPublisher. 2 | #[allow(clippy::derive_partial_eq_without_eq)] 3 | #[derive(Clone, PartialEq, ::prost::Message)] 4 | pub struct EventEnvelope { 5 | #[prost(oneof = "event_envelope::Event", tags = "2, 3, 4, 6")] 6 | pub event: ::core::option::Option, 7 | } 8 | /// Nested message and enum types in `EventEnvelope`. 9 | pub mod event_envelope { 10 | #[allow(clippy::derive_partial_eq_without_eq)] 11 | #[derive(Clone, PartialEq, ::prost::Oneof)] 12 | pub enum Event { 13 | #[prost(message, tag = "2")] 14 | PaymentReceived(super::PaymentReceived), 15 | #[prost(message, tag = "3")] 16 | PaymentSuccessful(super::PaymentSuccessful), 17 | #[prost(message, tag = "4")] 18 | PaymentFailed(super::PaymentFailed), 19 | #[prost(message, tag = "6")] 20 | PaymentForwarded(super::PaymentForwarded), 21 | } 22 | } 23 | /// PaymentReceived indicates a payment has been received. 24 | #[allow(clippy::derive_partial_eq_without_eq)] 25 | #[derive(Clone, PartialEq, ::prost::Message)] 26 | pub struct PaymentReceived { 27 | /// The payment details for the payment in event. 28 | #[prost(message, optional, tag = "1")] 29 | pub payment: ::core::option::Option, 30 | } 31 | /// PaymentSuccessful indicates a sent payment was successful. 32 | #[allow(clippy::derive_partial_eq_without_eq)] 33 | #[derive(Clone, PartialEq, ::prost::Message)] 34 | pub struct PaymentSuccessful { 35 | /// The payment details for the payment in event. 36 | #[prost(message, optional, tag = "1")] 37 | pub payment: ::core::option::Option, 38 | } 39 | /// PaymentFailed indicates a sent payment has failed. 40 | #[allow(clippy::derive_partial_eq_without_eq)] 41 | #[derive(Clone, PartialEq, ::prost::Message)] 42 | pub struct PaymentFailed { 43 | /// The payment details for the payment in event. 44 | #[prost(message, optional, tag = "1")] 45 | pub payment: ::core::option::Option, 46 | } 47 | /// PaymentForwarded indicates a payment was forwarded through the node. 48 | #[allow(clippy::derive_partial_eq_without_eq)] 49 | #[derive(Clone, PartialEq, ::prost::Message)] 50 | pub struct PaymentForwarded { 51 | #[prost(message, optional, tag = "1")] 52 | pub forwarded_payment: ::core::option::Option, 53 | } 54 | -------------------------------------------------------------------------------- /ldk-server-protos/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod error; 3 | pub mod events; 4 | pub mod types; 5 | -------------------------------------------------------------------------------- /ldk-server-protos/src/proto/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package api; 3 | 4 | import 'types.proto'; 5 | 6 | // Retrieve the latest node info like `node_id`, `current_best_block` etc. 7 | // See more: 8 | // - https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.node_id 9 | // - https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.status 10 | message GetNodeInfoRequest { 11 | } 12 | 13 | // The response `content` for the `GetNodeInfo` API, when HttpStatusCode is OK (200). 14 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 15 | message GetNodeInfoResponse { 16 | 17 | // The hex-encoded `node-id` or public key for our own lightning node. 18 | string node_id = 1; 19 | 20 | // The best block to which our Lightning wallet is currently synced. 21 | // 22 | // Should be always set, will never be `None`. 23 | types.BestBlock current_best_block = 3; 24 | 25 | // The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced our Lightning wallet to 26 | // the chain tip. 27 | // 28 | // Will be `None` if the wallet hasn't been synced yet. 29 | optional uint64 latest_lightning_wallet_sync_timestamp = 4; 30 | 31 | // The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced our on-chain 32 | // wallet to the chain tip. 33 | // 34 | // Will be `None` if the wallet hasn’t been synced since the node was initialized. 35 | optional uint64 latest_onchain_wallet_sync_timestamp = 5; 36 | 37 | // The timestamp, in seconds since start of the UNIX epoch, when we last successfully update our fee rate cache. 38 | // 39 | // Will be `None` if the cache hasn’t been updated since the node was initialized. 40 | optional uint64 latest_fee_rate_cache_update_timestamp = 6; 41 | 42 | // The timestamp, in seconds since start of the UNIX epoch, when the last rapid gossip sync (RGS) snapshot we 43 | // successfully applied was generated. 44 | // 45 | // Will be `None` if RGS isn’t configured or the snapshot hasn’t been updated since the node was initialized. 46 | optional uint64 latest_rgs_snapshot_timestamp = 7; 47 | 48 | // The timestamp, in seconds since start of the UNIX epoch, when we last broadcasted a node announcement. 49 | // 50 | // Will be `None` if we have no public channels or we haven’t broadcasted since the node was initialized. 51 | optional uint64 latest_node_announcement_broadcast_timestamp = 8; 52 | } 53 | 54 | // Retrieve a new on-chain funding address. 55 | // See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.new_address 56 | message OnchainReceiveRequest { 57 | } 58 | 59 | // The response `content` for the `OnchainReceive` API, when HttpStatusCode is OK (200). 60 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.. 61 | message OnchainReceiveResponse { 62 | 63 | // A Bitcoin on-chain address. 64 | string address = 1; 65 | } 66 | 67 | // Send an on-chain payment to the given address. 68 | message OnchainSendRequest { 69 | 70 | // The address to send coins to. 71 | string address = 1; 72 | 73 | // The amount in satoshis to send. 74 | // While sending the specified amount, we will respect any on-chain reserve we need to keep, 75 | // i.e., won't allow to cut into `total_anchor_channels_reserve_sats`. 76 | // See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.send_to_address 77 | optional uint64 amount_sats = 2; 78 | 79 | // If set, the amount_sats field should be unset. 80 | // It indicates that node will send full balance to the specified address. 81 | // 82 | // Please note that when send_all is used this operation will **not** retain any on-chain reserves, 83 | // which might be potentially dangerous if you have open Anchor channels for which you can't trust 84 | // the counterparty to spend the Anchor output after channel closure. 85 | // See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.send_all_to_address 86 | optional bool send_all = 3; 87 | 88 | // If `fee_rate_sat_per_vb` is set it will be used on the resulting transaction. Otherwise we'll retrieve 89 | // a reasonable estimate from BitcoinD. 90 | optional uint64 fee_rate_sat_per_vb = 4; 91 | } 92 | 93 | // The response `content` for the `OnchainSend` API, when HttpStatusCode is OK (200). 94 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 95 | message OnchainSendResponse { 96 | 97 | // The transaction ID of the broadcasted transaction. 98 | string txid = 1; 99 | } 100 | 101 | // Return a BOLT11 payable invoice that can be used to request and receive a payment 102 | // for the given amount, if specified. 103 | // The inbound payment will be automatically claimed upon arrival. 104 | // See more: 105 | // - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.receive 106 | // - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.receive_variable_amount 107 | message Bolt11ReceiveRequest { 108 | 109 | // The amount in millisatoshi to send. If unset, a "zero-amount" or variable-amount invoice is returned. 110 | optional uint64 amount_msat = 1; 111 | 112 | // An optional description to attach along with the invoice. 113 | // Will be set in the description field of the encoded payment request. 114 | types.Bolt11InvoiceDescription description = 2; 115 | 116 | // Invoice expiry time in seconds. 117 | uint32 expiry_secs = 3; 118 | } 119 | 120 | // The response `content` for the `Bolt11Receive` API, when HttpStatusCode is OK (200). 121 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 122 | message Bolt11ReceiveResponse { 123 | 124 | // An invoice for a payment within the Lightning Network. 125 | // With the details of the invoice, the sender has all the data necessary to send a payment 126 | // to the recipient. 127 | string invoice = 1; 128 | } 129 | 130 | // Send a payment for a BOLT11 invoice. 131 | // See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.send 132 | message Bolt11SendRequest { 133 | 134 | // An invoice for a payment within the Lightning Network. 135 | string invoice = 1; 136 | 137 | // Set this field when paying a so-called "zero-amount" invoice, i.e., an invoice that leaves the 138 | // amount paid to be determined by the user. 139 | // This operation will fail if the amount specified is less than the value required by the given invoice. 140 | optional uint64 amount_msat = 2; 141 | 142 | } 143 | 144 | // The response `content` for the `Bolt11Send` API, when HttpStatusCode is OK (200). 145 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 146 | message Bolt11SendResponse { 147 | 148 | // An identifier used to uniquely identify a payment in hex-encoded form. 149 | string payment_id = 1; 150 | } 151 | 152 | // Returns a BOLT12 offer for the given amount, if specified. 153 | // 154 | // See more: 155 | // - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt12Payment.html#method.receive 156 | // - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt12Payment.html#method.receive_variable_amount 157 | message Bolt12ReceiveRequest { 158 | 159 | // An optional description to attach along with the offer. 160 | // Will be set in the description field of the encoded offer. 161 | string description = 1; 162 | 163 | // The amount in millisatoshi to send. If unset, a "zero-amount" or variable-amount offer is returned. 164 | optional uint64 amount_msat = 2; 165 | 166 | // Offer expiry time in seconds. 167 | optional uint32 expiry_secs = 3; 168 | 169 | // If set, it represents the number of items requested, can only be set for fixed-amount offers. 170 | optional uint64 quantity = 4; 171 | } 172 | 173 | // The response `content` for the `Bolt12Receive` API, when HttpStatusCode is OK (200). 174 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 175 | message Bolt12ReceiveResponse { 176 | 177 | // An offer for a payment within the Lightning Network. 178 | // With the details of the offer, the sender has all the data necessary to send a payment 179 | // to the recipient. 180 | string offer = 1; 181 | } 182 | 183 | // Send a payment for a BOLT12 offer. 184 | // See more: 185 | // - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt12Payment.html#method.send 186 | // - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt12Payment.html#method.send_using_amount 187 | message Bolt12SendRequest { 188 | 189 | // An offer for a payment within the Lightning Network. 190 | string offer = 1; 191 | 192 | // Set this field when paying a so-called "zero-amount" offer, i.e., an offer that leaves the 193 | // amount paid to be determined by the user. 194 | // This operation will fail if the amount specified is less than the value required by the given offer. 195 | optional uint64 amount_msat = 2; 196 | 197 | // If set, it represents the number of items requested. 198 | optional uint64 quantity = 3; 199 | 200 | // If set, it will be seen by the recipient and reflected back in the invoice. 201 | optional string payer_note = 4; 202 | } 203 | 204 | // The response `content` for the `Bolt12Send` API, when HttpStatusCode is OK (200). 205 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 206 | message Bolt12SendResponse { 207 | 208 | // An identifier used to uniquely identify a payment in hex-encoded form. 209 | string payment_id = 1; 210 | } 211 | 212 | // Creates a new outbound channel to the given remote node. 213 | // See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.connect_open_channel 214 | message OpenChannelRequest { 215 | 216 | // The hex-encoded public key of the node to open a channel with. 217 | string node_pubkey = 1; 218 | 219 | // An address which can be used to connect to a remote peer. 220 | // It can be of type IPv4:port, IPv6:port, OnionV3:port or hostname:port 221 | string address = 2; 222 | 223 | // The amount of satoshis the caller is willing to commit to the channel. 224 | uint64 channel_amount_sats = 3; 225 | 226 | // The amount of satoshis to push to the remote side as part of the initial commitment state. 227 | optional uint64 push_to_counterparty_msat = 4; 228 | 229 | // The channel configuration to be used for opening this channel. If unset, default ChannelConfig is used. 230 | optional types.ChannelConfig channel_config = 5; 231 | 232 | // Whether the channel should be public. 233 | bool announce_channel = 6; 234 | } 235 | 236 | // The response `content` for the `OpenChannel` API, when HttpStatusCode is OK (200). 237 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 238 | message OpenChannelResponse { 239 | 240 | // The local channel id of the created channel that user can use to refer to channel. 241 | string user_channel_id = 1; 242 | } 243 | 244 | // Update the config for a previously opened channel. 245 | // See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.update_channel_config 246 | message UpdateChannelConfigRequest { 247 | 248 | // The local `user_channel_id` of this channel. 249 | string user_channel_id = 1; 250 | 251 | // The hex-encoded public key of the counterparty node to update channel config with. 252 | string counterparty_node_id = 2; 253 | 254 | // The updated channel configuration settings for a channel. 255 | types.ChannelConfig channel_config = 3; 256 | } 257 | 258 | // The response `content` for the `UpdateChannelConfig` API, when HttpStatusCode is OK (200). 259 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 260 | message UpdateChannelConfigResponse { 261 | } 262 | 263 | // Closes the channel specified by given request. 264 | // See more: 265 | // - https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.close_channel 266 | // - https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.force_close_channel 267 | message CloseChannelRequest { 268 | 269 | // The local `user_channel_id` of this channel. 270 | string user_channel_id = 1; 271 | 272 | // The hex-encoded public key of the node to close a channel with. 273 | string counterparty_node_id = 2; 274 | 275 | // Whether to force close the specified channel. 276 | optional bool force_close = 3; 277 | 278 | // The reason for force-closing, can only be set while force closing a channel. 279 | optional string force_close_reason = 4; 280 | } 281 | 282 | // The response `content` for the `CloseChannel` API, when HttpStatusCode is OK (200). 283 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 284 | message CloseChannelResponse { 285 | 286 | } 287 | 288 | // Returns a list of known channels. 289 | // See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.list_channels 290 | message ListChannelsRequest {} 291 | 292 | // The response `content` for the `ListChannels` API, when HttpStatusCode is OK (200). 293 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 294 | message ListChannelsResponse { 295 | 296 | // List of channels. 297 | repeated types.Channel channels = 1; 298 | } 299 | 300 | // Returns payment details for a given payment_id. 301 | // See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.payment 302 | message GetPaymentDetailsRequest { 303 | // An identifier used to uniquely identify a payment in hex-encoded form. 304 | string payment_id = 1; 305 | } 306 | 307 | // The response `content` for the `GetPaymentDetails` API, when HttpStatusCode is OK (200). 308 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 309 | message GetPaymentDetailsResponse { 310 | // Represents a payment. 311 | // Will be `None` if payment doesn't exist. 312 | types.Payment payment = 1; 313 | } 314 | 315 | // Retrieves list of all payments. 316 | // See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.list_payments 317 | message ListPaymentsRequest { 318 | // `page_token` is a pagination token. 319 | // 320 | // To query for the first page, `page_token` must not be specified. 321 | // 322 | // For subsequent pages, use the value that was returned as `next_page_token` in the previous 323 | // page's response. 324 | optional types.PageToken page_token = 1; 325 | } 326 | 327 | // The response `content` for the `ListPayments` API, when HttpStatusCode is OK (200). 328 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 329 | message ListPaymentsResponse { 330 | // List of payments. 331 | repeated types.Payment payments = 1; 332 | 333 | // `next_page_token` is a pagination token, used to retrieve the next page of results. 334 | // Use this value to query for next-page of paginated operation, by specifying 335 | // this value as the `page_token` in the next request. 336 | // 337 | // If `next_page_token` is `None`, then the "last page" of results has been processed and 338 | // there is no more data to be retrieved. 339 | // 340 | // If `next_page_token` is not `None`, it does not necessarily mean that there is more data in the 341 | // result set. The only way to know when you have reached the end of the result set is when 342 | // `next_page_token` is `None`. 343 | // 344 | // **Caution**: Clients must not assume a specific number of records to be present in a page for 345 | // paginated response. 346 | optional types.PageToken next_page_token = 2; 347 | } 348 | 349 | // Retrieves list of all forwarded payments. 350 | // See more: https://docs.rs/ldk-node/latest/ldk_node/enum.Event.html#variant.PaymentForwarded 351 | message ListForwardedPaymentsRequest { 352 | // `page_token` is a pagination token. 353 | // 354 | // To query for the first page, `page_token` must not be specified. 355 | // 356 | // For subsequent pages, use the value that was returned as `next_page_token` in the previous 357 | // page's response. 358 | optional types.PageToken page_token = 1; 359 | } 360 | 361 | // The response `content` for the `ListForwardedPayments` API, when HttpStatusCode is OK (200). 362 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 363 | message ListForwardedPaymentsResponse { 364 | // List of forwarded payments. 365 | repeated types.ForwardedPayment forwarded_payments = 1; 366 | 367 | // `next_page_token` is a pagination token, used to retrieve the next page of results. 368 | // Use this value to query for next-page of paginated operation, by specifying 369 | // this value as the `page_token` in the next request. 370 | // 371 | // If `next_page_token` is `None`, then the "last page" of results has been processed and 372 | // there is no more data to be retrieved. 373 | // 374 | // If `next_page_token` is not `None`, it does not necessarily mean that there is more data in the 375 | // result set. The only way to know when you have reached the end of the result set is when 376 | // `next_page_token` is `None`. 377 | // 378 | // **Caution**: Clients must not assume a specific number of records to be present in a page for 379 | // paginated response. 380 | optional types.PageToken next_page_token = 2; 381 | } 382 | 383 | // Retrieves an overview of all known balances. 384 | // See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.list_balances 385 | message GetBalancesRequest {} 386 | 387 | // The response `content` for the `GetBalances` API, when HttpStatusCode is OK (200). 388 | // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. 389 | message GetBalancesResponse { 390 | // The total balance of our on-chain wallet. 391 | uint64 total_onchain_balance_sats = 1; 392 | 393 | // The currently spendable balance of our on-chain wallet. 394 | // 395 | // This includes any sufficiently confirmed funds, minus `total_anchor_channels_reserve_sats`. 396 | uint64 spendable_onchain_balance_sats = 2; 397 | 398 | // The share of our total balance that we retain as an emergency reserve to (hopefully) be 399 | // able to spend the Anchor outputs when one of our channels is closed. 400 | uint64 total_anchor_channels_reserve_sats = 3; 401 | 402 | // The total balance that we would be able to claim across all our Lightning channels. 403 | // 404 | // Note this excludes balances that we are unsure if we are able to claim (e.g., as we are 405 | // waiting for a preimage or for a timeout to expire). These balances will however be included 406 | // as `MaybePreimageClaimableHTLC` and `MaybeTimeoutClaimableHTLC` in `lightning_balances`. 407 | uint64 total_lightning_balance_sats = 4; 408 | 409 | // A detailed list of all known Lightning balances that would be claimable on channel closure. 410 | // 411 | // Note that less than the listed amounts are spendable over lightning as further reserve 412 | // restrictions apply. Please refer to `Channel::outbound_capacity_msat` and 413 | // Channel::next_outbound_htlc_limit_msat as returned by `ListChannels` 414 | // for a better approximation of the spendable amounts. 415 | repeated types.LightningBalance lightning_balances = 5; 416 | 417 | // A detailed list of balances currently being swept from the Lightning to the on-chain 418 | // wallet. 419 | // 420 | // These are balances resulting from channel closures that may have been encumbered by a 421 | // delay, but are now being claimed and useable once sufficiently confirmed on-chain. 422 | // 423 | // Note that, depending on the sync status of the wallets, swept balances listed here might or 424 | // might not already be accounted for in `total_onchain_balance_sats`. 425 | repeated types.PendingSweepBalance pending_balances_from_channel_closures = 6; 426 | } 427 | -------------------------------------------------------------------------------- /ldk-server-protos/src/proto/error.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package error; 3 | 4 | // When HttpStatusCode is not ok (200), the response `content` contains a serialized `ErrorResponse` 5 | // with the relevant ErrorCode and `message` 6 | message ErrorResponse { 7 | 8 | // The error message containing a generic description of the error condition in English. 9 | // It is intended for a human audience only and should not be parsed to extract any information 10 | // programmatically. Client-side code may use it for logging only. 11 | string message = 1; 12 | 13 | // The error code uniquely identifying an error condition. 14 | // It is meant to be read and understood programmatically by code that detects/handles errors by 15 | // type. 16 | // 17 | // **Caution**: If a new type of `error_code` is introduced in the `ErrorCode` enum, `error_code` field will be set to 18 | // `UnknownError`. 19 | ErrorCode error_code = 2; 20 | } 21 | 22 | enum ErrorCode { 23 | 24 | // Will never be used as `error_code` by server. 25 | // 26 | // **Caution**: If a new type of `error_code` is introduced in the `ErrorCode` enum, `error_code` field will be set to 27 | // `UnknownError`. 28 | UNKNOWN_ERROR = 0; 29 | 30 | // Used in the following cases: 31 | // - The request was missing a required argument. 32 | // - The specified argument was invalid, incomplete or in the wrong format. 33 | // - The request body of api cannot be deserialized into corresponding protobuf object. 34 | // - The request does not follow api contract. 35 | INVALID_REQUEST_ERROR = 1; 36 | 37 | // Used when authentication fails or in case of an unauthorized request. 38 | AUTH_ERROR = 2; 39 | 40 | // Used to represent an error while doing a Lightning operation. 41 | LIGHTNING_ERROR = 3; 42 | 43 | // Used when an internal server error occurred. The client is probably at no fault. 44 | INTERNAL_SERVER_ERROR = 4; 45 | } 46 | -------------------------------------------------------------------------------- /ldk-server-protos/src/proto/events.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | import "types.proto"; 3 | package events; 4 | 5 | // EventEnvelope wraps different event types in a single message to be used by EventPublisher. 6 | message EventEnvelope { 7 | oneof event { 8 | PaymentReceived payment_received = 2; 9 | PaymentSuccessful payment_successful = 3; 10 | PaymentFailed payment_failed = 4; 11 | PaymentForwarded payment_forwarded = 6; 12 | } 13 | } 14 | 15 | // PaymentReceived indicates a payment has been received. 16 | message PaymentReceived { 17 | // The payment details for the payment in event. 18 | types.Payment payment = 1; 19 | } 20 | 21 | // PaymentSuccessful indicates a sent payment was successful. 22 | message PaymentSuccessful { 23 | // The payment details for the payment in event. 24 | types.Payment payment = 1; 25 | } 26 | 27 | // PaymentFailed indicates a sent payment has failed. 28 | message PaymentFailed { 29 | // The payment details for the payment in event. 30 | types.Payment payment = 1; 31 | } 32 | 33 | // PaymentForwarded indicates a payment was forwarded through the node. 34 | message PaymentForwarded { 35 | types.ForwardedPayment forwarded_payment = 1; 36 | } 37 | -------------------------------------------------------------------------------- /ldk-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ldk-server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ldk-node = { git = "https://github.com/lightningdevkit/ldk-node.git", rev = "f0338d19256615088fabab2b6927d478ae3ec1a1" } 8 | serde = { version = "1.0.203", default-features = false, features = ["derive"] } 9 | hyper = { version = "1", default-features = false, features = ["server", "http1"] } 10 | http-body-util = { version = "0.1", default-features = false } 11 | hyper-util = { version = "0.1", default-features = false, features = ["server-graceful"] } 12 | tokio = { version = "1.38.0", default-features = false, features = ["time", "signal", "rt-multi-thread"] } 13 | prost = { version = "0.11.6", default-features = false, features = ["std"] } 14 | ldk-server-protos = { path = "../ldk-server-protos" } 15 | bytes = { version = "1.4.0", default-features = false } 16 | hex = { package = "hex-conservative", version = "0.2.1", default-features = false } 17 | rusqlite = { version = "0.31.0", features = ["bundled"] } 18 | rand = { version = "0.8.5", default-features = false } 19 | async-trait = { version = "0.1.85", default-features = false } 20 | toml = { version = "0.8.9", default-features = false, features = ["parse"] } 21 | 22 | # Required for RabittMQ based EventPublisher. Only enabled for `events-rabbitmq` feature. 23 | lapin = { version = "2.4.0", features = ["rustls"], default-features = false, optional = true } 24 | 25 | [features] 26 | default = [] 27 | events-rabbitmq = ["dep:lapin"] 28 | 29 | # Experimental Features. 30 | experimental-lsps2-support = [] 31 | 32 | # Feature-flags related to integration tests. 33 | integration-tests-events-rabbitmq = ["events-rabbitmq"] 34 | 35 | [dev-dependencies] 36 | futures-util = "0.3.31" 37 | -------------------------------------------------------------------------------- /ldk-server/ldk-server-config.toml: -------------------------------------------------------------------------------- 1 | # Lightning node settings 2 | [node] 3 | network = "regtest" # Bitcoin network to use 4 | listening_address = "localhost:3001" # Lightning node listening address 5 | rest_service_address = "127.0.0.1:3002" # LDK Server REST address 6 | 7 | # Storage settings 8 | [storage.disk] 9 | dir_path = "/tmp/ldk-server/" # Path for LDK and BDK data persistence 10 | 11 | # Bitcoin Core settings 12 | [bitcoind] 13 | rpc_address = "127.0.0.1:18444" # RPC endpoint 14 | rpc_user = "polaruser" # RPC username 15 | rpc_password = "polarpass" # RPC password 16 | 17 | # RabbitMQ settings (only required if using events-rabbitmq feature) 18 | [rabbitmq] 19 | connection_string = "" # RabbitMQ connection string 20 | exchange_name = "" 21 | 22 | # Experimental LSPS2 Service Support 23 | # CAUTION: LSPS2 support is highly experimental and for testing purposes only. 24 | [liquidity.lsps2_service] 25 | # Indicates whether the LSPS service will be announced via the gossip network. 26 | advertise_service = false 27 | 28 | # The fee we withhold for the channel open from the initial payment. 29 | channel_opening_fee_ppm = 1000 # 0.1% fee 30 | 31 | # The proportional overprovisioning for the channel. 32 | channel_over_provisioning_ppm = 500000 # 50% extra capacity 33 | 34 | # The minimum fee required for opening a channel. 35 | min_channel_opening_fee_msat = 10000000 # 10,000 satoshis 36 | 37 | # The minimum number of blocks after confirmation we promise to keep the channel open. 38 | min_channel_lifetime = 4320 # ~30 days 39 | 40 | # The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. 41 | max_client_to_self_delay = 1440 # ~10 days 42 | 43 | # The minimum payment size that we will accept when opening a channel. 44 | min_payment_size_msat = 10000000 # 10,000 satoshis 45 | 46 | # The maximum payment size that we will accept when opening a channel. 47 | max_payment_size_msat = 25000000000 # 0.25 BTC 48 | 49 | # Optional token for clients (uncomment and set if required) 50 | ## A token we may require to be sent by the clients. 51 | ## If set, only requests matching this token will be accepted. (uncomment and set if required) 52 | # require_token = "" 53 | -------------------------------------------------------------------------------- /ldk-server/src/api/bolt11_receive.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::service::Context; 3 | use crate::util::proto_adapter::proto_to_bolt11_description; 4 | use ldk_server_protos::api::{Bolt11ReceiveRequest, Bolt11ReceiveResponse}; 5 | 6 | pub(crate) const BOLT11_RECEIVE_PATH: &str = "Bolt11Receive"; 7 | 8 | pub(crate) fn handle_bolt11_receive_request( 9 | context: Context, request: Bolt11ReceiveRequest, 10 | ) -> Result { 11 | let description = proto_to_bolt11_description(request.description)?; 12 | let invoice = match request.amount_msat { 13 | Some(amount_msat) => { 14 | context.node.bolt11_payment().receive(amount_msat, &description, request.expiry_secs)? 15 | }, 16 | None => context 17 | .node 18 | .bolt11_payment() 19 | .receive_variable_amount(&description, request.expiry_secs)?, 20 | }; 21 | 22 | let response = Bolt11ReceiveResponse { invoice: invoice.to_string() }; 23 | Ok(response) 24 | } 25 | -------------------------------------------------------------------------------- /ldk-server/src/api/bolt11_send.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::service::Context; 3 | use ldk_node::lightning_invoice::Bolt11Invoice; 4 | use ldk_server_protos::api::{Bolt11SendRequest, Bolt11SendResponse}; 5 | use std::str::FromStr; 6 | 7 | pub(crate) const BOLT11_SEND_PATH: &str = "Bolt11Send"; 8 | 9 | pub(crate) fn handle_bolt11_send_request( 10 | context: Context, request: Bolt11SendRequest, 11 | ) -> Result { 12 | let invoice = Bolt11Invoice::from_str(&request.invoice.as_str()) 13 | .map_err(|_| ldk_node::NodeError::InvalidInvoice)?; 14 | 15 | let payment_id = match request.amount_msat { 16 | None => context.node.bolt11_payment().send(&invoice, None), 17 | Some(amount_msat) => { 18 | context.node.bolt11_payment().send_using_amount(&invoice, amount_msat, None) 19 | }, 20 | }?; 21 | 22 | let response = Bolt11SendResponse { payment_id: payment_id.to_string() }; 23 | Ok(response) 24 | } 25 | -------------------------------------------------------------------------------- /ldk-server/src/api/bolt12_receive.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::service::Context; 3 | use ldk_server_protos::api::{Bolt12ReceiveRequest, Bolt12ReceiveResponse}; 4 | 5 | pub(crate) const BOLT12_RECEIVE_PATH: &str = "Bolt12Receive"; 6 | 7 | pub(crate) fn handle_bolt12_receive_request( 8 | context: Context, request: Bolt12ReceiveRequest, 9 | ) -> Result { 10 | let offer = match request.amount_msat { 11 | Some(amount_msat) => context.node.bolt12_payment().receive( 12 | amount_msat, 13 | &request.description, 14 | request.expiry_secs, 15 | request.quantity, 16 | )?, 17 | None => context 18 | .node 19 | .bolt12_payment() 20 | .receive_variable_amount(&request.description, request.expiry_secs)?, 21 | }; 22 | 23 | let response = Bolt12ReceiveResponse { offer: offer.to_string() }; 24 | Ok(response) 25 | } 26 | -------------------------------------------------------------------------------- /ldk-server/src/api/bolt12_send.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::service::Context; 3 | use ldk_node::lightning::offers::offer::Offer; 4 | use ldk_server_protos::api::{Bolt12SendRequest, Bolt12SendResponse}; 5 | use std::str::FromStr; 6 | 7 | pub(crate) const BOLT12_SEND_PATH: &str = "Bolt12Send"; 8 | 9 | pub(crate) fn handle_bolt12_send_request( 10 | context: Context, request: Bolt12SendRequest, 11 | ) -> Result { 12 | let offer = 13 | Offer::from_str(&request.offer.as_str()).map_err(|_| ldk_node::NodeError::InvalidOffer)?; 14 | 15 | let payment_id = match request.amount_msat { 16 | None => context.node.bolt12_payment().send(&offer, request.quantity, request.payer_note), 17 | Some(amount_msat) => context.node.bolt12_payment().send_using_amount( 18 | &offer, 19 | amount_msat, 20 | request.quantity, 21 | request.payer_note, 22 | ), 23 | }?; 24 | 25 | let response = Bolt12SendResponse { payment_id: payment_id.to_string() }; 26 | Ok(response) 27 | } 28 | -------------------------------------------------------------------------------- /ldk-server/src/api/close_channel.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::api::error::LdkServerErrorCode::InvalidRequestError; 3 | use crate::service::Context; 4 | use ldk_node::bitcoin::secp256k1::PublicKey; 5 | use ldk_node::UserChannelId; 6 | use ldk_server_protos::api::{CloseChannelRequest, CloseChannelResponse}; 7 | use std::str::FromStr; 8 | 9 | pub(crate) const CLOSE_CHANNEL_PATH: &str = "CloseChannel"; 10 | 11 | pub(crate) fn handle_close_channel_request( 12 | context: Context, request: CloseChannelRequest, 13 | ) -> Result { 14 | let user_channel_id = 15 | UserChannelId((&request.user_channel_id).parse::().map_err(|_| { 16 | LdkServerError::new(InvalidRequestError, "Invalid UserChannelId.".to_string()) 17 | })?); 18 | let counterparty_node_id = PublicKey::from_str(&request.counterparty_node_id).map_err(|e| { 19 | LdkServerError::new( 20 | InvalidRequestError, 21 | format!("Invalid counterparty node ID, error: {}", e), 22 | ) 23 | })?; 24 | 25 | match request.force_close { 26 | Some(true) => context.node.force_close_channel( 27 | &user_channel_id, 28 | counterparty_node_id, 29 | request.force_close_reason, 30 | )?, 31 | _ => context.node.close_channel(&user_channel_id, counterparty_node_id)?, 32 | }; 33 | 34 | let response = CloseChannelResponse {}; 35 | Ok(response) 36 | } 37 | -------------------------------------------------------------------------------- /ldk-server/src/api/error.rs: -------------------------------------------------------------------------------- 1 | use ldk_node::NodeError; 2 | use std::fmt; 3 | 4 | #[derive(Clone, Debug, PartialEq, Eq)] 5 | pub(crate) struct LdkServerError { 6 | // The error message containing a generic description of the error condition in English. 7 | // It is intended for a human audience only and should not be parsed to extract any information 8 | // programmatically. Client-side code may use it for logging only. 9 | pub(crate) message: String, 10 | 11 | // The error code uniquely identifying an error condition. 12 | // It is meant to be read and understood programmatically by code that detects/handles errors by 13 | // type. 14 | pub(crate) error_code: LdkServerErrorCode, 15 | } 16 | 17 | impl LdkServerError { 18 | pub(crate) fn new(error_code: LdkServerErrorCode, message: impl Into) -> Self { 19 | Self { error_code, message: message.into() } 20 | } 21 | } 22 | 23 | impl std::error::Error for LdkServerError {} 24 | 25 | impl fmt::Display for LdkServerError { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | write!(f, "Error: [{}]: {}", self.error_code, self.message) 28 | } 29 | } 30 | 31 | #[derive(Clone, Debug, PartialEq, Eq)] 32 | pub(crate) enum LdkServerErrorCode { 33 | /// Please refer to [`protos::error::ErrorCode::InvalidRequestError`]. 34 | InvalidRequestError, 35 | 36 | /// Please refer to [`protos::error::ErrorCode::AuthError`]. 37 | AuthError, 38 | 39 | /// Please refer to [`protos::error::ErrorCode::LightningError`]. 40 | LightningError, 41 | 42 | /// Please refer to [`protos::error::ErrorCode::InternalServerError`]. 43 | InternalServerError, 44 | } 45 | 46 | impl fmt::Display for LdkServerErrorCode { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | match self { 49 | LdkServerErrorCode::InvalidRequestError => write!(f, "InvalidRequestError"), 50 | LdkServerErrorCode::AuthError => write!(f, "AuthError"), 51 | LdkServerErrorCode::LightningError => write!(f, "LightningError"), 52 | LdkServerErrorCode::InternalServerError => write!(f, "InternalServerError"), 53 | } 54 | } 55 | } 56 | 57 | impl From for LdkServerError { 58 | fn from(error: NodeError) -> Self { 59 | let (message, error_code) = match error { 60 | NodeError::InvalidAddress 61 | | NodeError::InvalidSocketAddress 62 | | NodeError::InvalidPublicKey 63 | | NodeError::InvalidSecretKey 64 | | NodeError::InvalidOfferId 65 | | NodeError::InvalidNodeId 66 | | NodeError::InvalidPaymentId 67 | | NodeError::InvalidPaymentHash 68 | | NodeError::InvalidPaymentPreimage 69 | | NodeError::InvalidPaymentSecret 70 | | NodeError::InvalidAmount 71 | | NodeError::InvalidInvoice 72 | | NodeError::InvalidOffer 73 | | NodeError::InvalidRefund 74 | | NodeError::InvalidChannelId 75 | | NodeError::InvalidNetwork 76 | | NodeError::InvalidUri 77 | | NodeError::InvalidQuantity 78 | | NodeError::InvalidNodeAlias 79 | | NodeError::InvalidDateTime 80 | | NodeError::InvalidFeeRate 81 | | NodeError::UriParameterParsingFailed => { 82 | (error.to_string(), LdkServerErrorCode::InvalidRequestError) 83 | }, 84 | 85 | NodeError::ConnectionFailed 86 | | NodeError::InvoiceCreationFailed 87 | | NodeError::InvoiceRequestCreationFailed 88 | | NodeError::OfferCreationFailed 89 | | NodeError::RefundCreationFailed 90 | | NodeError::PaymentSendingFailed 91 | | NodeError::InvalidCustomTlvs 92 | | NodeError::ProbeSendingFailed 93 | | NodeError::ChannelCreationFailed 94 | | NodeError::ChannelClosingFailed 95 | | NodeError::ChannelConfigUpdateFailed 96 | | NodeError::DuplicatePayment 97 | | NodeError::InsufficientFunds 98 | | NodeError::UnsupportedCurrency 99 | | NodeError::LiquidityFeeTooHigh => (error.to_string(), LdkServerErrorCode::LightningError), 100 | 101 | NodeError::AlreadyRunning 102 | | NodeError::NotRunning 103 | | NodeError::PersistenceFailed 104 | | NodeError::FeerateEstimationUpdateFailed 105 | | NodeError::FeerateEstimationUpdateTimeout 106 | | NodeError::WalletOperationFailed 107 | | NodeError::WalletOperationTimeout 108 | | NodeError::GossipUpdateFailed 109 | | NodeError::GossipUpdateTimeout 110 | | NodeError::LiquiditySourceUnavailable 111 | | NodeError::LiquidityRequestFailed 112 | | NodeError::OnchainTxCreationFailed 113 | | NodeError::OnchainTxSigningFailed 114 | | NodeError::TxSyncFailed 115 | | NodeError::TxSyncTimeout => (error.to_string(), LdkServerErrorCode::InternalServerError), 116 | }; 117 | LdkServerError::new(error_code, message) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ldk-server/src/api/get_balances.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::service::Context; 3 | use crate::util::proto_adapter::{lightning_balance_to_proto, pending_sweep_balance_to_proto}; 4 | use ldk_server_protos::api::{GetBalancesRequest, GetBalancesResponse}; 5 | 6 | pub(crate) const GET_BALANCES: &str = "GetBalances"; 7 | 8 | pub(crate) fn handle_get_balances_request( 9 | context: Context, _request: GetBalancesRequest, 10 | ) -> Result { 11 | let balance_details = context.node.list_balances(); 12 | 13 | let response = GetBalancesResponse { 14 | total_onchain_balance_sats: balance_details.total_onchain_balance_sats, 15 | spendable_onchain_balance_sats: balance_details.spendable_onchain_balance_sats, 16 | total_anchor_channels_reserve_sats: balance_details.total_anchor_channels_reserve_sats, 17 | total_lightning_balance_sats: balance_details.total_lightning_balance_sats, 18 | lightning_balances: balance_details 19 | .lightning_balances 20 | .into_iter() 21 | .map(lightning_balance_to_proto) 22 | .collect(), 23 | pending_balances_from_channel_closures: balance_details 24 | .pending_balances_from_channel_closures 25 | .into_iter() 26 | .map(pending_sweep_balance_to_proto) 27 | .collect(), 28 | }; 29 | Ok(response) 30 | } 31 | -------------------------------------------------------------------------------- /ldk-server/src/api/get_node_info.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::service::Context; 3 | use ldk_server_protos::api::{GetNodeInfoRequest, GetNodeInfoResponse}; 4 | use ldk_server_protos::types::BestBlock; 5 | 6 | pub(crate) const GET_NODE_INFO: &str = "GetNodeInfo"; 7 | 8 | pub(crate) fn handle_get_node_info_request( 9 | context: Context, _request: GetNodeInfoRequest, 10 | ) -> Result { 11 | let node_status = context.node.status(); 12 | 13 | let best_block = BestBlock { 14 | block_hash: node_status.current_best_block.block_hash.to_string(), 15 | height: node_status.current_best_block.height, 16 | }; 17 | 18 | let response = GetNodeInfoResponse { 19 | node_id: context.node.node_id().to_string(), 20 | current_best_block: Some(best_block), 21 | latest_lightning_wallet_sync_timestamp: node_status.latest_lightning_wallet_sync_timestamp, 22 | latest_onchain_wallet_sync_timestamp: node_status.latest_onchain_wallet_sync_timestamp, 23 | latest_fee_rate_cache_update_timestamp: node_status.latest_fee_rate_cache_update_timestamp, 24 | latest_rgs_snapshot_timestamp: node_status.latest_rgs_snapshot_timestamp, 25 | latest_node_announcement_broadcast_timestamp: node_status 26 | .latest_node_announcement_broadcast_timestamp, 27 | }; 28 | Ok(response) 29 | } 30 | -------------------------------------------------------------------------------- /ldk-server/src/api/get_payment_details.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::api::error::LdkServerErrorCode::InvalidRequestError; 3 | use crate::service::Context; 4 | use crate::util::proto_adapter::payment_to_proto; 5 | use hex::FromHex; 6 | use ldk_node::lightning::ln::channelmanager::PaymentId; 7 | use ldk_server_protos::api::{GetPaymentDetailsRequest, GetPaymentDetailsResponse}; 8 | 9 | pub(crate) const GET_PAYMENT_DETAILS_PATH: &str = "GetPaymentDetails"; 10 | 11 | pub(crate) fn handle_get_payment_details_request( 12 | context: Context, request: GetPaymentDetailsRequest, 13 | ) -> Result { 14 | let payment_id_bytes = 15 | <[u8; PaymentId::LENGTH]>::from_hex(&request.payment_id).map_err(|_| { 16 | LdkServerError::new( 17 | InvalidRequestError, 18 | format!("Invalid payment_id, must be a {}-byte hex-string.", PaymentId::LENGTH), 19 | ) 20 | })?; 21 | 22 | let payment_details = context.node.payment(&PaymentId(payment_id_bytes)); 23 | 24 | let response = GetPaymentDetailsResponse { 25 | payment: payment_details.map(|payment| payment_to_proto(payment)), 26 | }; 27 | 28 | Ok(response) 29 | } 30 | -------------------------------------------------------------------------------- /ldk-server/src/api/list_channels.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::service::Context; 3 | use crate::util::proto_adapter::channel_to_proto; 4 | use ldk_server_protos::api::{ListChannelsRequest, ListChannelsResponse}; 5 | 6 | pub(crate) const LIST_CHANNELS_PATH: &str = "ListChannels"; 7 | 8 | pub(crate) fn handle_list_channels_request( 9 | context: Context, _request: ListChannelsRequest, 10 | ) -> Result { 11 | let channels = context.node.list_channels().into_iter().map(|c| channel_to_proto(c)).collect(); 12 | 13 | let response = ListChannelsResponse { channels }; 14 | Ok(response) 15 | } 16 | -------------------------------------------------------------------------------- /ldk-server/src/api/list_forwarded_payments.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::api::error::LdkServerErrorCode::InternalServerError; 3 | use crate::io::persist::{ 4 | FORWARDED_PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, 5 | FORWARDED_PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, 6 | }; 7 | use crate::service::Context; 8 | use bytes::Bytes; 9 | use ldk_server_protos::api::{ListForwardedPaymentsRequest, ListForwardedPaymentsResponse}; 10 | use ldk_server_protos::types::{ForwardedPayment, PageToken}; 11 | use prost::Message; 12 | 13 | pub(crate) const LIST_FORWARDED_PAYMENTS_PATH: &str = "ListForwardedPayments"; 14 | 15 | pub(crate) fn handle_list_forwarded_payments_request( 16 | context: Context, request: ListForwardedPaymentsRequest, 17 | ) -> Result { 18 | let page_token = request.page_token.map(|p| (p.token, p.index)); 19 | let list_response = context 20 | .paginated_kv_store 21 | .list( 22 | FORWARDED_PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, 23 | FORWARDED_PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, 24 | page_token, 25 | ) 26 | .map_err(|e| { 27 | LdkServerError::new( 28 | InternalServerError, 29 | format!("Failed to list forwarded payments: {}", e), 30 | ) 31 | })?; 32 | 33 | let mut forwarded_payments: Vec = 34 | Vec::with_capacity(list_response.keys.len()); 35 | for key in list_response.keys { 36 | let forwarded_payment_bytes = context 37 | .paginated_kv_store 38 | .read( 39 | FORWARDED_PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, 40 | FORWARDED_PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, 41 | &key, 42 | ) 43 | .map_err(|e| { 44 | LdkServerError::new( 45 | InternalServerError, 46 | format!("Failed to read forwarded payment data: {}", e), 47 | ) 48 | })?; 49 | let forwarded_payment = ForwardedPayment::decode(Bytes::from(forwarded_payment_bytes)) 50 | .map_err(|e| { 51 | LdkServerError::new( 52 | InternalServerError, 53 | format!("Failed to decode forwarded payment: {}", e), 54 | ) 55 | })?; 56 | forwarded_payments.push(forwarded_payment); 57 | } 58 | let response = ListForwardedPaymentsResponse { 59 | forwarded_payments, 60 | next_page_token: list_response 61 | .next_page_token 62 | .map(|(token, index)| PageToken { token, index }), 63 | }; 64 | Ok(response) 65 | } 66 | -------------------------------------------------------------------------------- /ldk-server/src/api/list_payments.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::api::error::LdkServerErrorCode::InternalServerError; 3 | use crate::io::persist::{ 4 | PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, 5 | }; 6 | use crate::service::Context; 7 | use bytes::Bytes; 8 | use ldk_server_protos::api::{ListPaymentsRequest, ListPaymentsResponse}; 9 | use ldk_server_protos::types::{PageToken, Payment}; 10 | use prost::Message; 11 | 12 | pub(crate) const LIST_PAYMENTS_PATH: &str = "ListPayments"; 13 | 14 | pub(crate) fn handle_list_payments_request( 15 | context: Context, request: ListPaymentsRequest, 16 | ) -> Result { 17 | let page_token = request.page_token.map(|p| (p.token, p.index)); 18 | let list_response = context 19 | .paginated_kv_store 20 | .list( 21 | PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, 22 | PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, 23 | page_token, 24 | ) 25 | .map_err(|e| { 26 | LdkServerError::new(InternalServerError, format!("Failed to list payments: {}", e)) 27 | })?; 28 | 29 | let mut payments: Vec = Vec::with_capacity(list_response.keys.len()); 30 | for key in list_response.keys { 31 | let payment_bytes = context 32 | .paginated_kv_store 33 | .read( 34 | PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, 35 | PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, 36 | &key, 37 | ) 38 | .map_err(|e| { 39 | LdkServerError::new( 40 | InternalServerError, 41 | format!("Failed to read payment data: {}", e), 42 | ) 43 | })?; 44 | let payment = Payment::decode(Bytes::from(payment_bytes)).map_err(|e| { 45 | LdkServerError::new(InternalServerError, format!("Failed to decode payment: {}", e)) 46 | })?; 47 | payments.push(payment); 48 | } 49 | let response = ListPaymentsResponse { 50 | payments, 51 | next_page_token: list_response 52 | .next_page_token 53 | .map(|(token, index)| PageToken { token, index }), 54 | }; 55 | Ok(response) 56 | } 57 | -------------------------------------------------------------------------------- /ldk-server/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod bolt11_receive; 2 | pub(crate) mod bolt11_send; 3 | pub(crate) mod bolt12_receive; 4 | pub(crate) mod bolt12_send; 5 | pub(crate) mod close_channel; 6 | pub(crate) mod error; 7 | pub(crate) mod get_balances; 8 | pub(crate) mod get_node_info; 9 | pub(crate) mod get_payment_details; 10 | pub(crate) mod list_channels; 11 | pub(crate) mod list_forwarded_payments; 12 | pub(crate) mod list_payments; 13 | pub(crate) mod onchain_receive; 14 | pub(crate) mod onchain_send; 15 | pub(crate) mod open_channel; 16 | pub(crate) mod update_channel_config; 17 | -------------------------------------------------------------------------------- /ldk-server/src/api/onchain_receive.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::service::Context; 3 | use ldk_server_protos::api::{OnchainReceiveRequest, OnchainReceiveResponse}; 4 | 5 | pub(crate) const ONCHAIN_RECEIVE_PATH: &str = "OnchainReceive"; 6 | pub(crate) fn handle_onchain_receive_request( 7 | context: Context, _request: OnchainReceiveRequest, 8 | ) -> Result { 9 | let response = OnchainReceiveResponse { 10 | address: context.node.onchain_payment().new_address()?.to_string(), 11 | }; 12 | Ok(response) 13 | } 14 | -------------------------------------------------------------------------------- /ldk-server/src/api/onchain_send.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::api::error::LdkServerErrorCode::InvalidRequestError; 3 | use crate::service::Context; 4 | use ldk_node::bitcoin::{Address, FeeRate}; 5 | use ldk_server_protos::api::{OnchainSendRequest, OnchainSendResponse}; 6 | use std::str::FromStr; 7 | 8 | pub(crate) const ONCHAIN_SEND_PATH: &str = "OnchainSend"; 9 | 10 | pub(crate) fn handle_onchain_send_request( 11 | context: Context, request: OnchainSendRequest, 12 | ) -> Result { 13 | let address = Address::from_str(&request.address) 14 | .map_err(|_| ldk_node::NodeError::InvalidAddress)? 15 | .require_network(context.node.config().network) 16 | .map_err(|_| { 17 | LdkServerError::new( 18 | InvalidRequestError, 19 | "Address is not valid for LdkServer's configured network.".to_string(), 20 | ) 21 | })?; 22 | 23 | let fee_rate = request.fee_rate_sat_per_vb.map(FeeRate::from_sat_per_vb).flatten(); 24 | let txid = match (request.amount_sats, request.send_all) { 25 | (Some(amount_sats), None) => { 26 | context.node.onchain_payment().send_to_address(&address, amount_sats, fee_rate)? 27 | }, 28 | // Retain existing api behaviour to not retain reserves on `send_all_to_address`. 29 | (None, Some(true)) => { 30 | context.node.onchain_payment().send_all_to_address(&address, false, fee_rate)? 31 | }, 32 | _ => { 33 | return Err(LdkServerError::new( 34 | InvalidRequestError, 35 | "Must specify either `send_all` or `amount_sats`, but not both or neither", 36 | )) 37 | }, 38 | }; 39 | let response = OnchainSendResponse { txid: txid.to_string() }; 40 | Ok(response) 41 | } 42 | -------------------------------------------------------------------------------- /ldk-server/src/api/open_channel.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::service::Context; 3 | use ldk_node::bitcoin::secp256k1::PublicKey; 4 | use ldk_node::lightning::ln::msgs::SocketAddress; 5 | use ldk_server_protos::api::{OpenChannelRequest, OpenChannelResponse}; 6 | use std::str::FromStr; 7 | 8 | pub(crate) const OPEN_CHANNEL_PATH: &str = "OpenChannel"; 9 | 10 | pub(crate) fn handle_open_channel( 11 | context: Context, request: OpenChannelRequest, 12 | ) -> Result { 13 | let node_id = PublicKey::from_str(&request.node_pubkey) 14 | .map_err(|_| ldk_node::NodeError::InvalidPublicKey)?; 15 | let address = SocketAddress::from_str(&request.address) 16 | .map_err(|_| ldk_node::NodeError::InvalidSocketAddress)?; 17 | 18 | let user_channel_id = if request.announce_channel { 19 | context.node.open_announced_channel( 20 | node_id, 21 | address, 22 | request.channel_amount_sats, 23 | request.push_to_counterparty_msat, 24 | // TODO: Allow setting ChannelConfig in open-channel. 25 | None, 26 | )? 27 | } else { 28 | context.node.open_channel( 29 | node_id, 30 | address, 31 | request.channel_amount_sats, 32 | request.push_to_counterparty_msat, 33 | None, 34 | )? 35 | }; 36 | 37 | let response = OpenChannelResponse { user_channel_id: user_channel_id.0.to_string() }; 38 | Ok(response) 39 | } 40 | -------------------------------------------------------------------------------- /ldk-server/src/api/update_channel_config.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::api::error::LdkServerErrorCode::{InvalidRequestError, LightningError}; 3 | use crate::service::Context; 4 | use ldk_node::bitcoin::secp256k1::PublicKey; 5 | use ldk_node::config::{ChannelConfig, MaxDustHTLCExposure}; 6 | use ldk_node::UserChannelId; 7 | use ldk_server_protos::api::{UpdateChannelConfigRequest, UpdateChannelConfigResponse}; 8 | use ldk_server_protos::types::channel_config::MaxDustHtlcExposure; 9 | use std::str::FromStr; 10 | 11 | pub(crate) const UPDATE_CHANNEL_CONFIG_PATH: &str = "UpdateChannelConfig"; 12 | 13 | pub(crate) fn handle_update_channel_config_request( 14 | context: Context, request: UpdateChannelConfigRequest, 15 | ) -> Result { 16 | let user_channel_id: u128 = request 17 | .user_channel_id 18 | .parse::() 19 | .map_err(|_| LdkServerError::new(InvalidRequestError, "Invalid UserChannelId."))?; 20 | 21 | //FIXME: Use ldk/ldk-node's partial config update api. 22 | let current_config = context 23 | .node 24 | .list_channels() 25 | .into_iter() 26 | .find(|c| c.user_channel_id.0 == user_channel_id) 27 | .ok_or_else(|| { 28 | LdkServerError::new(InvalidRequestError, "Channel not found for given user_channel_id.") 29 | })? 30 | .config; 31 | 32 | let updated_channel_config = build_updated_channel_config( 33 | current_config, 34 | request.channel_config.ok_or_else(|| { 35 | LdkServerError::new(InvalidRequestError, "Channel config must be provided.") 36 | })?, 37 | )?; 38 | 39 | let counterparty_node_id = PublicKey::from_str(&request.counterparty_node_id).map_err(|e| { 40 | LdkServerError::new( 41 | InvalidRequestError, 42 | format!("Invalid counterparty node id, error {}", e), 43 | ) 44 | })?; 45 | 46 | context 47 | .node 48 | .update_channel_config( 49 | &UserChannelId(user_channel_id), 50 | counterparty_node_id, 51 | updated_channel_config, 52 | ) 53 | .map_err(|e| { 54 | LdkServerError::new(LightningError, format!("Failed to update channel config: {}", e)) 55 | })?; 56 | 57 | Ok(UpdateChannelConfigResponse {}) 58 | } 59 | 60 | fn build_updated_channel_config( 61 | current_config: ChannelConfig, proto_channel_config: ldk_server_protos::types::ChannelConfig, 62 | ) -> Result { 63 | let max_dust_htlc_exposure = proto_channel_config 64 | .max_dust_htlc_exposure 65 | .map(|max_dust_htlc_exposure| match max_dust_htlc_exposure { 66 | MaxDustHtlcExposure::FixedLimitMsat(limit_msat) => { 67 | MaxDustHTLCExposure::FixedLimit { limit_msat } 68 | }, 69 | MaxDustHtlcExposure::FeeRateMultiplier(multiplier) => { 70 | MaxDustHTLCExposure::FeeRateMultiplier { multiplier } 71 | }, 72 | }) 73 | .unwrap_or(current_config.max_dust_htlc_exposure); 74 | 75 | let cltv_expiry_delta = match proto_channel_config.cltv_expiry_delta { 76 | Some(c) => Some(u16::try_from(c).map_err(|_| { 77 | LdkServerError::new( 78 | InvalidRequestError, 79 | format!("Invalid cltv_expiry_delta, must be between 0 and {}", u16::MAX), 80 | ) 81 | })?), 82 | None => None, 83 | } 84 | .unwrap_or_else(|| current_config.cltv_expiry_delta); 85 | 86 | Ok(ChannelConfig { 87 | forwarding_fee_proportional_millionths: proto_channel_config 88 | .forwarding_fee_proportional_millionths 89 | .unwrap_or(current_config.forwarding_fee_proportional_millionths), 90 | forwarding_fee_base_msat: proto_channel_config 91 | .forwarding_fee_base_msat 92 | .unwrap_or(current_config.forwarding_fee_base_msat), 93 | cltv_expiry_delta, 94 | max_dust_htlc_exposure, 95 | force_close_avoidance_max_fee_satoshis: proto_channel_config 96 | .force_close_avoidance_max_fee_satoshis 97 | .unwrap_or(current_config.force_close_avoidance_max_fee_satoshis), 98 | accept_underpaying_htlcs: proto_channel_config 99 | .accept_underpaying_htlcs 100 | .unwrap_or(current_config.accept_underpaying_htlcs), 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /ldk-server/src/io/events/event_publisher.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use async_trait::async_trait; 3 | use ldk_server_protos::events::EventEnvelope; 4 | 5 | /// A trait for publishing events or notifications from the LDK Server. 6 | /// 7 | /// Implementors of this trait define how events are sent to various messaging 8 | /// systems. It provides a consistent, asynchronous interface for event publishing, while allowing 9 | /// each implementation to manage its own initialization and configuration, typically sourced from 10 | /// the `ldk-server.config` file. A no-op implementation is included by default, 11 | /// with specific implementations enabled via feature flags. 12 | /// 13 | /// Events are represented as [`EventEnvelope`] messages, which are Protocol Buffers 14 | /// ([protobuf](https://protobuf.dev/)) objects defined in [`ldk_server_protos::events`]. 15 | /// These events are serialized to bytes by the publisher before transmission, and consumers can 16 | /// deserialize them using the protobuf definitions. 17 | /// 18 | /// The underlying messaging system is expected to support durably buffered events, 19 | /// enabling easy decoupling between the LDK Server and event consumers. 20 | #[async_trait] 21 | pub trait EventPublisher: Send + Sync { 22 | /// Publishes an event to the underlying messaging system. 23 | /// 24 | /// # Arguments 25 | /// * `event` - The event message to publish, provided as an [`EventEnvelope`] 26 | /// defined in [`ldk_server_protos::events`]. Implementors must serialize 27 | /// the whole [`EventEnvelope`] to bytes before publishing. 28 | /// 29 | /// In order to ensure no events are lost, implementors of this trait must publish events 30 | /// durably to underlying messaging system. An event is considered published when 31 | /// [`EventPublisher::publish`] returns `Ok(())`, thus implementors MUST durably persist/publish events *before* 32 | /// returning `Ok(())`. 33 | /// 34 | /// # Errors 35 | /// May return an [`LdkServerErrorCode::InternalServerError`] if the event cannot be published, 36 | /// such as due to network failures, misconfiguration, or transport-specific issues. 37 | /// If event publishing fails, the LDK Server will retry publishing the event indefinitely, which 38 | /// may degrade performance until the underlying messaging system is operational again. 39 | /// 40 | /// [`LdkServerErrorCode::InternalServerError`]: crate::api::error::LdkServerErrorCode 41 | async fn publish(&self, event: EventEnvelope) -> Result<(), LdkServerError>; 42 | } 43 | 44 | pub(crate) struct NoopEventPublisher; 45 | 46 | #[async_trait] 47 | impl EventPublisher for NoopEventPublisher { 48 | /// Publishes an event to a no-op sink, effectively discarding it. 49 | /// 50 | /// This implementation does nothing and always returns `Ok(())`, serving as a 51 | /// default when no messaging system is configured. 52 | async fn publish(&self, _event: EventEnvelope) -> Result<(), LdkServerError> { 53 | Ok(()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ldk-server/src/io/events/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod event_publisher; 2 | 3 | #[cfg(feature = "events-rabbitmq")] 4 | pub(crate) mod rabbitmq; 5 | 6 | use ldk_server_protos::events::event_envelope; 7 | 8 | /// Event variant to event name mapping. 9 | pub(crate) fn get_event_name(event: &event_envelope::Event) -> &'static str { 10 | match event { 11 | event_envelope::Event::PaymentReceived(_) => "PaymentReceived", 12 | event_envelope::Event::PaymentSuccessful(_) => "PaymentSuccessful", 13 | event_envelope::Event::PaymentFailed(_) => "PaymentFailed", 14 | event_envelope::Event::PaymentForwarded(_) => "PaymentForwarded", 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ldk-server/src/io/events/rabbitmq/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::api::error::LdkServerErrorCode::InternalServerError; 3 | use crate::io::events::event_publisher::EventPublisher; 4 | use ::prost::Message; 5 | use async_trait::async_trait; 6 | use lapin::options::{BasicPublishOptions, ConfirmSelectOptions, ExchangeDeclareOptions}; 7 | use lapin::types::FieldTable; 8 | use lapin::{ 9 | BasicProperties, Channel, Connection, ConnectionProperties, ConnectionState, ExchangeKind, 10 | }; 11 | use ldk_server_protos::events::EventEnvelope; 12 | use std::sync::Arc; 13 | use tokio::sync::Mutex; 14 | 15 | /// A RabbitMQ-based implementation of the EventPublisher trait. 16 | pub struct RabbitMqEventPublisher { 17 | /// The RabbitMQ connection, used for reconnection logic. 18 | connection: Arc>>, 19 | /// The RabbitMQ channel used for publishing events. 20 | channel: Arc>>, 21 | /// Configuration details, including connection string and exchange name. 22 | config: RabbitMqConfig, 23 | } 24 | 25 | /// Configuration for the RabbitMQ event publisher. 26 | #[derive(Debug, Clone)] 27 | pub struct RabbitMqConfig { 28 | pub connection_string: String, 29 | pub exchange_name: String, 30 | } 31 | 32 | /// Delivery mode for persistent messages (written to disk). 33 | const DELIVERY_MODE_PERSISTENT: u8 = 2; 34 | 35 | impl RabbitMqEventPublisher { 36 | /// Creates a new RabbitMqEventPublisher instance. 37 | pub fn new(config: RabbitMqConfig) -> Self { 38 | Self { connection: Arc::new(Mutex::new(None)), channel: Arc::new(Mutex::new(None)), config } 39 | } 40 | 41 | async fn connect(config: &RabbitMqConfig) -> Result<(Connection, Channel), LdkServerError> { 42 | let conn = Connection::connect(&config.connection_string, ConnectionProperties::default()) 43 | .await 44 | .map_err(|e| { 45 | LdkServerError::new( 46 | InternalServerError, 47 | format!("Failed to connect to RabbitMQ: {}", e), 48 | ) 49 | })?; 50 | 51 | let channel = conn.create_channel().await.map_err(|e| { 52 | LdkServerError::new(InternalServerError, format!("Failed to create channel: {}", e)) 53 | })?; 54 | 55 | channel.confirm_select(ConfirmSelectOptions::default()).await.map_err(|e| { 56 | LdkServerError::new(InternalServerError, format!("Failed to enable confirms: {}", e)) 57 | })?; 58 | 59 | channel 60 | .exchange_declare( 61 | &config.exchange_name, 62 | ExchangeKind::Fanout, 63 | ExchangeDeclareOptions { durable: true, ..Default::default() }, 64 | FieldTable::default(), 65 | ) 66 | .await 67 | .map_err(|e| { 68 | LdkServerError::new( 69 | InternalServerError, 70 | format!("Failed to declare exchange: {}", e), 71 | ) 72 | })?; 73 | 74 | Ok((conn, channel)) 75 | } 76 | 77 | async fn ensure_connected(&self) -> Result<(), LdkServerError> { 78 | { 79 | let connection = self.connection.lock().await; 80 | if let Some(connection) = &*connection { 81 | if connection.status().state() == ConnectionState::Connected { 82 | return Ok(()); 83 | } 84 | } 85 | } 86 | 87 | // Connection is not alive, attempt reconnecting. 88 | let (connection, channel) = Self::connect(&self.config) 89 | .await 90 | .map_err(|e| LdkServerError::new(InternalServerError, e.to_string()))?; 91 | *self.connection.lock().await = Some(connection); 92 | *self.channel.lock().await = Some(channel); 93 | Ok(()) 94 | } 95 | } 96 | 97 | #[async_trait] 98 | impl EventPublisher for RabbitMqEventPublisher { 99 | /// Publishes an event to RabbitMQ. 100 | /// 101 | /// The event is published to a fanout exchange with persistent delivery mode, 102 | /// and the method waits for confirmation from RabbitMQ to ensure durability. 103 | async fn publish(&self, event: EventEnvelope) -> Result<(), LdkServerError> { 104 | // Ensure connection is alive before proceeding 105 | self.ensure_connected().await?; 106 | 107 | let channel_guard = self.channel.lock().await; 108 | let channel = channel_guard.as_ref().ok_or_else(|| { 109 | LdkServerError::new(InternalServerError, "Channel not initialized".to_string()) 110 | })?; 111 | 112 | // Publish the event with persistent delivery mode 113 | let confirm = channel 114 | .basic_publish( 115 | &self.config.exchange_name, 116 | "", // Empty routing key should be used for fanout exchange, since it is ignored. 117 | BasicPublishOptions::default(), 118 | &event.encode_to_vec(), 119 | BasicProperties::default().with_delivery_mode(DELIVERY_MODE_PERSISTENT), 120 | ) 121 | .await 122 | .map_err(|e| { 123 | LdkServerError::new( 124 | InternalServerError, 125 | format!("Failed to publish event, error: {}", e), 126 | ) 127 | })?; 128 | 129 | let confirmation = confirm.await.map_err(|e| { 130 | LdkServerError::new(InternalServerError, format!("Failed to get confirmation: {}", e)) 131 | })?; 132 | 133 | match confirmation { 134 | lapin::publisher_confirm::Confirmation::Ack(_) => Ok(()), 135 | lapin::publisher_confirm::Confirmation::Nack(_) => Err(LdkServerError::new( 136 | InternalServerError, 137 | "Message not acknowledged".to_string(), 138 | )), 139 | _ => { 140 | Err(LdkServerError::new(InternalServerError, "Unexpected confirmation".to_string())) 141 | }, 142 | } 143 | } 144 | } 145 | 146 | #[cfg(test)] 147 | #[cfg(feature = "integration-tests-events-rabbitmq")] 148 | mod integration_tests_events_rabbitmq { 149 | use super::*; 150 | use lapin::{ 151 | options::{BasicAckOptions, BasicConsumeOptions, QueueBindOptions, QueueDeclareOptions}, 152 | types::FieldTable, 153 | Channel, Connection, 154 | }; 155 | use ldk_server_protos::events::event_envelope::Event; 156 | use ldk_server_protos::events::PaymentForwarded; 157 | use std::io; 158 | use std::time::Duration; 159 | use tokio; 160 | 161 | use futures_util::stream::StreamExt; 162 | #[tokio::test] 163 | async fn test_publish_and_consume_event() { 164 | let config = RabbitMqConfig { 165 | connection_string: "amqp://guest:guest@localhost:5672/%2f".to_string(), 166 | exchange_name: "test_exchange".to_string(), 167 | }; 168 | 169 | let publisher = RabbitMqEventPublisher::new(config.clone()); 170 | 171 | let conn = Connection::connect(&config.connection_string, ConnectionProperties::default()) 172 | .await 173 | .expect("Failed make rabbitmq connection"); 174 | let channel = conn.create_channel().await.expect("Failed to create rabbitmq channel"); 175 | 176 | let queue_name = "test_queue"; 177 | setup_queue(&queue_name, &channel, &config).await; 178 | 179 | let event = 180 | EventEnvelope { event: Some(Event::PaymentForwarded(PaymentForwarded::default())) }; 181 | publisher.publish(event.clone()).await.expect("Failed to publish event"); 182 | 183 | consume_event(&queue_name, &channel, &event).await.expect("Failed to consume event"); 184 | } 185 | 186 | async fn setup_queue(queue_name: &str, channel: &Channel, config: &RabbitMqConfig) { 187 | channel 188 | .queue_declare(queue_name, QueueDeclareOptions::default(), FieldTable::default()) 189 | .await 190 | .unwrap(); 191 | channel 192 | .exchange_declare( 193 | &config.exchange_name, 194 | ExchangeKind::Fanout, 195 | ExchangeDeclareOptions { durable: true, ..Default::default() }, 196 | FieldTable::default(), 197 | ) 198 | .await 199 | .unwrap(); 200 | 201 | channel 202 | .queue_bind( 203 | queue_name, 204 | &config.exchange_name, 205 | "", 206 | QueueBindOptions::default(), 207 | FieldTable::default(), 208 | ) 209 | .await 210 | .unwrap(); 211 | } 212 | 213 | async fn consume_event( 214 | queue_name: &str, channel: &Channel, expected_event: &EventEnvelope, 215 | ) -> io::Result<()> { 216 | let mut consumer = channel 217 | .basic_consume( 218 | queue_name, 219 | "test_consumer", 220 | BasicConsumeOptions::default(), 221 | FieldTable::default(), 222 | ) 223 | .await 224 | .unwrap(); 225 | let delivery = 226 | tokio::time::timeout(Duration::from_secs(10), consumer.next()).await?.unwrap().unwrap(); 227 | let received_event = EventEnvelope::decode(&*delivery.data)?; 228 | assert_eq!(received_event, *expected_event, "Event mismatch"); 229 | channel.basic_ack(delivery.delivery_tag, BasicAckOptions::default()).await.unwrap(); 230 | Ok(()) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /ldk-server/src/io/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod events; 2 | pub(crate) mod persist; 3 | pub(crate) mod utils; 4 | -------------------------------------------------------------------------------- /ldk-server/src/io/persist/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod paginated_kv_store; 2 | pub(crate) mod sqlite_store; 3 | 4 | /// The forwarded payments will be persisted under this prefix. 5 | pub(crate) const FORWARDED_PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE: &str = "forwarded_payments"; 6 | pub(crate) const FORWARDED_PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; 7 | 8 | /// The payments will be persisted under this prefix. 9 | pub(crate) const PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE: &str = "payments"; 10 | pub(crate) const PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; 11 | -------------------------------------------------------------------------------- /ldk-server/src/io/persist/paginated_kv_store.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | /// Provides an interface that allows storage and retrieval of persisted values that are associated 4 | /// with given keys, with support for pagination with time-based ordering. 5 | /// 6 | /// In order to avoid collisions, the key space is segmented based on the given `primary_namespace`s 7 | /// and `secondary_namespace`s. Implementations of this trait are free to handle them in different 8 | /// ways, as long as per-namespace key uniqueness is asserted. 9 | /// 10 | /// Keys and namespaces are required to be valid ASCII strings in the range of 11 | /// [`KVSTORE_NAMESPACE_KEY_ALPHABET`] and no longer than [`KVSTORE_NAMESPACE_KEY_MAX_LEN`]. Empty 12 | /// primary namespaces and secondary namespaces (`""`) are considered valid; however, if 13 | /// `primary_namespace` is empty, `secondary_namespace` must also be empty. This means that concerns 14 | /// should always be separated by primary namespace first, before secondary namespaces are used. 15 | /// While the number of primary namespaces will be relatively small and determined at compile time, 16 | /// there may be many secondary namespaces per primary namespace. Note that per-namespace uniqueness 17 | /// needs to also hold for keys *and* namespaces in any given namespace, i.e., conflicts between keys 18 | /// and equally named primary or secondary namespaces must be avoided. 19 | /// 20 | /// **Note:** This trait extends the functionality of [`KVStore`] by adding support for 21 | /// paginated listing of keys based on a monotonic counter or logical timestamp. This is useful 22 | /// when dealing with a large number of keys that cannot be efficiently retrieved all at once. 23 | /// 24 | /// See also [`KVStore`]. 25 | /// 26 | /// [`KVStore`]: ldk_node::lightning::util::persist::KVStore 27 | /// [`KVSTORE_NAMESPACE_KEY_ALPHABET`]: ldk_node::lightning::util::persist::KVSTORE_NAMESPACE_KEY_ALPHABET 28 | /// [`KVSTORE_NAMESPACE_KEY_MAX_LEN`]: ldk_node::lightning::util::persist::KVSTORE_NAMESPACE_KEY_MAX_LEN 29 | pub trait PaginatedKVStore: Send + Sync { 30 | /// Returns the data stored for the given `primary_namespace`, `secondary_namespace`, and `key`. 31 | /// 32 | /// Returns an [`ErrorKind::NotFound`] if the given `key` could not be found in the given 33 | /// `primary_namespace` and `secondary_namespace`. 34 | /// 35 | /// [`ErrorKind::NotFound`]: io::ErrorKind::NotFound 36 | fn read( 37 | &self, primary_namespace: &str, secondary_namespace: &str, key: &str, 38 | ) -> Result, io::Error>; 39 | 40 | /// Persists the given data under the given `key` with an associated `time`. 41 | /// 42 | /// The `time` parameter is a `i64` representing a monotonic counter or logical timestamp. 43 | /// It is used to track the order of keys for list operations. Implementations should store the 44 | /// `time` value and use it for ordering in the `list` method. 45 | /// 46 | /// Will create the given `primary_namespace` and `secondary_namespace` if not already present 47 | /// in the store. 48 | fn write( 49 | &self, primary_namespace: &str, secondary_namespace: &str, key: &str, time: i64, buf: &[u8], 50 | ) -> Result<(), io::Error>; 51 | 52 | /// Removes any data that had previously been persisted under the given `key`. 53 | /// 54 | /// If the `lazy` flag is set to `true`, the backend implementation might choose to lazily 55 | /// remove the given `key` at some point in time after the method returns, e.g., as part of an 56 | /// eventual batch deletion of multiple keys. As a consequence, subsequent calls to 57 | /// [`PaginatedKVStore::list`] might include the removed key until the changes are actually persisted. 58 | /// 59 | /// Note that while setting the `lazy` flag reduces the I/O burden of multiple subsequent 60 | /// `remove` calls, it also influences the atomicity guarantees as lazy `remove`s could 61 | /// potentially get lost on crash after the method returns. Therefore, this flag should only be 62 | /// set for `remove` operations that can be safely replayed at a later time. 63 | /// 64 | /// Returns successfully if no data will be stored for the given `primary_namespace`, 65 | /// `secondary_namespace`, and `key`, independently of whether it was present before its 66 | /// invocation or not. 67 | fn remove( 68 | &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, 69 | ) -> Result<(), io::Error>; 70 | 71 | /// Returns a paginated list of keys that are stored under the given `secondary_namespace` in 72 | /// `primary_namespace`, ordered in descending order of `time`. 73 | /// 74 | /// The `list` method returns the latest records first, based on the `time` associated with each key. 75 | /// Pagination is controlled by the `next_page_token`, which is an `Option<(String, i64)>` 76 | /// used to determine the starting point for the next page of results. If `next_page_token` is `None`, 77 | /// the listing starts from the most recent entry. The `next_page_token` in the returned 78 | /// [`ListResponse`] can be used to fetch the next page of results. 79 | /// 80 | /// Implementations should ensure that keys are returned in descending order of `time` and that 81 | /// pagination tokens are correctly managed. 82 | /// 83 | /// Returns an empty list if `primary_namespace` or `secondary_namespace` is unknown or if 84 | /// there are no more keys to return. 85 | /// 86 | /// [`ListResponse`]: struct.ListResponse.html 87 | fn list( 88 | &self, primary_namespace: &str, secondary_namespace: &str, 89 | next_page_token: Option<(String, i64)>, 90 | ) -> Result; 91 | } 92 | 93 | /// Represents the response from a paginated `list` operation. 94 | /// 95 | /// Contains the list of keys and an optional `next_page_token` that can be used to retrieve the 96 | /// next set of keys. 97 | pub struct ListResponse { 98 | /// A vector of keys, ordered in descending order of `time`. 99 | pub keys: Vec, 100 | 101 | /// A token that can be used to retrieve the next set of keys. 102 | pub next_page_token: Option<(String, i64)>, 103 | } 104 | -------------------------------------------------------------------------------- /ldk-server/src/io/persist/sqlite_store/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::io::persist::paginated_kv_store::{ListResponse, PaginatedKVStore}; 2 | use crate::io::utils::check_namespace_key_validity; 3 | use ldk_node::lightning::types::string::PrintableString; 4 | use rusqlite::{named_params, Connection}; 5 | use std::path::PathBuf; 6 | use std::sync::{Arc, Mutex}; 7 | use std::{fs, io}; 8 | 9 | /// The default database file name. 10 | pub const DEFAULT_SQLITE_DB_FILE_NAME: &str = "ldk_server_data.sqlite"; 11 | 12 | /// The default table in which we store all the paginated data. 13 | pub const DEFAULT_PAGINATED_KV_TABLE_NAME: &str = "ldk_paginated_data"; 14 | 15 | // The current SQLite `user_version`, which we can use if we'd ever need to do a schema migration. 16 | const SCHEMA_USER_VERSION: u16 = 1; 17 | 18 | // The maximum number of keys retrieved per page in paginated list operation. 19 | const LIST_KEYS_MAX_PAGE_SIZE: i32 = 100; 20 | 21 | pub struct SqliteStore { 22 | connection: Arc>, 23 | data_dir: PathBuf, 24 | paginated_kv_table_name: String, 25 | } 26 | 27 | impl SqliteStore { 28 | /// Constructs a new [`SqliteStore`]. 29 | /// 30 | /// If not already existing, a new SQLite database will be created in the given `data_dir` under the 31 | /// given `db_file_name` (or the default to [`DEFAULT_SQLITE_DB_FILE_NAME`] if set to `None`). 32 | /// 33 | /// Similarly, the given `paginated_kv_table_name` will be used or default to [`DEFAULT_PAGINATED_KV_TABLE_NAME`]. 34 | pub fn new( 35 | data_dir: PathBuf, db_file_name: Option, paginated_kv_table_name: Option, 36 | ) -> io::Result { 37 | let db_file_name = db_file_name.unwrap_or(DEFAULT_SQLITE_DB_FILE_NAME.to_string()); 38 | let paginated_kv_table_name = 39 | paginated_kv_table_name.unwrap_or(DEFAULT_PAGINATED_KV_TABLE_NAME.to_string()); 40 | 41 | fs::create_dir_all(data_dir.clone()).map_err(|e| { 42 | let msg = format!( 43 | "Failed to create database destination directory {}: {}", 44 | data_dir.display(), 45 | e 46 | ); 47 | io::Error::new(io::ErrorKind::Other, msg) 48 | })?; 49 | let mut db_file_path = data_dir.clone(); 50 | db_file_path.push(db_file_name); 51 | 52 | let connection = Connection::open(db_file_path.clone()).map_err(|e| { 53 | let msg = 54 | format!("Failed to open/create database file {}: {}", db_file_path.display(), e); 55 | io::Error::new(io::ErrorKind::Other, msg) 56 | })?; 57 | 58 | let sql = format!("SELECT user_version FROM pragma_user_version"); 59 | let version_res: u16 = connection.query_row(&sql, [], |row| row.get(0)).unwrap(); 60 | 61 | if version_res == 0 { 62 | // New database, set our SCHEMA_USER_VERSION and continue 63 | connection 64 | .pragma( 65 | Some(rusqlite::DatabaseName::Main), 66 | "user_version", 67 | SCHEMA_USER_VERSION, 68 | |_| Ok(()), 69 | ) 70 | .map_err(|e| { 71 | let msg = format!("Failed to set PRAGMA user_version: {}", e); 72 | io::Error::new(io::ErrorKind::Other, msg) 73 | })?; 74 | } else if version_res > SCHEMA_USER_VERSION { 75 | let msg = format!( 76 | "Failed to open database: incompatible schema version {}. Expected: {}", 77 | version_res, SCHEMA_USER_VERSION 78 | ); 79 | return Err(io::Error::new(io::ErrorKind::Other, msg)); 80 | } 81 | 82 | let create_paginated_kv_table_sql = format!( 83 | "CREATE TABLE IF NOT EXISTS {} ( 84 | primary_namespace TEXT NOT NULL, 85 | secondary_namespace TEXT DEFAULT \"\" NOT NULL, 86 | key TEXT NOT NULL CHECK (key <> ''), 87 | creation_time INTEGER NOT NULL, 88 | value BLOB, PRIMARY KEY ( primary_namespace, secondary_namespace, key ) 89 | );", 90 | paginated_kv_table_name 91 | ); 92 | 93 | connection.execute(&create_paginated_kv_table_sql, []).map_err(|e| { 94 | let msg = format!("Failed to create table {}: {}", paginated_kv_table_name, e); 95 | io::Error::new(io::ErrorKind::Other, msg) 96 | })?; 97 | 98 | let index_creation_time_sql = format!( 99 | "CREATE INDEX IF NOT EXISTS idx_creation_time ON {} (creation_time);", 100 | paginated_kv_table_name 101 | ); 102 | 103 | connection.execute(&index_creation_time_sql, []).map_err(|e| { 104 | let msg = format!( 105 | "Failed to create index on creation_time, table {}: {}", 106 | paginated_kv_table_name, e 107 | ); 108 | io::Error::new(io::ErrorKind::Other, msg) 109 | })?; 110 | 111 | let connection = Arc::new(Mutex::new(connection)); 112 | Ok(Self { connection, data_dir, paginated_kv_table_name }) 113 | } 114 | 115 | /// Returns the data directory. 116 | pub fn get_data_dir(&self) -> PathBuf { 117 | self.data_dir.clone() 118 | } 119 | 120 | fn read_internal( 121 | &self, kv_table_name: &str, primary_namespace: &str, secondary_namespace: &str, key: &str, 122 | ) -> io::Result> { 123 | check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "read")?; 124 | 125 | let locked_conn = self.connection.lock().unwrap(); 126 | let sql = 127 | format!("SELECT value FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace AND key=:key;", 128 | kv_table_name); 129 | 130 | let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { 131 | let msg = format!("Failed to prepare statement: {}", e); 132 | io::Error::new(io::ErrorKind::Other, msg) 133 | })?; 134 | 135 | let res = stmt 136 | .query_row( 137 | named_params! { 138 | ":primary_namespace": primary_namespace, 139 | ":secondary_namespace": secondary_namespace, 140 | ":key": key, 141 | }, 142 | |row| row.get(0), 143 | ) 144 | .map_err(|e| match e { 145 | rusqlite::Error::QueryReturnedNoRows => { 146 | let msg = format!( 147 | "Failed to read as key could not be found: {}/{}/{}", 148 | PrintableString(primary_namespace), 149 | PrintableString(secondary_namespace), 150 | PrintableString(key) 151 | ); 152 | io::Error::new(io::ErrorKind::NotFound, msg) 153 | }, 154 | e => { 155 | let msg = format!( 156 | "Failed to read from key {}/{}/{}: {}", 157 | PrintableString(primary_namespace), 158 | PrintableString(secondary_namespace), 159 | PrintableString(key), 160 | e 161 | ); 162 | io::Error::new(io::ErrorKind::Other, msg) 163 | }, 164 | })?; 165 | Ok(res) 166 | } 167 | 168 | fn remove_internal( 169 | &self, kv_table_name: &str, primary_namespace: &str, secondary_namespace: &str, key: &str, 170 | ) -> io::Result<()> { 171 | check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?; 172 | 173 | let locked_conn = self.connection.lock().unwrap(); 174 | 175 | let sql = format!("DELETE FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace AND key=:key;", kv_table_name); 176 | 177 | let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { 178 | let msg = format!("Failed to prepare statement: {}", e); 179 | io::Error::new(io::ErrorKind::Other, msg) 180 | })?; 181 | 182 | stmt.execute(named_params! { 183 | ":primary_namespace": primary_namespace, 184 | ":secondary_namespace": secondary_namespace, 185 | ":key": key, 186 | }) 187 | .map_err(|e| { 188 | let msg = format!( 189 | "Failed to delete key {}/{}/{}: {}", 190 | PrintableString(primary_namespace), 191 | PrintableString(secondary_namespace), 192 | PrintableString(key), 193 | e 194 | ); 195 | io::Error::new(io::ErrorKind::Other, msg) 196 | })?; 197 | Ok(()) 198 | } 199 | } 200 | 201 | impl PaginatedKVStore for SqliteStore { 202 | fn read( 203 | &self, primary_namespace: &str, secondary_namespace: &str, key: &str, 204 | ) -> io::Result> { 205 | self.read_internal( 206 | &self.paginated_kv_table_name, 207 | primary_namespace, 208 | secondary_namespace, 209 | key, 210 | ) 211 | } 212 | 213 | fn write( 214 | &self, primary_namespace: &str, secondary_namespace: &str, key: &str, time: i64, buf: &[u8], 215 | ) -> io::Result<()> { 216 | check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?; 217 | 218 | let locked_conn = self.connection.lock().unwrap(); 219 | 220 | let sql = format!( 221 | "INSERT INTO {} (primary_namespace, secondary_namespace, key, creation_time, value) 222 | VALUES (:primary_namespace, :secondary_namespace, :key, :creation_time, :value) 223 | ON CONFLICT(primary_namespace, secondary_namespace, key) 224 | DO UPDATE SET value = excluded.value;", 225 | self.paginated_kv_table_name 226 | ); 227 | 228 | let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { 229 | let msg = format!("Failed to prepare statement: {}", e); 230 | io::Error::new(io::ErrorKind::Other, msg) 231 | })?; 232 | 233 | stmt.execute(named_params! { 234 | ":primary_namespace": primary_namespace, 235 | ":secondary_namespace": secondary_namespace, 236 | ":key": key, 237 | ":creation_time": time, 238 | ":value": buf, 239 | }) 240 | .map(|_| ()) 241 | .map_err(|e| { 242 | let msg = format!( 243 | "Failed to write to key {}/{}/{}: {}", 244 | PrintableString(primary_namespace), 245 | PrintableString(secondary_namespace), 246 | PrintableString(key), 247 | e 248 | ); 249 | io::Error::new(io::ErrorKind::Other, msg) 250 | }) 251 | } 252 | 253 | fn remove( 254 | &self, primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, 255 | ) -> io::Result<()> { 256 | self.remove_internal( 257 | &self.paginated_kv_table_name, 258 | primary_namespace, 259 | secondary_namespace, 260 | key, 261 | ) 262 | } 263 | 264 | fn list( 265 | &self, primary_namespace: &str, secondary_namespace: &str, 266 | page_token: Option<(String, i64)>, 267 | ) -> io::Result { 268 | check_namespace_key_validity(primary_namespace, secondary_namespace, None, "list")?; 269 | 270 | let locked_conn = self.connection.lock().unwrap(); 271 | 272 | let sql = format!( 273 | "SELECT key, creation_time FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace \ 274 | AND ( creation_time < :creation_time_token OR (creation_time = :creation_time_token AND key > :key_token) ) \ 275 | ORDER BY creation_time DESC, key ASC LIMIT :page_size", 276 | self.paginated_kv_table_name 277 | ); 278 | 279 | let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { 280 | let msg = format!("Failed to prepare statement: {}", e); 281 | io::Error::new(io::ErrorKind::Other, msg) 282 | })?; 283 | 284 | let mut keys: Vec = Vec::new(); 285 | let page_token = page_token.unwrap_or(("".to_string(), i64::MAX)); 286 | 287 | let rows_iter = stmt 288 | .query_map( 289 | named_params! { 290 | ":primary_namespace": primary_namespace, 291 | ":secondary_namespace": secondary_namespace, 292 | ":key_token": page_token.0, 293 | ":creation_time_token": page_token.1, 294 | ":page_size": LIST_KEYS_MAX_PAGE_SIZE, 295 | }, 296 | |row| { 297 | let key: String = row.get(0)?; 298 | let creation_time: i64 = row.get(1)?; 299 | Ok((key, creation_time)) 300 | }, 301 | ) 302 | .map_err(|e| { 303 | let msg = format!("Failed to retrieve queried rows: {}", e); 304 | io::Error::new(io::ErrorKind::Other, msg) 305 | })?; 306 | 307 | let mut last_creation_time: Option = None; 308 | for r in rows_iter { 309 | let (k, ct) = r.map_err(|e| { 310 | let msg = format!("Failed to retrieve queried rows: {}", e); 311 | io::Error::new(io::ErrorKind::Other, msg) 312 | })?; 313 | keys.push(k); 314 | last_creation_time = Some(ct); 315 | } 316 | 317 | let last_key = keys.last().cloned(); 318 | let next_page_token = if let (Some(k), Some(ct)) = (last_key, last_creation_time) { 319 | Some((k, ct)) 320 | } else { 321 | None 322 | }; 323 | 324 | Ok(ListResponse { keys, next_page_token }) 325 | } 326 | } 327 | 328 | #[cfg(test)] 329 | mod tests { 330 | use super::*; 331 | use ldk_node::lightning::util::persist::KVSTORE_NAMESPACE_KEY_MAX_LEN; 332 | use rand::distributions::Alphanumeric; 333 | use rand::{thread_rng, Rng}; 334 | use std::panic::RefUnwindSafe; 335 | 336 | impl Drop for SqliteStore { 337 | fn drop(&mut self) { 338 | match fs::remove_dir_all(&self.data_dir) { 339 | Err(e) => println!("Failed to remove test store directory: {}", e), 340 | _ => {}, 341 | } 342 | } 343 | } 344 | 345 | #[test] 346 | fn read_write_remove_list_persist() { 347 | let mut temp_path = random_storage_path(); 348 | temp_path.push("read_write_remove_list_persist"); 349 | let store = SqliteStore::new( 350 | temp_path, 351 | Some("test_db".to_string()), 352 | Some("test_table".to_string()), 353 | ) 354 | .unwrap(); 355 | do_read_write_remove_list_persist(&store); 356 | } 357 | 358 | pub(crate) fn random_storage_path() -> PathBuf { 359 | let mut temp_path = std::env::temp_dir(); 360 | let mut rng = thread_rng(); 361 | let rand_dir: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); 362 | temp_path.push(rand_dir); 363 | temp_path 364 | } 365 | 366 | pub(crate) fn do_read_write_remove_list_persist( 367 | kv_store: &K, 368 | ) { 369 | let data = [42u8; 32]; 370 | 371 | let primary_namespace = "testspace"; 372 | let secondary_namespace = "testsubspace"; 373 | let testkey = "testkey_0"; 374 | 375 | let list_all_keys = |primary_namespace: &str, secondary_namespace: &str| -> Vec { 376 | let mut all_keys = Vec::new(); 377 | let mut page_token = None; 378 | loop { 379 | let list_response = 380 | kv_store.list(primary_namespace, secondary_namespace, page_token).unwrap(); 381 | assert!(list_response.keys.len() <= LIST_KEYS_MAX_PAGE_SIZE as usize); 382 | all_keys.extend(list_response.keys); 383 | if list_response.next_page_token.is_none() { 384 | break; 385 | } 386 | page_token = list_response.next_page_token; 387 | } 388 | all_keys 389 | }; 390 | 391 | // Test the basic KVStore operations. 392 | for i in 0..110 { 393 | kv_store 394 | .write(primary_namespace, secondary_namespace, &format!("testkey_{}", i), 0, &data) 395 | .unwrap(); 396 | } 397 | 398 | // Test empty primary/secondary namespaces are allowed, but not empty primary namespace and non-empty 399 | // secondary primary_namespace, and not empty key. 400 | kv_store.write("", "", testkey, 0, &data).unwrap(); 401 | let res = 402 | std::panic::catch_unwind(|| kv_store.write("", secondary_namespace, testkey, 0, &data)); 403 | assert!(res.is_err()); 404 | let res = std::panic::catch_unwind(|| { 405 | kv_store.write(primary_namespace, secondary_namespace, "", 0, &data) 406 | }); 407 | assert!(res.is_err()); 408 | 409 | let listed_keys = list_all_keys(primary_namespace, secondary_namespace); 410 | assert_eq!(listed_keys.len(), 110); 411 | assert_eq!(listed_keys[0], testkey); 412 | 413 | let read_data = kv_store.read(primary_namespace, secondary_namespace, testkey).unwrap(); 414 | assert_eq!(data, &*read_data); 415 | 416 | kv_store.remove(primary_namespace, secondary_namespace, testkey, false).unwrap(); 417 | 418 | let listed_keys = list_all_keys(primary_namespace, secondary_namespace); 419 | assert_eq!(listed_keys.len(), 109); 420 | 421 | // Ensure we have no issue operating with primary_namespace/secondary_namespace/key being KVSTORE_NAMESPACE_KEY_MAX_LEN 422 | let max_chars: String = 423 | std::iter::repeat('A').take(KVSTORE_NAMESPACE_KEY_MAX_LEN).collect(); 424 | kv_store.write(&max_chars, &max_chars, &max_chars, 0, &data).unwrap(); 425 | 426 | println!("{:?}", listed_keys); 427 | 428 | let listed_keys = list_all_keys(&max_chars, &max_chars); 429 | assert_eq!(listed_keys.len(), 1); 430 | assert_eq!(listed_keys[0], max_chars); 431 | 432 | let read_data = kv_store.read(&max_chars, &max_chars, &max_chars).unwrap(); 433 | assert_eq!(data, &*read_data); 434 | 435 | kv_store.remove(&max_chars, &max_chars, &max_chars, false).unwrap(); 436 | 437 | let listed_keys = list_all_keys(&max_chars, &max_chars); 438 | assert_eq!(listed_keys.len(), 0); 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /ldk-server/src/io/utils.rs: -------------------------------------------------------------------------------- 1 | use ldk_node::lightning::types::string::PrintableString; 2 | use ldk_node::lightning::util::persist::{ 3 | KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN, 4 | }; 5 | 6 | pub(crate) fn check_namespace_key_validity( 7 | primary_namespace: &str, secondary_namespace: &str, key: Option<&str>, operation: &str, 8 | ) -> Result<(), std::io::Error> { 9 | if let Some(key) = key { 10 | if key.is_empty() { 11 | debug_assert!( 12 | false, 13 | "Failed to {} {}/{}/{}: key may not be empty.", 14 | operation, 15 | PrintableString(primary_namespace), 16 | PrintableString(secondary_namespace), 17 | PrintableString(key) 18 | ); 19 | let msg = format!( 20 | "Failed to {} {}/{}/{}: key may not be empty.", 21 | operation, 22 | PrintableString(primary_namespace), 23 | PrintableString(secondary_namespace), 24 | PrintableString(key) 25 | ); 26 | return Err(std::io::Error::new(std::io::ErrorKind::Other, msg)); 27 | } 28 | 29 | if primary_namespace.is_empty() && !secondary_namespace.is_empty() { 30 | debug_assert!(false, 31 | "Failed to {} {}/{}/{}: primary namespace may not be empty if a non-empty secondary namespace is given.", 32 | operation, 33 | PrintableString(primary_namespace), PrintableString(secondary_namespace), PrintableString(key) 34 | ); 35 | let msg = format!( 36 | "Failed to {} {}/{}/{}: primary namespace may not be empty if a non-empty secondary namespace is given.", operation, 37 | PrintableString(primary_namespace), PrintableString(secondary_namespace), PrintableString(key) 38 | ); 39 | return Err(std::io::Error::new(std::io::ErrorKind::Other, msg)); 40 | } 41 | 42 | if !is_valid_kvstore_str(primary_namespace) 43 | || !is_valid_kvstore_str(secondary_namespace) 44 | || !is_valid_kvstore_str(key) 45 | { 46 | debug_assert!( 47 | false, 48 | "Failed to {} {}/{}/{}: primary namespace, secondary namespace, and key must be valid.", 49 | operation, 50 | PrintableString(primary_namespace), 51 | PrintableString(secondary_namespace), 52 | PrintableString(key) 53 | ); 54 | let msg = format!( 55 | "Failed to {} {}/{}/{}: primary namespace, secondary namespace, and key must be valid.", 56 | operation, 57 | PrintableString(primary_namespace), 58 | PrintableString(secondary_namespace), 59 | PrintableString(key) 60 | ); 61 | return Err(std::io::Error::new(std::io::ErrorKind::Other, msg)); 62 | } 63 | } else { 64 | if primary_namespace.is_empty() && !secondary_namespace.is_empty() { 65 | debug_assert!(false, 66 | "Failed to {} {}/{}: primary namespace may not be empty if a non-empty secondary namespace is given.", 67 | operation, PrintableString(primary_namespace), PrintableString(secondary_namespace) 68 | ); 69 | let msg = format!( 70 | "Failed to {} {}/{}: primary namespace may not be empty if a non-empty secondary namespace is given.", 71 | operation, PrintableString(primary_namespace), PrintableString(secondary_namespace) 72 | ); 73 | return Err(std::io::Error::new(std::io::ErrorKind::Other, msg)); 74 | } 75 | if !is_valid_kvstore_str(primary_namespace) || !is_valid_kvstore_str(secondary_namespace) { 76 | debug_assert!( 77 | false, 78 | "Failed to {} {}/{}: primary namespace and secondary namespace must be valid.", 79 | operation, 80 | PrintableString(primary_namespace), 81 | PrintableString(secondary_namespace) 82 | ); 83 | let msg = format!( 84 | "Failed to {} {}/{}: primary namespace and secondary namespace must be valid.", 85 | operation, 86 | PrintableString(primary_namespace), 87 | PrintableString(secondary_namespace) 88 | ); 89 | return Err(std::io::Error::new(std::io::ErrorKind::Other, msg)); 90 | } 91 | } 92 | 93 | Ok(()) 94 | } 95 | 96 | pub(crate) fn is_valid_kvstore_str(key: &str) -> bool { 97 | key.len() <= KVSTORE_NAMESPACE_KEY_MAX_LEN 98 | && key.chars().all(|c| KVSTORE_NAMESPACE_KEY_ALPHABET.contains(c)) 99 | } 100 | -------------------------------------------------------------------------------- /ldk-server/src/main.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod io; 3 | mod service; 4 | mod util; 5 | 6 | use crate::service::NodeService; 7 | 8 | use ldk_node::{Builder, Event, Node}; 9 | 10 | use tokio::net::TcpListener; 11 | use tokio::signal::unix::SignalKind; 12 | 13 | use hyper::server::conn::http1; 14 | use hyper_util::rt::TokioIo; 15 | 16 | use crate::io::events::event_publisher::{EventPublisher, NoopEventPublisher}; 17 | use crate::io::events::get_event_name; 18 | #[cfg(feature = "events-rabbitmq")] 19 | use crate::io::events::rabbitmq::{RabbitMqConfig, RabbitMqEventPublisher}; 20 | use crate::io::persist::paginated_kv_store::PaginatedKVStore; 21 | use crate::io::persist::sqlite_store::SqliteStore; 22 | use crate::io::persist::{ 23 | FORWARDED_PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, 24 | FORWARDED_PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, 25 | PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, 26 | }; 27 | use crate::util::config::load_config; 28 | use crate::util::proto_adapter::{forwarded_payment_to_proto, payment_to_proto}; 29 | use hex::DisplayHex; 30 | use ldk_node::config::Config; 31 | use ldk_node::lightning::ln::channelmanager::PaymentId; 32 | #[cfg(feature = "experimental-lsps2-support")] 33 | use ldk_node::liquidity::LSPS2ServiceConfig; 34 | use ldk_server_protos::events; 35 | use ldk_server_protos::events::{event_envelope, EventEnvelope}; 36 | use ldk_server_protos::types::Payment; 37 | use prost::Message; 38 | use rand::Rng; 39 | use std::fs; 40 | use std::path::{Path, PathBuf}; 41 | use std::sync::Arc; 42 | use std::time::{SystemTime, UNIX_EPOCH}; 43 | use tokio::select; 44 | 45 | const USAGE_GUIDE: &str = "Usage: ldk-server "; 46 | 47 | fn main() { 48 | let args: Vec = std::env::args().collect(); 49 | 50 | if args.len() < 2 { 51 | eprintln!("{USAGE_GUIDE}"); 52 | std::process::exit(-1); 53 | } 54 | 55 | let arg = args[1].as_str(); 56 | if arg == "-h" || arg == "--help" { 57 | println!("{}", USAGE_GUIDE); 58 | std::process::exit(0); 59 | } 60 | 61 | if fs::File::open(arg).is_err() { 62 | eprintln!("Unable to access configuration file."); 63 | std::process::exit(-1); 64 | } 65 | 66 | let mut ldk_node_config = Config::default(); 67 | let config_file = match load_config(Path::new(arg)) { 68 | Ok(config) => config, 69 | Err(e) => { 70 | eprintln!("Invalid configuration file: {}", e); 71 | std::process::exit(-1); 72 | }, 73 | }; 74 | 75 | ldk_node_config.storage_dir_path = config_file.storage_dir_path.clone(); 76 | ldk_node_config.listening_addresses = Some(vec![config_file.listening_addr]); 77 | ldk_node_config.network = config_file.network; 78 | 79 | let mut builder = Builder::from_config(ldk_node_config); 80 | builder.set_log_facade_logger(); 81 | 82 | let bitcoind_rpc_addr = config_file.bitcoind_rpc_addr; 83 | 84 | builder.set_chain_source_bitcoind_rpc( 85 | bitcoind_rpc_addr.ip().to_string(), 86 | bitcoind_rpc_addr.port(), 87 | config_file.bitcoind_rpc_user, 88 | config_file.bitcoind_rpc_password, 89 | ); 90 | 91 | // LSPS2 support is highly experimental and for testing purposes only. 92 | #[cfg(feature = "experimental-lsps2-support")] 93 | builder.set_liquidity_provider_lsps2( 94 | config_file.lsps2_service_config.expect("Missing liquidity.lsps2_server config"), 95 | ); 96 | 97 | let runtime = match tokio::runtime::Builder::new_multi_thread().enable_all().build() { 98 | Ok(runtime) => Arc::new(runtime), 99 | Err(e) => { 100 | eprintln!("Failed to setup tokio runtime: {}", e); 101 | std::process::exit(-1); 102 | }, 103 | }; 104 | 105 | let node = match builder.build() { 106 | Ok(node) => Arc::new(node), 107 | Err(e) => { 108 | eprintln!("Failed to build LDK Node: {}", e); 109 | std::process::exit(-1); 110 | }, 111 | }; 112 | 113 | let paginated_store: Arc = 114 | Arc::new(match SqliteStore::new(PathBuf::from(config_file.storage_dir_path), None, None) { 115 | Ok(store) => store, 116 | Err(e) => { 117 | eprintln!("Failed to create SqliteStore: {:?}", e); 118 | std::process::exit(-1); 119 | }, 120 | }); 121 | 122 | let event_publisher: Arc = Arc::new(NoopEventPublisher); 123 | 124 | #[cfg(feature = "events-rabbitmq")] 125 | let event_publisher: Arc = { 126 | let rabbitmq_config = RabbitMqConfig { 127 | connection_string: config_file.rabbitmq_connection_string, 128 | exchange_name: config_file.rabbitmq_exchange_name, 129 | }; 130 | Arc::new(RabbitMqEventPublisher::new(rabbitmq_config)) 131 | }; 132 | 133 | println!("Starting up..."); 134 | match node.start_with_runtime(Arc::clone(&runtime)) { 135 | Ok(()) => {}, 136 | Err(e) => { 137 | eprintln!("Failed to start up LDK Node: {}", e); 138 | std::process::exit(-1); 139 | }, 140 | } 141 | 142 | println!( 143 | "CONNECTION_STRING: {}@{}", 144 | node.node_id(), 145 | node.config().listening_addresses.as_ref().unwrap().first().unwrap() 146 | ); 147 | 148 | runtime.block_on(async { 149 | let mut sigterm_stream = match tokio::signal::unix::signal(SignalKind::terminate()) { 150 | Ok(stream) => stream, 151 | Err(e) => { 152 | println!("Failed to register for SIGTERM stream: {}", e); 153 | std::process::exit(-1); 154 | } 155 | }; 156 | let event_node = Arc::clone(&node); 157 | let rest_svc_listener = TcpListener::bind(config_file.rest_service_addr) 158 | .await 159 | .expect("Failed to bind listening port"); 160 | loop { 161 | select! { 162 | event = event_node.next_event_async() => { 163 | match event { 164 | Event::ChannelPending { channel_id, counterparty_node_id, .. } => { 165 | println!( 166 | "CHANNEL_PENDING: {} from counterparty {}", 167 | channel_id, counterparty_node_id 168 | ); 169 | event_node.event_handled(); 170 | }, 171 | Event::ChannelReady { channel_id, counterparty_node_id, .. } => { 172 | println!( 173 | "CHANNEL_READY: {} from counterparty {:?}", 174 | channel_id, counterparty_node_id 175 | ); 176 | event_node.event_handled(); 177 | }, 178 | Event::PaymentReceived { payment_id, payment_hash, amount_msat, .. } => { 179 | println!( 180 | "PAYMENT_RECEIVED: with id {:?}, hash {}, amount_msat {}", 181 | payment_id, payment_hash, amount_msat 182 | ); 183 | let payment_id = payment_id.expect("PaymentId expected for ldk-server >=0.1"); 184 | 185 | publish_event_and_upsert_payment(&payment_id, 186 | |payment_ref| event_envelope::Event::PaymentReceived(events::PaymentReceived { 187 | payment: Some(payment_ref.clone()), 188 | }), 189 | &event_node, 190 | Arc::clone(&event_publisher), 191 | Arc::clone(&paginated_store)).await; 192 | }, 193 | Event::PaymentSuccessful {payment_id, ..} => { 194 | let payment_id = payment_id.expect("PaymentId expected for ldk-server >=0.1"); 195 | 196 | publish_event_and_upsert_payment(&payment_id, 197 | |payment_ref| event_envelope::Event::PaymentSuccessful(events::PaymentSuccessful { 198 | payment: Some(payment_ref.clone()), 199 | }), 200 | &event_node, 201 | Arc::clone(&event_publisher), 202 | Arc::clone(&paginated_store)).await; 203 | }, 204 | Event::PaymentFailed {payment_id, ..} => { 205 | let payment_id = payment_id.expect("PaymentId expected for ldk-server >=0.1"); 206 | 207 | publish_event_and_upsert_payment(&payment_id, 208 | |payment_ref| event_envelope::Event::PaymentFailed(events::PaymentFailed { 209 | payment: Some(payment_ref.clone()), 210 | }), 211 | &event_node, 212 | Arc::clone(&event_publisher), 213 | Arc::clone(&paginated_store)).await; 214 | }, 215 | Event::PaymentClaimable {payment_id, ..} => { 216 | if let Some(payment_details) = event_node.payment(&payment_id) { 217 | let payment = payment_to_proto(payment_details); 218 | upsert_payment_details(&event_node, Arc::clone(&paginated_store), &payment); 219 | } else { 220 | eprintln!("Unable to find payment with paymentId: {}", payment_id.to_string()); 221 | } 222 | }, 223 | Event::PaymentForwarded { 224 | prev_channel_id, 225 | next_channel_id, 226 | prev_user_channel_id, 227 | next_user_channel_id, 228 | prev_node_id, 229 | next_node_id, 230 | total_fee_earned_msat, 231 | skimmed_fee_msat, 232 | claim_from_onchain_tx, 233 | outbound_amount_forwarded_msat 234 | } => { 235 | 236 | println!("PAYMENT_FORWARDED: with outbound_amount_forwarded_msat {}, total_fee_earned_msat: {}, inbound channel: {}, outbound channel: {}", 237 | outbound_amount_forwarded_msat.unwrap_or(0), total_fee_earned_msat.unwrap_or(0), prev_channel_id, next_channel_id 238 | ); 239 | 240 | let forwarded_payment = forwarded_payment_to_proto( 241 | prev_channel_id, 242 | next_channel_id, 243 | prev_user_channel_id, 244 | next_user_channel_id, 245 | prev_node_id, 246 | next_node_id, 247 | total_fee_earned_msat, 248 | skimmed_fee_msat, 249 | claim_from_onchain_tx, 250 | outbound_amount_forwarded_msat 251 | ); 252 | 253 | // We don't expose this payment-id to the user, it is a temporary measure to generate 254 | // some unique identifiers until we have forwarded-payment-id available in ldk. 255 | // Currently, this is the expected user handling behaviour for forwarded payments. 256 | let mut forwarded_payment_id = [0u8;32]; 257 | rand::thread_rng().fill(&mut forwarded_payment_id); 258 | 259 | let forwarded_payment_creation_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time must be > 1970").as_secs() as i64; 260 | 261 | match event_publisher.publish(EventEnvelope { 262 | event: Some(event_envelope::Event::PaymentForwarded(events::PaymentForwarded { 263 | forwarded_payment: Some(forwarded_payment.clone()), 264 | })), 265 | }).await { 266 | Ok(_) => {}, 267 | Err(e) => { 268 | println!("Failed to publish 'PaymentForwarded' event: {}", e); 269 | continue; 270 | } 271 | }; 272 | 273 | match paginated_store.write(FORWARDED_PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE,FORWARDED_PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, 274 | &forwarded_payment_id.to_lower_hex_string(), 275 | forwarded_payment_creation_time, 276 | &forwarded_payment.encode_to_vec(), 277 | ) { 278 | Ok(_) => { 279 | event_node.event_handled(); 280 | } 281 | Err(e) => { 282 | println!("Failed to write forwarded payment to persistence: {}", e); 283 | } 284 | } 285 | }, 286 | _ => { 287 | event_node.event_handled(); 288 | }, 289 | } 290 | }, 291 | res = rest_svc_listener.accept() => { 292 | match res { 293 | Ok((stream, _)) => { 294 | let io_stream = TokioIo::new(stream); 295 | let node_service = NodeService::new(Arc::clone(&node), Arc::clone(&paginated_store)); 296 | runtime.spawn(async move { 297 | if let Err(err) = http1::Builder::new().serve_connection(io_stream, node_service).await { 298 | eprintln!("Failed to serve connection: {}", err); 299 | } 300 | }); 301 | }, 302 | Err(e) => eprintln!("Failed to accept connection: {}", e), 303 | } 304 | } 305 | _ = tokio::signal::ctrl_c() => { 306 | println!("Received CTRL-C, shutting down.."); 307 | break; 308 | } 309 | _ = sigterm_stream.recv() => { 310 | println!("Received SIGTERM, shutting down.."); 311 | break; 312 | } 313 | } 314 | } 315 | }); 316 | 317 | node.stop().expect("Shutdown should always succeed."); 318 | println!("Shutdown complete.."); 319 | } 320 | 321 | async fn publish_event_and_upsert_payment( 322 | payment_id: &PaymentId, payment_to_event: fn(&Payment) -> event_envelope::Event, 323 | event_node: &Node, event_publisher: Arc, 324 | paginated_store: Arc, 325 | ) { 326 | if let Some(payment_details) = event_node.payment(payment_id) { 327 | let payment = payment_to_proto(payment_details); 328 | 329 | let event = payment_to_event(&payment); 330 | let event_name = get_event_name(&event); 331 | match event_publisher.publish(EventEnvelope { event: Some(event) }).await { 332 | Ok(_) => {}, 333 | Err(e) => { 334 | println!("Failed to publish '{}' event, : {}", event_name, e); 335 | return; 336 | }, 337 | }; 338 | 339 | upsert_payment_details(event_node, Arc::clone(&paginated_store), &payment); 340 | } else { 341 | eprintln!("Unable to find payment with paymentId: {}", payment_id); 342 | } 343 | } 344 | 345 | fn upsert_payment_details( 346 | event_node: &Node, paginated_store: Arc, payment: &Payment, 347 | ) { 348 | let time = 349 | SystemTime::now().duration_since(UNIX_EPOCH).expect("Time must be > 1970").as_secs() as i64; 350 | 351 | match paginated_store.write( 352 | PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, 353 | PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, 354 | &payment.id, 355 | time, 356 | &payment.encode_to_vec(), 357 | ) { 358 | Ok(_) => { 359 | event_node.event_handled(); 360 | }, 361 | Err(e) => { 362 | eprintln!("Failed to write payment to persistence: {}", e); 363 | }, 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /ldk-server/src/service.rs: -------------------------------------------------------------------------------- 1 | use ldk_node::Node; 2 | 3 | use http_body_util::{BodyExt, Full}; 4 | use hyper::body::{Bytes, Incoming}; 5 | use hyper::service::Service; 6 | use hyper::{Request, Response, StatusCode}; 7 | 8 | use prost::Message; 9 | 10 | use crate::api::bolt11_receive::{handle_bolt11_receive_request, BOLT11_RECEIVE_PATH}; 11 | use crate::api::bolt11_send::{handle_bolt11_send_request, BOLT11_SEND_PATH}; 12 | use crate::api::bolt12_receive::{handle_bolt12_receive_request, BOLT12_RECEIVE_PATH}; 13 | use crate::api::bolt12_send::{handle_bolt12_send_request, BOLT12_SEND_PATH}; 14 | use crate::api::close_channel::{handle_close_channel_request, CLOSE_CHANNEL_PATH}; 15 | use crate::api::error::LdkServerError; 16 | use crate::api::error::LdkServerErrorCode::InvalidRequestError; 17 | use crate::api::get_balances::{handle_get_balances_request, GET_BALANCES}; 18 | use crate::api::get_node_info::{handle_get_node_info_request, GET_NODE_INFO}; 19 | use crate::api::get_payment_details::{ 20 | handle_get_payment_details_request, GET_PAYMENT_DETAILS_PATH, 21 | }; 22 | use crate::api::list_channels::{handle_list_channels_request, LIST_CHANNELS_PATH}; 23 | use crate::api::list_forwarded_payments::{ 24 | handle_list_forwarded_payments_request, LIST_FORWARDED_PAYMENTS_PATH, 25 | }; 26 | use crate::api::list_payments::{handle_list_payments_request, LIST_PAYMENTS_PATH}; 27 | use crate::api::onchain_receive::{handle_onchain_receive_request, ONCHAIN_RECEIVE_PATH}; 28 | use crate::api::onchain_send::{handle_onchain_send_request, ONCHAIN_SEND_PATH}; 29 | use crate::api::open_channel::{handle_open_channel, OPEN_CHANNEL_PATH}; 30 | use crate::api::update_channel_config::{ 31 | handle_update_channel_config_request, UPDATE_CHANNEL_CONFIG_PATH, 32 | }; 33 | use crate::io::persist::paginated_kv_store::PaginatedKVStore; 34 | use crate::util::proto_adapter::to_error_response; 35 | use std::future::Future; 36 | use std::pin::Pin; 37 | use std::sync::Arc; 38 | 39 | #[derive(Clone)] 40 | pub struct NodeService { 41 | node: Arc, 42 | paginated_kv_store: Arc, 43 | } 44 | 45 | impl NodeService { 46 | pub(crate) fn new(node: Arc, paginated_kv_store: Arc) -> Self { 47 | Self { node, paginated_kv_store } 48 | } 49 | } 50 | 51 | pub(crate) struct Context { 52 | pub(crate) node: Arc, 53 | pub(crate) paginated_kv_store: Arc, 54 | } 55 | 56 | impl Service> for NodeService { 57 | type Response = Response>; 58 | type Error = hyper::Error; 59 | type Future = Pin> + Send>>; 60 | 61 | fn call(&self, req: Request) -> Self::Future { 62 | let context = Context { 63 | node: Arc::clone(&self.node), 64 | paginated_kv_store: Arc::clone(&self.paginated_kv_store), 65 | }; 66 | // Exclude '/' from path pattern matching. 67 | match &req.uri().path()[1..] { 68 | GET_NODE_INFO => Box::pin(handle_request(context, req, handle_get_node_info_request)), 69 | GET_BALANCES => Box::pin(handle_request(context, req, handle_get_balances_request)), 70 | ONCHAIN_RECEIVE_PATH => { 71 | Box::pin(handle_request(context, req, handle_onchain_receive_request)) 72 | }, 73 | ONCHAIN_SEND_PATH => { 74 | Box::pin(handle_request(context, req, handle_onchain_send_request)) 75 | }, 76 | BOLT11_RECEIVE_PATH => { 77 | Box::pin(handle_request(context, req, handle_bolt11_receive_request)) 78 | }, 79 | BOLT11_SEND_PATH => Box::pin(handle_request(context, req, handle_bolt11_send_request)), 80 | BOLT12_RECEIVE_PATH => { 81 | Box::pin(handle_request(context, req, handle_bolt12_receive_request)) 82 | }, 83 | BOLT12_SEND_PATH => Box::pin(handle_request(context, req, handle_bolt12_send_request)), 84 | OPEN_CHANNEL_PATH => Box::pin(handle_request(context, req, handle_open_channel)), 85 | CLOSE_CHANNEL_PATH => { 86 | Box::pin(handle_request(context, req, handle_close_channel_request)) 87 | }, 88 | LIST_CHANNELS_PATH => { 89 | Box::pin(handle_request(context, req, handle_list_channels_request)) 90 | }, 91 | UPDATE_CHANNEL_CONFIG_PATH => { 92 | Box::pin(handle_request(context, req, handle_update_channel_config_request)) 93 | }, 94 | GET_PAYMENT_DETAILS_PATH => { 95 | Box::pin(handle_request(context, req, handle_get_payment_details_request)) 96 | }, 97 | LIST_PAYMENTS_PATH => { 98 | Box::pin(handle_request(context, req, handle_list_payments_request)) 99 | }, 100 | LIST_FORWARDED_PAYMENTS_PATH => { 101 | Box::pin(handle_request(context, req, handle_list_forwarded_payments_request)) 102 | }, 103 | path => { 104 | let error = format!("Unknown request: {}", path).into_bytes(); 105 | Box::pin(async { 106 | Ok(Response::builder() 107 | .status(StatusCode::BAD_REQUEST) 108 | .body(Full::new(Bytes::from(error))) 109 | // unwrap safety: body only errors when previous chained calls failed. 110 | .unwrap()) 111 | }) 112 | }, 113 | } 114 | } 115 | } 116 | 117 | async fn handle_request< 118 | T: Message + Default, 119 | R: Message, 120 | F: Fn(Context, T) -> Result, 121 | >( 122 | context: Context, request: Request, handler: F, 123 | ) -> Result<>>::Response, hyper::Error> { 124 | // TODO: we should bound the amount of data we read to avoid allocating too much memory. 125 | let bytes = request.into_body().collect().await?.to_bytes(); 126 | match T::decode(bytes) { 127 | Ok(request) => match handler(context, request) { 128 | Ok(response) => Ok(Response::builder() 129 | .body(Full::new(Bytes::from(response.encode_to_vec()))) 130 | // unwrap safety: body only errors when previous chained calls failed. 131 | .unwrap()), 132 | Err(e) => { 133 | let (error_response, status_code) = to_error_response(e); 134 | Ok(Response::builder() 135 | .status(status_code) 136 | .body(Full::new(Bytes::from(error_response.encode_to_vec()))) 137 | // unwrap safety: body only errors when previous chained calls failed. 138 | .unwrap()) 139 | }, 140 | }, 141 | Err(_) => { 142 | let (error_response, status_code) = 143 | to_error_response(LdkServerError::new(InvalidRequestError, "Malformed request.")); 144 | Ok(Response::builder() 145 | .status(status_code) 146 | .body(Full::new(Bytes::from(error_response.encode_to_vec()))) 147 | // unwrap safety: body only errors when previous chained calls failed. 148 | .unwrap()) 149 | }, 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /ldk-server/src/util/config.rs: -------------------------------------------------------------------------------- 1 | use ldk_node::bitcoin::Network; 2 | use ldk_node::lightning::ln::msgs::SocketAddress; 3 | use ldk_node::liquidity::LSPS2ServiceConfig; 4 | use serde::{Deserialize, Serialize}; 5 | use std::net::SocketAddr; 6 | use std::path::Path; 7 | use std::str::FromStr; 8 | use std::{fs, io}; 9 | 10 | /// Configuration for LDK Server. 11 | #[derive(Debug)] 12 | pub struct Config { 13 | pub listening_addr: SocketAddress, 14 | pub network: Network, 15 | pub rest_service_addr: SocketAddr, 16 | pub storage_dir_path: String, 17 | pub bitcoind_rpc_addr: SocketAddr, 18 | pub bitcoind_rpc_user: String, 19 | pub bitcoind_rpc_password: String, 20 | pub rabbitmq_connection_string: String, 21 | pub rabbitmq_exchange_name: String, 22 | pub lsps2_service_config: Option, 23 | } 24 | 25 | impl TryFrom for Config { 26 | type Error = io::Error; 27 | 28 | fn try_from(toml_config: TomlConfig) -> io::Result { 29 | let listening_addr = 30 | SocketAddress::from_str(&toml_config.node.listening_address).map_err(|e| { 31 | io::Error::new( 32 | io::ErrorKind::InvalidInput, 33 | format!("Invalid listening address configured: {}", e), 34 | ) 35 | })?; 36 | let rest_service_addr = SocketAddr::from_str(&toml_config.node.rest_service_address) 37 | .map_err(|e| { 38 | io::Error::new( 39 | io::ErrorKind::InvalidInput, 40 | format!("Invalid rest service address configured: {}", e), 41 | ) 42 | })?; 43 | let bitcoind_rpc_addr = 44 | SocketAddr::from_str(&toml_config.bitcoind.rpc_address).map_err(|e| { 45 | io::Error::new( 46 | io::ErrorKind::InvalidInput, 47 | format!("Invalid bitcoind RPC address configured: {}", e), 48 | ) 49 | })?; 50 | 51 | let (rabbitmq_connection_string, rabbitmq_exchange_name) = { 52 | let rabbitmq = toml_config.rabbitmq.unwrap_or(RabbitmqConfig { 53 | connection_string: String::new(), 54 | exchange_name: String::new(), 55 | }); 56 | #[cfg(feature = "events-rabbitmq")] 57 | if rabbitmq.connection_string.is_empty() || rabbitmq.exchange_name.is_empty() { 58 | return Err(io::Error::new( 59 | io::ErrorKind::InvalidInput, 60 | "Both `rabbitmq.connection_string` and `rabbitmq.exchange_name` must be configured if enabling `events-rabbitmq` feature.".to_string(), 61 | )); 62 | } 63 | (rabbitmq.connection_string, rabbitmq.exchange_name) 64 | }; 65 | 66 | #[cfg(not(feature = "experimental-lsps2-support"))] 67 | let lsps2_service_config: Option = None; 68 | #[cfg(feature = "experimental-lsps2-support")] 69 | let lsps2_service_config = Some(toml_config.liquidity 70 | .and_then(|l| l.lsps2_service) 71 | .ok_or_else(|| io::Error::new( 72 | io::ErrorKind::InvalidInput, 73 | "`liquidity.lsps2_service` must be defined in config if enabling `experimental-lsps2-support` feature." 74 | ))? 75 | .into()); 76 | 77 | Ok(Config { 78 | listening_addr, 79 | network: toml_config.node.network, 80 | rest_service_addr, 81 | storage_dir_path: toml_config.storage.disk.dir_path, 82 | bitcoind_rpc_addr, 83 | bitcoind_rpc_user: toml_config.bitcoind.rpc_user, 84 | bitcoind_rpc_password: toml_config.bitcoind.rpc_password, 85 | rabbitmq_connection_string, 86 | rabbitmq_exchange_name, 87 | lsps2_service_config, 88 | }) 89 | } 90 | } 91 | 92 | /// Configuration loaded from a TOML file. 93 | #[derive(Deserialize, Serialize)] 94 | pub struct TomlConfig { 95 | node: NodeConfig, 96 | storage: StorageConfig, 97 | bitcoind: BitcoindConfig, 98 | rabbitmq: Option, 99 | liquidity: Option, 100 | } 101 | 102 | #[derive(Deserialize, Serialize)] 103 | struct NodeConfig { 104 | network: Network, 105 | listening_address: String, 106 | rest_service_address: String, 107 | } 108 | 109 | #[derive(Deserialize, Serialize)] 110 | struct StorageConfig { 111 | disk: DiskConfig, 112 | } 113 | 114 | #[derive(Deserialize, Serialize)] 115 | struct DiskConfig { 116 | dir_path: String, 117 | } 118 | 119 | #[derive(Deserialize, Serialize)] 120 | struct BitcoindConfig { 121 | rpc_address: String, 122 | rpc_user: String, 123 | rpc_password: String, 124 | } 125 | 126 | #[derive(Deserialize, Serialize)] 127 | struct RabbitmqConfig { 128 | connection_string: String, 129 | exchange_name: String, 130 | } 131 | 132 | #[derive(Deserialize, Serialize)] 133 | struct LiquidityConfig { 134 | lsps2_service: Option, 135 | } 136 | 137 | #[derive(Deserialize, Serialize, Debug)] 138 | struct LSPS2ServiceTomlConfig { 139 | advertise_service: bool, 140 | channel_opening_fee_ppm: u32, 141 | channel_over_provisioning_ppm: u32, 142 | min_channel_opening_fee_msat: u64, 143 | min_channel_lifetime: u32, 144 | max_client_to_self_delay: u32, 145 | min_payment_size_msat: u64, 146 | max_payment_size_msat: u64, 147 | require_token: Option, 148 | } 149 | 150 | impl Into for LSPS2ServiceTomlConfig { 151 | fn into(self) -> LSPS2ServiceConfig { 152 | match self { 153 | LSPS2ServiceTomlConfig { 154 | advertise_service, 155 | channel_opening_fee_ppm, 156 | channel_over_provisioning_ppm, 157 | min_channel_opening_fee_msat, 158 | min_channel_lifetime, 159 | max_client_to_self_delay, 160 | min_payment_size_msat, 161 | max_payment_size_msat, 162 | require_token, 163 | } => LSPS2ServiceConfig { 164 | advertise_service, 165 | channel_opening_fee_ppm, 166 | channel_over_provisioning_ppm, 167 | min_channel_opening_fee_msat, 168 | min_channel_lifetime, 169 | min_payment_size_msat, 170 | max_client_to_self_delay, 171 | max_payment_size_msat, 172 | require_token, 173 | }, 174 | } 175 | } 176 | } 177 | 178 | /// Loads the configuration from a TOML file at the given path. 179 | pub fn load_config>(config_path: P) -> io::Result { 180 | let file_contents = fs::read_to_string(config_path.as_ref()).map_err(|e| { 181 | io::Error::new( 182 | e.kind(), 183 | format!("Failed to read config file '{}': {}", config_path.as_ref().display(), e), 184 | ) 185 | })?; 186 | 187 | let toml_config: TomlConfig = toml::from_str(&file_contents).map_err(|e| { 188 | io::Error::new( 189 | io::ErrorKind::InvalidData, 190 | format!("Config file contains invalid TOML format: {}", e), 191 | ) 192 | })?; 193 | Ok(Config::try_from(toml_config)?) 194 | } 195 | 196 | #[cfg(test)] 197 | mod tests { 198 | use super::*; 199 | use ldk_node::{bitcoin::Network, lightning::ln::msgs::SocketAddress}; 200 | use std::str::FromStr; 201 | 202 | #[test] 203 | fn test_read_toml_config_from_file() { 204 | let storage_path = std::env::temp_dir(); 205 | let config_file_name = "config.toml"; 206 | 207 | let toml_config = r#" 208 | [node] 209 | network = "regtest" 210 | listening_address = "localhost:3001" 211 | rest_service_address = "127.0.0.1:3002" 212 | 213 | [storage.disk] 214 | dir_path = "/tmp" 215 | 216 | [bitcoind] 217 | rpc_address = "127.0.0.1:8332" # RPC endpoint 218 | rpc_user = "bitcoind-testuser" 219 | rpc_password = "bitcoind-testpassword" 220 | 221 | [rabbitmq] 222 | connection_string = "rabbitmq_connection_string" 223 | exchange_name = "rabbitmq_exchange_name" 224 | 225 | [liquidity.lsps2_service] 226 | advertise_service = false 227 | channel_opening_fee_ppm = 1000 # 0.1% fee 228 | channel_over_provisioning_ppm = 500000 # 50% extra capacity 229 | min_channel_opening_fee_msat = 10000000 # 10,000 satoshis 230 | min_channel_lifetime = 4320 # ~30 days 231 | max_client_to_self_delay = 1440 # ~10 days 232 | min_payment_size_msat = 10000000 # 10,000 satoshis 233 | max_payment_size_msat = 25000000000 # 0.25 BTC 234 | "#; 235 | 236 | fs::write(storage_path.join(config_file_name), toml_config).unwrap(); 237 | 238 | let config = load_config(storage_path.join(config_file_name)).unwrap(); 239 | let expected = Config { 240 | listening_addr: SocketAddress::from_str("localhost:3001").unwrap(), 241 | network: Network::Regtest, 242 | rest_service_addr: SocketAddr::from_str("127.0.0.1:3002").unwrap(), 243 | storage_dir_path: "/tmp".to_string(), 244 | bitcoind_rpc_addr: SocketAddr::from_str("127.0.0.1:8332").unwrap(), 245 | bitcoind_rpc_user: "bitcoind-testuser".to_string(), 246 | bitcoind_rpc_password: "bitcoind-testpassword".to_string(), 247 | rabbitmq_connection_string: "rabbitmq_connection_string".to_string(), 248 | rabbitmq_exchange_name: "rabbitmq_exchange_name".to_string(), 249 | lsps2_service_config: Some(LSPS2ServiceConfig { 250 | require_token: None, 251 | advertise_service: false, 252 | channel_opening_fee_ppm: 1000, 253 | channel_over_provisioning_ppm: 500000, 254 | min_channel_opening_fee_msat: 10000000, 255 | min_channel_lifetime: 4320, 256 | max_client_to_self_delay: 1440, 257 | min_payment_size_msat: 10000000, 258 | max_payment_size_msat: 25000000000, 259 | }), 260 | }; 261 | 262 | assert_eq!(config.listening_addr, expected.listening_addr); 263 | assert_eq!(config.network, expected.network); 264 | assert_eq!(config.rest_service_addr, expected.rest_service_addr); 265 | assert_eq!(config.storage_dir_path, expected.storage_dir_path); 266 | assert_eq!(config.bitcoind_rpc_addr, expected.bitcoind_rpc_addr); 267 | assert_eq!(config.bitcoind_rpc_user, expected.bitcoind_rpc_user); 268 | assert_eq!(config.bitcoind_rpc_password, expected.bitcoind_rpc_password); 269 | assert_eq!(config.rabbitmq_connection_string, expected.rabbitmq_connection_string); 270 | assert_eq!(config.rabbitmq_exchange_name, expected.rabbitmq_exchange_name); 271 | #[cfg(feature = "experimental-lsps2-support")] 272 | assert_eq!(config.lsps2_service_config.is_some(), expected.lsps2_service_config.is_some()); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /ldk-server/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod config; 2 | pub(crate) mod proto_adapter; 3 | -------------------------------------------------------------------------------- /ldk-server/src/util/proto_adapter.rs: -------------------------------------------------------------------------------- 1 | use crate::api::error::LdkServerError; 2 | use crate::api::error::LdkServerErrorCode::{ 3 | AuthError, InternalServerError, InvalidRequestError, LightningError, 4 | }; 5 | use bytes::Bytes; 6 | use hex::prelude::*; 7 | use hyper::StatusCode; 8 | use ldk_node::bitcoin::hashes::sha256; 9 | use ldk_node::bitcoin::secp256k1::PublicKey; 10 | use ldk_node::config::{ChannelConfig, MaxDustHTLCExposure}; 11 | use ldk_node::lightning::ln::types::ChannelId; 12 | use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description, Sha256}; 13 | use ldk_node::payment::{ 14 | ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, 15 | }; 16 | use ldk_node::{ChannelDetails, LightningBalance, PendingSweepBalance, UserChannelId}; 17 | use ldk_server_protos::error::{ErrorCode, ErrorResponse}; 18 | use ldk_server_protos::types::confirmation_status::Status::{Confirmed, Unconfirmed}; 19 | use ldk_server_protos::types::lightning_balance::BalanceType::{ 20 | ClaimableAwaitingConfirmations, ClaimableOnChannelClose, ContentiousClaimable, 21 | CounterpartyRevokedOutputClaimable, MaybePreimageClaimableHtlc, MaybeTimeoutClaimableHtlc, 22 | }; 23 | use ldk_server_protos::types::payment_kind::Kind::{ 24 | Bolt11, Bolt11Jit, Bolt12Offer, Bolt12Refund, Onchain, Spontaneous, 25 | }; 26 | use ldk_server_protos::types::pending_sweep_balance::BalanceType::{ 27 | AwaitingThresholdConfirmations, BroadcastAwaitingConfirmation, PendingBroadcast, 28 | }; 29 | use ldk_server_protos::types::{ 30 | bolt11_invoice_description, Channel, ForwardedPayment, LspFeeLimits, OutPoint, Payment, 31 | }; 32 | 33 | pub(crate) fn channel_to_proto(channel: ChannelDetails) -> Channel { 34 | Channel { 35 | channel_id: channel.channel_id.0.to_lower_hex_string(), 36 | counterparty_node_id: channel.counterparty_node_id.to_string(), 37 | funding_txo: channel 38 | .funding_txo 39 | .map(|o| OutPoint { txid: o.txid.to_string(), vout: o.vout }), 40 | user_channel_id: channel.user_channel_id.0.to_string(), 41 | unspendable_punishment_reserve: channel.unspendable_punishment_reserve, 42 | channel_value_sats: channel.channel_value_sats, 43 | feerate_sat_per_1000_weight: channel.feerate_sat_per_1000_weight, 44 | outbound_capacity_msat: channel.outbound_capacity_msat, 45 | inbound_capacity_msat: channel.inbound_capacity_msat, 46 | confirmations_required: channel.confirmations_required, 47 | confirmations: channel.confirmations, 48 | is_outbound: channel.is_outbound, 49 | is_channel_ready: channel.is_channel_ready, 50 | is_usable: channel.is_usable, 51 | is_announced: channel.is_announced, 52 | channel_config: Some(channel_config_to_proto(channel.config)), 53 | next_outbound_htlc_limit_msat: channel.next_outbound_htlc_limit_msat, 54 | next_outbound_htlc_minimum_msat: channel.next_outbound_htlc_minimum_msat, 55 | force_close_spend_delay: channel.force_close_spend_delay.map(|x| x as u32), 56 | counterparty_outbound_htlc_minimum_msat: channel.counterparty_outbound_htlc_minimum_msat, 57 | counterparty_outbound_htlc_maximum_msat: channel.counterparty_outbound_htlc_maximum_msat, 58 | counterparty_unspendable_punishment_reserve: channel 59 | .counterparty_unspendable_punishment_reserve, 60 | counterparty_forwarding_info_fee_base_msat: channel 61 | .counterparty_forwarding_info_fee_base_msat, 62 | counterparty_forwarding_info_fee_proportional_millionths: channel 63 | .counterparty_forwarding_info_fee_proportional_millionths, 64 | counterparty_forwarding_info_cltv_expiry_delta: channel 65 | .counterparty_forwarding_info_cltv_expiry_delta 66 | .map(|x| x as u32), 67 | } 68 | } 69 | 70 | pub(crate) fn channel_config_to_proto( 71 | channel_config: ChannelConfig, 72 | ) -> ldk_server_protos::types::ChannelConfig { 73 | ldk_server_protos::types::ChannelConfig { 74 | forwarding_fee_proportional_millionths: Some( 75 | channel_config.forwarding_fee_proportional_millionths, 76 | ), 77 | forwarding_fee_base_msat: Some(channel_config.forwarding_fee_base_msat), 78 | cltv_expiry_delta: Some(channel_config.cltv_expiry_delta as u32), 79 | force_close_avoidance_max_fee_satoshis: Some( 80 | channel_config.force_close_avoidance_max_fee_satoshis, 81 | ), 82 | accept_underpaying_htlcs: Some(channel_config.accept_underpaying_htlcs), 83 | max_dust_htlc_exposure: match channel_config.max_dust_htlc_exposure { 84 | MaxDustHTLCExposure::FixedLimit { limit_msat } => { 85 | Some(ldk_server_protos::types::channel_config::MaxDustHtlcExposure::FixedLimitMsat( 86 | limit_msat, 87 | )) 88 | }, 89 | MaxDustHTLCExposure::FeeRateMultiplier { multiplier } => Some( 90 | ldk_server_protos::types::channel_config::MaxDustHtlcExposure::FeeRateMultiplier( 91 | multiplier, 92 | ), 93 | ), 94 | }, 95 | } 96 | } 97 | 98 | pub(crate) fn payment_to_proto(payment: PaymentDetails) -> Payment { 99 | match payment { 100 | PaymentDetails { 101 | id, 102 | kind, 103 | amount_msat, 104 | fee_paid_msat, 105 | direction, 106 | status, 107 | latest_update_timestamp, 108 | } => Payment { 109 | id: id.to_string(), 110 | kind: Some(payment_kind_to_proto(kind)), 111 | amount_msat, 112 | fee_paid_msat, 113 | direction: match direction { 114 | PaymentDirection::Inbound => { 115 | ldk_server_protos::types::PaymentDirection::Inbound.into() 116 | }, 117 | PaymentDirection::Outbound => { 118 | ldk_server_protos::types::PaymentDirection::Outbound.into() 119 | }, 120 | }, 121 | status: match status { 122 | PaymentStatus::Pending => ldk_server_protos::types::PaymentStatus::Pending.into(), 123 | PaymentStatus::Succeeded => { 124 | ldk_server_protos::types::PaymentStatus::Succeeded.into() 125 | }, 126 | PaymentStatus::Failed => ldk_server_protos::types::PaymentStatus::Failed.into(), 127 | }, 128 | latest_update_timestamp, 129 | }, 130 | } 131 | } 132 | 133 | pub(crate) fn payment_kind_to_proto( 134 | payment_kind: PaymentKind, 135 | ) -> ldk_server_protos::types::PaymentKind { 136 | match payment_kind { 137 | PaymentKind::Onchain { txid, status } => ldk_server_protos::types::PaymentKind { 138 | kind: Some(Onchain(ldk_server_protos::types::Onchain { 139 | txid: txid.to_string(), 140 | status: Some(confirmation_status_to_proto(status)), 141 | })), 142 | }, 143 | PaymentKind::Bolt11 { hash, preimage, secret } => ldk_server_protos::types::PaymentKind { 144 | kind: Some(Bolt11(ldk_server_protos::types::Bolt11 { 145 | hash: hash.to_string(), 146 | preimage: preimage.map(|p| p.to_string()), 147 | secret: secret.map(|s| Bytes::copy_from_slice(&s.0)), 148 | })), 149 | }, 150 | PaymentKind::Bolt11Jit { hash, preimage, secret, lsp_fee_limits } => { 151 | ldk_server_protos::types::PaymentKind { 152 | kind: Some(Bolt11Jit(ldk_server_protos::types::Bolt11Jit { 153 | hash: hash.to_string(), 154 | preimage: preimage.map(|p| p.to_string()), 155 | secret: secret.map(|s| Bytes::copy_from_slice(&s.0)), 156 | lsp_fee_limits: Some(LspFeeLimits { 157 | max_total_opening_fee_msat: lsp_fee_limits.max_total_opening_fee_msat, 158 | max_proportional_opening_fee_ppm_msat: lsp_fee_limits 159 | .max_proportional_opening_fee_ppm_msat, 160 | }), 161 | })), 162 | } 163 | }, 164 | PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, payer_note, quantity } => { 165 | ldk_server_protos::types::PaymentKind { 166 | kind: Some(Bolt12Offer(ldk_server_protos::types::Bolt12Offer { 167 | hash: hash.map(|h| h.to_string()), 168 | preimage: preimage.map(|p| p.to_string()), 169 | secret: secret.map(|s| Bytes::copy_from_slice(&s.0)), 170 | offer_id: offer_id.0.to_lower_hex_string(), 171 | payer_note: payer_note.map(|s| s.to_string()), 172 | quantity, 173 | })), 174 | } 175 | }, 176 | PaymentKind::Bolt12Refund { hash, preimage, secret, payer_note, quantity } => { 177 | ldk_server_protos::types::PaymentKind { 178 | kind: Some(Bolt12Refund(ldk_server_protos::types::Bolt12Refund { 179 | hash: hash.map(|h| h.to_string()), 180 | preimage: preimage.map(|p| p.to_string()), 181 | secret: secret.map(|s| Bytes::copy_from_slice(&s.0)), 182 | payer_note: payer_note.map(|s| s.to_string()), 183 | quantity, 184 | })), 185 | } 186 | }, 187 | PaymentKind::Spontaneous { hash, preimage } => ldk_server_protos::types::PaymentKind { 188 | kind: Some(Spontaneous(ldk_server_protos::types::Spontaneous { 189 | hash: hash.to_string(), 190 | preimage: preimage.map(|p| p.to_string()), 191 | })), 192 | }, 193 | } 194 | } 195 | 196 | pub(crate) fn confirmation_status_to_proto( 197 | confirmation_status: ConfirmationStatus, 198 | ) -> ldk_server_protos::types::ConfirmationStatus { 199 | match confirmation_status { 200 | ConfirmationStatus::Confirmed { block_hash, height, timestamp } => { 201 | ldk_server_protos::types::ConfirmationStatus { 202 | status: Some(Confirmed(ldk_server_protos::types::Confirmed { 203 | block_hash: block_hash.to_string(), 204 | height, 205 | timestamp, 206 | })), 207 | } 208 | }, 209 | ConfirmationStatus::Unconfirmed => ldk_server_protos::types::ConfirmationStatus { 210 | status: Some(Unconfirmed(ldk_server_protos::types::Unconfirmed {})), 211 | }, 212 | } 213 | } 214 | 215 | pub(crate) fn lightning_balance_to_proto( 216 | lightning_balance: LightningBalance, 217 | ) -> ldk_server_protos::types::LightningBalance { 218 | match lightning_balance { 219 | LightningBalance::ClaimableOnChannelClose { 220 | channel_id, 221 | counterparty_node_id, 222 | amount_satoshis, 223 | transaction_fee_satoshis, 224 | outbound_payment_htlc_rounded_msat, 225 | outbound_forwarded_htlc_rounded_msat, 226 | inbound_claiming_htlc_rounded_msat, 227 | inbound_htlc_rounded_msat, 228 | } => ldk_server_protos::types::LightningBalance { 229 | balance_type: Some(ClaimableOnChannelClose( 230 | ldk_server_protos::types::ClaimableOnChannelClose { 231 | channel_id: channel_id.0.to_lower_hex_string(), 232 | counterparty_node_id: counterparty_node_id.to_string(), 233 | amount_satoshis, 234 | transaction_fee_satoshis, 235 | outbound_payment_htlc_rounded_msat, 236 | outbound_forwarded_htlc_rounded_msat, 237 | inbound_claiming_htlc_rounded_msat, 238 | inbound_htlc_rounded_msat, 239 | }, 240 | )), 241 | }, 242 | LightningBalance::ClaimableAwaitingConfirmations { 243 | channel_id, 244 | counterparty_node_id, 245 | amount_satoshis, 246 | confirmation_height, 247 | .. 248 | } => ldk_server_protos::types::LightningBalance { 249 | balance_type: Some(ClaimableAwaitingConfirmations( 250 | ldk_server_protos::types::ClaimableAwaitingConfirmations { 251 | channel_id: channel_id.0.to_lower_hex_string(), 252 | counterparty_node_id: counterparty_node_id.to_string(), 253 | amount_satoshis, 254 | confirmation_height, 255 | }, 256 | )), 257 | }, 258 | LightningBalance::ContentiousClaimable { 259 | channel_id, 260 | counterparty_node_id, 261 | amount_satoshis, 262 | timeout_height, 263 | payment_hash, 264 | payment_preimage, 265 | } => ldk_server_protos::types::LightningBalance { 266 | balance_type: Some(ContentiousClaimable( 267 | ldk_server_protos::types::ContentiousClaimable { 268 | channel_id: channel_id.0.to_lower_hex_string(), 269 | counterparty_node_id: counterparty_node_id.to_string(), 270 | amount_satoshis, 271 | timeout_height, 272 | payment_hash: payment_hash.to_string(), 273 | payment_preimage: payment_preimage.to_string(), 274 | }, 275 | )), 276 | }, 277 | LightningBalance::MaybeTimeoutClaimableHTLC { 278 | channel_id, 279 | counterparty_node_id, 280 | amount_satoshis, 281 | claimable_height, 282 | payment_hash, 283 | outbound_payment, 284 | } => ldk_server_protos::types::LightningBalance { 285 | balance_type: Some(MaybeTimeoutClaimableHtlc( 286 | ldk_server_protos::types::MaybeTimeoutClaimableHtlc { 287 | channel_id: channel_id.0.to_lower_hex_string(), 288 | counterparty_node_id: counterparty_node_id.to_string(), 289 | amount_satoshis, 290 | claimable_height, 291 | payment_hash: payment_hash.to_string(), 292 | outbound_payment, 293 | }, 294 | )), 295 | }, 296 | LightningBalance::MaybePreimageClaimableHTLC { 297 | channel_id, 298 | counterparty_node_id, 299 | amount_satoshis, 300 | expiry_height, 301 | payment_hash, 302 | } => ldk_server_protos::types::LightningBalance { 303 | balance_type: Some(MaybePreimageClaimableHtlc( 304 | ldk_server_protos::types::MaybePreimageClaimableHtlc { 305 | channel_id: channel_id.0.to_lower_hex_string(), 306 | counterparty_node_id: counterparty_node_id.to_string(), 307 | amount_satoshis, 308 | expiry_height, 309 | payment_hash: payment_hash.to_string(), 310 | }, 311 | )), 312 | }, 313 | LightningBalance::CounterpartyRevokedOutputClaimable { 314 | channel_id, 315 | counterparty_node_id, 316 | amount_satoshis, 317 | } => ldk_server_protos::types::LightningBalance { 318 | balance_type: Some(CounterpartyRevokedOutputClaimable( 319 | ldk_server_protos::types::CounterpartyRevokedOutputClaimable { 320 | channel_id: channel_id.0.to_lower_hex_string(), 321 | counterparty_node_id: counterparty_node_id.to_string(), 322 | amount_satoshis, 323 | }, 324 | )), 325 | }, 326 | } 327 | } 328 | 329 | pub(crate) fn pending_sweep_balance_to_proto( 330 | pending_sweep_balance: PendingSweepBalance, 331 | ) -> ldk_server_protos::types::PendingSweepBalance { 332 | match pending_sweep_balance { 333 | PendingSweepBalance::PendingBroadcast { channel_id, amount_satoshis } => { 334 | ldk_server_protos::types::PendingSweepBalance { 335 | balance_type: Some(PendingBroadcast(ldk_server_protos::types::PendingBroadcast { 336 | channel_id: channel_id.map(|c| c.0.to_lower_hex_string()), 337 | amount_satoshis, 338 | })), 339 | } 340 | }, 341 | PendingSweepBalance::BroadcastAwaitingConfirmation { 342 | channel_id, 343 | latest_broadcast_height, 344 | latest_spending_txid, 345 | amount_satoshis, 346 | } => ldk_server_protos::types::PendingSweepBalance { 347 | balance_type: Some(BroadcastAwaitingConfirmation( 348 | ldk_server_protos::types::BroadcastAwaitingConfirmation { 349 | channel_id: channel_id.map(|c| c.0.to_lower_hex_string()), 350 | latest_broadcast_height, 351 | latest_spending_txid: latest_spending_txid.to_string(), 352 | amount_satoshis, 353 | }, 354 | )), 355 | }, 356 | PendingSweepBalance::AwaitingThresholdConfirmations { 357 | channel_id, 358 | latest_spending_txid, 359 | confirmation_hash, 360 | confirmation_height, 361 | amount_satoshis, 362 | } => ldk_server_protos::types::PendingSweepBalance { 363 | balance_type: Some(AwaitingThresholdConfirmations( 364 | ldk_server_protos::types::AwaitingThresholdConfirmations { 365 | channel_id: channel_id.map(|c| c.0.to_lower_hex_string()), 366 | latest_spending_txid: latest_spending_txid.to_string(), 367 | confirmation_hash: confirmation_hash.to_string(), 368 | confirmation_height, 369 | amount_satoshis, 370 | }, 371 | )), 372 | }, 373 | } 374 | } 375 | 376 | pub(crate) fn forwarded_payment_to_proto( 377 | prev_channel_id: ChannelId, next_channel_id: ChannelId, 378 | prev_user_channel_id: Option, next_user_channel_id: Option, 379 | prev_node_id: Option, next_node_id: Option, 380 | total_fee_earned_msat: Option, skimmed_fee_msat: Option, claim_from_onchain_tx: bool, 381 | outbound_amount_forwarded_msat: Option, 382 | ) -> ForwardedPayment { 383 | ForwardedPayment { 384 | prev_channel_id: prev_channel_id.to_string(), 385 | next_channel_id: next_channel_id.to_string(), 386 | prev_user_channel_id: prev_user_channel_id 387 | .expect("prev_user_channel_id expected for ldk-server >=0.1") 388 | .0 389 | .to_string(), 390 | next_user_channel_id: next_user_channel_id.map(|u| u.0.to_string()), 391 | prev_node_id: prev_node_id.expect("prev_node_id expected for ldk-server >=0.1").to_string(), 392 | next_node_id: next_node_id.expect("next_node_id expected for ldk-node >=0.1").to_string(), 393 | total_fee_earned_msat, 394 | skimmed_fee_msat, 395 | claim_from_onchain_tx, 396 | outbound_amount_forwarded_msat, 397 | } 398 | } 399 | 400 | pub(crate) fn proto_to_bolt11_description( 401 | description: Option, 402 | ) -> Result { 403 | Ok(match description.and_then(|d| d.kind) { 404 | Some(bolt11_invoice_description::Kind::Direct(s)) => { 405 | Bolt11InvoiceDescription::Direct(Description::new(s).map_err(|e| { 406 | LdkServerError::new( 407 | InvalidRequestError, 408 | format!("Invalid invoice description: {}", e), 409 | ) 410 | })?) 411 | }, 412 | Some(bolt11_invoice_description::Kind::Hash(h)) => { 413 | let hash_bytes = <[u8; 32]>::from_hex(&h).map_err(|_| { 414 | LdkServerError::new( 415 | InvalidRequestError, 416 | "Invalid invoice description_hash, must be 32-byte hex string".to_string(), 417 | ) 418 | })?; 419 | Bolt11InvoiceDescription::Hash(Sha256(*sha256::Hash::from_bytes_ref(&hash_bytes))) 420 | }, 421 | None => { 422 | Bolt11InvoiceDescription::Direct(Description::new("".to_string()).map_err(|e| { 423 | LdkServerError::new( 424 | InvalidRequestError, 425 | format!("Invalid invoice description: {}", e), 426 | ) 427 | })?) 428 | }, 429 | }) 430 | } 431 | 432 | pub(crate) fn to_error_response(ldk_error: LdkServerError) -> (ErrorResponse, StatusCode) { 433 | let error_code = match ldk_error.error_code { 434 | InvalidRequestError => ErrorCode::InvalidRequestError, 435 | AuthError => ErrorCode::AuthError, 436 | LightningError => ErrorCode::LightningError, 437 | InternalServerError => ErrorCode::InternalServerError, 438 | } as i32; 439 | 440 | let status = match ldk_error.error_code { 441 | InvalidRequestError => StatusCode::BAD_REQUEST, 442 | AuthError => StatusCode::UNAUTHORIZED, 443 | LightningError => StatusCode::INTERNAL_SERVER_ERROR, 444 | InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, 445 | }; 446 | 447 | let error_response = ErrorResponse { message: ldk_error.message, error_code }; 448 | 449 | (error_response, status) 450 | } 451 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" 2 | fn_params_layout = "Compressed" 3 | hard_tabs = true 4 | use_field_init_shorthand = true 5 | max_width = 100 6 | match_block_trailing_comma = true 7 | # UNSTABLE: format_code_in_doc_comments = true 8 | # UNSTABLE: overflow_delimited_expr = true 9 | # UNSTABLE: comment_width = 100 10 | # UNSTABLE: format_macro_matchers = true 11 | # UNSTABLE: format_strings = true 12 | # UNSTABLE: group_imports = "StdExternalCrate" 13 | --------------------------------------------------------------------------------