diff --git a/Cargo.lock b/Cargo.lock index 6571908740..788f847296 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,9 +364,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bech32" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c7f7096bc256f5e5cb960f60dfc4f4ef979ca65abe7fb9d5a4f77150d3783d4" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" [[package]] name = "bellman" @@ -400,6 +400,95 @@ dependencies = [ "serde", ] +[[package]] +name = "bimap" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528c4b6f81eb2aadd3504da4ddc5bf5caec1b4aaf0d9dccfb8aaf2850f5b39c" + +[[package]] +name = "bitcoin" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a41df6ad9642c5c15ae312dd3d074de38fd3eb7cc87ad4ce10f90292a83fe4d" +dependencies = [ + "bech32", + "bitcoin_hashes", + "secp256k1", +] + +[[package]] +name = "bitcoin-cash" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35904d3f449fe9a01c5b8b872015415feea85cffc18b15170ddd665db9a78a82" +dependencies = [ + "base64 0.12.3", + "bimap", + "bitcoin-cash-base", + "bitcoin-cash-script-macro", + "bitflags", + "byteorder 1.4.3", + "error-chain", + "hex 0.4.3", + "hex-literal", + "num", + "num-derive", + "num-traits 0.2.12", + "ripemd160 0.9.1", + "serde", + "serde_derive", + "serde_json", + "sha-1 0.9.6", + "sha2 0.9.5", +] + +[[package]] +name = "bitcoin-cash-base" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d008cf4ceea63e1a9182c96ac320f263c53ddc70282b523f19500f454e490db" +dependencies = [ + "byteorder 1.4.3", + "hex 0.4.3", + "lazy_static", + "num", + "num-derive", + "num-traits 0.2.12", + "serde", + "serde_derive", +] + +[[package]] +name = "bitcoin-cash-script-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3469ca4ed0b2d142528565dae7f9bba9313a3f833e771e400ef0bc2269ee8e95" +dependencies = [ + "bitcoin-cash-base", + "proc-macro2", + "quote 1.0.7", + "regex", + "syn 1.0.72", + "tempfile", + "toolchain_find", +] + +[[package]] +name = "bitcoin-cash-slp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744837d4735aab854e4722a3a2ee942aaa96c6c02de7e06594e6511ef9fba309" +dependencies = [ + "bitcoin-cash", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" + [[package]] name = "bitcrypto" version = "0.1.0" @@ -407,7 +496,7 @@ dependencies = [ "groestl", "primitives", "ripemd160 0.8.0", - "sha-1", + "sha-1 0.8.2", "sha2 0.8.2", "sha3", "siphasher", @@ -753,6 +842,9 @@ dependencies = [ "async-trait", "base64 0.10.1", "bigdecimal", + "bitcoin", + "bitcoin-cash-slp", + "bitcoin_hashes", "bitcrypto", "byteorder 1.4.3", "bytes 0.4.12", @@ -769,7 +861,7 @@ dependencies = [ "futures 0.1.29", "futures 0.3.15", "gstuff", - "hex 0.3.2", + "hex 0.4.3", "http 0.2.1", "itertools 0.9.0", "js-sys", @@ -777,6 +869,10 @@ dependencies = [ "keys", "lazy_static", "libc", + "lightning", + "lightning-background-processor", + "lightning-net-tokio", + "lightning-persister", "metrics", "mocktopus", "num-traits 0.2.12", @@ -855,6 +951,8 @@ dependencies = [ "keys", "lazy_static", "libc", + "lightning", + "lightning-background-processor", "log 0.4.11", "log4rs", "metrics", @@ -862,7 +960,7 @@ dependencies = [ "metrics-runtime", "metrics-util", "num-bigint 0.2.6", - "num-rational", + "num-rational 0.2.4", "num-traits 0.2.12", "parking_lot 0.11.1", "parking_lot_core 0.6.2", @@ -1970,9 +2068,9 @@ checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" [[package]] name = "hex" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-literal" @@ -2750,6 +2848,50 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "lightning" +version = "0.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0bcd8d72c1177579fe7cfe42309967e11e432688b2e87c86d0d8b707d60530" +dependencies = [ + "bitcoin", + "secp256k1", +] + +[[package]] +name = "lightning-background-processor" +version = "0.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0629a41f3450eac5a702562cbcc52a3375b97524990c12442bdcd72dceb30c5" +dependencies = [ + "bitcoin", + "lightning", + "lightning-persister", +] + +[[package]] +name = "lightning-net-tokio" +version = "0.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a71819192c8aa987bf0db3ff1effd55bdc945d546ed2a401a137946c7ef2f88" +dependencies = [ + "bitcoin", + "lightning", + "tokio", +] + +[[package]] +name = "lightning-persister" +version = "0.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1888e0a7bc69df004b49b087648e95a3ee5540eb762768d9b4f35148234cd599" +dependencies = [ + "bitcoin", + "libc", + "lightning", + "winapi", +] + [[package]] name = "linked-hash-map" version = "0.5.3" @@ -3056,7 +3198,7 @@ dependencies = [ "gstuff", "hash-db", "hash256-std-hasher", - "hex 0.3.2", + "hex 0.4.3", "hex-literal", "http 0.2.1", "hyper", @@ -3068,7 +3210,7 @@ dependencies = [ "metrics", "mm2-libp2p", "mocktopus", - "num-rational", + "num-rational 0.2.4", "num-traits 0.2.12", "parity-util-mem", "parking_lot 0.11.1", @@ -3115,13 +3257,13 @@ dependencies = [ "env_logger", "futures 0.3.15", "getrandom 0.2.2", - "hex 0.4.2", + "hex 0.4.3", "lazy_static", "libp2p", "libp2p-floodsub 0.22.0", "log 0.4.11", "num-bigint 0.2.6", - "num-rational", + "num-rational 0.2.4", "rand 0.7.3", "regex", "rmp-serde", @@ -3254,6 +3396,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3e176191bc4faad357e3122c4747aa098ac880e88b168f106386128736cf4a" +dependencies = [ + "num-bigint 0.3.2", + "num-complex", + "num-integer", + "num-iter", + "num-rational 0.3.2", + "num-traits 0.2.12", +] + [[package]] name = "num-bigint" version = "0.2.6" @@ -3277,6 +3433,26 @@ dependencies = [ "num-traits 0.2.12", ] +[[package]] +name = "num-complex" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5" +dependencies = [ + "num-traits 0.2.12", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote 1.0.7", + "syn 1.0.72", +] + [[package]] name = "num-integer" version = "0.1.43" @@ -3287,6 +3463,17 @@ dependencies = [ "num-traits 0.2.12", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg 1.0.0", + "num-integer", + "num-traits 0.2.12", +] + [[package]] name = "num-rational" version = "0.2.4" @@ -3300,6 +3487,18 @@ dependencies = [ "serde", ] +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg 1.0.0", + "num-bigint 0.3.2", + "num-integer", + "num-traits 0.2.12", +] + [[package]] name = "num-traits" version = "0.1.43" @@ -4433,6 +4632,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scoped-tls" version = "1.0.0" @@ -4477,9 +4685,9 @@ dependencies = [ [[package]] name = "secp256k1" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee5070fdc6f26ca5be6dcfc3d07c76fdb974a63a8b246b459854274145f5a258" +checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" dependencies = [ "rand 0.6.5", "secp256k1-sys", @@ -4688,6 +4896,19 @@ dependencies = [ "opaque-debug 0.2.3", ] +[[package]] +name = "sha-1" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + [[package]] name = "sha1" version = "0.6.0" @@ -4884,7 +5105,7 @@ dependencies = [ "httparse", "log 0.4.11", "rand 0.7.3", - "sha-1", + "sha-1 0.8.2", ] [[package]] @@ -5486,6 +5707,19 @@ dependencies = [ "serde", ] +[[package]] +name = "toolchain_find" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e458af37ead6107144c2e3bb892f605ddfad20251f12143cda8b9c9072b1ca45" +dependencies = [ + "dirs", + "lazy_static", + "regex", + "semver 0.9.0", + "walkdir", +] + [[package]] name = "tower-service" version = "0.3.0" @@ -5630,7 +5864,7 @@ checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" dependencies = [ "byteorder 1.4.3", "crunchy 0.2.2", - "hex 0.4.2", + "hex 0.4.3", "static_assertions", ] @@ -5828,6 +6062,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9571542c2ce85ce642e6b58b3364da2fb53526360dfb7c211add4f5c23105ff7" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" @@ -6134,7 +6379,7 @@ dependencies = [ "bs58", "ff", "group", - "hex 0.4.2", + "hex 0.4.3", "jubjub", "nom", "percent-encoding 2.1.0", @@ -6178,7 +6423,7 @@ dependencies = [ "fpe", "funty", "group", - "hex 0.4.2", + "hex 0.4.3", "jubjub", "lazy_static", "log 0.4.11", diff --git a/Cargo.toml b/Cargo.toml index abe42a28b9..2946b12d07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ futures = { version = "0.3.1", package = "futures", features = ["compat", "async gstuff = { version = "0.7", features = ["nightly"] } hash256-std-hasher = "0.15.2" hash-db = "0.15.2" -hex = "0.3.2" +hex = "0.4.2" hex-literal = "0.3.1" http = "0.2" itertools = "0.9" diff --git a/etomic_build/client/enable_USDF b/etomic_build/client/enable_USDF old mode 100755 new mode 100644 diff --git a/etomic_build/client/enable_tBCH b/etomic_build/client/enable_tBCH old mode 100755 new mode 100644 diff --git a/etomic_build/client/validate_address b/etomic_build/client/validate_address old mode 100755 new mode 100644 diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 3f92995c59..2d54497fe0 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -16,6 +16,9 @@ async-std = { version = "1.5", features = ["unstable"] } async-trait = "0.1" base64 = "0.10.0" bigdecimal = { version = "0.1.0", features = ["serde"] } +bitcoin = "0.27.1" +bitcoin-cash-slp = "0.3.1" +bitcoin_hashes = "0.10.0" bitcrypto = { path = "../mm2_bitcoin/crypto" } byteorder = "1.3" bytes = "0.4" @@ -34,13 +37,14 @@ futures01 = { version = "0.1", package = "futures" } # using select macro requires the crate to be named futures, compilation failed with futures03 name futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } gstuff = { version = "0.7", features = ["nightly"] } -hex = "0.3.2" +hex = "0.4.2" http = "0.2" itertools = "0.9" jsonrpc-core = "8.0.1" keys = { path = "../mm2_bitcoin/keys" } lazy_static = "1.4" libc = "0.2" +lightning = "0.0.101" metrics = "0.12" mocktopus = "0.7.0" num-traits = "0.2" @@ -75,6 +79,9 @@ web-sys = { version = "0.3.4", features = ["console", "Headers", "Request", "Req [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dirs = { version = "1" } +lightning-background-processor = "0.0.101" +lightning-persister = "0.0.101" +lightning-net-tokio = "0.0.101" rusqlite = { version = "0.24.2", features = ["bundled"], optional = true } rust-ini = { version = "0.13" } rustls = { version = "0.19", features = ["dangerous_configuration"] } diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs new file mode 100644 index 0000000000..50f50b6818 --- /dev/null +++ b/mm2src/coins/lightning.rs @@ -0,0 +1,100 @@ +#[cfg(not(target_arch = "wasm32"))] +use crate::utxo::rpc_clients::UtxoRpcClientEnum; +#[cfg(not(target_arch = "wasm32"))] +use common::ip_addr::myipaddr; +use common::mm_ctx::MmArc; +use common::mm_error::prelude::*; +use ln_errors::{EnableLightningError, EnableLightningResult}; +#[cfg(not(target_arch = "wasm32"))] +use ln_utils::{network_from_string, start_lightning, LightningConf}; + +#[cfg(not(target_arch = "wasm32"))] +use super::{lp_coinfind_or_err, MmCoinEnum}; + +mod ln_errors; +mod ln_rpc; +#[cfg(not(target_arch = "wasm32"))] mod ln_utils; + +#[derive(Deserialize)] +pub struct EnableLightningRequest { + pub coin: String, + pub port: Option, + pub name: String, + pub color: Option, +} + +#[cfg(target_arch = "wasm32")] +pub async fn enable_lightning(_ctx: MmArc, _req: EnableLightningRequest) -> EnableLightningResult { + MmError::err(EnableLightningError::UnsupportedMode( + "'enable_lightning'".into(), + "native".into(), + )) +} + +/// Start a BTC lightning node (LTC should be added later). +#[cfg(not(target_arch = "wasm32"))] +pub async fn enable_lightning(ctx: MmArc, req: EnableLightningRequest) -> EnableLightningResult { + // coin has to be enabled in electrum to start a lightning node + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + + let utxo_coin = match coin { + MmCoinEnum::UtxoCoin(utxo) => utxo, + _ => { + return MmError::err(EnableLightningError::UnsupportedCoin( + req.coin, + "Only utxo coins are supported in lightning".into(), + )) + }, + }; + + if !utxo_coin.as_ref().conf.lightning { + return MmError::err(EnableLightningError::UnsupportedCoin( + req.coin, + "'lightning' field not found in coin config".into(), + )); + } + + let client = match &utxo_coin.as_ref().rpc_client { + UtxoRpcClientEnum::Electrum(c) => c, + UtxoRpcClientEnum::Native(_) => { + return MmError::err(EnableLightningError::UnsupportedMode( + "Lightning network".into(), + "electrum".into(), + )) + }, + }; + + let network = match &utxo_coin.as_ref().conf.network { + Some(n) => network_from_string(n.clone())?, + None => { + return MmError::err(EnableLightningError::UnsupportedCoin( + req.coin, + "'network' field not found in coin config".into(), + )) + }, + }; + + if req.name.len() > 32 { + return MmError::err(EnableLightningError::InvalidRequest( + "Node name length can't be more than 32 characters".into(), + )); + } + let node_name = format!("{}{:width$}", req.name, " ", width = 32 - req.name.len()); + + let mut node_color = [0u8; 3]; + hex::decode_to_slice( + req.color.unwrap_or_else(|| "000000".into()), + &mut node_color as &mut [u8], + ) + .map_to_mm(|_| EnableLightningError::InvalidRequest("Invalid Hex Color".into()))?; + + let listen_addr = myipaddr(ctx.clone()) + .await + .map_to_mm(EnableLightningError::InvalidAddress)?; + let port = req.port.unwrap_or(9735); + + let conf = LightningConf::new(client.clone(), network, listen_addr, port, node_name, node_color); + start_lightning(&ctx, conf).await?; + + Ok("success".into()) +} diff --git a/mm2src/coins/lightning/ln_errors.rs b/mm2src/coins/lightning/ln_errors.rs new file mode 100644 index 0000000000..7dc5f4e6e4 --- /dev/null +++ b/mm2src/coins/lightning/ln_errors.rs @@ -0,0 +1,61 @@ +use crate::CoinFindError; +use common::mm_error::prelude::*; +use common::HttpStatusCode; +use derive_more::Display; +use http::StatusCode; + +pub type EnableLightningResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum EnableLightningError { + #[display(fmt = "Invalid request: {}", _0)] + InvalidRequest(String), + #[display(fmt = "Invalid address: {}", _0)] + InvalidAddress(String), + #[display(fmt = "Invalid path: {}", _0)] + InvalidPath(String), + #[display(fmt = "Lightning node already running")] + AlreadyRunning, + #[display(fmt = "{} is only supported in {} mode", _0, _1)] + UnsupportedMode(String, String), + #[display(fmt = "Lightning network is not supported for {}: {}", _0, _1)] + UnsupportedCoin(String, String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "System time error {}", _0)] + SystemTimeError(String), + #[display(fmt = "I/O error {}", _0)] + IOError(String), + #[display(fmt = "Hash error {}", _0)] + HashError(String), + #[display(fmt = "RPC error {}", _0)] + RpcError(String), +} + +impl HttpStatusCode for EnableLightningError { + fn status_code(&self) -> StatusCode { + match self { + EnableLightningError::InvalidRequest(_) + | EnableLightningError::RpcError(_) + | EnableLightningError::UnsupportedCoin(_, _) => StatusCode::BAD_REQUEST, + EnableLightningError::AlreadyRunning | EnableLightningError::UnsupportedMode(_, _) => { + StatusCode::METHOD_NOT_ALLOWED + }, + EnableLightningError::InvalidAddress(_) + | EnableLightningError::InvalidPath(_) + | EnableLightningError::SystemTimeError(_) + | EnableLightningError::IOError(_) + | EnableLightningError::HashError(_) => StatusCode::INTERNAL_SERVER_ERROR, + EnableLightningError::NoSuchCoin(_) => StatusCode::PRECONDITION_REQUIRED, + } + } +} + +impl From for EnableLightningError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => EnableLightningError::NoSuchCoin(coin), + } + } +} diff --git a/mm2src/coins/lightning/ln_rpc.rs b/mm2src/coins/lightning/ln_rpc.rs new file mode 100644 index 0000000000..822f78592a --- /dev/null +++ b/mm2src/coins/lightning/ln_rpc.rs @@ -0,0 +1,73 @@ +use crate::utxo::rpc_clients::{electrum_script_hash, ElectrumClient, UtxoRpcClientOps, UtxoRpcError}; +use bitcoin::blockdata::script::Script; +use bitcoin::blockdata::transaction::Transaction; +use bitcoin::consensus::encode; +use bitcoin::hash_types::Txid; +use common::block_on; +use common::mm_error::prelude::MapToMmFutureExt; +use futures::compat::Future01CompatExt; +use lightning::chain::{chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}, + Filter, WatchedOutput}; +use rpc::v1::types::Bytes as BytesJson; + +impl FeeEstimator for ElectrumClient { + // Gets estimated satoshis of fee required per 1000 Weight-Units. + // TODO: use fn estimate_fee instead of fixed number when starting work on opening channels + fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 { + match confirmation_target { + // fetch background feerate + ConfirmationTarget::Background => 253, + // fetch normal feerate (~6 blocks) + ConfirmationTarget::Normal => 2000, + // fetch high priority feerate + ConfirmationTarget::HighPriority => 5000, + } + } +} + +impl BroadcasterInterface for ElectrumClient { + fn broadcast_transaction(&self, tx: &Transaction) { + let tx_bytes = BytesJson::from(encode::serialize_hex(tx).as_bytes()); + let _ = Box::new( + self.blockchain_transaction_broadcast(tx_bytes) + .map_to_mm_fut(UtxoRpcError::from), + ); + } +} + +impl Filter for ElectrumClient { + // Watches for this transaction on-chain + fn register_tx(&self, _txid: &Txid, _script_pubkey: &Script) { unimplemented!() } + + // Watches for any transactions that spend this output on-chain + fn register_output(&self, output: WatchedOutput) -> Option<(usize, Transaction)> { + let selfi = self.clone(); + let script_hash = hex::encode(electrum_script_hash(output.script_pubkey.as_ref())); + let history = block_on(selfi.scripthash_get_history(&script_hash).compat()).unwrap_or_default(); + + if history.len() < 2 { + return None; + } + + for item in history.iter() { + let transaction = match block_on(selfi.get_transaction_bytes(item.tx_hash.clone()).compat()) { + Ok(tx) => tx, + Err(_) => continue, + }; + + let maybe_spend_tx: Transaction = match encode::deserialize(transaction.as_slice()) { + Ok(tx) => tx, + Err(_) => continue, + }; + + for (index, input) in maybe_spend_tx.input.iter().enumerate() { + if input.previous_output.txid == output.outpoint.txid + && input.previous_output.vout == output.outpoint.index as u32 + { + return Some((index, maybe_spend_tx)); + } + } + } + None + } +} diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs new file mode 100644 index 0000000000..3846e88565 --- /dev/null +++ b/mm2src/coins/lightning/ln_utils.rs @@ -0,0 +1,445 @@ +use super::*; +use crate::utxo::rpc_clients::{BestBlock as RpcBestBlock, ElectrumBlockHeader, ElectrumClient, ElectrumNonce, + UtxoRpcClientOps}; +use bitcoin::blockdata::block::BlockHeader; +use bitcoin::blockdata::constants::genesis_block; +use bitcoin::consensus::encode::deserialize; +use bitcoin::hash_types::{BlockHash, TxMerkleNode}; +use bitcoin::network::constants::Network; +use bitcoin_hashes::{sha256d, Hash}; +use common::executor::{spawn, Timer}; +use common::ip_addr::fetch_external_ip; +use common::log; +use common::log::LogState; +use common::mm_ctx::MmArc; +use futures::compat::Future01CompatExt; +use lightning::chain::keysinterface::{InMemorySigner, KeysInterface, KeysManager}; +use lightning::chain::{chainmonitor, Access, BestBlock, Confirm}; +use lightning::ln::channelmanager; +use lightning::ln::channelmanager::{ChainParameters, SimpleArcChannelManager}; +use lightning::ln::msgs::NetAddress; +use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler, SimpleArcPeerManager}; +use lightning::routing::network_graph::{NetGraphMsgHandler, NetworkGraph}; +use lightning::util::config::UserConfig; +use lightning::util::events::Event; +use lightning_background_processor::BackgroundProcessor; +use lightning_net_tokio::SocketDescriptor; +use lightning_persister::FilesystemPersister; +use rand::RngCore; +use std::convert::TryInto; +use std::net::{IpAddr, Ipv4Addr}; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::SystemTime; +use tokio::net::TcpListener; + +const CHECK_FOR_NEW_BEST_BLOCK_INTERVAL: u64 = 60; +const BROADCAST_NODE_ANNOUNCEMENT_INTERVAL: u64 = 60; + +type ChainMonitor = chainmonitor::ChainMonitor< + InMemorySigner, + Arc, + Arc, + Arc, + Arc, + Arc, +>; + +type ChannelManager = channelmanager::ChannelManager< + InMemorySigner, + Arc, + Arc, + Arc, + Arc, + Arc, +>; + +type PeerManager = SimpleArcPeerManager< + SocketDescriptor, + ChainMonitor, + ElectrumClient, + ElectrumClient, + dyn Access + Send + Sync, + LogState, +>; + +type SimpleChannelManager = SimpleArcChannelManager; + +#[derive(Debug)] +pub struct LightningConf { + /// RPC client (Using only electrum for now as part of the PoC) + pub rpc_client: ElectrumClient, + // Mainnet/Testnet/Signet/RegTest + pub network: Network, + // The listening port for the p2p LN node + pub listening_port: u16, + /// The set (possibly empty) of socket addresses on which this node accepts incoming connections. + /// If the user wishes to preserve privacy, addresses should likely contain only Tor Onion addresses. + pub listening_addr: IpAddr, + // Printable human-readable string to describe this node to other users. + pub node_name: [u8; 32], + // Node's RGB color. This is used for showing the node in a network graph with the desired color. + pub node_color: [u8; 3], +} + +impl LightningConf { + pub fn new( + rpc_client: ElectrumClient, + network: Network, + listening_addr: IpAddr, + listening_port: u16, + node_name: String, + node_color: [u8; 3], + ) -> Self { + LightningConf { + rpc_client, + network, + listening_port, + listening_addr, + node_name: node_name.as_bytes().try_into().expect("Node name has incorrect length"), + node_color, + } + } +} + +pub fn network_from_string(network: String) -> EnableLightningResult { + network + .as_str() + .parse::() + .map_to_mm(|e| EnableLightningError::InvalidRequest(e.to_string())) +} + +// TODO: add TOR address option +fn netaddress_from_ipaddr(addr: IpAddr, port: u16) -> Vec { + if addr == Ipv4Addr::new(0, 0, 0, 0) || addr == Ipv4Addr::new(127, 0, 0, 1) { + return Vec::new(); + } + let mut addresses = Vec::new(); + let address = match addr { + IpAddr::V4(addr) => NetAddress::IPv4 { + addr: u32::from(addr).to_be_bytes(), + port, + }, + IpAddr::V6(addr) => NetAddress::IPv6 { + addr: u128::from(addr).to_be_bytes(), + port, + }, + }; + addresses.push(address); + addresses +} + +fn my_ln_data_dir(ctx: &MmArc) -> PathBuf { ctx.dbdir().join("LIGHTNING") } + +// TODO: Implement all the cases +async fn handle_ln_events(event: &Event) { + match event { + Event::FundingGenerationReady { .. } => (), + Event::PaymentReceived { .. } => (), + Event::PaymentSent { .. } => (), + Event::PaymentPathFailed { .. } => (), + Event::PendingHTLCsForwardable { .. } => (), + Event::SpendableOutputs { .. } => (), + Event::PaymentForwarded { .. } => (), + Event::ChannelClosed { .. } => (), + } +} + +pub async fn start_lightning(ctx: &MmArc, conf: LightningConf) -> EnableLightningResult<()> { + if ctx.ln_background_processor.is_some() { + return MmError::err(EnableLightningError::AlreadyRunning); + } + // Initialize the FeeEstimator. rpc_client implements the FeeEstimator trait, so it'll act as our fee estimator. + let fee_estimator = Arc::new(conf.rpc_client.clone()); + + // Initialize the Logger + let logger = ctx.log.clone(); + + // Initialize the BroadcasterInterface. rpc_client implements the BroadcasterInterface trait, so it'll act as our transaction + // broadcaster. + let broadcaster = Arc::new(conf.rpc_client.clone()); + + // Initialize Persist + let ln_data_dir = my_ln_data_dir(ctx) + .as_path() + .to_str() + .ok_or("Data dir is a non-UTF-8 string") + .map_to_mm(|e| EnableLightningError::InvalidPath(e.into()))? + .to_string(); + let persister = Arc::new(FilesystemPersister::new(ln_data_dir.clone())); + + // Initialize the Filter. rpc_client implements the Filter trait, so it'll act as our filter. + let filter = Some(Arc::new(conf.rpc_client.clone())); + + // Initialize the ChainMonitor + let chain_monitor: Arc = Arc::new(chainmonitor::ChainMonitor::new( + filter.clone(), + broadcaster.clone(), + logger.0.clone(), + fee_estimator.clone(), + persister.clone(), + )); + + let seed: [u8; 32] = ctx.secp256k1_key_pair().private().secret.into(); + + // The current time is used to derive random numbers from the seed where required, to ensure all random generation is unique across restarts. + let cur = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_to_mm(|e| EnableLightningError::SystemTimeError(e.to_string()))?; + + // Initialize the KeysManager + let keys_manager = Arc::new(KeysManager::new(&seed, cur.as_secs(), cur.subsec_nanos())); + + // Read ChannelMonitor state from disk, important for lightning node is restarting and has at least 1 channel + let channelmonitors = persister + .read_channelmonitors(keys_manager.clone()) + .map_to_mm(|e| EnableLightningError::IOError(e.to_string()))?; + + // This is used for Electrum only to prepare for chain synchronization + if let Some(ref filter) = filter { + for (_, chan_mon) in channelmonitors.iter() { + chan_mon.load_outputs_to_watch(filter); + } + } + + // Initialize the ChannelManager to starting a new node without history + // TODO: Add the case of restarting a node + let mut user_config = UserConfig::default(); + + // When set to false an incoming channel doesn't have to match our announced channel preference which allows public channels + // TODO: Add user config to LightningConf maybe get it from coin config + user_config + .peer_channel_config_limits + .force_announced_channel_preference = false; + + let best_block = conf + .rpc_client + .get_best_block() + .compat() + .await + .mm_err(|e| EnableLightningError::RpcError(e.to_string()))?; + let best_block_hash = + sha256d::Hash::from_slice(&best_block.hash.0).map_to_mm(|e| EnableLightningError::HashError(e.to_string()))?; + let chain_params = ChainParameters { + network: conf.network, + best_block: BestBlock::new(BlockHash::from_hash(best_block_hash), best_block.height as u32), + }; + let new_channel_manager = Arc::new(channelmanager::ChannelManager::new( + fee_estimator, + chain_monitor.clone(), + broadcaster, + logger.0.clone(), + keys_manager.clone(), + user_config, + chain_params, + )); + + // Initialize the NetGraphMsgHandler. This is used for providing routes to send payments over + let genesis = genesis_block(conf.network).header.block_hash(); + let router = Arc::new(NetGraphMsgHandler::new( + NetworkGraph::new(genesis), + None::>, + logger.0.clone(), + )); + + // Initialize the PeerManager + // ephemeral_random_data is used to derive per-connection ephemeral keys + let mut ephemeral_bytes = [0; 32]; + rand::thread_rng().fill_bytes(&mut ephemeral_bytes); + let lightning_msg_handler = MessageHandler { + chan_handler: new_channel_manager.clone(), + route_handler: router.clone(), + }; + // IgnoringMessageHandler is used as custom message types (experimental and application-specific messages) is not needed + let peer_manager: Arc = Arc::new(PeerManager::new( + lightning_msg_handler, + keys_manager.get_node_secret(), + &ephemeral_bytes, + logger.0.clone(), + Arc::new(IgnoringMessageHandler {}), + )); + + // Initialize p2p networking + let listener = TcpListener::bind(format!("{}:{}", conf.listening_addr, conf.listening_port)) + .await + .map_to_mm(|e| EnableLightningError::IOError(e.to_string()))?; + spawn(ln_p2p_loop(ctx.clone(), peer_manager.clone(), listener)); + + // Update best block whenever there's a new chain tip or a block has been newly disconnected + spawn(ln_best_block_update_loop( + ctx.clone(), + chain_monitor.clone(), + new_channel_manager.clone(), + conf.rpc_client.clone(), + best_block, + )); + + // Handle LN Events + // TODO: Implement EventHandler trait instead of this + let handle = tokio::runtime::Handle::current(); + let event_handler = move |event: &Event| handle.block_on(handle_ln_events(event)); + + // Persist ChannelManager + // Note: if the ChannelManager is not persisted properly to disk, there is risk of channels force closing the next time LN starts up + let persist_channel_manager_callback = + move |node: &SimpleChannelManager| FilesystemPersister::persist_manager(ln_data_dir.clone(), &*node); + + // Start Background Processing. Runs tasks periodically in the background to keep LN node operational + let background_processor = BackgroundProcessor::start( + persist_channel_manager_callback, + event_handler, + chain_monitor, + new_channel_manager.clone(), + Some(router), + peer_manager, + logger.0, + ); + + if ctx.ln_background_processor.pin(background_processor).is_err() { + return MmError::err(EnableLightningError::AlreadyRunning); + }; + + // Broadcast Node Announcement + spawn(ln_node_announcement_loop( + ctx.clone(), + new_channel_manager, + conf.node_name, + conf.node_color, + conf.listening_addr, + conf.listening_port, + )); + + Ok(()) +} + +async fn ln_p2p_loop(ctx: MmArc, peer_manager: Arc, listener: TcpListener) { + loop { + if ctx.is_stopping() { + break; + } + let peer_mgr = peer_manager.clone(); + let tcp_stream = match listener.accept().await { + Ok((stream, addr)) => { + log::debug!("New incoming lightning connection from peer address: {}", addr); + stream + }, + Err(e) => { + log::error!("Error on accepting lightning connection: {}", e); + continue; + }, + }; + if let Ok(stream) = tcp_stream.into_std() { + spawn(async move { + lightning_net_tokio::setup_inbound(peer_mgr.clone(), stream).await; + }) + }; + } +} + +async fn ln_best_block_update_loop( + ctx: MmArc, + chain_monitor: Arc, + channel_manager: Arc, + best_header_listener: ElectrumClient, + best_block: RpcBestBlock, +) { + let mut current_best_block = best_block; + loop { + if ctx.is_stopping() { + break; + } + let best_header = match best_header_listener.blockchain_headers_subscribe().compat().await { + Ok(h) => h, + Err(e) => { + log::error!("Error while requesting best header for lightning node: {}", e); + Timer::sleep(CHECK_FOR_NEW_BEST_BLOCK_INTERVAL as f64).await; + continue; + }, + }; + if current_best_block != best_header.clone().into() { + current_best_block = best_header.clone().into(); + let (new_best_header, new_best_height) = match best_header { + ElectrumBlockHeader::V12(h) => { + let nonce = match h.nonce { + ElectrumNonce::Number(n) => n as u32, + ElectrumNonce::Hash(_) => { + Timer::sleep(CHECK_FOR_NEW_BEST_BLOCK_INTERVAL as f64).await; + continue; + }, + }; + let prev_blockhash = match sha256d::Hash::from_slice(&h.prev_block_hash.0) { + Ok(h) => h, + Err(e) => { + log::error!("Error while parsing previous block hash for lightning node: {}", e); + Timer::sleep(CHECK_FOR_NEW_BEST_BLOCK_INTERVAL as f64).await; + continue; + }, + }; + let merkle_root = match sha256d::Hash::from_slice(&h.merkle_root.0) { + Ok(h) => h, + Err(e) => { + log::error!("Error while parsing merkle root for lightning node: {}", e); + Timer::sleep(CHECK_FOR_NEW_BEST_BLOCK_INTERVAL as f64).await; + continue; + }, + }; + ( + BlockHeader { + version: h.version as i32, + prev_blockhash: BlockHash::from_hash(prev_blockhash), + merkle_root: TxMerkleNode::from_hash(merkle_root), + time: h.timestamp as u32, + bits: h.bits as u32, + nonce, + }, + h.block_height as u32, + ) + }, + ElectrumBlockHeader::V14(h) => ( + deserialize(&h.hex.into_vec()).expect("Can't deserialize block header"), + h.height as u32, + ), + }; + channel_manager.best_block_updated(&new_best_header, new_best_height); + chain_monitor.best_block_updated(&new_best_header, new_best_height); + } + Timer::sleep(CHECK_FOR_NEW_BEST_BLOCK_INTERVAL as f64).await; + } +} + +async fn ln_node_announcement_loop( + ctx: MmArc, + channel_manager: Arc, + node_name: [u8; 32], + node_color: [u8; 3], + addr: IpAddr, + port: u16, +) { + let addresses = netaddress_from_ipaddr(addr, port); + loop { + if ctx.is_stopping() { + break; + } + + let addresses_to_announce = if addresses.is_empty() { + // Right now if the node is behind NAT the external ip is fetched on every loop + // If the node does not announce a public IP, it will not be displayed on the network graph, + // and other nodes will not be able to open a channel with it. But it can open channels with other nodes. + // TODO: Fetch external ip on reconnection only + match fetch_external_ip().await { + Ok(ip) => netaddress_from_ipaddr(ip, port), + Err(e) => { + log::error!("Error while fetching external ip for node announcement: {}", e); + Timer::sleep(BROADCAST_NODE_ANNOUNCEMENT_INTERVAL as f64).await; + continue; + }, + } + } else { + addresses.clone() + }; + + channel_manager.broadcast_node_announcement(node_color, node_name, addresses_to_announce); + + Timer::sleep(BROADCAST_NODE_ANNOUNCEMENT_INTERVAL as f64).await; + } +} diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 37d83f73e9..ace7b57d1f 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -96,6 +96,8 @@ use utxo::{GenerateTxError, UtxoFeeDetails, UtxoTx}; pub mod qrc20; use qrc20::{qrc20_coin_from_conf_and_params, Qrc20Coin, Qrc20FeeDetails}; +pub mod lightning; + #[doc(hidden)] #[allow(unused_variables)] pub mod test_coin; diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 0856d3b285..b1f09bb3fc 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -428,6 +428,11 @@ pub struct UtxoCoinConf { pub mature_confirmations: u32, /// The number of blocks used for estimate_fee/estimate_smart_fee RPC calls pub estimate_fee_blocks: u32, + /// Defines if the coin can be used in the lightning network + /// For now BTC is only supported by LDK but in the future any segwit coins can be supported in lightning network + pub lightning: bool, + /// bitcoin/testnet/signet/regtest Needed for lightning node to know which network to connect to + pub network: Option, } #[derive(Debug)] @@ -1058,6 +1063,8 @@ impl<'a> UtxoConfBuilder<'a> { let mtp_block_count = self.mtp_block_count(); let estimate_fee_mode = self.estimate_fee_mode(); let estimate_fee_blocks = self.estimate_fee_blocks(); + let lightning = self.lightning(); + let network = self.network(); Ok(UtxoCoinConf { ticker: self.ticker.to_owned(), @@ -1087,6 +1094,8 @@ impl<'a> UtxoConfBuilder<'a> { estimate_fee_mode, mature_confirmations, estimate_fee_blocks, + lightning, + network, }) } @@ -1247,6 +1256,16 @@ impl<'a> UtxoConfBuilder<'a> { } fn estimate_fee_blocks(&self) -> u32 { json::from_value(self.conf["estimate_fee_blocks"].clone()).unwrap_or(1) } + + fn lightning(&self) -> bool { + if self.segwit() && self.bech32_hrp().is_some() { + self.conf["lightning"].as_bool().unwrap_or(false) + } else { + false + } + } + + fn network(&self) -> Option { json::from_value(self.conf["network"].clone()).unwrap_or(None) } } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index ee985aac58..a4d4275e39 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -4,7 +4,7 @@ use crate::utxo::{output_script, sat_from_big_decimal}; use crate::{NumConversError, RpcTransportEventHandler, RpcTransportEventHandlerShared}; use bigdecimal::BigDecimal; -use chain::{BlockHeader, OutPoint, Transaction as UtxoTx}; +use chain::{BlockHeader, BlockHeaderBits, BlockHeaderNonce, OutPoint, Transaction as UtxoTx}; use common::custom_futures::{select_ok_sequential, FutureTimerExt}; use common::executor::{spawn, Timer}; use common::jsonrpc_client::{JsonRpcClient, JsonRpcError, JsonRpcErrorType, JsonRpcMultiClient, JsonRpcRemoteAddr, @@ -243,6 +243,8 @@ pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { fn get_block_count(&self) -> UtxoRpcFut; + fn get_best_block(&self) -> UtxoRpcFut; + fn display_balance(&self, address: Address, decimals: u8) -> RpcRes; /// returns fee estimation per KByte in satoshis @@ -626,6 +628,8 @@ impl UtxoRpcClientOps for NativeClient { Box::new(self.0.get_block_count().map_to_mm_fut(UtxoRpcError::from)) } + fn get_best_block(&self) -> UtxoRpcFut { unimplemented!() } + fn display_balance(&self, address: Address, _decimals: u8) -> RpcRes { Box::new( self.list_unspent_impl(0, std::i32::MAX, vec![address.to_string()]) @@ -936,13 +940,23 @@ pub struct ElectrumUnspent { pub value: u64, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(untagged)] pub enum ElectrumNonce { Number(u64), Hash(H256Json), } +#[allow(clippy::from_over_into)] +impl Into for ElectrumNonce { + fn into(self) -> BlockHeaderNonce { + match self { + ElectrumNonce::Number(n) => BlockHeaderNonce::U32(n as u32), + ElectrumNonce::Hash(h) => BlockHeaderNonce::H256(h.into()), + } + } +} + #[derive(Debug, Deserialize)] pub struct ElectrumBlockHeadersRes { count: u64, @@ -951,31 +965,73 @@ pub struct ElectrumBlockHeadersRes { } /// The block header compatible with Electrum 1.2 -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct ElectrumBlockHeaderV12 { - bits: u64, - block_height: u64, - merkle_root: H256Json, - nonce: ElectrumNonce, - prev_block_hash: H256Json, - timestamp: u64, - version: u64, + pub bits: u64, + pub block_height: u64, + pub merkle_root: H256Json, + pub nonce: ElectrumNonce, + pub prev_block_hash: H256Json, + pub timestamp: u64, + pub version: u64, +} + +impl ElectrumBlockHeaderV12 { + pub fn hash(&self) -> H256Json { + let block_header = BlockHeader { + version: self.version as u32, + previous_header_hash: self.prev_block_hash.clone().into(), + merkle_root_hash: self.merkle_root.clone().into(), + hash_final_sapling_root: None, + time: self.timestamp as u32, + bits: BlockHeaderBits::U32(self.bits as u32), + nonce: self.nonce.clone().into(), + solution: None, + aux_pow: None, + mtp_pow: None, + is_verus: false, + hash_state_root: None, + hash_utxo_root: None, + prevout_stake: None, + vch_block_sig_dlgt: None, + }; + BlockHeader::hash(&block_header).into() + } } /// The block header compatible with Electrum 1.4 -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct ElectrumBlockHeaderV14 { - height: u64, - hex: BytesJson, + pub height: u64, + pub hex: BytesJson, } -#[derive(Debug, Deserialize)] +impl ElectrumBlockHeaderV14 { + pub fn hash(&self) -> H256Json { self.hex.clone().into_vec()[..].into() } +} + +#[derive(Clone, Debug, Deserialize)] #[serde(untagged)] pub enum ElectrumBlockHeader { V12(ElectrumBlockHeaderV12), V14(ElectrumBlockHeaderV14), } +#[derive(Debug, PartialEq)] +pub struct BestBlock { + pub height: u64, + pub hash: H256Json, +} + +impl From for BestBlock { + fn from(block_header: ElectrumBlockHeader) -> Self { + BestBlock { + height: block_header.block_height(), + hash: block_header.block_hash(), + } + } +} + #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Deserialize, Serialize)] pub enum EstimateFeeMode { @@ -991,6 +1047,13 @@ impl ElectrumBlockHeader { ElectrumBlockHeader::V14(h) => h.height, } } + + fn block_hash(&self) -> H256Json { + match self { + ElectrumBlockHeader::V12(h) => h.hash(), + ElectrumBlockHeader::V14(h) => h.hash(), + } + } } #[derive(Debug, Deserialize)] @@ -1482,7 +1545,7 @@ impl ElectrumClient { } /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-broadcast - fn blockchain_transaction_broadcast(&self, tx: BytesJson) -> RpcRes { + pub fn blockchain_transaction_broadcast(&self, tx: BytesJson) -> RpcRes { rpc_func!(self, "blockchain.transaction.broadcast", tx) } @@ -1568,6 +1631,14 @@ impl UtxoRpcClientOps for ElectrumClient { ) } + fn get_best_block(&self) -> UtxoRpcFut { + Box::new( + self.blockchain_headers_subscribe() + .map(BestBlock::from) + .map_to_mm_fut(UtxoRpcError::from), + ) + } + fn display_balance(&self, address: Address, decimals: u8) -> RpcRes { let hash = electrum_script_hash(&output_script(&address, ScriptType::P2PKH)); let hash_str = hex::encode(hash); diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index b66e4e0619..652bc2659c 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -113,6 +113,8 @@ fn utxo_coin_fields_for_test( estimate_fee_mode: None, mature_confirmations: MATURE_CONFIRMATIONS_DEFAULT, estimate_fee_blocks: 1, + lightning: false, + network: None, }, decimals: 8, dust_amount: UTXO_DUST_AMOUNT, diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index 7669638960..f8a5ae656d 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -33,6 +33,7 @@ http-body = "0.1" itertools = "0.8" keys = { path = "../mm2_bitcoin/keys" } lazy_static = "1.4" +lightning = "0.0.101" log = "0.4.8" num-bigint = { version = "0.2", features = ["serde", "std"] } num-rational = { version = "0.2", features = ["serde", "bigint", "bigint-std"] } @@ -78,6 +79,7 @@ hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features hyper-rustls = { version = "0.22", default-features = false, features = ["webpki-tokio"] } libc = { version = "0.2" } +lightning-background-processor = "0.0.101" log4rs = { version = "1.0" } metrics = { version = "0.12" } metrics-runtime = { version = "0.13", default-features = false, features = ["metrics-observer-prometheus"] } diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 337de78ba6..7a83492c0c 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -94,6 +94,7 @@ pub mod duplex_mutex; pub mod for_tests; pub mod grpc_web; pub mod iguana_utils; +#[cfg(not(target_arch = "wasm32"))] pub mod ip_addr; pub mod mm_ctx; #[path = "mm_error/mm_error.rs"] pub mod mm_error; pub mod mm_number; diff --git a/mm2src/common/ip_addr.rs b/mm2src/common/ip_addr.rs new file mode 100644 index 0000000000..f3f50fad8b --- /dev/null +++ b/mm2src/common/ip_addr.rs @@ -0,0 +1,171 @@ +use super::mm_ctx::MmArc; +use super::slurp_url; +use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; +use std::fs; +use std::io::Read; +use std::net::{IpAddr, Ipv4Addr}; +use std::path::Path; + +const IP_PROVIDERS: [&str; 2] = ["http://checkip.amazonaws.com/", "http://api.ipify.org"]; + +/// Tries to serve on the given IP to check if it's available. +/// We need this check because our external IP, particularly under NAT, +/// might be outside of the set of IPs we can open and run a server on. +/// +/// Returns an error if the address did not work +/// (like when the `ip` does not belong to a connected interface). +/// +/// The primary concern of this function is to test the IP, +/// but this opportunity is also used to start the HTTP fallback server, +/// in order to improve the reliability of the said server (in the Lean "stop the line" manner). +/// +/// If the IP has passed the communication check then a shutdown Sender is returned. +/// Dropping or using that Sender will stop the HTTP fallback server. +/// +/// Also the port of the HTTP fallback server is returned. +#[cfg(not(target_arch = "wasm32"))] +fn test_ip(ctx: &MmArc, ip: IpAddr) -> Result<(), String> { + let netid = ctx.netid(); + + // Try a few pseudo-random ports. + // `netid` is used as the seed in order for the port selection to be determenistic, + // similar to how the port selection and probing worked before (since MM1) + // and in order to reduce the likehood of *unexpected* port conflicts. + let mut attempts_left = 9; + let mut rng = SmallRng::seed_from_u64(netid as u64); + loop { + if attempts_left < 1 { + break ERR!("Out of attempts"); + } + attempts_left -= 1; + // TODO: Avoid `mypubport`. + let port = rng.gen_range(1111, 65535); + log::info!("Trying to bind on {}:{}", ip, port); + match std::net::TcpListener::bind((ip, port)) { + Ok(_) => break Ok(()), + Err(err) => { + if attempts_left == 0 { + break ERR!("{}", err); + } + continue; + }, + } + } +} + +fn simple_ip_extractor(ip: &str) -> Result { + let ip = ip.trim(); + Ok(match ip.parse() { + Ok(ip) => ip, + Err(err) => return ERR!("Error parsing IP address '{}': {}", ip, err), + }) +} + +/// Detect the outer IP address, visible to the internet. +#[cfg(not(target_arch = "wasm32"))] +pub async fn fetch_external_ip() -> Result { + for url in IP_PROVIDERS.iter() { + log::info!("Trying to fetch the real IP from '{}' ...", url); + let (status, _headers, ip) = match slurp_url(url).await { + Ok(t) => t, + Err(err) => { + log::error!("Failed to fetch IP from '{}': {}", url, err); + continue; + }, + }; + if !status.is_success() { + log::error!("Failed to fetch IP from '{}': status {:?}", url, status); + continue; + } + let ip = match std::str::from_utf8(&ip) { + Ok(ip) => ip, + Err(err) => { + log::error!("Failed to fetch IP from '{}', not UTF-8: {}", url, err); + continue; + }, + }; + match simple_ip_extractor(ip) { + Ok(ip) => return Ok(ip), + Err(err) => { + log::error!("Failed to parse IP '{}' fetched from '{}': {}", ip, url, err); + continue; + }, + }; + } + ERR!("Couldn't fetch the real IP") +} + +/// Detect the real IP address. +/// +/// We're detecting the outer IP address, visible to the internet. +/// Later we'll try to *bind* on this IP address, +/// and this will break under NAT or forwarding because the internal IP address will be different. +/// Which might be a good thing, allowing us to detect the likehoodness of NAT early. +#[cfg(not(target_arch = "wasm32"))] +async fn detect_myipaddr(ctx: MmArc) -> Result { + let ip = try_s!(fetch_external_ip().await); + + // Try to bind on this IP. + // If we're not behind a NAT then the bind will likely succeed. + // If the bind fails then emit a user-visible warning and fall back to 0.0.0.0. + match test_ip(&ctx, ip) { + Ok(_) => { + ctx.log.log( + "🙂", + &[&"myipaddr"], + &fomat! ( + "We've detected an external IP " (ip) " and we can bind on it" + ", so probably a dedicated IP."), + ); + return Ok(ip); + }, + Err(err) => log::error!("IP {} not available: {}", ip, err), + } + let all_interfaces = Ipv4Addr::new(0, 0, 0, 0).into(); + if test_ip(&ctx, all_interfaces).is_ok() { + ctx.log.log ("😅", &[&"myipaddr"], &fomat! ( + "We couldn't bind on the external IP " (ip) ", so NAT is likely to be present. We'll be okay though.")); + return Ok(all_interfaces); + } + let localhost = Ipv4Addr::new(127, 0, 0, 1).into(); + if test_ip(&ctx, localhost).is_ok() { + ctx.log.log( + "🤫", + &[&"myipaddr"], + &fomat! ( + "We couldn't bind on " (ip) " or 0.0.0.0!" + " Looks like we can bind on 127.0.0.1 as a workaround, but that's not how we're supposed to work."), + ); + return Ok(localhost); + } + ctx.log.log( + "🤒", + &[&"myipaddr"], + &fomat! ( + "Couldn't bind on " (ip) ", 0.0.0.0 or 127.0.0.1."), + ); + Ok(all_interfaces) // Seems like a better default than 127.0.0.1, might still work for other ports. +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn myipaddr(ctx: MmArc) -> Result { + let myipaddr: IpAddr = if Path::new("myipaddr").exists() { + match fs::File::open("myipaddr") { + Ok(mut f) => { + let mut buf = String::new(); + if let Err(err) = f.read_to_string(&mut buf) { + return ERR!("Can't read from 'myipaddr': {}", err); + } + try_s!(simple_ip_extractor(&buf)) + }, + Err(err) => return ERR!("Can't read from 'myipaddr': {}", err), + } + } else if !ctx.conf["myipaddr"].is_null() { + let s = try_s!(ctx.conf["myipaddr"].as_str().ok_or("'myipaddr' is not a string")); + try_s!(simple_ip_extractor(s)) + } else { + try_s!(detect_myipaddr(ctx).await) + }; + Ok(myipaddr) +} diff --git a/mm2src/common/log.rs b/mm2src/common/log.rs index 68ccbcb09f..8472734378 100644 --- a/mm2src/common/log.rs +++ b/mm2src/common/log.rs @@ -7,7 +7,9 @@ use chrono::format::strftime::StrftimeItems; use chrono::format::DelayedFormat; use chrono::{Local, TimeZone, Utc}; use crossbeam::queue::SegQueue; -use log::Record; +#[cfg(not(target_arch = "wasm32"))] +use lightning::util::logger::{Level as LightningLevel, Logger as LightningLogger, Record as LightningRecord}; +use log::{Level, Record}; use parking_lot::Mutex; use serde_json::Value as Json; use std::cell::RefCell; @@ -961,6 +963,30 @@ impl LogState { pub fn register_my_thread(&self) -> Result<(), String> { Ok(()) } } +#[cfg(not(target_arch = "wasm32"))] +impl LightningLogger for LogState { + fn log(&self, record: &LightningRecord) { + let level = match record.level { + LightningLevel::Trace => Level::Trace, + LightningLevel::Debug => Level::Debug, + LightningLevel::Info => Level::Info, + LightningLevel::Warn => Level::Warn, + LightningLevel::Error => Level::Error, + }; + let record = Record::builder() + .args(record.args) + .level(level) + .target("mm_log") + .module_path(Some(record.module_path)) + .file(Some(record.file)) + .line(Some(record.line)) + .build(); + let as_string = format_record(&record); + let level = LogLevel::from(record.metadata().level()); + chunk2log(as_string, level); + } +} + #[cfg(not(target_arch = "wasm32"))] impl Drop for LogState { fn drop(&mut self) { @@ -1010,9 +1036,8 @@ impl FromStr for LogLevel { } } -impl From for LogLevel { - fn from(orig: log::Level) -> Self { - use log::Level; +impl From for LogLevel { + fn from(orig: Level) -> Self { match orig { Level::Error => LogLevel::Error, Level::Warn => LogLevel::Warn, diff --git a/mm2src/common/mm_ctx.rs b/mm2src/common/mm_ctx.rs index e097742525..5ca6ff2b19 100644 --- a/mm2src/common/mm_ctx.rs +++ b/mm2src/common/mm_ctx.rs @@ -23,6 +23,7 @@ cfg_wasm32! { cfg_native! { use crate::executor::Timer; use crate::mm_metrics::prometheus; + use lightning_background_processor::BackgroundProcessor; use rusqlite::Connection; use std::net::{IpAddr, SocketAddr}; use std::sync::MutexGuard; @@ -95,6 +96,9 @@ pub struct MmCtx { /// The RPC sender forwarding requests to writing part of underlying stream. #[cfg(target_arch = "wasm32")] pub wasm_rpc: Constructible, + /// The lightning node background processor that takes care of tasks that need to happen periodically + #[cfg(not(target_arch = "wasm32"))] + pub ln_background_processor: Constructible, #[cfg(not(target_arch = "wasm32"))] pub sqlite_connection: Constructible>, pub mm_version: String, @@ -126,6 +130,8 @@ impl MmCtx { #[cfg(target_arch = "wasm32")] wasm_rpc: Constructible::default(), #[cfg(not(target_arch = "wasm32"))] + ln_background_processor: Constructible::default(), + #[cfg(not(target_arch = "wasm32"))] sqlite_connection: Constructible::default(), mm_version: "".into(), #[cfg(target_arch = "wasm32")] diff --git a/mm2src/lp_native_dex.rs b/mm2src/lp_native_dex.rs index 6ac55d6c89..e01679da10 100644 --- a/mm2src/lp_native_dex.rs +++ b/mm2src/lp_native_dex.rs @@ -19,12 +19,10 @@ use coins::register_balance_update_handler; use mm2_libp2p::{spawn_gossipsub, NodeType, RelayAddress}; -use rand::rngs::SmallRng; -use rand::{random, Rng, SeedableRng}; +use rand::random; use serde_json::{self as json}; use std::fs; use std::io::{Read, Write}; -use std::net::{IpAddr, Ipv4Addr}; use std::path::Path; use std::str; @@ -38,12 +36,12 @@ use crate::mm2::rpc::spawn_rpc; use crate::mm2::{MM_DATETIME, MM_VERSION}; use bitcrypto::sha256; use common::executor::{spawn, spawn_boxed, Timer}; +#[cfg(not(target_arch = "wasm32"))] +use common::ip_addr::myipaddr; use common::log::{error, info, warn}; use common::mm_ctx::{MmArc, MmCtx}; use common::privkey::key_pair_from_seed; -use common::slurp_url; -const IP_PROVIDERS: [&str; 2] = ["http://checkip.amazonaws.com/", "http://api.ipify.org"]; const NETID_7777_SEEDNODES: [&str; 3] = ["seed1.defimania.live", "seed2.defimania.live", "seed3.defimania.live"]; #[cfg(target_arch = "wasm32")] @@ -238,51 +236,6 @@ pub fn lp_passphrase_init(ctx: &MmArc) -> Result<(), String> { Ok(()) } -/// Tries to serve on the given IP to check if it's available. -/// We need this check because our external IP, particularly under NAT, -/// might be outside of the set of IPs we can open and run a server on. -/// -/// Returns an error if the address did not work -/// (like when the `ip` does not belong to a connected interface). -/// -/// The primary concern of this function is to test the IP, -/// but this opportunity is also used to start the HTTP fallback server, -/// in order to improve the reliability of the said server (in the Lean "stop the line" manner). -/// -/// If the IP has passed the communication check then a shutdown Sender is returned. -/// Dropping or using that Sender will stop the HTTP fallback server. -/// -/// Also the port of the HTTP fallback server is returned. -#[cfg(not(target_arch = "wasm32"))] -fn test_ip(ctx: &MmArc, ip: IpAddr) -> Result<(), String> { - let netid = ctx.netid(); - - // Try a few pseudo-random ports. - // `netid` is used as the seed in order for the port selection to be determenistic, - // similar to how the port selection and probing worked before (since MM1) - // and in order to reduce the likehood of *unexpected* port conflicts. - let mut attempts_left = 9; - let mut rng = SmallRng::seed_from_u64(netid as u64); - loop { - if attempts_left < 1 { - break ERR!("Out of attempts"); - } - attempts_left -= 1; - // TODO: Avoid `mypubport`. - let port = rng.gen_range(1111, 65535); - info!("Trying to bind on {}:{}", ip, port); - match std::net::TcpListener::bind((ip, port)) { - Ok(_) => break Ok(()), - Err(err) => { - if attempts_left == 0 { - break ERR!("{}", err); - } - continue; - }, - } - } -} - #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] /// * `ctx_cb` - callback used to share the `MmCtx` ID with the call site. pub async fn lp_init(ctx: MmArc) -> Result<(), String> { @@ -348,116 +301,6 @@ async fn kick_start(ctx: MmArc) -> Result<(), String> { Ok(()) } -fn simple_ip_extractor(ip: &str) -> Result { - let ip = ip.trim(); - Ok(match ip.parse() { - Ok(ip) => ip, - Err(err) => return ERR!("Error parsing IP address '{}': {}", ip, err), - }) -} - -/// Detect the real IP address. -/// -/// We're detecting the outer IP address, visible to the internet. -/// Later we'll try to *bind* on this IP address, -/// and this will break under NAT or forwarding because the internal IP address will be different. -/// Which might be a good thing, allowing us to detect the likehoodness of NAT early. -#[cfg(not(target_arch = "wasm32"))] -async fn detect_myipaddr(ctx: MmArc) -> Result { - for url in IP_PROVIDERS.iter() { - info!("Trying to fetch the real IP from '{}' ...", url); - let (status, _headers, ip) = match slurp_url(url).await { - Ok(t) => t, - Err(err) => { - error!("Failed to fetch IP from '{}': {}", url, err); - continue; - }, - }; - if !status.is_success() { - error!("Failed to fetch IP from '{}': status {:?}", url, status); - continue; - } - let ip = match std::str::from_utf8(&ip) { - Ok(ip) => ip, - Err(err) => { - error!("Failed to fetch IP from '{}', not UTF-8: {}", url, err); - continue; - }, - }; - let ip = match simple_ip_extractor(ip) { - Ok(ip) => ip, - Err(err) => { - error!("Failed to parse IP '{}' fetched from '{}': {}", ip, url, err); - continue; - }, - }; - - // Try to bind on this IP. - // If we're not behind a NAT then the bind will likely succeed. - // If the bind fails then emit a user-visible warning and fall back to 0.0.0.0. - match test_ip(&ctx, ip) { - Ok(_) => { - ctx.log.log( - "🙂", - &[&"myipaddr"], - &fomat! ( - "We've detected an external IP " (ip) " and we can bind on it" - ", so probably a dedicated IP."), - ); - return Ok(ip); - }, - Err(err) => error!("IP {} not available: {}", ip, err), - } - let all_interfaces = Ipv4Addr::new(0, 0, 0, 0).into(); - if test_ip(&ctx, all_interfaces).is_ok() { - ctx.log.log ("😅", &[&"myipaddr"], &fomat! ( - "We couldn't bind on the external IP " (ip) ", so NAT is likely to be present. We'll be okay though.")); - return Ok(all_interfaces); - } - let localhost = Ipv4Addr::new(127, 0, 0, 1).into(); - if test_ip(&ctx, localhost).is_ok() { - ctx.log.log( - "🤫", - &[&"myipaddr"], - &fomat! ( - "We couldn't bind on " (ip) " or 0.0.0.0!" - " Looks like we can bind on 127.0.0.1 as a workaround, but that's not how we're supposed to work."), - ); - return Ok(localhost); - } - ctx.log.log( - "🤒", - &[&"myipaddr"], - &fomat! ( - "Couldn't bind on " (ip) ", 0.0.0.0 or 127.0.0.1."), - ); - return Ok(all_interfaces); // Seems like a better default than 127.0.0.1, might still work for other ports. - } - ERR!("Couldn't fetch the real IP") -} - -#[cfg(not(target_arch = "wasm32"))] -async fn myipaddr(ctx: MmArc) -> Result { - let myipaddr: IpAddr = if Path::new("myipaddr").exists() { - match fs::File::open("myipaddr") { - Ok(mut f) => { - let mut buf = String::new(); - if let Err(err) = f.read_to_string(&mut buf) { - return ERR!("Can't read from 'myipaddr': {}", err); - } - try_s!(simple_ip_extractor(&buf)) - }, - Err(err) => return ERR!("Can't read from 'myipaddr': {}", err), - } - } else if !ctx.conf["myipaddr"].is_null() { - let s = try_s!(ctx.conf["myipaddr"].as_str().ok_or("'myipaddr' is not a string")); - try_s!(simple_ip_extractor(s)) - } else { - try_s!(detect_myipaddr(ctx).await) - }; - Ok(myipaddr) -} - async fn init_p2p(ctx: MmArc) -> Result<(), String> { let i_am_seed = ctx.conf["i_am_seed"].as_bool().unwrap_or(false); let netid = ctx.netid(); diff --git a/mm2src/mm2_bitcoin/chain/src/lib.rs b/mm2src/mm2_bitcoin/chain/src/lib.rs index fbf5c9424b..568303c60b 100644 --- a/mm2src/mm2_bitcoin/chain/src/lib.rs +++ b/mm2src/mm2_bitcoin/chain/src/lib.rs @@ -21,7 +21,7 @@ pub trait RepresentH256 { pub use primitives::{bytes, compact, hash, U256}; pub use block::Block; -pub use block_header::BlockHeader; +pub use block_header::{BlockHeader, BlockHeaderBits, BlockHeaderNonce}; pub use merkle_root::{merkle_node_hash, merkle_root}; pub use transaction::{JoinSplit, OutPoint, ShieldedOutput, ShieldedSpend, Transaction, TransactionInput, TransactionOutput, TxHashAlgo}; diff --git a/mm2src/mm2_tests.rs b/mm2src/mm2_tests.rs index 34aa860c15..2c65deb13b 100644 --- a/mm2src/mm2_tests.rs +++ b/mm2src/mm2_tests.rs @@ -8328,6 +8328,95 @@ fn test_mm2_db_migration() { .unwrap(); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_enable_lightning() { + let seed = "valley embody about obey never adapt gesture trust screen tube glide bread"; + + let coins = json! ([ + { + "coin": "tBTC", + "name": "tbitcoin", + "fname": "tBitcoin", + "rpcport": 18332, + "pubtype": 111, + "p2shtype": 196, + "wiftype": 239, + "segwit": true, + "bech32_hrp": "tb", + "lightning": true, + "network": "testnet", + "txfee": 0, + "estimate_fee_mode": "ECONOMICAL", + "mm2": 1, + "required_confirmations": 0, + "protocol": { + "type": "UTXO" + } + } + ]); + + let mut mm = MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9998, + "myipaddr": env::var ("BOB_TRADE_IP") .ok(), + "rpcip": env::var ("BOB_TRADE_IP") .ok(), + "passphrase": seed.to_string(), + "coins": coins, + "i_am_seed": true, + "rpc_password": "pass", + }), + "pass".into(), + local_start!("bob"), + ) + .unwrap(); + let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!({ "log path: {}", mm.log_path.display() }); + + let electrum = block_on(mm.rpc(json!({ + "userpass": mm.userpass, + "method": "electrum", + "coin": "tBTC", + "servers": [{"url":"electrum1.cipig.net:10068"},{"url":"electrum2.cipig.net:10068"},{"url":"electrum3.cipig.net:10068"}], + "mm2": 1, + "address_format": { + "format": "segwit", + }, + }))).unwrap(); + assert_eq!( + electrum.0, + StatusCode::OK, + "RPC «electrum» failed with {} {}", + electrum.0, + electrum.1 + ); + + let enable_lightning = block_on(mm.rpc(json!({ + "mmrpc": "2.0", + "method": "enable_lightning", + "userpass": mm.userpass, + "params": { + "coin": "tBTC", + "name": "test_node", + }, + }))) + .unwrap(); + assert_eq!( + enable_lightning.0, + StatusCode::OK, + "RPC «enable_lightning» failed with {} {}", + enable_lightning.0, + enable_lightning.1 + ); + + block_on(mm.wait_for_log(60., |log| log.contains("Calling ChannelManager's timer_tick_occurred"))).unwrap(); + + block_on(mm.wait_for_log(60., |log| log.contains("Calling PeerManager's timer_tick_occurred"))).unwrap(); + + block_on(mm.stop()).unwrap(); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_get_public_key() { diff --git a/mm2src/rpc/dispatcher/dispatcher_v2.rs b/mm2src/rpc/dispatcher/dispatcher_v2.rs index 793db190ba..161d506798 100644 --- a/mm2src/rpc/dispatcher/dispatcher_v2.rs +++ b/mm2src/rpc/dispatcher/dispatcher_v2.rs @@ -5,6 +5,7 @@ use crate::{mm2::lp_stats::{add_node_to_version_stat, remove_node_from_version_s stop_version_stat_collection, update_version_stat_collection}, mm2::lp_swap::trade_preimage_rpc, mm2::rpc::get_public_key::get_public_key}; +use coins::lightning::enable_lightning; use coins::withdraw; use common::log::{error, warn}; use common::mm_ctx::MmArc; @@ -88,6 +89,7 @@ fn auth(request: &MmRpcRequest, ctx: &MmArc) -> DispatcherResult<()> { async fn dispatcher(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult>> { match request.method.as_str() { "add_node_to_version_stat" => handle_mmrpc(ctx, request, add_node_to_version_stat).await, + "enable_lightning" => handle_mmrpc(ctx, request, enable_lightning).await, "get_public_key" => handle_mmrpc(ctx, request, get_public_key).await, "remove_node_from_version_stat" => handle_mmrpc(ctx, request, remove_node_from_version_stat).await, "start_simple_market_maker_bot" => handle_mmrpc(ctx, request, start_simple_market_maker_bot).await,