diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index b6d159d1..bc0ae9f1 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -148,6 +157,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.8.0" @@ -280,7 +295,7 @@ dependencies = [ "anyhow", "bitcoin_hashes", "corepc-client", - "env_logger", + "env_logger 0.9.3", "flate2", "log", "minreq", @@ -315,6 +330,33 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "electrsd" +version = "0.36.1" +dependencies = [ + "bitcoin_hashes", + "corepc-client", + "corepc-node", + "electrum-client", + "env_logger 0.10.2", + "log", + "minreq", + "nix", + "zip", +] + +[[package]] +name = "electrum-client" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7b07e2578a6df0093b101915c79dca0119d7f7810099ad9eef11341d2ae57" +dependencies = [ + "bitcoin", + "log", + "serde", + "serde_json", +] + [[package]] name = "env_logger" version = "0.9.3" @@ -324,6 +366,19 @@ dependencies = [ "log", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "errno" version = "0.3.10" @@ -385,6 +440,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex-conservative" version = "0.2.1" @@ -406,6 +467,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -430,6 +497,17 @@ dependencies = [ "cc", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi 0.5.2", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "itoa" version = "1.0.11" @@ -469,7 +547,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.8.0", "libc", "redox_syscall", ] @@ -492,6 +570,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.8.4" @@ -522,12 +609,26 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -564,6 +665,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.31" @@ -600,9 +707,38 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags", + "bitflags 2.8.0", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "ring" version = "0.17.8" @@ -630,7 +766,7 @@ version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", @@ -737,7 +873,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.8.0", "core-foundation", "core-foundation-sys", "libc", @@ -853,6 +989,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "tiny_http" version = "0.12.0" @@ -1016,6 +1161,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo-recent.lock b/Cargo-recent.lock index 53897d05..29cc8bbc 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -148,6 +157,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.8.0" @@ -280,7 +295,7 @@ dependencies = [ "anyhow", "bitcoin_hashes", "corepc-client", - "env_logger", + "env_logger 0.9.3", "flate2", "log", "minreq", @@ -315,6 +330,33 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "electrsd" +version = "0.36.1" +dependencies = [ + "bitcoin_hashes", + "corepc-client", + "corepc-node", + "electrum-client", + "env_logger 0.10.2", + "log", + "minreq", + "nix", + "zip", +] + +[[package]] +name = "electrum-client" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7b07e2578a6df0093b101915c79dca0119d7f7810099ad9eef11341d2ae57" +dependencies = [ + "bitcoin", + "log", + "serde", + "serde_json", +] + [[package]] name = "env_logger" version = "0.9.3" @@ -324,6 +366,19 @@ dependencies = [ "log", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "errno" version = "0.3.10" @@ -379,6 +434,12 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex-conservative" version = "0.2.1" @@ -400,6 +461,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -430,11 +497,22 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags", + "bitflags 2.8.0", "cfg-if", "libc", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "itoa" version = "1.0.14" @@ -474,7 +552,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.8.0", "libc", "redox_syscall", ] @@ -497,6 +575,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.8.4" @@ -532,6 +619,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -568,6 +669,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.31" @@ -604,9 +711,38 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags", + "bitflags 2.8.0", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "ring" version = "0.17.8" @@ -634,7 +770,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", @@ -741,7 +877,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.8.0", "core-foundation", "core-foundation-sys", "libc", @@ -864,6 +1000,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "tiny_http" version = "0.12.0" @@ -1029,6 +1174,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index c62e9a3a..d4cc080a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "bitreq", "client", "jsonrpc", "node", "types"] +members = [ "bitreq", "client", "jsonrpc", "node", "types", "electrsd"] exclude = ["integration_test", "verify"] resolver = "2" diff --git a/contrib/update-lock-files.sh b/contrib/update-lock-files.sh index 4c8b861e..7bd877f9 100755 --- a/contrib/update-lock-files.sh +++ b/contrib/update-lock-files.sh @@ -6,6 +6,12 @@ set -euo pipefail for file in Cargo-minimal.lock Cargo-recent.lock; do cp --force "$file" Cargo.lock - cargo check --all-features + + cd electrsd + cargo check --features="corepc-node_29_0,electrs_0_10_6" + cd .. + + cargo check + cp --force Cargo.lock "$file" done diff --git a/electrsd/Cargo.toml b/electrsd/Cargo.toml new file mode 100644 index 00000000..a93453da --- /dev/null +++ b/electrsd/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "electrsd" +version = "0.36.1" +authors = ["Riccardo Casatta "] +description = "Utility to run a regtest electrs process, useful in integration testing environment" +repository = "https://github.com/RCasatta/electrsd" +documentation = "https://docs.rs/elecrtsd/" +license = "MIT" +edition = "2018" +categories = ["cryptography::cryptocurrencies", "development-tools::testing"] + +[dependencies] +corepc-node = { version = "0.10.0" } +corepc-client = { version = "0.10.0" } +electrum-client = { version = "0.24.0", default-features = false } +log = { version = "0.4" } + +[target.'cfg(not(windows))'.dependencies] +nix = { version = "0.25.0" } + +[dev-dependencies] +env_logger = { version = "0.10" } + +[build-dependencies] +bitcoin_hashes = { version = "0.14", optional = true } +zip = { version = "0.6", default-features = false, optional = true, features = [ + "bzip2", + "deflate", +] } +minreq = { version = "2.9.0", default-features = false, optional = true, features = [ + "https", +] } + +[features] +legacy = [] + +# download is not supposed to be used directly only through selecting one of the version feature +download = ["bitcoin_hashes", "zip", "minreq"] + +esplora_a33e97e1 = ["download", "legacy"] +electrs_0_8_10 = ["download"] +electrs_0_9_1 = ["download"] +electrs_0_9_11 = ["download"] +electrs_0_10_6 = ["download"] + +corepc-node_29_0 = ["corepc-node/download", "corepc-node/29_0"] +corepc-node_28_2 = ["corepc-node/download", "corepc-node/28_2"] +corepc-node_27_2 = ["corepc-node/download", "corepc-node/27_2"] +corepc-node_26_2 = ["corepc-node/download", "corepc-node/26_2"] +corepc-node_25_2 = ["corepc-node/download", "corepc-node/25_2"] +corepc-node_24_2 = ["corepc-node/download", "corepc-node/24_2"] +corepc-node_23_1 = ["corepc-node/download", "corepc-node/23_2"] +corepc-node_22_1 = ["corepc-node/download", "corepc-node/22_1"] +corepc-node_0_21_2 = ["corepc-node/download", "corepc-node/0_21_2"] +corepc-node_0_20_2 = ["corepc-node/download", "corepc-node/0_20_2"] +corepc-node_0_19_1 = ["corepc-node/download", "corepc-node/0_19_1"] +corepc-node_0_18_1 = ["corepc-node/download", "corepc-node/0_18_1"] +corepc-node_0_17_2 = ["corepc-node/download", "corepc-node/0_17_2"] diff --git a/electrsd/LICENSE b/electrsd/LICENSE new file mode 100644 index 00000000..6a083901 --- /dev/null +++ b/electrsd/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Riccardo Casatta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/electrsd/README.md b/electrsd/README.md new file mode 100644 index 00000000..e4c64f7d --- /dev/null +++ b/electrsd/README.md @@ -0,0 +1,86 @@ +[![MIT license](https://img.shields.io/github/license/RCasatta/electrsd)](https://github.com/RCasatta/electrsd/blob/master/LICENSE) +[![Crates](https://img.shields.io/crates/v/electrsd.svg)](https://crates.io/crates/electrsd) + +# Electrsd + +Utility to run a regtest [electrs](https://github.com/romanz/electrs/) process connected to a given [bitcoind](https://github.com/RCasatta/bitcoind) instance, +useful in integration testing environment. + +```rust +let bitcoind = bitcoind::BitcoinD::new("/usr/local/bin/bitcoind").unwrap(); +let electrsd = electrsd::ElectrsD::new("/usr/local/bin/electrs", bitcoind).unwrap(); +let header = electrsd.client.block_headers_subscribe().unwrap(); +assert_eq!(header.height, 0); +``` + +## Automatic binaries download + +In your project Cargo.toml, activate the following features + +```yml +electrsd = { version= "0.23", features = ["corepc-node_23_1", "electrs_0_9_1"] } +``` + +Then use it: + +```rust +let bitcoind_exe = bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled"); +let bitcoind = bitcoind::BitcoinD::new(bitcoind_exe).unwrap(); +let electrs_exe = electrsd::downloaded_exe_path().expect("electrs version feature must be enabled"); +let electrsd = electrsd::ElectrsD::new(electrs_exe, bitcoind).unwrap(); +``` + +When the `ELECTRSD_DOWNLOAD_ENDPOINT`/`BITCOIND_DOWNLOAD_ENDPOINT` environment variables are set, +`electrsd`/`bitcoind` will try to download the binaries from the given endpoints. + +When you don't use the auto-download feature you have the following options: + +- have `electrs` executable in the `PATH` +- provide the `electrs` executable via the `ELECTRS_EXEC` env var + +```rust +if let Ok(exe_path) = electrsd::exe_path() { + let electrsd = electrsd::electrsD::new(exe_path).unwrap(); +} +``` + +Startup options could be configured via the `Conf` struct using `electrsD::with_conf` or `electrsD::from_downloaded_with_conf`. + +## Nix + +For determinisim, in nix you cannot hit the internet within the `build.rs`. Moreover, some downstream crates cannot remove the auto-download feature from their dev-deps. In this case you can set the `ELECTRSD_SKIP_DOWNLOAD` env var and provide the electrs executable in the `PATH` (or skip the test execution). + +## Issues with traditional approach + +I used integration testing based on external bash script launching needed external processes, there are many issues with this approach like: + +* External script may interfere with local development environment https://github.com/rust-bitcoin/rust-bitcoincore-rpc/blob/200fc8247c1896709a673b82a89ca0da5e7aa2ce/integration_test/run.sh#L9 +* Use of a single huge test to test everything https://github.com/rust-bitcoin/rust-bitcoincore-rpc/blob/200fc8247c1896709a673b82a89ca0da5e7aa2ce/integration_test/src/main.rs#L122-L203 +* If test are separated, a failing test may fail to leave a clean situation, causing other test to fail (because of the initial situation, not a real failure) +* bash script are hard, especially support different OS and versions + +## Features + + * electrsd use a temporary directory as db dir + * A free port is asked to the OS (a very low probability race condition is still possible) + * The process is killed when the struct goes out of scope no matter how the test finishes + * Automatically download `electrs` executable with enabled features. Since there are no official binaries, they are built using the [manual workflow](.github/workflows/build_electrs.yml) under this project. Supported version are: + * [electrs 0.10.6](https://github.com/romanz/electrs/releases/tag/v0.10.6) (feature=electrs_0_10_6) + * [electrs 0.9.11](https://github.com/romanz/electrs/releases/tag/v0.9.11) (feature=electrs_0_9_11) + * [electrs 0.9.1](https://github.com/romanz/electrs/releases/tag/v0.9.1) (feature=electrs_0_9_1) + * [electrs 0.8.10](https://github.com/romanz/electrs/releases/tag/v0.8.10) (feature=electrs_0_8_10) + * [electrs esplora](https://github.com/Blockstream/electrs/tree/a33e97e1a1fc63fa9c20a116bb92579bbf43b254) (feature=esplora_a33e97e1) + +Thanks to these features every `#[test]` could easily run isolated with its own environment + +## Deprecations + +- Starting from version `0.26` the env var `ELECTRS_EXE` is deprecated in favor of `ELECTRS_EXEC`. + + +## Used by + + * [bdk](https://github.com/bitcoindevkit/bdk) + * [BEWallet](https://github.com/LeoComandini/BEWallet) + * [gdk rust](https://github.com/Blockstream/gdk/blob/master/subprojects/gdk_rust/) + * [lwk](https://github.com/Blockstream/lwk) diff --git a/electrsd/build.rs b/electrsd/build.rs new file mode 100644 index 00000000..727a1396 --- /dev/null +++ b/electrsd/build.rs @@ -0,0 +1,83 @@ +#[cfg(not(feature = "download"))] +fn main() {} + +#[cfg(feature = "download")] +fn main() { + download::download() +} + +#[cfg(feature = "download")] +mod download { + use bitcoin_hashes::{sha256, Hash}; + use std::fs::File; + use std::io::{BufRead, BufReader, Cursor}; + use std::os::unix::fs::PermissionsExt; + use std::path::Path; + use std::str::FromStr; + + include!("src/versions.rs"); + + const GITHUB_URL: &str = + "https://github.com/RCasatta/electrsd/releases/download/electrs_releases"; + + fn get_expected_sha256(filename: &str) -> Result { + let file = File::open("sha256").map_err(|_| ())?; + for line in BufReader::new(file).lines().flatten() { + let tokens: Vec<_> = line.split(" ").collect(); + if tokens.len() == 2 && filename == tokens[1] { + return sha256::Hash::from_str(tokens[0]).map_err(|_| ()); + } + } + Err(()) + } + + pub fn download() { + if std::env::var_os("ELECTRSD_SKIP_DOWNLOAD").is_some() { + return; + } + + if !HAS_FEATURE { + return; + } + let download_filename_without_extension = electrs_name(); + let download_filename = format!("{}.zip", download_filename_without_extension); + dbg!(&download_filename); + let expected_hash = get_expected_sha256(&download_filename).unwrap(); + let out_dir = std::env::var_os("OUT_DIR").unwrap(); + let electrs_exe_home = Path::new(&out_dir).join("electrs"); + let destination_filename = electrs_exe_home + .join(&download_filename_without_extension) + .join("electrs"); + + dbg!(&destination_filename); + + if !destination_filename.exists() { + println!( + "filename:{} version:{} hash:{}", + download_filename, VERSION, expected_hash + ); + + let download_endpoint = + std::env::var("ELECTRSD_DOWNLOAD_ENDPOINT").unwrap_or(GITHUB_URL.to_string()); + let url = format!("{}/{}", download_endpoint, download_filename); + + let downloaded_bytes = minreq::get(url).send().unwrap().into_bytes(); + + let downloaded_hash = sha256::Hash::hash(&downloaded_bytes); + assert_eq!(expected_hash, downloaded_hash); + let cursor = Cursor::new(downloaded_bytes); + + let mut archive = zip::ZipArchive::new(cursor).unwrap(); + let mut file = archive.by_index(0).unwrap(); + std::fs::create_dir_all(destination_filename.parent().unwrap()).unwrap(); + let mut outfile = std::fs::File::create(&destination_filename).unwrap(); + + std::io::copy(&mut file, &mut outfile).unwrap(); + std::fs::set_permissions( + &destination_filename, + std::fs::Permissions::from_mode(0o755), + ) + .unwrap(); + } + } +} diff --git a/electrsd/contrib/extra_tests.sh b/electrsd/contrib/extra_tests.sh new file mode 100755 index 00000000..a86d1aee --- /dev/null +++ b/electrsd/contrib/extra_tests.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# Additional tests for `electrs`. +# +# This script was written based on CI in +# `github.com/RCasatta/electrsd` when the crate was imported to this repo. +# (HEAD on that repo was: dc88d81520c9ec507fc830b88a3b92a034dbaf93). + +set -euox pipefail + +# Use the current `Cargo.lock` file without updating it. +cargo="cargo --locked" + + +main() { + corepc_node_versions + electrs_versions + electrs_esplora +} + +corepc_node_versions() { + local corepc_node_versions=("corepc-node_29_0" "corepc-node_28_2" "corepc-node_27_2" "corepc-node_26_2" "corepc-node_25_2") + + for feature in "${corepc_node_versions}"; do + $cargo test --features="$feature,electrs_0_10_6" + done +} + +electrs_versions() { + local electrs_versions=("electrs_0_10_6" "electrs_0_9_11" "electrs_0_9_1" "electrs_0_8_10") + + for feature in "${electrs_versions}"; do + $cargo test --features="corepc-node_29_0,$feature" + done +} + +electrs_esplora() { + $cargo test --features="corepc-node_22_1,legacy,esplora_a33e97e1" +} + +# +# Main script +# +main "$@" +exit 0 diff --git a/electrsd/contrib/test_vars.sh b/electrsd/contrib/test_vars.sh new file mode 100644 index 00000000..814c2341 --- /dev/null +++ b/electrsd/contrib/test_vars.sh @@ -0,0 +1,18 @@ +# No shebang, this file should not be executed. +# shellcheck disable=SC2148 +# +# disable verify unused vars, despite the fact that they are used when sourced +# shellcheck disable=SC2034 + +# Test all these features with "std" enabled. +FEATURES_WITH_STD="" + +# Test all these features without "std" or "alloc" enabled. +FEATURES_WITHOUT_STD="" + +# Run these examples. +EXAMPLES="" + +# Just check the latest minor version of the last three supported Core versions. +# This is mainly for docs and MSRV - integration tests will catch any other errors. +EXACT_FEATURES=("download,28_2" "download,27_2" "download,26_2") diff --git a/electrsd/sha256 b/electrsd/sha256 new file mode 100644 index 00000000..a8941189 --- /dev/null +++ b/electrsd/sha256 @@ -0,0 +1,10 @@ +2d5ff149e8a2482d3658e9b386830dfc40c8fbd7c175ca7cbac58240a9505bcd electrs_macos_esplora_a33e97e1a1fc63fa9c20a116bb92579bbf43b254.zip +865e26a96e8df77df01d96f2f569dcf9622fc87a8d99a9b8fe30861a4db9ddf1 electrs_linux_esplora_a33e97e1a1fc63fa9c20a116bb92579bbf43b254.zip +0459d493d399bdb9ef145c84125c3cd26c1993a48efe59fa9d3fa13a03b2f555 electrs_linux_v0.8.10.zip +48c857ca953ea66ee31c4da5a801298c85815e792ab57291107e77a4871d5421 electrs_macos_v0.8.10.zip +fee5cc9b6c8bbd3adc45c63c881844d948c1b4dd6817f99ee087a0ccc4ba3be0 electrs_linux_v0.9.1.zip +10f468e9e617bfe8f9f4897fa4cbbb92fe809d977b747e4326f4c8e5dc1b3a51 electrs_macos_v0.9.1.zip +2b2f8aef35cd8e16e109b948a903d010aa472f6cdf2147d47e01fd95cd1785da electrs_linux_v0.9.11.zip +b794287a5d98e590deadf07a3eb391cc1a53ef160c8cdcb8e6b14d856c7b181d electrs_macos_v0.9.11.zip +448693f42fa2e310bd86ba9a7304c9ab464854a3c7e4c3eaa8c774efeb0fbdd1 electrs_linux_v0.10.6.zip +016ad9ef227c12ae6096ada1db09179d3330a4062ba2ab65390d6988f659be05 electrs_macos_v0.10.6.zip diff --git a/electrsd/src/error.rs b/electrsd/src/error.rs new file mode 100644 index 00000000..c8dc5240 --- /dev/null +++ b/electrsd/src/error.rs @@ -0,0 +1,75 @@ +/// All the possible error in this crate +#[derive(Debug)] +pub enum Error { + /// Wrapper of io Error + Io(std::io::Error), + + /// Wrapper of bitcoind Error + Bitcoind(corepc_node::Error), + + /// Wrapper of electrum_client Error + ElectrumClient(electrum_client::Error), + + /// Wrapper of nix Error + #[cfg(not(target_os = "windows"))] + Nix(nix::Error), + + /// Wrapper of early exit status + EarlyExit(std::process::ExitStatus), + + /// Returned when both tmpdir and staticdir is specified in `Conf` options + BothDirsSpecified, + + /// Returned when calling methods requiring the bitcoind executable but none is found + /// (no feature, no `ELECTRS_EXEC`, no `electrs` in `PATH` ) + NoElectrsExecutableFound, + + /// Returned if both env vars `ELECTRS_EXEC` and `ELECTRS_EXE` are found + BothEnvVars, +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(e) => Some(e), + Error::Bitcoind(e) => Some(e), + Error::ElectrumClient(e) => Some(e), + // Error::BitcoinCoreRpc(e) => Some(e), + #[cfg(not(target_os = "windows"))] + Error::Nix(e) => Some(e), + + _ => None, + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: corepc_node::Error) -> Self { + Error::Bitcoind(e) + } +} + +impl From for Error { + fn from(e: electrum_client::Error) -> Self { + Error::ElectrumClient(e) + } +} + +#[cfg(not(target_os = "windows"))] +impl From for Error { + fn from(e: nix::Error) -> Self { + Error::Nix(e) + } +} diff --git a/electrsd/src/ext.rs b/electrsd/src/ext.rs new file mode 100644 index 00000000..147bd413 --- /dev/null +++ b/electrsd/src/ext.rs @@ -0,0 +1,97 @@ +//! Extra functions for the electrs rpc +//! + +use std::thread; +use std::time::Duration; + +use electrum_client::{bitcoin::Txid, ElectrumApi}; + +use crate::ElectrsD; + +impl ElectrsD { + #[cfg(not(feature = "electrs_0_8_10"))] + /// wait up to a minute the electrum server has indexed up to the given height. + pub fn wait_height(&self, height: usize) { + for _ in 0..600 { + match self.client.block_header_raw(height) { + Ok(_) => break, + Err(_) => thread::sleep(Duration::from_millis(100)), + } + } + } + + /// wait up to a minute the electrum server has indexed the given transaction + pub fn wait_tx(&self, txid: &Txid) { + 'main_loop: for _ in 0..600 { + match self.client.transaction_get(txid) { + Ok(tx) => { + // having the raw tx doesn't mean the scripts has been indexed + let txid = tx.compute_txid(); + if let Some(output) = tx.output.first() { + let history = self + .client + .script_get_history(&output.script_pubkey) + .unwrap(); + for el in history { + if el.tx_hash == txid { + // the tx has to be updated atomically, so founding one is enough + return; + } + } + // the tx output has not been yet found + continue 'main_loop; + } + // the tx has 0 ouptut, no need to ensure script_pubkey are indexed + return; + } + Err(_) => thread::sleep(Duration::from_millis(100)), + } + } + } +} + +#[cfg(test)] +mod test { + use crate::test::setup_nodes; + use electrum_client::{bitcoin::Amount, ElectrumApi}; + + #[cfg(not(feature = "electrs_0_8_10"))] + #[test] + fn test_wait_height() { + let (_, bitcoind, electrsd) = setup_nodes(); + let header = electrsd.client.block_headers_subscribe().unwrap(); + assert_eq!(header.height, 1); + let address = bitcoind.client.new_address().unwrap(); + bitcoind.client.generate_to_address(100, &address).unwrap(); + electrsd.wait_height(101); + let header = electrsd.client.block_headers_subscribe().unwrap(); + assert_eq!(header.height, 101); + } + + #[test] + fn test_wait_tx() { + let (_, bitcoind, electrsd) = setup_nodes(); + let header = electrsd.client.block_headers_subscribe().unwrap(); + assert_eq!(header.height, 1); + let generate_address = bitcoind.client.new_address().unwrap(); + bitcoind + .client + .generate_to_address(100, &generate_address) + .unwrap(); + + let address = bitcoind.client.new_address().unwrap(); + let txid = bitcoind + .client + .send_to_address(&address, Amount::from_sat(10000)) + .unwrap() + .txid() + .unwrap(); + + electrsd.wait_tx(&txid); + let history = electrsd + .client + .script_get_history(&address.script_pubkey()) + .unwrap(); + assert_eq!(history.len(), 1); + } +} diff --git a/electrsd/src/lib.rs b/electrsd/src/lib.rs new file mode 100644 index 00000000..9936b597 --- /dev/null +++ b/electrsd/src/lib.rs @@ -0,0 +1,500 @@ +#![warn(missing_docs)] + +//! +//! Electrsd +//! +//! Utility to run a regtest electrsd process, useful in integration testing environment +//! + +mod error; +mod ext; +mod versions; + +use corepc_node::anyhow::Context; +use corepc_node::get_available_port; +use corepc_node::serde_json::Value; +use corepc_node::tempfile::TempDir; +use corepc_node::{anyhow, Node}; +use electrum_client::raw_client::{ElectrumPlaintextStream, RawClient}; +use log::{debug, error, warn}; +use std::env; +use std::ffi::OsStr; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; +use std::time::Duration; + +// re-export corepc_node +pub use corepc_node; +// re-export corepc_client +pub use corepc_client; +// re-export electrum_client because calling RawClient methods requires the ElectrumApi trait +pub use electrum_client; + +pub use error::Error; + +/// Electrs configuration parameters, implements a convenient [Default] for most common use. +/// +/// Default values: +/// ``` +/// let mut conf = electrsd::Conf::default(); +/// conf.view_stderr = false; +/// conf.http_enabled = false; +/// conf.network = "regtest"; +/// conf.tmpdir = None; +/// conf.staticdir = None; +/// assert_eq!(conf, electrsd::Conf::default()); +/// ``` +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub struct Conf<'a> { + /// Electrsd command line arguments + /// note that `db-dir`, `cookie`, `cookie-file`, `daemon-rpc-addr`, `jsonrpc-import`, `electrum-rpc-addr`, `monitoring-addr`, `http-addr` cannot be used cause they are automatically initialized. + pub args: Vec<&'a str>, + + /// if `true` electrsd log output will not be suppressed + pub view_stderr: bool, + + /// if `true` electrsd exposes an esplora endpoint + pub http_enabled: bool, + + /// Must match bitcoind network + pub network: &'a str, + + /// Optionally specify a temporary or persistent working directory for the electrs. + /// electrs index files will be stored in this path. + /// The following two parameters can be configured to simulate desired working directory configuration. + /// + /// tmpdir is Some() && staticdir is Some() : Error. Cannot be enabled at same time. + /// tmpdir is Some(temp_path) && staticdir is None : Create temporary directory at `tmpdir` path. + /// tmpdir is None && staticdir is Some(work_path) : Create persistent directory at `staticdir` path. + /// tmpdir is None && staticdir is None: Creates a temporary directory in OS default temporary directory (eg /tmp) or `TEMPDIR_ROOT` env variable path. + /// + /// Temporary directory path + pub tmpdir: Option, + + /// Persistent directory path + pub staticdir: Option, + + /// Try to spawn the process `attempt` time + /// + /// The OS is giving available ports to use, however, they aren't booked, so it could rarely + /// happen they are used at the time the process is spawn. When retrying other available ports + /// are returned reducing the probability of conflicts to negligible. + attempts: u8, +} + +impl Default for Conf<'_> { + fn default() -> Self { + let args = if cfg!(feature = "electrs_0_9_1") + || cfg!(feature = "electrs_0_8_10") + || cfg!(feature = "esplora_a33e97e1") + || cfg!(feature = "legacy") + { + vec!["-vvv"] + } else { + vec![] + }; + + Conf { + args, + view_stderr: false, + http_enabled: false, + network: "regtest", + tmpdir: None, + staticdir: None, + attempts: 3, + } + } +} + +/// Struct representing the electrs process with related information +pub struct ElectrsD { + /// Process child handle, used to terminate the process when this struct is dropped + process: Child, + /// Electrum client connected to the electrs process + pub client: RawClient, + /// Work directory, where the electrs stores indexes and other stuffs. + work_dir: DataDir, + /// Url to connect to the electrum protocol (tcp) + pub electrum_url: String, + /// Url to connect to esplora protocol (http) + pub esplora_url: Option, +} + +/// The DataDir struct defining the kind of data directory electrs will use. +/// /// Data directory can be either persistent, or temporary. +pub enum DataDir { + /// Persistent Data Directory + Persistent(PathBuf), + /// Temporary Data Directory + Temporary(TempDir), +} + +impl DataDir { + /// Return the data directory path + fn path(&self) -> PathBuf { + match self { + Self::Persistent(path) => path.to_owned(), + Self::Temporary(tmp_dir) => tmp_dir.path().to_path_buf(), + } + } +} + +impl ElectrsD { + /// Create a new electrs process connected with the given bitcoind and default args. + pub fn new>(exe: S, bitcoind: &Node) -> anyhow::Result { + ElectrsD::with_conf(exe, bitcoind, &Conf::default()) + } + + /// Create a new electrs process using given [Conf] connected with the given bitcoind + pub fn with_conf>( + exe: S, + bitcoind: &Node, + conf: &Conf, + ) -> anyhow::Result { + let response = bitcoind.client.call::("getblockchaininfo", &[])?; + if response + .get("initialblockdownload") + .and_then(|v| v.as_bool()) + .unwrap_or(false) + { + // electrum will remain idle until bitcoind is in IBD + // bitcoind will remain in IBD if doesn't see a block from a long time, thus adding a block + let node_address = bitcoind.client.call::("getnewaddress", &[])?; + bitcoind + .client + .call::("generatetoaddress", &[1.into(), node_address]) + .unwrap(); + } + + let mut args = conf.args.clone(); + + let work_dir = match (&conf.tmpdir, &conf.staticdir) { + (Some(_), Some(_)) => return Err(Error::BothDirsSpecified.into()), + (Some(tmpdir), None) => DataDir::Temporary(TempDir::new_in(tmpdir)?), + (None, Some(workdir)) => { + std::fs::create_dir_all(workdir)?; + DataDir::Persistent(workdir.to_owned()) + } + (None, None) => match env::var("TEMPDIR_ROOT").map(PathBuf::from) { + Ok(path) => DataDir::Temporary(TempDir::new_in(path)?), + Err(_) => DataDir::Temporary(TempDir::new()?), + }, + }; + + let db_dir = format!("{}", work_dir.path().display()); + args.push("--db-dir"); + args.push(&db_dir); + + args.push("--network"); + args.push(conf.network); + + #[cfg(not(feature = "legacy"))] + let cookie_file; + #[cfg(not(feature = "legacy"))] + { + args.push("--cookie-file"); + cookie_file = format!("{}", bitcoind.params.cookie_file.display()); + args.push(&cookie_file); + } + + #[cfg(feature = "legacy")] + let mut cookie_value; + #[cfg(feature = "legacy")] + { + use std::io::Read; + args.push("--cookie"); + let mut cookie = std::fs::File::open(&bitcoind.params.cookie_file)?; + cookie_value = String::new(); + cookie.read_to_string(&mut cookie_value)?; + args.push(&cookie_value); + } + + args.push("--daemon-rpc-addr"); + let rpc_socket = bitcoind.params.rpc_socket.to_string(); + args.push(&rpc_socket); + + let p2p_socket; + if cfg!(feature = "electrs_0_8_10") + || cfg!(feature = "esplora_a33e97e1") + || cfg!(feature = "legacy") + { + args.push("--jsonrpc-import"); + } else { + args.push("--daemon-p2p-addr"); + p2p_socket = bitcoind + .params + .p2p_socket + .expect("electrs_0_9_1 requires bitcoind with p2p port open") + .to_string(); + args.push(&p2p_socket); + } + + let electrum_url = format!("0.0.0.0:{}", get_available_port()?); + args.push("--electrum-rpc-addr"); + args.push(&electrum_url); + + // would be better to disable it, didn't found a flag + let monitoring = format!("0.0.0.0:{}", get_available_port()?); + args.push("--monitoring-addr"); + args.push(&monitoring); + + let esplora_url_string; + let esplora_url = if conf.http_enabled { + esplora_url_string = format!("0.0.0.0:{}", get_available_port()?); + args.push("--http-addr"); + args.push(&esplora_url_string); + #[allow(clippy::redundant_clone)] + Some(esplora_url_string.clone()) + } else { + None + }; + + let view_stderr = if conf.view_stderr { + Stdio::inherit() + } else { + Stdio::null() + }; + + debug!("args: {:?}", args); + let mut process = Command::new(&exe) + .args(args) + .stderr(view_stderr) + .spawn() + .with_context(|| format!("Error while executing {:?}", exe.as_ref()))?; + + let client = loop { + if let Some(status) = process.try_wait()? { + if conf.attempts > 0 { + warn!("early exit with: {:?}. Trying to launch again ({} attempts remaining), maybe some other process used our available port", status, conf.attempts); + let mut conf = conf.clone(); + conf.attempts -= 1; + return Self::with_conf(exe, bitcoind, &conf) + .with_context(|| format!("Remaining attempts {}", conf.attempts)); + } else { + error!("early exit with: {:?}", status); + return Err(Error::EarlyExit(status).into()); + } + } + match RawClient::new(&electrum_url, None) { + Ok(client) => break client, + Err(_) => std::thread::sleep(Duration::from_millis(500)), + } + }; + + Ok(ElectrsD { + process, + client, + work_dir, + electrum_url, + esplora_url, + }) + } + + /// triggers electrs sync by sending the `SIGUSR1` signal, useful to call after a block for example + #[cfg(not(target_os = "windows"))] + pub fn trigger(&self) -> anyhow::Result<()> { + Ok(nix::sys::signal::kill( + nix::unistd::Pid::from_raw(self.process.id() as i32), + nix::sys::signal::SIGUSR1, + )?) + } + + #[cfg(target_os = "windows")] + pub fn trigger(&self) -> anyhow::Result<()> { + Ok(()) + } + + /// Return the current workdir path of the running electrs + pub fn workdir(&self) -> PathBuf { + self.work_dir.path() + } + + /// terminate the electrs process + pub fn kill(&mut self) -> anyhow::Result<()> { + match self.work_dir { + DataDir::Persistent(_) => { + self.inner_kill()?; + // Wait for the process to exit + match self.process.wait() { + Ok(_) => Ok(()), + Err(e) => Err(e.into()), + } + } + DataDir::Temporary(_) => Ok(self.process.kill()?), + } + } + + #[cfg(not(target_os = "windows"))] + fn inner_kill(&mut self) -> anyhow::Result<()> { + // Send SIGINT signal to electrsd + Ok(nix::sys::signal::kill( + nix::unistd::Pid::from_raw(self.process.id() as i32), + nix::sys::signal::SIGINT, + )?) + } + + #[cfg(target_os = "windows")] + fn inner_kill(&mut self) -> anyhow::Result<()> { + Ok(self.process.kill()?) + } +} + +impl Drop for ElectrsD { + fn drop(&mut self) { + let _ = self.kill(); + } +} + +/// Provide the electrs executable path if a version feature has been specified and `ELECTRSD_SKIP_DOWNLOAD` is not set. +pub fn downloaded_exe_path() -> Option { + if versions::HAS_FEATURE && std::env::var_os("ELECTRSD_SKIP_DOWNLOAD").is_none() { + Some(format!( + "{}/electrs/{}/electrs", + env!("OUT_DIR"), + versions::electrs_name(), + )) + } else { + None + } +} + +/// Returns the daemon `electrs` executable with the following precedence: +/// +/// 1) If it's specified in the `ELECTRS_EXEC` or in `ELECTRS_EXE` env var (errors if both env vars are present) +/// 2) If there is no env var but an auto-download feature such as `electrs_0_9_11` is enabled, returns the path of the downloaded executabled +/// 3) If neither of the precedent are available, the `electrs` executable is searched in the `PATH` +pub fn exe_path() -> anyhow::Result { + if let (Ok(_), Ok(_)) = (std::env::var("ELECTRS_EXEC"), std::env::var("ELECTRS_EXE")) { + return Err(error::Error::BothEnvVars.into()); + } + if let Ok(path) = std::env::var("ELECTRS_EXEC") { + return Ok(path); + } + if let Ok(path) = std::env::var("ELECTRS_EXE") { + return Ok(path); + } + if let Some(path) = downloaded_exe_path() { + return Ok(path); + } + // Manually search for electrs in PATH + let path_var = env::var("PATH").map_err(|_| Error::NoElectrsExecutableFound)?; + + #[cfg(target_os = "windows")] + let path_separator = ';'; + #[cfg(not(target_os = "windows"))] + let path_separator = ':'; + + for path_dir in path_var.split(path_separator) { + let mut candidate = PathBuf::from(path_dir); + candidate.push("electrs"); + + #[cfg(target_os = "windows")] + { + // On Windows, try with .exe extension + candidate.set_extension("exe"); + } + + if candidate.is_file() { + // Check if the file is executable + #[cfg(not(target_os = "windows"))] + { + use std::os::unix::fs::PermissionsExt; + if let Ok(metadata) = std::fs::metadata(&candidate) { + let permissions = metadata.permissions(); + if permissions.mode() & 0o111 != 0 { + return Ok(candidate.display().to_string()); + } + } + } + + #[cfg(target_os = "windows")] + { + return Ok(candidate.display().to_string()); + } + } + } + + Err(Error::NoElectrsExecutableFound.into()) +} + +#[cfg(test)] +mod test { + use crate::exe_path; + use crate::ElectrsD; + use corepc_node::P2P; + use electrum_client::ElectrumApi; + use log::{debug, log_enabled, Level}; + use std::env; + + #[test] + #[ignore] // launch singularly since env are globals + fn test_both_env_vars() { + env::set_var("ELECTRS_EXEC", "placeholder"); + env::set_var("ELECTRS_EXE", "placeholder"); + assert!(exe_path().is_err()); + // unsetting because this errors everything in mod test! + env::remove_var("ELECTRS_EXEC"); + env::remove_var("ELECTRS_EXE"); + } + + #[test] + fn test_electrsd() { + let (electrs_exe, bitcoind, electrsd) = setup_nodes(); + let header = electrsd.client.block_headers_subscribe().unwrap(); + assert_eq!(header.height, 1); + let address = bitcoind.client.new_address().unwrap(); + bitcoind.client.generate_to_address(100, &address).unwrap(); + + electrsd.trigger().unwrap(); + + let header = loop { + std::thread::sleep(std::time::Duration::from_millis(100)); + let header = electrsd.client.block_headers_subscribe().unwrap(); + if header.height > 100 { + break header; + } + }; + assert_eq!(header.height, 101); + + // launch another instance to check there are no fixed port used + let electrsd = ElectrsD::new(&electrs_exe, &bitcoind).unwrap(); + let header = electrsd.client.block_headers_subscribe().unwrap(); + assert_eq!(header.height, 101); + } + + #[test] + fn test_kill() { + let (_, bitcoind, mut electrsd) = setup_nodes(); + let _ = bitcoind.client.get_network_info().unwrap(); // without using bitcoind, it is dropped and all the rest fails. + let _ = electrsd.client.ping().unwrap(); + assert!(electrsd.client.ping().is_ok()); + electrsd.kill().unwrap(); + assert!(electrsd.client.ping().is_err()); + } + + pub(crate) fn setup_nodes() -> (String, corepc_node::Node, ElectrsD) { + let (bitcoind_exe, electrs_exe) = init(); + debug!("bitcoind: {}", &bitcoind_exe); + debug!("electrs: {}", &electrs_exe); + let mut conf = corepc_node::Conf::default(); + conf.view_stdout = log_enabled!(Level::Debug); + if !cfg!(feature = "electrs_0_8_10") && !cfg!(feature = "esplora_a33e97e1") { + conf.p2p = P2P::Yes; + } + let bitcoind = corepc_node::Node::with_conf(&bitcoind_exe, &conf).unwrap(); + let electrs_conf = crate::Conf { + view_stderr: log_enabled!(Level::Debug), + ..Default::default() + }; + let electrsd = ElectrsD::with_conf(&electrs_exe, &bitcoind, &electrs_conf).unwrap(); + (electrs_exe, bitcoind, electrsd) + } + + fn init() -> (String, String) { + let _ = env_logger::try_init(); + let bitcoind_exe_path = corepc_node::exe_path().unwrap(); + let electrs_exe_path = exe_path().unwrap(); + (bitcoind_exe_path, electrs_exe_path) + } +} diff --git a/electrsd/src/versions.rs b/electrsd/src/versions.rs new file mode 100644 index 00000000..999c39d6 --- /dev/null +++ b/electrsd/src/versions.rs @@ -0,0 +1,44 @@ +#[cfg(target_os = "macos")] +const OS: &str = "macos"; + +#[cfg(target_os = "linux")] +const OS: &str = "linux"; + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +const OS: &str = "undefined"; + +#[cfg(feature = "electrs_0_8_10")] +const VERSION: &str = "v0.8.10"; + +#[cfg(feature = "esplora_a33e97e1")] +const VERSION: &str = "esplora_a33e97e1a1fc63fa9c20a116bb92579bbf43b254"; + +#[cfg(feature = "electrs_0_9_1")] +const VERSION: &str = "v0.9.1"; + +#[cfg(feature = "electrs_0_9_11")] +const VERSION: &str = "v0.9.11"; + +#[cfg(feature = "electrs_0_10_6")] +const VERSION: &str = "v0.10.6"; + +#[cfg(not(any( + feature = "electrs_0_8_10", + feature = "electrs_0_9_1", + feature = "electrs_0_9_11", + feature = "electrs_0_10_6", + feature = "esplora_a33e97e1", +)))] +const VERSION: &str = "NA"; + +pub const HAS_FEATURE: bool = cfg!(any( + feature = "electrs_0_8_10", + feature = "electrs_0_9_1", + feature = "electrs_0_9_11", + feature = "electrs_0_10_6", + feature = "esplora_a33e97e1", +)); + +pub fn electrs_name() -> String { + format!("electrs_{}_{}", OS, VERSION) +}