diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 73109e5f..adf31ef8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,7 +22,7 @@ jobs: # run all tests - uses: actions/checkout@v3 - name: Run tests - run: cargo test --verbose + run: cargo build && cargo test --verbose linting: @@ -54,4 +54,4 @@ jobs: toolchain: ${{ matrix.rust }} - name: Cross compile - run: cargo test --verbose + run: cargo build && cargo test --verbose diff --git a/Cargo.lock b/Cargo.lock index 01d6b484..7229b38e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand", + "fastrand 1.9.0", "futures-lite", "slab", ] @@ -161,7 +161,7 @@ dependencies = [ "log", "parking", "polling", - "rustix", + "rustix 0.37.20", "slab", "socket2", "waker-fn", @@ -249,9 +249,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.13.1" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bech32" @@ -371,7 +371,7 @@ dependencies = [ "async-lock", "async-task", "atomic-waker", - "fastrand", + "fastrand 1.9.0", "futures-lite", "log", ] @@ -496,6 +496,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "core2" version = "0.3.3" @@ -697,6 +713,15 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -718,23 +743,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -752,6 +766,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "floresta" version = "0.1.0" @@ -798,9 +818,11 @@ dependencies = [ "anyhow", "bitcoin 0.31.0", "clap", - "jsonrpc", + "rand 0.8.5", + "reqwest", "serde", "serde_json", + "tempfile", ] [[package]] @@ -927,6 +949,30 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding 2.3.1", +] + [[package]] name = "fs2" version = "0.4.3" @@ -998,7 +1044,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -1134,6 +1180,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b553656127a00601c8ae5590fcfdc118e4083a7924b6cf4ffc1ea4b99dc429d7" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util 0.7.8", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.0" @@ -1239,6 +1304,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "httparse", @@ -1252,6 +1318,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.1.5" @@ -1263,6 +1342,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "2.0.1" @@ -1293,6 +1382,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itoa" version = "1.0.6" @@ -1317,17 +1412,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonrpc" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8128f36b47411cd3f044be8c1f5cc0c9e24d1d1bfdc45f0a57897b32513053f2" -dependencies = [ - "base64", - "serde", - "serde_json", -] - [[package]] name = "jsonrpc-client-transports" version = "18.0.0" @@ -1343,7 +1427,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "url", + "url 1.7.2", ] [[package]] @@ -1428,7 +1512,7 @@ dependencies = [ "log", "tokio", "tokio-stream", - "tokio-util", + "tokio-util 0.6.10", "unicase", ] @@ -1499,6 +1583,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + [[package]] name = "lock_api" version = "0.4.10" @@ -1562,6 +1652,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniscript" version = "10.0.0" @@ -1603,6 +1699,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "net2" version = "0.2.39" @@ -1669,6 +1783,50 @@ dependencies = [ "loom", ] +[[package]] +name = "openssl" +version = "0.10.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" +dependencies = [ + "bitflags 2.4.1", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -1735,6 +1893,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -1980,6 +2144,44 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding 2.3.1", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url 2.5.0", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2005,10 +2207,23 @@ dependencies = [ "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", "windows-sys 0.48.0", ] +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys 0.4.13", + "windows-sys 0.52.0", +] + [[package]] name = "rustreexo" version = "0.1.0" @@ -2039,6 +2254,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2090,6 +2314,29 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.20" @@ -2136,6 +2383,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.7" @@ -2243,6 +2502,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.0" @@ -2262,6 +2542,19 @@ version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if 1.0.0", + "fastrand 2.0.1", + "redox_syscall 0.4.1", + "rustix 0.38.28", + "windows-sys 0.52.0", +] + [[package]] name = "termcolor" version = "1.4.0" @@ -2347,6 +2640,16 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -2372,6 +2675,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" version = "0.5.11" @@ -2531,9 +2848,20 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" dependencies = [ - "idna", + "idna 0.1.5", "matches", - "percent-encoding", + "percent-encoding 1.0.1", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding 2.3.1", ] [[package]] @@ -2554,6 +2882,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.1.1" @@ -2860,6 +3194,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if 1.0.0", + "windows-sys 0.48.0", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/crates/floresta-cli/Cargo.toml b/crates/floresta-cli/Cargo.toml index 9239684d..6a4c552f 100644 --- a/crates/floresta-cli/Cargo.toml +++ b/crates/floresta-cli/Cargo.toml @@ -15,9 +15,21 @@ categories = ["bitcoin", "blockchain", "node"] [dependencies] -jsonrpc = "0.14.1" clap = { version = "4.0.29", features = ["derive"] } bitcoin = { version = "0.31", features = ["serde", "std"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -anyhow = "*" +anyhow = "1.0" +reqwest = { version = "0.11.23", optional = true, features = ["blocking"] } + +[features] +default = ["with-reqwest"] +with-reqwest = ["reqwest"] + +[dev-dependencies] +rand = "0.8.5" +tempfile = "3.9.0" + +[lib] +name = "floresta_cli" +path = "src/lib.rs" diff --git a/crates/floresta-cli/src/lib.rs b/crates/floresta-cli/src/lib.rs new file mode 100644 index 00000000..76622bfa --- /dev/null +++ b/crates/floresta-cli/src/lib.rs @@ -0,0 +1,218 @@ +// SPDX license specifier: MIT + +//! # floresta-cli - A command line interface for florestad +//! +//! Florestad is a lightweight Bitcoin full node, built with libfloresta. It gives +//! you complete control over your Bitcoin node with a simple json-rpc interface that +//! may be used either from command line or programmatically. This crate provides a +//! ready-to-use library for interacting with florestad's json-rpc interface in your rust +//! application. + +#[cfg(feature = "with-reqwest")] +pub mod reqwest_client; + +pub mod rpc; +pub mod rpc_types; + +// Those tests doesn't work on windowns +// TODO (Davidson): work on windows? +#[cfg(all(test, not(target_os = "windows")))] +mod tests { + use std::fs; + use std::process::Child; + use std::process::Command; + use std::process::Stdio; + use std::str::FromStr; + use std::thread::sleep; + use std::time::Duration; + + use bitcoin::BlockHash; + use bitcoin::Txid; + + use crate::reqwest_client::ReqwestClient; + use crate::rpc::FlorestaRPC; + + struct Florestad { + proc: Child, + } + + impl Drop for Florestad { + fn drop(&mut self) { + self.proc.kill().unwrap(); + } + } + + /// A helper function for tests. + /// + /// This function will start a florestad process and return a client that can be used to + /// interact with it through RPC. It also returns a handle to the process itself, so that + /// you can poke at the stdin and out for this process. You don't have to kill it though, + /// once the handle goes out of scope, the process will be killed. + /// + /// The process created by this method will run in a random datadir and use random ports + /// for both RPC and Electrum. The datadir will be in the current dir, under a `tmp` subdir. + /// If you're at $HOME/floresta it will run on $HOME/floresta/tmp// + fn start_florestad() -> (Florestad, ReqwestClient) { + let here = env!("PWD"); + let port = rand::random::() % 1000 + 18443; + + // makes a temporary directory + let test_code = rand::random::(); + let dirname = format!("{here}/tmp/floresta.{test_code}"); + fs::DirBuilder::new() + .recursive(true) + .create(dirname.clone()) + .unwrap(); + + let fld = Command::new(format!("{here}/target/debug/florestad")) + .args(["-n", "regtest"]) + .args(["run"]) + .args(["--data-dir", &dirname]) + .args(["--rpc-address", &format!("127.0.0.1:{}", port)]) + .args(["--electrum-address", &format!("127.0.0.1:{}", port + 1)]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + + let client = ReqwestClient::new(format!("http://127.0.0.1:{port}")); + + let mut retries = 10; + + loop { + sleep(Duration::from_secs(1)); + retries -= 1; + if retries == 0 { + panic!("florestad didn't start {:?}", fld.stdout); + } + match client.get_blockchain_info() { + Ok(_) => break, + Err(_) => continue, + } + } + + (Florestad { proc: fld }, client) + } + + #[test] + fn test_rescan() { + let (_proc, client) = start_florestad(); + + let rescan = client.rescan(0).expect("rpc not working"); + assert!(rescan); + } + + #[test] + fn test_stop() { + let (mut _proc, client) = start_florestad(); + + let stop = client.stop().expect("rpc not working"); + assert!(stop); + } + + #[test] + fn test_get_blockchaininfo() { + let (_proc, client) = start_florestad(); + + let gbi = client.get_blockchain_info().expect("rpc not working"); + + assert_eq!(gbi.height, 0); + assert_eq!(gbi.chain, "regtest".to_owned()); + assert!(gbi.ibd); + assert_eq!(gbi.leaf_count, 0); + assert_eq!(gbi.root_hashes, Vec::::new()); + } + + #[test] + fn test_get_roots() { + let (_proc, client) = start_florestad(); + + let gbi = client.get_blockchain_info().expect("rpc not working"); + + assert_eq!(gbi.root_hashes, Vec::::new()); + } + + #[test] + fn test_get_block() { + let (_proc, client) = start_florestad(); + + let block_hash: BlockHash = + "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" + .parse() + .unwrap(); + let block = client.get_block(block_hash).unwrap(); + + assert_eq!( + block.hash, + "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206".to_owned() + ); + } + + #[test] + fn test_get_block_hash() { + let (_proc, client) = start_florestad(); + + let blockhash = client.get_block_hash(0).expect("rpc not working"); + + assert_eq!( + blockhash, + "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" + .parse() + .unwrap() + ); + } + + #[test] + fn test_get_block_header() { + let (_proc, client) = start_florestad(); + + let blockhash = client.get_block_hash(0).expect("rpc not working"); + let block_header = client.get_block_header(blockhash).expect("rpc not working"); + + assert_eq!(block_header.block_hash(), blockhash); + } + + #[test] + fn test_get_block_filter() { + let (_proc, client) = start_florestad(); + + let block_filter = client.get_block_filter(0); + + // this should err, because there is no filter for genesis block + assert!(block_filter.is_err()); + } + + #[test] + fn test_load_descriptor() { + let (_proc, client) = start_florestad(); + + let desc = " + wsh(sortedmulti(1,[54ff5a12/48h/1h/0h/2h]tpubDDw6pwZA3hYxcSN32q7a5ynsKmWr4BbkBNHydHPKkM4BZwUfiK7tQ26h7USm8kA1E2FvCy7f7Er7QXKF8RNptATywydARtzgrxuPDwyYv4x/<0;1>/*,[bcf969c0/48h/1h/0h/2h]tpubDEFdgZdCPgQBTNtGj4h6AehK79Jm4LH54JrYBJjAtHMLEAth7LuY87awx9ZMiCURFzFWhxToRJK6xp39aqeJWrG5nuW3eBnXeMJcvDeDxfp/<0;1>/*))#fuw35j0q"; + + let res = client.load_descriptor(desc.to_string(), Some(0)).unwrap(); + + assert!(res) + } + + #[test] + fn test_get_height() { + let (_proc, client) = start_florestad(); + + let height = client.get_height().unwrap(); + assert_eq!(height, 0); + } + + #[test] + fn test_send_raw_transaction() { + let (_proc, client) = start_florestad(); + + let tx = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000".to_string(); + + let res = client.send_raw_transaction(tx).unwrap(); + assert_eq!( + res, + Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b") + .unwrap() + ); + } +} diff --git a/crates/floresta-cli/src/main.rs b/crates/floresta-cli/src/main.rs index 8f7b332c..76abfe95 100644 --- a/crates/floresta-cli/src/main.rs +++ b/crates/floresta-cli/src/main.rs @@ -1,108 +1,79 @@ +use std::fmt::Debug; + +use anyhow::Ok; use bitcoin::BlockHash; use bitcoin::Network; use bitcoin::Txid; use clap::Parser; use clap::Subcommand; -use jsonrpc::arg; -use jsonrpc::simple_http::SimpleHttpTransport; -use jsonrpc::Client; -use jsonrpc::Request; -use serde_json::value::RawValue; -use serde_json::Value; +use floresta_cli::reqwest_client::ReqwestClient; +use floresta_cli::rpc::FlorestaRPC; + +mod reqwest_client; +mod rpc; +mod rpc_types; fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - let (params, method) = get_req(&cli); - - let mut transport = SimpleHttpTransport::builder().url(&get_host(&cli))?; - if let Some(username) = cli.rpc_user { - transport = transport.auth(username, cli.rpc_password); - } - let transport = transport.build(); - let client = Client::with_transport(transport); - let request = Request { - id: Value::from(0), - method: &method, - params: ¶ms, - jsonrpc: Some("2.0"), - }; - let response = client.send_request(request)?; + let client = ReqwestClient::new(get_host(&cli)); + let res = do_request(&cli, client)?; - let response = response.result::()?; - println!("{}", ::serde_json::to_string_pretty(&response).unwrap()); + println!("{}", res); anyhow::Ok(()) } + fn get_host(cmd: &Cli) -> String { if let Some(host) = cmd.rpc_host.clone() { return host; } + match cmd.network { - Network::Bitcoin => "127.0.0.1:8332".into(), - Network::Testnet => "127.0.0.1:18332".into(), - Network::Signet => "127.0.0.1:38332".into(), - Network::Regtest => "127.0.0.1:18442".into(), - _ => "127.0.0.1:8332".into(), + Network::Bitcoin => "http://127.0.0.1:8332".into(), + Network::Testnet => "http://127.0.0.1:18332".into(), + Network::Signet => "http://127.0.0.1:38332".into(), + Network::Regtest => "http://127.0.0.1:18442".into(), + _ => "http://127.0.0.1:8332".into(), } } -fn get_req(cmd: &Cli) -> (Vec>, String) { - let method = match cmd.methods { - Methods::GetBlockchainInfo => "getblockchaininfo", - Methods::GetBlockHash { .. } => "getblockhash", - Methods::GetTxOut { .. } => "gettxout", - Methods::GetTxProof { .. } => "gettxproof", - Methods::GetRawTransaction { .. } => "gettransaction", - Methods::RescanBlockchain { .. } => "rescan", - Methods::SendRawTransaction { .. } => "sendrawtransaction", - Methods::GetBlockHeader { .. } => "getblockheader", - Methods::LoadDescriptor { .. } => "loaddescriptor", - Methods::GetRoots => "getroots", - Methods::GetBlock { .. } => "getblock", - Methods::GetPeerInfo => "getpeerinfo", - Methods::ListTransactions => "gettransactions", - Methods::Stop => "stop", - Methods::AddNode { .. } => "addnode", - Methods::GetFilters { .. } => "getblockfilter", - }; - let params = match &cmd.methods { - Methods::GetBlockchainInfo => Vec::new(), - Methods::GetBlockHash { height } => vec![arg(height)], - Methods::GetTxOut { txid, vout } => vec![arg(txid), arg(vout)], - Methods::GetTxProof { txids, blockhash } => { - if let Some(blockhash) = blockhash { - vec![arg(txids), arg(blockhash)] - } else { - vec![arg(txids)] - } + +fn do_request(cmd: &Cli, client: ReqwestClient) -> anyhow::Result { + Ok(match cmd.methods.clone() { + Methods::GetBlockchainInfo => serde_json::to_string_pretty(&client.get_blockchain_info()?)?, + Methods::GetBlockHash { height } => { + serde_json::to_string_pretty(&client.get_block_hash(height)?)? + } + Methods::GetTxOut { txid, vout } => { + serde_json::to_string_pretty(&client.get_tx_out(txid, vout)?)? + } + Methods::GetTxProof { txids, .. } => { + serde_json::to_string_pretty(&client.get_tx_proof(txids)?)? } Methods::GetRawTransaction { txid, .. } => { - vec![arg(txid)] + serde_json::to_string_pretty(&client.get_transaction(txid, Some(true))?)? } - Methods::GetBlockHeader { hash } => vec![arg(hash)], - Methods::LoadDescriptor { rescan, desc } => { - if let Some(rescan) = rescan { - vec![arg(desc), arg(rescan)] - } else { - vec![arg(desc)] - } + Methods::RescanBlockchain { start_height } => { + serde_json::to_string_pretty(&client.rescan(start_height)?)? } - Methods::RescanBlockchain { start_height } => vec![arg(start_height)], - Methods::SendRawTransaction { tx } => vec![arg(tx)], - Methods::GetRoots => Vec::new(), - Methods::GetBlock { hash, verbosity } => vec![arg(hash), arg(verbosity)], - Methods::GetPeerInfo => Vec::new(), - Methods::ListTransactions => Vec::new(), - Methods::Stop => Vec::new(), - Methods::AddNode { node } => { - vec![arg(node)] + Methods::SendRawTransaction { tx } => { + serde_json::to_string_pretty(&client.send_raw_transaction(tx)?)? } + Methods::GetBlockHeader { hash } => { + serde_json::to_string_pretty(&client.get_block_header(hash)?)? + } + Methods::LoadDescriptor { desc, rescan } => { + serde_json::to_string_pretty(&client.load_descriptor(desc, rescan)?)? + } + Methods::GetRoots => serde_json::to_string_pretty(&client.get_roots()?)?, + Methods::GetBlock { hash, .. } => serde_json::to_string_pretty(&client.get_block(hash)?)?, + Methods::GetPeerInfo => serde_json::to_string_pretty(&client.get_peer_info()?)?, + Methods::Stop => serde_json::to_string_pretty(&client.stop()?)?, + Methods::AddNode { node } => serde_json::to_string_pretty(&client.add_node(node)?)?, Methods::GetFilters { height } => { - vec![arg(height)] + serde_json::to_string_pretty(&client.get_block_filter(height)?)? } - }; - - (params, method.to_string()) + }) } #[derive(Debug, Parser)] @@ -133,7 +104,7 @@ pub struct Cli { pub methods: Methods, } -#[derive(Debug, Subcommand)] +#[derive(Debug, Clone, Subcommand)] pub enum Methods { /// Returns information about the current state of the blockchain #[command(name = "getblockchaininfo")] @@ -171,9 +142,6 @@ pub enum Methods { /// Returns information about the peers we are connected to #[command(name = "getpeerinfo")] GetPeerInfo, - /// List all transactions we are watching - #[command(name = "listtransactions")] - ListTransactions, /// Returns the value associated with a UTXO, if it's still not spent. /// This function only works properly if we have the compact block filters /// feature enabled diff --git a/crates/floresta-cli/src/reqwest_client.rs b/crates/floresta-cli/src/reqwest_client.rs new file mode 100644 index 00000000..7ebe048a --- /dev/null +++ b/crates/floresta-cli/src/reqwest_client.rs @@ -0,0 +1,106 @@ +use std::fmt::Debug; + +use serde::Deserialize; +use serde_json::json; + +use crate::rpc::JsonRPCClient; + +#[derive(Debug, Default, Clone)] +pub struct ReqwestClient { + client: reqwest::blocking::Client, + url: String, + auth: Option<(String, String)>, +} + +pub struct ReqwestConfig { + pub url: String, + pub proxy: Option, + pub auth: Option<(String, String)>, + pub timeout: Option, + pub headers: Option, +} + +impl ReqwestClient { + pub fn new(url: String) -> Self { + Self { + url, + ..Default::default() + } + } + + pub fn new_with_config(config: ReqwestConfig) -> Self { + let mut client_builder = reqwest::blocking::Client::builder(); + + if let Some(proxy) = config.proxy { + client_builder = client_builder.proxy(proxy); + } + + if let Some(timeout) = config.timeout { + client_builder = client_builder.timeout(timeout); + } + + if let Some(headers) = config.headers { + client_builder = client_builder.default_headers(headers); + } + + let client = client_builder.build().unwrap(); + Self { + url: config.url, + auth: config.auth, + client, + } + } + + pub fn rpc_call( + &self, + method: &str, + params: &[serde_json::Value], + ) -> Result + where + Response: for<'a> serde::de::Deserialize<'a> + Debug, + { + let mut req = self + .client + .post(&self.url) + .body( + json!({ + "jsonrpc": "2.0", + "id": 0, + "method": method, + "params": params, + }) + .to_string(), + ) + .header("Content-Type", "application/json"); + + if let Some((user, pass)) = &self.auth { + req = req.basic_auth(user, Some(pass)); + } + + let resp = req.send()?; + let resp = serde_json::from_str::>(&resp.text()?)?; + match resp.result { + Some(resp) => Ok(resp), + None if resp.error.is_some() => Err(crate::rpc_types::Error::Api(resp.error.unwrap())), + None => Err(crate::rpc_types::Error::EmtpyResponse), + } + } +} + +impl JsonRPCClient for ReqwestClient { + fn call serde::de::Deserialize<'a> + Debug>( + &self, + method: &str, + params: &[serde_json::Value], + ) -> Result { + self.rpc_call(method, params) + } +} + +#[derive(Debug, Deserialize)] +pub struct JsonRpcResponse { + pub jsonrpc: String, + pub id: u64, + pub result: Option, + pub error: Option, +} diff --git a/crates/floresta-cli/src/rpc.rs b/crates/floresta-cli/src/rpc.rs new file mode 100644 index 00000000..c0b0377b --- /dev/null +++ b/crates/floresta-cli/src/rpc.rs @@ -0,0 +1,210 @@ +use std::fmt::Debug; + +use bitcoin::block::Header as BlockHeader; +use bitcoin::BlockHash; +use bitcoin::Txid; +use serde_json::Number; +use serde_json::Value; + +use crate::rpc_types; +use crate::rpc_types::*; + +type Result = std::result::Result; + +/// A trait specifying all possible methods for floresta's json-rpc +pub trait FlorestaRPC { + /// Get the BIP158 filter for a given block height + /// + /// BIP158 filters are a compact representation of the set of transactions in a block, + /// designed for efficient light client synchronization. This method returns the filter + /// for a given block height, encoded as a hexadecimal string. + /// You need to have enabled block filters by setting the `blockfilters=1` option + fn get_block_filter(&self, heigth: u32) -> Result; + /// Returns general information about the chain we are on + /// + /// This method returns a bunch of information about the chain we are on, including + /// the current height, the best block hash, the difficulty, and whether we are + /// currently in IBD (Initial Block Download) mode. + fn get_blockchain_info(&self) -> Result; + /// Returns the hash of the block at the given height + /// + /// This method returns the hash of the block at the given height. If the height is + /// invalid, an error is returned. + fn get_block_hash(&self, height: u32) -> Result; + /// Returns the block header for the given block hash + /// + /// This method returns the block header for the given block hash, as defined + /// in the Bitcoin protocol specification. A header contains the block's version, + /// the previous block hash, the merkle root, the timestamp, the difficulty target, + /// and the nonce. + fn get_block_header(&self, hash: BlockHash) -> Result; + /// Gets a transaction from the blockchain + /// + /// This method returns a transaction that's cached in our wallet. If the verbosity flag is + /// set to false, the transaction is returned as a hexadecimal string. If the verbosity + /// flag is set to true, the transaction is returned as a json object. + fn get_transaction(&self, tx_id: Txid, verbosity: Option) -> Result; + /// Returns the proof that one or more transactions were included in a block + /// + /// This method returns the Merkle proof, showing that a transaction was included in a block. + /// The pooof is returned as a vector hexadecimal string. + fn get_tx_proof(&self, tx_id: Txid) -> Result>; + /// Loads up a descriptor into the wallet + /// + /// This method loads up a descriptor into the wallet. If the rescan option is not None, + /// the wallet will be rescanned for transactions matching the descriptor. If you have + /// compact block filters enabled, this process will be much faster and use less bandwidth. + /// The rescan parameter is the height at which to start the rescan, and should be at least + /// as old as the oldest transaction this descriptor could have been used in. + fn load_descriptor(&self, descriptor: String, rescan: Option) -> Result; + /// Trigger a rescan of the wallet + /// + /// This method triggers a rescan of the wallet. If you have compact block filters enabled, + /// this process will be much faster and use less bandwidth. If you don't have compact block + /// filters, we'll need to download the entire blockchain again, which will take a while. + /// The rescan parameter is the height at which to start the rescan, and should be at least + /// as old as the oldest transaction this descriptor could have been used in. + fn rescan(&self, rescan: u32) -> Result; + /// Returns the current height of the blockchain + fn get_height(&self) -> Result; + /// Sends a hex-encoded transaction to the network + /// + /// This method sends a transaction to the network. The transaction should be encoded as a + /// hexadecimal string. If the transaction is valid, it will be broadcast to the network, and + /// return the transaction id. If the transaction is invalid, an error will be returned. + fn send_raw_transaction(&self, tx: String) -> Result; + /// Gets the current accumulator for the chain we're on + /// + /// This method returns the current accumulator for the chain we're on. The accumulator is + /// a set of roots, that let's us prove that a UTXO exists in the chain. This method returns + /// a vector of hexadecimal strings, each of which is a root in the accumulator. + fn get_roots(&self) -> Result>; + /// Gets information about the peers we're connected with + /// + /// This method returns information about the peers we're connected with. This includes + /// the peer's IP address, the peer's version, the peer's user agent, and the peer's + /// current height. + fn get_peer_info(&self) -> Result>; + /// Returns a block, given a block hash + /// + /// This method returns a block, given a block hash. If the verbosity flag is 0, the block + /// is returned as a hexadecimal string. If the verbosity flag is 1, the block is returned + /// as a json object. + fn get_block(&self, hash: BlockHash) -> Result; + /// Finds an specific utxo in the chain + /// + /// You can use this to look for a utxo. If it exists, it will return the amount and + /// scriptPubKey of this utxo. It returns an empty object if the utxo doesn't exist. + /// You must have enabled block filters by setting the `blockfilters=1` option. + fn get_tx_out(&self, tx_id: Txid, outpoint: u32) -> Result; + /// Stops the florestad process + /// + /// This can be used to gracefully stop the florestad process. + fn stop(&self) -> Result; + /// Tells florestad to connect with a peer + /// + /// You can use this to connect with a given node, providing it's IP address and port. + fn add_node(&self, node: String) -> Result; +} + +/// Since the workflow for jsonrpc is the same for all methods, we can implement a trait +/// that will let us call any method on the client, and then implement the methods on any +/// client that implements this trait. +pub trait JsonRPCClient: Sized { + /// Calls a method on the client + /// + /// This should call the appropriated rpc method and return a parsed response or error. + fn call(&self, method: &str, params: &[Value]) -> Result + where + T: for<'a> serde::de::Deserialize<'a> + Debug; +} + +impl FlorestaRPC for T { + fn add_node(&self, node: String) -> Result { + self.call("addnode", &[Value::String(node)]) + } + + fn stop(&self) -> Result { + self.call("stop", &[]) + } + + fn rescan(&self, rescan: u32) -> Result { + self.call("rescan", &[Value::Number(Number::from(rescan))]) + } + + fn get_roots(&self) -> Result> { + self.call("getroots", &[]) + } + + fn get_block(&self, hash: BlockHash) -> Result { + let verbosity = 1; // Return the block in json format + self.call( + "getblock", + &[ + Value::String(hash.to_string()), + Value::Number(Number::from(verbosity)), + ], + ) + } + + fn get_height(&self) -> Result { + self.call("getheight", &[]) + } + + fn get_tx_out(&self, tx_id: Txid, outpoint: u32) -> Result { + self.call( + "gettxout", + &[ + Value::String(tx_id.to_string()), + Value::Number(Number::from(outpoint)), + ], + ) + } + + fn get_tx_proof(&self, tx_id: Txid) -> Result> { + self.call("gettxoutproof", &[Value::String(tx_id.to_string())]) + } + + fn get_peer_info(&self) -> Result> { + self.call("getpeerinfo", &[]) + } + + fn get_block_hash(&self, height: u32) -> Result { + self.call("getblockhash", &[Value::Number(Number::from(height))]) + } + + fn get_transaction(&self, tx_id: Txid, verbosity: Option) -> Result { + let verbosity = verbosity.unwrap_or(false); + self.call( + "getrawtransaction", + &[Value::String(tx_id.to_string()), Value::Bool(verbosity)], + ) + } + + fn load_descriptor(&self, descriptor: String, rescan: Option) -> Result { + let rescan = rescan.unwrap_or(0); + self.call( + "loaddescriptor", + &[ + Value::String(descriptor), + Value::Number(Number::from(rescan)), + ], + ) + } + + fn get_block_filter(&self, heigth: u32) -> Result { + self.call("getblockfilter", &[Value::Number(Number::from(heigth))]) + } + + fn get_block_header(&self, hash: BlockHash) -> Result { + self.call("getblockheader", &[Value::String(hash.to_string())]) + } + + fn get_blockchain_info(&self) -> Result { + self.call("getblockchaininfo", &[]) + } + + fn send_raw_transaction(&self, tx: String) -> Result { + self.call("sendrawtransaction", &[Value::String(tx)]) + } +} diff --git a/crates/floresta-cli/src/rpc_types.rs b/crates/floresta-cli/src/rpc_types.rs new file mode 100644 index 00000000..d29866ed --- /dev/null +++ b/crates/floresta-cli/src/rpc_types.rs @@ -0,0 +1,288 @@ +use std::fmt::Display; + +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetBlockchainInfoRes { + /// The best block we know about + /// + /// This should be the hash of the latest block in the most PoW chain we know about. We may + /// or may not have fully-validated it yet + pub best_block: String, + /// The depth of the most-PoW chain we know about + pub height: u32, + /// Whether we are on Initial Block Download + pub ibd: bool, + /// How many blocks we have fully-validated so far? This number will be smaller than + /// height during IBD, and should be equal to height otherwise + pub validated: u32, + /// The work performed by the last block + /// + /// This is the estimated amount of hashes the miner of this block had to perform + /// before mining that block, on average + pub latest_work: String, + /// The UNIX timestamp for the latest block, as reported by the block's header + pub latest_block_time: u32, + /// How many leaves we have in the utreexo accumulator so far + /// + /// This should be equal to the number of UTXOs returned by core's `gettxoutsetinfo` + pub leaf_count: u32, + /// How many roots we have in the acc + pub root_count: u32, + /// The actual hex-encoded roots + pub root_hashes: Vec, + /// A short string representing the chain we're in + pub chain: String, + /// The validation progress + /// + /// 0% means we didn't validate any block. 100% means we've validated all blocks, so + /// validated == height + pub progress: Option, + /// Current network "difficulty" + /// + /// On average, miners needs to make `difficulty` hashes before finding one that + /// solves a block's PoW + pub difficulty: u64, +} + +/// The information returned by a get_raw_tx +#[derive(Deserialize, Serialize)] +pub struct RawTx { + /// Whether this tx is in our best known chain + pub in_active_chain: bool, + /// The hex-encoded tx + pub hex: String, + /// Tha sha256d of the serialized transaction without witness + pub txid: String, + /// The sha256d of the serialized transaction including witness + pub hash: String, + /// The size this transaction occupies on disk + pub size: u32, + /// The virtual size of this transaction, as define by the segwit soft-fork + pub vsize: u32, + /// The weight of this transacion, as defined by the segwit soft-fork + pub weight: u32, + /// This transaction's version. The current bigger version is 2 + pub version: u32, + /// This transaction's locktime + pub locktime: u32, + /// A list of inputs being spent by this transaction + /// + /// See [TxIn] for more information about the contents of this + pub vin: Vec, + /// A list of outputs being created by this tx + /// + /// Se [TxOut] for more information + pub vout: Vec, + /// The hash of the block that included this tx, if any + pub blockhash: String, + /// How many blocks have been mined after this transaction's confirmation + /// including the block that confirms it. A zero value means this tx is unconfirmed + pub confirmations: u32, + /// The timestamp for the block confirming this tx, if confirmed + pub blocktime: u32, + /// Same as blocktime + pub time: u32, +} + +/// A transaction output returned by some RPCs like getrawtransaction and getblock +#[derive(Deserialize, Serialize)] +pub struct TxOut { + /// The amount in sats locked in this UTXO + pub value: u64, + /// This utxo's index inside the transaction + pub n: u32, + /// The loking script of this utxo + pub script_pub_key: ScriptPubKey, +} + +/// The locking script inside a txout +#[derive(Deserialize, Serialize)] +pub struct ScriptPubKey { + /// A ASM representation for this script + /// + /// Assembly is a high-level representation of a lower level code. Instructions + /// are turned into OP_XXXXX and data is hex-encoded. + /// E.g: OP_DUP OP_HASH160 <0000000000000000000000000000000000000000> OP_EQUALVERIFY OP_CHECKSIG + pub asm: String, + /// The hex-encoded raw script + pub hex: String, + /// How many signatures are required to spend this UTXO. + /// + /// This field is deprecated and is here for compatibility with Core + pub req_sigs: u32, + #[serde(rename = "type")] + /// The type of this spk. E.g: PKH, SH, WSH, WPKH, TR, non-standard... + pub type_: String, + /// Encode this script using one of the standard address types, if possible + pub address: String, +} + +/// A transaction input returned by some rpcs, like getrawtransaction and getblock +#[derive(Deserialize, Serialize)] +pub struct TxIn { + /// The txid that created this UTXO + pub txid: String, + /// The index of this UTXO inside the tx that created it + pub vout: u32, + /// Unlocking script that should solve the challenge and prove ownership over + /// that UTXO + pub script_sig: ScriptSigJson, + /// The nSequence field, used in relative and absolute lock-times + pub sequence: u32, + /// A vector of witness elements for this input + pub witness: Vec, +} + +/// A representation for the transaction ScriptSig, returned by some rpcs +/// like getrawtransaction and getblock +#[derive(Deserialize, Serialize)] +pub struct ScriptSigJson { + /// A ASM representation for this scriptSig + /// + /// Assembly is a high-level representation of a lower level code. Instructions + /// are turned into OP_XXXXX and data is hex-encoded. + /// E.g: OP_PUSHBYTES32 <000000000000000000000000000000000000000000000000000000000000000000> + pub asm: String, + /// The hex-encoded script sig + pub hex: String, +} + +/// General information about our peers. Returned by get_peer_info +#[derive(Debug, Deserialize, Serialize)] +pub struct PeerInfo { + /// The network address for this peer. + pub address: String, + /// A string with the services this peer advertises. E.g. NODE_NETWORK, UTREEXO, WITNESS... + pub services: String, + /// User agent is a string that represents the client being used by our peer. E.g. + /// /Satoshi-26.0/ for bitcoin core version 26 + pub user_agent: String, + /// This peer's height at the time we've openned a connection with them + pub initial_height: u32, +} + +/// A full bitcoin block, returned by get_block +#[derive(Debug, Deserialize, Serialize)] +pub struct GetBlockRes { + /// This block's hash. + pub hash: String, + /// How many blocks have been added to the chain, after this one have been found. This is + /// inclusive, so it starts with one when this block is the latest. If another one is found, + /// then it increments to 2 and so on... + pub confirmations: u32, + /// The size of this block, without the witness + pub strippedsize: usize, + /// This block's size, with the witness + pub size: usize, + /// This block's weight. + /// + /// Data inside a segwit block is counted differently, 'base data' has a weight of 4, while + /// witness only counts 1. This is (3 * base_size) + size + pub weight: usize, + /// How many blocks there are before this block + pub height: u32, + /// This block's version field + /// + /// Currently, blocks have version 2 (see BIP34), but it may also flip some of the LSB for + /// either consensus reason (see BIPs 8 and 9) or for version rolling mining, usually bits + /// after the 24th are not touched. Therefore, the actual version is likelly the result of + /// version & ~(1 << 24). + /// This is encoded as a number, see `version_hex` for a hex-encoded version + pub version: i32, + #[serde(rename = "versionHex")] + /// Same as `version` by hex-encoded + pub version_hex: String, + /// This block's merkle root + /// + /// A Merkle Tree is a binary tree where every leaf is some data, and the branches are pairwise + /// hashes util reaching the root. This allows for compact proof of inclusion in the original + /// set. This merkle tree commits to the txid of all transactions in a block, and is used by + /// some light clients to determine whether a transaction is in a given block + pub merkleroot: String, + /// A list of hex-encoded transaction id for the tx's in this block + pub tx: Vec, + /// The timestamp commited to in this block's header + /// + /// Since there's no central clock that can tell time precisely in Bitcoin, this value is + /// reported by miners and only constrained by a couple of consensus rules. More sensibly, it + /// is **not** garanteed to be monotonical. So a block n might have a lower timestamp than + /// block `n - 1`. + /// If you need it to be monotonical, see `mediantime` insted + pub time: u32, + /// The meadian of the last 11 blocktimes. + /// + /// This is a monotonically increasing number that bounds how old a block can be. Blocks may + /// not have a timestamp less than the current `mediantime`. This is also used in relative + /// timelocks. + pub mediantime: u32, + /// The nonce used to mine this block. + /// + /// Blocks are mined by increasing this value until you find a hash that is less than a network + /// defined target. This number has no meaning in itself and is just a random u32. + pub nonce: u32, + /// Bits is a compact representation for the target. + /// + /// This is a exponential format (with well-define rouding) used by openssl that Satoshi + /// decided to make consensus critical :/ + pub bits: String, + /// The difficulty is derived from the current target and is defined as how many hashes, on + /// average, one has to make before finding a valid block + /// + /// This is computed as 1 / (target / 2 ^ 256). In most softwares (this one inclued) the + /// difficulty is a multiple of the smallest possible difficulty. So to find the actual + /// difficulty you have to multiply this by the min_diff. + /// For mainnet, mindiff is 2 ^ 32 + pub difficulty: u128, + /// Commullative work in this network + /// + /// This is a estimate of how many hashes the network has ever made to produce this chain + pub chainwork: String, + /// How many transactions in this block + pub n_tx: usize, + /// The hash of the block comming before this one + pub previousblockhash: String, + #[serde(skip_serializing_if = "Option::is_none")] + /// The hash of the block comming after this one, if any + pub nextblockhash: Option, +} + +#[derive(Debug)] +/// All possible errors returned by the jsonrpc +pub enum Error { + /// An error while deserializing our response + Serde(serde_json::Error), + #[cfg(feature = "with-reqwest")] + /// An internal reqwest error + Reqwest(reqwest::Error), + /// An error internal to our jsonrpc server + Api(serde_json::Value), + /// The server sent an empty response + EmtpyResponse, +} + +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Error::Serde(value) + } +} + +impl From for Error { + fn from(value: reqwest::Error) -> Self { + Error::Reqwest(value) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Reqwest(e) => write!(f, "reqwest returned an error {e}"), + Error::Api(e) => write!(f, "general jsonrpc error: {e}"), + Error::Serde(e) => write!(f, "error while deserializing the response: {e}"), + Error::EmtpyResponse => write!(f, "got an empty response from server"), + } + } +} + +impl std::error::Error for Error {} diff --git a/crates/floresta-electrum/src/electrum_protocol.rs b/crates/floresta-electrum/src/electrum_protocol.rs index d0e47f6f..f45cf95f 100644 --- a/crates/floresta-electrum/src/electrum_protocol.rs +++ b/crates/floresta-electrum/src/electrum_protocol.rs @@ -117,7 +117,7 @@ pub struct ElectrumServer { impl ElectrumServer { pub async fn new( - address: &'static str, + address: String, address_cache: Arc>>, chain: Arc, block_filters: Option>, diff --git a/florestad/src/cli.rs b/florestad/src/cli.rs index 87a8559c..705192e4 100644 --- a/florestad/src/cli.rs +++ b/florestad/src/cli.rs @@ -111,6 +111,10 @@ pub enum Commands { zmq_address: Option, #[arg(long)] connect: Option, + #[arg(long)] + rpc_address: Option, + #[arg(long)] + electrum_address: Option, }, } diff --git a/florestad/src/json_rpc/server.rs b/florestad/src/json_rpc/server.rs index 679de29f..a008d333 100644 --- a/florestad/src/json_rpc/server.rs +++ b/florestad/src/json_rpc/server.rs @@ -1,3 +1,4 @@ +use std::net::SocketAddr; use std::sync::Arc; use async_std::sync::RwLock; @@ -5,6 +6,7 @@ use bitcoin::block::Header as BlockHeader; use bitcoin::consensus::deserialize; use bitcoin::consensus::encode::serialize_hex; use bitcoin::consensus::serialize; +use bitcoin::constants::genesis_block; use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::Hash; use bitcoin::hex::DisplayHex; @@ -60,7 +62,7 @@ pub trait Rpc { #[rpc(name = "gettxproof")] fn get_tx_proof(&self, tx_id: Txid) -> Result>; #[rpc(name = "loaddescriptor")] - fn load_descriptor(&self, descriptor: String, rescan: Option) -> Result<()>; + fn load_descriptor(&self, descriptor: String, rescan: Option) -> Result; #[rpc(name = "rescan")] fn rescan(&self, rescan: u32) -> Result; #[rpc(name = "getheight")] @@ -265,7 +267,7 @@ impl Rpc for RpcImpl { Err(Error::TxNotFound.into()) } - fn load_descriptor(&self, descriptor: String, rescan: Option) -> Result<()> { + fn load_descriptor(&self, descriptor: String, rescan: Option) -> Result { let wallet = block_on(self.wallet.write()); let result = wallet.push_descriptor(&descriptor); if let Some(rescan) = rescan { @@ -276,7 +278,7 @@ impl Rpc for RpcImpl { if result.is_err() { return Err(Error::InvalidDescriptor.into()); } - Ok(()) + Ok(true) } fn rescan(&self, rescan: u32) -> Result { @@ -313,7 +315,14 @@ impl Rpc for RpcImpl { fn get_block(&self, hash: BlockHash, verbosity: Option) -> Result { let verbosity = verbosity.unwrap_or(1); - if let Ok(Some(block)) = self.node.get_block(hash) { + + let block = if self.chain.get_block_hash(0).unwrap().eq(&hash) { + Some(genesis_block(self.network)) + } else { + self.node.get_block(hash).map_err(|_| Error::Chain)? + }; + + if let Some(block) = block { if verbosity == 1 { let tip = self.chain.get_height().map_err(|_| Error::Chain)?; let height = self @@ -321,20 +330,22 @@ impl Rpc for RpcImpl { .get_block_height(&hash) .map_err(|_| Error::Chain)? .unwrap(); - let mut last_block_times: Vec<_> = ((height - 11)..height) - .map(|h| { - self.chain - .get_block_header(&self.chain.get_block_hash(h).unwrap()) - .unwrap() - .time - }) - .collect(); - last_block_times.sort(); - let median_time_past = if last_block_times.len() > 5 { + + let median_time_past = if height > 11 { + let mut last_block_times: Vec<_> = ((height - 11)..height) + .map(|h| { + self.chain + .get_block_header(&self.chain.get_block_hash(h).unwrap()) + .unwrap() + .time + }) + .collect(); + last_block_times.sort(); last_block_times[5] } else { - 0 + block.header.time }; + let block = BlockJson { bits: serialize_hex(&block.header.bits), chainwork: block.header.work().to_string(), @@ -498,6 +509,8 @@ impl RpcImpl { _ => 8332, } } + + #[allow(clippy::too_many_arguments)] pub fn create( chain: Arc>, wallet: Arc>>, @@ -506,6 +519,7 @@ impl RpcImpl { kill_signal: Arc>, network: Network, block_filter_storage: Option>, + address: Option, ) -> jsonrpc_http_server::Server { let mut io = jsonrpc_core::IoHandler::new(); let rpc_impl = RpcImpl { @@ -517,14 +531,14 @@ impl RpcImpl { block_filter_storage, }; io.extend_with(rpc_impl.to_delegate()); - + let address = address.unwrap_or_else(|| { + format!("127.0.0.1:{}", Self::get_port(net)) + .parse() + .unwrap() + }); ServerBuilder::new(io) .threads(1) - .start_http( - &format!("127.0.0.1:{}", Self::get_port(net)) - .parse() - .unwrap(), - ) + .start_http(&address) .unwrap() } } diff --git a/florestad/src/main.rs b/florestad/src/main.rs index 7a65d5db..334349d7 100644 --- a/florestad/src/main.rs +++ b/florestad/src/main.rs @@ -87,6 +87,9 @@ struct Ctx { #[cfg(feature = "zmq-server")] zmq_address: Option, connect: Option, + #[cfg(feature = "json-rpc")] + json_rpc_address: Option, + electrum_address: Option, } fn main() { // Setup global logger @@ -109,6 +112,8 @@ fn main() { cfilters, cfilter_types, connect, + rpc_address, + electrum_address, }) => { // By default, we build filters for WPKH and TR outputs, as they are the newest. // We also build the `inputs` filters to find spends @@ -133,6 +138,9 @@ fn main() { #[cfg(feature = "zmq-server")] zmq_address: _zmq_address, connect, + #[cfg(feature = "json-rpc")] + json_rpc_address: rpc_address, + electrum_address, }; run_with_ctx(ctx); @@ -329,11 +337,14 @@ fn run_with_ctx(ctx: Ctx) { kill_signal.clone(), get_net(&ctx.network), cfilters.clone(), + ctx.json_rpc_address + .map(|x| x.parse().expect("Invalid json rpc address")), ); // Electrum + let electrum_address = ctx.electrum_address.unwrap_or("0.0.0.0:50001".into()); let electrum_server = block_on(ElectrumServer::new( - "0.0.0.0:50001", + electrum_address, wallet, blockchain_state, cfilters,