├── .editorconfig ├── .gitignore ├── .rpm └── hal-bitcoin.spec ├── .rustfmt.toml ├── .travis.yml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── contrib └── create-vendored-tar.sh └── src ├── address.rs ├── bech32.rs ├── bin └── hal │ ├── args.rs │ ├── cmd │ ├── address.rs │ ├── bech32.rs │ ├── bip32.rs │ ├── bip39.rs │ ├── block.rs │ ├── hash.rs │ ├── key.rs │ ├── ln.rs │ ├── merkle.rs │ ├── message.rs │ ├── miniscript.rs │ ├── mod.rs │ ├── psbt.rs │ ├── random.rs │ ├── script.rs │ └── tx.rs │ ├── main.rs │ ├── process_builder.rs │ └── util.rs ├── bip32.rs ├── bip39.rs ├── block.rs ├── key.rs ├── lib.rs ├── lightning.rs ├── message.rs ├── miniscript.rs ├── psbt.rs ├── serde_utils.rs └── tx.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # see https://editorconfig.org for more options, and setup instructions for yours editor 2 | 3 | [*.rs] 4 | indent_style = tab 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /.rpm/hal-bitcoin.spec: -------------------------------------------------------------------------------- 1 | %define __spec_install_post %{nil} 2 | %define __os_install_post %{_dbpath}/brp-compress 3 | %define debug_package %{nil} 4 | 5 | Name: hal-bitcoin 6 | Summary: hal - the Bitcoin companion 7 | Version: @@VERSION@@ 8 | Release: @@RELEASE@@%{?dist} 9 | License: CC0 10 | Group: Applications/System 11 | Source0: %{name}-%{version}.tar.gz 12 | URL: https://github.com/stevenroose/hal/ 13 | 14 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root 15 | 16 | %description 17 | %{summary} 18 | 19 | %prep 20 | %setup -q 21 | 22 | %install 23 | rm -rf %{buildroot} 24 | mkdir -p %{buildroot} 25 | cp -a * %{buildroot} 26 | 27 | %clean 28 | rm -rf %{buildroot} 29 | 30 | %files 31 | %defattr(-,root,root,-) 32 | %{_bindir}/* 33 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | use_small_heuristics = "Off" 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | - 1.32.0 7 | 8 | script: 9 | # The standard linker fails to link the fuzzing infrastructure but 10 | # gold seems to handle it. 11 | # See https://github.com/rust-lang/rust/issues/53945 12 | - export RUSTFLAGS='-Clink-arg=-fuse-ld=gold' 13 | - cargo build --verbose 14 | - cargo test --verbose 15 | 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 5 | # v0.10.0 -- 2025-03-21 6 | - update dependencies: 7 | - bitcoin to 0.32.5 8 | - secp256k1 to 0.29 9 | - bip32 to 2.1.0 10 | - lightning-invoice to 0.32.0 11 | - miniscript to 12.3.0 12 | - add support for p2tr addresses 13 | - support creating address from raw scriptPubkey 14 | - allow parsing block headers without txs 15 | 16 | # v0.9.5 -- 2023-11-16 17 | 18 | - Use BIP-341-suggested NUMS key for p2tr tapscript address creation 19 | - Deprecate field `hd_keypaths` in favor of `bip32_derivations`. 20 | - Use SIGHASH_ALL by default if no sighash set in rawsign command 21 | - Make all key commands print to stdout instead of stderr 22 | - Rename --nums-internal-key to --internal-key. 23 | 24 | # v0.9.4 -- 2023-08-29 25 | 26 | - Add `random bytes` command 27 | - Add `hash` subcommands with some hash utils 28 | - Add `merkle` subcommands to work with merkle proofs 29 | - Add `cli` feature so that library builders can opt-out of cli dependencies 30 | 31 | # v0.9.3 -- 2023-07-16 32 | 33 | - Rename `key sign` command to `key ecdsa-sign`, but `key sign` still works. 34 | - Rename `key verify` command to `key ecdsa-verify`, but `key verify` still works. 35 | - Add `key schnorr-sign` command 36 | - Add `key schnorr-verify` command 37 | - Make network selection options global 38 | - Make yaml output selection global 39 | 40 | # v0.9.2 -- 2023-07-14 41 | 42 | - Support signet 43 | 44 | # v0.9.1 -- 2023-06-13 45 | 46 | - Add `p2tr` to address output 47 | - Add `xonly_pubkey` to public key output 48 | - Add taproot related arguments to `address create` command 49 | - Add `key derive` command 50 | - Add `key pubkey-tweak-add` command 51 | - Add `key pubkey combine` command 52 | 53 | # v0.9.0 -- 2023/03/23 54 | 55 | - Enable 2018 edition 56 | - Set MSRV at 1.41.1 57 | - Bump bitcoin dependency to v0.29.2 58 | - Bump secp256k1 dependency to v0.24.3 59 | - Bump miniscript dependency to v9.0.1 60 | 61 | # v0.8.2 -- 2022/06/27 62 | 63 | - Add a `descriptor` field to `DescriptorInfo` 64 | - Bump miniscript dep to v6.1.0 because v6.0.1. is yanked 65 | 66 | # v0.8.1 -- 2022/03/06 67 | 68 | - Support passing various arguments through stdin 69 | 70 | # v0.8.0 -- 2021/12/07 71 | 72 | - Update bitcoin dependency to v0.27.0 73 | - Update miniscript dependency to v6.0.1 74 | - Add `TxInfo::total_output_value` 75 | - Reinstate compatibility with Rust 1.32 76 | - Add `psbt rawsign` command 77 | - Fix bug in `miniscript inspect` 78 | 79 | # v0.7.2 -- 2020/12/04 80 | 81 | - Add hex private key support for `hal key inspect`. 82 | 83 | # v0.7.1 -- 2020/10/10 84 | 85 | - Support parsing DER signatures in `key verify`. 86 | - Add `key sign` for signing with raw secp keys 87 | - When verifying, if a signature is valid for the reversed message, 88 | suggest to use the `--reverse` option. 89 | 90 | # v0.7.0 -- 2020/05/17 91 | 92 | - Add miniscript commands 93 | - Add xpub and xpriv fields to BIP-32 derivation outputs 94 | - Add --reverse field to message signature verification 95 | - Change a bunch of types in the hal library types 96 | 97 | # v0.6.1 -- 2020/04/16 98 | 99 | - Support `tx/block create` reading JSON from stdin. 100 | - Warn earlier when conflicting addresses are used in `tx create`. 101 | - Improve description on `tx/block create` commands. 102 | 103 | # v0.6.0 -- 2020/03/24 104 | 105 | - Don't print newlines after output. 106 | - Update `quote` dependency. 107 | - Use `base64-compat` crate instead of `base64`. 108 | 109 | # v0.5.4 -- 2020/02/25 110 | 111 | - Fix compressedness bug in `hal message verify`. 112 | 113 | # v0.5.3 -- 2020/02/19 114 | 115 | - Add `hal message hash` command. 116 | 117 | # v0.5.2 -- 2020/01/24 118 | 119 | - Small fix in `hal message recover` and compressedness. 120 | 121 | # v0.5.1 -- 2020/01/24 122 | 123 | - Add `hal message recover`. 124 | - Fix `hal message sign`. 125 | 126 | # v0.5.0 -- 2020/01/10 127 | 128 | - Renamed `address-*` fields in bip32 info to single `addresses` object. 129 | - Remove `compressed_public_key` field from key info. 130 | - Add signature and pubkey info to lightnig invoice. 131 | - Add support for Bitcoin Signed Message 132 | - Update `bitcoin` dependency to v0.23.0. 133 | 134 | # v0.4.4 -- 2019/10/01 135 | 136 | - add `hal key verify` command for signature verification 137 | 138 | # v0.4.3 -- 2019/09/23 139 | 140 | - make compatible with Rust v1.32.0 141 | 142 | # v0.4.2 -- 2019/09/23 143 | 144 | - add bip39 support 145 | 146 | # v0.3.0 -- 2019/07/26 147 | 148 | - add bech32 command tree 149 | - add key inspect command 150 | - add bip32 inspect command 151 | - print a newline after output 152 | 153 | # v0.2.0 154 | 155 | - Update rust-bitcoin dependency v0.18.0 156 | 157 | # v0.1.2 158 | 159 | - Added utility methods to `HexBytes` 160 | 161 | # v0.1.1 162 | 163 | - Added `block decode` and `block create` commands. 164 | - Added better description for `tx create`. 165 | 166 | # v0.1.0 167 | 168 | First version. Commands provided: 169 | - address 170 | - inspect 171 | - create 172 | - bip32 173 | - derive 174 | - ln 175 | - invoice 176 | - decode 177 | - psbt 178 | - create 179 | - decode 180 | - edit 181 | - finalize 182 | - merge 183 | - script 184 | - decode 185 | - tx 186 | - create 187 | - decode 188 | 189 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.11.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "arrayvec" 16 | version = "0.7.6" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.0.1" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 36 | 37 | [[package]] 38 | name = "base58ck" 39 | version = "0.1.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" 42 | dependencies = [ 43 | "bitcoin-internals 0.3.0", 44 | "bitcoin_hashes 0.14.0", 45 | ] 46 | 47 | [[package]] 48 | name = "base64-compat" 49 | version = "1.0.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" 52 | dependencies = [ 53 | "byteorder", 54 | ] 55 | 56 | [[package]] 57 | name = "bech32" 58 | version = "0.9.1" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" 61 | 62 | [[package]] 63 | name = "bech32" 64 | version = "0.11.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" 67 | 68 | [[package]] 69 | name = "bip39" 70 | version = "2.1.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "33415e24172c1b7d6066f6d999545375ab8e1d95421d6784bdfff9496f292387" 73 | dependencies = [ 74 | "bitcoin_hashes 0.13.0", 75 | "serde", 76 | "unicode-normalization", 77 | ] 78 | 79 | [[package]] 80 | name = "bitcoin" 81 | version = "0.32.5" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" 84 | dependencies = [ 85 | "base58ck", 86 | "bech32 0.11.0", 87 | "bitcoin-internals 0.3.0", 88 | "bitcoin-io", 89 | "bitcoin-units", 90 | "bitcoin_hashes 0.14.0", 91 | "hex-conservative 0.2.1", 92 | "hex_lit", 93 | "secp256k1", 94 | "serde", 95 | ] 96 | 97 | [[package]] 98 | name = "bitcoin-internals" 99 | version = "0.2.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" 102 | 103 | [[package]] 104 | name = "bitcoin-internals" 105 | version = "0.3.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" 108 | dependencies = [ 109 | "serde", 110 | ] 111 | 112 | [[package]] 113 | name = "bitcoin-io" 114 | version = "0.1.3" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" 117 | 118 | [[package]] 119 | name = "bitcoin-units" 120 | version = "0.1.2" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" 123 | dependencies = [ 124 | "bitcoin-internals 0.3.0", 125 | "serde", 126 | ] 127 | 128 | [[package]] 129 | name = "bitcoin_hashes" 130 | version = "0.13.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" 133 | dependencies = [ 134 | "bitcoin-internals 0.2.0", 135 | "hex-conservative 0.1.2", 136 | ] 137 | 138 | [[package]] 139 | name = "bitcoin_hashes" 140 | version = "0.14.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" 143 | dependencies = [ 144 | "bitcoin-io", 145 | "hex-conservative 0.2.1", 146 | "serde", 147 | ] 148 | 149 | [[package]] 150 | name = "bitflags" 151 | version = "1.2.1" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 154 | 155 | [[package]] 156 | name = "byteorder" 157 | version = "1.3.4" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 160 | 161 | [[package]] 162 | name = "cc" 163 | version = "1.0.41" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "8dae9c4b8fedcae85592ba623c4fd08cfdab3e3b72d6df780c6ead964a69bfff" 166 | 167 | [[package]] 168 | name = "cfg-if" 169 | version = "1.0.0" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 172 | 173 | [[package]] 174 | name = "chrono" 175 | version = "0.4.19" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 178 | dependencies = [ 179 | "libc", 180 | "num-integer", 181 | "num-traits", 182 | "serde", 183 | "time", 184 | "winapi", 185 | ] 186 | 187 | [[package]] 188 | name = "clap" 189 | version = "2.33.3" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 192 | dependencies = [ 193 | "ansi_term", 194 | "atty", 195 | "bitflags", 196 | "strsim", 197 | "textwrap", 198 | "unicode-width", 199 | "vec_map", 200 | ] 201 | 202 | [[package]] 203 | name = "dtoa" 204 | version = "0.4.8" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" 207 | 208 | [[package]] 209 | name = "fern" 210 | version = "0.5.9" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "e69ab0d5aca163e388c3a49d284fed6c3d0810700e77c5ae2756a50ec1a4daaa" 213 | dependencies = [ 214 | "chrono", 215 | "log", 216 | ] 217 | 218 | [[package]] 219 | name = "fuchsia-cprng" 220 | version = "0.1.1" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 223 | 224 | [[package]] 225 | name = "getrandom" 226 | version = "0.2.5" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" 229 | dependencies = [ 230 | "cfg-if", 231 | "libc", 232 | "wasi", 233 | ] 234 | 235 | [[package]] 236 | name = "hal" 237 | version = "0.10.0" 238 | dependencies = [ 239 | "base64-compat", 240 | "bip39", 241 | "bitcoin", 242 | "byteorder", 243 | "chrono", 244 | "clap", 245 | "fern", 246 | "hex", 247 | "jobserver", 248 | "lazy_static", 249 | "lightning-invoice", 250 | "log", 251 | "miniscript", 252 | "secp256k1", 253 | "serde", 254 | "serde_json", 255 | "serde_yaml", 256 | "shell-escape", 257 | ] 258 | 259 | [[package]] 260 | name = "hermit-abi" 261 | version = "0.1.18" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 264 | dependencies = [ 265 | "libc", 266 | ] 267 | 268 | [[package]] 269 | name = "hex" 270 | version = "0.3.2" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" 273 | 274 | [[package]] 275 | name = "hex-conservative" 276 | version = "0.1.2" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" 279 | 280 | [[package]] 281 | name = "hex-conservative" 282 | version = "0.2.1" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" 285 | dependencies = [ 286 | "arrayvec", 287 | ] 288 | 289 | [[package]] 290 | name = "hex_lit" 291 | version = "0.1.1" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" 294 | 295 | [[package]] 296 | name = "itoa" 297 | version = "0.4.7" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 300 | 301 | [[package]] 302 | name = "jobserver" 303 | version = "0.1.11" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "60af5f849e1981434e4a31d3d782c4774ae9b434ce55b101a96ecfd09147e8be" 306 | dependencies = [ 307 | "libc", 308 | "log", 309 | "rand 0.4.6", 310 | ] 311 | 312 | [[package]] 313 | name = "lazy_static" 314 | version = "1.4.0" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 317 | 318 | [[package]] 319 | name = "libc" 320 | version = "0.2.92" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" 323 | 324 | [[package]] 325 | name = "lightning-invoice" 326 | version = "0.32.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "90ab9f6ea77e20e3129235e62a2e6bd64ed932363df104e864ee65ccffb54a8f" 329 | dependencies = [ 330 | "bech32 0.9.1", 331 | "bitcoin", 332 | "lightning-types", 333 | ] 334 | 335 | [[package]] 336 | name = "lightning-types" 337 | version = "0.1.0" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "1083b8d9137000edf3bfcb1ff011c0d25e0cdd2feb98cc21d6765e64a494148f" 340 | dependencies = [ 341 | "bech32 0.9.1", 342 | "bitcoin", 343 | "hex-conservative 0.2.1", 344 | ] 345 | 346 | [[package]] 347 | name = "linked-hash-map" 348 | version = "0.5.4" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" 351 | 352 | [[package]] 353 | name = "log" 354 | version = "0.4.14" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 357 | dependencies = [ 358 | "cfg-if", 359 | ] 360 | 361 | [[package]] 362 | name = "miniscript" 363 | version = "12.3.0" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" 366 | dependencies = [ 367 | "bech32 0.11.0", 368 | "bitcoin", 369 | ] 370 | 371 | [[package]] 372 | name = "num-integer" 373 | version = "0.1.44" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 376 | dependencies = [ 377 | "autocfg", 378 | "num-traits", 379 | ] 380 | 381 | [[package]] 382 | name = "num-traits" 383 | version = "0.2.14" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 386 | dependencies = [ 387 | "autocfg", 388 | ] 389 | 390 | [[package]] 391 | name = "ppv-lite86" 392 | version = "0.2.17" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 395 | 396 | [[package]] 397 | name = "proc-macro2" 398 | version = "1.0.26" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" 401 | dependencies = [ 402 | "unicode-xid", 403 | ] 404 | 405 | [[package]] 406 | name = "quote" 407 | version = "1.0.9" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 410 | dependencies = [ 411 | "proc-macro2", 412 | ] 413 | 414 | [[package]] 415 | name = "rand" 416 | version = "0.4.6" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 419 | dependencies = [ 420 | "fuchsia-cprng", 421 | "libc", 422 | "rand_core 0.3.1", 423 | "rdrand", 424 | "winapi", 425 | ] 426 | 427 | [[package]] 428 | name = "rand" 429 | version = "0.8.5" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 432 | dependencies = [ 433 | "libc", 434 | "rand_chacha", 435 | "rand_core 0.6.4", 436 | ] 437 | 438 | [[package]] 439 | name = "rand_chacha" 440 | version = "0.3.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 443 | dependencies = [ 444 | "ppv-lite86", 445 | "rand_core 0.6.4", 446 | ] 447 | 448 | [[package]] 449 | name = "rand_core" 450 | version = "0.3.1" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 453 | dependencies = [ 454 | "rand_core 0.4.2", 455 | ] 456 | 457 | [[package]] 458 | name = "rand_core" 459 | version = "0.4.2" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 462 | 463 | [[package]] 464 | name = "rand_core" 465 | version = "0.6.4" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 468 | dependencies = [ 469 | "getrandom", 470 | ] 471 | 472 | [[package]] 473 | name = "rdrand" 474 | version = "0.4.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 477 | dependencies = [ 478 | "rand_core 0.3.1", 479 | ] 480 | 481 | [[package]] 482 | name = "ryu" 483 | version = "1.0.5" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 486 | 487 | [[package]] 488 | name = "secp256k1" 489 | version = "0.29.1" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" 492 | dependencies = [ 493 | "bitcoin_hashes 0.14.0", 494 | "rand 0.8.5", 495 | "secp256k1-sys", 496 | "serde", 497 | ] 498 | 499 | [[package]] 500 | name = "secp256k1-sys" 501 | version = "0.10.1" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" 504 | dependencies = [ 505 | "cc", 506 | ] 507 | 508 | [[package]] 509 | name = "serde" 510 | version = "1.0.125" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" 513 | dependencies = [ 514 | "serde_derive", 515 | ] 516 | 517 | [[package]] 518 | name = "serde_derive" 519 | version = "1.0.125" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" 522 | dependencies = [ 523 | "proc-macro2", 524 | "quote", 525 | "syn", 526 | ] 527 | 528 | [[package]] 529 | name = "serde_json" 530 | version = "1.0.64" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" 533 | dependencies = [ 534 | "itoa", 535 | "ryu", 536 | "serde", 537 | ] 538 | 539 | [[package]] 540 | name = "serde_yaml" 541 | version = "0.8.17" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" 544 | dependencies = [ 545 | "dtoa", 546 | "linked-hash-map", 547 | "serde", 548 | "yaml-rust", 549 | ] 550 | 551 | [[package]] 552 | name = "shell-escape" 553 | version = "0.1.5" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" 556 | 557 | [[package]] 558 | name = "strsim" 559 | version = "0.8.0" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 562 | 563 | [[package]] 564 | name = "syn" 565 | version = "1.0.68" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87" 568 | dependencies = [ 569 | "proc-macro2", 570 | "quote", 571 | "unicode-xid", 572 | ] 573 | 574 | [[package]] 575 | name = "textwrap" 576 | version = "0.11.0" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 579 | dependencies = [ 580 | "unicode-width", 581 | ] 582 | 583 | [[package]] 584 | name = "time" 585 | version = "0.1.44" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 588 | dependencies = [ 589 | "libc", 590 | "wasi", 591 | "winapi", 592 | ] 593 | 594 | [[package]] 595 | name = "tinyvec" 596 | version = "1.8.0" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 599 | dependencies = [ 600 | "tinyvec_macros", 601 | ] 602 | 603 | [[package]] 604 | name = "tinyvec_macros" 605 | version = "0.1.1" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 608 | 609 | [[package]] 610 | name = "unicode-normalization" 611 | version = "0.1.22" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 614 | dependencies = [ 615 | "tinyvec", 616 | ] 617 | 618 | [[package]] 619 | name = "unicode-width" 620 | version = "0.1.8" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 623 | 624 | [[package]] 625 | name = "unicode-xid" 626 | version = "0.2.1" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 629 | 630 | [[package]] 631 | name = "vec_map" 632 | version = "0.8.2" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 635 | 636 | [[package]] 637 | name = "wasi" 638 | version = "0.10.0+wasi-snapshot-preview1" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 641 | 642 | [[package]] 643 | name = "winapi" 644 | version = "0.3.9" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 647 | dependencies = [ 648 | "winapi-i686-pc-windows-gnu", 649 | "winapi-x86_64-pc-windows-gnu", 650 | ] 651 | 652 | [[package]] 653 | name = "winapi-i686-pc-windows-gnu" 654 | version = "0.4.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 657 | 658 | [[package]] 659 | name = "winapi-x86_64-pc-windows-gnu" 660 | version = "0.4.0" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 663 | 664 | [[package]] 665 | name = "yaml-rust" 666 | version = "0.4.5" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 669 | dependencies = [ 670 | "linked-hash-map", 671 | ] 672 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hal" 3 | version = "0.10.0" 4 | authors = ["Steven Roose "] 5 | license = "CC0-1.0" 6 | homepage = "https://github.com/stevenroose/hal/" 7 | repository = "https://github.com/stevenroose/hal/" 8 | description = "hal - the Bitcoin companion" 9 | keywords = [ "crypto", "bitcoin" ] 10 | readme = "README.md" 11 | edition = "2018" 12 | 13 | [lib] 14 | name = "hal" 15 | path = "src/lib.rs" 16 | 17 | [[bin]] 18 | name = "hal" 19 | path = "src/bin/hal/main.rs" 20 | required-features = ["cli"] 21 | 22 | [features] 23 | default = ["cli"] 24 | cli = ["base64-compat", "clap", "fern", "log", "jobserver", "shell-escape"] 25 | 26 | [dependencies] 27 | bitcoin = { version = "0.32.5", features = [ "std", "serde", "rand", "rand-std" ] } 28 | secp256k1 = { version = "0.29", features = [ "recovery" ] } 29 | bip39 = { version = "2.1.0", features = [ "all-languages" ] } 30 | lightning-invoice = { version = "0.32.0", features = [ "std" ] } 31 | miniscript = { version = "12.3.0", features = ["compiler"] } 32 | 33 | byteorder = "1.3.1" 34 | chrono = { version = "0.4.6", features = ["serde"] } 35 | lazy_static = "1.4" 36 | hex = "0.3.2" 37 | 38 | serde = { version = "1.0.84", features = [ "derive" ] } 39 | serde_json = "1.0.34" 40 | serde_yaml = "0.8.8" 41 | 42 | # for the CLI 43 | base64-compat = { version = "1.0.0", optional = true } 44 | clap = { version = "=2.33.3", optional = true } 45 | fern = { version = "0.5.6", optional = true } 46 | log = { version = "0.4.5", optional = true } 47 | 48 | # For external commands 49 | jobserver = { version = "0.1.11", optional = true } 50 | shell-escape = { version = "0.1.4", optional = true } 51 | 52 | 53 | [package.metadata.rpm] 54 | package = "hal-bitcoin" 55 | 56 | [package.metadata.rpm.cargo] 57 | buildflags = ["--release", "--frozen"] 58 | 59 | [package.metadata.rpm.targets] 60 | hal = { path = "/usr/bin/hal" } 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hal -- the Bitcoin companion 2 | ============================ 3 | 4 | hal is a command line tool that provides all kinds of Bitcoin-related utilities. 5 | 6 | 7 | # Installation 8 | 9 | ``` 10 | $ cargo install --locked hal 11 | ``` 12 | 13 | 14 | # Summary of commands: 15 | 16 | - address 17 | - inspect: get information about addresses 18 | - create: create addresses using public keys or scripts 19 | 20 | - bech32 21 | - decode: parse the elements of the Bech32 format 22 | - encode: encode data in the Bech32 format 23 | 24 | - bip32 25 | - derive: derive keys and addresses from extended keys 26 | - inspect: inspect a BIP-32 xpub or xpriv 27 | 28 | - bip39 29 | - generate: generate a new BIP-39 mnemonic 30 | - get-seed: get the seed value and BIP-32 master key for a given BIP-39 mnemonic 31 | 32 | - block 33 | - create: create a binary block from JSON 34 | - decode: decode a binary block to JSON 35 | 36 | - hash 37 | - sha256: hash data with SHA-256 38 | - sha256d: hash data with double SHA-256 39 | 40 | - key 41 | - generate: generate a random keypair 42 | - derive: generate a public key from a private key 43 | - inspect: inspect private keys 44 | - ecdsa-sign: make ECDSA signatures 45 | - ecdsa-verify: verify ECDSA signatures 46 | - pubkey-tweak-add: add a scalar to a point 47 | - pubkey-combine: add two points together 48 | 49 | - ln 50 | - invoice 51 | - decode: decode Lightning invoices 52 | 53 | - merkle 54 | - proof-create: create a merkle proof 55 | - proof-check: check a merkle proof 56 | 57 | - message 58 | - hash: get hashes of Bitcoin Signed Message 59 | - sign: sign a message using Bitcoin Signed Message 60 | - verify: verify a Bitcoin Signed Message 61 | - recover: recover the pubkey or address that signed a message 62 | 63 | - miniscript 64 | - descriptor: get information about an output descriptor 65 | - instpect: inspect miniscripts 66 | - parse: parse a script into a miniscript 67 | - policy: inspect policies 68 | 69 | - psbt 70 | - create: create a PSBT from a raw unsigned transaction 71 | - decode: decode a PSBT to JSON 72 | - edit: edit a PSBT inline 73 | - finalize: finalize a PSBT into a fully signed transaction 74 | - merge: merge multiple PSBTs into one 75 | 76 | - random 77 | - bytes: generate random bytes 78 | 79 | - script 80 | - decode: decode a PSBT to JSON 81 | 82 | - tx 83 | - create: create a binary transaction from JSON 84 | - decode: decode a binary transaction to JSON 85 | 86 | 87 | ## Minimum Supported Rust Version (MSRV) 88 | 89 | `hal` should always compile on **Rust 1.41.1**. 90 | Note that it should be build using the `Cargo.lock` file, so using `--locked`. 91 | 92 | # Extensions 93 | 94 | hal allows the use of extensions that can be installed separately. 95 | 96 | ## Known extensions: 97 | 98 | - [hal-elements](https://github.com/stevenroose/hal-elements/): support for Elements sidechains like Liquid 99 | 100 | 101 | ## Ideas: 102 | - optional [Trezor](https://github.com/stevenroose/rust-trezor-api/) and Ledger integration 103 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); 3 | let output = std::process::Command::new(rustc) 4 | .arg("--version") 5 | .output() 6 | .expect("Failed to run rustc --version"); 7 | assert!(output.status.success(), "Failed to get rust version"); 8 | let stdout = String::from_utf8(output.stdout).expect("rustc produced non-UTF-8 output"); 9 | let version_prefix = "rustc "; 10 | if !stdout.starts_with(version_prefix) { 11 | panic!("unexpected rustc output: {}", stdout); 12 | } 13 | 14 | let version = &stdout[version_prefix.len()..]; 15 | let end = version.find(&[' ', '-'] as &[_]).unwrap_or(version.len()); 16 | let version = &version[..end]; 17 | let mut version_components = version.split('.'); 18 | let major = version_components.next().unwrap(); 19 | assert_eq!(major, "1", "Unexpected Rust version"); 20 | let minor = version_components 21 | .next() 22 | .unwrap_or("0") 23 | .parse::() 24 | .expect("invalid Rust minor version"); 25 | 26 | for activate_version in &[] { 27 | if minor >= *activate_version { 28 | println!("cargo:rustc-cfg=rust_v_1_{}", activate_version); 29 | } 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /contrib/create-vendored-tar.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | BASEDIR=$(dirname $(readlink -f $0)) 5 | BASEDIR=$(git -C ${BASEDIR} rev-parse --show-toplevel) 6 | cd "$BASEDIR" 7 | PROJ=$(basename ${BASEDIR}) 8 | GITDATE=$(git log --pretty=format:"%ai" -1) 9 | 10 | WORKDIR=./vendored-tar 11 | TAG=$(git describe --tags) 12 | echo "On tag ${TAG}" 13 | # Remove the v prefix from semver versions. 14 | if [[ "${TAG}" =~ ^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(|-.*)$ ]]; then 15 | TAG=${TAG:1} 16 | fi 17 | TARFILE=${PWD}/${PROJ}-${TAG}-vendored.tar.gz 18 | 19 | 20 | echo Creating tarball ${TARFILE} 21 | 22 | rm -rf ${WORKDIR} 23 | mkdir ${WORKDIR} 24 | 25 | # Copy all relevant files 26 | cp -r ./src/ ./Cargo.toml ./Cargo.lock ./LICENSE ./README.md ./contrib ${WORKDIR} 27 | pushd ${WORKDIR} 28 | 29 | cargo vendor --locked ./vendor 30 | 31 | mkdir ./.cargo 32 | cat < ./.cargo/config 33 | [source.crates-io] 34 | replace-with = "vendored-sources" 35 | 36 | [source.vendored-sources] 37 | directory = "vendor" 38 | EOF 39 | 40 | tar --sort=name --mtime="${GITDATE}" -czf ${TARFILE} . 41 | 42 | popd 43 | rm -rf ${WORKDIR} 44 | 45 | # Sign tarball with 46 | # $ gpg --detach-sign --armor 47 | -------------------------------------------------------------------------------- /src/address.rs: -------------------------------------------------------------------------------- 1 | 2 | use bitcoin::{ 3 | address, Address, Network, Script, PubkeyHash, ScriptHash, WPubkeyHash, WScriptHash, 4 | }; 5 | use secp256k1::XOnlyPublicKey; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::SECP; 9 | use crate::tx; 10 | 11 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 12 | pub struct AddressInfo { 13 | #[serde(rename = "type")] 14 | pub type_: Option, 15 | pub script_pub_key: tx::OutputScriptInfo, 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | pub witness_program_version: Option, 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub pubkey_hash: Option, 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | pub script_hash: Option, 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | pub witness_pubkey_hash: Option, 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | pub witness_script_hash: Option, 26 | } 27 | 28 | #[derive(Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize)] 29 | pub struct Addresses { 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | pub p2pkh: Option>, 32 | #[serde(skip_serializing_if = "Option::is_none")] 33 | pub p2wpkh: Option>, 34 | #[serde(skip_serializing_if = "Option::is_none")] 35 | pub p2shwpkh: Option>, 36 | #[serde(skip_serializing_if = "Option::is_none")] 37 | pub p2sh: Option>, 38 | #[serde(skip_serializing_if = "Option::is_none")] 39 | pub p2wsh: Option>, 40 | #[serde(skip_serializing_if = "Option::is_none")] 41 | pub p2shwsh: Option>, 42 | #[serde(skip_serializing_if = "Option::is_none")] 43 | pub p2tr: Option>, 44 | } 45 | 46 | impl Addresses { 47 | pub fn from_pubkey(pubkey: &bitcoin::PublicKey, network: Network) -> Addresses { 48 | Addresses { 49 | p2pkh: Some(Address::p2pkh(pubkey, network).as_unchecked().clone()), 50 | p2wpkh: if pubkey.compressed { 51 | let pk = bitcoin::CompressedPublicKey(pubkey.inner); 52 | Some(Address::p2wpkh(&pk, network).as_unchecked().clone()) 53 | } else { 54 | None 55 | }, 56 | p2shwpkh: if pubkey.compressed { 57 | let pk = bitcoin::CompressedPublicKey(pubkey.inner); 58 | Some(Address::p2shwpkh(&pk, network).as_unchecked().clone()) 59 | } else { 60 | None 61 | }, 62 | p2tr: if pubkey.compressed { 63 | let pk = pubkey.inner.into(); 64 | Some(Address::p2tr(&SECP, pk, None, network).as_unchecked().clone()) 65 | } else { 66 | None 67 | }, 68 | ..Default::default() 69 | } 70 | } 71 | 72 | pub fn from_xonly_pubkey(pubkey: XOnlyPublicKey, network: Network) -> Addresses { 73 | Addresses { 74 | p2tr: Some(Address::p2tr(&SECP, pubkey, None, network).as_unchecked().clone()), 75 | ..Default::default() 76 | } 77 | } 78 | 79 | pub fn from_script(script: &Script, network: Network) -> Addresses { 80 | Addresses { 81 | p2sh: Address::p2sh(&script, network).ok().map(|a| a.as_unchecked().clone()), 82 | p2wsh: Some(Address::p2wsh(&script, network).as_unchecked().clone()), 83 | p2shwsh: Some(Address::p2shwsh(&script, network).as_unchecked().clone()), 84 | // NB to make a p2tr here we need a NUMS internal key and it's 85 | // probably not safe to pick one ourselves. AFAIK there is no 86 | // standard NUMS point for this purpose. 87 | // (Though BIP341 suggests one..) 88 | ..Default::default() 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/bech32.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::HexBytes; 4 | 5 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 6 | pub struct Bech32Info { 7 | /// Bech32 encoded string 8 | pub bech32: String, 9 | /// Human-readable part 10 | pub hrp: String, 11 | /// Hex-encoded data payload 12 | pub payload: HexBytes, 13 | } 14 | -------------------------------------------------------------------------------- /src/bin/hal/args.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::borrow::Borrow; 3 | use std::str::FromStr; 4 | 5 | use bitcoin::consensus::encode; 6 | use bitcoin::{Network, NetworkKind}; 7 | use secp256k1::{self, XOnlyPublicKey}; 8 | 9 | use crate::exit; 10 | 11 | /// Construct a new boolean command flag. 12 | pub fn flag<'a>(name: &'a str, help: &'a str) -> clap::Arg<'a, 'a> { 13 | clap::Arg::with_name(name).long(name).help(help).takes_value(false).required(false) 14 | } 15 | 16 | /// Construct a new command option. 17 | pub fn opt<'a>(name: &'a str, help: &'a str) -> clap::Arg<'a, 'a> { 18 | clap::Arg::with_name(name).long(name).help(help).takes_value(true).required(false) 19 | } 20 | 21 | /// Construct a new positional argument. 22 | pub fn arg<'a>(name: &'a str, help: &'a str) -> clap::Arg<'a, 'a> { 23 | clap::Arg::with_name(name).help(help).takes_value(true) 24 | } 25 | 26 | /// Global options for network selection. 27 | pub fn opts_networks() -> Vec> { 28 | vec![ 29 | flag("mainnet", "run in mainnet mode") 30 | .short("m") 31 | .required(false) 32 | .global(true), 33 | flag("testnet", "run in testnet mode") 34 | .short("t") 35 | .required(false) 36 | .global(true), 37 | flag("signet", "run in signet mode") 38 | .required(false) 39 | .global(true), 40 | flag("regtest", "run in regtest mode") 41 | .required(false) 42 | .global(true), 43 | ] 44 | } 45 | 46 | /// Global option for changing output format to YAML. 47 | pub fn opt_yaml() -> clap::Arg<'static, 'static> { 48 | flag("yaml", "print output in YAML instead of JSON") 49 | .short("y") 50 | .required(false) 51 | .global(true) 52 | } 53 | 54 | /// A flexible pubkey return type that accepts both xonly and regular pubkeys. 55 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 56 | pub enum FlexiblePubkey { 57 | Regular(bitcoin::PublicKey), 58 | XOnly(XOnlyPublicKey), 59 | } 60 | 61 | pub trait ArgMatchesExt<'a>: Borrow> { 62 | fn verbose(&self) -> bool { 63 | self.borrow().is_present("verbose") 64 | } 65 | 66 | fn explicit_network(&self) -> Option { 67 | if self.borrow().is_present("mainnet") { 68 | Some(Network::Bitcoin) 69 | } else if self.borrow().is_present("testnet") { 70 | Some(Network::Testnet) 71 | } else if self.borrow().is_present("signet") { 72 | Some(Network::Signet) 73 | } else if self.borrow().is_present("regtest") { 74 | Some(Network::Regtest) 75 | } else { 76 | None 77 | } 78 | } 79 | 80 | fn network(&self) -> Network { 81 | self.explicit_network().unwrap_or(Network::Bitcoin) 82 | } 83 | 84 | fn network_from_kind(&self, kind: NetworkKind) -> Network { 85 | match kind { 86 | NetworkKind::Main => Network::Bitcoin, 87 | NetworkKind::Test => self.explicit_network().unwrap_or(Network::Testnet), 88 | } 89 | } 90 | 91 | fn privkey(&self, key: &str) -> Option { 92 | self.borrow().value_of(key).map(|s| { 93 | bitcoin::PrivateKey::from_str(&s).unwrap_or_else(|_| { 94 | let key = secp256k1::SecretKey::from_str(&s).unwrap_or_else(|_| { 95 | exit!("invalid WIF/hex private key provided for argument '{}'", key); 96 | }); 97 | bitcoin::PrivateKey { 98 | compressed: true, 99 | network: self.network().into(), 100 | inner: key, 101 | } 102 | }) 103 | }) 104 | } 105 | 106 | fn need_privkey(&self, key: &str) -> bitcoin::PrivateKey { 107 | self.privkey(key).unwrap_or_else(|| { 108 | exit!("expected a private key for argument '{}'", key); 109 | }) 110 | } 111 | 112 | fn pubkey(&self, key: &str) -> Option { 113 | self.borrow().value_of(key).map(|s| { 114 | bitcoin::PublicKey::from_str(&s).unwrap_or_else(|_| { 115 | exit!("invalid public key provided for argument '{}'", key); 116 | }) 117 | }) 118 | } 119 | 120 | fn need_pubkey(&self, key: &str) -> bitcoin::PublicKey { 121 | self.pubkey(key).unwrap_or_else(|| { 122 | exit!("expected a public key for argument '{}'", key); 123 | }) 124 | } 125 | 126 | fn xonly_pubkey(&self, key: &str) -> Option { 127 | self.borrow().value_of(key).map(|s| { 128 | XOnlyPublicKey::from_str(&s).or_else(|_| { 129 | bitcoin::PublicKey::from_str(&s).map(|pk| pk.inner.x_only_public_key().0) 130 | }).unwrap_or_else(|_| { 131 | exit!("invalid public key provided for argument '{}'", key); 132 | }) 133 | }) 134 | } 135 | 136 | fn need_xonly_pubkey(&self, key: &str) -> XOnlyPublicKey { 137 | self.xonly_pubkey(key).unwrap_or_else(|| { 138 | exit!("expected a public key for argument '{}'", key); 139 | }) 140 | } 141 | 142 | fn flexible_pubkey(&self, key: &str) -> Option { 143 | self.borrow().value_of(key).map(|s| { 144 | if let Ok(xonly) = XOnlyPublicKey::from_str(&s) { 145 | FlexiblePubkey::XOnly(xonly) 146 | } else if let Ok(reg) = bitcoin::PublicKey::from_str(&s) { 147 | FlexiblePubkey::Regular(reg) 148 | } else { 149 | exit!("invalid public key provided for argument '{}'", key); 150 | } 151 | }) 152 | } 153 | 154 | fn hex_consensus(&self, key: &str) -> Option> { 155 | self.borrow().value_of(key).map(|s| -> Result { 156 | let hex = hex::decode(s).map_err(|e| format!("invalid hex: {}", e))?; 157 | let ret = encode::deserialize(&hex).map_err(|e| format!("invalid format: {}", e))?; 158 | Ok(ret) 159 | }) 160 | } 161 | 162 | fn out_yaml(&self) -> bool { 163 | self.borrow().is_present("yaml") 164 | } 165 | 166 | fn print_output(&self, out: &T) { 167 | if self.out_yaml() { 168 | serde_yaml::to_writer(::std::io::stdout(), &out).unwrap(); 169 | } else { 170 | serde_json::to_writer_pretty(::std::io::stdout(), &out).unwrap(); 171 | } 172 | } 173 | } 174 | 175 | impl<'a> ArgMatchesExt<'a> for clap::ArgMatches<'a> {} 176 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/address.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::str::FromStr; 3 | 4 | use bitcoin::address::AddressData; 5 | use bitcoin::hashes::Hash; 6 | use bitcoin::hashes::hex::FromHex; 7 | use bitcoin::{Address, ScriptBuf, WPubkeyHash, WScriptHash}; 8 | use clap; 9 | 10 | use hal; 11 | 12 | use crate::prelude::*; 13 | 14 | lazy_static! { 15 | /// The H point as used in BIP-341 which is constructed by taking the hash 16 | /// of the standard uncompressed encoding of the secp256k1 base point G as 17 | /// X coordinate. 18 | /// 19 | /// See: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs 20 | static ref NUMS_H: secp256k1::PublicKey = secp256k1::PublicKey::from_str( 21 | "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" 22 | ).unwrap(); 23 | } 24 | 25 | /// Create a NUMS point from the given entropy. 26 | fn nums(entropy: secp256k1::Scalar) -> secp256k1::PublicKey { 27 | NUMS_H.add_exp_tweak(&SECP, &entropy).need("invalid NUMS entropy") 28 | } 29 | 30 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 31 | cmd::subcommand_group("address", "work with addresses") 32 | .subcommand(cmd_create()) 33 | .subcommand(cmd_inspect()) 34 | } 35 | 36 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 37 | match args.subcommand() { 38 | ("create", Some(ref m)) => exec_create(&m), 39 | ("inspect", Some(ref m)) => exec_inspect(&m), 40 | (_, _) => unreachable!("clap prints help"), 41 | }; 42 | } 43 | 44 | fn cmd_create<'a>() -> clap::App<'a, 'a> { 45 | cmd::subcommand("create", "create addresses") 46 | .arg(args::opt("scriptpubkey", "a scriptPubkey in hex")) 47 | .arg(args::opt("pubkey", "a public key in hex")) 48 | .arg(args::opt("script", "a script in hex")) 49 | .arg(args::opt( 50 | "internal-key", 51 | "internal pubkey to use with --script for p2tr", 52 | )) 53 | .arg(args::opt( 54 | "nums-internal-key", 55 | "internal pubkey to use with --script for p2tr", 56 | ).hidden(true)) 57 | .arg(args::flag( 58 | "nums-internal-key-h", 59 | "Use the H NUMS key from BIP-341 for p2tr address when using --script.\n\ 60 | This point will be used by default if no NUMS point is specified.", 61 | )) 62 | .arg(args::opt( 63 | "nums-internal-key-entropy", 64 | "entropy to use to create NUMS internal pubkey to use with --script for p2tr\n\ 65 | the zero scalar is used when left empty, this means the BIP-341 NUMS point H is used", 66 | )) 67 | } 68 | 69 | fn exec_create<'a>(args: &clap::ArgMatches<'a>) { 70 | let network = args.network(); 71 | 72 | if let Some(spk) = args.value_of("scriptpubkey") { 73 | let script_bytes = hex::decode(spk).need("invalid scriptpubkey hex"); 74 | let script = ScriptBuf::from(script_bytes); 75 | let addr = Address::from_script(&script, network).need("invalid scriptPubkey"); 76 | println!("{}", addr) 77 | } else if let Some(pubkey) = args.flexible_pubkey("pubkey") { 78 | let addr = match pubkey { 79 | FlexiblePubkey::Regular(pk) => hal::address::Addresses::from_pubkey(&pk, network), 80 | FlexiblePubkey::XOnly(pk) => hal::address::Addresses::from_xonly_pubkey(pk, network), 81 | }; 82 | args.print_output(&addr) 83 | } else if let Some(script_hex) = args.value_of("script") { 84 | let script_bytes = hex::decode(script_hex).need("invalid script hex"); 85 | let script = ScriptBuf::from(script_bytes); 86 | 87 | let p2tr = { 88 | // If the user provided NUMS information we can add a p2tr address. 89 | // If not, we assume H NUMS from BIP-341. 90 | if util::more_than_one(&[ 91 | args.is_present("internal-key"), 92 | args.is_present("nums-internal-key-h"), 93 | args.is_present("nums-internal-key-entropy"), 94 | // deprecated 95 | args.is_present("nums-internal-key"), 96 | ]) { 97 | println!("Use only either nums-h, nums-internal-key or \ 98 | nums-internal-key-entropy.\n"); 99 | cmd_create().print_help().unwrap(); 100 | std::process::exit(1); 101 | } 102 | let internal = if args.is_present("nums-internal-key-h") { 103 | *NUMS_H 104 | } else if let Some(int) = args.value_of("nums-internal-key") { 105 | eprintln!("--nums-internal-key is deprecated in favor of --internal-key"); 106 | int.parse().need("invalid nums internal key") 107 | } else if let Some(int) = args.value_of("internal-key") { 108 | int.parse().need("invalid nums internal key") 109 | } else if let Some(ent) = args.value_of("nums-internal-key-entropy") { 110 | let scalar = <[u8; 32]>::from_hex(ent) 111 | .need("invalid entropy format: must be 32-byte hex"); 112 | nums(secp256k1::Scalar::from_be_bytes(scalar).need("invalid NUMS entropy")) 113 | } else { 114 | eprintln!("No NUMS key info provided, will use H NUMS from BIP-341 for p2tr."); 115 | *NUMS_H 116 | }; 117 | Address::from_script(&script.to_p2tr(&SECP, internal.into()), network).unwrap() 118 | }; 119 | 120 | let mut ret = hal::address::Addresses::from_script(&script, network); 121 | assert!(ret.p2tr.replace(p2tr.as_unchecked().clone()).is_none(), "Addresses::from_script shouldn't set p2tr"); 122 | 123 | args.print_output(&ret) 124 | } else { 125 | cmd_create().print_help().unwrap(); 126 | std::process::exit(1); 127 | } 128 | } 129 | 130 | fn cmd_inspect<'a>() -> clap::App<'a, 'a> { 131 | cmd::subcommand("inspect", "inspect addresses") 132 | .arg(args::arg("address", "the address").required(true)) 133 | } 134 | 135 | fn exec_inspect<'a>(args: &clap::ArgMatches<'a>) { 136 | let address_str = args.value_of("address").need("no address provided"); 137 | let address = Address::from_str(address_str) 138 | .need("invalid address format") 139 | .assume_checked(); 140 | let script_pk = address.script_pubkey(); 141 | 142 | let mut info = hal::address::AddressInfo { 143 | script_pub_key: hal::tx::OutputScriptInfo { 144 | hex: Some(script_pk.to_bytes().into()), 145 | asm: Some(script_pk.to_asm_string()), 146 | address: None, 147 | type_: None, 148 | }, 149 | type_: None, 150 | pubkey_hash: None, 151 | script_hash: None, 152 | witness_pubkey_hash: None, 153 | witness_script_hash: None, 154 | witness_program_version: None, 155 | }; 156 | 157 | match address.to_address_data() { 158 | AddressData::P2pkh { pubkey_hash } => { 159 | info.type_ = Some("p2pkh".to_owned()); 160 | info.pubkey_hash = Some(pubkey_hash); 161 | }, 162 | AddressData::P2sh { script_hash } => { 163 | info.type_ = Some("p2sh".to_owned()); 164 | info.script_hash = Some(script_hash); 165 | }, 166 | AddressData::Segwit { witness_program } => { 167 | let version = witness_program.version().to_num() as usize; 168 | info.witness_program_version = Some(version); 169 | let program = witness_program.program(); 170 | 171 | if version == 0 { 172 | if program.len() == 20 { 173 | info.type_ = Some("p2wpkh".to_owned()); 174 | info.witness_pubkey_hash = 175 | Some(WPubkeyHash::from_slice(program.as_bytes()).need("size 20")); 176 | } else if program.len() == 32 { 177 | info.type_ = Some("p2wsh".to_owned()); 178 | info.witness_script_hash = 179 | Some(WScriptHash::from_slice(program.as_bytes()).need("size 32")); 180 | } else { 181 | info.type_ = Some("invalid-witness-program".to_owned()); 182 | } 183 | } else { 184 | info.type_ = Some("unknown-witness-program-version".to_owned()); 185 | } 186 | }, 187 | _ => exit!("unknown address type"), 188 | } 189 | 190 | args.print_output(&info) 191 | } 192 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/bech32.rs: -------------------------------------------------------------------------------- 1 | 2 | use bitcoin::bech32; 3 | use clap; 4 | use hex; 5 | 6 | use hal; 7 | use crate::prelude::*; 8 | 9 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 10 | cmd::subcommand_group("bech32", "encode and decode the bech32 format") 11 | .subcommand(cmd_encode()) 12 | .subcommand(cmd_decode()) 13 | } 14 | 15 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 16 | match args.subcommand() { 17 | ("encode", Some(ref m)) => exec_encode(&m), 18 | ("decode", Some(ref m)) => exec_decode(&m), 19 | (_, _) => unreachable!("clap prints help"), 20 | }; 21 | } 22 | 23 | fn cmd_encode<'a>() -> clap::App<'a, 'a> { 24 | cmd::subcommand("encode", "encode bech32 format") 25 | .arg(args::arg("hrp", "human-readable part").required(true)) 26 | .arg(args::arg( 27 | "payload-hex", 28 | "hex-encoded payload bytes, 8-bit values\nunless --no-convert is specified", 29 | )) 30 | .arg(args::opt("legacy", "encode using legacy bech32, not bech32m")) 31 | } 32 | 33 | fn exec_encode<'a>(args: &clap::ArgMatches<'a>) { 34 | let hrp = args.value_of("hrp").need("missing required argument"); 35 | let hrp = bech32::Hrp::parse(hrp).need("invalid HRP"); 36 | let hex = util::arg_or_stdin(args, "payload-hex"); 37 | 38 | let payload = hex::decode(hex.as_ref()).need("invalid hex"); 39 | let bech32 = if args.is_present("legacy") { 40 | bech32::encode::(hrp, &payload).need("encode failure") 41 | } else { 42 | bech32::encode::(hrp, &payload).need("encode failure") 43 | }; 44 | let info = hal::bech32::Bech32Info { 45 | bech32, 46 | hrp: hrp.to_string(), 47 | payload: payload.into(), 48 | }; 49 | 50 | args.print_output(&info) 51 | } 52 | 53 | fn cmd_decode<'a>() -> clap::App<'a, 'a> { 54 | cmd::subcommand("decode", "decode bech32 format") 55 | .arg(args::arg("bech32", "a bech32 string")) 56 | } 57 | 58 | fn exec_decode<'a>(args: &clap::ArgMatches<'a>) { 59 | let s = util::arg_or_stdin(args, "string"); 60 | 61 | let (hrp, payload) = bech32::decode(&s).need("invalid bech32"); 62 | 63 | let info = hal::bech32::Bech32Info { 64 | bech32: s.to_string(), 65 | hrp: hrp.to_string(), 66 | payload: payload.into(), 67 | }; 68 | 69 | args.print_output(&info) 70 | } 71 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/bip32.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::str::FromStr; 3 | 4 | use bitcoin::bip32; 5 | use clap; 6 | 7 | use crate::prelude::*; 8 | 9 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 10 | cmd::subcommand_group("bip32", "BIP-32 key derivation") 11 | .subcommand(cmd_derive()) 12 | .subcommand(cmd_inspect()) 13 | } 14 | 15 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 16 | match args.subcommand() { 17 | ("derive", Some(ref m)) => exec_derive(&m), 18 | ("inspect", Some(ref m)) => exec_inspect(&m), 19 | (_, _) => unreachable!("clap prints help"), 20 | }; 21 | } 22 | 23 | fn cmd_derive<'a>() -> clap::App<'a, 'a> { 24 | cmd::subcommand("derive", "derive keys from an extended key") 25 | .arg(args::arg("ext-key", "extended public or private key").required(true)) 26 | .arg(args::arg("derivation-path", "the derivation path").required(true)) 27 | } 28 | 29 | fn exec_derive<'a>(args: &clap::ArgMatches<'a>) { 30 | let path_str = args.value_of("derivation-path").unwrap(); 31 | let path: bip32::DerivationPath = path_str.parse().need("error parsing derivation path"); 32 | let key_str = args.value_of("ext-key").unwrap(); 33 | 34 | let master_fingerprint; 35 | let mut derived_xpriv = None; 36 | let derived_xpub = match bip32::Xpriv::from_str(&key_str) { 37 | Ok(ext_priv) => { 38 | derived_xpriv = Some(ext_priv.derive_priv(&SECP, &path).need("derivation error")); 39 | master_fingerprint = ext_priv.fingerprint(&SECP); 40 | bip32::Xpub::from_priv(&SECP, derived_xpriv.as_ref().unwrap()) 41 | } 42 | Err(_) => { 43 | let ext_pub = bip32::Xpub::from_str(key_str).need("invalid extended key"); 44 | master_fingerprint = ext_pub.fingerprint(); 45 | ext_pub.derive_pub(&SECP, &path).need("derivation error") 46 | } 47 | }; 48 | 49 | let info = hal::bip32::DerivationInfo { 50 | network: derived_xpub.network, 51 | master_fingerprint: Some(master_fingerprint), 52 | path: Some(path), 53 | xpriv: derived_xpriv, 54 | xpub: derived_xpub, 55 | chain_code: derived_xpub.chain_code, 56 | identifier: derived_xpub.identifier(), 57 | fingerprint: derived_xpub.fingerprint(), 58 | public_key: derived_xpub.public_key, 59 | private_key: derived_xpriv.map(|x| x.private_key), 60 | addresses: hal::address::Addresses::from_pubkey( 61 | &bitcoin::PublicKey::new(derived_xpub.public_key), 62 | args.network_from_kind(derived_xpub.network), 63 | ), 64 | }; 65 | 66 | args.print_output(&info) 67 | } 68 | 69 | fn cmd_inspect<'a>() -> clap::App<'a, 'a> { 70 | cmd::subcommand("inspect", "inspect a BIP-32 xpub or xpriv") 71 | .arg(args::arg("ext-key", "extended public or private key").required(true)) 72 | } 73 | 74 | fn exec_inspect<'a>(args: &clap::ArgMatches<'a>) { 75 | let key_str = args.value_of("ext-key").unwrap(); 76 | 77 | let mut xpriv = None; 78 | 79 | let xpub = match bip32::Xpriv::from_str(&key_str) { 80 | Ok(ext_priv) => { 81 | xpriv = Some(ext_priv); 82 | bip32::Xpub::from_priv(&SECP, xpriv.as_ref().unwrap()) 83 | } 84 | Err(_) => key_str.parse().need("invalid extended key"), 85 | }; 86 | 87 | let info = hal::bip32::DerivationInfo { 88 | network: xpub.network, 89 | master_fingerprint: None, 90 | path: None, 91 | xpriv: xpriv, 92 | xpub: xpub, 93 | chain_code: xpub.chain_code, 94 | identifier: xpub.identifier(), 95 | fingerprint: xpub.fingerprint(), 96 | public_key: xpub.public_key, 97 | private_key: xpriv.map(|x| x.private_key), 98 | addresses: hal::address::Addresses::from_pubkey( 99 | &bitcoin::PublicKey::new(xpub.public_key), 100 | args.network_from_kind(xpub.network), 101 | ), 102 | }; 103 | 104 | args.print_output(&info) 105 | } 106 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/bip39.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use bip39::Mnemonic; 4 | use bitcoin::hashes::{sha256, Hash}; 5 | use bitcoin::secp256k1::rand::{self, RngCore}; 6 | use clap; 7 | use hex; 8 | 9 | use hal; 10 | use crate::prelude::*; 11 | 12 | /// List of languages we support. 13 | const LANGUAGES: &[&'static str] = &[ 14 | "english", 15 | "czech", 16 | "french", 17 | "italian", 18 | "japanese", 19 | "korean", 20 | "spanish", 21 | "simplified-chinese", 22 | "traditional-chinese", 23 | ]; 24 | 25 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 26 | cmd::subcommand_group("bip39", "BIP-39 mnemonics") 27 | .subcommand(cmd_generate()) 28 | .subcommand(cmd_get_seed()) 29 | } 30 | 31 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 32 | match args.subcommand() { 33 | ("generate", Some(ref m)) => exec_generate(&m), 34 | ("get-seed", Some(ref m)) => exec_get_seed(&m), 35 | (_, _) => unreachable!("clap prints help"), 36 | }; 37 | } 38 | 39 | fn cmd_generate<'a>() -> clap::App<'a, 'a> { 40 | lazy_static! { 41 | static ref LANGUAGE_HELP: String = format!( 42 | "the language to use for the mnemonic. \ 43 | Supported languages are: {}", LANGUAGES.join(", "), 44 | ); 45 | } 46 | 47 | cmd::subcommand("generate", "generate a new BIP-39 mnemonic") 48 | .unset_setting(clap::AppSettings::ArgRequiredElseHelp) 49 | .arg(args::arg("words", "the number of words") 50 | .long("words").short("w") 51 | .default_value("24")) 52 | .arg(args::arg("language", "the language to use") 53 | .long("language").short("l") 54 | .default_value("english") 55 | .help(&LANGUAGE_HELP)) 56 | .arg(args::arg("entropy", "hex-encoded entropy data").long("entropy")) 57 | .arg(args::flag("stdin", "read entropy from stdin")) 58 | } 59 | 60 | fn exec_generate<'a>(args: &clap::ArgMatches<'a>) { 61 | let network = args.network(); 62 | 63 | let language = { 64 | let s = args.value_of("language").unwrap_or("en"); 65 | hal::bip39::parse_language(s).need("invalid language string") 66 | }; 67 | 68 | let word_count = args.value_of("words").unwrap_or("24").parse::() 69 | .need("invalid number of words"); 70 | if word_count < 12 || word_count % 6 != 0 || word_count > 24 { 71 | exit!("invalid word count: {}", word_count); 72 | } 73 | let nb_entropy_bytes = (word_count / 3) * 4; 74 | 75 | let mut entropy; 76 | match (args.is_present("entropy"), args.is_present("stdin")) { 77 | (true, true) => exit!("can't provide --entropy and --stdin"), 78 | (true, false) => { 79 | let entropy_hex = args.value_of("entropy").unwrap(); 80 | if entropy_hex.len() != nb_entropy_bytes * 2 { 81 | exit!( 82 | "invalid entropy length for {} word mnemonic, need {} bytes", 83 | word_count, nb_entropy_bytes 84 | ); 85 | } 86 | entropy = hex::decode(&entropy_hex).need("invalid entropy hex"); 87 | } 88 | (false, true) => { 89 | let mut hasher = sha256::Hash::engine(); 90 | let stdin = io::stdin(); 91 | let read = io::copy(&mut stdin.lock(), &mut hasher).need("error reading stdin"); 92 | if read < nb_entropy_bytes as u64 { 93 | warn!("Low entropy provided! Do not use this mnemonic in production!"); 94 | } 95 | entropy = sha256::Hash::from_engine(hasher)[0..nb_entropy_bytes].to_vec(); 96 | } 97 | (false, false) => { 98 | entropy = vec![0; nb_entropy_bytes]; 99 | rand::thread_rng().fill_bytes(&mut entropy); 100 | } 101 | } 102 | 103 | assert!(entropy.len() == nb_entropy_bytes); 104 | let mnemonic = Mnemonic::from_entropy_in(language, &entropy).unwrap(); 105 | args.print_output(&hal::GetInfo::get_info(&mnemonic, network)) 106 | } 107 | 108 | fn cmd_get_seed<'a>() -> clap::App<'a, 'a> { 109 | cmd::subcommand( 110 | "get-seed", 111 | "get the seed value and BIP-32 master key for a given BIP-39 mnemonic", 112 | ) 113 | .arg(args::arg("mnemonic", "the mnemonic phrase").required(true)) 114 | .arg(args::arg("passphrase", "the BIP-39 passphrase").long("passphrase")) 115 | } 116 | 117 | fn exec_get_seed<'a>(args: &clap::ArgMatches<'a>) { 118 | let network = args.network(); 119 | 120 | let mnemonic = args.value_of("mnemonic").need("no mnemonic provided"); 121 | let mnemonic = Mnemonic::parse(mnemonic) 122 | .need("invalid mnemonic phrase"); 123 | 124 | let info = ::hal::bip39::MnemonicInfo::from_mnemonic_with_passphrase( 125 | &mnemonic, 126 | args.value_of("passphrase").unwrap_or(""), 127 | network, 128 | ); 129 | args.print_output(&info) 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | use super::*; 135 | 136 | #[test] 137 | fn test_languages() { 138 | let mut unique = std::collections::HashSet::new(); 139 | for l in LANGUAGES { 140 | let lang = hal::bip39::parse_language(l).unwrap(); 141 | assert!(unique.insert(lang)); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/block.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use bitcoin::consensus::encode::{deserialize, serialize}; 4 | use bitcoin::{block, Block}; 5 | 6 | use hal::block::{BlockHeaderInfo, BlockInfo}; 7 | use crate::prelude::*; 8 | use crate::cmd::tx::create_transaction; 9 | 10 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 11 | cmd::subcommand_group("block", "manipulate blocks") 12 | .subcommand(cmd_create()) 13 | .subcommand(cmd_decode()) 14 | } 15 | 16 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 17 | match args.subcommand() { 18 | ("create", Some(ref m)) => exec_create(&m), 19 | ("decode", Some(ref m)) => exec_decode(&m), 20 | (_, _) => unreachable!("clap prints help"), 21 | }; 22 | } 23 | 24 | fn cmd_create<'a>() -> clap::App<'a, 'a> { 25 | cmd::subcommand("create", "create a raw block from JSON") 26 | .args(&[ 27 | args::arg("block-info", "the block info in JSON").required(false), 28 | args::flag("raw-stdout", "output the raw bytes of the result to stdout") 29 | .short("r"), 30 | ]) 31 | .long_about(r#" 32 | Create a block from JSON. Use the same format as the `hal block decode` output. 33 | 34 | It's possible to pass the JSON string as the first argument or pass it via stdin. 35 | 36 | One can chose to pass in transaction info objects like used for `hal tx create` into the 37 | "transactions" field or hexadecimal raw transactions in the "raw_transactions" field. 38 | 39 | Example format: 40 | { 41 | "header": { 42 | "block_hash": "0000000000000000000b52e5f79972ccb1b42f14bd5097381551ed7bb4f78478", 43 | "version": 1, 44 | "previous_block_hash": "00000000000000000011604b6d32a6074720f60aae04edf20396bbaf65e50edc", 45 | "merkle_root": "b7f360ae3bb1f6ca1935269d8955808c6606ff20af9b98fbc3ddb7de6c9df5c3", 46 | "time": 12345, 47 | "bits": 12345, 48 | "nonce": 12345 49 | }, 50 | 51 | !! only either of these fields should be provided !! 52 | "transactions": [ 53 | { ... } 54 | ], 55 | "raw_transaction": [ 56 | "deadbeef" 57 | ] 58 | }"# 59 | ) 60 | } 61 | 62 | fn create_block_header(info: BlockHeaderInfo) -> block::Header { 63 | if info.block_hash.is_some() { 64 | warn!("Field \"block_hash\" is ignored."); 65 | } 66 | 67 | block::Header { 68 | version: block::Version::from_consensus(info.version), 69 | prev_blockhash: info.previous_block_hash, 70 | merkle_root: info.merkle_root, 71 | time: info.time, 72 | bits: bitcoin::pow::CompactTarget::from_consensus(info.bits), 73 | nonce: info.nonce, 74 | } 75 | } 76 | 77 | fn exec_create<'a>(args: &clap::ArgMatches<'a>) { 78 | let info = serde_json::from_str::(&util::arg_or_stdin(args, "block-info")) 79 | .need("invaid json JSON input"); 80 | 81 | if info.txids.is_some() { 82 | warn!("Field \"txids\" is ignored."); 83 | } 84 | 85 | let mut used_network = cmd::tx::UsedNetwork::new(args.explicit_network()); 86 | let block = Block { 87 | header: create_block_header(info.header), 88 | txdata: match (info.transactions, info.raw_transactions) { 89 | (Some(_), Some(_)) => exit!("Can't provide transactions both in JSON and raw."), 90 | (None, None) => exit!("No transactions provided."), 91 | (Some(infos), None) => infos.into_iter().map(|t| { 92 | create_transaction(t, &mut used_network) 93 | }).collect(), 94 | (None, Some(raws)) => raws 95 | .into_iter() 96 | .map(|r| deserialize(&r.0).need("invalid raw transaction")) 97 | .collect(), 98 | }, 99 | }; 100 | 101 | let block_bytes = serialize(&block); 102 | if args.is_present("raw-stdout") { 103 | ::std::io::stdout().write_all(&block_bytes).unwrap(); 104 | } else { 105 | print!("{}", hex::encode(&block_bytes)); 106 | } 107 | } 108 | 109 | fn cmd_decode<'a>() -> clap::App<'a, 'a> { 110 | cmd::subcommand("decode", "decode a raw block to JSON") 111 | .arg(args::arg("raw-block", "the raw block in hex").required(false)) 112 | .arg(args::flag("txids", "provide transactions IDs instead of full transactions")) 113 | } 114 | 115 | fn exec_decode<'a>(args: &clap::ArgMatches<'a>) { 116 | let hex_tx = util::arg_or_stdin(args, "raw-block"); 117 | let raw_tx = hex::decode(hex_tx.as_ref()).need("could not decode raw block hex"); 118 | 119 | if args.is_present("txids") { 120 | let block: Block = deserialize(&raw_tx).need("invalid block format"); 121 | let info = hal::block::BlockInfo { 122 | header: hal::GetInfo::get_info(&block.header, args.network()), 123 | bip34_block_height: block.bip34_block_height().ok(), 124 | txids: Some(block.txdata.iter().map(|t| t.compute_txid()).collect()), 125 | transactions: None, 126 | raw_transactions: None, 127 | }; 128 | args.print_output(&info) 129 | } else { 130 | let block: Block = match deserialize(&raw_tx) { 131 | Ok(block) => block, 132 | Err(_) => { 133 | let header = deserialize::(&raw_tx).expect("invalid block format"); 134 | let block = Block { 135 | header: header, 136 | txdata: Default::default(), 137 | }; 138 | block 139 | }, 140 | }; 141 | let info = hal::GetInfo::get_info(&block, args.network()); 142 | args.print_output(&info) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/hash.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use bitcoin::hashes::hex::FromHex; 4 | use bitcoin::hashes::{sha256, sha256d, Hash}; 5 | 6 | use crate::prelude::*; 7 | 8 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 9 | cmd::subcommand_group("hash", "commands to hash data") 10 | .subcommand(cmd_sha256()) 11 | .subcommand(cmd_sha256d()) 12 | } 13 | 14 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 15 | match args.subcommand() { 16 | ("sha256", Some(ref m)) => exec_sha256(&m), 17 | ("sha256d", Some(ref m)) => exec_sha256d(&m), 18 | (_, _) => unreachable!("clap prints help"), 19 | }; 20 | } 21 | 22 | fn cmd_sha256<'a>() -> clap::App<'a, 'a> { 23 | cmd::subcommand("sha256", "hash input with SHA-256") 24 | .arg(args::arg("hex", "the input bytes in hex to hash").required(true)) 25 | .arg(args::flag("raw-stdout", "output the raw bytes of the result to stdout") 26 | .short("r")) 27 | } 28 | 29 | fn exec_sha256<'a>(args: &clap::ArgMatches<'a>) { 30 | let hex = args.value_of("hex").need("no input bytes given"); 31 | let bytes = Vec::::from_hex(&hex).need("invalid hex"); 32 | 33 | let ret = sha256::Hash::hash(&bytes); 34 | if args.is_present("raw-stdout") { 35 | ::std::io::stdout().write_all(&ret[..]).unwrap(); 36 | } else { 37 | print!("{}", ret); 38 | } 39 | } 40 | 41 | fn cmd_sha256d<'a>() -> clap::App<'a, 'a> { 42 | cmd::subcommand("sha256d", "hash input with double SHA-256") 43 | .arg(args::arg("hex", "the input bytes in hex to hash").required(true)) 44 | .arg(args::flag("raw-stdout", "output the raw bytes of the result to stdout") 45 | .short("r")) 46 | } 47 | 48 | fn exec_sha256d<'a>(args: &clap::ArgMatches<'a>) { 49 | let hex = args.value_of("hex").need("no input bytes given"); 50 | let bytes = Vec::::from_hex(&hex).need("invalid hex"); 51 | 52 | let ret = sha256d::Hash::hash(&bytes); 53 | if args.is_present("raw-stdout") { 54 | ::std::io::stdout().write_all(&ret[..]).unwrap(); 55 | } else { 56 | print!("{}", ret); 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/key.rs: -------------------------------------------------------------------------------- 1 | use std::process; 2 | 3 | use bitcoin::secp256k1; 4 | use bitcoin::secp256k1::rand; 5 | use bitcoin::hashes::hex::FromHex; 6 | use clap; 7 | 8 | use hal::{self, GetInfo}; 9 | 10 | use crate::prelude::*; 11 | 12 | 13 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 14 | cmd::subcommand_group("key", "work with private and public keys") 15 | .subcommand(cmd_generate()) 16 | .subcommand(cmd_derive()) 17 | .subcommand(cmd_inspect()) 18 | .subcommand(cmd_ecdsa_sign()) 19 | .subcommand(cmd_ecdsa_verify()) 20 | .subcommand(cmd_schnorr_sign()) 21 | .subcommand(cmd_schnorr_verify()) 22 | .subcommand(cmd_negate_pubkey()) 23 | .subcommand(cmd_pubkey_tweak_add()) 24 | .subcommand(cmd_pubkey_combine()) 25 | } 26 | 27 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 28 | match args.subcommand() { 29 | ("generate", Some(ref m)) => exec_generate(&m), 30 | ("derive", Some(ref m)) => exec_derive(&m), 31 | ("inspect", Some(ref m)) => exec_inspect(&m), 32 | ("ecdsa-sign", Some(ref m)) => exec_ecdsa_sign(&m), 33 | ("ecdsa-verify", Some(ref m)) => exec_ecdsa_verify(&m), 34 | ("schnorr-sign", Some(ref m)) => exec_schnorr_sign(&m), 35 | ("schnorr-verify", Some(ref m)) => exec_schnorr_verify(&m), 36 | ("sign", Some(ref m)) => exec_ecdsa_sign(&m), // deprecate 37 | ("verify", Some(ref m)) => exec_ecdsa_verify(&m), // deprecate 38 | ("negate-pubkey", Some(ref m)) => exec_negate_pubkey(&m), 39 | ("pubkey-tweak-add", Some(ref m)) => exec_pubkey_tweak_add(&m), 40 | ("pubkey-combine", Some(ref m)) => exec_pubkey_combine(&m), 41 | (_, _) => unreachable!("clap prints help"), 42 | }; 43 | } 44 | 45 | fn cmd_generate<'a>() -> clap::App<'a, 'a> { 46 | cmd::subcommand("generate", "generate a new ECDSA keypair") 47 | .unset_setting(clap::AppSettings::ArgRequiredElseHelp) 48 | } 49 | 50 | fn exec_generate<'a>(args: &clap::ArgMatches<'a>) { 51 | let network = args.network(); 52 | 53 | let entropy: [u8; 32] = rand::random(); 54 | let secret_key = secp256k1::SecretKey::from_slice(&entropy[..]).unwrap(); 55 | let privkey = bitcoin::PrivateKey { 56 | compressed: true, 57 | network: network.into(), 58 | inner: secret_key, 59 | }; 60 | 61 | let info = privkey.get_info(network); 62 | args.print_output(&info) 63 | } 64 | 65 | fn cmd_derive<'a>() -> clap::App<'a, 'a> { 66 | cmd::subcommand("derive", "generate a public key from a private key") 67 | .arg(args::arg("privkey", "the secret key").required(true)) 68 | } 69 | 70 | fn exec_derive<'a>(args: &clap::ArgMatches<'a>) { 71 | let network = args.network(); 72 | let privkey = args.need_privkey("privkey"); 73 | let info = privkey.get_info(network); 74 | args.print_output(&info) 75 | } 76 | 77 | fn cmd_inspect<'a>() -> clap::App<'a, 'a> { 78 | cmd::subcommand("inspect", "inspect private keys") 79 | .arg(args::arg("key", "the key").required(true)) 80 | } 81 | 82 | fn exec_inspect<'a>(args: &clap::ArgMatches<'a>) { 83 | let key = args.need_privkey("key"); 84 | let info = key.get_info(args.network()); 85 | args.print_output(&info) 86 | } 87 | 88 | fn cmd_ecdsa_sign<'a>() -> clap::App<'a, 'a> { 89 | cmd::subcommand( 90 | "ecdsa-sign", 91 | "sign messages using ECDSA\n\nNOTE!! For SHA-256-d hashes, the --reverse \ 92 | flag must be used because Bitcoin Core reverses the hex order for those!", 93 | ) 94 | .arg(args::arg("privkey", "the private key in hex or WIF").required(true)) 95 | .arg(args::arg("message", "the message to be signed in hex (must be 32 bytes)").required(true)) 96 | .arg(args::flag("reverse", "reverse the message")) 97 | } 98 | 99 | fn exec_ecdsa_sign<'a>(args: &clap::ArgMatches<'a>) { 100 | let network = args.network(); 101 | 102 | let msg_hex = args.value_of("message").need("no message given"); 103 | let mut msg_bytes = hex::decode(&msg_hex).need("invalid hex message"); 104 | if args.is_present("reverse") { 105 | msg_bytes.reverse(); 106 | } 107 | let msg = secp256k1::Message::from_digest_slice(&msg_bytes[..]) 108 | .need("invalid message to be signed"); 109 | let privkey = args.need_privkey("privkey"); 110 | let signature = SECP.sign_ecdsa(&msg, &privkey.inner); 111 | args.print_output(&signature.get_info(network)) 112 | } 113 | 114 | fn cmd_ecdsa_verify<'a>() -> clap::App<'a, 'a> { 115 | cmd::subcommand( 116 | "ecdsa-verify", 117 | "verify ECDSA signatures\n\nNOTE!! For SHA-256-d hashes, the --reverse \ 118 | flag must be used because Bitcoin Core reverses the hex order for those!", 119 | ) 120 | .arg(args::arg("message", "the message to be signed in hex (must be 32 bytes)").required(true)) 121 | .arg(args::arg("pubkey", "the public key in hex").required(true)) 122 | .arg(args::arg("signature", "the ECDSA signature in hex").required(true)) 123 | .arg(args::flag("reverse", "reverse the message")) 124 | .arg(args::flag("no-try-reverse", "don't try to verify for reversed message")) 125 | } 126 | 127 | fn exec_ecdsa_verify<'a>(args: &clap::ArgMatches<'a>) { 128 | let msg_hex = args.value_of("message").need("no message given"); 129 | let mut msg_bytes = hex::decode(&msg_hex).need("invalid hex message"); 130 | if args.is_present("reverse") { 131 | msg_bytes.reverse(); 132 | } 133 | let msg = secp256k1::Message::from_digest_slice(&msg_bytes[..]) 134 | .need("invalid message to be signed"); 135 | let pubkey = args.need_pubkey("pubkey"); 136 | let sig = { 137 | let hex = args.value_of("signature").need("no signature provided"); 138 | let bytes = hex::decode(&hex).need("invalid signature: not hex"); 139 | if bytes.len() == 64 { 140 | secp256k1::ecdsa::Signature::from_compact(&bytes).need("invalid signature") 141 | } else { 142 | secp256k1::ecdsa::Signature::from_der(&bytes).need("invalid DER signature") 143 | } 144 | }; 145 | 146 | let valid = SECP.verify_ecdsa(&msg, &sig, &pubkey.inner).is_ok(); 147 | 148 | // Perhaps the user should have passed --reverse. 149 | if !valid && !args.is_present("no-try-reverse") { 150 | msg_bytes.reverse(); 151 | let msg = secp256k1::Message::from_digest_slice(&msg_bytes[..]) 152 | .need("invalid message to be signed"); 153 | if SECP.verify_ecdsa(&msg, &sig, &pubkey.inner).is_ok() { 154 | eprintln!("Signature is valid for the reverse message."); 155 | if args.is_present("reverse") { 156 | eprintln!("Try dropping the --reverse"); 157 | } else { 158 | eprintln!("If the message is a Bitcoin SHA256 hash, try --reverse"); 159 | } 160 | } 161 | } 162 | 163 | if valid { 164 | println!("Signature is valid."); 165 | } else { 166 | eprintln!("Signature is invalid!"); 167 | process::exit(1); 168 | } 169 | } 170 | 171 | fn cmd_schnorr_sign<'a>() -> clap::App<'a, 'a> { 172 | cmd::subcommand( 173 | "schnorr-sign", 174 | "sign messages using Schnorr\n\nNOTE!! For SHA-256-d hashes, the --reverse \ 175 | flag must be used because Bitcoin Core reverses the hex order for those!", 176 | ) 177 | .arg(args::arg("privkey", "the private key in hex or WIF").required(true)) 178 | .arg(args::arg("message", "the message to be signed in hex (must be 32 bytes)").required(true)) 179 | .arg(args::flag("reverse", "reverse the message")) 180 | } 181 | 182 | fn exec_schnorr_sign<'a>(args: &clap::ArgMatches<'a>) { 183 | let msg_hex = args.value_of("message").need("no message given"); 184 | let mut msg_bytes = hex::decode(&msg_hex).need("invalid hex message"); 185 | if args.is_present("reverse") { 186 | msg_bytes.reverse(); 187 | } 188 | let msg = secp256k1::Message::from_digest_slice(&msg_bytes[..]) 189 | .need("invalid message to be signed"); 190 | let privkey = args.need_privkey("privkey"); 191 | let keypair = secp256k1::Keypair::from_secret_key(&SECP, &privkey.inner); 192 | let signature = SECP.sign_schnorr_with_rng(&msg, &keypair, &mut rand::thread_rng()); 193 | print!("{:x}", &signature); 194 | } 195 | 196 | fn cmd_schnorr_verify<'a>() -> clap::App<'a, 'a> { 197 | cmd::subcommand( 198 | "schnorr-verify", 199 | "verify Schnorr signatures\n\nNOTE!! For SHA-256-d hashes, the --reverse \ 200 | flag must be used because Bitcoin Core reverses the hex order for those!", 201 | ) 202 | .arg(args::arg("message", "the message to be signed in hex (must be 32 bytes)").required(true)) 203 | .arg(args::arg("pubkey", "the public key in hex").required(true)) 204 | .arg(args::arg("signature", "the Schnorr signature in hex").required(true)) 205 | .arg(args::flag("reverse", "reverse the message")) 206 | .arg(args::flag("no-try-reverse", "don't try to verify for reversed message")) 207 | } 208 | 209 | fn exec_schnorr_verify<'a>(args: &clap::ArgMatches<'a>) { 210 | let msg_hex = args.value_of("message").need("no message given"); 211 | let mut msg_bytes = hex::decode(&msg_hex).need("invalid hex message"); 212 | if args.is_present("reverse") { 213 | msg_bytes.reverse(); 214 | } 215 | let msg = secp256k1::Message::from_digest_slice(&msg_bytes[..]) 216 | .need("invalid message to be signed"); 217 | let pubkey = args.need_xonly_pubkey("pubkey"); 218 | let sig = { 219 | let hex = args.value_of("signature").need("no signature provided"); 220 | let bytes = hex::decode(&hex).need("invalid signature: not hex"); 221 | secp256k1::schnorr::Signature::from_slice(&bytes).need("invalid signature") 222 | }; 223 | 224 | let valid = SECP.verify_schnorr(&sig, &msg, &pubkey).is_ok(); 225 | 226 | // Perhaps the user should have passed --reverse. 227 | if !valid && !args.is_present("no-try-reverse") { 228 | msg_bytes.reverse(); 229 | let msg = secp256k1::Message::from_digest_slice(&msg_bytes[..]) 230 | .need("invalid message to be signed"); 231 | if SECP.verify_schnorr(&sig, &msg, &pubkey).is_ok() { 232 | eprintln!("Signature is valid for the reverse message."); 233 | if args.is_present("reverse") { 234 | eprintln!("Try dropping the --reverse"); 235 | } else { 236 | eprintln!("If the message is a Bitcoin SHA256 hash, try --reverse"); 237 | } 238 | } 239 | } 240 | 241 | if valid { 242 | println!("Signature is valid."); 243 | } else { 244 | eprintln!("Signature is invalid!"); 245 | process::exit(1); 246 | } 247 | } 248 | 249 | fn cmd_negate_pubkey<'a>() -> clap::App<'a, 'a> { 250 | cmd::subcommand("negate-pubkey", "negate the public key") 251 | .arg(args::arg("pubkey", "the public key").required(true)) 252 | } 253 | 254 | fn exec_negate_pubkey<'a>(args: &clap::ArgMatches<'a>) { 255 | let key = args.need_pubkey("pubkey"); 256 | let negated = key.inner.negate(&SECP); 257 | print!("{}", negated); 258 | } 259 | 260 | fn cmd_pubkey_tweak_add<'a>() -> clap::App<'a, 'a> { 261 | cmd::subcommand("pubkey-tweak-add", "add a scalar (private key) to a point (public key)") 262 | .arg(args::arg("point", "the public key in hex").required(true)) 263 | .arg(args::arg("scalar", "the private key in hex").required(true)) 264 | } 265 | 266 | fn exec_pubkey_tweak_add<'a>(args: &clap::ArgMatches<'a>) { 267 | let point = args.need_pubkey("point"); 268 | 269 | let scalar = { 270 | let hex = args.value_of("scalar").need("no scalar given"); 271 | let bytes = <[u8; 32]>::from_hex(hex).need("invalid scalar hex"); 272 | secp256k1::Scalar::from_be_bytes(bytes).need("invalid scalar") 273 | }; 274 | 275 | match point.inner.add_exp_tweak(&SECP, &scalar.into()) { 276 | Ok(..) => { 277 | print!("{}", point.to_string()); 278 | } 279 | Err(err) => { 280 | eprintln!("error: {}", err); 281 | process::exit(1); 282 | } 283 | } 284 | } 285 | 286 | fn cmd_pubkey_combine<'a>() -> clap::App<'a, 'a> { 287 | cmd::subcommand("pubkey-combine", "add a point (public key) to another; \ 288 | note that this is NOT MuSig2 compatible, use the musig command for that") 289 | .arg(args::arg("pubkey1", "the first public key in hex").required(true)) 290 | .arg(args::arg("pubkey2", "the second public key in hex").required(true)) 291 | } 292 | 293 | fn exec_pubkey_combine<'a>(args: &clap::ArgMatches<'a>) { 294 | let pk1 = args.need_pubkey("pubkey1"); 295 | let pk2 = args.need_pubkey("pubkey2"); 296 | 297 | match pk1.inner.combine(&pk2.inner) { 298 | Ok(sum) => { 299 | print!("{}", sum.to_string()); 300 | } 301 | Err(err) => { 302 | eprintln!("error: {}", err); 303 | process::exit(1); 304 | } 305 | } 306 | } 307 | 308 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/ln.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use clap; 4 | use lightning_invoice::Bolt11Invoice; 5 | 6 | use crate::prelude::*; 7 | 8 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 9 | cmd::subcommand_group("ln", "everything Lightning").subcommand( 10 | cmd::subcommand_group("invoice", "handle Lightning invoices") 11 | .subcommand(cmd_invoice_decode()), 12 | ) 13 | } 14 | 15 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 16 | match args.subcommand() { 17 | ("invoice", Some(ref args)) => match args.subcommand() { 18 | ("decode", Some(ref m)) => exec_invoice_decode(&m), 19 | (_, _) => unreachable!("clap prints help"), 20 | }, 21 | (_, _) => unreachable!("clap prints help"), 22 | }; 23 | } 24 | 25 | fn cmd_invoice_decode<'a>() -> clap::App<'a, 'a> { 26 | cmd::subcommand("decode", "decode Lightning invoices") 27 | .arg(args::arg("invoice", "the invoice in bech32").required(false)) 28 | } 29 | 30 | fn exec_invoice_decode<'a>(args: &clap::ArgMatches<'a>) { 31 | let invoice_str = util::arg_or_stdin(args, "invoice"); 32 | let invoice = Bolt11Invoice::from_str(invoice_str.as_ref()).need("invalid invoice encoding"); 33 | 34 | let info = hal::GetInfo::get_info(&invoice, args.network()); 35 | args.print_output(&info) 36 | } 37 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/merkle.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::io::Write; 3 | use std::str::FromStr; 4 | use std::process; 5 | 6 | use bitcoin::{Block, Txid}; 7 | use bitcoin::merkle_tree::PartialMerkleTree; 8 | use clap; 9 | 10 | use crate::prelude::*; 11 | 12 | 13 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 14 | cmd::subcommand_group("merkle", "tool for merkle tree and proof creation") 15 | .subcommand(cmd_proof_create()) 16 | .subcommand(cmd_proof_check()) 17 | } 18 | 19 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 20 | match args.subcommand() { 21 | ("proof-create", Some(ref m)) => exec_proof_create(&m), 22 | ("proof-check", Some(ref m)) => exec_proof_check(&m), 23 | (_, _) => unreachable!("clap prints help"), 24 | }; 25 | } 26 | 27 | fn cmd_proof_create<'a>() -> clap::App<'a, 'a> { 28 | cmd::subcommand("proof-create", "create a merkle inclusion proof") 29 | .arg(args::opt("block", "the full block in hex")) 30 | .arg(args::opt("block-txids", "all txids in the block, separated by a comma")) 31 | .arg(args::opt("txid", "the txids to proof inclusion for").required(true)) 32 | .arg(args::flag("raw-stdout", "output the raw bytes of the result to stdout") 33 | .short("r")) 34 | } 35 | 36 | fn exec_proof_create<'a>(args: &clap::ArgMatches<'a>) { 37 | let block_txids: Vec = { 38 | if let Some(block_res) = args.hex_consensus::("block") { 39 | let block = block_res.need("invalid block"); 40 | block.txdata.iter().map(|tx| tx.compute_txid()).collect() 41 | } else if let Some(txids) = args.value_of("block-txid") { 42 | txids.split(",").map(|s| Txid::from_str(s).need("invalid block txid")).collect() 43 | } else { 44 | exit!("Need to provide either --block or --block-txids"); 45 | } 46 | }; 47 | 48 | let txids = args.values_of("txid").need("at least one txid should be provided") 49 | .map(|s| Txid::from_str(s).need("invalid txid")) 50 | .collect::>(); 51 | 52 | let included = block_txids.iter().map(|txid| txids.contains(txid)).collect::>(); 53 | 54 | let proof = PartialMerkleTree::from_txids(&block_txids, &included); 55 | let proof_bytes = bitcoin::consensus::serialize(&proof); 56 | 57 | if args.is_present("raw-stdout") { 58 | ::std::io::stdout().write_all(&proof_bytes).unwrap(); 59 | } else { 60 | print!("{}", hex::encode(&proof_bytes)); 61 | } 62 | } 63 | 64 | fn cmd_proof_check<'a>() -> clap::App<'a, 'a> { 65 | cmd::subcommand("proof-check", "check for txids in the proof; \ 66 | if no txids are provided, it prints all of the included ones") 67 | .arg(args::arg("proof", "merkle proof in hex").required(true)) 68 | .arg(args::opt("txid", "the txids to proof inclusion for")) 69 | .arg(args::flag("indices", "also print the indices for the txids")) 70 | } 71 | 72 | fn exec_proof_check<'a>(args: &clap::ArgMatches<'a>) { 73 | let proof = args.hex_consensus::("proof") 74 | .need("no proof provided") 75 | .need("invalid proof format"); 76 | 77 | let mut txids = Vec::new(); 78 | let mut idxs = Vec::new(); 79 | //TODO(stevenroose) make this into a need() when we update bitcoin dep 80 | proof.extract_matches(&mut txids, &mut idxs).expect("invalid proof"); 81 | 82 | if let Some(check_txids) = args.values_of("txid") { 83 | let mut ok = true; 84 | for check_txid in check_txids.map(|s| Txid::from_str(s).need("invalid txid")) { 85 | if !txids.contains(&check_txid) { 86 | eprintln!("Txid {} is not included", check_txid); 87 | ok = false; 88 | } 89 | } 90 | 91 | if ok { 92 | eprintln!("All txids are included"); 93 | } else { 94 | process::exit(1); 95 | } 96 | } else { 97 | for (txid, idx) in txids.iter().zip(idxs.iter()) { 98 | if args.is_present("indices") { 99 | println!("{} {}", txid, idx); 100 | } else { 101 | println!("{}", txid); 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/message.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use bitcoin::hashes::Hash; 4 | use bitcoin::secp256k1; 5 | use bitcoin::{Address, AddressType, PublicKey}; 6 | use clap; 7 | 8 | use crate::prelude::*; 9 | 10 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 11 | cmd::subcommand_group("message", "Bitcoin Signed Messages") 12 | .subcommand(cmd_hash()) 13 | .subcommand(cmd_sign()) 14 | .subcommand(cmd_verify()) 15 | .subcommand(cmd_recover()) 16 | } 17 | 18 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 19 | match args.subcommand() { 20 | ("hash", Some(ref m)) => exec_hash(&m), 21 | ("sign", Some(ref m)) => exec_sign(&m), 22 | ("verify", Some(ref m)) => exec_verify(&m), 23 | ("recover", Some(ref m)) => exec_recover(&m), 24 | (_, _) => unreachable!("clap prints help"), 25 | }; 26 | } 27 | 28 | fn cmd_hash<'a>() -> clap::App<'a, 'a> { 29 | cmd::subcommand("hash", "calculate Bitcoin Signed Message hash") 30 | .arg(args::arg("message", "the message to sign (without prefix)").required(true)) 31 | } 32 | 33 | fn exec_hash<'a>(args: &clap::ArgMatches<'a>) { 34 | let msg = args.value_of("message").need("no message provided"); 35 | let res = hal::message::MessageHash { 36 | sha256: bitcoin::hashes::sha256::Hash::hash(msg.as_bytes()), 37 | sha256d: bitcoin::hashes::sha256d::Hash::hash(msg.as_bytes()), 38 | sign_hash: bitcoin::sign_message::signed_msg_hash(&msg), 39 | }; 40 | 41 | args.print_output(&res) 42 | } 43 | 44 | fn cmd_sign<'a>() -> clap::App<'a, 'a> { 45 | cmd::subcommand("sign", "create a new Bitcoin Signed Message") 46 | .arg(args::arg("key", "the private key to sign with in WIF format").required(true)) 47 | .arg(args::arg("message", "the message to sign (without prefix)").required(false)) 48 | } 49 | 50 | fn exec_sign<'a>(args: &clap::ArgMatches<'a>) { 51 | let privkey = args.need_privkey("key"); 52 | 53 | let msg = util::arg_or_stdin(args, "message"); 54 | let hash = bitcoin::sign_message::signed_msg_hash(&msg); 55 | 56 | let signature = SECP.sign_ecdsa_recoverable( 57 | &secp256k1::Message::from_digest(hash.to_byte_array()), &privkey.inner, 58 | ); 59 | 60 | let (recid, raw) = signature.serialize_compact(); 61 | let mut serialized = [0u8; 65]; 62 | serialized[0] = 27; 63 | serialized[0] += recid.to_i32() as u8; 64 | if privkey.compressed { 65 | serialized[0] += 4; 66 | } 67 | serialized[1..].copy_from_slice(&raw[..]); 68 | 69 | print!("{}", base64::encode(&serialized[..])); 70 | } 71 | 72 | fn cmd_verify<'a>() -> clap::App<'a, 'a> { 73 | cmd::subcommand("verify", "recover the pubkey and address of a Bitcoin Signed Messages") 74 | .arg(args::arg("signer", "the signer's public key or address").required(true)) 75 | .arg(args::arg("signature", "the signature in hex").required(true)) 76 | .arg(args::arg("message", "the message that was signed (without prefix)").required(false)) 77 | } 78 | 79 | fn exec_verify<'a>(args: &clap::ArgMatches<'a>) { 80 | let signer = args.value_of("signer").need("no signer provided"); 81 | let signer_addr_res = Address::from_str(&signer); 82 | let signer_pubk_res = PublicKey::from_str(&signer); 83 | if signer_addr_res.is_err() && signer_pubk_res.is_err() { 84 | if let Err(e) = signer_addr_res { 85 | error!("Error parsing signer as address: {}", e); 86 | } 87 | if let Err(e) = signer_pubk_res { 88 | error!("Error parsing signer as public key: {}", e); 89 | } 90 | exit!("Failed to parse signer."); 91 | } 92 | if signer_addr_res.is_ok() && signer_pubk_res.is_ok() { 93 | debug!("Rare/impossible case that signer can both be parsed as pubkey and address."); 94 | } 95 | 96 | let sig = args.value_of("signature").need("no signature provided"); 97 | let sig_bytes = match (hex::decode(&sig), base64::decode(&sig)) { 98 | (Ok(b), Err(_)) => b, 99 | (Err(_), Ok(b)) => b, 100 | (Ok(b), Ok(_)) => { 101 | debug!("Signature is both valid hex and base64, assuming it's hex."); 102 | b 103 | } 104 | (Err(e1), Err(e2)) => exit!("Invalid signature: \"{}\"; \"{}\"", e1, e2), 105 | }; 106 | 107 | if sig_bytes.len() != 65 { 108 | exit!("Invalid signature: length is {} instead of 65 bytes", sig_bytes.len()); 109 | } 110 | let recid = secp256k1::ecdsa::RecoveryId::from_i32(((sig_bytes[0] - 27) & 0x03) as i32) 111 | .need("invalid recoverable signature (invalid recid)"); 112 | let compressed = ((sig_bytes[0] - 27) & 0x04) != 0; 113 | let signature = secp256k1::ecdsa::RecoverableSignature::from_compact(&sig_bytes[1..], recid) 114 | .need("invalid recoverable signature"); 115 | 116 | let msg = util::arg_or_stdin(args, "message"); 117 | let hash = bitcoin::sign_message::signed_msg_hash(&msg); 118 | 119 | let pubkey = PublicKey { 120 | inner: SECP 121 | .recover_ecdsa(&secp256k1::Message::from_digest(hash.to_byte_array()), &signature) 122 | .need("invalid signature"), 123 | compressed: compressed, 124 | }; 125 | 126 | let network = args.network(); 127 | if let Ok(pk) = signer_pubk_res { 128 | if pubkey != pk { 129 | exit!("Signed for pubkey {}, expected {}", pubkey, pk); 130 | } 131 | } else if let Ok(expected) = signer_addr_res { 132 | let expected = expected.require_network(network) 133 | .need("invalid network on expected address"); 134 | let addr = match expected.address_type() { 135 | None => exit!("Unknown address type provided"), 136 | Some(AddressType::P2pkh) => Address::p2pkh(&pubkey, network), 137 | Some(AddressType::P2wpkh) => if compressed { 138 | let pk = bitcoin::CompressedPublicKey(pubkey.inner); 139 | Address::p2wpkh(&pk, network) 140 | } else { 141 | exit!("Uncompressed key in Segwit"); 142 | }, 143 | Some(AddressType::P2sh) => if compressed { 144 | let pk = bitcoin::CompressedPublicKey(pubkey.inner); 145 | Address::p2shwpkh(&pk, network) 146 | } else { 147 | exit!("Uncompressed key in Segwit") 148 | }, 149 | Some(tp) => exit!("Address of type {} can't sign messages.", tp), 150 | }; 151 | // We need to use to_string because regtest and testnet addresses are the same. 152 | if addr.to_string() != expected.to_string() { 153 | exit!( 154 | "Signed for address {:?}, expected {:?} ({})", 155 | addr, 156 | expected, 157 | expected.address_type().map(|t| t.to_string()).unwrap_or("unknown type".into()), 158 | ); 159 | } 160 | } else { 161 | unreachable!(); 162 | } 163 | eprintln!("Signature is valid."); 164 | } 165 | 166 | fn cmd_recover<'a>() -> clap::App<'a, 'a> { 167 | cmd::subcommand("recover", "recover the pubkey and address of a Bitcoin Signed Messages") 168 | .arg(args::arg("signature", "the signature in hex").required(true)) 169 | .arg(args::arg("message", "the message that was signed (without prefix)").required(true)) 170 | } 171 | 172 | fn exec_recover<'a>(args: &clap::ArgMatches<'a>) { 173 | let sig = args.value_of("signature").need("no signature provided"); 174 | let sig_bytes = match (hex::decode(&sig), base64::decode(&sig)) { 175 | (Ok(b), Err(_)) => b, 176 | (Err(_), Ok(b)) => b, 177 | (Ok(b), Ok(_)) => { 178 | debug!("Signature is both valid hex and base64, assuming it's hex."); 179 | b 180 | } 181 | (Err(e1), Err(e2)) => exit!("Invalid signature: \"{}\"; \"{}\"", e1, e2), 182 | }; 183 | 184 | if sig_bytes.len() != 65 { 185 | exit!("Invalid signature: length is {} instead of 65 bytes", sig_bytes.len()); 186 | } 187 | let recid = secp256k1::ecdsa::RecoveryId::from_i32((sig_bytes[0] - 27 & 0x03) as i32) 188 | .need("invalid recoverable signature (invalid recid)"); 189 | let compressed = sig_bytes[0] & 0x04 != 0x04; 190 | let signature = secp256k1::ecdsa::RecoverableSignature::from_compact(&sig_bytes[1..], recid) 191 | .need("invalid recoverable signature"); 192 | 193 | let msg = args.value_of("message").need("no message given"); 194 | let hash = bitcoin::sign_message::signed_msg_hash(&msg); 195 | 196 | let pubkey = SECP 197 | .recover_ecdsa(&secp256k1::Message::from_digest(hash.to_byte_array()), &signature) 198 | .need("invalid signature"); 199 | 200 | let bitcoin_key = PublicKey { 201 | inner: pubkey, 202 | compressed: compressed, 203 | }; 204 | let info = hal::GetInfo::get_info(&bitcoin_key, args.network()); 205 | args.print_output(&info) 206 | } 207 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/miniscript.rs: -------------------------------------------------------------------------------- 1 | 2 | use bitcoin::hex::{DisplayHex, FromHex}; 3 | use bitcoin::ScriptBuf; 4 | use clap; 5 | use hal::miniscript::{ 6 | DescriptorInfo, MiniscriptInfo, MiniscriptKeyType, Miniscripts, PolicyInfo, ScriptContexts, 7 | }; 8 | use miniscript::miniscript::{BareCtx, Legacy, Miniscript, Segwitv0}; 9 | use miniscript::policy::Liftable; 10 | use miniscript::{policy, Descriptor, FromStrKey, MiniscriptKey}; 11 | 12 | use crate::prelude::*; 13 | 14 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 15 | cmd::subcommand_group("miniscript", "work with miniscript (alias: ms)") 16 | .alias("ms") 17 | .subcommand(cmd_descriptor()) 18 | .subcommand(cmd_inspect()) 19 | .subcommand(cmd_parse()) 20 | .subcommand(cmd_policy()) 21 | .subcommand(cmd_compile()) 22 | } 23 | 24 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 25 | match args.subcommand() { 26 | ("descriptor", Some(ref m)) => exec_descriptor(&m), 27 | ("inspect", Some(ref m)) => exec_inspect(&m), 28 | ("parse", Some(ref m)) => exec_parse(&m), 29 | ("policy", Some(ref m)) => exec_policy(&m), 30 | ("compile", Some(ref m)) => exec_compile(&m), 31 | (_, _) => unreachable!("clap prints help"), 32 | }; 33 | } 34 | 35 | fn cmd_descriptor<'a>() -> clap::App<'a, 'a> { 36 | cmd::subcommand("descriptor", "get information about an output descriptor") 37 | .args(&[args::arg("descriptor", "the output descriptor to inspect").required(false)]) 38 | } 39 | 40 | fn exec_descriptor<'a>(args: &clap::ArgMatches<'a>) { 41 | let desc_str = util::arg_or_stdin(args, "descriptor"); 42 | let network = args.network(); 43 | 44 | let info = desc_str 45 | .parse::>() 46 | .map(|desc| DescriptorInfo { 47 | descriptor: desc.to_string(), 48 | key_type: MiniscriptKeyType::PublicKey, 49 | address: desc.address(network).map(|a| a.to_string()).ok(), 50 | script_pubkey: Some(desc.script_pubkey().into_bytes().into()), 51 | unsigned_script_sig: Some(desc.unsigned_script_sig().into_bytes().into()), 52 | witness_script: desc.explicit_script().map(|s| s.into_bytes().into()).ok(), 53 | max_satisfaction_weight: desc.max_weight_to_satisfy().ok().map(|w| w.to_wu()), 54 | policy: policy::Liftable::lift(&desc).map(|pol| pol.to_string()).ok(), 55 | }) 56 | .or_else(|e| { 57 | debug!("Can't parse descriptor with public keys: {}", e); 58 | // Then try with strings. 59 | desc_str.parse::>().map(|desc| DescriptorInfo { 60 | descriptor: desc.to_string(), 61 | key_type: MiniscriptKeyType::String, 62 | address: None, 63 | script_pubkey: None, 64 | unsigned_script_sig: None, 65 | witness_script: None, 66 | max_satisfaction_weight: desc.max_weight_to_satisfy().ok().map(|w| w.to_wu()), 67 | policy: policy::Liftable::lift(&desc).map(|pol| pol.to_string()).ok(), 68 | }) 69 | }) 70 | .need("invalid miniscript"); 71 | args.print_output(&info); 72 | } 73 | 74 | fn cmd_inspect<'a>() -> clap::App<'a, 'a> { 75 | cmd::subcommand("inspect", "inspect miniscripts") 76 | .arg(args::arg("miniscript", "the miniscript to inspect").required(false)) 77 | } 78 | 79 | fn exec_inspect<'a>(args: &clap::ArgMatches<'a>) { 80 | let input = util::arg_or_stdin(args, "miniscript"); 81 | let miniscript_str = input.as_ref(); 82 | 83 | // First try with pubkeys. 84 | let bare_info = Miniscript::::from_str_insane(miniscript_str) 85 | .map_err(|e| debug!("Cannot parse as Bare Miniscript {}", e)) 86 | .map(|x| { 87 | let script = x.encode(); 88 | MiniscriptInfo::from_bare(x, MiniscriptKeyType::PublicKey, Some(script)) 89 | }) 90 | .ok(); 91 | let p2sh_info = Miniscript::::from_str_insane(miniscript_str) 92 | .map_err(|e| debug!("Cannot parse as Legacy/p2sh Miniscript {}", e)) 93 | .map(|x| { 94 | let script = x.encode(); 95 | MiniscriptInfo::from_p2sh(x, MiniscriptKeyType::PublicKey, Some(script)) 96 | }) 97 | .ok(); 98 | let segwit_info = Miniscript::::from_str_insane(miniscript_str) 99 | .map_err(|e| info!("Cannot parse as Segwitv0 Miniscript {}", e)) 100 | .map(|x| { 101 | let script = x.encode(); 102 | MiniscriptInfo::from_segwitv0(x, MiniscriptKeyType::PublicKey, Some(script)) 103 | }) 104 | .ok(); 105 | let info = if bare_info.is_none() && p2sh_info.is_none() && segwit_info.is_none() { 106 | // Try as Strings 107 | let bare_info = Miniscript::::from_str_insane(miniscript_str) 108 | .map_err(|e| debug!("Cannot parse as Bare Miniscript {}", e)) 109 | .map(|x| MiniscriptInfo::from_bare(x, MiniscriptKeyType::String, None)) 110 | .ok(); 111 | let p2sh_info = Miniscript::::from_str_insane(miniscript_str) 112 | .map_err(|e| debug!("Cannot parse as Legacy/p2sh Miniscript {}", e)) 113 | .map(|x| MiniscriptInfo::from_p2sh(x, MiniscriptKeyType::String, None)) 114 | .ok(); 115 | let segwit_info = Miniscript::::from_str_insane(miniscript_str) 116 | .map_err(|e| info!("Cannot parse as Segwitv0 Miniscript {}", e)) 117 | .map(|x| MiniscriptInfo::from_segwitv0(x, MiniscriptKeyType::String, None)) 118 | .ok(); 119 | 120 | MiniscriptInfo::combine(MiniscriptInfo::combine(bare_info, p2sh_info), segwit_info) 121 | .need("Invalid Miniscript") 122 | } else { 123 | MiniscriptInfo::combine(MiniscriptInfo::combine(bare_info, p2sh_info), segwit_info) 124 | .unwrap() 125 | }; 126 | args.print_output(&info); 127 | } 128 | 129 | fn cmd_parse<'a>() -> clap::App<'a, 'a> { 130 | cmd::subcommand("parse", "parse a script into a miniscript") 131 | .arg(args::arg("script", "hex script to parse").required(false)) 132 | } 133 | 134 | fn exec_parse<'a>(args: &clap::ArgMatches<'a>) { 135 | let script_hex = util::arg_or_stdin(args, "script"); 136 | let script = ScriptBuf::from_hex(&script_hex).need("invalid hex script"); 137 | 138 | let segwit_info = Miniscript::<_, Segwitv0>::parse_insane(&script) 139 | .map_err(|e| info!("Cannot parse as segwit Miniscript {}", e)) 140 | .map(|x| { 141 | MiniscriptInfo::from_segwitv0(x, MiniscriptKeyType::PublicKey, Some(script.clone())) 142 | }) 143 | .ok(); 144 | let legacy_info = Miniscript::<_, Legacy>::parse_insane(&script) 145 | .map_err(|e| debug!("Cannot parse as Legacy Miniscript {}", e)) 146 | .map(|x| MiniscriptInfo::from_p2sh(x, MiniscriptKeyType::PublicKey, Some(script.clone()))) 147 | .ok(); 148 | let bare_info = Miniscript::<_, BareCtx>::parse_insane(&script) 149 | .map_err(|e| debug!("Cannot parse as Bare Miniscript {}", e)) 150 | .map(|x| MiniscriptInfo::from_bare(x, MiniscriptKeyType::PublicKey, Some(script))) 151 | .ok(); 152 | if segwit_info.is_none() && legacy_info.is_none() && bare_info.is_none() { 153 | exit!("Invalid Miniscript under all script contexts") 154 | } 155 | 156 | let comb_info = 157 | MiniscriptInfo::combine(MiniscriptInfo::combine(bare_info, legacy_info), segwit_info) 158 | .unwrap(); 159 | args.print_output(&comb_info); 160 | } 161 | 162 | fn cmd_policy<'a>() -> clap::App<'a, 'a> { 163 | cmd::subcommand("policy", "inspect policies") 164 | .arg(args::arg("policy", "the miniscript policy to inspect").required(false)) 165 | } 166 | 167 | fn get_policy_info( 168 | policy_str: &str, 169 | key_type: MiniscriptKeyType, 170 | ) -> Result 171 | where 172 | Pk: std::str::FromStr, 173 | ::Err: std::fmt::Display, 174 | ::Sha256: std::str::FromStr, 175 | ::Hash256: std::str::FromStr, 176 | ::Ripemd160: std::str::FromStr, 177 | ::Hash160: std::str::FromStr, 178 | <::Sha256 as std::str::FromStr>::Err: std::fmt::Display, 179 | <::Hash256 as std::str::FromStr>::Err: std::fmt::Display, 180 | <::Ripemd160 as std::str::FromStr>::Err: std::fmt::Display, 181 | <::Hash160 as std::str::FromStr>::Err: std::fmt::Display, 182 | { 183 | let concrete_pol: Option> = policy_str.parse().ok(); 184 | let policy = match concrete_pol { 185 | Some(ref concrete) => policy::Liftable::lift(concrete)?, 186 | None => policy_str.parse()?, 187 | }; 188 | Ok(PolicyInfo { 189 | is_concrete: concrete_pol.is_some(), 190 | key_type: key_type, 191 | is_trivial: policy.is_trivial(), 192 | is_unsatisfiable: policy.is_unsatisfiable(), 193 | relative_timelocks: policy.relative_timelocks(), 194 | n_keys: policy.n_keys(), 195 | minimum_n_keys: policy.minimum_n_keys().ok_or(miniscript::Error::CouldNotSatisfy)?, 196 | sorted: policy.clone().sorted().to_string(), 197 | normalized: policy.clone().normalized().to_string(), 198 | miniscript: concrete_pol.map(|p| Miniscripts { 199 | bare: match policy::compiler::best_compilation::(&p) { 200 | Ok(ms) => Some(ms.to_string()), 201 | Err(e) => { 202 | debug!("Compiler error: {}", e); 203 | None 204 | } 205 | }, 206 | p2sh: match policy::compiler::best_compilation::(&p) { 207 | Ok(ms) => Some(ms.to_string()), 208 | Err(e) => { 209 | debug!("Compiler error: {}", e); 210 | None 211 | } 212 | }, 213 | segwitv0: match policy::compiler::best_compilation::(&p) { 214 | Ok(ms) => Some(ms.to_string()), 215 | Err(e) => { 216 | debug!("Compiler error: {}", e); 217 | None 218 | } 219 | }, 220 | }), 221 | }) 222 | } 223 | 224 | fn exec_policy<'a>(args: &clap::ArgMatches<'a>) { 225 | let input = util::arg_or_stdin(args, "policy"); 226 | let policy_str = input.as_ref(); 227 | 228 | // First try a concrete policy with pubkeys. 229 | if let Ok(info) = 230 | get_policy_info::(policy_str, MiniscriptKeyType::PublicKey) 231 | { 232 | args.print_output(&info) 233 | } else { 234 | // Then try with strings. 235 | match get_policy_info::(policy_str, MiniscriptKeyType::String) { 236 | Ok(info) => args.print_output(&info), 237 | Err(e) => exit!("Invalid policy: {}", e), 238 | } 239 | } 240 | } 241 | 242 | fn cmd_compile<'a>() -> clap::App<'a, 'a> { 243 | cmd::subcommand("compile", "compile a policy into a script") 244 | .arg(args::arg("policy", "the miniscript policy to compile").required(false)) 245 | .arg( 246 | clap::Arg::with_name("type") 247 | .long("type") 248 | .takes_value(true) 249 | .possible_values(&["bare", "p2sh", "segwitv0", "tapscript"]) 250 | .default_value("tapscript") 251 | .help("script type to compile to"), 252 | ) 253 | } 254 | 255 | fn exec_compile<'a>(args: &clap::ArgMatches<'a>) { 256 | let policy_str = util::arg_or_stdin(args, "policy"); 257 | let script_type = args.value_of("type").unwrap(); 258 | 259 | let result = policy_str 260 | .parse::>() 261 | .map_err(|e| format!("Invalid concrete policy: {}", e)) 262 | .and_then(|concrete| { 263 | match script_type { 264 | "bare" => policy::compiler::best_compilation::<_, BareCtx>(&concrete) 265 | .map(|ms| ms.encode()), 266 | "p2sh" => policy::compiler::best_compilation::<_, Legacy>(&concrete) 267 | .map(|ms| ms.encode()), 268 | "segwitv0" => policy::compiler::best_compilation::<_, Segwitv0>(&concrete) 269 | .map(|ms| ms.encode()), 270 | "tapscript" => policy::compiler::best_compilation::<_, miniscript::Tap>(&concrete) 271 | .map(|ms| ms.encode()), 272 | _ => unreachable!("clap enforces valid values"), 273 | } 274 | .map_err(|e| format!("Compilation error: {}", e)) 275 | }); 276 | 277 | match result { 278 | Ok(script) => { 279 | #[derive(serde::Serialize)] 280 | struct ScriptOutput { 281 | hex: String, 282 | asm: String, 283 | } 284 | let output = ScriptOutput { 285 | hex: script.as_bytes().as_hex().to_string(), 286 | asm: script.to_string(), 287 | }; 288 | 289 | args.print_output(&output) 290 | } 291 | Err(e) => exit!("{}", e), 292 | } 293 | } 294 | 295 | trait FromScriptContexts: Sized { 296 | fn from_bare( 297 | ms: Miniscript, 298 | key_type: MiniscriptKeyType, 299 | script: Option, 300 | ) -> Self; 301 | fn from_p2sh( 302 | ms: Miniscript, 303 | key_type: MiniscriptKeyType, 304 | script: Option, 305 | ) -> Self; 306 | fn from_segwitv0( 307 | ms: Miniscript, 308 | key_type: MiniscriptKeyType, 309 | script: Option, 310 | ) -> Self; 311 | fn combine(a: Option, b: Option) -> Option; 312 | } 313 | 314 | impl FromScriptContexts for MiniscriptInfo { 315 | fn from_bare( 316 | ms: Miniscript, 317 | key_type: MiniscriptKeyType, 318 | script: Option, 319 | ) -> Self { 320 | Self { 321 | key_type: key_type, 322 | valid_script_contexts: ScriptContexts::from_bare(true), 323 | script_size: ms.script_size(), 324 | max_satisfaction_witness_elements: ms.max_satisfaction_witness_elements().ok(), 325 | max_satisfaction_size_segwit: None, 326 | max_satisfaction_size_non_segwit: ms.max_satisfaction_size().ok(), 327 | script: script.map(|x| x.into_bytes().into()), 328 | policy: match ms.lift() { 329 | Ok(pol) => Some(pol.to_string()), 330 | Err(e) => { 331 | info!("Lift error {}: BareCtx", e); 332 | None 333 | } 334 | }, 335 | requires_sig: ms.requires_sig(), 336 | non_malleable: ScriptContexts::from_bare(ms.is_non_malleable()), 337 | within_resource_limits: ScriptContexts::from_bare(ms.within_resource_limits()), 338 | has_mixed_timelocks: ms.has_mixed_timelocks(), 339 | has_repeated_keys: ms.has_repeated_keys(), 340 | sane_miniscript: ScriptContexts::from_bare(ms.sanity_check().is_ok()), 341 | } 342 | } 343 | 344 | fn from_p2sh( 345 | ms: Miniscript, 346 | key_type: MiniscriptKeyType, 347 | script: Option, 348 | ) -> Self { 349 | Self { 350 | key_type: key_type, 351 | valid_script_contexts: ScriptContexts::from_p2sh(true), 352 | script_size: ms.script_size(), 353 | max_satisfaction_witness_elements: ms.max_satisfaction_witness_elements().ok(), 354 | max_satisfaction_size_segwit: None, 355 | max_satisfaction_size_non_segwit: ms.max_satisfaction_size().ok(), 356 | script: script.map(|x| x.into_bytes().into()), 357 | policy: match ms.lift() { 358 | Ok(pol) => Some(pol.to_string()), 359 | Err(e) => { 360 | info!("Lift error {}: Legacy(p2sh) context", e); 361 | None 362 | } 363 | }, 364 | requires_sig: ms.requires_sig(), 365 | non_malleable: ScriptContexts::from_p2sh(ms.is_non_malleable()), 366 | within_resource_limits: ScriptContexts::from_p2sh(ms.within_resource_limits()), 367 | has_mixed_timelocks: ms.has_mixed_timelocks(), 368 | has_repeated_keys: ms.has_repeated_keys(), 369 | sane_miniscript: ScriptContexts::from_p2sh(ms.sanity_check().is_ok()), 370 | } 371 | } 372 | 373 | fn from_segwitv0( 374 | ms: Miniscript, 375 | key_type: MiniscriptKeyType, 376 | script: Option, 377 | ) -> Self { 378 | Self { 379 | key_type: key_type, 380 | valid_script_contexts: ScriptContexts::from_segwitv0(true), 381 | script_size: ms.script_size(), 382 | max_satisfaction_witness_elements: ms.max_satisfaction_witness_elements().ok(), 383 | max_satisfaction_size_segwit: ms.max_satisfaction_size().ok(), 384 | max_satisfaction_size_non_segwit: None, 385 | script: script.map(|x| x.into_bytes().into()), 386 | policy: match ms.lift() { 387 | Ok(pol) => Some(pol.to_string()), 388 | Err(e) => { 389 | info!("Lift error {}: Segwitv0 Context", e); 390 | None 391 | } 392 | }, 393 | requires_sig: ms.requires_sig(), 394 | non_malleable: ScriptContexts::from_segwitv0(ms.is_non_malleable()), 395 | within_resource_limits: ScriptContexts::from_segwitv0(ms.within_resource_limits()), 396 | has_mixed_timelocks: ms.has_mixed_timelocks(), 397 | has_repeated_keys: ms.has_repeated_keys(), 398 | sane_miniscript: ScriptContexts::from_segwitv0(ms.sanity_check().is_ok()), 399 | } 400 | } 401 | 402 | // Helper function to combine two Miniscript Infos of same key types 403 | // Used to combine Infos from different scriptContexts 404 | fn combine(a: Option, b: Option) -> Option { 405 | match (a, b) { 406 | (None, None) => None, 407 | (None, Some(b)) => Some(b), 408 | (Some(a), None) => Some(a), 409 | (Some(a), Some(b)) => { 410 | debug_assert!(a.key_type == b.key_type); 411 | Some(Self { 412 | key_type: a.key_type, 413 | valid_script_contexts: ScriptContexts::or( 414 | a.valid_script_contexts, 415 | b.valid_script_contexts, 416 | ), 417 | script_size: a.script_size, 418 | max_satisfaction_witness_elements: a 419 | .max_satisfaction_witness_elements 420 | .or(b.max_satisfaction_witness_elements), 421 | max_satisfaction_size_segwit: a 422 | .max_satisfaction_size_segwit 423 | .or(b.max_satisfaction_size_segwit), 424 | max_satisfaction_size_non_segwit: a 425 | .max_satisfaction_size_non_segwit 426 | .or(b.max_satisfaction_size_non_segwit), 427 | script: a.script, 428 | policy: a.policy.or(b.policy), 429 | requires_sig: a.requires_sig, 430 | non_malleable: ScriptContexts::or(a.non_malleable,b.non_malleable), 431 | within_resource_limits: ScriptContexts::or(a.within_resource_limits,b.within_resource_limits), 432 | has_mixed_timelocks: a.has_mixed_timelocks, 433 | has_repeated_keys: b.has_repeated_keys, 434 | sane_miniscript: ScriptContexts::or(a.sane_miniscript,b.sane_miniscript), 435 | }) 436 | } 437 | } 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod address; 2 | pub mod bech32; 3 | pub mod bip32; 4 | pub mod bip39; 5 | pub mod block; 6 | pub mod hash; 7 | pub mod key; 8 | pub mod ln; 9 | pub mod merkle; 10 | pub mod message; 11 | pub mod miniscript; 12 | pub mod psbt; 13 | pub mod random; 14 | pub mod script; 15 | pub mod tx; 16 | 17 | /// Build a list of all built-in subcommands. 18 | pub fn subcommands() -> Vec> { 19 | vec![ 20 | address::subcommand(), 21 | bech32::subcommand(), 22 | bip32::subcommand(), 23 | bip39::subcommand(), 24 | block::subcommand(), 25 | hash::subcommand(), 26 | key::subcommand(), 27 | ln::subcommand(), 28 | merkle::subcommand(), 29 | message::subcommand(), 30 | miniscript::subcommand(), 31 | psbt::subcommand(), 32 | random::subcommand(), 33 | script::subcommand(), 34 | tx::subcommand(), 35 | ] 36 | } 37 | 38 | /// Create a new subcommand group using the template that sets all the common settings. 39 | /// This is not intended for actual commands, but for subcommands that host a bunch of other 40 | /// subcommands. 41 | pub fn subcommand_group<'a>(name: &'a str, about: &'a str) -> clap::App<'a, 'a> { 42 | clap::SubCommand::with_name(name).about(about).settings(&[ 43 | clap::AppSettings::SubcommandRequiredElseHelp, 44 | clap::AppSettings::DisableHelpSubcommand, 45 | clap::AppSettings::VersionlessSubcommands, 46 | clap::AppSettings::UnifiedHelpMessage, 47 | ]) 48 | } 49 | 50 | /// Create a new subcommand using the template that sets all the common settings. 51 | pub fn subcommand<'a>(name: &'a str, about: &'a str) -> clap::App<'a, 'a> { 52 | clap::SubCommand::with_name(name) 53 | .about(about) 54 | .setting(clap::AppSettings::DisableHelpSubcommand) 55 | } 56 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/psbt.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{self, BufRead, Read, Write}; 3 | use std::str::FromStr; 4 | 5 | use base64; 6 | use clap; 7 | use hex; 8 | 9 | use bitcoin::{bip32, ecdsa, EcdsaSighashType, Psbt, PublicKey, Transaction}; 10 | use bitcoin::consensus::{deserialize, serialize}; 11 | use bitcoin::hashes::Hash; 12 | use miniscript::psbt::PsbtExt; 13 | use secp256k1; 14 | 15 | use crate::prelude::*; 16 | 17 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 18 | cmd::subcommand_group("psbt", "partially signed Bitcoin transactions") 19 | .subcommand(cmd_create()) 20 | .subcommand(cmd_decode()) 21 | .subcommand(cmd_edit()) 22 | .subcommand(cmd_finalize()) 23 | .subcommand(cmd_merge()) 24 | .subcommand(cmd_rawsign()) 25 | } 26 | 27 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 28 | match args.subcommand() { 29 | ("create", Some(ref m)) => exec_create(&m), 30 | ("decode", Some(ref m)) => exec_decode(&m), 31 | ("edit", Some(ref m)) => exec_edit(&m), 32 | ("finalize", Some(ref m)) => exec_finalize(&m), 33 | ("merge", Some(ref m)) => exec_merge(&m), 34 | ("rawsign", Some(ref m)) => exec_rawsign(&m), 35 | (c, _) => eprintln!("command {} unknown", c), 36 | }; 37 | } 38 | 39 | #[derive(Debug)] 40 | enum PsbtSource { 41 | Base64, 42 | Hex, 43 | File, 44 | } 45 | 46 | /// Tries to decode the string as hex and base64, if it works, returns the bytes. 47 | /// If not, tries to open a filename with the given string as relative path, if it works, returns 48 | /// the content bytes. 49 | /// Also returns an enum value indicating which source worked. 50 | fn file_or_raw(flag: &str) -> (Vec, PsbtSource) { 51 | if let Ok(raw) = hex::decode(&flag) { 52 | (raw, PsbtSource::Hex) 53 | } else if let Ok(raw) = base64::decode(&flag) { 54 | (raw, PsbtSource::Base64) 55 | } else if let Ok(mut file) = File::open(&flag) { 56 | let mut buf = Vec::new(); 57 | file.read_to_end(&mut buf).need("error reading file"); 58 | (buf, PsbtSource::File) 59 | } else { 60 | exit!("Can't load PSBT: invalid hex, base64 or unknown file"); 61 | } 62 | } 63 | 64 | fn cmd_create<'a>() -> clap::App<'a, 'a> { 65 | cmd::subcommand("create", "create a PSBT from an unsigned raw transaction").args(&[ 66 | args::arg("raw-tx", "the raw transaction in hex").required(false), 67 | args::opt("output", "where to save the merged PSBT output") 68 | .short("o"), 69 | args::flag("raw-stdout", "output the raw bytes of the result to stdout") 70 | .short("r"), 71 | ]) 72 | } 73 | 74 | fn exec_create<'a>(args: &clap::ArgMatches<'a>) { 75 | let hex_tx = util::arg_or_stdin(args, "raw-tx"); 76 | let raw_tx = hex::decode(hex_tx.as_ref()).need("could not decode raw tx"); 77 | let tx = deserialize::(&raw_tx).need("invalid tx format"); 78 | 79 | let psbt = Psbt::from_unsigned_tx(tx).need("couldn't create a PSBT from the transaction"); 80 | 81 | let serialized = psbt.serialize(); 82 | if let Some(path) = args.value_of("output") { 83 | let mut file = File::create(&path).need("failed to open output file"); 84 | file.write_all(&serialized).need("error writing output file"); 85 | } else if args.is_present("raw-stdout") { 86 | ::std::io::stdout().write_all(&serialized).unwrap(); 87 | } else { 88 | print!("{}", base64::encode(&serialized)); 89 | } 90 | } 91 | 92 | fn cmd_decode<'a>() -> clap::App<'a, 'a> { 93 | cmd::subcommand("decode", "decode a PSBT to JSON") 94 | .arg(args::arg("psbt", "the PSBT file or raw PSBT in base64/hex").required(false)) 95 | } 96 | 97 | fn exec_decode<'a>(args: &clap::ArgMatches<'a>) { 98 | let input = util::arg_or_stdin(args, "psbt"); 99 | let (raw_psbt, _) = file_or_raw(input.as_ref()); 100 | 101 | let psbt = Psbt::deserialize(&raw_psbt).need("invalid PSBT"); 102 | 103 | let info = hal::GetInfo::get_info(&psbt, args.network()); 104 | args.print_output(&info) 105 | } 106 | 107 | fn cmd_edit<'a>() -> clap::App<'a, 'a> { 108 | cmd::subcommand("edit", "edit a PSBT").args(&[ 109 | args::arg("psbt", "PSBT to edit, either base64/hex or a file path").required(false), 110 | args::opt("input-idx", "the input index to edit") 111 | .display_order(1), 112 | args::opt("output-idx", "the output index to edit") 113 | .display_order(2), 114 | args::opt("output", "where to save the resulting PSBT file -- in place if omitted") 115 | .short("o") 116 | .display_order(3) 117 | .next_line_help(true), 118 | args::flag("raw-stdout", "output the raw bytes of the result to stdout") 119 | .short("r"), 120 | // 121 | // values used in both inputs and outputs 122 | args::opt("redeem-script", "the redeem script") 123 | .display_order(99) 124 | .next_line_help(true), 125 | args::opt("witness-script", "the witness script") 126 | .display_order(99) 127 | .next_line_help(true), 128 | args::opt("hd-keypaths", "the HD wallet keypaths `::,...`") 129 | .display_order(99) 130 | .next_line_help(true), 131 | args::opt("hd-keypaths-add", "add an HD wallet keypath `::`") 132 | .display_order(99) 133 | .next_line_help(true), 134 | // 135 | // input values 136 | args::opt("non-witness-utxo", "the non-witness UTXO field in hex (full transaction)") 137 | .display_order(99) 138 | .next_line_help(true), 139 | args::opt("witness-utxo", "the witness UTXO field in hex (only output)") 140 | .display_order(99) 141 | .next_line_help(true), 142 | args::opt("partial-sigs", "set partial sigs `:,...`") 143 | .display_order(99) 144 | .next_line_help(true), 145 | args::opt("partial-sigs-add", "add a partial sig pair `:`") 146 | .display_order(99) 147 | .next_line_help(true), 148 | args::opt("sighash-type", "the sighash type") 149 | .display_order(99) 150 | .next_line_help(true), 151 | // (omitted) redeem-script 152 | // (omitted) witness-script 153 | // (omitted) hd-keypaths 154 | // (omitted) hd-keypaths-add 155 | args::opt("final-script-sig", "set final script signature") 156 | .display_order(99) 157 | .next_line_help(true), 158 | args::opt("final-script-witness", "set final script witness as comma-separated hex values") 159 | .display_order(99) 160 | .next_line_help(true), 161 | // 162 | // output values 163 | // (omitted) redeem-script 164 | // (omitted) witness-script 165 | // (omitted) hd-keypaths 166 | // (omitted) hd-keypaths-add 167 | ]) 168 | } 169 | 170 | /// Parses a `:` pair. 171 | fn parse_partial_sig_pair(pair_str: &str) -> (PublicKey, ecdsa::Signature) { 172 | let mut pair = pair_str.splitn(2, ":"); 173 | let pubkey = pair.next().unwrap().parse().need("invalid partial sig pubkey"); 174 | let sig = { 175 | let hex = pair.next().need("invalid partial sig pair: missing signature"); 176 | hex::decode(&hex).need("invalid partial sig signature hex") 177 | }; 178 | (pubkey, ecdsa::Signature::from_slice(&sig).need("partial sig is not valid ecdsa")) 179 | } 180 | 181 | fn parse_hd_keypath_triplet( 182 | triplet_str: &str, 183 | ) -> (bitcoin::secp256k1::PublicKey, (bip32::Fingerprint, bip32::DerivationPath)) { 184 | let mut triplet = triplet_str.splitn(3, ":"); 185 | let pubkey = triplet.next().unwrap().parse().need("invalid HD keypath pubkey"); 186 | let fp = { 187 | let hex = triplet.next().need("invalid HD keypath triplet: missing fingerprint"); 188 | bip32::Fingerprint::from_str(&hex).need("invalid HD keypath fingerprint hex") 189 | }; 190 | let path = triplet 191 | .next() 192 | .need("invalid HD keypath triplet: missing HD path") 193 | .parse() 194 | .need("invalid derivation path format"); 195 | (pubkey, (fp, path)) 196 | } 197 | 198 | fn edit_input<'a>( 199 | idx: usize, 200 | args: &clap::ArgMatches<'a>, 201 | psbt: &mut Psbt, 202 | ) { 203 | let input = psbt.inputs.get_mut(idx).need("input index out of range"); 204 | 205 | if let Some(hex) = args.value_of("non-witness-utxo") { 206 | let raw = hex::decode(&hex).need("invalid non-witness-utxo hex"); 207 | let utxo = deserialize(&raw).need("invalid non-witness-utxo transaction"); 208 | input.non_witness_utxo = Some(utxo); 209 | } 210 | 211 | if let Some(hex) = args.value_of("witness-utxo") { 212 | let raw = hex::decode(&hex).need("invalid witness-utxo hex"); 213 | let utxo = deserialize(&raw).need("invalid witness-utxo transaction"); 214 | input.witness_utxo = Some(utxo); 215 | } 216 | 217 | if let Some(csv) = args.value_of("partial-sigs") { 218 | input.partial_sigs = csv.split(",").map(parse_partial_sig_pair).collect(); 219 | } 220 | if let Some(pairs) = args.values_of("partial-sigs-add") { 221 | for (pk, sig) in pairs.map(parse_partial_sig_pair) { 222 | if input.partial_sigs.insert(pk, sig).is_some() { 223 | exit!("public key {} is already in partial sigs", &pk); 224 | } 225 | } 226 | } 227 | 228 | if let Some(sht) = args.value_of("sighash-type") { 229 | input.sighash_type = Some(hal::psbt::ecdsa_sighashtype_from_string(&sht).need("invalid sighash string")); 230 | } 231 | 232 | if let Some(hex) = args.value_of("redeem-script") { 233 | let raw = hex::decode(&hex).need("invalid redeem-script hex"); 234 | input.redeem_script = Some(raw.into()); 235 | } 236 | 237 | if let Some(hex) = args.value_of("witness-script") { 238 | let raw = hex::decode(&hex).need("invalid witness-script hex"); 239 | input.witness_script = Some(raw.into()); 240 | } 241 | 242 | if let Some(csv) = args.value_of("hd-keypaths") { 243 | input.bip32_derivation = csv.split(",").map(parse_hd_keypath_triplet).collect(); 244 | } 245 | if let Some(triplets) = args.values_of("hd-keypaths-add") { 246 | for (pk, pair) in triplets.map(parse_hd_keypath_triplet) { 247 | if input.bip32_derivation.insert(pk, pair).is_some() { 248 | exit!("public key {} is already in HD keypaths", &pk); 249 | } 250 | } 251 | } 252 | 253 | if let Some(hex) = args.value_of("final-script-sig") { 254 | let raw = hex::decode(&hex).need("invalid final-script-sig hex"); 255 | input.final_script_sig = Some(raw.into()); 256 | } 257 | 258 | if let Some(csv) = args.value_of("final-script-witness") { 259 | let vhex = csv.split(","); 260 | let vraw = vhex.map(|h| hex::decode(&h).need("invalid final-script-witness hex")); 261 | input.final_script_witness = Some(bitcoin::Witness::from_slice(&vraw.collect::>())); 262 | } 263 | } 264 | 265 | fn edit_output<'a>(idx: usize, args: &clap::ArgMatches<'a>, psbt: &mut Psbt) { 266 | let output = psbt.outputs.get_mut(idx).need("output index out of range"); 267 | 268 | if let Some(hex) = args.value_of("redeem-script") { 269 | let raw = hex::decode(&hex).need("invalid redeem-script hex"); 270 | output.redeem_script = Some(raw.into()); 271 | } 272 | 273 | if let Some(hex) = args.value_of("witness-script") { 274 | let raw = hex::decode(&hex).need("invalid witness-script hex"); 275 | output.witness_script = Some(raw.into()); 276 | } 277 | 278 | if let Some(csv) = args.value_of("hd-keypaths") { 279 | output.bip32_derivation = csv.split(",").map(parse_hd_keypath_triplet).collect(); 280 | } 281 | if let Some(triplets) = args.values_of("hd-keypaths-add") { 282 | for (pk, pair) in triplets.map(parse_hd_keypath_triplet) { 283 | if output.bip32_derivation.insert(pk, pair).is_some() { 284 | exit!("public key {} is already in HD keypaths", &pk); 285 | } 286 | } 287 | } 288 | } 289 | 290 | fn exec_edit<'a>(args: &clap::ArgMatches<'a>) { 291 | let input = util::arg_or_stdin(args, "psbt"); 292 | let (raw, source) = file_or_raw(input.as_ref()); 293 | let mut psbt = Psbt::deserialize(&raw).need("invalid PSBT format"); 294 | 295 | match (args.value_of("input-idx"), args.value_of("output-idx")) { 296 | (None, None) => exit!("no input or output index provided"), 297 | (Some(_), Some(_)) => exit!("can only edit an input or an output at a time"), 298 | (Some(idx), _) => { 299 | edit_input(idx.parse().need("invalid input index"), &args, &mut psbt) 300 | } 301 | (_, Some(idx)) => { 302 | edit_output(idx.parse().need("invalid output index"), &args, &mut psbt) 303 | } 304 | } 305 | 306 | let edited_raw = psbt.serialize(); 307 | if let Some(path) = args.value_of("output") { 308 | let mut file = File::create(&path).need("failed to open output file"); 309 | file.write_all(&edited_raw).need("error writing output file"); 310 | } else if args.is_present("raw-stdout") { 311 | ::std::io::stdout().write_all(&edited_raw).unwrap(); 312 | } else { 313 | match source { 314 | PsbtSource::Hex => print!("{}", hex::encode(&edited_raw)), 315 | PsbtSource::Base64 => print!("{}", base64::encode(&edited_raw)), 316 | PsbtSource::File => { 317 | let path = args.value_of("psbt").unwrap(); 318 | let mut file = File::create(&path).need("failed to PSBT file for writing"); 319 | file.write_all(&edited_raw).need("error writing PSBT file"); 320 | } 321 | } 322 | } 323 | } 324 | 325 | fn cmd_finalize<'a>() -> clap::App<'a, 'a> { 326 | cmd::subcommand("finalize", "finalize a PSBT and print the fully signed tx in hex").args(&[ 327 | args::arg("psbt", "PSBT to finalize, either base64/hex or a file path").required(false), 328 | args::flag("raw-stdout", "output the raw bytes of the result to stdout") 329 | .short("r"), 330 | ]) 331 | } 332 | 333 | fn exec_finalize<'a>(args: &clap::ArgMatches<'a>) { 334 | let input = util::arg_or_stdin(args, "psbt"); 335 | let (raw, _) = file_or_raw(input.as_ref()); 336 | let psbt = Psbt::deserialize(&raw).need("invalid PSBT format"); 337 | 338 | // Create a secp context, should there be one with static lifetime? 339 | let psbt = psbt.finalize(&SECP).unwrap_or_else(|(_, errs)| { 340 | let errs = errs.into_iter().map(|e| e.to_string()).collect::>(); 341 | exit!("failed to finalize psbt: {}", errs.join(", ")); 342 | }); 343 | 344 | let finalized_raw = serialize(&psbt.extract_tx().need("failed to extract tx from psbt")); 345 | if args.is_present("raw-stdout") { 346 | ::std::io::stdout().write_all(&finalized_raw).unwrap(); 347 | } else { 348 | print!("{}", ::hex::encode(&finalized_raw)); 349 | } 350 | } 351 | 352 | fn cmd_merge<'a>() -> clap::App<'a, 'a> { 353 | cmd::subcommand("merge", "merge multiple PSBT files into one").args(&[ 354 | args::arg("psbts", "PSBTs to merge; can be file paths or base64/hex") 355 | .multiple(true) 356 | .required(true), 357 | args::opt("output", "where to save the merged PSBT output") 358 | .short("o"), 359 | args::flag("raw-stdout", "output the raw bytes of the result to stdout") 360 | .short("r"), 361 | ]) 362 | } 363 | 364 | fn exec_merge<'a>(args: &clap::ArgMatches<'a>) { 365 | let stdin = io::stdin(); 366 | let mut parts: Box> = if let Some(values) = args.values_of("psbts") { 367 | Box::new(values.into_iter().map(|f| { 368 | let (raw, _) = file_or_raw(&f); 369 | Psbt::deserialize(&raw).need("invalid PSBT format") 370 | })) 371 | } else { 372 | // Read from stdin. 373 | let stdin_lock = stdin.lock(); 374 | let buf = io::BufReader::new(stdin_lock); 375 | Box::new(buf.lines().take_while(|l| l.is_ok() && !l.as_ref().unwrap().is_empty()).map(|l| { 376 | let (raw, _) = file_or_raw(&l.unwrap()); 377 | Psbt::deserialize(&raw).need("invalid PSBT format") 378 | })) 379 | }; 380 | 381 | let mut merged = parts.next().unwrap(); 382 | for (idx, part) in parts.enumerate() { 383 | if part.unsigned_tx != merged.unsigned_tx { 384 | panic!("PSBTs are not compatible"); 385 | } 386 | 387 | merged.combine(part).need(&format!("error merging PSBT #{}", idx)); 388 | } 389 | 390 | let merged_raw = merged.serialize(); 391 | if let Some(path) = args.value_of("output") { 392 | let mut file = File::create(&path).need("failed to open output file"); 393 | file.write_all(&merged_raw).need("error writing output file"); 394 | } else if args.is_present("raw-stdout") { 395 | ::std::io::stdout().write_all(&merged_raw).unwrap(); 396 | } else { 397 | print!("{}", base64::encode(&merged_raw)); 398 | } 399 | } 400 | 401 | fn cmd_rawsign<'a>() -> clap::App<'a, 'a> { 402 | cmd::subcommand("rawsign", "sign a psbt with private key and add sig to partial sigs").args(&[ 403 | args::arg("psbt", "PSBT to finalize, either base64/hex or a file path").required(false), 404 | args::arg("input-idx", "the input index to edit").required(true), 405 | args::arg("priv-key", "the private key in WIF/hex").required(true), 406 | args::flag("compressed", "Whether the corresponding pk is compressed") 407 | .required(false) 408 | .default_value("true"), 409 | args::flag("raw-stdout", "output the raw bytes of the result to stdout") 410 | .short("r"), 411 | args::opt("output", "where to save the resulting PSBT file -- in place if omitted") 412 | .short("o"), 413 | ]) 414 | } 415 | 416 | fn exec_rawsign<'a>(args: &clap::ArgMatches<'a>) { 417 | let input = util::arg_or_stdin(args, "psbt"); 418 | let (raw, source) = file_or_raw(input.as_ref()); 419 | let mut psbt = Psbt::deserialize(&raw).need("invalid PSBT format"); 420 | 421 | let sk = args.need_privkey("priv-key").inner; 422 | let i = args.value_of("input-idx").need("Input index not provided") 423 | .parse::().need("input-idx must be a positive integer"); 424 | let compressed = args.value_of("compressed").unwrap() 425 | .parse::().need("Compressed must be boolean"); 426 | 427 | if i >= psbt.inputs.len() { 428 | panic!("PSBT input index out of range") 429 | } 430 | 431 | let tx = psbt.clone().extract_tx().need("failed to extract tx from psbt"); 432 | let mut cache = bitcoin::sighash::SighashCache::new(&tx); 433 | 434 | let pk = secp256k1::PublicKey::from_secret_key(&SECP, &sk); 435 | let pk = bitcoin::PublicKey { compressed, inner: pk }; 436 | let msg = psbt.sighash_msg(i, &mut cache, None) 437 | .need("error computing sighash message on psbt"); 438 | let secp_sig = match msg { 439 | miniscript::psbt::PsbtSighashMsg::LegacySighash(sighash) => { 440 | let msg = secp256k1::Message::from_digest(sighash.to_byte_array()); 441 | SECP.sign_ecdsa(&msg, &sk) 442 | }, 443 | miniscript::psbt::PsbtSighashMsg::SegwitV0Sighash(sighash) => { 444 | let msg = secp256k1::Message::from_digest(sighash.to_byte_array()); 445 | SECP.sign_ecdsa(&msg, &sk) 446 | }, 447 | miniscript::psbt::PsbtSighashMsg::TapSighash(_) => { 448 | //TODO(stevenroose) 449 | panic!("Signing taproot transactions is not yet suppported") 450 | }, 451 | }; 452 | 453 | let sighashtype = psbt.inputs[i].sighash_type 454 | .map(|t| t.ecdsa_hash_ty().need("schnorr signatures are not yet supported")) 455 | .unwrap_or_else(|| { 456 | eprintln!("No sighash type set for input {}, so signing with SIGHASH_ALL", i+1); 457 | EcdsaSighashType::All 458 | }); 459 | let sig = ecdsa::Signature { 460 | signature: secp_sig, 461 | sighash_type: sighashtype, 462 | }; 463 | 464 | // mutate the psbt 465 | psbt.inputs[i].partial_sigs.insert(pk, sig); 466 | let raw = psbt.serialize(); 467 | if let Some(path) = args.value_of("output") { 468 | let mut file = File::create(&path).need("failed to open output file"); 469 | file.write_all(&raw).need("error writing output file"); 470 | } else if args.is_present("raw-stdout") { 471 | ::std::io::stdout().write_all(&raw).unwrap(); 472 | } else { 473 | match source { 474 | PsbtSource::Hex => println!("{}", hex::encode(&raw)), 475 | PsbtSource::Base64 => println!("{}", base64::encode(&raw)), 476 | PsbtSource::File => { 477 | let path = args.value_of("psbt").unwrap(); 478 | let mut file = File::create(&path).need("failed to PSBT file for writing"); 479 | file.write_all(&raw).need("error writing PSBT file"); 480 | } 481 | } 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/random.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use bitcoin::secp256k1::rand::{self, RngCore}; 4 | 5 | use crate::prelude::*; 6 | 7 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 8 | cmd::subcommand_group("random", "generate random data") 9 | .subcommand(cmd_bytes()) 10 | } 11 | 12 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 13 | match args.subcommand() { 14 | ("bytes", Some(ref m)) => exec_bytes(&m), 15 | (_, _) => unreachable!("clap prints help"), 16 | }; 17 | } 18 | 19 | fn cmd_bytes<'a>() -> clap::App<'a, 'a> { 20 | cmd::subcommand("bytes", "generate random bytes") 21 | .arg(args::arg("number", "the number of bytes").required(true)) 22 | .arg(args::flag("raw-stdout", "output the raw bytes of the result to stdout") 23 | .short("r")) 24 | } 25 | 26 | fn exec_bytes<'a>(args: &clap::ArgMatches<'a>) { 27 | let nb = args.value_of("number").need("no number of bytes given") 28 | .parse::() 29 | .need("invalid number of bytes"); 30 | let mut bytes = vec![0u8; nb]; 31 | rand::thread_rng().fill_bytes(&mut bytes); 32 | 33 | if args.is_present("raw-stdout") { 34 | ::std::io::stdout().write_all(&bytes).unwrap(); 35 | } else { 36 | print!("{}", hex::encode(&bytes)); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/script.rs: -------------------------------------------------------------------------------- 1 | 2 | use bitcoin::ScriptBuf; 3 | use clap; 4 | use hex; 5 | 6 | use crate::prelude::*; 7 | 8 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 9 | cmd::subcommand_group("script", "manipulate scripts").subcommand(cmd_decode()) 10 | } 11 | 12 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 13 | match args.subcommand() { 14 | ("decode", Some(ref m)) => exec_decode(&m), 15 | (_, _) => unreachable!("clap prints help"), 16 | }; 17 | } 18 | 19 | fn cmd_decode<'a>() -> clap::App<'a, 'a> { 20 | cmd::subcommand("decode", "decode hex script") 21 | .arg(args::arg("hex-script", "script in hex").required(true)) 22 | } 23 | 24 | fn exec_decode<'a>(args: &clap::ArgMatches<'a>) { 25 | let hex_script = args.value_of("hex-script").need("no script provided"); 26 | let raw_script = hex::decode(hex_script).need("could not decode raw script"); 27 | let script = ScriptBuf::from_bytes(raw_script); 28 | 29 | print!("{}", script.to_asm_string()); 30 | } 31 | -------------------------------------------------------------------------------- /src/bin/hal/cmd/tx.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::io::Write; 3 | 4 | use bitcoin::address::NetworkUnchecked; 5 | use bitcoin::consensus::encode::{deserialize, serialize}; 6 | use bitcoin::blockdata::transaction; 7 | use bitcoin::{Address, Network, OutPoint, ScriptBuf, Transaction, TxIn, TxOut}; 8 | 9 | use hal::tx::{InputInfo, InputScriptInfo, OutputInfo, OutputScriptInfo, TransactionInfo}; 10 | use crate::prelude::*; 11 | 12 | pub fn subcommand<'a>() -> clap::App<'a, 'a> { 13 | cmd::subcommand_group("tx", "manipulate transactions") 14 | .subcommand(cmd_create()) 15 | .subcommand(cmd_decode()) 16 | } 17 | 18 | pub fn execute<'a>(args: &clap::ArgMatches<'a>) { 19 | match args.subcommand() { 20 | ("create", Some(ref m)) => exec_create(&m), 21 | ("decode", Some(ref m)) => exec_decode(&m), 22 | (_, _) => unreachable!("clap prints help"), 23 | }; 24 | } 25 | 26 | fn cmd_create<'a>() -> clap::App<'a, 'a> { 27 | cmd::subcommand("create", "create a raw transaction from JSON") 28 | .arg(args::arg("tx-info", "the transaction info in JSON; If omitted, reads from stdin.") 29 | .required(false)) 30 | .arg(args::flag("raw-stdout", "output the raw bytes of the result to stdout") 31 | .short("r")) 32 | .long_about(r#" 33 | Create a transaction from JSON. Use the same format as the `hal tx decode` output. 34 | 35 | It's possible to pass the JSON string as the first argument or pass it via stdin. 36 | 37 | Example format: 38 | { 39 | "version": 1, 40 | "locktime": 0, 41 | "inputs": [ 42 | { 43 | "prevout": "78a0f5b35b73f1f6e054274aa3904867774600f09bd194e97e7a0fd953b27c54:6", 44 | "script_sig": { 45 | "hex": 46 | "483045022100fad8d9b44d1d3a86bd9719ef642b32ed0a1c8f4e3de4e2009936988f73f12ad702207a2204cbdfd166d099cbb08e6c7886db5b986ef4fdfee383c1b8fc4df82ecea80121030a696d89d161c086586cf0de7d98fb97181a1ee0265130f7ddbecd17d616c780" 47 | }, 48 | "sequence": 4294967295 49 | }, 50 | { 51 | "txid": "c182fa9182957c5b906fd2b339d7a01dd110340bced99e049e2bd2c135f4513a", 52 | "vout": 1, 53 | "script_sig": { 54 | "hex": "220020fa28dc1e5eb222055e90f8cade9bcd13ca9ddab7a5ed029e27d41a736f7455ce" 55 | }, 56 | "sequence": 4294967294, 57 | "witness": [ 58 | "", 59 | "30440220725e1c098d85013166fae52794811f6531ff3962ea6bc3228ecfdd4699ae669b022064d5c88f2b838968a345681bbfeb2c09f0433ece511bc4d139c4805adf59d74601", 60 | "3044022055aa0f675bf0c21e113527f838b93d5922143ae6e52b094416d44551ff6d236202205ef3773cc9a7fe2076310c92adc73670747309265ecedb0cffe194885a89863601", 61 | "5221027111c0d6cbc3a40c6e6197ed234bd6e59f277c88094fd33297b1e0a3787a5b7d2102e71711c9840d68e6401d4bd5df78f1850e25ae41f082f4b38ceec37d60cab5442103eeae18900c0d12046f644b960a1ef84589f7f4f71d07914006d550bf85c576e153ae" 62 | ] 63 | } 64 | ], 65 | "outputs": [ 66 | { 67 | "value": 500000, 68 | "script_pub_key": { 69 | "hex": "a91405394a3a5dedce4f945ed9f650fa9ff23f011d4687" 70 | } 71 | }, 72 | { 73 | "value": 2590000, 74 | "script_pub_key": { 75 | "address": "34nFYcfPNTuWCV76YrwdVc4MyXmeVMMpsZ" 76 | } 77 | } 78 | ] 79 | }"# 80 | ) 81 | } 82 | 83 | /// Check both ways to specify the outpoint and panic if conflicting. 84 | fn outpoint_from_input_info(input: &InputInfo) -> OutPoint { 85 | let prevout: Option = input.prevout.as_ref().map( 86 | |ref op| op.parse().need("invalid prevout format") 87 | ); 88 | let txid = input.txid; 89 | let vout = input.vout; 90 | 91 | match (prevout, txid, vout) { 92 | (Some(p), Some(t), _) if t != p.txid => exit!("prevout and txid don't match"), 93 | (Some(p), _, Some(v)) if v != p.vout => exit!("prevout and vout don't match"), 94 | (Some(p), _, _) => p, 95 | (None, Some(t), Some(v)) => OutPoint::new(t, v), 96 | _ => exit!("inputs should specify either the prevout or both the txid and vout"), 97 | } 98 | } 99 | 100 | fn create_script_sig(ss: InputScriptInfo) -> ScriptBuf { 101 | if let Some(hex) = ss.hex { 102 | if ss.asm.is_some() { 103 | warn!("Field \"asm\" of input is ignored."); 104 | } 105 | 106 | hex.0.into() 107 | } else if let Some(_) = ss.asm { 108 | exit!("Decoding script assembly is not yet supported."); 109 | } else { 110 | exit!("No scriptSig info provided."); 111 | } 112 | } 113 | 114 | fn create_input(input: InputInfo) -> TxIn { 115 | TxIn { 116 | previous_output: outpoint_from_input_info(&input), 117 | script_sig: input.script_sig.map(create_script_sig).unwrap_or_default(), 118 | sequence: bitcoin::Sequence::from_height(input.sequence.unwrap_or_default().try_into().need("Invalid sequence")), 119 | witness: match input.witness { 120 | Some(ref w) => bitcoin::Witness::from_slice(&w.iter().map(|h| &h.0).collect::>()), 121 | None => bitcoin::Witness::new(), 122 | }, 123 | } 124 | } 125 | 126 | /// We use this type to track possible networks used by the user. 127 | pub struct UsedNetwork { 128 | possible: Vec, 129 | } 130 | 131 | impl UsedNetwork { 132 | pub fn new(network: Option) -> UsedNetwork { 133 | UsedNetwork { possible: network.into_iter().collect() } 134 | } 135 | 136 | fn register_addr(&mut self, addr: &Address) { 137 | self.possible.retain(|n| addr.is_valid_for_network(*n)); 138 | if self.possible.is_empty() { 139 | exit!("incompatible use of networks in addresses: {:?}", addr); 140 | } 141 | } 142 | } 143 | 144 | fn create_script_pubkey(spk: OutputScriptInfo, used_network: &mut UsedNetwork) -> ScriptBuf { 145 | if spk.type_.is_some() { 146 | warn!("Field \"type\" of output is ignored."); 147 | } 148 | 149 | // First check consistency of the address, if given. 150 | if let Some(ref addr) = spk.address { 151 | // Error if another network had already been used. 152 | used_network.register_addr(addr); 153 | } 154 | 155 | if let Some(hex) = spk.hex { 156 | if spk.asm.is_some() { 157 | warn!("Field \"asm\" of output is ignored."); 158 | } 159 | if spk.address.is_some() { 160 | warn!("Field \"address\" of output is ignored."); 161 | } 162 | 163 | //TODO(stevenroose) do script sanity check to avoid blackhole? 164 | hex.0.into() 165 | } else if let Some(_) = spk.asm { 166 | if spk.address.is_some() { 167 | warn!("Field \"address\" of output is ignored."); 168 | } 169 | 170 | //TODO(stevenroose) support script disassembly 171 | exit!("Decoding script assembly is not yet supported."); 172 | } else if let Some(address) = spk.address { 173 | address.assume_checked().script_pubkey() 174 | } else { 175 | exit!("No scriptPubKey info provided."); 176 | } 177 | } 178 | 179 | fn create_output(output: OutputInfo, used_network: &mut UsedNetwork) -> TxOut { 180 | TxOut { 181 | value: output.value.need("Field \"value\" is required for outputs."), 182 | script_pubkey: output 183 | .script_pub_key 184 | .map(|s| create_script_pubkey(s, used_network)) 185 | .unwrap_or_default(), 186 | } 187 | } 188 | 189 | pub fn create_transaction(info: TransactionInfo, used_network: &mut UsedNetwork) -> Transaction { 190 | // Fields that are ignored. 191 | if info.txid.is_some() { 192 | warn!("Field \"txid\" is ignored."); 193 | } 194 | if info.wtxid.is_some() { 195 | warn!("Field \"wtxid\" is ignored."); 196 | } 197 | if info.size.is_some() { 198 | warn!("Field \"size\" is ignored."); 199 | } 200 | if info.weight.is_some() { 201 | warn!("Field \"weight\" is ignored."); 202 | } 203 | if info.vsize.is_some() { 204 | warn!("Field \"vsize\" is ignored."); 205 | } 206 | 207 | Transaction { 208 | version: transaction::Version(info.version.need("Field \"version\" is required.")), 209 | lock_time: bitcoin::absolute::LockTime::from_height( 210 | info.locktime.need("Field \"locktime\" is required.") 211 | ).need("Field \"lockime\" is invalid").into(), 212 | input: info 213 | .inputs 214 | .need("Field \"inputs\" is required.") 215 | .into_iter() 216 | .map(create_input) 217 | .collect(), 218 | output: { 219 | info.outputs.need("Field \"outputs\" is required.") 220 | .into_iter().map(|o| create_output(o, used_network)) 221 | .collect() 222 | }, 223 | } 224 | } 225 | 226 | fn exec_create<'a>(args: &clap::ArgMatches<'a>) { 227 | let info = serde_json::from_str::(&util::arg_or_stdin(args, "tx-info")) 228 | .need("invalid JSON provided"); 229 | 230 | let mut used_network = UsedNetwork::new(args.explicit_network()); 231 | let tx = create_transaction(info, &mut used_network); 232 | let tx_bytes = serialize(&tx); 233 | if args.is_present("raw-stdout") { 234 | ::std::io::stdout().write_all(&tx_bytes).unwrap(); 235 | } else { 236 | print!("{}", hex::encode(&tx_bytes)); 237 | } 238 | } 239 | 240 | fn cmd_decode<'a>() -> clap::App<'a, 'a> { 241 | cmd::subcommand("decode", "decode a raw transaction to JSON") 242 | .arg(args::arg("raw-tx", "the raw transaction in hex").required(false)) 243 | } 244 | 245 | fn exec_decode<'a>(args: &clap::ArgMatches<'a>) { 246 | let hex_tx = util::arg_or_stdin(args, "raw-tx"); 247 | let raw_tx = hex::decode(hex_tx.as_ref()).need("could not decode raw tx"); 248 | let tx: Transaction = deserialize(&raw_tx).need("invalid tx format"); 249 | 250 | let info = hal::GetInfo::get_info(&tx, args.network()); 251 | args.print_output(&info) 252 | } 253 | -------------------------------------------------------------------------------- /src/bin/hal/main.rs: -------------------------------------------------------------------------------- 1 | extern crate bip39; 2 | extern crate bitcoin; 3 | #[macro_use] 4 | extern crate lazy_static; 5 | extern crate lightning_invoice; 6 | #[macro_use] 7 | extern crate log; 8 | extern crate miniscript; 9 | extern crate base64; 10 | extern crate clap; 11 | extern crate fern; 12 | extern crate hex; 13 | extern crate jobserver; 14 | extern crate serde_json; 15 | extern crate secp256k1; 16 | extern crate shell_escape; 17 | 18 | extern crate hal; 19 | 20 | use std::{env, panic, process}; 21 | 22 | pub mod args; 23 | pub mod cmd; 24 | mod process_builder; 25 | pub mod util; 26 | 27 | pub mod prelude { 28 | pub use crate::{args, cmd, util}; 29 | pub use crate::args::{ArgMatchesExt, FlexiblePubkey}; 30 | pub use crate::util::{OptionExt, ResultExt}; 31 | pub use hal::SECP; 32 | pub use super::exit; 33 | } 34 | 35 | 36 | use crate::prelude::*; 37 | 38 | #[macro_export] 39 | macro_rules! exit { 40 | ($($arg:tt)*) => {{ 41 | if $crate::init_app().get_matches().is_present("verbose") { 42 | let msg = format!($($arg)*); 43 | eprintln!("{}", msg); 44 | panic!("{}", msg); 45 | } else { 46 | eprintln!($($arg)*); 47 | std::process::exit(1); 48 | } 49 | }} 50 | } 51 | 52 | /// Setup logging with the given log level. 53 | fn setup_logger(lvl: log::LevelFilter) { 54 | fern::Dispatch::new() 55 | .format(|out, message, _record| out.finish(format_args!("{}", message))) 56 | .level(lvl) 57 | .chain(std::io::stderr()) 58 | .apply() 59 | .need("error setting up logger"); 60 | } 61 | 62 | /// Create the main app object. 63 | fn init_app() -> clap::App<'static, 'static> { 64 | clap::App::new("hal") 65 | .version(clap::crate_version!()) 66 | .author("Steven Roose ") 67 | .about("hal - the Bitcoin companion") 68 | .settings(&[ 69 | clap::AppSettings::GlobalVersion, 70 | clap::AppSettings::UnifiedHelpMessage, 71 | clap::AppSettings::VersionlessSubcommands, 72 | clap::AppSettings::AllowExternalSubcommands, 73 | clap::AppSettings::DisableHelpSubcommand, 74 | clap::AppSettings::AllArgsOverrideSelf, 75 | clap::AppSettings::SubcommandRequiredElseHelp, 76 | ]) 77 | .subcommands(cmd::subcommands()) 78 | .arg( 79 | args::flag("verbose", "Print verbose logging output to stderr") 80 | .short("v") 81 | .global(true), 82 | ) 83 | .args(&args::opts_networks()) 84 | .arg(&args::opt_yaml()) 85 | } 86 | 87 | /// The help appendix listing external subcommands. 88 | fn external_help() -> String { 89 | let mut cmds: Vec = util::list_commands() 90 | .into_iter() 91 | .filter_map(|c| match c { 92 | util::CommandInfo::External { 93 | name, 94 | path: _, 95 | } => Some(name), 96 | _ => None, 97 | }) 98 | .collect(); 99 | cmds.sort(); 100 | 101 | "EXTERNAL SUBCOMMANDS:\n ".to_owned() + &cmds.join("\n ") 102 | } 103 | 104 | fn main() { 105 | // Apply a custom panic hook to print a more user-friendly message 106 | // in case the execution fails. 107 | // We skip this for people that are interested in the panic message. 108 | if env::var("RUST_BACKTRACE").unwrap_or(String::new()) != "1" { 109 | panic::set_hook(Box::new(|info| { 110 | let message = if let Some(m) = info.payload().downcast_ref::() { 111 | m 112 | } else if let Some(m) = info.payload().downcast_ref::<&str>() { 113 | m 114 | } else { 115 | "No error message provided" 116 | }; 117 | eprintln!("Execution failed: {}", message); 118 | process::exit(1); 119 | })); 120 | } 121 | 122 | let external_help = external_help(); 123 | let app = init_app().after_help(external_help.as_str()); 124 | let args = app.clone().get_matches(); 125 | 126 | // Enable logging in verbose mode. 127 | match args.is_present("verbose") { 128 | true => setup_logger(log::LevelFilter::Trace), 129 | false => setup_logger(log::LevelFilter::Warn), 130 | } 131 | 132 | match args.subcommand() { 133 | ("address", Some(ref m)) => cmd::address::execute(&m), 134 | ("bech32", Some(ref m)) => cmd::bech32::execute(&m), 135 | ("bip32", Some(ref m)) => cmd::bip32::execute(&m), 136 | ("bip39", Some(ref m)) => cmd::bip39::execute(&m), 137 | ("block", Some(ref m)) => cmd::block::execute(&m), 138 | ("hash", Some(ref m)) => cmd::hash::execute(&m), 139 | ("key", Some(ref m)) => cmd::key::execute(&m), 140 | ("ln", Some(ref m)) => cmd::ln::execute(&m), 141 | ("merkle", Some(ref m)) => cmd::merkle::execute(&m), 142 | ("message", Some(ref m)) => cmd::message::execute(&m), 143 | ("miniscript", Some(ref m)) => cmd::miniscript::execute(&m), 144 | ("psbt", Some(ref m)) => cmd::psbt::execute(&m), 145 | ("random", Some(ref m)) => cmd::random::execute(&m), 146 | ("script", Some(ref m)) => cmd::script::execute(&m), 147 | ("tx", Some(ref m)) => cmd::tx::execute(&m), 148 | (cmd, subcommand_args) => { 149 | // Try execute an external subcommand. 150 | 151 | let command_exe = format!("hal-{}{}", cmd, env::consts::EXE_SUFFIX); 152 | let path = util::search_directories() 153 | .iter() 154 | .map(|dir| dir.join(&command_exe)) 155 | .find(|file| util::is_executable(file)); 156 | let command = match path { 157 | Some(command) => command, 158 | None => { 159 | if let Some(closest) = util::find_closest(cmd) { 160 | exit!("no such subcommand: `{}`\n\n\tDid you mean `{}`?\n", cmd, closest); 161 | } 162 | exit!("no such subcommand: `{}`", cmd); 163 | } 164 | }; 165 | 166 | let mut ext_args: Vec<&str> = vec![cmd]; 167 | if let Some(args) = subcommand_args { 168 | ext_args.extend(args.values_of("").unwrap_or_default()); 169 | } 170 | 171 | info!("Delegating to external executable '{}'", command.as_path().display()); 172 | process_builder::process(&command).args(&ext_args).exec_replace(); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/bin/hal/process_builder.rs: -------------------------------------------------------------------------------- 1 | // This file is copied from the Cargo project and adapted only to remove 2 | // unnecessary parts. 3 | // Cargo is primarily distributed under the terms of both the MIT license and 4 | // the Apache License (Version 2.0). 5 | #![allow(unused)] 6 | 7 | use std::collections::HashMap; 8 | use std::env; 9 | use std::ffi::{OsStr, OsString}; 10 | use std::fmt; 11 | use std::path::Path; 12 | use std::process::Command; 13 | 14 | use jobserver::Client; 15 | use shell_escape::escape; 16 | 17 | use crate::prelude::*; 18 | 19 | /// A builder object for an external process, similar to `std::process::Command`. 20 | #[derive(Clone, Debug)] 21 | pub struct ProcessBuilder { 22 | /// The program to execute. 23 | program: OsString, 24 | /// A list of arguments to pass to the program. 25 | args: Vec, 26 | /// Any environment variables that should be set for the program. 27 | env: HashMap>, 28 | /// The directory to run the program from. 29 | cwd: Option, 30 | /// The `make` jobserver. See the [jobserver crate][jobserver_docs] for 31 | /// more information. 32 | /// 33 | /// [jobserver_docs]: https://docs.rs/jobserver/0.1.6/jobserver/ 34 | jobserver: Option, 35 | /// `true` to include environment variable in display. 36 | display_env_vars: bool, 37 | } 38 | 39 | impl fmt::Display for ProcessBuilder { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | write!(f, "`")?; 42 | 43 | if self.display_env_vars { 44 | for (key, val) in self.env.iter() { 45 | if let Some(val) = val { 46 | let val = escape(val.to_string_lossy()); 47 | if cfg!(windows) { 48 | write!(f, "set {}={}&& ", key, val)?; 49 | } else { 50 | write!(f, "{}={} ", key, val)?; 51 | } 52 | } 53 | } 54 | } 55 | 56 | write!(f, "{}", self.program.to_string_lossy())?; 57 | 58 | for arg in &self.args { 59 | write!(f, " {}", escape(arg.to_string_lossy()))?; 60 | } 61 | 62 | write!(f, "`") 63 | } 64 | } 65 | 66 | impl ProcessBuilder { 67 | /// (chainable) Sets the executable for the process. 68 | pub fn program>(&mut self, program: T) -> &mut ProcessBuilder { 69 | self.program = program.as_ref().to_os_string(); 70 | self 71 | } 72 | 73 | /// (chainable) Adds `arg` to the args list. 74 | pub fn arg>(&mut self, arg: T) -> &mut ProcessBuilder { 75 | self.args.push(arg.as_ref().to_os_string()); 76 | self 77 | } 78 | 79 | /// (chainable) Adds multiple `args` to the args list. 80 | pub fn args>(&mut self, args: &[T]) -> &mut ProcessBuilder { 81 | self.args.extend(args.iter().map(|t| t.as_ref().to_os_string())); 82 | self 83 | } 84 | 85 | /// (chainable) Replaces the args list with the given `args`. 86 | pub fn args_replace>(&mut self, args: &[T]) -> &mut ProcessBuilder { 87 | self.args = args.iter().map(|t| t.as_ref().to_os_string()).collect(); 88 | self 89 | } 90 | 91 | /// (chainable) Sets the current working directory of the process. 92 | pub fn cwd>(&mut self, path: T) -> &mut ProcessBuilder { 93 | self.cwd = Some(path.as_ref().to_os_string()); 94 | self 95 | } 96 | 97 | /// (chainable) Sets an environment variable for the process. 98 | pub fn env>(&mut self, key: &str, val: T) -> &mut ProcessBuilder { 99 | self.env.insert(key.to_string(), Some(val.as_ref().to_os_string())); 100 | self 101 | } 102 | 103 | /// (chainable) Unsets an environment variable for the process. 104 | pub fn env_remove(&mut self, key: &str) -> &mut ProcessBuilder { 105 | self.env.insert(key.to_string(), None); 106 | self 107 | } 108 | 109 | /// Gets the executable name. 110 | pub fn get_program(&self) -> &OsString { 111 | &self.program 112 | } 113 | 114 | /// Gets the program arguments. 115 | pub fn get_args(&self) -> &[OsString] { 116 | &self.args 117 | } 118 | 119 | /// Gets the current working directory for the process. 120 | pub fn get_cwd(&self) -> Option<&Path> { 121 | self.cwd.as_ref().map(Path::new) 122 | } 123 | 124 | /// Gets an environment variable as the process will see it (will inherit from environment 125 | /// unless explicitally unset). 126 | pub fn get_env(&self, var: &str) -> Option { 127 | self.env.get(var).cloned().or_else(|| Some(env::var_os(var))).and_then(|s| s) 128 | } 129 | 130 | /// Gets all environment variables explicitly set or unset for the process (not inherited 131 | /// vars). 132 | pub fn get_envs(&self) -> &HashMap> { 133 | &self.env 134 | } 135 | 136 | /// Sets the `make` jobserver. See the [jobserver crate][jobserver_docs] for 137 | /// more information. 138 | /// 139 | /// [jobserver_docs]: https://docs.rs/jobserver/0.1.6/jobserver/ 140 | pub fn inherit_jobserver(&mut self, jobserver: &Client) -> &mut Self { 141 | self.jobserver = Some(jobserver.clone()); 142 | self 143 | } 144 | 145 | /// Enables environment variable display. 146 | pub fn display_env_vars(&mut self) -> &mut Self { 147 | self.display_env_vars = true; 148 | self 149 | } 150 | 151 | /// Runs the process, waiting for completion, and mapping non-success exit codes to an error. 152 | pub fn exec(&self) { 153 | let mut command = self.build_command(); 154 | let exit = command.status().need(&format!("could not execute process {}", self)); 155 | 156 | if !exit.success() { 157 | exit!("process didn't exit successfully: {}", self); 158 | } 159 | } 160 | 161 | /// Replaces the current process with the target process. 162 | /// 163 | /// On Unix, this executes the process using the Unix syscall `execvp`, which will block 164 | /// this process, and will only return if there is an error. 165 | /// 166 | /// On Windows this isn't technically possible. Instead we emulate it to the best of our 167 | /// ability. One aspect we fix here is that we specify a handler for the Ctrl-C handler. 168 | /// In doing so (and by effectively ignoring it) we should emulate proxying Ctrl-C 169 | /// handling to the application at hand, which will either terminate or handle it itself. 170 | /// According to Microsoft's documentation at 171 | /// . 172 | /// the Ctrl-C signal is sent to all processes attached to a terminal, which should 173 | /// include our child process. If the child terminates then we'll reap them in Cargo 174 | /// pretty quickly, and if the child handles the signal then we won't terminate 175 | /// (and we shouldn't!) until the process itself later exits. 176 | pub fn exec_replace(&self) { 177 | imp::exec_replace(self) 178 | } 179 | 180 | /// Executes the process, returning the stdio output, or an error if non-zero exit status. 181 | pub fn exec_with_output(&self) { 182 | let mut command = self.build_command(); 183 | 184 | let output = command.output().need(&format!("could not execute process {}", self)); 185 | 186 | if !output.status.success() { 187 | exit!("process didn't exit successfully: {}", self); 188 | } 189 | } 190 | 191 | /// Converts `ProcessBuilder` into a `std::process::Command`, and handles the jobserver, if 192 | /// present. 193 | pub fn build_command(&self) -> Command { 194 | let mut command = Command::new(&self.program); 195 | if let Some(cwd) = self.get_cwd() { 196 | command.current_dir(cwd); 197 | } 198 | for arg in &self.args { 199 | command.arg(arg); 200 | } 201 | for (k, v) in &self.env { 202 | match *v { 203 | Some(ref v) => { 204 | command.env(k, v); 205 | } 206 | None => { 207 | command.env_remove(k); 208 | } 209 | } 210 | } 211 | if let Some(ref c) = self.jobserver { 212 | c.configure(&mut command); 213 | } 214 | command 215 | } 216 | } 217 | 218 | /// A helper function to create a `ProcessBuilder`. 219 | pub fn process>(cmd: T) -> ProcessBuilder { 220 | ProcessBuilder { 221 | program: cmd.as_ref().to_os_string(), 222 | args: Vec::new(), 223 | cwd: None, 224 | env: HashMap::new(), 225 | jobserver: None, 226 | display_env_vars: false, 227 | } 228 | } 229 | 230 | #[cfg(unix)] 231 | mod imp { 232 | use super::ProcessBuilder; 233 | use std::os::unix::process::CommandExt; 234 | 235 | pub fn exec_replace(process_builder: &ProcessBuilder) { 236 | let mut command = process_builder.build_command(); 237 | command.exec(); 238 | } 239 | } 240 | 241 | //TODO(stevenroose) test this 242 | #[cfg(windows)] 243 | mod imp { 244 | use crate::util::{process_error, ProcessBuilder}; 245 | use crate::CargoResult; 246 | use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE}; 247 | use winapi::um::consoleapi::SetConsoleCtrlHandler; 248 | 249 | unsafe extern "system" fn ctrlc_handler(_: DWORD) -> BOOL { 250 | // Do nothing; let the child process handle it. 251 | TRUE 252 | } 253 | 254 | pub fn exec_replace(process_builder: &ProcessBuilder) -> CargoResult<()> { 255 | unsafe { 256 | if SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE) == FALSE { 257 | return Err(process_error("Could not set Ctrl-C handler.", None, None).into()); 258 | } 259 | } 260 | 261 | // Just execute the process as normal. 262 | process_builder.exec() 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/bin/hal/util.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp, env, fmt, fs, io}; 2 | use std::borrow::Cow; 3 | use std::collections::BTreeSet; 4 | use std::io::Read; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use crate::prelude::*; 8 | 9 | #[derive(PartialEq, PartialOrd, Eq, Ord)] 10 | pub enum CommandInfo { 11 | BuiltIn { 12 | name: String, 13 | }, 14 | External { 15 | name: String, 16 | path: PathBuf, 17 | }, 18 | } 19 | 20 | impl CommandInfo { 21 | pub fn name(&self) -> String { 22 | match self { 23 | CommandInfo::BuiltIn { 24 | name, 25 | } => name.to_string(), 26 | CommandInfo::External { 27 | name, 28 | .. 29 | } => name.to_string(), 30 | } 31 | } 32 | } 33 | 34 | /// Get the named argument from the CLI arguments or try read from stdin if not provided. 35 | pub fn arg_or_stdin<'a>(args: &'a clap::ArgMatches<'a>, arg: &str) -> Cow<'a, str> { 36 | if let Some(s) = args.value_of(arg) { 37 | s.into() 38 | } else { 39 | // Read from stdin. 40 | let mut input = Vec::new(); 41 | let stdin = io::stdin(); 42 | let mut stdin_lock = stdin.lock(); 43 | let _ = stdin_lock.read_to_end(&mut input); 44 | while stdin_lock.read_to_end(&mut input).unwrap_or(0) > 0 {} 45 | if input.is_empty() { 46 | exit!("no '{}' argument given", arg); 47 | } 48 | String::from_utf8(input).need(&format!("invalid utf8 on stdin for '{}'", arg)) 49 | .trim().to_owned().into() 50 | } 51 | } 52 | 53 | /// Return all directories in which to search for external executables. 54 | pub fn search_directories() -> Vec { 55 | let mut dirs = Vec::new(); 56 | if let Some(val) = env::var_os("PATH") { 57 | dirs.extend(env::split_paths(&val)); 58 | } 59 | dirs 60 | } 61 | 62 | #[cfg(unix)] 63 | pub fn is_executable>(path: P) -> bool { 64 | use std::os::unix::prelude::*; 65 | fs::metadata(path) 66 | .map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) 67 | .unwrap_or(false) 68 | } 69 | #[cfg(windows)] 70 | pub fn is_executable>(path: P) -> bool { 71 | fs::metadata(path).map(|metadata| metadata.is_file()).unwrap_or(false) 72 | } 73 | 74 | /// List all runnable commands 75 | pub fn list_commands() -> BTreeSet { 76 | let prefix = "hal-"; 77 | let suffix = env::consts::EXE_SUFFIX; 78 | let mut commands = BTreeSet::new(); 79 | 80 | for cmd in cmd::subcommands() { 81 | commands.insert(CommandInfo::BuiltIn { 82 | name: cmd.get_name().to_string(), 83 | }); 84 | } 85 | 86 | for dir in search_directories() { 87 | let entries = match fs::read_dir(dir) { 88 | Ok(entries) => entries, 89 | _ => continue, 90 | }; 91 | for entry in entries.filter_map(|e| e.ok()) { 92 | let path = entry.path(); 93 | let filename = match path.file_name().and_then(|s| s.to_str()) { 94 | Some(filename) => filename, 95 | _ => continue, 96 | }; 97 | if !filename.starts_with(prefix) || !filename.ends_with(suffix) { 98 | continue; 99 | } 100 | if is_executable(entry.path()) { 101 | let end = filename.len() - suffix.len(); 102 | commands.insert(CommandInfo::External { 103 | name: filename[prefix.len()..end].to_string(), 104 | path: path.clone(), 105 | }); 106 | } 107 | } 108 | } 109 | 110 | commands 111 | } 112 | 113 | pub fn lev_distance(me: &str, t: &str) -> usize { 114 | if me.is_empty() { 115 | return t.chars().count(); 116 | } 117 | if t.is_empty() { 118 | return me.chars().count(); 119 | } 120 | 121 | let mut dcol = (0..=t.len()).collect::>(); 122 | let mut t_last = 0; 123 | 124 | for (i, sc) in me.chars().enumerate() { 125 | let mut current = i; 126 | dcol[0] = current + 1; 127 | 128 | for (j, tc) in t.chars().enumerate() { 129 | let next = dcol[j + 1]; 130 | 131 | if sc == tc { 132 | dcol[j + 1] = current; 133 | } else { 134 | dcol[j + 1] = cmp::min(current, next); 135 | dcol[j + 1] = cmp::min(dcol[j + 1], dcol[j]) + 1; 136 | } 137 | 138 | current = next; 139 | t_last = j; 140 | } 141 | } 142 | 143 | dcol[t_last + 1] 144 | } 145 | 146 | pub fn find_closest(cmd: &str) -> Option { 147 | let cmds = list_commands(); 148 | // Only consider candidates with a lev_distance of 3 or less so we don't 149 | // suggest out-of-the-blue options. 150 | cmds.into_iter() 151 | .map(|c| c.name()) 152 | .map(|c| (lev_distance(&c, cmd), c)) 153 | .filter(|&(d, _)| d < 4) 154 | .min_by_key(|a| a.0) 155 | .map(|slot| slot.1) 156 | } 157 | 158 | /// Return true if more than one of the input booleans is true. 159 | pub fn more_than_one(bools: &[bool]) -> bool { 160 | let mut one = false; 161 | for b in bools { 162 | if *b && one { 163 | return true; 164 | } 165 | one |= *b; 166 | } 167 | false 168 | } 169 | 170 | pub trait ResultExt: Into> { 171 | #[track_caller] 172 | fn need(self, msg: &str) -> T { 173 | match self.into() { 174 | Ok(t) => t, 175 | Err(e) => exit!("{}: {}", msg, e), 176 | } 177 | } 178 | } 179 | 180 | impl ResultExt for std::result::Result {} 181 | 182 | pub trait OptionExt: Into> { 183 | #[track_caller] 184 | fn need(self, msg: &str) -> T { 185 | match self.into() { 186 | Some(t) => t, 187 | None => exit!("{}", msg), 188 | } 189 | } 190 | } 191 | 192 | impl OptionExt for std::option::Option {} 193 | 194 | #[test] 195 | fn test_lev_distance() { 196 | use std::char::{from_u32, MAX}; 197 | // Test bytelength agnosticity 198 | for c in (0u32..MAX as u32).filter_map(from_u32).map(|i| i.to_string()) { 199 | assert_eq!(lev_distance(&c, &c), 0); 200 | } 201 | 202 | let a = "\nMäry häd ä little lämb\n\nLittle lämb\n"; 203 | let b = "\nMary häd ä little lämb\n\nLittle lämb\n"; 204 | let c = "Mary häd ä little lämb\n\nLittle lämb\n"; 205 | assert_eq!(lev_distance(a, b), 1); 206 | assert_eq!(lev_distance(b, a), 1); 207 | assert_eq!(lev_distance(a, c), 2); 208 | assert_eq!(lev_distance(c, a), 2); 209 | assert_eq!(lev_distance(b, c), 1); 210 | assert_eq!(lev_distance(c, b), 1); 211 | } 212 | -------------------------------------------------------------------------------- /src/bip32.rs: -------------------------------------------------------------------------------- 1 | 2 | use bitcoin::{bip32, NetworkKind}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::address; 6 | 7 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 8 | pub struct DerivationInfo { 9 | #[serde(with = "crate::serde_utils::network_kind")] 10 | pub network: NetworkKind, 11 | #[serde(skip_serializing_if = "Option::is_none")] 12 | pub master_fingerprint: Option, 13 | #[serde(skip_serializing_if = "Option::is_none")] 14 | pub path: Option, 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | pub xpriv: Option, 17 | pub xpub: bip32::Xpub, 18 | pub chain_code: bip32::ChainCode, 19 | pub identifier: bip32::XKeyIdentifier, 20 | pub fingerprint: bip32::Fingerprint, 21 | pub public_key: bitcoin::secp256k1::PublicKey, 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | pub private_key: Option, 24 | pub addresses: address::Addresses, 25 | } 26 | -------------------------------------------------------------------------------- /src/bip39.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::borrow::Cow; 3 | 4 | use bip39lib::{Language, Mnemonic}; 5 | use bitcoin::Network; 6 | use bitcoin::bip32; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::{SECP, GetInfo, HexBytes}; 10 | 11 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 12 | pub struct MnemonicInfo { 13 | pub mnemonic: String, 14 | pub entropy: HexBytes, 15 | pub entropy_bits: usize, 16 | pub language: &'static str, 17 | pub passphrase: String, 18 | pub seed: SeedInfo, 19 | } 20 | 21 | impl MnemonicInfo { 22 | pub fn from_mnemonic_with_passphrase( 23 | mnemonic: &Mnemonic, 24 | passphrase: &str, 25 | network: Network, 26 | ) -> MnemonicInfo { 27 | let entropy: Vec = mnemonic.to_entropy().into(); 28 | MnemonicInfo { 29 | mnemonic: mnemonic.to_string(), 30 | entropy_bits: entropy.len() * 8, 31 | entropy: entropy.into(), 32 | language: match mnemonic.language() { 33 | Language::English => "english", 34 | Language::Czech => "czech", 35 | Language::French => "french", 36 | Language::Italian => "italian", 37 | Language::Japanese => "japanese", 38 | Language::Korean => "korean", 39 | Language::Portuguese => "portuguese", 40 | Language::Spanish => "spanish", 41 | Language::SimplifiedChinese => "simplified-chinese", 42 | Language::TraditionalChinese => "traditional-chinese", 43 | }, 44 | passphrase: passphrase.to_owned(), 45 | seed: GetInfo::get_info(&mnemonic.to_seed(passphrase), network), 46 | } 47 | } 48 | } 49 | 50 | impl GetInfo for Mnemonic { 51 | fn get_info(&self, network: Network) -> MnemonicInfo { 52 | MnemonicInfo::from_mnemonic_with_passphrase(self, "", network) 53 | } 54 | } 55 | 56 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 57 | pub struct SeedInfo { 58 | pub seed: HexBytes, 59 | pub bip32_xpriv: bip32::Xpriv, 60 | pub bip32_xpub: bip32::Xpub, 61 | } 62 | 63 | impl GetInfo for [u8; 64] { 64 | fn get_info(&self, network: Network) -> SeedInfo { 65 | let xpriv = bip32::Xpriv::new_master(network, &self[..]).unwrap(); 66 | let xpub = 67 | bip32::Xpub::from_priv(&SECP, &xpriv); 68 | SeedInfo { 69 | seed: self.to_vec().into(), 70 | bip32_xpriv: xpriv, 71 | bip32_xpub: xpub, 72 | } 73 | } 74 | } 75 | 76 | /// Parse a BIP-39 language from string. 77 | /// 78 | /// Supported formats are (case-insensitive): 79 | /// - full name in English 80 | /// - full name in English with hyphen instead of space 81 | /// - ISO 639-1 code 82 | /// - except for Simplified Chinese: "sc" or "zhs" 83 | /// - except for Traditional Chinese: "tc" or "zht" 84 | pub fn parse_language(s: &str) -> Option { 85 | if !s.is_ascii() { 86 | return None; 87 | } 88 | 89 | let s = if s.chars().all(|c| c.is_lowercase()) { 90 | Cow::Borrowed(s) 91 | } else { 92 | Cow::Owned(s.to_lowercase()) 93 | }; 94 | let ret = match s.as_ref() { 95 | "en" | "english" => Language::English, 96 | "sc" | "zhs" | "simplified chinese" | "simplified-chinese" 97 | | "simplifiedchinese" => Language::SimplifiedChinese, 98 | "tc" | "zht" | "traditional chinese"| "traditional-chinese" 99 | | "traditionalchinese" => Language::TraditionalChinese, 100 | "cs" | "czech" => Language::Czech, 101 | "fr" | "french" => Language::French, 102 | "it" | "italian" => Language::Italian, 103 | "ja" | "japanese" => Language::Japanese, 104 | "ko" | "korean" => Language::Korean, 105 | "pt" | "portuguese" => Language::Portuguese, 106 | "es" | "spanish" => Language::Spanish, 107 | _ => return None, 108 | }; 109 | Some(ret) 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | 116 | #[test] 117 | fn test_parse_language() { 118 | // a simple check all 119 | for l in Language::ALL { 120 | assert_eq!(Some(*l), parse_language(&l.to_string()), "lang: {}", l); 121 | assert_eq!(Some(*l), parse_language(&l.to_string().to_uppercase()), "lang: {}", l); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/block.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::{block, Block, BlockHash, Network, TxMerkleNode, Txid}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::tx::TransactionInfo; 5 | use crate::{GetInfo, HexBytes}; 6 | 7 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 8 | pub struct BlockHeaderInfo { 9 | #[serde(skip_serializing_if = "Option::is_none")] 10 | pub block_hash: Option, 11 | pub version: i32, 12 | pub previous_block_hash: BlockHash, 13 | pub merkle_root: TxMerkleNode, 14 | pub time: u32, 15 | pub bits: u32, 16 | pub nonce: u32, 17 | } 18 | 19 | impl<'a> GetInfo for block::Header { 20 | fn get_info(&self, _network: Network) -> BlockHeaderInfo { 21 | BlockHeaderInfo { 22 | block_hash: Some(self.block_hash()), 23 | version: self.version.to_consensus(), 24 | previous_block_hash: self.prev_blockhash, 25 | merkle_root: self.merkle_root, 26 | time: self.time, 27 | bits: self.bits.to_consensus(), 28 | nonce: self.nonce, 29 | } 30 | } 31 | } 32 | 33 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 34 | pub struct BlockInfo { 35 | pub header: BlockHeaderInfo, 36 | pub bip34_block_height: Option, 37 | #[serde(skip_serializing_if = "Option::is_none")] 38 | pub transactions: Option>, 39 | 40 | #[serde(skip_serializing_if = "Option::is_none")] 41 | pub txids: Option>, 42 | #[serde(skip_serializing_if = "Option::is_none")] 43 | pub raw_transactions: Option>, 44 | } 45 | 46 | impl GetInfo for Block { 47 | fn get_info(&self, network: Network) -> BlockInfo { 48 | BlockInfo { 49 | header: self.header.get_info(network), 50 | bip34_block_height: self.bip34_block_height().ok(), 51 | transactions: Some(self.txdata.iter().map(|t| t.get_info(network)).collect()), 52 | txids: None, 53 | raw_transactions: None, 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/key.rs: -------------------------------------------------------------------------------- 1 | 2 | use bitcoin::{secp256k1, Network, PrivateKey, PublicKey, XOnlyPublicKey}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{SECP, address, GetInfo, HexBytes}; 6 | 7 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 8 | pub struct KeyInfo { 9 | pub raw_private_key: HexBytes, 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub wif_private_key: Option, 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub uncompressed_wif_private_key: Option, 14 | pub public_key: PublicKey, 15 | pub xonly_public_key: XOnlyPublicKey, 16 | pub uncompressed_public_key: PublicKey, 17 | pub addresses: address::Addresses, 18 | } 19 | 20 | impl GetInfo for PrivateKey { 21 | fn get_info(&self, network: Network) -> KeyInfo { 22 | let pubkey = self.public_key(&SECP); 23 | let mut compressed_wif_privkey = *self; 24 | compressed_wif_privkey.compressed = true; 25 | let mut uncompressed_wif_privkey = *self; 26 | uncompressed_wif_privkey.compressed = false; 27 | KeyInfo { 28 | raw_private_key: (&self.inner[..]).into(), 29 | wif_private_key: Some(compressed_wif_privkey), 30 | uncompressed_wif_private_key: Some(uncompressed_wif_privkey), 31 | public_key: pubkey, 32 | xonly_public_key: pubkey.inner.into(), 33 | uncompressed_public_key: { 34 | let mut uncompressed = pubkey.clone(); 35 | uncompressed.compressed = false; 36 | uncompressed 37 | }, 38 | addresses: address::Addresses::from_pubkey(&pubkey, network), 39 | } 40 | } 41 | } 42 | 43 | impl GetInfo for secp256k1::SecretKey { 44 | fn get_info(&self, network: Network) -> KeyInfo { 45 | let pubkey = secp256k1::PublicKey::from_secret_key(&SECP, self); 46 | let btc_pubkey = PublicKey { 47 | compressed: true, 48 | inner: pubkey.clone(), 49 | }; 50 | KeyInfo { 51 | raw_private_key: self[..].into(), 52 | wif_private_key: None, 53 | uncompressed_wif_private_key: None, 54 | public_key: btc_pubkey, 55 | xonly_public_key: pubkey.into(), 56 | uncompressed_public_key: PublicKey { 57 | compressed: false, 58 | inner: pubkey, 59 | }, 60 | addresses: address::Addresses::from_pubkey(&btc_pubkey, network), 61 | } 62 | } 63 | } 64 | 65 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 66 | pub struct PublicKeyInfo { 67 | pub public_key: PublicKey, 68 | pub uncompressed_public_key: PublicKey, 69 | pub addresses: address::Addresses, 70 | } 71 | 72 | impl GetInfo for PublicKey { 73 | fn get_info(&self, network: Network) -> PublicKeyInfo { 74 | PublicKeyInfo { 75 | public_key: { 76 | let mut key = self.clone(); 77 | key.compressed = true; 78 | key 79 | }, 80 | uncompressed_public_key: { 81 | let mut key = self.clone(); 82 | key.compressed = false; 83 | key 84 | }, 85 | addresses: address::Addresses::from_pubkey(&self, network), 86 | } 87 | } 88 | } 89 | 90 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 91 | pub struct EcdsaSignatureInfo { 92 | pub der: HexBytes, 93 | pub compact: HexBytes, 94 | } 95 | 96 | impl GetInfo for secp256k1::ecdsa::Signature { 97 | fn get_info(&self, _network: Network) -> EcdsaSignatureInfo { 98 | EcdsaSignatureInfo { 99 | der: self.serialize_der().as_ref().into(), 100 | compact: self.serialize_compact().to_vec().into(), 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate bip39 as bip39lib; 2 | extern crate bitcoin; 3 | extern crate byteorder; 4 | extern crate chrono; 5 | extern crate hex; 6 | #[macro_use] 7 | extern crate lazy_static; 8 | extern crate lightning_invoice; 9 | extern crate miniscript as miniscriptlib; 10 | extern crate secp256k1; 11 | extern crate serde; 12 | 13 | pub mod address; 14 | pub mod bech32; 15 | pub mod bip32; 16 | pub mod bip39; 17 | pub mod block; 18 | pub mod key; 19 | pub mod lightning; 20 | pub mod message; 21 | pub mod miniscript; 22 | pub mod psbt; 23 | pub mod tx; 24 | mod serde_utils; 25 | pub use serde_utils::HexBytes; 26 | 27 | use bitcoin::Network; 28 | 29 | lazy_static! { 30 | /// A global secp256k1 context. 31 | pub static ref SECP: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); 32 | } 33 | 34 | 35 | /// Get JSON-able objects that describe the type. 36 | pub trait GetInfo { 37 | /// Get a description of this object given the network of interest. 38 | fn get_info(&self, network: Network) -> T; 39 | } 40 | -------------------------------------------------------------------------------- /src/lightning.rs: -------------------------------------------------------------------------------- 1 | 2 | use bitcoin::{address, Address, Network}; 3 | use bitcoin::hashes::{sha256, Hash}; 4 | use chrono::{offset::Local, DateTime, Duration}; 5 | use lightning_invoice::{Bolt11Invoice, Currency, Bolt11InvoiceDescription, RouteHintHop}; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::{GetInfo, HexBytes}; 9 | 10 | const WRONG_CID: &'static str = "incorrect short channel ID HRF format"; 11 | 12 | /// Parse a short channel is in the form of `${blockheight)x$(txindex}x${outputindex}`. 13 | pub fn parse_short_channel_id(cid: &str) -> Result { 14 | let mut split = cid.split("x"); 15 | let blocknum: u64 = split.next().ok_or(WRONG_CID)?.parse().map_err(|_| WRONG_CID)?; 16 | if blocknum & 0xFFFFFF != blocknum { 17 | return Err(WRONG_CID); 18 | } 19 | let txnum: u64 = split.next().ok_or(WRONG_CID)?.parse().map_err(|_| WRONG_CID)?; 20 | if txnum & 0xFFFFFF != txnum { 21 | return Err(WRONG_CID); 22 | } 23 | let outnum: u64 = split.next().ok_or(WRONG_CID)?.parse().map_err(|_| WRONG_CID)?; 24 | if outnum & 0xFFFF != outnum { 25 | return Err(WRONG_CID); 26 | } 27 | Ok(blocknum << 40 | txnum << 16 | outnum) 28 | } 29 | 30 | /// Parse a short channel is in the form of `${blockheight)x$(txindex}x${outputindex}`. 31 | pub fn fmt_short_channel_id(cid: u64) -> String { 32 | let blocknum = cid >> 40; 33 | let txnum = cid >> 16 & 0x00FFFFFF; 34 | let outnum = cid & 0xFFFF; 35 | format!("{}x{}x{}", blocknum, txnum, outnum) 36 | } 37 | 38 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 39 | pub struct RouteHopInfo { 40 | pub src_node_id: HexBytes, 41 | pub short_channel_id: u64, 42 | pub short_channel_id_hex: HexBytes, 43 | pub short_channel_id_hrf: String, 44 | pub fee_base_msat: u32, 45 | pub fee_proportional_millionths: u32, 46 | pub cltv_expiry_delta: u16, 47 | } 48 | 49 | impl GetInfo for RouteHintHop { 50 | fn get_info(&self, _network: Network) -> RouteHopInfo { 51 | RouteHopInfo { 52 | src_node_id: self.src_node_id.serialize()[..].into(), 53 | short_channel_id: self.short_channel_id, 54 | short_channel_id_hex: self.short_channel_id.to_be_bytes()[..].into(), 55 | short_channel_id_hrf: fmt_short_channel_id(self.short_channel_id), 56 | fee_base_msat: self.fees.base_msat, 57 | fee_proportional_millionths: self.fees.proportional_millionths, 58 | cltv_expiry_delta: self.cltv_expiry_delta, 59 | } 60 | } 61 | } 62 | 63 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 64 | pub struct InvoiceInfo { 65 | pub timestamp: DateTime, 66 | pub payment_hash: sha256::Hash, 67 | pub description: String, 68 | #[serde(skip_serializing_if = "Option::is_none")] 69 | pub payee_pub_key: Option, 70 | #[serde(skip_serializing_if = "Option::is_none")] 71 | pub expiry_time: Option>, 72 | #[serde(skip_serializing_if = "Option::is_none")] 73 | pub min_final_cltv_expiry: Option, 74 | #[serde(skip_serializing_if = "Vec::is_empty")] 75 | pub fallback_addresses: Vec>, 76 | #[serde(skip_serializing_if = "Vec::is_empty")] 77 | pub routes: Vec>, 78 | pub currency: String, 79 | 80 | // For signed invoices. 81 | pub signature: HexBytes, 82 | pub signature_recover_id: i32, 83 | pub payee_pubkey: Option, 84 | } 85 | 86 | impl GetInfo for Bolt11Invoice { 87 | fn get_info(&self, network: Network) -> InvoiceInfo { 88 | let signed_raw = self.clone().into_signed_raw(); 89 | let (sig_rec, sig) = signed_raw.signature().0.serialize_compact(); 90 | 91 | InvoiceInfo { 92 | timestamp: self.timestamp().clone().into(), 93 | payment_hash: sha256::Hash::from_slice(&self.payment_hash()[..]).unwrap(), 94 | description: match self.description() { 95 | Bolt11InvoiceDescription::Direct(s) => s.clone().into_inner().0, 96 | Bolt11InvoiceDescription::Hash(h) => h.0.to_string(), 97 | }, 98 | payee_pub_key: self.payee_pub_key().map(|pk| pk.serialize()[..].into()), 99 | expiry_time: Some( 100 | Local::now() + Duration::from_std(self.expiry_time()) 101 | .expect("invalid expiry in invoice"), 102 | ), 103 | min_final_cltv_expiry: Some(self.min_final_cltv_expiry_delta()), 104 | fallback_addresses: self.fallback_addresses().into_iter() 105 | .map(|a| a.to_string().parse().unwrap()).collect(), 106 | routes: self.route_hints() 107 | .iter().map(|r| r.0.iter().map(|h| GetInfo::get_info(h, network)).collect()) 108 | .collect(), 109 | currency: match self.currency() { 110 | Currency::Bitcoin => "bitcoin".to_owned(), 111 | Currency::BitcoinTestnet => "bitcoin-testnet".to_owned(), 112 | Currency::Regtest => "bitcoin-regtest".to_owned(), 113 | Currency::Simnet => "bitcoin-simnet".to_owned(), 114 | Currency::Signet => "bitcoin-signet".to_owned(), 115 | }, 116 | signature: sig[..].into(), 117 | signature_recover_id: sig_rec.to_i32(), 118 | payee_pubkey: signed_raw 119 | .recover_payee_pub_key() 120 | .ok() 121 | .map(|s| s.0.serialize()[..].into()), 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/message.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::hashes::{sha256, sha256d}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 5 | pub struct MessageHash { 6 | pub sha256: sha256::Hash, 7 | pub sha256d: sha256d::Hash, 8 | pub sign_hash: sha256d::Hash, 9 | } 10 | -------------------------------------------------------------------------------- /src/miniscript.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::HexBytes; 4 | 5 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 6 | #[serde(rename_all = "snake_case")] 7 | pub enum MiniscriptKeyType { 8 | PublicKey, 9 | String, 10 | } 11 | 12 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 13 | pub struct ScriptContexts { 14 | pub bare: bool, // in bare script pubkey 15 | pub p2sh: bool, 16 | pub segwitv0: bool, 17 | } 18 | 19 | impl ScriptContexts { 20 | 21 | pub fn from_bare(bare: bool) -> Self { 22 | Self { 23 | bare: bare, 24 | p2sh: false, 25 | segwitv0: false, 26 | } 27 | } 28 | 29 | pub fn from_p2sh(p2sh: bool) -> Self { 30 | Self { 31 | bare: false, 32 | p2sh: p2sh, 33 | segwitv0: false, 34 | } 35 | } 36 | 37 | pub fn from_segwitv0(segwitv0: bool) -> Self { 38 | Self { 39 | bare: false, 40 | p2sh: false, 41 | segwitv0: segwitv0, 42 | } 43 | } 44 | 45 | pub fn or(a: Self, b: Self) -> Self { 46 | Self { 47 | bare: a.bare || b.bare, 48 | p2sh: a.p2sh || b.p2sh, 49 | segwitv0: a.segwitv0 || b.segwitv0, 50 | } 51 | } 52 | } 53 | 54 | #[derive(Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize)] 55 | pub struct Miniscripts { 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | pub bare: Option, 58 | #[serde(skip_serializing_if = "Option::is_none")] 59 | pub p2sh: Option, 60 | #[serde(skip_serializing_if = "Option::is_none")] 61 | pub segwitv0: Option, 62 | // Taproot to come 63 | } 64 | 65 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 66 | pub struct DescriptorInfo { 67 | pub descriptor: String, 68 | pub key_type: MiniscriptKeyType, 69 | #[serde(skip_serializing_if = "Option::is_none")] 70 | pub address: Option, 71 | #[serde(skip_serializing_if = "Option::is_none")] 72 | pub script_pubkey: Option, 73 | #[serde(skip_serializing_if = "Option::is_none")] 74 | pub unsigned_script_sig: Option, 75 | #[serde(skip_serializing_if = "Option::is_none")] 76 | pub witness_script: Option, 77 | #[serde(skip_serializing_if = "Option::is_none")] 78 | pub max_satisfaction_weight: Option, 79 | #[serde(skip_serializing_if = "Option::is_none")] 80 | pub policy: Option, 81 | } 82 | 83 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 84 | pub struct MiniscriptInfo { 85 | pub key_type: MiniscriptKeyType, 86 | pub valid_script_contexts: ScriptContexts, 87 | pub requires_sig: bool, 88 | pub has_mixed_timelocks: bool, 89 | pub has_repeated_keys: bool, 90 | pub non_malleable: ScriptContexts, 91 | pub within_resource_limits: ScriptContexts, 92 | pub sane_miniscript: ScriptContexts, 93 | pub script_size: usize, 94 | #[serde(skip_serializing_if = "Option::is_none")] 95 | pub max_satisfaction_witness_elements: Option, 96 | #[serde(skip_serializing_if = "Option::is_none")] 97 | pub max_satisfaction_size_segwit: Option, 98 | #[serde(skip_serializing_if = "Option::is_none")] 99 | pub max_satisfaction_size_non_segwit: Option, 100 | #[serde(skip_serializing_if = "Option::is_none")] 101 | pub script: Option, 102 | #[serde(skip_serializing_if = "Option::is_none")] 103 | pub policy: Option, 104 | } 105 | 106 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 107 | pub struct PolicyInfo { 108 | pub is_concrete: bool, 109 | pub key_type: MiniscriptKeyType, 110 | pub is_trivial: bool, 111 | pub is_unsatisfiable: bool, 112 | pub relative_timelocks: Vec, 113 | pub n_keys: usize, 114 | pub minimum_n_keys: usize, 115 | pub sorted: String, 116 | pub normalized: String, 117 | #[serde(skip_serializing_if = "Option::is_none")] 118 | pub miniscript: Option, 119 | } 120 | -------------------------------------------------------------------------------- /src/psbt.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | 4 | use bitcoin::{bip32, sighash, psbt, Network}; 5 | 6 | use crate::{tx, GetInfo, HexBytes}; 7 | 8 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 9 | pub struct HDPathInfo { 10 | pub master_fingerprint: bip32::Fingerprint, 11 | pub path: bip32::DerivationPath, 12 | } 13 | 14 | pub fn sighashtype_to_string(sht: psbt::PsbtSighashType) -> &'static str { 15 | if let Ok(t) = sht.ecdsa_hash_ty() { 16 | match t { 17 | sighash::EcdsaSighashType::All => "ALL", 18 | sighash::EcdsaSighashType::None => "NONE", 19 | sighash::EcdsaSighashType::Single => "SINGLE", 20 | sighash::EcdsaSighashType::AllPlusAnyoneCanPay => "ALL|ANYONECANPAY", 21 | sighash::EcdsaSighashType::NonePlusAnyoneCanPay => "NONE|ANYONECANPAY", 22 | sighash::EcdsaSighashType::SinglePlusAnyoneCanPay => "SINGLE|ANYONECANPAY", 23 | } 24 | } else if let Ok(t) = sht.taproot_hash_ty() { 25 | match t { 26 | sighash::TapSighashType::Default => "SIGHASH_DEFAULT", 27 | sighash::TapSighashType::All => "SIGHASH_ALL", 28 | sighash::TapSighashType::None => "SIGHASH_NONE", 29 | sighash::TapSighashType::Single => "SIGHASH_SINGLE", 30 | sighash::TapSighashType::AllPlusAnyoneCanPay => "SIGHASH_ALL|SIGHASH_ANYONECANPAY", 31 | sighash::TapSighashType::NonePlusAnyoneCanPay => "SIGHASH_NONE|SIGHASH_ANYONECANPAY", 32 | sighash::TapSighashType::SinglePlusAnyoneCanPay => "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY", 33 | } 34 | } else { 35 | unreachable!(); 36 | } 37 | } 38 | 39 | pub fn sighashtype_values() -> &'static [&'static str] { 40 | &["ALL", "NONE", "SINGLE", "ALL|ANYONECANPAY", "NONE|ANYONECANPAY", "SINGLE|ANYONECANPAY"] 41 | } 42 | 43 | pub fn ecdsa_sighashtype_from_string(sht: &str) -> Result { 44 | lazy_static! { 45 | static ref ERR: &'static str = Box::leak(format!( 46 | "invalid ecdsa SIGHASH type value -- possible values: {:?}", &sighashtype_values(), 47 | ).into_boxed_str()); 48 | } 49 | 50 | use bitcoin::EcdsaSighashType::*; 51 | let ecdsa_sighash = match sht { 52 | "ALL" => All, 53 | "NONE" => None, 54 | "SINGLE" => Single, 55 | "ALL|ANYONECANPAY" => AllPlusAnyoneCanPay, 56 | "NONE|ANYONECANPAY" => NonePlusAnyoneCanPay, 57 | "SINGLE|ANYONECANPAY" => SinglePlusAnyoneCanPay, 58 | _ => return Err(&ERR), 59 | }; 60 | Ok(psbt::PsbtSighashType::from(ecdsa_sighash)) 61 | } 62 | 63 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 64 | pub struct PsbtInputInfo { 65 | #[serde(skip_serializing_if = "Option::is_none")] 66 | pub non_witness_utxo: Option, 67 | #[serde(skip_serializing_if = "Option::is_none")] 68 | pub witness_utxo: Option, 69 | #[serde(skip_serializing_if = "HashMap::is_empty")] 70 | pub partial_sigs: HashMap, 71 | #[serde(skip_serializing_if = "Option::is_none")] 72 | pub sighash_type: Option, 73 | #[serde(skip_serializing_if = "Option::is_none")] 74 | pub redeem_script: Option, 75 | #[serde(skip_serializing_if = "Option::is_none")] 76 | pub witness_script: Option, 77 | #[serde(skip_serializing_if = "HashMap::is_empty")] 78 | pub bip32_derivation: HashMap, 79 | #[deprecated(since = "0.9.5", note = "use bip32_derivation instead")] 80 | #[serde(skip_serializing_if = "HashMap::is_empty")] 81 | pub hd_keypaths: HashMap, 82 | #[serde(skip_serializing_if = "Option::is_none")] 83 | pub final_script_sig: Option, 84 | #[serde(skip_serializing_if = "Option::is_none")] 85 | pub final_script_witness: Option>, 86 | } 87 | 88 | impl GetInfo for psbt::Input { 89 | fn get_info(&self, network: Network) -> PsbtInputInfo { 90 | let bip32_derivation = { 91 | let mut ret = HashMap::new(); 92 | for (key, value) in self.bip32_derivation.iter() { 93 | ret.insert(key.serialize().to_vec().into(), 94 | HDPathInfo { 95 | master_fingerprint: value.0, 96 | path: value.1.clone(), 97 | }, 98 | ); 99 | } 100 | ret 101 | }; 102 | #[allow(deprecated)] // for hd_keypaths 103 | PsbtInputInfo { 104 | non_witness_utxo: self.non_witness_utxo.as_ref().map(|u| u.get_info(network)), 105 | witness_utxo: self.witness_utxo.as_ref().map(|u| u.get_info(network)), 106 | partial_sigs: { 107 | let mut partial_sigs = HashMap::new(); 108 | for (key, value) in self.partial_sigs.iter() { 109 | partial_sigs.insert(key.to_bytes().into(), value.clone().to_vec().into()); 110 | } 111 | partial_sigs 112 | }, 113 | sighash_type: self.sighash_type.map(|s| sighashtype_to_string(s).to_owned()), 114 | redeem_script: self.redeem_script.as_ref() 115 | .map(|s| tx::OutputScript(s).get_info(network)), 116 | witness_script: self.witness_script.as_ref() 117 | .map(|s| tx::OutputScript(s).get_info(network)), 118 | bip32_derivation: bip32_derivation.clone(), 119 | hd_keypaths: bip32_derivation, 120 | final_script_sig: self.final_script_sig.as_ref() 121 | .map(|s| tx::InputScript(s).get_info(network)), 122 | final_script_witness: self.final_script_witness.as_ref() 123 | .map(|w| w.iter().map(|p| p.into()).collect()), 124 | } 125 | } 126 | } 127 | 128 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 129 | pub struct PsbtOutputInfo { 130 | #[serde(skip_serializing_if = "Option::is_none")] 131 | pub redeem_script: Option, 132 | #[serde(skip_serializing_if = "Option::is_none")] 133 | pub witness_script: Option, 134 | #[serde(skip_serializing_if = "HashMap::is_empty")] 135 | pub hd_keypaths: HashMap, 136 | } 137 | 138 | impl GetInfo for psbt::Output { 139 | fn get_info(&self, network: Network) -> PsbtOutputInfo { 140 | PsbtOutputInfo { 141 | redeem_script: self.redeem_script.as_ref() 142 | .map(|s| tx::OutputScript(s).get_info(network)), 143 | witness_script: self.witness_script.as_ref() 144 | .map(|s| tx::OutputScript(s).get_info(network)), 145 | hd_keypaths: { 146 | let mut hd_keypaths = HashMap::new(); 147 | for (key, value) in self.bip32_derivation.iter() { 148 | hd_keypaths.insert(key.serialize().to_vec().into(), 149 | HDPathInfo { 150 | master_fingerprint: value.0, 151 | path: value.1.clone(), 152 | }, 153 | ); 154 | } 155 | hd_keypaths 156 | }, 157 | } 158 | } 159 | } 160 | 161 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 162 | pub struct PsbtInfo { 163 | pub unsigned_tx: tx::TransactionInfo, 164 | pub inputs: Vec, 165 | pub outputs: Vec, 166 | } 167 | 168 | impl GetInfo for psbt::Psbt { 169 | fn get_info(&self, network: Network) -> PsbtInfo { 170 | PsbtInfo { 171 | unsigned_tx: self.unsigned_tx.get_info(network), 172 | inputs: self.inputs.iter().map(|i| i.get_info(network)).collect(), 173 | outputs: self.outputs.iter().map(|o| o.get_info(network)).collect(), 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/serde_utils.rs: -------------------------------------------------------------------------------- 1 | 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 | 4 | pub mod network_kind { 5 | use super::*; 6 | use bitcoin::NetworkKind; 7 | 8 | pub fn serialize(network: &NetworkKind, s: S) -> Result { 9 | match network { 10 | NetworkKind::Main => s.serialize_str("main"), 11 | NetworkKind::Test => s.serialize_str("test"), 12 | } 13 | } 14 | 15 | pub fn deserialize<'de, D: ::serde::Deserializer<'de>>(d: D) -> Result { 16 | use serde::de::Error; 17 | 18 | match <&str>::deserialize(d)? { 19 | "main" => Ok(NetworkKind::Main), 20 | "test" => Ok(NetworkKind::Test), 21 | k => Err(D::Error::custom(format!("unknown network kind: {}", k))), 22 | } 23 | } 24 | } 25 | 26 | /// Utility struct to serialize byte strings as hex. 27 | #[derive(Clone, PartialEq, Eq, Debug, Hash)] 28 | pub struct HexBytes(pub Vec); 29 | 30 | impl HexBytes { 31 | pub fn hex(&self) -> String { 32 | hex::encode(&self.0) 33 | } 34 | 35 | pub fn bytes(&self) -> &[u8] { 36 | &self.0 37 | } 38 | 39 | pub fn take_bytes(self) -> Vec { 40 | self.0 41 | } 42 | } 43 | 44 | impl From> for HexBytes { 45 | fn from(vec: Vec) -> HexBytes { 46 | HexBytes(vec) 47 | } 48 | } 49 | 50 | impl<'a> From<&'a [u8]> for HexBytes { 51 | fn from(slice: &'a [u8]) -> HexBytes { 52 | HexBytes(slice.to_vec()) 53 | } 54 | } 55 | 56 | impl Serialize for HexBytes { 57 | fn serialize(&self, s: S) -> Result { 58 | s.serialize_str(&hex::encode(&self.0)) 59 | } 60 | } 61 | 62 | impl<'de> Deserialize<'de> for HexBytes { 63 | fn deserialize>(d: D) -> Result { 64 | use serde::de::Error; 65 | 66 | let hex_str = <&str>::deserialize(d)?; 67 | Ok(HexBytes(hex::decode(hex_str).map_err(D::Error::custom)?)) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/tx.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::consensus::encode::serialize; 2 | use bitcoin::{address, Address, Amount, Network, Script, Transaction, TxIn, TxOut, Txid, Wtxid}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{GetInfo, HexBytes}; 6 | 7 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 8 | pub struct InputScriptInfo { 9 | pub hex: Option, 10 | pub asm: Option, 11 | } 12 | 13 | pub struct InputScript<'a>(pub &'a Script); 14 | 15 | impl<'a> GetInfo for InputScript<'a> { 16 | fn get_info(&self, _network: Network) -> InputScriptInfo { 17 | InputScriptInfo { 18 | hex: Some(self.0.to_bytes().into()), 19 | asm: Some(self.0.to_asm_string()), 20 | } 21 | } 22 | } 23 | 24 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 25 | pub struct InputInfo { 26 | pub prevout: Option, 27 | pub txid: Option, 28 | pub vout: Option, 29 | pub script_sig: Option, 30 | pub sequence: Option, 31 | pub witness: Option>, 32 | } 33 | 34 | impl GetInfo for TxIn { 35 | fn get_info(&self, network: Network) -> InputInfo { 36 | InputInfo { 37 | prevout: Some(self.previous_output.to_string()), 38 | txid: Some(self.previous_output.txid), 39 | vout: Some(self.previous_output.vout), 40 | sequence: Some(self.sequence.to_consensus_u32()), 41 | script_sig: Some(InputScript(&self.script_sig).get_info(network)), 42 | witness: if self.witness.len() > 0 { 43 | Some(self.witness.iter().map(|h| h.into()).collect()) 44 | } else { 45 | None 46 | }, 47 | } 48 | } 49 | } 50 | 51 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 52 | pub struct OutputScriptInfo { 53 | pub hex: Option, 54 | pub asm: Option, 55 | #[serde(skip_serializing_if = "Option::is_none", rename = "type")] 56 | pub type_: Option, 57 | #[serde(skip_serializing_if = "Option::is_none")] 58 | pub address: Option>, 59 | } 60 | 61 | pub struct OutputScript<'a>(pub &'a Script); 62 | 63 | impl<'a> GetInfo for OutputScript<'a> { 64 | fn get_info(&self, network: Network) -> OutputScriptInfo { 65 | OutputScriptInfo { 66 | hex: Some(self.0.to_bytes().into()), 67 | asm: Some(self.0.to_asm_string()), 68 | type_: Some( 69 | if self.0.is_p2pk() { 70 | "p2pk" 71 | } else if self.0.is_p2pkh() { 72 | "p2pkh" 73 | } else if self.0.is_op_return() { 74 | "opreturn" 75 | } else if self.0.is_p2sh() { 76 | "p2sh" 77 | } else if self.0.is_p2wpkh() { 78 | "p2wpkh" 79 | } else if self.0.is_p2wsh() { 80 | "p2wsh" 81 | } else if self.0.is_p2tr() { 82 | "p2tr" 83 | } else { 84 | "unknown" 85 | } 86 | .to_owned(), 87 | ), 88 | address: Address::from_script(&self.0, network).ok().map(|a| a.as_unchecked().clone()), 89 | } 90 | } 91 | } 92 | 93 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 94 | pub struct OutputInfo { 95 | #[serde(with = "bitcoin::amount::serde::as_sat::opt")] 96 | pub value: Option, 97 | pub script_pub_key: Option, 98 | } 99 | 100 | impl GetInfo for TxOut { 101 | fn get_info(&self, network: Network) -> OutputInfo { 102 | OutputInfo { 103 | value: Some(self.value), 104 | script_pub_key: Some(OutputScript(&self.script_pubkey).get_info(network)), 105 | } 106 | } 107 | } 108 | 109 | #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] 110 | pub struct TransactionInfo { 111 | pub txid: Option, 112 | pub wtxid: Option, 113 | pub size: Option, 114 | pub weight: Option, 115 | pub vsize: Option, 116 | pub version: Option, 117 | pub locktime: Option, 118 | pub inputs: Option>, 119 | pub outputs: Option>, 120 | pub total_output_value: Option, 121 | } 122 | 123 | impl GetInfo for Transaction { 124 | fn get_info(&self, network: Network) -> TransactionInfo { 125 | let weight = self.weight().to_wu() as usize; 126 | TransactionInfo { 127 | txid: Some(self.compute_txid()), 128 | wtxid: Some(self.compute_wtxid()), 129 | version: Some(self.version.0), 130 | locktime: Some(self.lock_time.to_consensus_u32()), 131 | size: Some(serialize(self).len()), 132 | weight: Some(weight), 133 | vsize: Some(weight / 4), 134 | inputs: Some(self.input.iter().map(|i| i.get_info(network)).collect()), 135 | outputs: Some(self.output.iter().map(|o| o.get_info(network)).collect()), 136 | total_output_value: Some(self.output.iter().map(|o| o.value.to_sat()).sum()), 137 | } 138 | } 139 | } 140 | --------------------------------------------------------------------------------