diff --git a/.cargo/config b/.cargo/config index 908f2d6dde..f6764a55b4 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,2 +1,3 @@ [target.wasm32-unknown-unknown] -runner = 'wasm-bindgen-test-runner' \ No newline at end of file +runner = 'wasm-bindgen-test-runner-0.2.78' +rustflags = ["--cfg=web_sys_unstable_apis"] diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..2bd048103b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,140 @@ +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: cargo + # Define the location of the package manifests + directory: "/mm2src/coins/" + schedule: + # By default, Dependabot checks for new versions on Monday at a random set time for the repository + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/common/shared_ref_counter/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/coins/lightning_persister/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/coins/lightning_background_processor/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/coins/utxo_signer/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/coins_activation/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/crypto/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/db_common/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/derives/ser_error/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/derives/ser_error_derive/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/floodsub/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/gossipsub/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + - package-ecosystem: cargo + directory: "/mm2src/hw_common/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_bitcoin/crypto/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_bitcoin/chain/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_bitcoin/keys/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_bitcoin/rpc/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_bitcoin/primitives/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_bitcoin/script/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_bitcoin/serialization/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_bitcoin/serialization_derive/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_bitcoin/test_helpers/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_core/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_db/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_err_handle/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_test_helpers/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_libp2p/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_main/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_net/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_io/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/mm2_rpc/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/rpc_task/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/mm2src/trezor/" + schedule: + interval: weekly \ No newline at end of file diff --git a/.gitignore b/.gitignore index 02b05a90e7..7c885a57a3 100755 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,12 @@ iguana/exchanges/manychains # coins configuration file /coins +# MM2 conf to avoid occasional adding to git by someone +MM2.json + +# vim +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1429f1d6c3..cc6a24afa1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to AtomicDEX Marketmaker +# Contributing to AtomicDEX-API We welcome contribution from everyone in the form of suggestions, bug reports, pull requests, and feedback. Please note we have a code of conduct, please follow it in all your interactions with the project. @@ -14,16 +14,40 @@ cargo test --all We also use [Clippy](https://github.com/rust-lang/rust-clippy) to avoid common mistakes and we use [rustfmt](https://github.com/rust-lang/rustfmt) to make our code clear to everyone. -1. Install these tools (only once): - ``` - rustup component add rustfmt - rustup component add clippy - ``` 1. Format the code using rustfmt: - ``` + ```shell cargo fmt ``` -1. Make sure there are no warnings and errors. Run the Clippy: - ``` +2. Make sure there are no warnings and errors. Run the Clippy: + ```shell cargo clippy -- -D warnings ``` + Install cargo udeps + ```shell + cargo install cargo-udeps + ``` +3. Make sure there are no unused dependencies. Run the following check + ```shell + cargo udeps + ``` + Install cargo deny + ```shell + cargo install cargo-deny + ``` +4. Make sure that no new dependencies duplicates appear. Run the following check + ```shell + cargo deny check bans + ``` +5. Make sure that dependencies do not have known vulnerabilities. If they do, update them. + ```shell + cargo deny check advisories + ``` + +### Run WASM tests + +1. Install Firefox. +1. Download Gecko driver for your OS: https://github.com/mozilla/geckodriver/releases +1. Run the tests + ``` + WASM_BINDGEN_TEST_TIMEOUT=120 GECKODRIVER=PATH_TO_GECKO_DRIVER_BIN wasm-pack test --firefox --headless mm2src/mm2_main + ``` diff --git a/Cargo.lock b/Cargo.lock index a5ce80b61e..ec11587fa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,7 +39,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "922b33332f54fc0ad13fa3e514601e8d30fb54e1f3eadc36643f6526db645621" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", ] [[package]] @@ -55,13 +55,13 @@ dependencies = [ [[package]] name = "aes" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495ee669413bfbe9e8cace80f4d3d78e6d8c8d99579f97fb93bde351b185f2d4" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if 1.0.0", "cipher 0.3.0", - "cpufeatures", + "cpufeatures 0.2.1", "opaque-debug 0.3.0", ] @@ -72,7 +72,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" dependencies = [ "aead", - "aes 0.7.4", + "aes 0.7.5", "cipher 0.3.0", "ctr", "ghash", @@ -99,21 +99,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "ahash" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f33b5018f120946c1dcf279194f238a9f146725593ead1c08fa47ff22b0b5d3" -dependencies = [ - "const-random", -] - -[[package]] -name = "ahash" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" - [[package]] name = "ahash" version = "0.4.7" @@ -122,24 +107,39 @@ checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" [[package]] name = "ahash" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.2", + "getrandom 0.2.6", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.12" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c259a748ac706ba73d609b73fc13469e128337f9a6b2fb3cc82d100f8dd8d511" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.42" @@ -185,6 +185,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d6e24d2cce90c53b948c46271bfb053e4bdc2db9b5d3f65e20f8cf28a1b7fc3" +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "async-std" version = "1.6.2" @@ -192,23 +198,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00d68a33ebc8b57800847d00787307f84a562224a14db069b0acefe4c2abbf5d" dependencies = [ "async-task", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "futures-channel", "futures-core", "futures-io", "futures-timer", "kv-log-macro", - "log 0.4.11", + "log 0.4.14", "memchr", "num_cpus", "once_cell", - "pin-project-lite 0.1.7", + "pin-project-lite 0.1.12", "pin-utils", "slab 0.4.2", "smol", "wasm-bindgen-futures", ] +[[package]] +name = "async-stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +dependencies = [ + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + [[package]] name = "async-task" version = "3.0.0" @@ -217,13 +244,13 @@ checksum = "c17772156ef2829aadc587461c7753af20b7e8db1529bc66855add962a3b35d3" [[package]] name = "async-trait" -version = "0.1.36" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a265e3abeffdce30b2e26b7a11b222fe37c6067404001b434101457d0385eb92" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] @@ -232,20 +259,11 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0de5164e5edbf51c45fb8c2d9664ae1c095cce1b265ecf7569093c0d66ef690" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "futures-sink", "futures-util", "memchr", - "pin-project-lite 0.2.6", -] - -[[package]] -name = "atomic" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3410529e8288c463bedb5930f82833bc0c90e5d2fe639a56582a4d09220b281" -dependencies = [ - "autocfg 1.0.0", + "pin-project-lite 0.2.9", ] [[package]] @@ -266,7 +284,7 @@ dependencies = [ "byteorder 1.4.3", "bytes 0.5.6", "common", - "env_logger", + "env_logger 0.7.1", "fnv", "futures 0.3.15", "futures_codec", @@ -274,13 +292,12 @@ dependencies = [ "libp2p-plaintext", "libp2p-swarm", "libp2p-yamux", - "log 0.4.11", - "lru 0.4.3", + "log 0.4.14", "prost", "prost-build", "quickcheck", "rand 0.7.3", - "sha2 0.8.2", + "sha2 0.9.9", "smallvec 1.6.1", "unsigned-varint 0.4.0", "wasm-timer", @@ -309,6 +326,49 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" +[[package]] +name = "axum" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2504b827a8bef941ba3dd64bdffe9cf56ca182908a147edd6189c95fbcae7d" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes 1.1.0", + "futures-util", + "http 0.2.7", + "http-body 0.4.4", + "hyper", + "itoa 1.0.1", + "matchit", + "memchr", + "mime", + "percent-encoding 2.1.0", + "pin-project-lite 0.2.9", + "serde", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da31c0ed7b4690e2c78fe4b880d21cd7db04a346ebc658b4270251b695437f17" +dependencies = [ + "async-trait", + "bytes 1.1.0", + "futures-util", + "http 0.2.7", + "http-body 0.4.4", + "mime", +] + [[package]] name = "backtrace" version = "0.3.49" @@ -329,11 +389,23 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base58" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5024ee8015f02155eee35c711107ddd9a9bf3cb689cf2a9089c97e79b6e1ae83" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" [[package]] name = "base64" @@ -364,9 +436,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bech32" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c7f7096bc256f5e5cb960f60dfc4f4ef979ca65abe7fb9d5a4f77150d3783d4" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" [[package]] name = "bellman" @@ -378,10 +450,10 @@ dependencies = [ "blake2s_simd", "byteorder 1.4.3", "crossbeam", - "ff", + "ff 0.8.0", "futures 0.1.29", "futures-cpupool", - "group", + "group 0.8.0", "num_cpus", "pairing", "rand_core 0.5.1", @@ -390,86 +462,67 @@ dependencies = [ [[package]] name = "bigdecimal" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1374191e2dd25f9ae02e3aa95041ed5d747fc77b3c102b49fe2dd9a8117a6244" +checksum = "6aaf33151a6429fe9211d1b276eafdf70cdff28b071e76c0b0e1503221ea3744" dependencies = [ - "num-bigint 0.2.6", + "num-bigint 0.4.3", "num-integer", - "num-traits 0.2.12", + "num-traits", "serde", ] [[package]] -name = "bimap" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528c4b6f81eb2aadd3504da4ddc5bf5caec1b4aaf0d9dccfb8aaf2850f5b39c" - -[[package]] -name = "bitcoin-cash" -version = "0.2.3" +name = "bincode" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35904d3f449fe9a01c5b8b872015415feea85cffc18b15170ddd665db9a78a82" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" dependencies = [ - "base64 0.12.3", - "bimap", - "bitcoin-cash-base", - "bitcoin-cash-script-macro", - "bitflags", - "byteorder 1.4.3", - "error-chain", - "hex 0.4.2", - "hex-literal", - "num", - "num-derive", - "num-traits 0.2.12", - "ripemd160 0.9.1", "serde", - "serde_derive", - "serde_json", - "sha-1 0.9.4", - "sha2 0.9.5", ] [[package]] -name = "bitcoin-cash-base" -version = "0.2.0" +name = "bip32" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d008cf4ceea63e1a9182c96ac320f263c53ddc70282b523f19500f454e490db" +checksum = "c2d0f0fc59c7ba0333eed9dcc1b6980baa7b7a4dc7c6c5885994d0674f7adf34" dependencies = [ - "byteorder 1.4.3", - "hex 0.4.2", - "lazy_static", - "num", - "num-derive", - "num-traits 0.2.12", - "serde", - "serde_derive", + "bs58", + "hkd32", + "hmac 0.11.0", + "ripemd160", + "secp256k1", + "sha2 0.9.9", + "subtle 2.4.0", + "zeroize", ] [[package]] -name = "bitcoin-cash-script-macro" -version = "0.2.0" +name = "bitcoin" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3469ca4ed0b2d142528565dae7f9bba9313a3f833e771e400ef0bc2269ee8e95" +checksum = "9a41df6ad9642c5c15ae312dd3d074de38fd3eb7cc87ad4ce10f90292a83fe4d" dependencies = [ - "bitcoin-cash-base", - "proc-macro2", - "quote 1.0.7", - "regex", - "syn 1.0.72", - "tempfile", - "toolchain_find", + "bech32", + "bitcoin_hashes", + "bitcoinconsensus", + "secp256k1", ] [[package]] -name = "bitcoin-cash-slp" -version = "0.3.1" +name = "bitcoin_hashes" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" + +[[package]] +name = "bitcoinconsensus" +version = "0.19.0-3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744837d4735aab854e4722a3a2ee942aaa96c6c02de7e06594e6511ef9fba309" +checksum = "1a8aa43b5cd02f856cb126a9af819e77b8910fdd74dd1407be649f2f5fe3a1b5" dependencies = [ - "bitcoin-cash", + "cc", + "libc", ] [[package]] @@ -478,18 +531,19 @@ version = "0.1.0" dependencies = [ "groestl", "primitives", - "ripemd160 0.8.0", - "sha-1 0.8.2", - "sha2 0.8.2", + "ripemd160", + "serialization", + "sha-1", + "sha2 0.9.9", "sha3", "siphasher", ] [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitmaps" @@ -506,9 +560,9 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98fcd36dda4e17b7d7abc64cb549bf0201f4ab71e00700c798ca7e62ed3761fa" dependencies = [ - "funty", + "funty 1.1.0", "radium 0.3.0", - "wyz", + "wyz 0.2.0", ] [[package]] @@ -517,33 +571,31 @@ version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" dependencies = [ - "funty", + "funty 1.1.0", "radium 0.5.3", "tap", - "wyz", + "wyz 0.2.0", ] [[package]] name = "bitvec" -version = "0.20.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +checksum = "1489fcb93a5bb47da0462ca93ad252ad6af2145cce58d10d46a83931ba9f016b" dependencies = [ - "funty", - "radium 0.6.2", + "funty 2.0.0", + "radium 0.7.0", "tap", - "wyz", + "wyz 0.5.0", ] [[package]] name = "blake2" -version = "0.9.1" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a5720225ef5daecf08657f23791354e1685a8c91a4c60c7f3d3b2892f978f4" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" dependencies = [ - "crypto-mac 0.8.0", - "digest 0.9.0", - "opaque-debug 0.3.0", + "digest 0.10.3", ] [[package]] @@ -568,6 +620,20 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake3" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" +dependencies = [ + "arrayref", + "arrayvec 0.7.1", + "cc", + "cfg-if 1.0.0", + "constant_time_eq", + "digest 0.10.3", +] + [[package]] name = "block-buffer" version = "0.7.3" @@ -586,7 +652,17 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.4", + "block-padding 0.2.1", + "generic-array 0.14.5", +] + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array 0.14.5", ] [[package]] @@ -634,27 +710,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4caf0101205582491f772d60a6fcb6bcec19963e68209cb631851eeadb01421f" dependencies = [ "bitvec 0.18.5", - "ff", - "group", + "ff 0.8.0", + "group 0.8.0", "pairing", "rand_core 0.5.1", "subtle 2.4.0", ] [[package]] -name = "bs58" -version = "0.4.0" +name = "borsh" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive", + "hashbrown 0.11.2", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" dependencies = [ - "sha2 0.9.5", + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2 1.0.39", + "syn 1.0.95", ] [[package]] -name = "build_const" -version = "0.2.1" +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "bs58" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +dependencies = [ + "sha2 0.9.9", +] [[package]] name = "bumpalo" @@ -662,6 +777,16 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + [[package]] name = "byte-slice-cast" version = "1.0.0" @@ -674,6 +799,26 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytemuck" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e851ca7c24871e7336801608a4797d7376545b6928a10d32d75685687141ead" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54" +dependencies = [ + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + [[package]] name = "byteorder" version = "0.5.3" @@ -704,9 +849,30 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "1.0.1" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "bzip2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] [[package]] name = "cache-padded" @@ -714,11 +880,25 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24508e28c677875c380c20f4d28124fab6f8ed4ef929a1397d7b1a31e92f1005" +[[package]] +name = "caps" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61bf7211aad104ce2769ec05efcdfabf85ee84ac92461d142f22cf8badd0e54c" +dependencies = [ + "errno", + "libc", + "thiserror", +] + [[package]] name = "cc" -version = "1.0.54" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -734,21 +914,21 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chacha20" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412" +checksum = "01b72a433d0cf2aef113ba70f62634c56fddb0f244e6377185c56a7cadbd8f91" dependencies = [ "cfg-if 1.0.0", "cipher 0.3.0", - "cpufeatures", + "cpufeatures 0.2.1", "zeroize", ] [[package]] name = "chacha20poly1305" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1580317203210c517b6d44794abfbe600698276db18127e37ad3e69bf5e848e5" +checksum = "3b84ed6d1d5f7aa9bdde921a5090e0ca4d934d250ea3b402a5fab3a994e28a2a" dependencies = [ "aead", "chacha20", @@ -761,6 +941,7 @@ dependencies = [ name = "chain" version = "0.1.0" dependencies = [ + "bitcoin", "bitcrypto", "primitives", "rustc-hex 2.1.0", @@ -776,7 +957,8 @@ checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" dependencies = [ "js-sys", "num-integer", - "num-traits 0.2.12", + "num-traits", + "serde", "time 0.1.43", "wasm-bindgen", ] @@ -787,7 +969,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", ] [[package]] @@ -796,7 +978,22 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", ] [[package]] @@ -817,49 +1014,82 @@ dependencies = [ "bitflags", ] +[[package]] +name = "cmake" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b858541263efe664aead4a5209a4ae5c5d2811167d4ed4ee0944503f8d2089" +dependencies = [ + "cc", +] + [[package]] name = "coins" version = "0.1.0" dependencies = [ "async-std", "async-trait", + "base58", "base64 0.10.1", - "bigdecimal", - "bitcoin-cash-slp", + "bincode", + "bip32", + "bitcoin", + "bitcoin_hashes", "bitcrypto", "byteorder 1.4.3", "bytes 0.4.12", "cfg-if 1.0.0", "chain", "common", + "crossbeam", + "crypto", + "db_common", "derive_more", "dirs", + "ed25519-dalek", + "ed25519-dalek-bip32 0.2.0", "ethabi", "ethcore-transaction", "ethereum-types 0.4.2", "ethkey", - "fomat-macros 0.2.1", "futures 0.1.29", "futures 0.3.15", "gstuff", - "hex 0.3.2", - "http 0.2.1", - "itertools 0.9.0", + "hex 0.4.2", + "http 0.2.7", + "itertools", "js-sys", - "jsonrpc-core", + "jsonrpc-core 8.0.1", "keys", "lazy_static", "libc", + "lightning", + "lightning-background-processor", + "lightning-invoice", + "lightning-net-tokio", + "lightning-persister", "metrics", + "mm2_core", + "mm2_db", + "mm2_err_handle", + "mm2_io", + "mm2_net", + "mm2_number", + "mm2_test_helpers", "mocktopus", - "num-traits 0.2.12", + "num-traits", + "parking_lot 0.12.0", "primitives", + "prost", + "prost-build", + "protobuf", "rand 0.7.3", "rlp 0.3.0", "rmp-serde", "rpc", + "rpc_task", "rust-ini", - "rustls", + "rustls 0.20.4", "script", "secp256k1", "ser_error", @@ -869,86 +1099,107 @@ dependencies = [ "serde_json", "serialization", "serialization_derive", - "sha2 0.8.2", + "sha2 0.9.9", "sha3", + "solana-client", + "solana-sdk", + "solana-transaction-status", + "spl-associated-token-account", + "spl-token", + "spv_validation", + "tiny-bip39", "tokio", "tokio-rustls", + "tonic", + "tonic-build", + "utxo_signer", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", "web-sys", "web3", - "webpki-roots 0.19.0", + "webpki-roots", "winapi", + "zbase32", "zcash_client_backend", + "zcash_client_sqlite", "zcash_primitives", "zcash_proofs", ] [[package]] -name = "common" +name = "coins_activation" version = "0.1.0" dependencies = [ - "anyhow", - "arrayref", - "async-std", "async-trait", - "backtrace", - "base64 0.10.1", - "bigdecimal", - "bitcrypto", - "bytes 0.4.12", + "coins", + "common", + "crypto", + "derive_more", + "futures 0.3.15", + "hex 0.4.2", + "mm2_core", + "mm2_err_handle", + "mm2_number", + "rpc", + "rpc_task", + "ser_error", + "ser_error_derive", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "common" +version = "0.1.0" +dependencies = [ + "anyhow", + "arrayref", + "async-trait", + "backtrace", + "base64 0.10.1", + "bytes 1.1.0", "cc", "cfg-if 1.0.0", "chrono", "crossbeam", "crossterm", - "derive_more", "findshlibs", "fnv", - "fomat-macros 0.2.1", - "fomat-macros 0.3.1", "futures 0.1.29", "futures 0.3.15", "futures-cpupool", - "getrandom 0.2.2", + "getrandom 0.2.6", "gstuff", "hdrhistogram 7.1.0", - "hex 0.3.2", - "http 0.2.1", + "hex 0.4.2", + "http 0.2.7", "http-body 0.1.0", "hyper", "hyper-rustls", - "indexmap", - "itertools 0.8.2", + "itertools", "js-sys", - "keys", "lazy_static", "libc", - "log 0.4.11", + "lightning", + "log 0.4.14", "log4rs", "metrics", "metrics-core", "metrics-runtime", "metrics-util", - "num-bigint 0.2.6", - "num-rational 0.2.4", - "num-traits 0.2.12", - "parking_lot 0.11.1", + "parking_lot 0.12.0", "parking_lot_core 0.6.2", - "paste", - "primitives", "rand 0.7.3", - "regex", - "rusqlite", "ser_error", "ser_error_derive", "serde", - "serde_bencode", - "serde_bytes 0.11.5", + "serde-wasm-bindgen", "serde_derive", "serde_json", "serde_repr", + "shared_ref_counter", "tokio", "uuid", "wasm-bindgen", @@ -969,35 +1220,46 @@ dependencies = [ ] [[package]] -name = "console_error_panic_hook" -version = "0.1.6" +name = "console" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" +checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" dependencies = [ - "cfg-if 0.1.10", - "wasm-bindgen", + "encode_unicode", + "libc", + "once_cell", + "regex", + "terminal_size", + "unicode-width", + "winapi", ] [[package]] -name = "const-random" -version = "0.1.8" +name = "console_error_panic_hook" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "const-random-macro", - "proc-macro-hack", + "cfg-if 1.0.0", + "wasm-bindgen", ] [[package]] -name = "const-random-macro" -version = "0.1.8" +name = "console_log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a" +checksum = "501a375961cef1a0d44767200e66e4a559283097e91d0730b1d75dfb2f8a1494" dependencies = [ - "getrandom 0.1.14", - "proc-macro-hack", + "log 0.4.14", + "web-sys", ] +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + [[package]] name = "const_fn" version = "0.4.7" @@ -1010,6 +1272,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.1.4" @@ -1020,27 +1291,21 @@ dependencies = [ ] [[package]] -name = "cpuid-bool" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" - -[[package]] -name = "crc" -version = "1.8.1" +name = "cpufeatures" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" dependencies = [ - "build_const", + "libc", ] [[package]] name = "crc32fast" -version = "1.2.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", ] [[package]] @@ -1050,34 +1315,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" dependencies = [ "cfg-if 0.1.10", - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", + "crossbeam-channel 0.4.4", + "crossbeam-deque 0.7.4", + "crossbeam-epoch 0.8.2", "crossbeam-queue", - "crossbeam-utils", + "crossbeam-utils 0.7.2", ] [[package]] name = "crossbeam-channel" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" +checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "maybe-uninit", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.8", +] + [[package]] name = "crossbeam-deque" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" +checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", + "crossbeam-epoch 0.8.2", + "crossbeam-utils 0.7.2", "maybe-uninit", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch 0.9.5", + "crossbeam-utils 0.8.8", +] + [[package]] name = "crossbeam-epoch" version = "0.8.2" @@ -1086,10 +1372,23 @@ checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ "autocfg 1.0.0", "cfg-if 0.1.10", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "lazy_static", "maybe-uninit", - "memoffset", + "memoffset 0.5.4", + "scopeguard 1.1.0", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.8", + "lazy_static", + "memoffset 0.6.4", "scopeguard 1.1.0", ] @@ -1100,7 +1399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" dependencies = [ "cfg-if 0.1.10", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "maybe-uninit", ] @@ -1115,6 +1414,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +dependencies = [ + "cfg-if 1.0.0", + "lazy_static", +] + [[package]] name = "crossterm" version = "0.20.0" @@ -1124,7 +1433,7 @@ dependencies = [ "bitflags", "crossterm_winapi", "libc", - "mio", + "mio 0.7.13", "parking_lot 0.11.1", "signal-hook", "signal-hook-mio", @@ -1152,6 +1461,59 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto" +version = "1.0.0" +dependencies = [ + "async-trait", + "bip32", + "bitcrypto", + "common", + "derive_more", + "enum-primitive-derive", + "futures 0.3.15", + "hex 0.4.2", + "http 0.2.7", + "hw_common", + "keys", + "mm2_core", + "mm2_err_handle", + "num-traits", + "parking_lot 0.12.0", + "primitives", + "rpc_task", + "rustc-hex 2.1.0", + "secp256k1", + "ser_error", + "ser_error_derive", + "serde", + "serde_derive", + "serde_json", + "trezor", +] + +[[package]] +name = "crypto-bigint" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +dependencies = [ + "generic-array 0.14.5", + "rand_core 0.6.3", + "subtle 2.4.0", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array 0.14.5", + "typenum", +] + [[package]] name = "crypto-mac" version = "0.7.0" @@ -1168,7 +1530,27 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", + "subtle 2.4.0", +] + +[[package]] +name = "crypto-mac" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca" +dependencies = [ + "generic-array 0.14.5", + "subtle 2.4.0", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array 0.14.5", "subtle 2.4.0", ] @@ -1188,12 +1570,13 @@ dependencies = [ ] [[package]] -name = "ct-logs" -version = "0.8.0" +name = "ctor" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" +checksum = "7fbaabec2c953050352311293be5c6aba8e141ba19d6811862b232d6fd020484" dependencies = [ - "sct", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] @@ -1228,9 +1611,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.0.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8492de420e9e60bc9a1d66e2dbb91825390b738a388606600663fc529b4b307" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder 1.4.3", "digest 0.9.0", @@ -1239,12 +1622,47 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "4.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4033478fbf70d6acf2655ac70da91ee65852d69daf7a67bf7a2f518fb47aafcf" +dependencies = [ + "byteorder 1.4.3", + "digest 0.9.0", + "rand_core 0.6.3", + "subtle 2.4.0", + "zeroize", +] + +[[package]] +name = "dashmap" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +dependencies = [ + "cfg-if 1.0.0", + "num_cpus", + "rayon", +] + [[package]] name = "data-encoding" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72aa14c04dfae8dd7d8a2b1cb7ca2152618cd01336dbfe704b8dcbf8d41dbd69" +[[package]] +name = "db_common" +version = "0.1.0" +dependencies = [ + "hex 0.4.2", + "log 0.4.14", + "rusqlite", + "sql-builder", + "uuid", +] + [[package]] name = "debug_stub_derive" version = "0.3.0" @@ -1255,15 +1673,39 @@ dependencies = [ "syn 0.11.11", ] +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid", +] + +[[package]] +name = "derivation-path" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193388a8c8c75a490b604ff61775e236541b8975e98e5ca1f6ea97d122b7e2db" +dependencies = [ + "failure", +] + +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + [[package]] name = "derivative" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] @@ -1272,9 +1714,21 @@ version = "0.99.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "dialoguer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb" +dependencies = [ + "console", + "lazy_static", + "tempfile", + "zeroize", ] [[package]] @@ -1292,7 +1746,27 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", + "subtle 2.4.0", +] + +[[package]] +name = "dir-diff" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2860407d7d7e2e004bb2128510ad9e8d669e76fa005ccf567977b5d71b8b4a0b" +dependencies = [ + "walkdir", ] [[package]] @@ -1315,6 +1789,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.6" @@ -1326,6 +1810,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.0", + "winapi", +] + [[package]] name = "discard" version = "1.0.4" @@ -1333,13 +1828,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" [[package]] -name = "dtoa" -version = "0.4.6" +name = "dlopen" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" - -[[package]] -name = "ed25519" +checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937" +dependencies = [ + "dlopen_derive", + "lazy_static", + "libc", + "winapi", +] + +[[package]] +name = "dlopen_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581" +dependencies = [ + "libc", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "dtoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5caaa75cbd2b960ff1e5392d2cfb1f44717fffe12fc1f32b7b5d1267f99732a6" + +[[package]] +name = "ecdsa" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf038a7b6fd7ef78ad3348b63f3a17550877b0e28f8d68bcc94894d1412158bc" @@ -1353,14 +1883,39 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "ed25519", "rand 0.7.3", "serde", - "sha2 0.9.5", + "sha2 0.9.9", "zeroize", ] +[[package]] +name = "ed25519-dalek-bip32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057f328f31294b5ab432e6c39642f54afd1531677d6d4ba2905932844cc242f3" +dependencies = [ + "derivation-path 0.1.3", + "ed25519-dalek", + "failure", + "hmac 0.9.0", + "sha2 0.9.9", +] + +[[package]] +name = "ed25519-dalek-bip32" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2be62a4061b872c8c0873ee4fc6f101ce7b889d039f019c5fa2af471a59908" +dependencies = [ + "derivation-path 0.2.0", + "ed25519-dalek", + "hmac 0.12.1", + "sha2 0.10.2", +] + [[package]] name = "edit-distance" version = "2.1.0" @@ -1373,27 +1928,60 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" +[[package]] +name = "elliptic-curve" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b477563c2bfed38a3b7a60964c49e058b2510ad3f12ba3483fd8f62c2306d6" +dependencies = [ + "base16ct", + "crypto-bigint", + "der", + "ff 0.11.1", + "generic-array 0.14.5", + "group 0.11.0", + "rand_core 0.6.3", + "sec1", + "subtle 2.4.0", + "zeroize", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "enum-as-inner" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" +checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" dependencies = [ "heck", - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "enum-primitive-derive" -version = "0.1.2" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b90e520ec62c1864c8c78d637acbfe8baf5f63240f2fb8165b8325c07812dd" +checksum = "c375b9c5eadb68d0a6efee2999fef292f45854c3444c86f09d8ab086ba942b0e" dependencies = [ - "num-traits 0.1.43", - "quote 0.3.15", - "syn 0.11.11", + "num-traits", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] @@ -1404,7 +1992,20 @@ checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "atty", "humantime 1.3.0", - "log 0.4.11", + "log 0.4.14", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime 2.1.0", + "log 0.4.14", "regex", "termcolor", ] @@ -1412,12 +2013,33 @@ dependencies = [ [[package]] name = "equihash" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git#7da40e902463a78c93390686ee34e0379c121b40" +source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" dependencies = [ "blake2b_simd", "byteorder 1.4.3", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "error-chain" version = "0.12.2" @@ -1457,9 +2079,9 @@ dependencies = [ [[package]] name = "ethbloom" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779864b9c7f7ead1f092972c3257496c6a84b46dba2ce131dd8a282cb2cc5972" +checksum = "11da94e443c60508eb62cf256243a64da87304c2802ac2528847f79d750007ef" dependencies = [ "crunchy 0.2.2", "fixed-hash 0.7.0", @@ -1471,7 +2093,7 @@ dependencies = [ [[package]] name = "ethcore-transaction" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-ethereum.git#d4b2179e76b447f7def274e68e60e4c0a52c4070" +source = "git+https://github.com/artemii235/parity-ethereum.git#0a090f9b3efd7e24193265cf0e4109bf2369ad98" dependencies = [ "ethereum-types 0.4.2", "ethkey", @@ -1497,11 +2119,11 @@ dependencies = [ [[package]] name = "ethereum-types" -version = "0.11.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64b5df66a228d85e4b17e5d6c6aa43b0310898ffe8a85988c4c032357aaabfd" +checksum = "b2827b94c556145446fcce834ca86b7abf0c39a805883fe20e72c5bfdb5a0dc6" dependencies = [ - "ethbloom 0.11.0", + "ethbloom 0.12.1", "fixed-hash 0.7.0", "impl-rlp", "impl-serde", @@ -1521,7 +2143,7 @@ dependencies = [ [[package]] name = "ethkey" version = "0.3.0" -source = "git+https://github.com/artemii235/parity-ethereum.git#d4b2179e76b447f7def274e68e60e4c0a52c4070" +source = "git+https://github.com/artemii235/parity-ethereum.git#0a090f9b3efd7e24193265cf0e4109bf2369ad98" dependencies = [ "byteorder 1.4.3", "edit-distance", @@ -1536,6 +2158,28 @@ dependencies = [ "tiny-keccak 1.4.4", ] +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", + "synstructure", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -1556,9 +2200,18 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "1.2.4" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "feature-probe" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64b0126b293b050395b37b10489951590ed024c03d7df4f249d219c8ded7cbf" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" [[package]] name = "ff" @@ -1571,6 +2224,28 @@ dependencies = [ "subtle 2.4.0", ] +[[package]] +name = "ff" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" +dependencies = [ + "rand_core 0.6.3", + "subtle 2.4.0", +] + +[[package]] +name = "filetime" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.10", + "winapi", +] + [[package]] name = "findshlibs" version = "0.5.0" @@ -1605,17 +2280,17 @@ dependencies = [ [[package]] name = "fixedbitset" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" [[package]] name = "flate2" -version = "1.0.16" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c90b0fc46cf89d227cc78b40e494ff81287a92dd07631e5af0d06fe3cf885e" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "crc32fast", "libc", "libz-sys", @@ -1629,16 +2304,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "fomat-macros" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b541b35d643a4dfbba66fc2a4d401314d6eb16e36155c92b439baeb232c5d0c7" - -[[package]] -name = "fomat-macros" -version = "0.3.1" +name = "form_urlencoded" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe56556a8c9f9f556150eb6b390bc1a8b3715fd2ddbb4585f36b6a5672c6a833" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding 2.1.0", +] [[package]] name = "fpe" @@ -1650,9 +2323,15 @@ dependencies = [ "block-modes", "num-bigint 0.3.2", "num-integer", - "num-traits 0.2.12", + "num-traits", ] +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1665,6 +2344,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.1.29" @@ -1688,9 +2373,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.15" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -1698,9 +2383,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.15" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-cpupool" @@ -1726,21 +2411,19 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.15" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-macro" -version = "0.3.15" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ - "autocfg 1.0.0", - "proc-macro-hack", - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] @@ -1750,21 +2433,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a1387e07917c711fb4ee4f48ea0adb04a3c9739e53ef85bf43ae1edc2937a8b" dependencies = [ "futures-io", - "rustls", - "webpki", + "rustls 0.19.1", + "webpki 0.21.3", +] + +[[package]] +name = "futures-rustls" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01fe9932a224b72b45336d96040aa86386d674a31d0af27d800ea7bc8ca97fe" +dependencies = [ + "futures-io", + "rustls 0.20.4", + "webpki 0.22.0", ] [[package]] name = "futures-sink" -version = "0.3.15" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" -version = "0.3.15" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-timer" @@ -1778,11 +2472,10 @@ dependencies = [ [[package]] name = "futures-util" -version = "0.3.15" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ - "autocfg 1.0.0", "futures 0.1.29", "futures-channel", "futures-core", @@ -1791,10 +2484,8 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.6", + "pin-project-lite 0.2.9", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab 0.4.2", ] @@ -1807,7 +2498,7 @@ dependencies = [ "bytes 0.5.6", "futures 0.3.15", "memchr", - "pin-project 0.4.22", + "pin-project 0.4.29", ] [[package]] @@ -1821,14 +2512,25 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ + "serde", "typenum", "version_check", ] +[[package]] +name = "gethostname" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e692e296bfac1d2533ef168d0b60ff5897b8b70a4009276834014dd8924cc028" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "getrandom" version = "0.1.14" @@ -1843,9 +2545,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.2" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -1885,13 +2587,13 @@ dependencies = [ [[package]] name = "groestl" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0261755b496855e90fb72689afbd2e8fa8a4d1e529073163149e9628eea20afc" +checksum = "2432787a9b8f0d58dca43fe2240399479b7582dc8afa2126dc7652b864029e47" dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "opaque-debug 0.2.3", + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -1901,11 +2603,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc11f9f5fbf1943b48ae7c2bf6846e7d827a512d1be4f23af708f5ca5d01dde1" dependencies = [ "byteorder 1.4.3", - "ff", + "ff 0.8.0", "rand_core 0.5.1", "subtle 2.4.0", ] +[[package]] +name = "group" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" +dependencies = [ + "ff 0.11.1", + "rand_core 0.6.3", + "subtle 2.4.0", +] + [[package]] name = "gstuff" version = "0.7.4" @@ -1919,20 +2632,20 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.3" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "fnv", "futures-core", "futures-sink", "futures-util", - "http 0.2.1", + "http 0.2.7", "indexmap", "slab 0.4.2", "tokio", - "tokio-util", + "tokio-util 0.7.2", "tracing", ] @@ -1953,40 +2666,29 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6073d0ca812575946eb5f35ff68dbe519907b25c42530389ff946dc84c6ead" -dependencies = [ - "ahash 0.2.18", - "autocfg 0.1.7", -] - -[[package]] -name = "hashbrown" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" dependencies = [ - "ahash 0.3.8", - "autocfg 1.0.0", + "ahash 0.4.7", ] [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash 0.4.7", + "ahash 0.7.6", ] [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" dependencies = [ - "ahash 0.7.4", + "ahash 0.7.6", ] [[package]] @@ -2005,7 +2707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d331ebcdbca4acbefe5da8c3299b2e246f198a8294cc5163354e743398b89d" dependencies = [ "byteorder 1.4.3", - "num-traits 0.2.12", + "num-traits", ] [[package]] @@ -2015,18 +2717,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3c22708574c44e924720c5b3a116326c688e6d532f438c77c007ec8768644f9" dependencies = [ "byteorder 1.4.3", - "crossbeam-channel", - "num-traits 0.2.12", + "crossbeam-channel 0.4.4", + "num-traits", ] [[package]] name = "heck" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" -dependencies = [ - "unicode-segmentation", -] +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hermit-abi" @@ -2050,10 +2749,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" [[package]] -name = "hex-literal" -version = "0.3.1" +name = "hidapi" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b1717343691998deb81766bfcd1dce6df0d5d6c37070b5a3de2bb6d39f7822" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "hkd32" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5af1f635ef1bc545d78392b136bfe1c9809e029023c84a3638a864a10b8819c8" +checksum = "84f2a5541afe0725f0b95619d6af614f48c1b176385b8aa30918cfb8c4bfafc8" +dependencies = [ + "hmac 0.11.0", + "rand_core 0.6.3", + "sha2 0.9.9", + "zeroize", +] [[package]] name = "hmac" @@ -2076,21 +2792,50 @@ dependencies = [ ] [[package]] -name = "hmac-drbg" -version = "0.3.0" +name = "hmac" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff" dependencies = [ + "crypto-mac 0.9.1", "digest 0.9.0", - "generic-array 0.14.4", - "hmac 0.8.1", ] [[package]] -name = "hostname" -version = "0.3.1" +name = "hmac" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.3", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array 0.14.5", + "hmac 0.8.1", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" dependencies = [ "libc", "match_cfg", @@ -2105,18 +2850,18 @@ checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" dependencies = [ "bytes 0.4.12", "fnv", - "itoa", + "itoa 0.4.6", ] [[package]] name = "http" -version = "0.2.1" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" dependencies = [ - "bytes 0.5.6", + "bytes 1.1.0", "fnv", - "itoa", + "itoa 1.0.1", ] [[package]] @@ -2133,20 +2878,26 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ - "bytes 1.0.1", - "http 0.2.1", - "pin-project-lite 0.2.6", + "bytes 1.1.0", + "http 0.2.7", + "pin-project-lite 0.2.9", ] +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + [[package]] name = "httparse" -version = "1.4.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" [[package]] name = "httpdate" @@ -2169,24 +2920,45 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hw_common" +version = "0.1.0" +dependencies = [ + "async-trait", + "bip32", + "common", + "derive_more", + "futures 0.3.15", + "js-sys", + "mm2_err_handle", + "rusb", + "secp256k1", + "serde", + "serde_derive", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] + [[package]] name = "hyper" -version = "0.14.11" +version = "0.14.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "futures-channel", "futures-core", "futures-util", "h2", - "http 0.2.1", - "http-body 0.4.2", + "http 0.2.7", + "http-body 0.4.4", "httparse", "httpdate", - "itoa", - "pin-project-lite 0.2.6", - "socket2 0.4.0", + "itoa 1.0.1", + "pin-project-lite 0.2.9", + "socket2 0.4.4", "tokio", "tower-service", "tracing", @@ -2195,19 +2967,28 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" +checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" dependencies = [ - "ct-logs", - "futures-util", + "http 0.2.7", "hyper", - "log 0.4.11", - "rustls", + "rustls 0.20.4", "tokio", "tokio-rustls", - "webpki", - "webpki-roots 0.21.1", + "webpki-roots", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite 0.2.9", + "tokio", + "tokio-io-timeout", ] [[package]] @@ -2223,9 +3004,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", @@ -2234,33 +3015,22 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.6.5" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28538916eb3f3976311f5dfbe67b5362d0add1293d0a9cad17debf86f8e3aa48" +checksum = "cbc0fa01ffc752e9dbc72818cdb072cd028b86be5e09dd04c5a643704fe101a9" dependencies = [ - "if-addrs-sys", "libc", "winapi", ] -[[package]] -name = "if-addrs-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de74b9dd780476e837e5eb5ab7c88b49ed304126e412030a0adba99c8efe79ea" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "im" -version = "15.0.0" +version = "15.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111c1983f3c5bb72732df25cddacee9b546d08325fb584b5ebd38148be7b0246" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" dependencies = [ "bitmaps", - "rand_core 0.5.1", + "rand_core 0.6.3", "rand_xoshiro", "sized-chunks", "typenum", @@ -2269,9 +3039,9 @@ dependencies = [ [[package]] name = "impl-codec" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" dependencies = [ "parity-scale-codec", ] @@ -2287,9 +3057,9 @@ dependencies = [ [[package]] name = "impl-serde" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47ca4d2b6931707a55fce5cf66aff80e2178c8b63bbb4ecb5695cbc870ddf6f" +checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" dependencies = [ "serde", ] @@ -2300,26 +3070,50 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] +[[package]] +name = "index_list" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9d968042a4902e08810946fc7cd5851eb75e80301342305af755ca06cb82ce" + [[package]] name = "indexmap" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg 1.0.0", - "hashbrown 0.9.1", + "hashbrown 0.11.2", +] + +[[package]] +name = "indicatif" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" +dependencies = [ + "console", + "lazy_static", + "number_prefix", + "regex", ] [[package]] name = "instant" -version = "0.1.6" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b141fdc7836c525d4d594027d318c84161ca17aaf8113ab1f81ab93ae897485" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] [[package]] name = "iovec" @@ -2332,11 +3126,11 @@ dependencies = [ [[package]] name = "ipconfig" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" +checksum = "723519edce41262b05d4143ceb95050e4c614f483e78e9fd9e39a8275a84ad98" dependencies = [ - "socket2 0.3.19", + "socket2 0.4.4", "widestring", "winapi", "winreg", @@ -2350,42 +3144,39 @@ checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" [[package]] name = "itertools" -version = "0.8.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" dependencies = [ "either", ] [[package]] -name = "itertools" -version = "0.9.0" +name = "itoa" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -dependencies = [ - "either", -] +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" [[package]] -name = "itertools" -version = "0.10.1" +name = "itoa" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" -dependencies = [ - "either", -] +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] -name = "itoa" -version = "0.4.6" +name = "jobserver" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] [[package]] name = "js-sys" -version = "0.3.51" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" dependencies = [ "wasm-bindgen", ] @@ -2403,6 +3194,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonrpc-core" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" +dependencies = [ + "futures 0.3.15", + "futures-executor", + "futures-util", + "log 0.4.14", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "jubjub" version = "0.5.1" @@ -2411,8 +3217,8 @@ checksum = "620638af3b80d23f4df0cae21e3cc9809ac8826767f345066f010bcea66a2c55" dependencies = [ "bitvec 0.18.5", "bls12_381", - "ff", - "group", + "ff 0.8.0", + "group 0.8.0", "rand_core 0.5.1", "subtle 2.4.0", ] @@ -2442,9 +3248,11 @@ dependencies = [ "derive_more", "lazy_static", "primitives", + "rand 0.6.5", "rustc-hex 2.1.0", "secp256k1", "serde", + "serde_derive", ] [[package]] @@ -2453,7 +3261,7 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ff57d6d215f7ca7eb35a9a64d656ba4d9d2bef114d741dc08048e75e2f5d418" dependencies = [ - "log 0.4.11", + "log 0.4.14", ] [[package]] @@ -2477,22 +3285,35 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.97" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "libloading" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] [[package]] name = "libp2p" -version = "0.39.1" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.45.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ - "atomic", - "bytes 1.0.1", + "bytes 1.1.0", "futures 0.3.15", + "futures-timer", + "getrandom 0.2.6", + "instant", "lazy_static", "libp2p-core", "libp2p-dns", - "libp2p-floodsub 0.30.0", + "libp2p-floodsub 0.36.0", + "libp2p-metrics", "libp2p-mplex", "libp2p-noise", "libp2p-ping", @@ -2503,16 +3324,16 @@ dependencies = [ "libp2p-wasm-ext", "libp2p-websocket", "multiaddr", - "parking_lot 0.11.1", - "pin-project 1.0.7", + "parking_lot 0.12.0", + "pin-project 1.0.10", + "rand 0.7.3", "smallvec 1.6.1", - "wasm-timer", ] [[package]] name = "libp2p-core" -version = "0.29.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.33.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "asn1_der", "bs58", @@ -2521,35 +3342,38 @@ dependencies = [ "fnv", "futures 0.3.15", "futures-timer", + "instant", "lazy_static", - "libsecp256k1", - "log 0.4.11", + "libsecp256k1 0.7.0", + "log 0.4.14", "multiaddr", "multihash", "multistream-select", - "parking_lot 0.11.1", - "pin-project 1.0.7", + "p256", + "parking_lot 0.12.0", + "pin-project 1.0.10", "prost", "prost-build", - "rand 0.7.3", + "rand 0.8.4", "ring", "rw-stream-sink", - "sha2 0.9.5", + "sha2 0.10.2", "smallvec 1.6.1", "thiserror", - "unsigned-varint 0.7.0", + "unsigned-varint 0.7.1", "void", "zeroize", ] [[package]] name = "libp2p-dns" -version = "0.29.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.33.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "futures 0.3.15", "libp2p-core", - "log 0.4.11", + "log 0.4.14", + "parking_lot 0.12.0", "smallvec 1.6.1", "trust-dns-resolver", ] @@ -2570,53 +3394,64 @@ dependencies = [ [[package]] name = "libp2p-floodsub" -version = "0.30.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.36.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "cuckoofilter 0.5.0", "fnv", "futures 0.3.15", "libp2p-core", "libp2p-swarm", - "log 0.4.11", + "log 0.4.14", "prost", "prost-build", "rand 0.7.3", "smallvec 1.6.1", ] +[[package]] +name = "libp2p-metrics" +version = "0.6.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +dependencies = [ + "libp2p-core", + "libp2p-ping", + "libp2p-swarm", + "prometheus-client", +] + [[package]] name = "libp2p-mplex" -version = "0.29.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.33.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "asynchronous-codec", - "bytes 1.0.1", + "bytes 1.1.0", "futures 0.3.15", "libp2p-core", - "log 0.4.11", + "log 0.4.14", "nohash-hasher", - "parking_lot 0.11.1", + "parking_lot 0.12.0", "rand 0.7.3", "smallvec 1.6.1", - "unsigned-varint 0.7.0", + "unsigned-varint 0.7.1", ] [[package]] name = "libp2p-noise" -version = "0.32.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.36.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ - "bytes 1.0.1", - "curve25519-dalek", + "bytes 1.1.0", + "curve25519-dalek 3.2.0", "futures 0.3.15", "lazy_static", "libp2p-core", - "log 0.4.11", + "log 0.4.14", "prost", "prost-build", "rand 0.8.4", - "sha2 0.9.5", + "sha2 0.10.2", "snow", "static_assertions", "x25519-dalek", @@ -2625,81 +3460,84 @@ dependencies = [ [[package]] name = "libp2p-ping" -version = "0.30.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.36.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "futures 0.3.15", + "futures-timer", + "instant", "libp2p-core", "libp2p-swarm", - "log 0.4.11", + "log 0.4.14", "rand 0.7.3", "void", - "wasm-timer", ] [[package]] name = "libp2p-plaintext" -version = "0.29.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.33.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "asynchronous-codec", - "bytes 1.0.1", + "bytes 1.1.0", "futures 0.3.15", "libp2p-core", - "log 0.4.11", + "log 0.4.14", "prost", "prost-build", - "unsigned-varint 0.7.0", + "unsigned-varint 0.7.1", "void", ] [[package]] name = "libp2p-request-response" -version = "0.12.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.18.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "async-trait", - "bytes 1.0.1", + "bytes 1.1.0", "futures 0.3.15", + "instant", "libp2p-core", "libp2p-swarm", - "log 0.4.11", - "lru 0.6.0", - "minicbor", + "log 0.4.14", "rand 0.7.3", "smallvec 1.6.1", - "unsigned-varint 0.7.0", - "wasm-timer", + "unsigned-varint 0.7.1", ] [[package]] name = "libp2p-swarm" -version = "0.30.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.36.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "either", + "fnv", "futures 0.3.15", + "futures-timer", + "instant", "libp2p-core", - "log 0.4.11", + "log 0.4.14", + "pin-project 1.0.10", "rand 0.7.3", "smallvec 1.6.1", + "thiserror", "void", - "wasm-timer", ] [[package]] name = "libp2p-swarm-derive" -version = "0.24.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.27.2" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ - "quote 1.0.7", - "syn 1.0.72", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "libp2p-tcp" -version = "0.29.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.33.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "futures 0.3.15", "futures-timer", @@ -2707,15 +3545,15 @@ dependencies = [ "ipnet", "libc", "libp2p-core", - "log 0.4.11", - "socket2 0.4.0", + "log 0.4.14", + "socket2 0.4.4", "tokio", ] [[package]] name = "libp2p-wasm-ext" -version = "0.29.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.33.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "futures 0.3.15", "js-sys", @@ -2727,57 +3565,88 @@ dependencies = [ [[package]] name = "libp2p-websocket" -version = "0.30.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.35.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "either", "futures 0.3.15", - "futures-rustls", + "futures-rustls 0.22.1", "libp2p-core", - "log 0.4.11", + "log 0.4.14", + "parking_lot 0.12.0", "quicksink", "rw-stream-sink", "soketto", - "url 2.1.1", - "webpki-roots 0.21.1", + "url 2.2.2", + "webpki-roots", ] [[package]] name = "libp2p-yamux" -version = "0.33.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.37.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "futures 0.3.15", "libp2p-core", - "parking_lot 0.11.1", + "parking_lot 0.12.0", "thiserror", "yamux", ] [[package]] name = "libsecp256k1" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd1137239ab33b41aa9637a88a28249e5e70c40a42ccc92db7f12cc356c1fcd7" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" dependencies = [ "arrayref", "base64 0.12.3", "digest 0.9.0", "hmac-drbg", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", + "libsecp256k1-core 0.2.2", + "libsecp256k1-gen-ecmult 0.2.1", + "libsecp256k1-gen-genmult 0.2.1", "rand 0.7.3", "serde", - "sha2 0.9.5", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0452aac8bab02242429380e9b2f94ea20cea2b37e2c1777a1358799bbe97f37" +dependencies = [ + "arrayref", + "base64 0.13.0", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core 0.3.0", + "libsecp256k1-gen-ecmult 0.3.0", + "libsecp256k1-gen-genmult 0.3.0", + "rand 0.8.4", + "serde", + "sha2 0.9.9", "typenum", ] [[package]] name = "libsecp256k1-core" -version = "0.2.1" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy 0.2.2", + "digest 0.9.0", + "subtle 2.4.0", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee11012b293ea30093c129173cac4335513064094619f4639a25b310fd33c11" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" dependencies = [ "crunchy 0.2.2", "digest 0.9.0", @@ -2786,20 +3655,38 @@ dependencies = [ [[package]] name = "libsecp256k1-gen-ecmult" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32239626ffbb6a095b83b37a02ceb3672b2443a87a000a884fc3c4d16925c9c0" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" dependencies = [ - "libsecp256k1-core", + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core 0.3.0", ] [[package]] name = "libsecp256k1-gen-genmult" -version = "0.2.0" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76acb433e21d10f5f9892b1962c2856c58c7f39a9e4bd68ac82b9436a0ffd5b9" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" dependencies = [ - "libsecp256k1-core", + "libsecp256k1-core 0.3.0", ] [[package]] @@ -2814,10 +3701,10 @@ dependencies = [ ] [[package]] -name = "libz-sys" -version = "1.0.25" +name = "libusb1-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" +checksum = "e22e89d08bbe6816c6c5d446203b859eba35b8fa94bf1b7edb2f6d25d43f023f" dependencies = [ "cc", "libc", @@ -2826,18 +3713,94 @@ dependencies = [ ] [[package]] -name = "linked-hash-map" -version = "0.5.3" +name = "libz-sys" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] [[package]] -name = "lock_api" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" +name = "lightning" +version = "0.0.106" +source = "git+https://github.com/shamardy/rust-lightning?branch=0.0.106#af4a89c08c22d0110d386df0e288b2f825aaebbc" dependencies = [ - "owning_ref", + "bitcoin", + "hex 0.4.2", + "regex", + "secp256k1", +] + +[[package]] +name = "lightning-background-processor" +version = "0.0.106" +dependencies = [ + "bitcoin", + "db_common", + "lightning", + "lightning-invoice", + "lightning-persister", +] + +[[package]] +name = "lightning-invoice" +version = "0.14.0" +source = "git+https://github.com/shamardy/rust-lightning?branch=0.0.106#af4a89c08c22d0110d386df0e288b2f825aaebbc" +dependencies = [ + "bech32", + "bitcoin_hashes", + "lightning", + "num-traits", + "secp256k1", +] + +[[package]] +name = "lightning-net-tokio" +version = "0.0.106" +source = "git+https://github.com/shamardy/rust-lightning?branch=0.0.106#af4a89c08c22d0110d386df0e288b2f825aaebbc" +dependencies = [ + "bitcoin", + "lightning", + "tokio", +] + +[[package]] +name = "lightning-persister" +version = "0.0.106" +dependencies = [ + "async-trait", + "bitcoin", + "common", + "db_common", + "derive_more", + "hex 0.4.2", + "libc", + "lightning", + "mm2_io", + "parking_lot 0.12.0", + "rand 0.7.3", + "secp256k1", + "serde", + "serde_json", + "winapi", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + +[[package]] +name = "lock_api" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" +dependencies = [ + "owning_ref", "scopeguard 0.3.3", ] @@ -2852,9 +3815,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard 1.1.0", ] @@ -2865,17 +3828,17 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" dependencies = [ - "log 0.4.11", + "log 0.4.14", ] [[package]] name = "log" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 0.1.10", - "serde", + "cfg-if 1.0.0", + "value-bag", ] [[package]] @@ -2895,38 +3858,21 @@ dependencies = [ "chrono", "derivative", "fnv", - "humantime 2.1.0", "libc", - "log 0.4.11", + "log 0.4.14", "log-mdc", - "parking_lot 0.11.1", - "regex", - "serde", - "serde-value", - "serde_json", - "serde_yaml", "thiserror", "thread-id", - "typemap", "winapi", ] [[package]] name = "lru" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0609345ddee5badacf857d4f547e0e5a2e987db77085c24cd887f73573a04237" -dependencies = [ - "hashbrown 0.6.3", -] - -[[package]] -name = "lru" -version = "0.6.0" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111b945ac72ec09eb7bc62a0fbdc3cc6e80555a7245f52a69d3921a75b53b153" +checksum = "32613e41de4c47ab04970c348ca7ae7382cf116625755af070b008a15516a889" dependencies = [ - "hashbrown 0.8.2", + "hashbrown 0.11.2", ] [[package]] @@ -2950,6 +3896,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "matchit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" + [[package]] name = "maybe-uninit" version = "2.0.0" @@ -2959,13 +3911,22 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "mem" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-ethereum.git#d4b2179e76b447f7def274e68e60e4c0a52c4070" +source = "git+https://github.com/artemii235/parity-ethereum.git#0a090f9b3efd7e24193265cf0e4109bf2369ad98" [[package]] name = "memchr" -version = "2.3.3" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memmap2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f" +dependencies = [ + "libc", +] [[package]] name = "memoffset" @@ -2976,14 +3937,23 @@ dependencies = [ "autocfg 1.0.0", ] +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg 1.0.0", +] + [[package]] name = "memory-db" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "814bbecfc0451fc314eeea34f05bbcd5b98a7ad7af37faee088b86a1e633f1d4" +checksum = "6566c70c1016f525ced45d7b7f97730a2bafb037c788211d0c186ef5b2189f0a" dependencies = [ "hash-db", - "hashbrown 0.9.1", + "hashbrown 0.12.1", "parity-util-mem", ] @@ -3021,7 +3991,7 @@ checksum = "ce0e4f69639ccc0c6b2f0612164f9817349eb25545ed1ffb5ef3e1e1c1d220b4" dependencies = [ "arc-swap", "atomic-shim", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "im", "metrics", "metrics-core", @@ -3037,29 +4007,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d11f8090a8886339f9468a04eeea0711e4cf27538b134014664308041307a1c5" dependencies = [ - "crossbeam-epoch", + "crossbeam-epoch 0.8.2", "serde", ] [[package]] -name = "minicbor" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51aa5bb0ca22415daca596a227b507f880ad1b2318a87fa9325312a5d285ca0d" -dependencies = [ - "minicbor-derive", -] - -[[package]] -name = "minicbor-derive" -version = "0.6.3" +name = "mime" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2b9e8883d58e34b18facd16c4564a77ea50fce028ad3d0ee6753440e37acc8" -dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", -] +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "miniz_oxide" @@ -3086,9 +4042,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" dependencies = [ "libc", - "log 0.4.11", + "log 0.4.14", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "mio" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +dependencies = [ + "libc", + "log 0.4.14", "miow", "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", "winapi", ] @@ -3102,74 +4072,195 @@ dependencies = [ ] [[package]] -name = "mm2" +name = "mm2-libp2p" +version = "0.1.0" +dependencies = [ + "async-std", + "async-trait", + "atomicdex-gossipsub", + "derive_more", + "env_logger 0.7.1", + "futures 0.3.15", + "futures-rustls 0.21.1", + "getrandom 0.2.6", + "hex 0.4.2", + "lazy_static", + "libp2p", + "libp2p-floodsub 0.22.0", + "log 0.4.14", + "rand 0.7.3", + "regex", + "rmp-serde", + "secp256k1", + "serde", + "serde_bytes", + "serde_json", + "sha2 0.9.9", + "tokio", + "void", + "wasm-bindgen-futures", + "wasm-timer", +] + +[[package]] +name = "mm2_core" +version = "0.1.0" +dependencies = [ + "arrayref", + "async-trait", + "cfg-if 1.0.0", + "common", + "db_common", + "derive_more", + "futures 0.3.15", + "gstuff", + "hex 0.4.2", + "keys", + "lazy_static", + "mm2_rpc", + "primitives", + "rand 0.7.3", + "serde", + "serde_bytes", + "serde_json", + "shared_ref_counter", + "uuid", +] + +[[package]] +name = "mm2_db" +version = "0.1.0" +dependencies = [ + "async-trait", + "common", + "derive_more", + "futures 0.3.15", + "hex 0.4.2", + "itertools", + "js-sys", + "lazy_static", + "mm2_core", + "mm2_err_handle", + "mm2_number", + "num-traits", + "primitives", + "rand 0.7.3", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "mm2_err_handle" +version = "0.1.0" +dependencies = [ + "common", + "derive_more", + "futures 0.1.29", + "http 0.2.7", + "itertools", + "ser_error", + "ser_error_derive", + "serde", + "serde_json", +] + +[[package]] +name = "mm2_io" +version = "0.1.0" +dependencies = [ + "async-std", + "common", + "derive_more", + "futures 0.3.15", + "gstuff", + "mm2_err_handle", + "rand 0.7.3", + "serde", + "serde_json", +] + +[[package]] +name = "mm2_main" version = "0.1.0" dependencies = [ "async-std", "async-trait", - "bigdecimal", - "bitcoin-cash-slp", "bitcrypto", "blake2", "bytes 0.4.12", + "cfg-if 1.0.0", "chain", "chrono", "coins", + "coins_activation", "common", - "crc", "crc32fast", "crossbeam", + "crypto", + "db_common", "derive_more", "dirs", "either", "enum-primitive-derive", "ethereum-types 0.4.2", - "fomat-macros 0.2.1", "futures 0.1.29", "futures 0.3.15", "futures-cpupool", + "futures-rustls 0.21.1", "gstuff", "hash-db", "hash256-std-hasher", - "hex 0.3.2", - "hex-literal", - "http 0.2.1", + "hex 0.4.2", + "http 0.2.7", + "hw_common", "hyper", - "itertools 0.9.0", + "instant", + "itertools", "js-sys", "keys", "lazy_static", "libc", "metrics", "mm2-libp2p", + "mm2_core", + "mm2_db", + "mm2_err_handle", + "mm2_io", + "mm2_net", + "mm2_number", + "mm2_rpc", + "mm2_test_helpers", "mocktopus", - "num-rational 0.2.4", - "num-traits 0.2.12", + "num-traits", "parity-util-mem", - "parking_lot 0.11.1", + "parking_lot 0.12.0", "primitives", "rand 0.6.5", "rand 0.7.3", "regex", "rmp-serde", "rpc", + "rpc_task", "script", "secp256k1", "ser_error", "ser_error_derive", "serde", - "serde_bencode", "serde_derive", "serde_json", "serialization", "serialization_derive", "sp-runtime-interface", "sp-trie", - "sql-builder", + "spv_validation", "testcontainers", "tokio", "trie-db", - "trie-root", + "trie-root 0.16.0", "uuid", "wasm-bindgen", "wasm-bindgen-futures", @@ -3180,59 +4271,111 @@ dependencies = [ ] [[package]] -name = "mm2-libp2p" +name = "mm2_net" version = "0.1.0" dependencies = [ - "async-std", "async-trait", - "atomicdex-gossipsub", - "env_logger", + "bytes 1.1.0", + "cfg-if 1.0.0", + "common", + "derive_more", "futures 0.3.15", - "getrandom 0.2.2", - "hex 0.4.2", + "gstuff", + "http 0.2.7", + "hyper", + "js-sys", "lazy_static", - "libp2p", - "libp2p-floodsub 0.22.0", - "log 0.4.11", - "num-bigint 0.2.6", - "num-rational 0.2.4", + "mm2_core", + "mm2_err_handle", + "prost", "rand 0.7.3", - "rmp-serde", - "secp256k1", "serde", - "serde_bytes 0.11.5", - "sha2 0.9.5", - "tokio", - "void", + "serde_json", + "wasm-bindgen", "wasm-bindgen-futures", - "wasm-timer", + "wasm-bindgen-test", + "web-sys", ] [[package]] -name = "mocktopus" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1e54a5bbecd61a064cb9c6ef396f8c896aee14e5baba8d1d555f35167dfd7c3" +name = "mm2_number" +version = "0.1.0" dependencies = [ - "mocktopus_macros", + "bigdecimal", + "num-bigint 0.4.3", + "num-rational", + "num-traits", + "paste", + "serde", + "serde_json", ] [[package]] -name = "mocktopus_macros" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3048ef3680533a27f9f8e7d6a0bce44dc61e4895ea0f42709337fa1c8616fefe" +name = "mm2_rpc" +version = "0.1.0" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "common", + "derive_more", + "futures 0.3.15", + "gstuff", + "http 0.2.7", + "mm2_err_handle", + "ser_error", + "ser_error_derive", + "serde", + "serde_json", ] [[package]] -name = "multiaddr" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48ee4ea82141951ac6379f964f71b20876d43712bea8faf6dd1a375e08a46499" +name = "mm2_test_helpers" +version = "0.1.0" +dependencies = [ + "bytes 1.1.0", + "cfg-if 1.0.0", + "chrono", + "common", + "crossterm", + "db_common", + "futures 0.3.15", + "gstuff", + "http 0.2.7", + "lazy_static", + "mm2_core", + "mm2_io", + "mm2_net", + "mm2_number", + "rand 0.7.3", + "regex", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "mocktopus" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e54a5bbecd61a064cb9c6ef396f8c896aee14e5baba8d1d555f35167dfd7c3" +dependencies = [ + "mocktopus_macros", +] + +[[package]] +name = "mocktopus_macros" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3048ef3680533a27f9f8e7d6a0bce44dc61e4895ea0f42709337fa1c8616fefe" +dependencies = [ + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "multiaddr" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c580bfdd8803cce319b047d239559a22f809094aaea4ac13902a1fdcfcd4261" dependencies = [ "arrayref", "bs58", @@ -3242,34 +4385,34 @@ dependencies = [ "percent-encoding 2.1.0", "serde", "static_assertions", - "unsigned-varint 0.7.0", - "url 2.1.1", + "unsigned-varint 0.7.1", + "url 2.2.2", ] [[package]] name = "multihash" -version = "0.14.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "752a61cd890ff691b4411423d23816d5866dd5621e4d1c5687a53b94b5a979d8" +checksum = "e3db354f401db558759dfc1e568d010a5d4146f4d3f637be1275ec4a3cf09689" dependencies = [ - "digest 0.9.0", - "generic-array 0.14.4", + "core2", + "digest 0.10.3", "multihash-derive", - "sha2 0.9.5", - "unsigned-varint 0.7.0", + "sha2 0.10.2", + "unsigned-varint 0.7.1", ] [[package]] name = "multihash-derive" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "424f6e86263cd5294cbd7f1e95746b95aca0e0d66bff31e5a40d6baa87b4aa99" +checksum = "fc076939022111618a5026d3be019fd8b366e76314538ff9a1b59ffbcbf98bcd" dependencies = [ - "proc-macro-crate 1.0.0", + "proc-macro-crate 1.1.3", "proc-macro-error", - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", "synstructure", ] @@ -3281,15 +4424,28 @@ checksum = "d8883adfde9756c1d30b0f519c9b8c502a94b41ac62f696453c37c7fc0a958ce" [[package]] name = "multistream-select" -version = "0.10.3" -source = "git+https://github.com/libp2p/rust-libp2p.git#20183c1ea152f5bfe183543e4934e082c1428011" +version = "0.11.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "futures 0.3.15", - "log 0.4.11", - "pin-project 1.0.7", + "log 0.4.14", + "pin-project 1.0.10", "smallvec 1.6.1", - "unsigned-varint 0.7.0", + "unsigned-varint 0.7.1", +] + +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.4", ] [[package]] @@ -3311,7 +4467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" dependencies = [ "bitvec 0.19.5", - "funty", + "funty 1.1.0", "lexical-core", "memchr", "version_check", @@ -3326,50 +4482,27 @@ dependencies = [ "winapi", ] -[[package]] -name = "num" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3e176191bc4faad357e3122c4747aa098ac880e88b168f106386128736cf4a" -dependencies = [ - "num-bigint 0.3.2", - "num-complex", - "num-integer", - "num-iter", - "num-rational 0.3.2", - "num-traits 0.2.12", -] - [[package]] name = "num-bigint" -version = "0.2.6" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +checksum = "7d0a3d5e207573f948a9e5376662aa743a2ea13f7c50a554d7af443a73fbfeba" dependencies = [ "autocfg 1.0.0", "num-integer", - "num-traits 0.2.12", - "serde", + "num-traits", ] [[package]] name = "num-bigint" -version = "0.3.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d0a3d5e207573f948a9e5376662aa743a2ea13f7c50a554d7af443a73fbfeba" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" dependencies = [ "autocfg 1.0.0", "num-integer", - "num-traits 0.2.12", -] - -[[package]] -name = "num-complex" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5" -dependencies = [ - "num-traits 0.2.12", + "num-traits", + "serde", ] [[package]] @@ -3378,9 +4511,9 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] @@ -3390,72 +4523,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" dependencies = [ "autocfg 1.0.0", - "num-traits 0.2.12", + "num-traits", ] [[package]] -name = "num-iter" -version = "0.1.42" +name = "num-rational" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" dependencies = [ "autocfg 1.0.0", + "num-bigint 0.4.3", "num-integer", - "num-traits 0.2.12", + "num-traits", + "serde", ] [[package]] -name = "num-rational" -version = "0.2.4" +name = "num-traits" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" dependencies = [ "autocfg 1.0.0", - "num-bigint 0.2.6", - "num-integer", - "num-traits 0.2.12", - "serde", ] [[package]] -name = "num-rational" -version = "0.3.2" +name = "num_cpus" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ - "autocfg 1.0.0", - "num-bigint 0.3.2", - "num-integer", - "num-traits 0.2.12", + "hermit-abi", + "libc", ] [[package]] -name = "num-traits" -version = "0.1.43" +name = "num_enum" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" dependencies = [ - "num-traits 0.2.12", + "derivative", + "num_enum_derive", ] [[package]] -name = "num-traits" -version = "0.2.12" +name = "num_enum_derive" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" dependencies = [ - "autocfg 1.0.0", + "proc-macro-crate 1.1.3", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] -name = "num_cpus" -version = "1.13.0" +name = "number_prefix" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" -dependencies = [ - "hermit-abi", - "libc", -] +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" @@ -3482,12 +4611,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] -name = "ordered-float" -version = "2.7.0" +name = "ouroboros" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f357ef82d1b4db66fbed0b8d542cbd3c22d0bf5b393b3c257b9ba4568e70c9c3" +dependencies = [ + "aliasable", + "ouroboros_macro", + "stable_deref_trait", +] + +[[package]] +name = "ouroboros_macro" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "039f02eb0f69271f26abe3202189275d7aa2258b903cb0281b5de710a2570ff3" +checksum = "44a0b52c2cbaef7dffa5fec1a43274afe8bd2a644fa9fc50a9ef4ff0269b1257" dependencies = [ - "num-traits 0.2.12", + "Inflector", + "proc-macro-error", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] @@ -3499,24 +4643,36 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "p256" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19736d80675fbe9fe33426268150b951a3fb8f5cfca2a23a17c85ef3adb24e3b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sec1", + "sha2 0.9.9", +] + [[package]] name = "pairing" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f702cdbee9e0a6272452c20dec82465bc821116598b4eeb63e9a71a69dbf7fd" dependencies = [ - "ff", - "group", + "ff 0.8.0", + "group 0.8.0", ] [[package]] name = "parity-scale-codec" -version = "2.2.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8975095a2a03bbbdc70a74ab11a4f76a6d0b84680d87c68d722531b0ac28e8a9" +checksum = "e8b44461635bbb1a0300f100a841e571e7d919c81c73075ef5d152ffdb521066" dependencies = [ "arrayvec 0.7.1", - "bitvec 0.20.4", + "bitvec 1.0.0", "byte-slice-cast", "impl-trait-for-tuples", "parity-scale-codec-derive", @@ -3525,14 +4681,14 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "2.2.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40dbbfef7f0a1143c5b06e0d76a6278e25dac0bc1af4be51a0fbb73f07e7ad09" +checksum = "c45ed1f39709f5a89338fab50e59816b2e8815f5bb58276e7ddf9afd495f73f8" dependencies = [ - "proc-macro-crate 1.0.0", - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro-crate 1.1.3", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] @@ -3543,17 +4699,17 @@ checksum = "aa9777aa91b8ad9dd5aaa04a9b6bcb02c7f1deb952fca5a66034d5e63afc5c6f" [[package]] name = "parity-util-mem" -version = "0.9.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664a8c6b8e62d8f9f2f937e391982eb433ab285b4cd9545b342441e04a906e42" +checksum = "c32561d248d352148124f036cac253a644685a21dc9fea383eb4907d7bd35a8f" dependencies = [ "cfg-if 1.0.0", - "ethereum-types 0.11.0", - "hashbrown 0.9.1", + "ethereum-types 0.13.1", + "hashbrown 0.12.1", "impl-trait-for-tuples", - "lru 0.6.0", + "lru", "parity-util-mem-derive", - "parking_lot 0.11.1", + "parking_lot 0.12.0", "primitive-types", "smallvec 1.6.1", "winapi", @@ -3565,8 +4721,8 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f557c32c6d268a07c921471619c0295f5efad3a0e76d4f97a05c091a51d110b2" dependencies = [ - "proc-macro2", - "syn 1.0.72", + "proc-macro2 1.0.39", + "syn 1.0.95", "synstructure", ] @@ -3614,10 +4770,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" dependencies = [ "instant", - "lock_api 0.4.4", + "lock_api 0.4.6", "parking_lot_core 0.8.0", ] +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api 0.4.6", + "parking_lot_core 0.9.1", +] + [[package]] name = "parking_lot_core" version = "0.4.0" @@ -3675,12 +4841,43 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking_lot_core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.10", + "smallvec 1.6.1", + "windows-sys", +] + [[package]] name = "paste" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1" +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac 0.8.0", +] + +[[package]] +name = "pbkdf2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739" +dependencies = [ + "crypto-mac 0.11.1", +] + [[package]] name = "percent-encoding" version = "1.0.1" @@ -3693,20 +4890,11 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - [[package]] name = "petgraph" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +checksum = "51b305cc4569dd4e8765bab46261f67ef5d4d11a4b6e745100ee5dad8948b46c" dependencies = [ "fixedbitset", "indexmap", @@ -3714,55 +4902,55 @@ dependencies = [ [[package]] name = "pin-project" -version = "0.4.22" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12e3a6cdbfe94a5e4572812a0201f8c0ed98c1c452c7b8563ce2276988ef9c17" +checksum = "9615c18d31137579e9ff063499264ddc1278e7b1982757ebc111028c4d1dc909" dependencies = [ - "pin-project-internal 0.4.22", + "pin-project-internal 0.4.29", ] [[package]] name = "pin-project" -version = "1.0.7" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" dependencies = [ - "pin-project-internal 1.0.7", + "pin-project-internal 1.0.10", ] [[package]] name = "pin-project-internal" -version = "0.4.22" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a0ffd45cf79d88737d7cc85bfd5d2894bee1139b356e616fe85dc389c61aaf7" +checksum = "044964427019eed9d49d9d5bbce6047ef18f37100ea400912a9fa4a3523ab12a" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "pin-project-internal" -version = "1.0.7" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "pin-project-lite" -version = "0.1.7" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.6" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -3782,7 +4970,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fe800695325da85083cd23b56826fccb2e2dc29b218e7811a6f33bc93f414be" dependencies = [ - "cpufeatures", + "cpufeatures 0.1.4", "opaque-debug 0.3.0", "universal-hash", ] @@ -3794,7 +4982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e597450cbf209787f0e6de80bf3795c6b2356a380ee87837b545aded8dbc1823" dependencies = [ "cfg-if 1.0.0", - "cpufeatures", + "cpufeatures 0.1.4", "opaque-debug 0.3.0", "universal-hash", ] @@ -3805,16 +4993,27 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" +[[package]] +name = "prettyplease" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28f53e8b192565862cf99343194579a022eb9c7dd3a8d03134734803c7b3125" +dependencies = [ + "proc-macro2 1.0.39", + "syn 1.0.95", +] + [[package]] name = "primitive-types" -version = "0.9.1" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06345ee39fbccfb06ab45f3a1a5798d9dafa04cb8921a76d227040003a234b0e" +checksum = "e28720988bff275df1f51b171e1b2a18c30d194c4d2b61defdacecd625a5d94a" dependencies = [ "fixed-hash 0.7.0", "impl-codec", "impl-rlp", "impl-serde", + "scale-info", "uint 0.9.1", ] @@ -3838,9 +5037,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.0.0" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ "thiserror", "toml", @@ -3853,9 +5052,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", "version_check", ] @@ -3865,8 +5064,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2", - "quote 1.0.7", + "proc-macro2 1.0.39", + "quote 1.0.18", "version_check", ] @@ -3877,68 +5076,98 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] -name = "proc-macro-nested" -version = "0.1.6" +name = "proc-macro2" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid 0.2.0", + "unicode-ident", +] + +[[package]] +name = "prometheus-client" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1abe0255c04d15f571427a2d1e00099016506cf3297b53853acd2b7eb87825" +dependencies = [ + "dtoa", + "itoa 1.0.1", + "owning_ref", + "prometheus-client-derive-text-encode", +] + +[[package]] +name = "prometheus-client-derive-text-encode" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e12d01b9d66ad9eb4529c57666b6263fc1993cb30261d83ead658fdd932652" +dependencies = [ + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "prost" -version = "0.8.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5e2533f59d08fcf364fd374ebda0692a70bd6d7e66ef97f306f45c6c5d8020" +checksum = "bc03e116981ff7d8da8e5c220e374587b98d294af7ba7dd7fda761158f00086f" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "prost-derive", ] [[package]] name = "prost-build" -version = "0.8.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355f634b43cdd80724ee7848f95770e7e70eefa6dcf14fea676216573b8fd603" +checksum = "65a1118354442de7feb8a2a76f3d80ef01426bd45542c8c1fdffca41a758f846" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", + "cfg-if 1.0.0", + "cmake", "heck", - "itertools 0.10.1", - "log 0.4.11", + "itertools", + "lazy_static", + "log 0.4.14", "multimap", "petgraph", "prost", "prost-types", + "regex", "tempfile", "which", ] [[package]] name = "prost-derive" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600d2f334aa05acb02a755e217ef1ab6dea4d51b58b7846588b747edec04efba" +checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" dependencies = [ "anyhow", - "itertools 0.10.1", - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "itertools", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "prost-types" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "603bbd6394701d13f3f25aada59c7de9d35a6a5887cfc156181234a44002771b" +checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "prost", ] @@ -3967,6 +5196,15 @@ dependencies = [ "protobuf-codegen", ] +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding 2.1.0", +] + [[package]] name = "quanta" version = "0.3.1" @@ -3989,8 +5227,8 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" dependencies = [ - "env_logger", - "log 0.4.11", + "env_logger 0.7.1", + "log 0.4.14", "rand 0.7.3", "rand_core 0.5.1", ] @@ -4003,7 +5241,7 @@ checksum = "77de3c815e5a160b1539c6592796801df2043ae35e123b46d73380cfa57af858" dependencies = [ "futures-core", "futures-sink", - "pin-project-lite 0.1.7", + "pin-project-lite 0.1.12", ] [[package]] @@ -4014,11 +5252,20 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" [[package]] name = "quote" -version = "1.0.7" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ - "proc-macro2", + "proc-macro2 1.0.39", ] [[package]] @@ -4035,9 +5282,9 @@ checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" [[package]] name = "radium" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" @@ -4167,7 +5414,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.2", + "getrandom 0.2.6", ] [[package]] @@ -4261,11 +5508,36 @@ dependencies = [ [[package]] name = "rand_xoshiro" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9fcdd2e881d02f1d9390ae47ad8e5696a9e4be7b547a1da2afbc61973217004" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" dependencies = [ - "rand_core 0.5.1", + "rand_core 0.6.3", +] + +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg 1.0.0", + "crossbeam-deque 0.8.1", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel 0.5.1", + "crossbeam-deque 0.8.1", + "crossbeam-utils 0.8.8", + "lazy_static", + "num_cpus", ] [[package]] @@ -4285,9 +5557,9 @@ checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" [[package]] name = "redox_syscall" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] @@ -4309,8 +5581,8 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ - "getrandom 0.2.2", - "redox_syscall 0.2.8", + "getrandom 0.2.6", + "redox_syscall 0.2.10", ] [[package]] @@ -4328,16 +5600,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c523ccaed8ac4b0288948849a350b37d3035827413c458b6a40ddb614bb4f72" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "regex" -version = "1.4.6" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", @@ -4346,9 +5618,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "remove_dir_all" @@ -4360,39 +5632,77 @@ dependencies = [ ] [[package]] -name = "resolv-conf" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" -dependencies = [ - "hostname", - "quick-error", -] - -[[package]] -name = "ring" -version = "0.16.15" +name = "reqwest" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4" +checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", + "base64 0.13.0", + "bytes 1.1.0", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.7", + "http-body 0.4.4", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "lazy_static", + "log 0.4.14", + "mime", + "percent-encoding 2.1.0", + "pin-project-lite 0.2.9", + "rustls 0.20.4", + "rustls-pemfile 0.2.1", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "url 2.2.2", + "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", - "winapi", + "webpki-roots", + "winreg", ] [[package]] -name = "ripemd160" -version = "0.8.0" +name = "resolv-conf" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad5112e0dbbb87577bfbc56c42450235e3012ce336e29c5befd7807bd626da4a" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "opaque-debug 0.2.3", + "hostname", + "quick-error", +] + +[[package]] +name = "rfc6979" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525" +dependencies = [ + "crypto-bigint", + "hmac 0.11.0", + "zeroize", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", ] [[package]] @@ -4422,7 +5732,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e54369147e3e7796c9b885c7304db87ca3d09a0a98f72843d532868675bbfba8" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", "rustc-hex 2.1.0", ] @@ -4433,7 +5743,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f10b46df14cf1ee1ac7baa4d2fbc2c52c0622a4b82fa8740e37bc452ac0184f" dependencies = [ "byteorder 1.4.3", - "num-traits 0.2.12", + "num-traits", ] [[package]] @@ -4447,13 +5757,23 @@ dependencies = [ "serde", ] +[[package]] +name = "rpassword" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rpc" version = "0.1.0" dependencies = [ "chain", "keys", - "log 0.4.11", + "log 0.4.14", "primitives", "rustc-hex 2.1.0", "script", @@ -4463,6 +5783,32 @@ dependencies = [ "serialization", ] +[[package]] +name = "rpc_task" +version = "0.1.0" +dependencies = [ + "async-trait", + "common", + "derive_more", + "futures 0.3.15", + "mm2_err_handle", + "mm2_rpc", + "ser_error", + "ser_error_derive", + "serde", + "serde_derive", +] + +[[package]] +name = "rusb" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c470dc7dc6e4710b6f85e9c4aa4650bc742260b39a36328180578db76fa258c1" +dependencies = [ + "libc", + "libusb1-sys", +] + [[package]] name = "rusqlite" version = "0.24.2" @@ -4476,6 +5822,7 @@ dependencies = [ "libsqlite3-sys", "memchr", "smallvec 1.6.1", + "time 0.2.27", ] [[package]] @@ -4487,7 +5834,7 @@ dependencies = [ "base64 0.11.0", "blake2b_simd", "constant_time_eq", - "crossbeam-utils", + "crossbeam-utils 0.7.2", ] [[package]] @@ -4502,6 +5849,12 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hex" version = "1.0.0" @@ -4525,11 +5878,11 @@ dependencies = [ [[package]] name = "rustc_version" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 0.11.0", + "semver 1.0.6", ] [[package]] @@ -4539,20 +5892,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ "base64 0.13.0", - "log 0.4.11", + "log 0.4.14", "ring", - "sct", - "webpki", + "sct 0.6.0", + "webpki 0.21.3", ] [[package]] -name = "rw-stream-sink" +name = "rustls" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" +dependencies = [ + "log 0.4.14", + "ring", + "sct 0.7.0", + "webpki 0.22.0", +] + +[[package]] +name = "rustls-pemfile" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4da5fcb054c46f5a5dff833b129285a93d3f0179531735e6c866e8cc307d2020" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64 0.13.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" +dependencies = [ + "base64 0.13.0", +] + +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + +[[package]] +name = "rw-stream-sink" +version = "0.3.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" dependencies = [ "futures 0.3.15", - "pin-project 0.4.22", + "pin-project 1.0.10", "static_assertions", ] @@ -4571,6 +5959,30 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scale-info" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c46be926081c9f4dd5dd9b6f1d3e3229f2360bc6502dd8836f84a93b7c75e99a" +dependencies = [ + "cfg-if 1.0.0", + "derive_more", + "parity-scale-codec", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e334bb10a245e28e5fd755cabcafd96cfcd167c99ae63a46924ca8d8703a3c" +dependencies = [ + "proc-macro-crate 1.1.3", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + [[package]] name = "scoped-tls" version = "1.0.0" @@ -4597,7 +6009,7 @@ dependencies = [ "blake2b_simd", "chain", "keys", - "log 0.4.11", + "log 0.4.14", "primitives", "serde", "serialization", @@ -4613,11 +6025,33 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" +dependencies = [ + "der", + "generic-array 0.14.5", + "subtle 2.4.0", + "zeroize", +] + [[package]] name = "secp256k1" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee5070fdc6f26ca5be6dcfc3d07c76fdb974a63a8b246b459854274145f5a258" +checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" dependencies = [ "rand 0.6.5", "secp256k1-sys", @@ -4634,9 +6068,9 @@ dependencies = [ [[package]] name = "secrecy" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0673d6a6449f5e7d12a1caf424fd9363e2af3a4953023ed455e3c4beef4597c0" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" dependencies = [ "zeroize", ] @@ -4647,17 +6081,14 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser 0.7.0", + "semver-parser", ] [[package]] name = "semver" -version = "0.11.0" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser 0.10.2", -] +checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d" [[package]] name = "semver-parser" @@ -4665,15 +6096,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - [[package]] name = "send_wrapper" version = "0.2.0" @@ -4697,48 +6119,30 @@ dependencies = [ name = "ser_error_derive" version = "0.1.0" dependencies = [ - "proc-macro2", - "quote 1.0.7", + "proc-macro2 1.0.39", + "quote 1.0.18", "ser_error", - "syn 1.0.72", + "syn 1.0.95", ] [[package]] name = "serde" -version = "1.0.114" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" dependencies = [ "serde_derive", ] [[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - -[[package]] -name = "serde_bencode" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315c49c11b6b10acc209df75b757ee70957b911ecd0e29bcbf2b735ebd580d45" -dependencies = [ - "serde", - "serde_bytes 0.10.5", -] - -[[package]] -name = "serde_bytes" -version = "0.10.5" +name = "serde-wasm-bindgen" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defbb8a83d7f34cc8380751eeb892b825944222888aff18996ea7901f24aec88" +checksum = "1cfc62771e7b829b517cb213419236475f434fb480eddd76112ae182d274434a" dependencies = [ + "js-sys", "serde", + "wasm-bindgen", ] [[package]] @@ -4752,23 +6156,23 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.114" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "serde_json" -version = "1.0.57" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" dependencies = [ "indexmap", - "itoa", + "itoa 1.0.1", "ryu", "serde", ] @@ -4779,19 +6183,31 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa 0.4.6", + "ryu", + "serde", ] [[package]] name = "serde_yaml" -version = "0.8.14" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7baae0a99f1a324984bcdc5f0718384c1f69775f1c7eec8b859b71b443e3fd7" +checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" dependencies = [ - "dtoa", - "linked-hash-map", + "indexmap", + "ryu", "serde", "yaml-rust", ] @@ -4801,7 +6217,9 @@ name = "serialization" version = "0.1.0" dependencies = [ "byteorder 1.4.3", + "derive_more", "primitives", + "test_helpers", ] [[package]] @@ -4815,25 +6233,13 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - -[[package]] -name = "sha-1" -version = "0.9.4" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpuid-bool", + "cpufeatures 0.2.1", "digest 0.9.0", "opaque-debug 0.3.0", ] @@ -4858,28 +6264,45 @@ dependencies = [ [[package]] name = "sha2" -version = "0.9.5" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures", + "cpufeatures 0.2.1", "digest 0.9.0", "opaque-debug 0.3.0", ] +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures 0.2.1", + "digest 0.10.3", +] + [[package]] name = "sha3" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" dependencies = [ - "block-buffer 0.7.3", - "byte-tools", - "digest 0.8.1", + "block-buffer 0.9.0", + "digest 0.9.0", "keccak", - "opaque-debug 0.2.3", + "opaque-debug 0.3.0", +] + +[[package]] +name = "shared_ref_counter" +version = "0.1.0" +dependencies = [ + "log 0.4.14", ] [[package]] @@ -4899,7 +6322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" dependencies = [ "libc", - "mio", + "mio 0.7.13", "signal-hook", ] @@ -4914,9 +6337,13 @@ dependencies = [ [[package]] name = "signature" -version = "1.2.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f060a7d147e33490ec10da418795238fd7545bba241504d6b31a409f2e6210" +checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" +dependencies = [ + "digest 0.9.0", + "rand_core 0.6.3", +] [[package]] name = "siphasher" @@ -4967,114 +6394,729 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "620cbb3c6e34da57d3a248cda0cd01cd5848164dc062e764e65d06fe3ea7aed5" dependencies = [ - "async-task", - "blocking", - "concurrent-queue", - "fastrand", - "futures-io", - "futures-util", - "libc", - "once_cell", - "scoped-tls", - "slab 0.4.2", - "socket2 0.3.19", - "wepoll-sys-stjepang", - "winapi", + "async-task", + "blocking", + "concurrent-queue", + "fastrand", + "futures-io", + "futures-util", + "libc", + "once_cell", + "scoped-tls", + "slab 0.4.2", + "socket2 0.3.19", + "wepoll-sys-stjepang", + "winapi", +] + +[[package]] +name = "snow" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774d05a3edae07ce6d68ea6984f3c05e9bba8927e3dd591e3b479e5b03213d0d" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek 4.0.0-pre.1", + "rand_core 0.6.3", + "ring", + "rustc_version 0.4.0", + "sha2 0.10.2", + "subtle 2.4.0", +] + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "soketto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "083624472e8817d44d02c0e55df043737ff11f279af924abdf93845717c2b75c" +dependencies = [ + "base64 0.13.0", + "bytes 1.1.0", + "flate2", + "futures 0.3.15", + "httparse", + "log 0.4.14", + "rand 0.8.4", + "sha-1", +] + +[[package]] +name = "solana-account-decoder" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea8c1862fc46c6ab40d83d15ced24a7afb1f3422da5824f1e9260f5ac10141f" +dependencies = [ + "Inflector", + "base64 0.12.3", + "bincode", + "bs58", + "bv", + "lazy_static", + "serde", + "serde_derive", + "serde_json", + "solana-config-program", + "solana-sdk", + "solana-vote-program", + "spl-token", + "thiserror", + "zstd", +] + +[[package]] +name = "solana-address-lookup-table-program" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c60728aec35d772e6614319558cdccbe3f845102699b65ba5ac7497da0b626a" +dependencies = [ + "bincode", + "bytemuck", + "log 0.4.14", + "num-derive", + "num-traits", + "rustc_version 0.4.0", + "serde", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-program-runtime", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "solana-bloom" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddcd7c6adb802bc812a5a80c8de06ba0f0e8df0cca296a8b4e67cd04c16218f" +dependencies = [ + "bv", + "fnv", + "log 0.4.14", + "rand 0.7.3", + "rayon", + "rustc_version 0.4.0", + "serde", + "serde_derive", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk", +] + +[[package]] +name = "solana-bucket-map" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3435b145894971a58a08a7b6be997ec782239fdecd5edd9846cd1d6aa5986" +dependencies = [ + "fs_extra", + "log 0.4.14", + "memmap2", + "rand 0.7.3", + "rayon", + "solana-logger", + "solana-measure", + "solana-sdk", + "tempfile", +] + +[[package]] +name = "solana-clap-utils" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8417a89c377728dbfbf1966b6493544f6e5e168ebc5bb444f3526481fae94e31" +dependencies = [ + "chrono", + "clap", + "rpassword", + "solana-perf", + "solana-remote-wallet", + "solana-sdk", + "thiserror", + "tiny-bip39", + "uriparse", + "url 2.2.2", +] + +[[package]] +name = "solana-cli-config" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e8b011d36369ef2bc3dff63fee078bf2916a4fd21f3aa702ee731c7ddf83d28" +dependencies = [ + "dirs-next", + "lazy_static", + "serde", + "serde_derive", + "serde_yaml", + "url 2.2.2", +] + +[[package]] +name = "solana-client" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f4df8cee4a1819f1c5b0d3d85d50c30f27133b2ae68c2fd92655e4aede34a" +dependencies = [ + "base64 0.13.0", + "bincode", + "bs58", + "clap", + "indicatif", + "jsonrpc-core 18.0.0", + "log 0.4.14", + "rayon", + "reqwest", + "semver 1.0.6", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-clap-utils", + "solana-faucet", + "solana-measure", + "solana-net-utils", + "solana-sdk", + "solana-transaction-status", + "solana-version", + "solana-vote-program", + "thiserror", + "tokio", + "tungstenite", + "url 2.2.2", +] + +[[package]] +name = "solana-compute-budget-program" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685567c221f6bb5b64387f7b45d03036ad112b2ecbcd0f94b11204efab9f891e" +dependencies = [ + "solana-program-runtime", + "solana-sdk", +] + +[[package]] +name = "solana-config-program" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4b04403ff77f09eba5cf94078c1178161e26d346245b06180866ab5286fe6b" +dependencies = [ + "bincode", + "chrono", + "serde", + "serde_derive", + "solana-program-runtime", + "solana-sdk", +] + +[[package]] +name = "solana-faucet" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a11e1b6d5ce435bb3df95f2a970cd80500a8abf94ea87558c35fe0cce8456ab" +dependencies = [ + "bincode", + "byteorder 1.4.3", + "clap", + "log 0.4.14", + "serde", + "serde_derive", + "solana-clap-utils", + "solana-cli-config", + "solana-logger", + "solana-metrics", + "solana-sdk", + "solana-version", + "spl-memo", + "thiserror", + "tokio", +] + +[[package]] +name = "solana-frozen-abi" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5f69a79200f5ba439eb8b790c5e00beab4d1fae4da69ce023c69c6ac1b55bf1" +dependencies = [ + "bs58", + "bv", + "generic-array 0.14.5", + "log 0.4.14", + "memmap2", + "rustc_version 0.4.0", + "serde", + "serde_derive", + "sha2 0.9.9", + "solana-frozen-abi-macro", + "solana-logger", + "thiserror", +] + +[[package]] +name = "solana-frozen-abi-macro" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402fffb54bf5d335e6df26fc1719feecfbd7a22fafdf6649fe78380de3c47384" +dependencies = [ + "proc-macro2 1.0.39", + "quote 1.0.18", + "rustc_version 0.4.0", + "syn 1.0.95", +] + +[[package]] +name = "solana-logger" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942dc59fc9da66d178362051b658646b65dc11cea0bc804e4ecd2528d3c1279f" +dependencies = [ + "env_logger 0.9.0", + "lazy_static", + "log 0.4.14", +] + +[[package]] +name = "solana-measure" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ccd5b1278b115249d6ca5a203fd852f7d856e048488c24442222ee86e682bd9" +dependencies = [ + "log 0.4.14", + "solana-sdk", +] + +[[package]] +name = "solana-metrics" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9774cd8309f599797b1612731fbc56df6c612879ab4923a3dc7234400eea419" +dependencies = [ + "env_logger 0.9.0", + "gethostname", + "lazy_static", + "log 0.4.14", + "reqwest", + "solana-sdk", +] + +[[package]] +name = "solana-net-utils" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb530af085d8aab563530ed39703096aa526249d350082823882fdd59cdf839" +dependencies = [ + "bincode", + "clap", + "log 0.4.14", + "nix", + "rand 0.7.3", + "serde", + "serde_derive", + "socket2 0.4.4", + "solana-logger", + "solana-sdk", + "solana-version", + "tokio", + "url 2.2.2", +] + +[[package]] +name = "solana-perf" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4117c0cf7753bc18f3a09f4973175c3f2c7c5d8e3c9bc15cab09060b06f3434f" +dependencies = [ + "ahash 0.7.6", + "bincode", + "bv", + "caps", + "curve25519-dalek 3.2.0", + "dlopen", + "dlopen_derive", + "fnv", + "lazy_static", + "libc", + "log 0.4.14", + "nix", + "rand 0.7.3", + "rayon", + "serde", + "solana-bloom", + "solana-logger", + "solana-metrics", + "solana-rayon-threadlimit", + "solana-sdk", + "solana-vote-program", +] + +[[package]] +name = "solana-program" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a463f546a2f5842d35974bd4691ae5ceded6785ec24db440f773723f6ce4e11" +dependencies = [ + "base64 0.13.0", + "bincode", + "bitflags", + "blake3", + "borsh", + "borsh-derive", + "bs58", + "bv", + "bytemuck", + "console_error_panic_hook", + "console_log", + "curve25519-dalek 3.2.0", + "getrandom 0.1.14", + "itertools", + "js-sys", + "lazy_static", + "libsecp256k1 0.6.0", + "log 0.4.14", + "num-derive", + "num-traits", + "parking_lot 0.11.1", + "rand 0.7.3", + "rustc_version 0.4.0", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "sha2 0.9.9", + "sha3", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-logger", + "solana-sdk-macro", + "thiserror", + "wasm-bindgen", +] + +[[package]] +name = "solana-program-runtime" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09841673334eab958d5bedab9c9d75ed2ff7a7ef70e7dfd6b239c6838a3d79ec" +dependencies = [ + "base64 0.13.0", + "bincode", + "itertools", + "libc", + "libloading", + "log 0.4.14", + "num-derive", + "num-traits", + "rustc_version 0.4.0", + "serde", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-logger", + "solana-measure", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "solana-rayon-threadlimit" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92893e3129dfabb703cd045e1367f3ced91202a2d0b6179a3dcd62ad6bead3b" +dependencies = [ + "lazy_static", + "num_cpus", +] + +[[package]] +name = "solana-remote-wallet" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "315534baecaae3f804548ccc4738d73ae01bf6523219787ebe55ee66d8db9a85" +dependencies = [ + "base32", + "console", + "dialoguer", + "hidapi", + "log 0.4.14", + "num-derive", + "num-traits", + "parking_lot 0.11.1", + "qstring", + "semver 1.0.6", + "solana-sdk", + "thiserror", + "uriparse", +] + +[[package]] +name = "solana-runtime" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd06e905260433f7e8d18bccb2e2eb2aa5cc53379d104d331ddeb12e13230a0" +dependencies = [ + "arrayref", + "bincode", + "blake3", + "bv", + "bytemuck", + "byteorder 1.4.3", + "bzip2", + "crossbeam-channel 0.5.1", + "dashmap", + "dir-diff", + "flate2", + "fnv", + "index_list", + "itertools", + "lazy_static", + "log 0.4.14", + "memmap2", + "num-derive", + "num-traits", + "num_cpus", + "ouroboros", + "rand 0.7.3", + "rayon", + "regex", + "rustc_version 0.4.0", + "serde", + "serde_derive", + "solana-address-lookup-table-program", + "solana-bloom", + "solana-bucket-map", + "solana-compute-budget-program", + "solana-config-program", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-logger", + "solana-measure", + "solana-metrics", + "solana-program-runtime", + "solana-rayon-threadlimit", + "solana-sdk", + "solana-stake-program", + "solana-vote-program", + "symlink", + "tar", + "tempfile", + "thiserror", + "zstd", +] + +[[package]] +name = "solana-sdk" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6560e605c68fa1e3e66a9d3c8529d097d402e1183f80dd06a2c870d0ecb795c2" +dependencies = [ + "assert_matches", + "base64 0.13.0", + "bincode", + "bitflags", + "borsh", + "bs58", + "bytemuck", + "byteorder 1.4.3", + "chrono", + "derivation-path 0.1.3", + "digest 0.9.0", + "ed25519-dalek", + "ed25519-dalek-bip32 0.1.1", + "generic-array 0.14.5", + "hmac 0.11.0", + "itertools", + "js-sys", + "lazy_static", + "libsecp256k1 0.6.0", + "log 0.4.14", + "memmap2", + "num-derive", + "num-traits", + "pbkdf2 0.9.0", + "qstring", + "rand 0.7.3", + "rand_chacha 0.2.2", + "rustc_version 0.4.0", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "sha2 0.9.9", + "sha3", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-logger", + "solana-program", + "solana-sdk-macro", + "thiserror", + "uriparse", + "wasm-bindgen", +] + +[[package]] +name = "solana-sdk-macro" +version = "1.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c834b4e02ac911b13c13aed08b3f847e722f6be79d31b1c660c1dbd2dee83cdb" +dependencies = [ + "bs58", + "proc-macro2 1.0.39", + "quote 1.0.18", + "rustversion", + "syn 1.0.95", ] [[package]] -name = "snow" -version = "0.8.0" +name = "solana-stake-program" +version = "1.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6142f7c25e94f6fd25a32c3348ec230df9109b463f59c8c7acc4bd34936babb7" +checksum = "f92597c0ed16d167d5ee48e5b13e92dfaed9c55b23a13ec261440136cd418649" dependencies = [ - "aes-gcm", - "blake2", - "chacha20poly1305", - "rand 0.8.4", - "rand_core 0.6.3", - "ring", - "rustc_version 0.3.3", - "sha2 0.9.5", - "subtle 2.4.0", - "x25519-dalek", + "bincode", + "log 0.4.14", + "num-derive", + "num-traits", + "rustc_version 0.4.0", + "serde", + "serde_derive", + "solana-config-program", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-metrics", + "solana-program-runtime", + "solana-sdk", + "solana-vote-program", + "thiserror", ] [[package]] -name = "socket2" -version = "0.3.19" +name = "solana-transaction-status" +version = "1.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +checksum = "612a51efa19380992e81fc64a2fb55d42aed32c67d795848d980cbe1f9693250" dependencies = [ - "cfg-if 1.0.0", - "libc", - "winapi", + "Inflector", + "base64 0.12.3", + "bincode", + "bs58", + "lazy_static", + "log 0.4.14", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-measure", + "solana-metrics", + "solana-runtime", + "solana-sdk", + "solana-vote-program", + "spl-associated-token-account", + "spl-memo", + "spl-token", + "thiserror", ] [[package]] -name = "socket2" -version = "0.4.0" +name = "solana-version" +version = "1.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +checksum = "222e2c91640d45cd9617dfc07121555a9bdac10e6e105f6931b758f46db6faaa" dependencies = [ - "libc", - "winapi", + "log 0.4.14", + "rustc_version 0.4.0", + "serde", + "serde_derive", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk", ] [[package]] -name = "soketto" -version = "0.4.1" +name = "solana-vote-program" +version = "1.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85457366ae0c6ce56bf05a958aef14cd38513c236568618edbcd9a8c52cb80b0" +checksum = "b4cc64945010e9e76d368493ad091aa5cf43ee16f69296290ebb5c815e433232" dependencies = [ - "base64 0.12.3", - "bytes 0.5.6", - "flate2", - "futures 0.3.15", - "httparse", - "log 0.4.11", - "rand 0.7.3", - "sha-1 0.8.2", + "bincode", + "log 0.4.14", + "num-derive", + "num-traits", + "rustc_version 0.4.0", + "serde", + "serde_derive", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-logger", + "solana-metrics", + "solana-program-runtime", + "solana-sdk", + "thiserror", ] [[package]] name = "sp-core" -version = "3.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abbc8d4e9b8a7d5819ed26f1374017bb32833ef4890e4ff065e1da30669876bc" +checksum = "77963e2aa8fadb589118c3aede2e78b6c4bcf1c01d588fbf33e915b390825fbd" dependencies = [ + "bitflags", "byteorder 1.4.3", "hash-db", "hash256-std-hasher", - "log 0.4.11", - "num-traits 0.2.12", + "log 0.4.14", + "num-traits", "parity-scale-codec", "parity-util-mem", "primitive-types", + "scale-info", "secrecy", "sp-debug-derive", "sp-runtime-interface", "sp-std", "sp-storage", + "ss58-registry", "zeroize", ] [[package]] name = "sp-debug-derive" -version = "3.0.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e80275f23b4e7ba8f54dec5f90f016530e7307d2ee9445f617ab986cbe97f31e" +checksum = "d676664972e22a0796176e81e7bec41df461d1edf52090955cdab55f2c956ff2" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "sp-runtime-interface" -version = "3.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5c88b4bc8d607e4e2ff767a85db58cf7101f3dd6064f06929342ea67fe8fb" +checksum = "158bf0305c75a50fc0e334b889568f519a126e32b87900c3f4251202dece7b4b" dependencies = [ "impl-trait-for-tuples", "parity-scale-codec", @@ -5089,28 +7131,28 @@ dependencies = [ [[package]] name = "sp-runtime-interface-proc-macro" -version = "3.0.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a6c7c2251512c9e533d15db8a863b06ece1cbee778130dd9adbe44b6b39aa9" +checksum = "22ecb916b9664ed9f90abef0ff5a3e61454c1efea5861b2997e03f39b59b955f" dependencies = [ "Inflector", - "proc-macro-crate 0.1.5", - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro-crate 1.1.3", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "sp-std" -version = "3.0.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35391ea974fa5ee869cb094d5b437688fbf3d8127d64d1b9fed5822a1ed39b12" +checksum = "14804d6069ee7a388240b665f17908d98386ffb0b5d39f89a4099fc7a2a4c03f" [[package]] name = "sp-storage" -version = "3.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86af458d4a0251c490cdde9dcaaccb88d398f3b97ac6694cdd49ed9337e6b961" +checksum = "5dab53af846068e3e0716d3ccc70ea0db44035c79b2ed5821aaa6635039efa37" dependencies = [ "parity-scale-codec", "ref-cast", @@ -5120,9 +7162,9 @@ dependencies = [ [[package]] name = "sp-tracing" -version = "3.0.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567382d8d4e14fb572752863b5cd57a78f9e9a6583332b590b726f061f3ea957" +checksum = "69a67e555d171c4238bd223393cda747dd20ec7d4f5fe5c042c056cb7fde9eda" dependencies = [ "parity-scale-codec", "sp-std", @@ -5132,24 +7174,25 @@ dependencies = [ [[package]] name = "sp-trie" -version = "3.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b85b7f745da41ef825c6f7b93f1fdc897b03df94a4884adfbb70fbcd0aed1298" +checksum = "d6fc34f4f291886914733e083b62708d829f3e6b8d7a7ca7fa8a55a3d7640b0b" dependencies = [ "hash-db", "memory-db", "parity-scale-codec", + "scale-info", "sp-core", "sp-std", "trie-db", - "trie-root", + "trie-root 0.17.0", ] [[package]] name = "sp-wasm-interface" -version = "3.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b214e125666a6416cf30a70cc6a5dacd34a4e5197f8a3d479f714af7e1dc7a47" +checksum = "10d88debe690c2b24eaa9536a150334fcef2ae184c21a0e5b3e80135407a7d52" dependencies = [ "impl-trait-for-tuples", "parity-scale-codec", @@ -5162,6 +7205,54 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spl-associated-token-account" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "393e2240d521c3dd770806bff25c2c00d761ac962be106e14e22dd912007f428" +dependencies = [ + "solana-program", + "spl-token", +] + +[[package]] +name = "spl-memo" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0dc6f70db6bacea7ff25870b016a65ba1d1b6013536f08e4fd79a8f9005325" +dependencies = [ + "solana-program", +] + +[[package]] +name = "spl-token" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93bfdd5bd7c869cb565c7d7635c4fafe189b988a0bdef81063cd9585c6b8dc01" +dependencies = [ + "arrayref", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "thiserror", +] + +[[package]] +name = "spv_validation" +version = "0.1.0" +dependencies = [ + "chain", + "primitives", + "ripemd160", + "rustc-hex 2.1.0", + "serde", + "serde_json", + "serialization", + "sha2 0.9.9", + "test_helpers", +] + [[package]] name = "sql-builder" version = "3.1.1" @@ -5172,11 +7263,25 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ss58-registry" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9799e6d412271cb2414597581128b03f3285f260ea49f5363d07df6a332b3e" +dependencies = [ + "Inflector", + "proc-macro2 1.0.39", + "quote 1.0.18", + "serde", + "serde_json", + "unicode-xid 0.2.0", +] + [[package]] name = "stable_deref_trait" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "standback" @@ -5213,11 +7318,11 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" dependencies = [ - "proc-macro2", - "quote 1.0.7", + "proc-macro2 1.0.39", + "quote 1.0.18", "serde", "serde_derive", - "syn 1.0.72", + "syn 1.0.95", ] [[package]] @@ -5227,13 +7332,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" dependencies = [ "base-x", - "proc-macro2", - "quote 1.0.7", + "proc-macro2 1.0.39", + "quote 1.0.18", "serde", "serde_derive", "serde_json", "sha1", - "syn 1.0.72", + "syn 1.0.95", ] [[package]] @@ -5242,6 +7347,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "subtle" version = "1.0.0" @@ -5254,6 +7365,12 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "0.11.11" @@ -5267,15 +7384,32 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.72" +version = "0.15.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "unicode-xid 0.2.0", + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +dependencies = [ + "proc-macro2 1.0.39", + "quote 1.0.18", + "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + [[package]] name = "synom" version = "0.11.3" @@ -5291,9 +7425,9 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", "unicode-xid 0.2.0", ] @@ -5303,12 +7437,23 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tc_cli_client" version = "0.2.0" source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ - "log 0.4.11", + "log 0.4.14", "serde", "serde_derive", "serde_json", @@ -5322,7 +7467,7 @@ source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1 dependencies = [ "hex 0.3.2", "hmac 0.7.1", - "log 0.4.11", + "log 0.4.14", "rand 0.7.3", "sha2 0.8.2", "tc_core", @@ -5334,7 +7479,7 @@ version = "0.3.0" source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "debug_stub_derive", - "log 0.4.11", + "log 0.4.14", ] [[package]] @@ -5342,7 +7487,7 @@ name = "tc_dynamodb_local" version = "0.2.0" source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ - "log 0.4.11", + "log 0.4.14", "tc_core", ] @@ -5367,7 +7512,7 @@ name = "tc_parity_parity" version = "0.5.0" source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ - "log 0.4.11", + "log 0.4.14", "tc_core", ] @@ -5376,7 +7521,7 @@ name = "tc_postgres" version = "0.2.0" source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ - "log 0.4.11", + "log 0.4.14", "tc_core", ] @@ -5398,14 +7543,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.1.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", + "fastrand", "libc", - "rand 0.7.3", - "redox_syscall 0.1.56", + "redox_syscall 0.2.10", "remove_dir_all", "winapi", ] @@ -5419,6 +7564,23 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "test_helpers" +version = "0.1.0" +dependencies = [ + "hex 0.4.2", +] + [[package]] name = "testcontainers" version = "0.7.0" @@ -5436,24 +7598,33 @@ dependencies = [ "tc_trufflesuite_ganachecli", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" -version = "1.0.25" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.25" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] @@ -5479,9 +7650,9 @@ dependencies = [ [[package]] name = "time" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" dependencies = [ "const_fn", "libc", @@ -5504,15 +7675,34 @@ dependencies = [ [[package]] name = "time-macros-impl" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" dependencies = [ "proc-macro-hack", - "proc-macro2", - "quote 1.0.7", + "proc-macro2 1.0.39", + "quote 1.0.18", "standback", - "syn 1.0.72", + "syn 1.0.95", +] + +[[package]] +name = "tiny-bip39" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +dependencies = [ + "anyhow", + "hmac 0.8.1", + "once_cell", + "pbkdf2 0.4.0", + "rand 0.7.3", + "rustc-hash", + "sha2 0.9.9", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", ] [[package]] @@ -5533,12 +7723,6 @@ dependencies = [ "crunchy 0.2.2", ] -[[package]] -name = "tinyvec" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed" - [[package]] name = "tinyvec" version = "1.2.0" @@ -5556,17 +7740,20 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.8.1" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98c8b05dc14c75ea83d63dd391100353789f5f24b8b3866542a5e85c8be8e985" +checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" dependencies = [ - "autocfg 1.0.0", - "bytes 1.0.1", + "bytes 1.1.0", "libc", "memchr", - "mio", + "mio 0.8.2", "num_cpus", - "pin-project-lite 0.2.6", + "once_cell", + "parking_lot 0.12.0", + "pin-project-lite 0.2.9", + "signal-hook-registry", + "socket2 0.4.4", "tokio-macros", "winapi", ] @@ -5582,25 +7769,46 @@ dependencies = [ ] [[package]] -name = "tokio-macros" +name = "tokio-io-timeout" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite 0.2.9", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "tokio-rustls" -version = "0.22.0" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" +dependencies = [ + "rustls 0.20.4", + "tokio", + "webpki 0.22.0", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" dependencies = [ - "rustls", + "futures-core", + "pin-project-lite 0.2.9", "tokio", - "webpki", ] [[package]] @@ -5619,12 +7827,26 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" dependencies = [ - "bytes 1.0.1", + "bytes 1.1.0", + "futures-core", + "futures-sink", + "log 0.4.14", + "pin-project-lite 0.2.9", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" +dependencies = [ + "bytes 1.1.0", "futures-core", "futures-sink", - "log 0.4.11", - "pin-project-lite 0.2.6", + "pin-project-lite 0.2.9", "tokio", + "tracing", ] [[package]] @@ -5637,18 +7859,100 @@ dependencies = [ ] [[package]] -name = "toolchain_find" -version = "0.1.4" +name = "tonic" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9d60db39854b30b835107500cf0aca0b0d14d6e1c3de124217c23a29c2ddb" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.13.0", + "bytes 1.1.0", + "flate2", + "futures-core", + "futures-util", + "h2", + "http 0.2.7", + "http-body 0.4.4", + "hyper", + "hyper-timeout", + "percent-encoding 2.1.0", + "pin-project 1.0.10", + "prost", + "prost-derive", + "rustls-pemfile 1.0.0", + "tokio", + "tokio-rustls", + "tokio-stream", + "tokio-util 0.7.2", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", + "webpki-roots", +] + +[[package]] +name = "tonic-build" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9263bf4c9bfaae7317c1c2faf7f18491d2fe476f70c414b73bf5d445b00ffa1" +dependencies = [ + "prettyplease", + "proc-macro2 1.0.39", + "prost-build", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "tower" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5651b5f6860a99bd1adb59dbfe1db8beb433e73709d9032b413a77e2fb7c066a" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project 1.0.10", + "pin-project-lite 0.2.9", + "rand 0.8.4", + "slab 0.4.2", + "tokio", + "tokio-stream", + "tokio-util 0.6.7", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e458af37ead6107144c2e3bb892f605ddfad20251f12143cda8b9c9072b1ca45" +checksum = "7d342c6d58709c0a6d48d48dabbb62d4ef955cf5f0f3bbfd845838e7ae88dbae" dependencies = [ - "dirs", - "lazy_static", - "regex", - "semver 0.9.0", - "walkdir", + "bitflags", + "bytes 1.1.0", + "futures-core", + "futures-util", + "http 0.2.7", + "http-body 0.4.4", + "http-range-header", + "pin-project-lite 0.2.9", + "tower", + "tower-layer", + "tower-service", ] +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + [[package]] name = "tower-service" version = "0.3.0" @@ -5657,39 +7961,80 @@ checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] name = "tracing" -version = "0.1.25" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ "cfg-if 1.0.0", - "pin-project-lite 0.2.6", + "log 0.4.14", + "pin-project-lite 0.2.9", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +dependencies = [ + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + [[package]] name = "tracing-core" -version = "0.1.17" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" dependencies = [ "lazy_static", ] [[package]] -name = "traitobject" -version = "0.1.0" +name = "tracing-futures" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project 1.0.10", + "tracing", +] + +[[package]] +name = "trezor" +version = "0.1.1" +dependencies = [ + "async-trait", + "bip32", + "byteorder 1.4.3", + "common", + "derive_more", + "futures 0.3.15", + "hw_common", + "js-sys", + "mm2_err_handle", + "prost", + "rand 0.7.3", + "rpc_task", + "serde", + "serde_derive", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] [[package]] name = "trie-db" -version = "0.22.6" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eac131e334e81b6b3be07399482042838adcd7957aa0010231d0813e39e02fa" +checksum = "d32d034c0d3db64b43c31de38e945f15b40cd4ca6d2dcfc26d4798ce8de4ab83" dependencies = [ "hash-db", - "hashbrown 0.11.2", - "log 0.4.11", + "hashbrown 0.12.1", + "log 0.4.14", "smallvec 1.6.1", ] @@ -5702,11 +8047,20 @@ dependencies = [ "hash-db", ] +[[package]] +name = "trie-root" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a36c5ca3911ed3c9a5416ee6c679042064b93fc637ded67e25f92e68d783891" +dependencies = [ + "hash-db", +] + [[package]] name = "trust-dns-proto" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0d7f5db438199a6e2609debe3f69f808d074e0a2888ee0bccb45fe234d03f4" +checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d" dependencies = [ "async-trait", "cfg-if 1.0.0", @@ -5715,31 +8069,31 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "idna 0.2.0", + "idna 0.2.3", "ipnet", "lazy_static", - "log 0.4.11", + "log 0.4.14", "rand 0.8.4", "smallvec 1.6.1", "thiserror", - "tinyvec 1.2.0", + "tinyvec", "tokio", - "url 2.1.1", + "url 2.2.2", ] [[package]] name = "trust-dns-resolver" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ad17b608a64bd0735e67bde16b0636f8aa8591f831a25d18443ed00a699770" +checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558" dependencies = [ "cfg-if 1.0.0", "futures-util", "ipconfig", "lazy_static", - "log 0.4.11", + "log 0.4.14", "lru-cache", - "parking_lot 0.11.1", + "parking_lot 0.12.0", "resolv-conf", "smallvec 1.6.1", "thiserror", @@ -5754,25 +8108,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" [[package]] -name = "typemap" -version = "0.3.3" +name = "tungstenite" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" +checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1" dependencies = [ - "unsafe-any", + "base64 0.13.0", + "byteorder 1.4.3", + "bytes 1.1.0", + "http 0.2.7", + "httparse", + "log 0.4.14", + "rand 0.8.4", + "rustls 0.20.4", + "sha-1", + "thiserror", + "url 2.2.2", + "utf-8", + "webpki 0.22.0", + "webpki-roots", ] [[package]] name = "typenum" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" - -[[package]] -name = "ucd-trie" -version = "0.1.3" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "uint" @@ -5800,7 +8161,7 @@ dependencies = [ [[package]] name = "unexpected" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-ethereum.git#d4b2179e76b447f7def274e68e60e4c0a52c4070" +source = "git+https://github.com/artemii235/parity-ethereum.git#0a090f9b3efd7e24193265cf0e4109bf2369ad98" [[package]] name = "unicode-bidi" @@ -5811,20 +8172,26 @@ dependencies = [ "matches", ] +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" + [[package]] name = "unicode-normalization" -version = "0.1.13" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" dependencies = [ - "tinyvec 0.3.3", + "tinyvec", ] [[package]] -name = "unicode-segmentation" -version = "1.6.0" +name = "unicode-width" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" @@ -5832,6 +8199,12 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + [[package]] name = "unicode-xid" version = "0.2.0" @@ -5844,19 +8217,10 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", "subtle 2.4.0", ] -[[package]] -name = "unsafe-any" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" -dependencies = [ - "traitobject", -] - [[package]] name = "unsigned-varint" version = "0.4.0" @@ -5869,12 +8233,12 @@ dependencies = [ [[package]] name = "unsigned-varint" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f8d425fafb8cd76bc3f22aace4af471d3156301d7508f2107e98fbeae10bc7f" +checksum = "d86a8dc7f45e4c1b0d30e43038c38f274e77af056aa5f74b93c2cf9eb3c1c836" dependencies = [ "asynchronous-codec", - "bytes 1.0.1", + "bytes 1.1.0", "futures-io", "futures-util", ] @@ -5885,6 +8249,16 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "uriparse" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e515b1ada404168e145ac55afba3c42f04cf972201a8552d42e2abb17c1b7221" +dependencies = [ + "fnv", + "lazy_static", +] + [[package]] name = "url" version = "1.7.2" @@ -5898,15 +8272,40 @@ dependencies = [ [[package]] name = "url" -version = "2.1.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ - "idna 0.2.0", + "form_urlencoded", + "idna 0.2.3", "matches", "percent-encoding 2.1.0", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utxo_signer" +version = "0.1.0" +dependencies = [ + "async-trait", + "chain", + "common", + "crypto", + "derive_more", + "hex 0.4.2", + "keys", + "mm2_err_handle", + "primitives", + "rpc", + "script", + "serialization", +] + [[package]] name = "uuid" version = "0.7.4" @@ -5917,12 +8316,28 @@ dependencies = [ "serde", ] +[[package]] +name = "value-bag" +version = "1.0.0-alpha.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f" +dependencies = [ + "ctor", + "version_check", +] + [[package]] name = "vcpkg" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.2" @@ -5935,56 +8350,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -[[package]] -name = "wagyu-zcash-parameters" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c904628658374e651288f000934c33ef738b2d8b3e65d4100b70b395dbe2bb" -dependencies = [ - "wagyu-zcash-parameters-1", - "wagyu-zcash-parameters-2", - "wagyu-zcash-parameters-3", - "wagyu-zcash-parameters-4", - "wagyu-zcash-parameters-5", - "wagyu-zcash-parameters-6", -] - -[[package]] -name = "wagyu-zcash-parameters-1" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bf2e21bb027d3f8428c60d6a720b54a08bf6ce4e6f834ef8e0d38bb5695da8" - -[[package]] -name = "wagyu-zcash-parameters-2" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a616ab2e51e74cc48995d476e94de810fb16fc73815f390bf2941b046cc9ba2c" - -[[package]] -name = "wagyu-zcash-parameters-3" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14da1e2e958ff93c0830ee68e91884069253bf3462a67831b02b367be75d6147" - -[[package]] -name = "wagyu-zcash-parameters-4" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f058aeef03a2070e8666ffb5d1057d8bb10313b204a254a6e6103eb958e9a6d6" - -[[package]] -name = "wagyu-zcash-parameters-5" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffe916b30e608c032ae1b734f02574a3e12ec19ab5cc5562208d679efe4969d" - -[[package]] -name = "wagyu-zcash-parameters-6" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7b6d5a78adc3e8f198e9cd730f219a695431467f7ec29dcfc63ade885feebe1" - [[package]] name = "waker-fn" version = "1.0.0" @@ -6008,7 +8373,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" dependencies = [ - "log 0.4.11", + "log 0.4.14", "try-lock", ] @@ -6024,30 +8389,34 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if 1.0.0", - "serde", - "serde_json", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" dependencies = [ "bumpalo", "lazy_static", - "log 0.4.11", - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "log 0.4.14", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", "wasm-bindgen-shared", ] @@ -6065,32 +8434,32 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" dependencies = [ - "quote 1.0.7", + "quote 1.0.18", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" [[package]] name = "wasm-bindgen-test" @@ -6112,8 +8481,8 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c2e18093f11c19ca4e188c177fecc7c372304c311189f12c2f9bea5b7324ac7" dependencies = [ - "proc-macro2", - "quote 1.0.7", + "proc-macro2 1.0.39", + "quote 1.0.18", ] [[package]] @@ -6134,9 +8503,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.40" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b72fe77fd39e4bd3eaa4412fd299a0be6b3dfe9d2597e2f1c20beb968f41d17" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" dependencies = [ "js-sys", "wasm-bindgen", @@ -6153,8 +8522,8 @@ dependencies = [ "ethabi", "ethereum-types 0.4.2", "futures 0.1.29", - "jsonrpc-core", - "log 0.4.11", + "jsonrpc-core 8.0.1", + "log 0.4.14", "parking_lot 0.7.1", "rustc-hex 1.0.0", "serde", @@ -6175,21 +8544,22 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.19.0" +name = "webpki" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" dependencies = [ - "webpki", + "ring", + "untrusted", ] [[package]] name = "webpki-roots" -version = "0.21.1" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" dependencies = [ - "webpki", + "webpki 0.22.0", ] [[package]] @@ -6213,9 +8583,9 @@ dependencies = [ [[package]] name = "widestring" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" [[package]] name = "winapi" @@ -6248,11 +8618,54 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + [[package]] name = "winreg" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ "winapi", ] @@ -6263,51 +8676,75 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" +[[package]] +name = "wyz" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" +dependencies = [ + "tap", +] + [[package]] name = "x25519-dalek" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc614d95359fd7afc321b66d2107ede58b246b844cf5d8a0adcca413e439f088" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "rand_core 0.5.1", "zeroize", ] +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + [[package]] name = "yaml-rust" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] [[package]] name = "yamux" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7d9028f208dd5e63c614be69f115c1b53cacc1111437d4c765185856666c107" +checksum = "0c0608f53c1dc0bad505d03a34bbd49fbf2ad7b51eb036123e896365532745a1" dependencies = [ "futures 0.3.15", - "log 0.4.11", + "log 0.4.14", "nohash-hasher", - "parking_lot 0.11.1", + "parking_lot 0.12.0", "rand 0.8.4", "static_assertions", ] +[[package]] +name = "zbase32" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9079049688da5871a7558ddacb7f04958862c703e68258594cb7a862b5e33f" + [[package]] name = "zcash_client_backend" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git#7da40e902463a78c93390686ee34e0379c121b40" +source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" dependencies = [ "base64 0.13.0", "bech32", "bls12_381", "bs58", - "ff", - "group", + "ff 0.8.0", + "group 0.8.0", "hex 0.4.2", "jubjub", "nom", @@ -6316,21 +8753,39 @@ dependencies = [ "protobuf-codegen-pure", "rand_core 0.5.1", "subtle 2.4.0", - "time 0.2.25", + "time 0.2.27", "zcash_note_encryption", "zcash_primitives", ] +[[package]] +name = "zcash_client_sqlite" +version = "0.3.0" +source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" +dependencies = [ + "bech32", + "bs58", + "ff 0.8.0", + "group 0.8.0", + "jubjub", + "protobuf", + "rand_core 0.5.1", + "rusqlite", + "time 0.2.27", + "zcash_client_backend", + "zcash_primitives", +] + [[package]] name = "zcash_note_encryption" version = "0.0.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git#7da40e902463a78c93390686ee34e0379c121b40" +source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" dependencies = [ "blake2b_simd", "byteorder 1.4.3", "crypto_api_chachapoly", - "ff", - "group", + "ff 0.8.0", + "group 0.8.0", "rand_core 0.5.1", "subtle 2.4.0", ] @@ -6338,7 +8793,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git#7da40e902463a78c93390686ee34e0379c121b40" +source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" dependencies = [ "aes 0.6.0", "bitvec 0.18.5", @@ -6348,19 +8803,19 @@ dependencies = [ "byteorder 1.4.3", "crypto_api_chachapoly", "equihash", - "ff", + "ff 0.8.0", "fpe", - "funty", - "group", + "funty 1.1.0", + "group 0.8.0", "hex 0.4.2", "jubjub", "lazy_static", - "log 0.4.11", + "log 0.4.14", "rand 0.7.3", "rand_core 0.5.1", - "ripemd160 0.9.1", + "ripemd160", "secp256k1", - "sha2 0.9.5", + "sha2 0.9.9", "subtle 2.4.0", "zcash_note_encryption", ] @@ -6368,43 +8823,71 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git#7da40e902463a78c93390686ee34e0379c121b40" +source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" dependencies = [ "bellman", "blake2b_simd", "bls12_381", "byteorder 1.4.3", "directories", - "ff", - "group", + "ff 0.8.0", + "group 0.8.0", "jubjub", "lazy_static", "rand_core 0.5.1", - "wagyu-zcash-parameters", "zcash_primitives", ] [[package]] name = "zeroize" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeafe61337cb2c879d328b74aa6cd9d794592c82da6be559fdf11493f02a2d18" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.0.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ - "proc-macro2", - "quote 1.0.7", - "syn 1.0.72", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", "synstructure", ] +[[package]] +name = "zstd" +version = "0.9.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.3+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" +dependencies = [ + "cc", + "libc", +] + [[patch.unused]] name = "backtrace" version = "0.3.32" @@ -6414,8 +8897,3 @@ source = "git+https://github.com/artemii235/backtrace-rs.git#6f9ac910252ef783310 name = "backtrace-sys" version = "0.1.30" source = "git+https://github.com/artemii235/backtrace-rs.git#6f9ac910252ef7833105080c9cb4d5948dcb74c2" - -[[patch.unused]] -name = "num-rational" -version = "0.2.2" -source = "git+https://github.com/artemii235/num-rational.git#7e4aa03de722b6e965075cba3088a341a99901b0" diff --git a/Cargo.toml b/Cargo.toml index e34c5b0fce..4e27bed80d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,143 +1,18 @@ -# Support for split-debuginfo [should eventually](https://github.com/rust-lang/rust/issues/34651) land, -# hopefully giving us an out-of-the-box way to separate the code from the debugging information. -# We should use the "objcopy --only-keep-debug" and "add-symbol-file" meanwhile -# and separating stack tracing into raw trace and symbolication parts. - -[package] -name = "mm2" -version = "0.1.0" -edition = "2018" -default-run = "mm2" - -[features] -# Deprecated -native = [] -zhtlc = ["coins/zhtlc"] - -[[bin]] -name = "mm2" -path = "mm2src/mm2_bin.rs" -test = false -doctest = false -bench = false - -[[bin]] -name = "docker_tests" -path = "mm2src/docker_tests.rs" - -[lib] -name = "mm2" -path = "mm2src/mm2_lib.rs" -crate-type = ["cdylib", "staticlib"] -test = false -doctest = false -bench = false - -[profile.release] -# Due to the "overrides" only affects our workspace crates, as intended. -debug = true -debug-assertions = false -# For better or worse, might affect the stack traces in our portion of the code. -#opt-level = 1 - -[profile.release.overrides."*"] -# Turns debugging symbols off for the out-of-workspace dependencies. -debug = false - -[dependencies] -async-std = { version = "1.5", features = ["unstable"] } -async-trait = "0.1" -bigdecimal = { version = "0.1", features = ["serde"] } -bitcrypto = { path = "mm2src/mm2_bitcoin/crypto" } -blake2 = "0.9.1" -bytes = "0.4" -chain = { path = "mm2src/mm2_bitcoin/chain" } -coins = { path = "mm2src/coins" } -common = { path = "mm2src/common" } -crc = "1.8" -crc32fast = { version = "1.2", features = ["std", "nightly"] } -crossbeam = "0.7" -derive_more = "0.99" -either = "1.6" -ethereum-types = { version = "0.4", default-features = false, features = ["std", "serialize"] } -enum-primitive-derive = "0.1" -fomat-macros = "0.2" -futures01 = { version = "0.1", package = "futures" } -futures-cpupool = "0.1" -futures = { version = "0.3.1", package = "futures", features = ["compat", "async-await"] } -gstuff = { version = "0.7", features = ["nightly"] } -hash256-std-hasher = "0.15.2" -hash-db = "0.15.2" -hex = "0.3.2" -hex-literal = "0.3.1" -http = "0.2" -itertools = "0.9" -keys = { path = "mm2src/mm2_bitcoin/keys" } -lazy_static = "1.4" -libc = "0.2" -metrics = "0.12" -mm2-libp2p = { path = "mm2src/mm2_libp2p" } -num-rational = { version = "0.2", features = ["serde", "bigint", "bigint-std"] } -num-traits = "0.2" -rpc = { path = "mm2src/mm2_bitcoin/rpc" } -parking_lot = { version = "0.11", features = ["nightly"] } -parity-util-mem = "0.9" -# AP: portfolio RPCs are not documented and not used as of now -# so the crate is disabled to speed up the entire removal of C code -# portfolio = { path = "mm2src/portfolio" } -primitives = { path = "mm2src/mm2_bitcoin/primitives" } -rand = { version = "0.7", features = ["std", "small_rng"] } -rmp-serde = "0.14.3" -# TODO: Reduce the size of regex by disabling the features we don't use. -# cf. https://github.com/rust-lang/regex/issues/583 -regex = "1" -script = { path = "mm2src/mm2_bitcoin/script" } -serde = "1.0" -serde_bencode = "0.2" -serde_json = { version = "1.0", features = ["preserve_order"] } -serde_derive = "1.0" -ser_error = { path = "mm2src/derives/ser_error" } -ser_error_derive = { path = "mm2src/derives/ser_error_derive" } -serialization = { path = "mm2src/mm2_bitcoin/serialization" } -serialization_derive = { path = "mm2src/mm2_bitcoin/serialization_derive" } -sp-runtime-interface = { version = "3.0.0", default-features = false, features = ["disable_target_static_assertions"] } -sp-trie = { version = "3.0", default-features = false } -sql-builder = "3.1.1" - -trie-db = { version = "0.22.6", default-features = false } -trie-root = "0.16.0" -uuid = { version = "0.7", features = ["serde", "v4"] } -wasm-timer = "0.2.4" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = { version = "0.3.27" } -wasm-bindgen = { version = "0.2.50", features = ["serde-serialize", "nightly"] } -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.1" } -web-sys = { version = "0.3.4", features = ["console"] } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -dirs = { version = "1" } -hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } -tokio = { version = "1.7", features = ["io-util", "rt-multi-thread", "net"] } - -[dev-dependencies] -bitcoin-cash-slp = "0.3.1" -mocktopus = "0.7.0" -rand6 = { version = "0.6", package = "rand" } -secp256k1 = { version = "0.20", features = ["rand"] } -testcontainers = { git = "https://github.com/artemii235/testcontainers-rs.git" } -winapi = "0.3" - -[build-dependencies] -chrono = "0.4" -gstuff = { version = "0.7", features = ["nightly"] } -regex = "1" - [workspace] members = [ "mm2src/coins", + "mm2src/coins/lightning_persister", + "mm2src/coins/lightning_background_processor", + "mm2src/coins/utxo_signer", + "mm2src/coins_activation", + "mm2src/common/shared_ref_counter", + "mm2src/crypto", + "mm2src/db_common", + "mm2src/derives/ser_error", + "mm2src/derives/ser_error_derive", "mm2src/floodsub", + "mm2src/gossipsub", + "mm2src/hw_common", "mm2src/mm2_bitcoin/crypto", "mm2src/mm2_bitcoin/chain", "mm2src/mm2_bitcoin/keys", @@ -146,14 +21,39 @@ members = [ "mm2src/mm2_bitcoin/script", "mm2src/mm2_bitcoin/serialization", "mm2src/mm2_bitcoin/serialization_derive", + "mm2src/mm2_bitcoin/test_helpers", + "mm2src/mm2_core", + "mm2src/mm2_db", + "mm2src/mm2_err_handle", + "mm2src/mm2_test_helpers", "mm2src/mm2_libp2p", - "mm2src/gossipsub", - "mm2src/derives/ser_error", - "mm2src/derives/ser_error_derive", + "mm2src/mm2_main", + "mm2src/mm2_net", + "mm2src/mm2_number", + "mm2src/mm2_io", + "mm2src/mm2_rpc", + "mm2src/rpc_task", + "mm2src/trezor", ] + # https://doc.rust-lang.org/beta/cargo/reference/features.html#feature-resolver-version-2 resolver = "2" +[profile.release] +# Due to the "overrides" only affects our workspace crates, as intended. +debug = true +debug-assertions = false +# For better or worse, might affect the stack traces in our portion of the code. +#opt-level = 1 + +[profile.test] +# required to avoid a long running process of librustcash additional chain validation that is enabled with debug assertions +debug-assertions = false + +[profile.release.overrides."*"] +# Turns debugging symbols off for the out-of-workspace dependencies. +debug = false + # The backtrace disables build.define("HAVE_DL_ITERATE_PHDR", "1"); for android which results in "unknown" function # names being printed, but dl_iterate_phdr is present since API version 21 https://github.com/rust-lang/rust/issues/17520#issuecomment-344885468 # We're using 21 version for Android build so we're fine to use the patch. @@ -162,4 +62,3 @@ resolver = "2" [patch.crates-io] backtrace = { git = "https://github.com/artemii235/backtrace-rs.git" } backtrace-sys = { git = "https://github.com/artemii235/backtrace-rs.git" } -num-rational = { git = "https://github.com/artemii235/num-rational.git" } diff --git a/Dockerfile.dev-release b/Dockerfile.dev-release new file mode 100644 index 0000000000..036c3c6195 --- /dev/null +++ b/Dockerfile.dev-release @@ -0,0 +1,5 @@ +FROM debian:stable-slim +WORKDIR /mm2 +COPY target/release/mm2 /usr/local/bin/mm2 +EXPOSE 7783 +CMD ["mm2"] diff --git a/LEGAL/AUTHORS b/LEGAL/AUTHORS deleted file mode 100755 index 8b5005c4da..0000000000 --- a/LEGAL/AUTHORS +++ /dev/null @@ -1,2 +0,0 @@ -jl777 NXT-SQ9J-JCAN-8XVY-5XN7K - diff --git a/LEGAL/CONTRIBUTOR-LICENSE-AGREEMENT b/LEGAL/CONTRIBUTOR-LICENSE-AGREEMENT new file mode 100644 index 0000000000..4c48d95b9b --- /dev/null +++ b/LEGAL/CONTRIBUTOR-LICENSE-AGREEMENT @@ -0,0 +1,62 @@ +Software Grant and Contributor License Agreement (CLA) + +In order to clarify the intellectual property license granted with Contributions from any person or entity, we must have a Contributor License Agreement (CLA) on file that has been signed by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of " Atomic Private Limited" (the "Company, Us and Our) users; + +This version of the Agreement allows Contributors to submit Contributions to us, to authorize Contributions and to grant copyright and patent licenses thereto. + +You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Us. Except for the license granted herein to Us and recipients of software distributed by Us, you reserve certain rights, title, and interest in and to your Contributions. + +"Contribution" shall mean the code, documentation or any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by you to us for inclusion in, or documentation of, any of the products owned or managed by us (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to us or our representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, us for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + +"Code" means the computer software code, whether in human-readable or machine-executable form. + +"Submit" is the act of uploading, submitting, transmitting, or distributing code or other content to project, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the project for the purpose of discussing and improving that project. + +You must agree to the terms of this Agreement before making a Submission to the Project. + +Copyright License: Subject to the terms and conditions of this Agreement, you hereby grant to us and to recipients of software distributed by us a perpetual, worldwide, exclusive, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your contributions and such derivative works. + +Patent License: Subject to the terms and conditions of this Agreement, You hereby grant to us and to recipients of software distributed by us a perpetual, worldwide, exclusive, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by you that are necessarily infringed by your contribution(s) alone or by combination of your contribution(s) with the Work to which such contribution(s) was submitted. + +If any entity institutes patent litigation against you or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that contribution or Work shall terminate as of the date such litigation is filed. + +If you choose to provide us with suggestions, ideas for improvement, recommendations or other feedback, on any Work we may use your feedback without any restriction or payment. + +Each party reserves all rights not expressly granted in this Agreement. No additional licenses or rights whatsoever (including, without limitation, any implied licenses) are granted by implication, exhaustion, estoppel or otherwise. For permitted and/or restricted uses of software, please see the copyright notice attached to this project. + +You represent that You are legally entitled to grant the above licenses. You represent that each of Your Submissions is entirely Your original work (except as You may have disclosed under this Agreement). + +You represent that You have secured permission from Your employer to make the Submission in cases where Your Submission is made in the course of Your work for Your employer or Your employer has intellectual property rights in Your Submission by contract or applicable law. + +If You are signing this Agreement on behalf of Your employer, you represent and warrant that You have the necessary authority to bind the listed employer to the obligations contained in this Agreement. + +You are not expected to provide support for your contributions, except to the extent you desire to provide support. You may provide support for free, for a fee, or not at all. + +Should You wish to submit work that is not your original creation, you may submit it to us separately from any contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party" followed by the names of the third party and any licenses or other restrictions of which you are aware, and follow any other instructions in the Project's written guidelines concerning Submissions. + +References to "employer" in this Agreement include Your employer or anyone else for whom You are acting in making Your Submission, e.g. as a contractor, vendor, or agent. If Your Submission is made in the course of Your work for an employer or Your employer has intellectual property rights in Your Submission by contract or applicable law, you must secure permission from Your employer to make the Submission before signing this Agreement. In that case, the term "You" in this Agreement will refer to You and the employer collectively. + +If You change employers in the future and desire to Submit additional Submissions for the new employer, then You agree to sign a new Agreement and secure permission from the new employer before Submitting those Submissions. + +You agree to notify " Atomic Private Limited" in writing of any facts or circumstances of which You later become aware that would make Your representations in this Agreement inaccurate in any respect. + +You agree that contributions to Project and information about contributions may be maintained indefinitely and disclosed publicly, including Your name and other information that You submit with Your Submission. + +You agree to resolve disputes in a prompt, low-cost and mutually beneficial way. Before taking legal action or requesting legal proceedings, you will personally participate in an alternative dispute resolution procedure. Mediation and/or arbitral forum will be mutually selected and parties of the alternative dispute resolution will choose the procedure. + +The arbitration will be administered by JAMS, Inc. pursuant to its Streamlined Arbitration Rules and Procedures (the "Rules"). The Rules are available at https://www.jamsadr.com/rules-streamlined-arbitration/. + +Any arbitration must be commenced by filing a demand for arbitration within 6 months after the date the party asserting the claim first knows or reasonably should know of the act, omission or default giving rise to the claim, otherwise, such cause of action or claim is permanently barred. If applicable law provides a different limitation period for asserting claims, any claim must be asserted within the shortest time period permitted by applicable law. + +Any arbitration hearing ("Hearing") may be conducted through online means including but not limited to videoconference, upon request from either party. The Hearing will be conducted in English, and the Arbitrator may, at his or her discretion, also select a secondary language upon request by either party. + +You may be required, at Company's sole discretion, to give up any and all rights you may have to seek legal action to resolve any disputes arising from these terms through any other means, including but not limited to any court of law. + +The statute of limitations and any filing fee deadlines shall be tolled while the parties engage in the informal dispute resolution process required by this section. + +You give up your right to participate in a class action or other class proceeding. +This Agreement is the entire agreement between the parties, and supersedes any and all prior agreements, understandings or communications, written or oral, between the parties relating to the subject matter hereof. This Agreement may be assigned by " Atomic Private Limited". + +By signing, you accept and agree to the terms of this Contribution License Agreement for Your present and future Submissions to " Atomic Private Limited". + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software with a limited restrictions as envisaged under the Copyright notice, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. \ No newline at end of file diff --git a/LEGAL/COPYING b/LEGAL/COPYING index d159169d10..012664a9a6 100755 --- a/LEGAL/COPYING +++ b/LEGAL/COPYING @@ -1,339 +1,601 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to your programs, too. - When we speak of free software, we are referring to freedom, not +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to +TERMS AND CONDITIONS + + 0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights from Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is affected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: +a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + +7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: +a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. +c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + +d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: +a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or +e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least +state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - + +Copyright (C) +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: Copyright (C) +This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/LEGAL/DEVELOPER-AGREEMENT b/LEGAL/DEVELOPER-AGREEMENT deleted file mode 100755 index a5d2aabfee..0000000000 --- a/LEGAL/DEVELOPER-AGREEMENT +++ /dev/null @@ -1,67 +0,0 @@ -This document describes the agreement between the SuperNET developers -regarding copyright and licensing policies. - - -0. License. - -The SuperNET software is distributed under the GPL version 2, with the exception of -the code that allows SuperNET agents to be created. Agent reference code uses the MIT -license to allow fully unencumbered development of SuperNET agents. Independently created -SuperNET agents can even be closed source and be made available via the service provider -functionality within SuperNET. Also, service providers are free to decide on what -type of fees to charge for their services. - - -1. Individual copyright. - -Each core developer retains full copyright over his contributions to -the code. The aggregate "Copyright © The SuperNET Developers" notice -can still be used in some places for brevity, but the metadata -maintained by the version control software (currently Git) about the -origin and subsequent modifications of each file shall be used as a -definitive record of the specific copyright holders for that file or -modification (if original enough to be copyrightable). - - -2. Outside contributions. - -Contributions of non-committers (those without write access to the -repository) shall only be accepted if submitted under the MIT license, -or if placed in the public domain. Contributions of non-committers that -do not specify a license shall be deemed to be public domain work. - - -3. Closed source releases. - -Each copyright holder grants a non-transferable permission to the SuperNET -development team to use his code in closed source experimental -releases, provided that those are clearly labeled as experimental, for -testing purposes only, and are in a reasonable timeframe (not to exceed -six months) superseded by open source non-experimental releases with -essentially the same functionality. - - -4. Re-licensing. - -Re-licensing of the SuperNET software under a different license requires the -agreement of all copyright holders whose work is being re-licensed. To -ensure that an unreachable copyright holder cannot prevent the active -development team from making licensing decisions, each copyright holder -who leaves the development team shall provide an NXT account number in -the AUTHORS file, at which he can be contacted to discuss such -decisions. Lack of such contact info, or lack of any type of response to -a re-licensing permission request after more than 28 days, as recorded -in the NXT blockchain, shall be interpreted as an irrevocable permission -to the then active development team to perform the specific re-licensing -for which such a permission has been sought. - - -5. Pseudonymous developers. - -Developers may choose to contribute under a fictitious name. Such -developers shall provide verifiable crypto addresses in the AUTHORS file -A verified signature with such addresses shall be considered -sufficient, for making legally binding statements, or as a proof of -copyright ownership, by such pseudonymous developers. - - diff --git a/LEGAL/DEVELOPER-CERTIFICATE-OF-ORIGIN b/LEGAL/DEVELOPER-CERTIFICATE-OF-ORIGIN new file mode 100755 index 0000000000..fa0ce4f4ca --- /dev/null +++ b/LEGAL/DEVELOPER-CERTIFICATE-OF-ORIGIN @@ -0,0 +1,18 @@ +DCO +(Developer Certificate of Origin) + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I have the right to submit it under the open-source license indicated in the file; or + +(b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open-source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open-source license (unless I am permitted to submit under a different license), as indicated in the file; or + +(c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. + +(d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open-source license(s) involved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software with a limited restriction as envisaged under the Copyright notice, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. + + + + diff --git a/LEGAL/LICENSE b/LEGAL/LICENSE deleted file mode 100755 index 50c41bc792..0000000000 --- a/LEGAL/LICENSE +++ /dev/null @@ -1,32 +0,0 @@ -Copyright © 2013-2018 The SuperNET Developers. - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 2, -as published by the Free Software Foundation. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License version 2 for more details. - -You should have received a copy of the GNU General Public License version 2 -along with this program in the file COPYING. If not, see -. - -The SuperNET development team will consider granting exceptions to allow use of -this software under a different license on a case by case basis. Please see the -DEVELOPER-AGREEMENT file describing the developer agreement on copyright -and licensing policies, and the AUTHORS file for individual copyright holder -information. - -This software uses third party libraries, distributed under licenses described -in THIRDPARTY-LICENSES. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - diff --git a/LEGAL/LICENSE-COPYRIGHT-NOTICE b/LEGAL/LICENSE-COPYRIGHT-NOTICE new file mode 100755 index 0000000000..6af58751a5 --- /dev/null +++ b/LEGAL/LICENSE-COPYRIGHT-NOTICE @@ -0,0 +1,58 @@ +Copyright © 2022 Atomic Private Limited and its contributors + +Permission to include in application software or to make digital or hard copies of part or all of this work is subject to the following Agreement. + +All software, both binary and source published by Atomic Private Limited (hereinafter, Software) is copyrighted by the Atomic Private Limited (hereinafter, "Atomic Private Limited", "Company","We" or "Us") and ownership of all right, title and interest in and to the Software remains with Atomic Private Limited. By using or copying the Software, User agrees to abide by the terms of this Agreement. + +The Company grants to you (hereinafter, "User" or "You") a royalty-free, non-exclusive right to execute, copy, modify and distribute both the binary and source code solely for the uses, subject to the following conditions: + +You acknowledge that the Software is being supplied "as is," without any support services from the Company. We don't make any representations or warranties, express or implied, including, without limitation, any representations or warranties of the merchantability or fitness for any particular purpose, or that the application of the software, will not infringe on any patents or other proprietary rights of others. + +Company shall not be held liable for direct, indirect, incidental or consequential damages arising from any claim by User or any third party with respect to uses allowed under this Agreement, or from any use of the Software. + +User agrees to fully indemnify and hold harmless Company from and against any and all claims, demands, suits, losses, damages, costs and expenses arising out of the User's use of the Software, including, without limitation, arising out of the User's modification of the Software. + + +User may modify the Software and distribute that modified work to third parties under open-source license GPL Version 3.0 terms provided that: + +(a) if posted separately, it clearly acknowledges that it contains material copyrighted by "Atomic Private Limited" + +(b) no charge is associated with such copies, + +(c) User agrees to notify "Atomic Private Limited" of the distribution. + +(d) User clearly notifies secondary users that such modified work is not the original Software. + +(e) Code sections starting with a "license protected" comment and/or "Dex_Fee" code prefix and code-sections / software-logic related to these aforementioned sections shall not be modified by the Users/third parties and are subject of the copyright limitation covered under the foregoing Agreement. + +(f) User Agrees that he/she will not modify the following code sections and/or cryptographic keys: +https://github.com/KomodoPlatform/atomicDEX-API/blob/2fe5be95e166744667bc1fdd75fa5bb1eb5c5903/mm2src/common/common.rs#L164 +https://github.com/KomodoPlatform/atomicDEX-API/blob/2fe5be95e166744667bc1fdd75fa5bb1eb5c5903/mm2src/common/common.rs#L166 +https://github.com/KomodoPlatform/atomicDEX-API/blob/6a634c09198ff18a3825164d2ca1e597cd8ebb51/mm2src/coins/z_coin.rs#L97 +https://github.com/KomodoPlatform/atomicDEX-API/blob/2fe5be95e166744667bc1fdd75fa5bb1eb5c5903/mm2src/lp_swap.rs#L504 + +User agrees that "Atomic Private Limited", the authors of the original work and others may enjoy a royalty-free, non-exclusive license to use, copy, modify and redistribute these modifications to the Software made by the User and distributed to third parties as a derivative work under this agreement. + +This agreement will terminate immediately upon User's breach of, or non-compliance with, any of its terms. User may be held liable for any copyright infringement or the infringement of any other proprietary rights in the Software that is caused or facilitated by the User's failure to abide by the terms of this agreement. + +This agreement will be construed and enforced in accordance with the international private laws applicable to contracts performed entirely within the designated country of the Company. + +Parties agree to resolve disputes in a prompt, low-cost and mutually beneficial way. Before taking legal action or requesting legal proceedings, User will personally participate in an alternative dispute resolution procedure. Mediation and/or arbitral forum will be mutually selected and parties of the alternative dispute resolution will choose the procedure. + +The arbitration will be administered by JAMS, Inc. pursuant to its Streamlined Arbitration Rules and Procedures (the "Rules"). The Rules are available at https://www.jamsadr.com/rules-streamlined-arbitration/. + +Any arbitration must be commenced by filing a demand for arbitration within 6 months after the date the party asserting the claim first knows or reasonably should know of the act, omission or default giving rise to the claim, otherwise, such cause of action or claim is permanently barred. If applicable law provides a different limitation period for asserting claims, any claim must be asserted within the shortest time period permitted by applicable law. + +Any arbitration hearing ("Hearing") may be conducted through online means including but not limited to videoconference, upon request from either party. The Hearing will be conducted in English, and the Arbitrator may, at his or her discretion, also select a secondary language upon request by either party. + +You may be required, at Company's sole discretion, to give up any and all rights you may have to seek legal action to resolve any disputes arising from these terms through any other means, including but not limited to any court of law. + +The statute of limitations and any filing fee deadlines shall be tolled while the parties engage in the informal dispute resolution process required by this section. + +You give up your right to participate in a class action or other class proceeding. + +This Agreement is the entire agreement between the parties, and supersedes any and all prior agreements, understandings or communications, written or oral, between the parties relating to the subject matter hereof. This Agreement may be assigned by "Atomic Private Limited" and hence you accept and agree to the terms of this Agreement. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software with a limited restrictions as envisaged under the Copyright notice, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. + +The above copyright notice/Agreement and this permission notice shall be included in all copies or substantial portions of the Software. \ No newline at end of file diff --git a/LEGAL/THIRDPARTY-LICENSES b/LEGAL/THIRDPARTY-LICENSES index db260ab548..96ae454e1f 100755 --- a/LEGAL/THIRDPARTY-LICENSES +++ b/LEGAL/THIRDPARTY-LICENSES @@ -1,21 +1,25 @@ -The following third party projects are incorporated into SuperNET and their respective licenses are adopted for each of these projects. Please see the files for each project for the exact details of their licensing. Most of them are in the public domain, MIT license or GPL. +List of 3rd party licenses - Warning: this list may be incomplete. + +0BSD OR Apache-2.0 OR MIT (1): adler +Apache-2.0 (26): edit-distance, hash-db, hash256-std-hasher, hmac-drbg, libsecp256k1, libsecp256k1-core, libsecp256k1-gen-ecmult, libsecp256k1-gen-genmult, memory-db, parity-scale-codec, parity-scale-codec-derive, prost, prost-build, prost-derive, prost-types, sp-core, sp-debug-derive, sp-runtime-interface, sp-runtime-interface-proc-macro, sp-std, sp-storage, sp-tracing, sp-trie, sp-wasm-interface, trie-db, trie-root +Apache-2.0 AND BSD-2-Clause OR MIT (2): crossbeam-channel, crossbeam-queue +Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT (2): wasi, wasi +Apache-2.0 OR BSL-1.0 (1): ryu +Apache-2.0 OR ISC OR MIT (4): ct-logs, hyper-rustls, rustls, sct +Apache-2.0 OR MIT (343): addr2line, aead, aes, aes-gcm, ahash, ahash, ahash, ahash, anyhow, arc-swap, arrayvec, arrayvec, arrayvec, async-std, async-task, async-trait, atomic, atomic-shim, autocfg, autocfg, backtrace, base64, base64, base64, base64, bigdecimal, bimap, bitflags, blake2, block-buffer, block-buffer, block-padding, blocking, bs58, bumpalo, byte-tools, cache-padded, cc, cfg-if, cfg-if, chacha20, chacha20poly1305, chrono, cipher, concurrent-queue, console_error_panic_hook, const-random, const-random-macro, cpufeatures, cpuid-bool, crc, crc32fast, crossbeam, crossbeam-deque, crossbeam-epoch, crossbeam-utils, crypto-mac, crypto-mac, ctr, debug_stub_derive, derivative, digest, digest, dirs, dtoa, ed25519, either, enum-as-inner, env_logger, error-chain, ethabi, ethbloom, ethereum-types, fake-simd, fallible-iterator, fallible-streaming-iterator, fastrand, findshlibs, fixed-hash, fixedbitset, flate2, fnv, futures, futures, futures-channel, futures-core, futures-cpupool, futures-executor, futures-io, futures-macro, futures-rustls, futures-sink, futures-task, futures-timer, futures-util, getrandom, getrandom, ghash, gimli, gloo-timers, groestl, hashbrown, hashbrown, hashbrown, hashbrown, hashlink, hdrhistogram, hdrhistogram, heck, hermit-abi, hex, hex, hex-literal, hmac, hmac, http, http, httparse, httpdate, humantime, humantime, idna, idna, impl-codec, impl-rlp, impl-serde, impl-trait-for-tuples, indexmap, iovec, ipconfig, ipnet, itertools, itertools, itertools, itoa, js-sys, kv-log-macro, lazy_static, libc, libz-sys, linked-hash-map, lock_api, lock_api, lock_api, log, log, log-mdc, log4rs, lru-cache, match_cfg, maybe-uninit, miow, multimap, nodrop, nohash-hasher, ntapi, num, num-bigint, num-bigint, num-complex, num-derive, num-integer, num-iter, num-rational, num-rational, num-traits, num-traits, num_cpus, object, once_cell, opaque-debug, opaque-debug, parity-send-wrapper, parity-util-mem, parking, parking_lot, parking_lot, parking_lot, parking_lot, parking_lot_core, parking_lot_core, parking_lot_core, parking_lot_core, paste, percent-encoding, percent-encoding, pest, petgraph, pin-project, pin-project, pin-project-internal, pin-project-internal, pin-project-lite, pin-project-lite, pin-utils, pkg-config, poly1305, polyval, ppv-lite86, primitive-types, proc-macro-crate, proc-macro-crate, proc-macro-error, proc-macro-error-attr, proc-macro-hack, proc-macro-nested, proc-macro2, quick-error, quicksink, quote, quote, rand, rand, rand, rand, rand, rand_chacha, rand_chacha, rand_chacha, rand_core, rand_core, rand_core, rand_core, rand_hc, rand_hc, rand_hc, rand_isaac, rand_jitter, rand_os, rand_pcg, rand_pcg, rand_xorshift, rand_xoshiro, ref-cast, ref-cast-impl, regex, regex-syntax, remove_dir_all, resolv-conf, ripemd160, ripemd160, rlp, rlp, rust-argon2, rustc-demangle, rustc-hex, rustc-hex, rustc_version, rustc_version, scoped-tls, scopeguard, scopeguard, secrecy, semver, semver, semver-parser, semver-parser, send_wrapper, send_wrapper, serde, serde_bytes, serde_bytes, serde_derive, serde_json, serde_repr, serde_yaml, sha-1, sha-1, sha2, sha2, sha3, signal-hook, signal-hook-mio, signal-hook-registry, signature, siphasher, smallvec, smallvec, smol, snow, socket2, socket2, soketto, stable_deref_trait, static_assertions, syn, syn, synom, tc_cli_client, tc_coblox_bitcoincore, tc_core, tc_dynamodb_local, tc_elasticmq, tc_generic, tc_parity_parity, tc_postgres, tc_redis, tc_trufflesuite_ganachecli, tempfile, testcontainers, thiserror, thiserror-impl, thread-id, time, tokio-rustls, tokio-timer, toml, traitobject, trust-dns-proto, trust-dns-resolver, typenum, ucd-trie, uint, uint, unicode-bidi, unicode-normalization, unicode-segmentation, unicode-xid, unicode-xid, universal-hash, url, url, uuid, vcpkg, version_check, waker-fn, wasm-bindgen, wasm-bindgen-backend, wasm-bindgen-futures, wasm-bindgen-macro, wasm-bindgen-macro-support, wasm-bindgen-shared, wasm-bindgen-test, wasm-bindgen-test-macro, web-sys, widestring, winapi, winapi-i686-pc-windows-gnu, winapi-x86_64-pc-windows-gnu, yaml-rust, yamux, zeroize, zeroize_derive +Apache-2.0 OR MIT OR Zlib (2): tinyvec, tinyvec_macros +BSD-2-Clause (4): Inflector, arrayref, cloudabi, cloudabi +BSD-2-Clause OR MIT (1): asn1_der +BSD-3-Clause (6): curve25519-dalek, ed25519-dalek, instant, subtle, subtle, x25519-dalek +BSD-3-Clause OR MIT (2): if-addrs, if-addrs-sys +BlueOak-1.0.0 (2): minicbor, minicbor-derive +CC0-1.0 (6): constant_time_eq, keccak, secp256k1, secp256k1-sys, tiny-keccak, tiny-keccak +GPL-3.0 (10): keccak-hash, bitcrypto, chain, keys, primitives, rpc, script, serialization, serialization_derive, unexpected +ISC (2): rdrand, untrusted +MIT (117): asynchronous-codec, atomicdex-gossipsub, atty, base58, bech32, bitcoin-cash, bitcoin-cash-base, bitcoin-cash-script-macro, bitcoin-cash-slp, bitvec, blake2b_simd, build_const, byte-slice-cast, bytes, bytes, bytes, crossterm, crossterm_winapi, crunchy, crunchy, cuckoofilter, cuckoofilter, data-encoding, derive_more, enum-primitive-derive, ethbloom, ethereum-types, ethereum-types-serialize, fixed-hash, fomat-macros, fomat-macros, funty, futures_codec, generic-array, generic-array, gstuff, h2, hostname, http-body, http-body, hyper, jsonrpc-core, libp2p, libp2p-core, libp2p-dns, libp2p-floodsub, libp2p-floodsub, libp2p-mplex, libp2p-noise, libp2p-ping, libp2p-plaintext, libp2p-request-response, libp2p-swarm, libp2p-swarm-derive, libp2p-tcp, libp2p-wasm-ext, libp2p-websocket, libp2p-yamux, libsqlite3-sys, lru, lru, matches, memoffset, metrics, metrics-core, metrics-observer-prometheus, metrics-runtime, metrics-util, miniz_oxide, miniz_oxide, mio, mocktopus, mocktopus_macros, multiaddr, multihash, multihash-derive, multistream-select, ordered-float, owning_ref, parity-util-mem-derive, quanta, radium, redox_syscall, redox_users, rmp, rmp-serde, rusqlite, rust-ini, rw-stream-sink, serde-value, serde_bencode, slab, slab, spin, sql-builder, synstructure, tap, tokio, tokio-buf, tokio-macros, tokio-util, toolchain_find, tower-service, tracing, tracing-core, try-lock, typemap, unsafe-any, unsigned-varint, unsigned-varint, void, want, wasm-timer, web3, which, winreg, wyz +MIT OR Unlicense (9): aho-corasick, byteorder, byteorder, memchr, quickcheck, same-file, termcolor, walkdir, winapi-util +MPL-2.0 (3): webpki-roots, webpki-roots, wepoll-sys-stjepang +MPL-2.0+ (3): bitmaps, im, sized-chunks +specific (12): coins, common, ethcore-transaction, ethkey, fuchsia-cprng, mem, mm2, mm2-libp2p, ring (https://github.com/briansmith/ring/blob/main/LICENSE), ser_error, ser_error_derive, , webpki (https://github.com/briansmith/webpki/blob/main/LICENSE (ISC-style)) +Zlib (2): adler32, tinyvec -libtom: Tom St Denis, tomstdenis@gmail.com, http://libtom.org - -tweetnacl: http://tweetnacl.cr.yp.to/ - -curve25519: http://code.google.com/p/curve25519-donna/ and http://cr.yp.to/ecdh.html - -libtai: also from DJB http://tweetnacl.cr.yp.to/ - -SaM and vps: from Come-from-Beyond - -cJSON: Copyright (c) 2009 Dave Gamble http://sourceforge.net/projects/cjson/ - -uthash/utlist: Copyright (c) 2003-2014, Troy D. Hanson http://troydhanson.github.com/uthash/ - -inet.c: Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC") Copyright (c) 1996-1999 by Internet Software Consortium. - -libgfshare: Copyright Daniel Silverstone - -misc: there might be some other third party files not listed above, in such cases the relevant copyright header in the top of these files govern diff --git a/README.md b/README.md index 217499c5bd..b9627935c5 100755 --- a/README.md +++ b/README.md @@ -79,12 +79,12 @@ If you want to build from source, the following prerequisites are required: - [Rustup](https://rustup.rs/) - [Cmake](https://cmake.org/download/) version 3.12 or higher - OS specific build tools (e.g. [build-essential](https://linuxhint.com/install-build-essential-ubuntu/) on Linux, [XCode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) on OSX or [MSVC](https://docs.microsoft.com/en-us/cpp/build/vscpp-step-0-installation?view=vs-2017) on Win) -- (Optional) OSX: install [openssl](https://www.openssl.org/), e.g. `brew install openssl`. +- (Optional) OSX: install [openssl](https://www.openssl.org/), e.g. `brew install openssl`. - (Optional) OSX: run `LIBRARY_PATH=/usr/local/opt/openssl/lib` - Additional Rust Components ``` - rustup install nightly-2021-05-17 - rustup default nightly-2021-05-17 + rustup install nightly-2022-02-01 + rustup default nightly-2022-02-01 rustup component add rustfmt-preview ``` @@ -92,6 +92,9 @@ To build, run `cargo build` (or `cargo build -vv` to get verbose build output). For more detailed instructions, please refer to the [Installation Guide](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/get-started-atomicdex.html). +## Building WASM binary + +Please refer to the [WASM Build Guide](./docs/WASM_BUILD.md). ## Configuration @@ -165,9 +168,9 @@ Refer to the [Komodo Developer Docs](https://developers.komodoplatform.com/basic ## Additional docs for developers -- [Contribution guide](./CONTRIBUTING.md) -- [Setting up the environment to run the full tests suite](./docs/DEV_ENVIRONMENT.md) -- [Git flow and general workflow](./docs/GIT_FLOW_AND_WORKING_PROCESS.md) +- [Contribution guide](./CONTRIBUTING.md) +- [Setting up the environment to run the full tests suite](./docs/DEV_ENVIRONMENT.md) +- [Git flow and general workflow](./docs/GIT_FLOW_AND_WORKING_PROCESS.md) - [Komodo Developer Docs](https://developers.komodoplatform.com/basic-docs/atomicdex/introduction-to-atomicdex.html) diff --git a/azure-pipelines-build-stage-job.yml b/azure-pipelines-build-stage-job.yml index f2540c75f1..2efa0c5b58 100644 --- a/azure-pipelines-build-stage-job.yml +++ b/azure-pipelines-build-stage-job.yml @@ -7,6 +7,7 @@ parameters: bob_userpass: '' alice_passphrase: '' alice_userpass: '' + telegram_api_key: '' jobs: - job: ${{ parameters.name }} @@ -30,25 +31,58 @@ jobs: export TAG="$(git rev-parse --short=9 HEAD)" echo "##vso[task.setvariable variable=COMMIT_HASH]${TAG}" displayName: Setup ENV + # On MacOS, cross-compile for x86_64-apple-darwin - bash: | rm -rf upload mkdir upload - echo 2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Release > MM_VERSION + VERSION=2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Release + if ! grep -q $VERSION MM_VERSION; then + echo $VERSION > MM_VERSION + fi cat MM_VERSION - cargo build --release --bin mm2 + if [ $AGENT_OS = "Darwin" ] + then + cargo build --bin mm2 --release --target x86_64-apple-darwin + else + cargo build --bin mm2 --release + fi displayName: 'Build MM2 Release' - - bash: | - cargo test --target wasm32-unknown-unknown --release --bin mm2 - displayName: 'Test MM2 WASM' - condition: eq( variables['Agent.OS'], 'Linux' ) + condition: ne ( variables['Build.Reason'], 'PullRequest' ) env: - WASM_BINDGEN_TEST_TIMEOUT: 120 - GECKODRIVER: '/home/azureagent/wasm/geckodriver' + MANUAL_MM_VERSION: true + - task: Docker@2 + displayName: Build & Push container of dev branch + condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['Build.SourceBranchName'], 'dev' ) ) + inputs: + containerRegistry: dockerhub + repository: komodoofficial/atomicdexapi + command: buildAndPush + tags: | + dev-$(COMMIT_HASH) + dev-latest + Dockerfile: Dockerfile.dev-release + - bash: | + rm -rf atomicdex-deployments + git clone git@github.com:KomodoPlatform/atomicdex-deployments.git + if [ -d "atomicdex-deployments/atomicDEX-API" ]; then + cd atomicdex-deployments/atomicDEX-API + sed -i "1s/^.*$/$(COMMIT_HASH)/" .commit + git add .commit + git commit -m "[atomicDEX-API] $(COMMIT_HASH) is committed for git & container registry" + git push + fi + condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['Build.SourceBranchName'], 'dev' ) ) + displayName: 'Update playground deployment' # Explicit --test-threads=16 makes testing process slightly faster on agents that have <16 CPU cores. # Always run tests on mm2.1 branch and PRs + # On MacOS, run for x86_64-apple-darwin - bash: | - cargo clean -p mm2-libp2p -p mm2 - cargo test --all -- --test-threads=16 + if [ $AGENT_OS = "Darwin" ] + then + cargo test --all --target x86_64-apple-darwin -- --test-threads=16 + else + cargo test --all -- --test-threads=32 + fi displayName: 'Test MM2' timeoutInMinutes: 22 env: @@ -56,34 +90,32 @@ jobs: BOB_USERPASS: $(${{ parameters.bob_userpass }}) ALICE_PASSPHRASE: $(${{ parameters.alice_passphrase }}) ALICE_USERPASS: $(${{ parameters.alice_userpass }}) + TELEGRAM_API_KEY: $(${{ parameters.telegram_api_key }}) RUST_LOG: debug + MANUAL_MM_VERSION: true condition: or( eq( variables['Build.Reason'], 'PullRequest' ), eq( variables['Build.SourceBranchName'], 'mm2.1' ), eq( variables['Build.SourceBranchName'], 'dev' ) ) - bash: | - cargo clippy -- -D warnings - displayName: 'Check Clippy warnings' - condition: eq( variables['Agent.OS'], 'Linux' ) - - bash: | - cargo deny check advisories - displayName: 'Check Cargo deny advisories' - condition: eq( variables['Agent.OS'], 'Linux' ) - enabled: false - - bash: | - cargo fmt -- --check - displayName: 'Check rustfmt warnings' - condition: eq( variables['Agent.OS'], 'Linux' ) + containers=$(docker ps -q | wc -l) + echo $containers + if [ $containers -gt 0 ]; then + docker rm -f $(docker ps -q) + fi + displayName: 'Clean up Docker containers' + # Run unconditionally even if previous steps failed + condition: true - bash: | zip upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Release target/release/mm2 -j displayName: 'Prepare release build upload Linux' - condition: eq( variables['Agent.OS'], 'Linux' ) + condition: and ( eq( variables['Agent.OS'], 'Linux' ), ne ( variables['Build.Reason'], 'PullRequest' ) ) - bash: | - cd target/release - zip ../../upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Release mm2.dSYM mm2 -r + cd target/x86_64-apple-darwin/release + zip ../../../upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Release mm2.dSYM mm2 -r displayName: 'Prepare release build upload MacOS' - condition: eq( variables['Agent.OS'], 'Darwin' ) + condition: and ( eq( variables['Agent.OS'], 'Darwin' ), ne ( variables['Build.Reason'], 'PullRequest' ) ) - powershell: | 7z a .\upload\mm2-$(COMMIT_HASH)-$(Agent.OS)-Release.zip .\target\release\mm2.exe .\target\release\*.dll "$Env:windir\system32\msvcr100.dll" "$Env:windir\system32\msvcp140.dll" "$Env:windir\system32\vcruntime140.dll" displayName: 'Prepare release build upload Windows' - condition: eq( variables['Agent.OS'], 'Windows_NT' ) + condition: and ( eq( variables['Agent.OS'], 'Windows_NT' ), ne ( variables['Build.Reason'], 'PullRequest' ) ) # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/copy-files-over-ssh?view=vsts - task: CopyFilesOverSSH@0 inputs: @@ -93,4 +125,4 @@ jobs: targetFolder: "uploads/$(Build.SourceBranchName)" # Optional overwrite: true displayName: 'Upload nightly' - condition: ne(variables['build.sourceBranch'], 'refs/heads/mm2') \ No newline at end of file + condition: ne ( variables['Build.Reason'], 'PullRequest' ) diff --git a/azure-pipelines-lint-stage-job.yml b/azure-pipelines-lint-stage-job.yml new file mode 100644 index 0000000000..3f2ab6f495 --- /dev/null +++ b/azure-pipelines-lint-stage-job.yml @@ -0,0 +1,50 @@ +# Job template for MM2 Build + +parameters: + name: '' # defaults for any parameters that aren't specified + os: '' + bob_passphrase: '' + bob_userpass: '' + alice_passphrase: '' + alice_userpass: '' + telegram_api_key: '' + +jobs: + - job: ${{ parameters.name }} + timeoutInMinutes: 0 # 0 means infinite for self-hosted agent + pool: + name: Default + demands: agent.os -equals ${{ parameters.os }} + steps: + - checkout: self # self represents the repo where the initial Pipelines YAML file was found + clean: ${{ eq( variables['Build.Reason'], 'Schedule' ) }} # clean up only on Scheduled build + - bash: | + if [ $CLEANUP = "true" ] + then + git clean -ffdx + fi + displayName: Clean Up + failOnStderr: false + continueOnError: true + - bash: | + cargo fmt -- --check + displayName: 'Check rustfmt warnings' + env: + MANUAL_MM_VERSION: true + - bash: | + cargo clippy -- -D warnings + displayName: 'Check Clippy warnings' + env: + MANUAL_MM_VERSION: true + - bash: | + cargo check --tests + displayName: 'Check Tests' + env: + MANUAL_MM_VERSION: true + - bash: | + cargo udeps + displayName: 'Check unused dependencies' + - bash: | + cargo deny check bans --hide-inclusion-graph + cargo deny check advisories --hide-inclusion-graph + displayName: 'Cargo deny checks' diff --git a/azure-pipelines-release-stage-job.yml b/azure-pipelines-release-stage-job.yml index 5d2f92b55f..3ea90d3059 100644 --- a/azure-pipelines-release-stage-job.yml +++ b/azure-pipelines-release-stage-job.yml @@ -41,7 +41,10 @@ jobs: mkdir upload displayName: 'Recreate upload dir' - bash: | - echo 2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Debug > MM_VERSION + VERSION=2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Debug + if ! grep -q $VERSION MM_VERSION; then + echo $VERSION > MM_VERSION + fi cat MM_VERSION if [ $AGENT_OS = "Linux" ] then @@ -62,6 +65,8 @@ jobs: fi displayName: 'Build MM2 Debug' condition: eq( variables['DEBUG_UPLOADED'], '' ) + env: + MANUAL_MM_VERSION: true - bash: | zip upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Debug target-xenial/debug/mm2 target-xenial/debug/libmm2.a -j displayName: 'Prepare debug build upload Linux' @@ -77,7 +82,10 @@ jobs: condition: and( eq( variables['Agent.OS'], 'Windows_NT' ), eq( variables['DEBUG_UPLOADED'], '' ) ) - bash: | rm -f MM_VERSION - echo 2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Release > MM_VERSION + VERSION=2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Release + if ! grep -q $VERSION MM_VERSION; then + echo $VERSION > MM_VERSION + fi cat MM_VERSION touch mm2src/common/build.rs if [ $AGENT_OS = "Linux" ] @@ -99,6 +107,8 @@ jobs: fi displayName: 'Build MM2 Release' condition: eq( variables['RELEASE_UPLOADED'], '' ) + env: + MANUAL_MM_VERSION: true - bash: | objcopy --only-keep-debug target-xenial/release/mm2 target-xenial/release/mm2.debug objcopy --only-keep-debug target-xenial/release/libmm2.a target-xenial/release/libmm2.debug.a diff --git a/azure-pipelines-wasm-stage-job.yml b/azure-pipelines-wasm-stage-job.yml new file mode 100644 index 0000000000..65519832df --- /dev/null +++ b/azure-pipelines-wasm-stage-job.yml @@ -0,0 +1,72 @@ +# Job template for MM2 Build + +parameters: + name: '' # defaults for any parameters that aren't specified + os: '' + bob_passphrase: '' + bob_userpass: '' + alice_passphrase: '' + alice_userpass: '' + telegram_api_key: '' + +jobs: + - job: ${{ parameters.name }} + timeoutInMinutes: 0 # 0 means infinite for self-hosted agent + pool: + name: Default + demands: agent.os -equals ${{ parameters.os }} + steps: + - checkout: self # self represents the repo where the initial Pipelines YAML file was found + clean: ${{ eq( variables['Build.Reason'], 'Schedule' ) }} # clean up only on Scheduled build + - bash: | + if [ $CLEANUP = "true" ] + then + git clean -ffdx + fi + displayName: Clean Up + failOnStderr: false + continueOnError: true + # https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#set-a-job-scoped-variable-from-a-script + - bash: | + export TAG="$(git rev-parse --short=9 HEAD)" + echo "##vso[task.setvariable variable=COMMIT_HASH]${TAG}" + displayName: Setup ENV + # Build WASM. + - bash: | + rm -rf upload + mkdir upload + VERSION=2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Release + if ! grep -q $VERSION MM_VERSION; then + echo $VERSION > MM_VERSION + fi + cat MM_VERSION + CC=clang-8 wasm-pack build mm2src/mm2_main --release --target web --out-dir ../../target/target-wasm-release + displayName: 'Build MM2 WASM Release' + condition: ne ( variables['Build.Reason'], 'PullRequest' ) + env: + MANUAL_MM_VERSION: true + - bash: | + CC=clang-8 cargo test --package mm2_main --target wasm32-unknown-unknown --release --bin mm2 + displayName: 'Test MM2 WASM' + env: + WASM_BINDGEN_TEST_TIMEOUT: 120 + GECKODRIVER: '/home/azureagent/wasm/geckodriver' + BOB_PASSPHRASE: $(${{ parameters.bob_passphrase }}) + ALICE_PASSPHRASE: $(${{ parameters.alice_passphrase }}) + MANUAL_MM_VERSION: true + condition: or( eq( variables['Build.Reason'], 'PullRequest' ), eq( variables['Build.SourceBranchName'], 'mm2.1' ), eq( variables['Build.SourceBranchName'], 'dev' ) ) + - bash: | + cd target/target-wasm-release/ + zip ../../upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Wasm-Release mm2_bg.wasm mm2.js snippets -r + displayName: 'Prepare release WASM build upload Linux' + condition: ne ( variables['Build.Reason'], 'PullRequest' ) + # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/copy-files-over-ssh?view=vsts + - task: CopyFilesOverSSH@0 + inputs: + sshEndpoint: nightly_build_server + sourceFolder: 'upload' # Optional + contents: "**" + targetFolder: "uploads/$(Build.SourceBranchName)" # Optional + overwrite: true + displayName: 'Upload nightly' + condition: ne ( variables['Build.Reason'], 'PullRequest' ) \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4348f292da..2159a81bdd 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,9 +19,45 @@ trigger: - iguana/Readme.md - .gitignore +pr: # https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema%2Cparameter-schema#pr-trigger + drafts: false + stages: + - stage: Lint + displayName: Formatting, Clippy, and other checks + jobs: + - template: azure-pipelines-lint-stage-job.yml # Template reference + parameters: + name: 'MM2_Lint_Linux' + os: 'Linux' + + - template: azure-pipelines-lint-stage-job.yml # Template reference + parameters: + name: 'MM2_Lint_MacOS' + os: 'Darwin' + + - template: azure-pipelines-lint-stage-job.yml # Template reference + parameters: + name: 'MM2_Lint_Win' + os: 'Windows_NT' + + - stage: WASM + displayName: WASM build and test + condition: succeeded('Lint') + jobs: + - template: azure-pipelines-wasm-stage-job.yml # Template reference + parameters: + name: 'MM2_WASM_Linux' + os: 'Linux' + bob_passphrase: 'BOB_PASSPHRASE_LINUX' + bob_userpass: 'BOB_USERPASS_LINUX' + alice_passphrase: 'ALICE_PASSPHRASE_LINUX' + alice_userpass: 'ALICE_USERPASS_LINUX' + telegram_api_key: 'TELEGRAM_API_KEY' + - stage: Build displayName: Build and test Debug + condition: succeeded('WASM') jobs: - template: azure-pipelines-build-stage-job.yml # Template reference parameters: @@ -31,6 +67,7 @@ stages: bob_userpass: 'BOB_USERPASS_LINUX' alice_passphrase: 'ALICE_PASSPHRASE_LINUX' alice_userpass: 'ALICE_USERPASS_LINUX' + telegram_api_key: 'TELEGRAM_API_KEY' - template: azure-pipelines-build-stage-job.yml # Template reference parameters: @@ -40,6 +77,7 @@ stages: bob_userpass: 'BOB_USERPASS_MAC' alice_passphrase: 'ALICE_PASSPHRASE_MAC' alice_userpass: 'ALICE_USERPASS_MAC' + telegram_api_key: 'TELEGRAM_API_KEY' - template: azure-pipelines-build-stage-job.yml # Template reference parameters: @@ -49,22 +87,4 @@ stages: bob_userpass: 'BOB_USERPASS_WIN' alice_passphrase: 'ALICE_PASSPHRASE_WIN' alice_userpass: 'ALICE_USERPASS_WIN' - - - stage: Release - displayName: Release - condition: and(eq(variables['build.sourceBranch'], 'refs/heads/mm2.1'), succeeded('Build')) - jobs: - - template: azure-pipelines-release-stage-job.yml # Template reference - parameters: - name: 'MM2_Release_Linux' - os: 'Linux' - - - template: azure-pipelines-release-stage-job.yml # Template reference - parameters: - name: 'MM2_Release_MacOS' - os: 'Darwin' - - - template: azure-pipelines-release-stage-job.yml # Template reference - parameters: - name: 'MM2_Release_Windows' - os: 'Windows_NT' + telegram_api_key: 'TELEGRAM_API_KEY' diff --git a/deny.toml b/deny.toml index 5306ee72eb..c27fd4a6de 100644 --- a/deny.toml +++ b/deny.toml @@ -47,7 +47,12 @@ yanked = "warn" notice = "warn" # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. + +# RUSTSEC-2021-0113 is related to metrics-util crate that is not actively used for now despite being still present in deps tree +# RUSTSEC-2020-0071 is related to time crate, which is used only by chrono in our deps tree, remove when https://github.com/chronotope/chrono/issues/700 is resolved ignore = [ + "RUSTSEC-2021-0113", + "RUSTSEC-2020-0071", #"RUSTSEC-0000-0000", ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score @@ -143,7 +148,7 @@ registries = [ # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html [bans] # Lint level for when multiple versions of the same crate are detected -multiple-versions = "warn" +multiple-versions = "deny" # Lint level for when a crate version requirement is `*` wildcards = "allow" # The graph highlighting used when creating dotgraphs for crates @@ -167,8 +172,112 @@ deny = [ #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, ] # Certain crates/versions that will be skipped when doing duplicate detection. +# The goal is to reduce this list as much as possible skip = [ - #{ name = "ansi_term", version = "=0.11.0" }, + { name = "aes", version = "*" }, + { name = "ahash", version = "*" }, + { name = "arrayvec", version = "*" }, + { name = "autocfg", version = "*" }, + { name = "base64", version = "*" }, + { name = "bitvec", version = "*" }, + { name = "block-buffer", version = "*" }, + { name = "block-padding", version = "*" }, + { name = "byteorder", version = "*" }, + { name = "bytes", version = "*" }, + { name = "cfg-if", version = "*" }, + { name = "cipher", version = "*" }, + { name = "cloudabi", version = "*" }, + { name = "cpufeatures", version = "*" }, + { name = "crossbeam-channel", version = "*" }, + { name = "crossbeam-deque", version = "*" }, + { name = "crossbeam-epoch", version = "*" }, + { name = "crossbeam-utils", version = "*" }, + { name = "crunchy", version = "*" }, + { name = "crypto-mac", version = "*" }, + { name = "cuckoofilter", version = "*" }, + { name = "curve25519-dalek", version = "*" }, + { name = "derivation-path", version = "*" }, + { name = "digest", version = "*" }, + { name = "ed25519-dalek-bip32", version = "*" }, + { name = "env_logger", version = "*" }, + { name = "ethbloom", version = "*" }, + { name = "ethereum-types", version = "*" }, + { name = "ff", version = "*" }, + { name = "fixed-hash", version = "*" }, + { name = "funty", version = "*" }, + { name = "futures", version = "*" }, + { name = "futures-rustls", version = "*" }, + { name = "generic-array", version = "*" }, + { name = "getrandom", version = "*" }, + { name = "group", version = "*" }, + { name = "hashbrown", version = "*" }, + { name = "hdrhistogram", version = "*" }, + { name = "hex", version = "*" }, + { name = "hmac", version = "*" }, + { name = "http", version = "*" }, + { name = "http-body", version = "*" }, + { name = "humantime", version = "*" }, + { name = "idna", version = "*" }, + { name = "impl-codec", version = "*" }, + { name = "itoa", version = "*" }, + { name = "jsonrpc-core", version = "*" }, + { name = "libp2p-floodsub", version = "*" }, + { name = "libsecp256k1", version = "*" }, + { name = "libsecp256k1-core", version = "*" }, + { name = "libsecp256k1-gen-ecmult", version = "*" }, + { name = "libsecp256k1-gen-genmult", version = "*" }, + { name = "lock_api", version = "*" }, + { name = "log", version = "*" }, + { name = "memoffset", version = "*" }, + { name = "miniz_oxide", version = "*" }, + { name = "mio", version = "*" }, + { name = "num-bigint", version = "*" }, + { name = "opaque-debug", version = "*" }, + { name = "parking_lot", version = "*" }, + { name = "parking_lot_core", version = "*" }, + { name = "pbkdf2", version = "*" }, + { name = "percent-encoding", version = "*" }, + { name = "petgraph", version = "*" }, + { name = "pin-project", version = "*" }, + { name = "pin-project-internal", version = "*" }, + { name = "pin-project-lite", version = "*" }, + { name = "proc-macro-crate", version = "*" }, + { name = "proc-macro2", version = "*" }, + { name = "quote", version = "*" }, + { name = "radium", version = "*" }, + { name = "rand", version = "*" }, + { name = "rand_chacha", version = "*" }, + { name = "rand_core", version = "*" }, + { name = "rand_hc", version = "*" }, + { name = "rand_pcg", version = "*" }, + { name = "redox_syscall", version = "*" }, + { name = "redox_users", version = "*" }, + { name = "rlp", version = "*" }, + { name = "rustc-hex", version = "*" }, + { name = "rustc_version", version = "*" }, + { name = "rustls", version = "*" }, + { name = "rustls-pemfile", version = "*" }, + { name = "scopeguard", version = "*" }, + { name = "sct", version = "*" }, + { name = "semver", version = "*" }, + { name = "send_wrapper", version = "*" }, + { name = "sha2", version = "*" }, + { name = "slab", version = "*" }, + { name = "smallvec", version = "*" }, + { name = "socket2", version = "*" }, + { name = "subtle", version = "*" }, + { name = "syn", version = "*" }, + { name = "time", version = "*" }, + { name = "tiny-keccak", version = "*" }, + { name = "tokio-util", version = "*" }, + { name = "trie-root", version = "*" }, + { name = "uint", version = "*" }, + { name = "unicode-xid", version = "*" }, + { name = "unsigned-varint", version = "*" }, + { name = "url", version = "*" }, + { name = "wasi", version = "*" }, + { name = "webpki", version = "*" }, + { name = "wyz", version = "*" }, ] # Similarly to `skip` allows you to skip certain crates during duplicate # detection. Unlike skip, it also includes the entire tree of transitive diff --git a/docs/DEV_ENVIRONMENT.md b/docs/DEV_ENVIRONMENT.md index 4fe516eeac..990a502ea7 100644 --- a/docs/DEV_ENVIRONMENT.md +++ b/docs/DEV_ENVIRONMENT.md @@ -1,29 +1,70 @@ # Setting up the dev environment for AtomicDEX-API to run full tests suite -1. Install docker. -2. Download ZCash params files: [Windows](https://github.com/KomodoPlatform/komodo/blob/master/zcutil/fetch-params.bat), [Unix/Linux](https://github.com/KomodoPlatform/komodo/blob/master/zcutil/fetch-params.sh) -3. Create `.env.client` file with the following content -``` -PASSPHRASE=spice describe gravity federal blast come thank unfair canal monkey style afraid -``` -4. Create `.env.seed` file with the following content -``` -PASSPHRASE=also shoot benefit prefer juice shell elder veteran woman mimic image kidney -``` -5. MacOS specific: run script (required after each reboot) -```shell -#!/bin/bash -for ((i=2;i<256;i++)) -do - sudo ifconfig lo0 alias 127.0.0.$i up -done -``` -Please note that you have to run it again after each reboot -6. Linux specific: -``` -sudo groupadd docker -sudo usermod -aG docker $USER -``` -7. Try `cargo test --features native --all -- --test-threads=16`. +## Running native tests + +1. Install Docker or Podman. +2. Install `libudev-dev` (dpkg) or `libudev-devel` (rpm) package. +3. Install protobuf compiler, so `protoc` is available in your PATH. +4. Download ZCash params files: [Windows](https://github.com/KomodoPlatform/komodo/blob/master/zcutil/fetch-params.bat), + [Unix/Linux](https://github.com/KomodoPlatform/komodo/blob/master/zcutil/fetch-params.sh) +5. Create `.env.client` file with the following content + ``` + PASSPHRASE=spice describe gravity federal blast come thank unfair canal monkey style afraid + ``` +6. Create `.env.seed` file with the following content + ``` + PASSPHRASE=also shoot benefit prefer juice shell elder veteran woman mimic image kidney + ``` +7. MacOS specific: run script (required after each reboot) + ```shell + #!/bin/bash + for ((i=2;i<256;i++)) + do + sudo ifconfig lo0 alias 127.0.0.$i up + done + sudo ifconfig lo0 inet6 -alias ::1 + sudo ifconfig lo0 inet6 -alias fe80::1%lo0 + ``` + Please note that you have to run it again after each reboot +8. Linux specific: + - for Docker users: + ``` + sudo groupadd docker + sudo usermod -aG docker $USER + ``` + - for Podman users: + ``` + sudo ln -s $(which podman) /usr/bin/docker + ``` +9. Try `cargo test --features native --all -- --test-threads=16`. + +## Running WASM tests + +1. Set up [WASM Build Environment](../docs/WASM_BUILD.md#Setting-up-the-environment) +2. Install Firefox. +3. Download [Gecko driver](https://github.com/mozilla/geckodriver/releases) for your OS +4. Set environment variables required to run WASM tests + ```shell + # wasm-bindgen specific variables + export WASM_BINDGEN_TEST_TIMEOUT=120 + export GECKODRIVER=PATH_TO_GECKO_DRIVER_BIN + # MarketMaker specific variables + export BOB_PASSPHRASE="also shoot benefit prefer juice shell elder veteran woman mimic image kidney" + export ALICE_PASSPHRASE="spice describe gravity federal blast come thank unfair canal monkey style afraid" + ``` +6. Run WASM tests + - for Linux users: + ``` + wasm-pack test --firefox --headless mm2src/mm2_main + ``` + - for OSX users (Intel): + ``` + CC=/usr/local/opt/llvm/bin/clang AR=/usr/local/opt/llvm/bin/llvm-ar wasm-pack test --firefox --headless mm2src/mm2_main + ``` + - for OSX users (M1): + ``` + CC=/opt/homebrew/opt/llvm/bin/clang AR=/opt/homebrew/opt/llvm/bin/llvm-ar wasm-pack test --firefox --headless mm2src/mm2_main + ``` + Please note `CC` and `AR` must be specified in the same line as `wasm-pack test mm2src/mm2_main`. PS If you notice that this guide is outdated, please submit a PR. diff --git a/docs/GIT_FLOW_AND_WORKING_PROCESS.md b/docs/GIT_FLOW_AND_WORKING_PROCESS.md index 84cc75b0ca..7f010a669d 100644 --- a/docs/GIT_FLOW_AND_WORKING_PROCESS.md +++ b/docs/GIT_FLOW_AND_WORKING_PROCESS.md @@ -5,7 +5,7 @@ 3. The dev is merged to mm2.1 when a new release is planned. 4. The feature branch lifetime should be <= 1-2 weeks. 5. Big task should be decomposed to the several feature branches. Each of which is merged periodically with a code review process. The new feature branch should be started from dev afterward. -6. In certain cases, the dev might be not in a "releasable" state. The feature branch might be merged directly to mm2.1 in this case if required (hotfix, very useful feature, blocker). Dev is synced with mm2.1 afterward. +6. In certain cases, the dev might not be in a "releasable" state. In this case, if the feature branch has no dependencies updates, it might be merged directly to mm2.1 if required (hotfix, very useful feature, blocker). Dev is synced with mm2.1 afterward. 7. For convenience, we can consider making several minor features/fixes in the single feature branch. Pros: @@ -20,7 +20,8 @@ Cons: 1. It's desired to have a separate issue for any bug report or feature request. 2. Once the issue is created, add it to the MM2.0 Github project. Select an appropriate column. -3. Decide whether you should base your feature branch from mm2.1 or dev. For hotfixes or minor useful features choose mm2.1. In other cases choose dev. +3. Decide whether you should base your feature branch on mm2.1 or dev. For hotfixes or minor useful features that don't include any dependencies updates choose mm2.1. In other cases choose dev. +5. PR titles must have a prefix that displays the current status of PR. Such as `[wip] X feat integration`, `[r2r] X feat integration`, where `[wip]` prefix stands for "Work in Progress", and `[r2r]` for Ready to Review. 4. PRs to dev can be merged right after approval. Request the tests in the dev branch from Tony by assigning the issue to him and moving it to the `Testing` column. Provide a detailed explanation of what changed and what should be tested. Indicate the critical points. 5. PRs to mm2.1 must be tested by QA *before* merging. 6. If documentation update is required, prepare examples and notify smk762. Assign issue to him. Move the issue to the documentation column. Smk will then prepare PR in [developer-docs](https://github.com/KomodoPlatform/developer-docs) repo. @@ -30,5 +31,5 @@ Cons: [@artemii235](https://github.com/artemii235) [@sergeyboyko0791](https://github.com/sergeyboyko0791) [@shamardy](https://github.com/shamardy) -[@Milerius](https://github.com/Milerius) +[@ozkanonur](https://github.com/ozkanonur) diff --git a/docs/WASM_BUILD.md b/docs/WASM_BUILD.md new file mode 100644 index 0000000000..c2e5bd7bee --- /dev/null +++ b/docs/WASM_BUILD.md @@ -0,0 +1,42 @@ +# Building WASM binary + +## Setting up the environment + +To build WASM binary from source, the following prerequisites are required: + +1. Install `wasm-pack` + ``` + cargo install wasm-pack + ``` +2. OSX specific: install `llvm` + ``` + brew install llvm + ``` + +## Compiling WASM release binary + +To build WASM release binary run one of the following commands according to your environment: + +- for Linux users: + ``` + wasm-pack build mm2src/mm2_main --target web --out-dir wasm_build/deps/pkg/ + ``` +- for OSX users (Intel): + ``` + CC=/usr/local/opt/llvm/bin/clang AR=/usr/local/opt/llvm/bin/llvm-ar wasm-pack build mm2src/mm2_main --target web --out-dir wasm_build/deps/pkg/ + ``` +- for OSX users (M1): + ``` + CC=/opt/homebrew/opt/llvm/bin/clang AR=/opt/homebrew/opt/llvm/bin/llvm-ar wasm-pack build mm2src/mm2_main --target web --out-dir wasm_build/deps/pkg/ + ``` + +Please note `CC` and `AR` must be specified in the same line as `wasm-pack test mm2src/mm2_main`. + +## Compiling WASM binary with debug symbols + +If you want to disable optimizations to reduce the compilation time, run `wasm-pack build mm2src/mm2_main` with an additional `--dev` flag: +``` +wasm-pack build mm2src/mm2_main --target web --out-dir wasm_build/deps/pkg/ --dev +``` + +Please don't forget to specify `CC` and `AR` if you run the command on OSX. \ No newline at end of file diff --git a/etomic_build/client/buy_ONE_ANOTHER b/etomic_build/client/buy_ONE_ANOTHER index 84545b5aec..62bea8f117 100755 --- a/etomic_build/client/buy_ONE_ANOTHER +++ b/etomic_build/client/buy_ONE_ANOTHER @@ -6,7 +6,7 @@ curl --url "http://127.0.0.1:7783" --data ' "method":"buy", "base":"'$1'", "rel":"'$2'", - "volume":"0.1", - "price":"2" + "volume":"0.777", + "price":"1" } ' diff --git a/etomic_build/client/enable_USDF b/etomic_build/client/enable_USDF new file mode 100755 index 0000000000..965a1d2345 --- /dev/null +++ b/etomic_build/client/enable_USDF @@ -0,0 +1,13 @@ +#!/bin/bash +source userpass +curl --url "http://127.0.0.1:7783" --data '{ + "userpass":"'$userpass'", + "method":"enable_slp", + "mmrpc":"2.0", + "params":{ + "ticker":"USDF", + "activation_params": { + "required_confirmations": 1 + } + } +}' diff --git a/etomic_build/client/enable_tBCH b/etomic_build/client/enable_tBCH index 0fb6f2f2fa..18f7c033a2 100755 --- a/etomic_build/client/enable_tBCH +++ b/etomic_build/client/enable_tBCH @@ -1,3 +1,14 @@ #!/bin/bash source userpass -curl --url "http://127.0.0.1:7783" --data "{\"userpass\":\"$userpass\",\"method\":\"electrum\",\"coin\":\"tBCH\",\"servers\":[{\"url\":\"bch0.kister.net:51002\",\"protocol\":\"SSL\"},{\"url\":\"electroncash.de:50004\",\"protocol\":\"SSL\"},{\"url\":\"electrs.electroncash.de:60002\",\"protocol\":\"SSL\"},{\"url\":\"testnet.bitcoincash.network:60002\",\"protocol\":\"SSL\"},{\"url\":\"electroncash.de:50004\",\"protocol\":\"SSL\"}]}" +curl --url "http://127.0.0.1:7783" --data '{ + "userpass":"'$userpass'", + "method":"electrum", + "coin":"tBCH", + "servers":[ + {"url":"tbch.loping.net:60002","protocol":"SSL"}, + {"url":"electroncash.de:50004","protocol":"SSL"}, + {"url":"testnet.bitcoincash.network:60002","protocol":"SSL"}, + {"url":"electrs.electroncash.de:60002","protocol":"SSL"} + ], + "bchd_urls": ["https://bchd-testnet.electroncash.de:18335"] +}' diff --git a/etomic_build/client/enable_tBCH_USDF b/etomic_build/client/enable_tBCH_USDF new file mode 100755 index 0000000000..4d3531ac12 --- /dev/null +++ b/etomic_build/client/enable_tBCH_USDF @@ -0,0 +1,54 @@ +#!/bin/bash +source userpass +curl --url "http://127.0.0.1:7783" --data '{ + "userpass":"'$userpass'", + "method":"enable_bch_with_tokens", + "mmrpc":"2.0", + "params":{ + "ticker":"tBCH", + "allow_slp_unsafe_conf":false, + "bchd_urls":[ + "https://bchd-testnet.electroncash.de:18335" + ], + "mode":{ + "rpc":"Electrum", + "rpc_data":{ + "servers":[ + { + "url":"electroncash.de:50003" + }, + { + "url":"tbch.loping.net:60001" + }, + { + "url":"blackie.c3-soft.com:60001" + }, + { + "url":"bch0.kister.net:51001" + }, + { + "url":"testnet.imaginary.cash:50001" + } + ] + } + }, + "tx_history":true, + "slp_tokens_requests":[ + { + "ticker":"USDF", + "required_confirmations": 1 + } + ], + "required_confirmations": 1, + "requires_notarization":false, + "address_format":{ + "format":"cashaddress", + "network":"bchtest" + }, + "utxo_merge_params":{ + "merge_at":50, + "check_every":10, + "max_merge_at_once":25 + } + } +}' \ No newline at end of file diff --git a/etomic_build/client/my_tx_history_from_id b/etomic_build/client/my_tx_history_from_id new file mode 100755 index 0000000000..a72e5deebb --- /dev/null +++ b/etomic_build/client/my_tx_history_from_id @@ -0,0 +1,14 @@ +#!/bin/bash +source userpass +curl --url "http://127.0.0.1:7783" --data '{ + "userpass":"'$userpass'", + "method":"my_tx_history", + "mmrpc":"2.0", + "params": { + "coin": "USDF", + "limit": 2, + "paging_options": { + "FromId": "433b641bc89e1b59c22717918583c60ec98421805c8e85b064691705d9aeb970" + } + } +}' diff --git a/etomic_build/client/my_tx_history_page_number b/etomic_build/client/my_tx_history_page_number new file mode 100755 index 0000000000..14eb61deb3 --- /dev/null +++ b/etomic_build/client/my_tx_history_page_number @@ -0,0 +1,14 @@ +#!/bin/bash +source userpass +curl --url "http://127.0.0.1:7783" --data '{ + "userpass":"'$userpass'", + "method":"my_tx_history", + "mmrpc":"2.0", + "params": { + "coin": "tBCH", + "limit": 2, + "paging_options": { + "PageNumber": 2 + } + } +}' diff --git a/etomic_build/client/validate_address b/etomic_build/client/validate_address new file mode 100644 index 0000000000..b6a7285310 --- /dev/null +++ b/etomic_build/client/validate_address @@ -0,0 +1,10 @@ +#!/bin/bash +source userpass +curl --url "http://127.0.0.1:7783" --data ' +{ + "userpass":"'$userpass'", + "method":"validateaddress", + "coin":"'$1'", + "address":"'$2'" +} +' diff --git a/js/defined-in-js.js b/js/defined-in-js.js deleted file mode 100644 index 214a83ca2a..0000000000 --- a/js/defined-in-js.js +++ /dev/null @@ -1,53 +0,0 @@ -//! The wasm plug functions. - -// pub fn host_ensure_dir_is_writable(ptr: *const c_char, len: i32) -> i32; -export function host_ensure_dir_is_writable(ptr, len) { -} - -// pub fn host_env(name: *const c_char, nameˡ: i32, rbuf: *mut c_char, rcap: i32) -> i32; -export function host_env(name, name2, rbuf, rcap) { - return 0; -} - -// pub fn host_slurp(path_p: *const c_char, path_l: i32, rbuf: *mut c_char, rcap: i32) -> i32; -export function host_slurp(path_p, path_l, rbuf, rcap) { - return 0; -} - -// pub fn temp_dir(rbuf: *mut c_char, rcap: i32) -> i32; -export function temp_dir(rbuf, rcap) { - return 0; -} - -// pub fn host_rm(ptr: *const c_char, len: i32) -> i32; -export function host_rm(ptr, len) { - return 0; -} - -// pub fn host_write(path_p: *const c_char, path_l: i32, ptr: *const c_char, len: i32) -> i32; -export function host_write(path_p, path_l, ptr, len) { - return 0; -} - -// pub fn host_read_dir(path_p: *const c_char, path_l: i32, rbuf: *mut c_char, rcap: i32) -> i32; -export function host_read_dir(path_p, path_l, rbuf, rcap) { - return 0; -} - -// fn http_helper_if(helper: *const u8, helper_len: i32, payload: *const u8, payload_len: i32, timeout_ms: i32) -> i32; -export function http_helper_if(helper, helper_len, payload, payload_len, timeout_ms) { -} - -// pub fn http_helper_check(helper_request_id: i32, rbuf: *mut u8, rcap: i32) -> i32; -export function http_helper_check(helper_request_id, rbuf, rcap) { - return 0; -} - -// pub fn call_back(cb_id: i32, ptr: *const c_char, len: i32); -export function call_back(cb_id, ptr, len) { -} - -// fn sleep(ms: u32) -> Promise; -export function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index f2947ae57a..d541a1153b 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2018" [features] -zhtlc = ["zcash_client_backend", "zcash_primitives", "zcash_proofs"] +zhtlc-native-tests = [] [lib] name = "coins" @@ -13,43 +13,62 @@ doctest = false [dependencies] async-std = { version = "1.5", features = ["unstable"] } -async-trait = "0.1" +async-trait = "0.1.52" base64 = "0.10.0" -bigdecimal = { version = "0.1.0", features = ["serde"] } -bitcoin-cash-slp = "0.3.1" +base58 = "0.2.0" +bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } +bitcoin = "0.27.1" +bitcoin_hashes = "0.10.0" bitcrypto = { path = "../mm2_bitcoin/crypto" } +bincode = "1.3.3" byteorder = "1.3" bytes = "0.4" cfg-if = "1.0" chain = { path = "../mm2_bitcoin/chain" } common = { path = "../common" } +crossbeam = "0.7" +crypto = { path = "../crypto" } +db_common = { path = "../db_common" } derive_more = "0.99" +ed25519-dalek = "1.0.1" +ed25519-dalek-bip32 = "0.2.0" ethabi = { git = "https://github.com/artemii235/ethabi" } ethcore-transaction = { git = "https://github.com/artemii235/parity-ethereum.git" } ethereum-types = { version = "0.4", default-features = false, features = ["std", "serialize"] } ethkey = { git = "https://github.com/artemii235/parity-ethereum.git" } # Waiting for https://github.com/rust-lang/rust/issues/54725 to use on Stable. #enum_dispatch = "0.1" -fomat-macros = "0.2" futures01 = { version = "0.1", package = "futures" } # using select macro requires the crate to be named futures, compilation failed with futures03 name futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } gstuff = { version = "0.7", features = ["nightly"] } -hex = "0.3.2" +hex = "0.4.2" http = "0.2" -itertools = "0.9" +itertools = { version = "0.10", features = ["use_std"] } jsonrpc-core = "8.0.1" keys = { path = "../mm2_bitcoin/keys" } lazy_static = "1.4" libc = "0.2" +lightning = { git = "https://github.com/shamardy/rust-lightning", branch = "0.0.106" } +lightning-background-processor = { path = "lightning_background_processor" } +lightning-invoice = { git = "https://github.com/shamardy/rust-lightning", branch = "0.0.106" } metrics = "0.12" +mm2_core = { path = "../mm2_core" } +mm2_err_handle = { path = "../mm2_err_handle" } +mm2_io = { path = "../mm2_io" } +mm2_net = { path = "../mm2_net" } +mm2_number = { path = "../mm2_number" } mocktopus = "0.7.0" num-traits = "0.2" +parking_lot = { version = "0.12.0", features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } +prost = "0.10" +protobuf = "2.20" rand = { version = "0.7", features = ["std", "small_rng"] } rlp = { git = "https://github.com/artemii235/parity-common" } rmp-serde = "0.14.3" rpc = { path = "../mm2_bitcoin/rpc" } +rpc_task = { path = "../rpc_task" } script = { path = "../mm2_bitcoin/script" } secp256k1 = { version = "0.20" } ser_error = { path = "../derives/ser_error" } @@ -59,27 +78,51 @@ serde_derive = "1.0" serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } serialization = { path = "../mm2_bitcoin/serialization" } serialization_derive = { path = "../mm2_bitcoin/serialization_derive" } -sha2 = "0.8" -sha3 = "0.8" +spv_validation = { path = "../mm2_bitcoin/spv_validation" } +sha2 = "0.9" +sha3 = "0.9" +utxo_signer = { path = "utxo_signer" } +tiny-bip39 = "0.8.0" # One of web3 dependencies is the old `tokio-uds 0.1.7` which fails cross-compiling to ARM. # We don't need the default web3 features at all since we added our own web3 transport using shared HYPER instance. web3 = { git = "https://github.com/artemii235/rust-web3", default-features = false } -winapi = "0.3" +zbase32 = "0.1.2" [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = { version = "0.3.27" } -wasm-bindgen = { version = "0.2.50", features = ["serde-serialize", "nightly"] } +mm2_db = { path = "../mm2_db" } +mm2_test_helpers = { path = "../mm2_test_helpers" } +wasm-bindgen = { version = "0.2.50", features = ["nightly"] } wasm-bindgen-futures = { version = "0.4.1" } wasm-bindgen-test = { version = "0.3.2" } -web-sys = { version = "0.3.4", features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } +web-sys = { version = "0.3.55", features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dirs = { version = "1" } +lightning-persister = { path = "lightning_persister" } +lightning-net-tokio = { git = "https://github.com/shamardy/rust-lightning", branch = "0.0.106" } rust-ini = { version = "0.13" } -rustls = { version = "0.19", features = ["dangerous_configuration"] } +rustls = { version = "0.20", features = ["dangerous_configuration"] } tokio = { version = "1.7" } -tokio-rustls = { version = "0.22.0" } -webpki-roots = { version = "0.19.0" } -zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", optional = true } -zcash_primitives = { features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", optional = true } -zcash_proofs = { features = ["bundled-prover"], git = "https://github.com/KomodoPlatform/librustzcash.git", optional = true } +tokio-rustls = { version = "0.23" } +tonic = { version = "0.7", features = ["tls", "tls-webpki-roots", "compression"] } +webpki-roots = { version = "0.22" } +solana-client = { version = "1", default-features = false } +solana-sdk = { version = "1", default-features = false } +solana-transaction-status = "1" +spl-token = { version = "3" } +spl-associated-token-account = "1" +zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git" } +zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git" } +zcash_primitives = { features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git" } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git" } + +[target.'cfg(windows)'.dependencies] +winapi = "0.3" + +[dev-dependencies] +mm2_test_helpers = { path = "../mm2_test_helpers" } + +[build-dependencies] +prost-build = { version = "0.10.3", default-features = false } +tonic-build = { version = "0.7", features = ["prost", "compression"] } diff --git a/mm2src/coins/build.rs b/mm2src/coins/build.rs new file mode 100644 index 0000000000..2625bb9ded --- /dev/null +++ b/mm2src/coins/build.rs @@ -0,0 +1,10 @@ +fn main() { + let mut prost = prost_build::Config::new(); + prost.out_dir("utxo"); + prost.compile_protos(&["utxo/bchrpc.proto"], &["utxo"]).unwrap(); + + tonic_build::configure() + .build_server(false) + .compile(&["z_coin/service.proto"], &["z_coin"]) + .unwrap(); +} diff --git a/mm2src/coins/coin_balance.rs b/mm2src/coins/coin_balance.rs new file mode 100644 index 0000000000..da1b49eef2 --- /dev/null +++ b/mm2src/coins/coin_balance.rs @@ -0,0 +1,330 @@ +use crate::hd_pubkey::HDXPubExtractor; +use crate::hd_wallet::{HDWalletCoinOps, NewAccountCreatingError}; +use crate::{BalanceError, BalanceResult, CoinBalance, CoinWithDerivationMethod, DerivationMethod, HDAddress, + MarketCoinOps}; +use async_trait::async_trait; +use common::custom_iter::TryUnzip; +use common::log::{debug, info}; +use crypto::{Bip44Chain, RpcDerivationPath}; +use derive_more::Display; +use futures::compat::Future01CompatExt; +use mm2_err_handle::prelude::*; +use std::fmt; +use std::ops::Range; + +pub type AddressIdRange = Range; + +#[derive(Display)] +pub enum EnableCoinBalanceError { + NewAccountCreatingError(NewAccountCreatingError), + BalanceError(BalanceError), +} + +impl From for EnableCoinBalanceError { + fn from(e: NewAccountCreatingError) -> Self { EnableCoinBalanceError::NewAccountCreatingError(e) } +} + +impl From for EnableCoinBalanceError { + fn from(e: BalanceError) -> Self { EnableCoinBalanceError::BalanceError(e) } +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(tag = "wallet_type")] +pub enum EnableCoinBalance { + Iguana(IguanaWalletBalance), + HD(HDWalletBalance), +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct IguanaWalletBalance { + pub address: String, + pub balance: CoinBalance, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct HDWalletBalance { + pub accounts: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct HDAccountBalance { + pub account_index: u32, + pub derivation_path: RpcDerivationPath, + pub total_balance: CoinBalance, + pub addresses: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct HDAddressBalance { + pub address: String, + pub derivation_path: RpcDerivationPath, + pub chain: Bip44Chain, + pub balance: CoinBalance, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum EnableCoinScanPolicy { + /// Don't scan for new addresses. + DoNotScan, + /// Scan for new addresses if the coin HD wallet hasn't been enabled *only*. + /// In other words, scan for new addresses if there were no HD accounts in the HD wallet storage. + ScanIfNewWallet, + /// Scan for new addresses even if the coin HD wallet has been enabled before. + Scan, +} + +impl Default for EnableCoinScanPolicy { + fn default() -> Self { EnableCoinScanPolicy::ScanIfNewWallet } +} + +#[async_trait] +pub trait EnableCoinBalanceOps { + async fn enable_coin_balance( + &self, + xpub_extractor: &XPubExtractor, + scan_policy: EnableCoinScanPolicy, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Sync; +} + +#[async_trait] +impl EnableCoinBalanceOps for Coin +where + Coin: CoinWithDerivationMethod::HDWallet> + + HDWalletBalanceOps + + MarketCoinOps + + Sync, + ::Address: fmt::Display + Sync, +{ + async fn enable_coin_balance( + &self, + xpub_extractor: &XPubExtractor, + scan_policy: EnableCoinScanPolicy, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Sync, + { + match self.derivation_method() { + DerivationMethod::Iguana(my_address) => self + .my_balance() + .compat() + .await + .map(|balance| { + EnableCoinBalance::Iguana(IguanaWalletBalance { + address: my_address.to_string(), + balance, + }) + }) + .mm_err(EnableCoinBalanceError::from), + DerivationMethod::HDWallet(hd_wallet) => self + .enable_hd_wallet(hd_wallet, xpub_extractor, scan_policy) + .await + .map(EnableCoinBalance::HD), + } + } +} + +#[async_trait] +pub trait HDWalletBalanceOps: HDWalletCoinOps { + type HDAddressScanner: HDAddressBalanceScanner
; + + async fn produce_hd_address_scanner(&self) -> BalanceResult; + + /// Requests balances of already known addresses, and if it's prescribed by [`EnableCoinParams::scan_policy`], + /// scans for new addresses of every HD account by using [`HDWalletBalanceOps::scan_for_new_addresses`]. + /// This method is used on coin initialization to index working addresses and to return the wallet balance to the user. + async fn enable_hd_wallet( + &self, + hd_wallet: &Self::HDWallet, + xpub_extractor: &XPubExtractor, + scan_policy: EnableCoinScanPolicy, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Sync; + + /// Scans for the new addresses of the specified `hd_account` using the given `address_scanner`. + /// Returns balances of the new addresses. + async fn scan_for_new_addresses( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut Self::HDAccount, + address_scanner: &Self::HDAddressScanner, + gap_limit: u32, + ) -> BalanceResult>; + + /// Requests balances of every known addresses of the given `hd_account`. + async fn all_known_addresses_balances(&self, hd_account: &Self::HDAccount) -> BalanceResult>; + + /// Requests balances of known addresses of the given `address_ids` addresses at the specified `chain`. + async fn known_addresses_balances_with_ids( + &self, + hd_account: &Self::HDAccount, + chain: Bip44Chain, + address_ids: Ids, + ) -> BalanceResult> + where + Self::Address: fmt::Display + Clone, + Ids: Iterator + Send, + { + let (addresses, der_paths) = address_ids + .into_iter() + .map(|address_id| -> BalanceResult<_> { + let HDAddress { + address, + derivation_path, + .. + } = self.derive_address(hd_account, chain, address_id)?; + Ok((address, derivation_path)) + }) + // Try to unzip `Result<(Address, DerivationPath)>` elements into `Result<(Vec
, Vec)>`. + .try_unzip::, Vec<_>>()?; + + let balances = self + .known_addresses_balances(addresses) + .await? + .into_iter() + // [`HDWalletBalanceOps::known_addresses_balances`] returns pairs `(Address, CoinBalance)` + // that are guaranteed to be in the same order in which they were requested. + // So we can zip the derivation paths with the pairs `(Address, CoinBalance)`. + .zip(der_paths) + .map(|((address, balance), derivation_path)| HDAddressBalance { + address: address.to_string(), + derivation_path: RpcDerivationPath(derivation_path), + chain, + balance, + }) + .collect(); + Ok(balances) + } + + /// Requests balance of the given `address`. + /// This function is expected to be more efficient than ['HDWalletBalanceOps::is_address_used'] in most cases + /// since many of RPC clients allow us to request the address balance without the history. + async fn known_address_balance(&self, address: &Self::Address) -> BalanceResult; + + /// Requests balances of the given `addresses`. + /// The pairs `(Address, CoinBalance)` are guaranteed to be in the same order in which they were requested. + async fn known_addresses_balances( + &self, + addresses: Vec, + ) -> BalanceResult>; + + /// Checks if the address has been used by the user by checking if the transaction history of the given `address` is not empty. + /// Please note the function can return zero balance even if the address has been used before. + async fn is_address_used( + &self, + address: &Self::Address, + address_scanner: &Self::HDAddressScanner, + ) -> BalanceResult> { + if !address_scanner.is_address_used(address).await? { + return Ok(AddressBalanceStatus::NotUsed); + } + // Now we know that the address has been used. + let balance = self.known_address_balance(address).await?; + Ok(AddressBalanceStatus::Used(balance)) + } +} + +#[async_trait] +pub trait HDAddressBalanceScanner: Sync { + type Address; + + async fn is_address_used(&self, address: &Self::Address) -> BalanceResult; +} + +pub enum AddressBalanceStatus { + Used(Balance), + NotUsed, +} + +pub mod common_impl { + use super::*; + use crate::hd_wallet::{HDAccountOps, HDWalletOps}; + + pub(crate) async fn enable_hd_account( + coin: &Coin, + hd_wallet: &Coin::HDWallet, + hd_account: &mut Coin::HDAccount, + address_scanner: &Coin::HDAddressScanner, + scan_new_addresses: bool, + ) -> MmResult + where + Coin: HDWalletBalanceOps + Sync, + { + let gap_limit = hd_wallet.gap_limit(); + let mut addresses = coin.all_known_addresses_balances(hd_account).await?; + if scan_new_addresses { + addresses.extend( + coin.scan_for_new_addresses(hd_wallet, hd_account, address_scanner, gap_limit) + .await?, + ); + } + + let total_balance = addresses.iter().fold(CoinBalance::default(), |total, addr_balance| { + total + addr_balance.balance.clone() + }); + let account_balance = HDAccountBalance { + account_index: hd_account.account_id(), + derivation_path: RpcDerivationPath(hd_account.account_derivation_path()), + total_balance, + addresses, + }; + + Ok(account_balance) + } + + pub(crate) async fn enable_hd_wallet( + coin: &Coin, + hd_wallet: &Coin::HDWallet, + xpub_extractor: &XPubExtractor, + scan_policy: EnableCoinScanPolicy, + ) -> MmResult + where + Coin: HDWalletBalanceOps + MarketCoinOps + Sync, + XPubExtractor: HDXPubExtractor + Sync, + { + let mut accounts = hd_wallet.get_accounts_mut().await; + let address_scanner = coin.produce_hd_address_scanner().await?; + + let mut result = HDWalletBalance { + accounts: Vec::with_capacity(accounts.len() + 1), + }; + + if accounts.is_empty() { + // Is seems that we couldn't find any HD account from the HD wallet storage. + drop(accounts); + info!( + "{} HD wallet hasn't been enabled before. Create default HD account", + coin.ticker() + ); + + // Create new HD account. + let mut new_account = coin.create_new_account(hd_wallet, xpub_extractor).await?; + let scan_new_addresses = matches!( + scan_policy, + EnableCoinScanPolicy::ScanIfNewWallet | EnableCoinScanPolicy::Scan + ); + + let account_balance = + enable_hd_account(coin, hd_wallet, &mut new_account, &address_scanner, scan_new_addresses).await?; + result.accounts.push(account_balance); + return Ok(result); + } + + debug!( + "{} HD accounts were found on {} coin activation", + accounts.len(), + coin.ticker() + ); + let scan_new_addresses = matches!(scan_policy, EnableCoinScanPolicy::Scan); + for (_account_id, hd_account) in accounts.iter_mut() { + let account_balance = + enable_hd_account(coin, hd_wallet, hd_account, &address_scanner, scan_new_addresses).await?; + result.accounts.push(account_balance); + } + + Ok(result) + } +} diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 80b7765f44..1a2f6dc78e 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -1,13 +1,15 @@ /****************************************************************************** - * Copyright © 2014-2019 The SuperNET Developers. * + * Copyright © 2022 Atomic Private Limited and its contributors * * * - * See the AUTHORS, DEVELOPER-AGREEMENT and LICENSE files at * + * See the CONTRIBUTOR-LICENSE-AGREEMENT, COPYING, LICENSE-COPYRIGHT-NOTICE * + * and DEVELOPER-CERTIFICATE-OF-ORIGIN files in the LEGAL directory in * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * - * SuperNET software, including this file may be copied, modified, propagated * - * or distributed except according to the terms contained in the LICENSE file * + * AtomicDEX software, including this file may be copied, modified, propagated* + * or distributed except according to the terms contained in the * + * LICENSE-COPYRIGHT-NOTICE file. * * * * Removal or modification of this copyright notice is prohibited. * * * @@ -16,57 +18,62 @@ // eth.rs // marketmaker // -// Copyright © 2017-2019 SuperNET. All rights reserved. +// Copyright © 2022 AtomicDEX. All rights reserved. // -use bigdecimal::BigDecimal; -use bitcrypto::sha256; -use common::custom_futures::TimedAsyncMutex; +use async_trait::async_trait; +use bitcrypto::{keccak256, sha256}; use common::executor::Timer; -use common::log::error; -use common::mm_ctx::{MmArc, MmWeak}; -use common::mm_error::prelude::*; -use common::{now_ms, slurp_url, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::log::{error, info, warn}; +use common::{now_ms, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; +use crypto::privkey::key_pair_from_secret; use derive_more::Display; use ethabi::{Contract, Token}; +pub use ethcore_transaction::SignedTransaction as SignedEthTx; use ethcore_transaction::{Action, Transaction as UnSignedEthTx, UnverifiedTransaction}; -use ethereum_types::{Address, H160, U256}; -use ethkey::{public_to_address, KeyPair, Public}; +use ethereum_types::{Address, H160, H256, U256}; +use ethkey::{public_to_address, KeyPair, Public, Signature}; +use ethkey::{sign, verify_address}; use futures::compat::Future01CompatExt; use futures::future::{join_all, select, Either, FutureExt, TryFutureExt}; use futures01::Future; use http::StatusCode; +use mm2_core::mm_ctx::{MmArc, MmWeak}; +use mm2_err_handle::prelude::*; +use mm2_net::transport::{slurp_url, SlurpError}; +use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; use rand::seq::SliceRandom; use rpc::v1::types::Bytes as BytesJson; use secp256k1::PublicKey; use serde_json::{self as json, Value as Json}; +use serialization::{CompactInteger, Serializable, Stream}; use sha3::{Digest, Keccak256}; -use std::cmp::Ordering; use std::collections::HashMap; use std::ops::Deref; use std::path::PathBuf; use std::str::FromStr; -use std::sync::atomic::{AtomicU64, Ordering as AtomicOrderding}; +use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; use std::sync::{Arc, Mutex}; use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallRequest, FilterBuilder, Log, Trace, TraceFilterBuilder, Transaction as Web3Transaction, TransactionId}; use web3::{self, Web3}; +use web3_transport::{EthFeeHistoryNamespace, Web3Transport}; -use super::{BalanceError, BalanceFut, CoinBalance, CoinProtocol, CoinTransportMetrics, CoinsContext, FeeApproxStage, - FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, - NumConversResult, RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, SwapOps, - TradeFee, TradePreimageError, TradePreimageFut, TradePreimageValue, Transaction, TransactionDetails, - TransactionEnum, TransactionFut, ValidateAddressResult, WithdrawError, WithdrawFee, WithdrawFut, - WithdrawRequest, WithdrawResult}; -pub use ethcore_transaction::SignedTransaction as SignedEthTx; -pub use rlp; +use super::{AsyncMutex, BalanceError, BalanceFut, CoinBalance, CoinProtocol, CoinTransportMetrics, CoinsContext, + FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, + NumConversError, NumConversResult, RawTransactionError, RawTransactionFut, RawTransactionRequest, + RawTransactionRes, RawTransactionResult, RpcClientType, RpcTransportEventHandler, + RpcTransportEventHandlerShared, SearchForSwapTxSpendInput, SignatureError, SignatureResult, SwapOps, + TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, UnexpectedDerivationMethod, + ValidateAddressResult, ValidatePaymentInput, VerificationError, VerificationResult, WithdrawError, + WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; -mod web3_transport; -use common::mm_number::MmNumber; -use web3_transport::{EthFeeHistoryNamespace, Web3Transport}; +pub use rlp; #[cfg(test)] mod eth_tests; #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; +mod web3_transport; /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol /// Dev chain (195.201.0.6:8565) contract address: 0xa09ad3cd7e96586ebd05a2607ee56b56fb2db8fd @@ -111,16 +118,33 @@ pub type GasStationResult = Result>; #[derive(Debug, Display)] pub enum GasStationReqErr { - #[display(fmt = "Transport: {}", _0)] - Transport(String), + #[display(fmt = "Transport '{}' error: {}", uri, error)] + Transport { + uri: String, + error: String, + }, #[display(fmt = "Invalid response: {}", _0)] InvalidResponse(String), + Internal(String), } impl From for GasStationReqErr { fn from(e: serde_json::Error) -> Self { GasStationReqErr::InvalidResponse(e.to_string()) } } +impl From for GasStationReqErr { + fn from(e: SlurpError) -> Self { + let error = e.to_string(); + match e { + SlurpError::ErrorDeserializing { .. } => GasStationReqErr::InvalidResponse(error), + SlurpError::Transport { uri, .. } | SlurpError::Timeout { uri, .. } => { + GasStationReqErr::Transport { uri, error } + }, + SlurpError::Internal(_) | SlurpError::InvalidRequest(_) => GasStationReqErr::Internal(error), + } + } +} + #[derive(Debug, Display)] pub enum Web3RpcError { #[display(fmt = "Transport: {}", _0)] @@ -134,8 +158,9 @@ pub enum Web3RpcError { impl From for Web3RpcError { fn from(err: GasStationReqErr) -> Self { match err { - GasStationReqErr::Transport(err) => Web3RpcError::Transport(err), + GasStationReqErr::Transport { .. } => Web3RpcError::Transport(err.to_string()), GasStationReqErr::InvalidResponse(err) => Web3RpcError::InvalidResponse(err), + GasStationReqErr::Internal(err) => Web3RpcError::Internal(err), } } } @@ -158,6 +183,10 @@ impl From for Web3RpcError { } } +impl From for RawTransactionError { + fn from(e: web3::Error) -> Self { RawTransactionError::Transport(e.to_string()) } +} + impl From for Web3RpcError { fn from(e: ethabi::Error) -> Web3RpcError { // Currently, we use the `ethabi` crate to work with a smart contract ABI known at compile time. @@ -256,6 +285,7 @@ pub struct EthCoinImpl { coin_type: EthCoinType, key_pair: KeyPair, my_address: Address, + sign_message_prefix: Option, swap_contract_address: Address, fallback_swap_contract: Option
, web3: Web3, @@ -273,6 +303,7 @@ pub struct EthCoinImpl { chain_id: Option, /// the block range used for eth_getLogs logs_block_range: u64, + nonce_lock: Arc>, } #[derive(Clone, Debug)] @@ -295,10 +326,13 @@ pub enum EthAddressFormat { #[cfg_attr(test, mockable)] async fn make_gas_station_request(url: &str) -> GasStationResult { - let resp = slurp_url(url).await.map_to_mm(GasStationReqErr::Transport)?; + let resp = slurp_url(url).await?; if resp.0 != StatusCode::OK { let error = format!("Gas price request failed with status code {}", resp.0); - return MmError::err(GasStationReqErr::Transport(error)); + return MmError::err(GasStationReqErr::Transport { + uri: url.to_owned(), + error, + }); } let result: GasStationData = json::from_slice(&resp.2)?; Ok(result) @@ -480,7 +514,7 @@ impl EthCoinImpl { swap_contract_address: Address, from_block: u64, to_block: u64, - ) -> Box, Error = String>> { + ) -> Box, Error = String> + Send> { let contract_event = try_fus!(SWAP_CONTRACT.event("SenderRefunded")); let filter = FilterBuilder::default() .topics(Some(vec![contract_event.signature()]), None, None, None) @@ -498,7 +532,21 @@ impl EthCoinImpl { } } -async fn withdraw_impl(ctx: MmArc, coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { +async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> RawTransactionResult { + let tx = match req.tx_hash.strip_prefix("0x") { + Some(tx) => tx, + None => &req.tx_hash, + }; + let hash = H256::from_str(tx).map_to_mm(|e| RawTransactionError::InvalidHashError(e.to_string()))?; + let web3_tx = coin.web3.eth().transaction(TransactionId::Hash(hash)).compat().await?; + let web3_tx = web3_tx.or_mm_err(|| RawTransactionError::HashNotExist(req.tx_hash))?; + let raw = signed_tx_from_web3_tx(web3_tx).map_to_mm(RawTransactionError::InternalError)?; + Ok(RawTransactionRes { + tx_hex: BytesJson(rlp::encode(&raw)), + }) +} + +async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { let to_addr = coin .address_from_str(&req.to) .map_to_mm(WithdrawError::InvalidAddress)?; @@ -574,15 +622,7 @@ async fn withdraw_impl(ctx: MmArc, coin: EthCoin, req: WithdrawRequest) -> Withd eth_value -= total_fee; wei_amount -= total_fee; }; - let _nonce_lock = NONCE_LOCK - .lock(|_start, _now| { - if ctx.is_stopping() { - let error = "MM is stopping, aborting withdraw_impl in NONCE_LOCK".to_owned(); - return MmError::err(WithdrawError::InternalError(error)); - } - Ok(0.5) - }) - .await?; + let _nonce_lock = coin.nonce_lock.lock().await; let nonce_fut = get_addr_nonce(coin.my_address, coin.web3_instances.clone()).compat(); let nonce = match select(nonce_fut, Timer::sleep(30.)).await { Either::Left((nonce_res, _)) => nonce_res.map_to_mm(WithdrawError::Transport)?, @@ -619,13 +659,14 @@ async fn withdraw_impl(ctx: MmArc, coin: EthCoin, req: WithdrawRequest) -> Withd spent_by_me, received_by_me, tx_hex: bytes.into(), - tx_hash: signed.tx_hash(), + tx_hash: format!("{:02x}", signed.tx_hash()), block_height: 0, fee_details: Some(fee_details.into()), coin: coin.ticker.clone(), internal_id: vec![].into(), timestamp: now_ms() / 1000, kmd_rewards: None, + transaction_type: Default::default(), }) } @@ -636,12 +677,13 @@ impl Deref for EthCoin { fn deref(&self) -> &EthCoinImpl { &*self.0 } } +#[async_trait] impl SwapOps for EthCoin { - fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal) -> TransactionFut { - let address = try_fus!(addr_from_raw_pubkey(fee_addr)); + fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { + let address = try_tx_fus!(addr_from_raw_pubkey(fee_addr)); Box::new( - self.send_to_address(address, try_fus!(wei_from_big_decimal(&amount, self.decimals))) + self.send_to_address(address, try_tx_fus!(wei_from_big_decimal(&amount, self.decimals))) .map(TransactionEnum::from), ) } @@ -653,14 +695,15 @@ impl SwapOps for EthCoin { secret_hash: &[u8], amount: BigDecimal, swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let taker_addr = try_fus!(addr_from_raw_pubkey(taker_pub)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let taker_addr = try_tx_fus!(addr_from_raw_pubkey(taker_pub)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); Box::new( self.send_hash_time_locked_payment( self.etomic_swap_id(time_lock, secret_hash), - try_fus!(wei_from_big_decimal(&amount, self.decimals)), + try_tx_fus!(wei_from_big_decimal(&amount, self.decimals)), time_lock, secret_hash, taker_addr, @@ -677,14 +720,15 @@ impl SwapOps for EthCoin { secret_hash: &[u8], amount: BigDecimal, swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let maker_addr = try_fus!(addr_from_raw_pubkey(maker_pub)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let maker_addr = try_tx_fus!(addr_from_raw_pubkey(maker_pub)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); Box::new( self.send_hash_time_locked_payment( self.etomic_swap_id(time_lock, secret_hash), - try_fus!(wei_from_big_decimal(&amount, self.decimals)), + try_tx_fus!(wei_from_big_decimal(&amount, self.decimals)), time_lock, secret_hash, maker_addr, @@ -701,10 +745,11 @@ impl SwapOps for EthCoin { _taker_pub: &[u8], secret: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let tx: UnverifiedTransaction = try_fus!(rlp::decode(taker_payment_tx)); - let signed = try_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_payment_tx)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address(), signed); Box::new( self.spend_hash_time_locked_payment(signed, swap_contract_address, secret) @@ -719,10 +764,11 @@ impl SwapOps for EthCoin { _maker_pub: &[u8], secret: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let tx: UnverifiedTransaction = try_fus!(rlp::decode(maker_payment_tx)); - let signed = try_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_payment_tx)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); Box::new( self.spend_hash_time_locked_payment(signed, swap_contract_address, secret) .map(TransactionEnum::from), @@ -736,10 +782,11 @@ impl SwapOps for EthCoin { _maker_pub: &[u8], _secret_hash: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let tx: UnverifiedTransaction = try_fus!(rlp::decode(taker_payment_tx)); - let signed = try_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_payment_tx)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); Box::new( self.refund_hash_time_locked_payment(swap_contract_address, signed) @@ -754,10 +801,11 @@ impl SwapOps for EthCoin { _taker_pub: &[u8], _secret_hash: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let tx: UnverifiedTransaction = try_fus!(rlp::decode(maker_payment_tx)); - let signed = try_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_payment_tx)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); Box::new( self.refund_hash_time_locked_payment(swap_contract_address, signed) @@ -772,6 +820,7 @@ impl SwapOps for EthCoin { fee_addr: &[u8], amount: &BigDecimal, min_block_number: u64, + _uuid: &[u8], ) -> Box + Send> { let selfi = self.clone(); let tx = match fee_tx { @@ -871,42 +920,26 @@ impl SwapOps for EthCoin { Box::new(fut.boxed().compat()) } - fn validate_maker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - secret_hash: &[u8], - amount: BigDecimal, - swap_contract_address: &Option, - ) -> Box + Send> { - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + let swap_contract_address = try_fus!(input.swap_contract_address.try_to_address()); self.validate_payment( - payment_tx, - time_lock, - maker_pub, - secret_hash, - amount, + &input.payment_tx, + input.time_lock, + &input.other_pub, + &input.secret_hash, + input.amount, swap_contract_address, ) } - fn validate_taker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - secret_hash: &[u8], - amount: BigDecimal, - swap_contract_address: &Option, - ) -> Box + Send> { - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + let swap_contract_address = try_fus!(input.swap_contract_address.try_to_address()); self.validate_payment( - payment_tx, - time_lock, - taker_pub, - secret_hash, - amount, + &input.payment_tx, + input.time_lock, + &input.other_pub, + &input.secret_hash, + input.amount, swap_contract_address, ) } @@ -918,6 +951,7 @@ impl SwapOps for EthCoin { secret_hash: &[u8], from_block: u64, swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> Box, Error = String> + Send> { let id = self.etomic_swap_id(time_lock, secret_hash); let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); @@ -980,30 +1014,22 @@ impl SwapOps for EthCoin { Box::new(fut.boxed().compat()) } - fn search_for_swap_tx_spend_my( + async fn search_for_swap_tx_spend_my( &self, - _time_lock: u32, - _other_pub: &[u8], - _secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - swap_contract_address: &Option, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { - let swap_contract_address = try_s!(swap_contract_address.try_to_address()); - self.search_for_swap_tx_spend(tx, swap_contract_address, search_from_block) + let swap_contract_address = try_s!(input.swap_contract_address.try_to_address()); + self.search_for_swap_tx_spend(input.tx, swap_contract_address, input.search_from_block) + .await } - fn search_for_swap_tx_spend_other( + async fn search_for_swap_tx_spend_other( &self, - _time_lock: u32, - _other_pub: &[u8], - _secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - swap_contract_address: &Option, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { - let swap_contract_address = try_s!(swap_contract_address.try_to_address()); - self.search_for_swap_tx_spend(tx, swap_contract_address, search_from_block) + let swap_contract_address = try_s!(input.swap_contract_address.try_to_address()); + self.search_for_swap_tx_spend(input.tx, swap_contract_address, input.search_from_block) + .await } fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { @@ -1047,6 +1073,10 @@ impl SwapOps for EthCoin { .ok_or_else(|| MmError::new(NegotiateSwapContractAddrErr::NoOtherAddrAndNoFallback)), } } + + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> keys::KeyPair { + key_pair_from_secret(self.key_pair.secret()).expect("valid key") + } } #[cfg_attr(test, mockable)] @@ -1055,6 +1085,40 @@ impl MarketCoinOps for EthCoin { fn my_address(&self) -> Result { Ok(checksum_address(&format!("{:#02x}", self.my_address))) } + fn get_public_key(&self) -> Result> { unimplemented!() } + + /// Hash message for signature using Ethereum's message signing format. + /// keccak256(PREFIX_LENGTH + PREFIX + MESSAGE_LENGTH + MESSAGE) + fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { + let message_prefix = self.sign_message_prefix.as_ref()?; + let mut stream = Stream::new(); + let prefix_len = CompactInteger::from(message_prefix.len()); + prefix_len.serialize(&mut stream); + stream.append_slice(message_prefix.as_bytes()); + stream.append_slice(message.len().to_string().as_bytes()); + stream.append_slice(message.as_bytes()); + Some(keccak256(&stream.out()).take()) + } + + fn sign_message(&self, message: &str) -> SignatureResult { + let message_hash = self.sign_message_hash(message).ok_or(SignatureError::PrefixNotFound)?; + let privkey = &self.key_pair.secret(); + let signature = sign(privkey, &H256::from(message_hash))?; + Ok(format!("0x{}", signature)) + } + + fn verify_message(&self, signature: &str, message: &str, address: &str) -> VerificationResult { + let message_hash = self + .sign_message_hash(message) + .ok_or(VerificationError::PrefixNotFound)?; + let address = self + .address_from_str(address) + .map_err(VerificationError::AddressDecodingError)?; + let signature = Signature::from_str(signature.strip_prefix("0x").unwrap_or(signature))?; + let is_verified = verify_address(&address, &signature, &H256::from(message_hash))?; + Ok(is_verified) + } + fn my_balance(&self) -> BalanceFut { let decimals = self.decimals; let fut = self @@ -1074,6 +1138,13 @@ impl MarketCoinOps for EthCoin { ) } + fn platform_ticker(&self) -> &str { + match &self.coin_type { + EthCoinType::Eth => self.ticker(), + EthCoinType::Erc20 { platform, .. } => platform, + } + } + fn send_raw_tx(&self, mut tx: &str) -> Box + Send> { if tx.starts_with("0x") { tx = &tx[2..]; @@ -1088,6 +1159,16 @@ impl MarketCoinOps for EthCoin { ) } + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + Box::new( + self.web3 + .eth() + .send_raw_transaction(tx.into()) + .map(|res| format!("{:02x}", res)) + .map_err(|e| ERRL!("{}", e)), + ) + } + fn wait_for_confirmations( &self, tx: &[u8], @@ -1120,7 +1201,12 @@ impl MarketCoinOps for EthCoin { let web3_receipt = match selfi.web3.eth().transaction_receipt(tx.hash()).compat().await { Ok(r) => r, Err(e) => { - log!("Error " [e] " getting the " (selfi.ticker()) " transaction " [tx.tx_hash()] ", retrying in 15 seconds"); + error!( + "Error {:?} getting the {} transaction {:?}, retrying in 15 seconds", + e, + selfi.ticker(), + tx.tx_hash() + ); Timer::sleep(check_every as f64).await; continue; }, @@ -1140,7 +1226,11 @@ impl MarketCoinOps for EthCoin { let current_block = match selfi.web3.eth().block_number().compat().await { Ok(b) => b, Err(e) => { - log!("Error " [e] " getting the " (selfi.ticker()) " block number retrying in 15 seconds"); + error!( + "Error {:?} getting the {} block number retrying in 15 seconds", + e, + selfi.ticker() + ); Timer::sleep(check_every as f64).await; continue; }, @@ -1165,17 +1255,17 @@ impl MarketCoinOps for EthCoin { from_block: u64, swap_contract_address: &Option, ) -> TransactionFut { - let unverified: UnverifiedTransaction = try_fus!(rlp::decode(tx_bytes)); - let tx = try_fus!(SignedEthTx::new(unverified)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let unverified: UnverifiedTransaction = try_tx_fus!(rlp::decode(tx_bytes)); + let tx = try_tx_fus!(SignedEthTx::new(unverified)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let func_name = match self.coin_type { EthCoinType::Eth => "ethPayment", EthCoinType::Erc20 { .. } => "erc20Payment", }; - let payment_func = try_fus!(SWAP_CONTRACT.function(func_name)); - let decoded = try_fus!(payment_func.decode_input(&tx.data)); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function(func_name)); + let decoded = try_tx_fus!(payment_func.decode_input(&tx.data)); let id = match &decoded[0] { Token::FixedBytes(bytes) => bytes.clone(), _ => panic!(), @@ -1187,7 +1277,7 @@ impl MarketCoinOps for EthCoin { let current_block = match selfi.current_block().compat().await { Ok(b) => b, Err(e) => { - log!("Error " (e) " getting block number"); + error!("Error getting block number: {}", e); Timer::sleep(5.).await; continue; }, @@ -1200,7 +1290,7 @@ impl MarketCoinOps for EthCoin { { Ok(ev) => ev, Err(e) => { - log!("Error " (e) " getting spend events"); + error!("Error getting spend events: {}", e); Timer::sleep(5.).await; continue; }, @@ -1219,26 +1309,26 @@ impl MarketCoinOps for EthCoin { { Ok(Some(t)) => t, Ok(None) => { - log!("Tx " (tx_hash) " not found yet"); + info!("Tx {} not found yet", tx_hash); Timer::sleep(5.).await; continue; }, Err(e) => { - log!("Get tx " (tx_hash) " error " (e)); + error!("Get tx {} error: {}", tx_hash, e); Timer::sleep(5.).await; continue; }, }; - return Ok(TransactionEnum::from(try_s!(signed_tx_from_web3_tx(transaction)))); + return Ok(TransactionEnum::from(try_tx_s!(signed_tx_from_web3_tx(transaction)))); } } if now_ms() / 1000 > wait_until { - return ERR!( + return TX_PLAIN_ERR!( "Waited too long until {} for transaction {:?} to be spent ", wait_until, - tx + tx, ); } Timer::sleep(5.).await; @@ -1262,7 +1352,7 @@ impl MarketCoinOps for EthCoin { ) } - fn display_priv_key(&self) -> String { format!("{:#02x}", self.key_pair.secret()) } + fn display_priv_key(&self) -> Result { Ok(format!("{:#02x}", self.key_pair.secret())) } fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } @@ -1278,15 +1368,13 @@ pub fn signed_eth_tx_from_bytes(bytes: &[u8]) -> Result { Ok(signed) } -// We can use a shared nonce lock for all ETH coins. -// It's highly likely that we won't experience any issues with it as we won't need to send "a lot" of transactions concurrently. -// For ETH it makes even more sense because different ERC20 tokens can be running on same ETH blockchain. -// So we would need to handle shared locks anyway. +// We can use a nonce lock shared between tokens using the same platform coin and the platform itself. +// For example, ETH/USDT-ERC20 should use the same lock, but it will be different for BNB/USDT-BEP20. lazy_static! { - static ref NONCE_LOCK: TimedAsyncMutex<()> = TimedAsyncMutex::new(()); + static ref NONCE_LOCK: Mutex>>> = Mutex::new(HashMap::new()); } -type EthTxFut = Box + Send + 'static>; +type EthTxFut = Box + Send + 'static>; async fn sign_and_send_transaction_impl( ctx: MmArc, @@ -1295,32 +1383,22 @@ async fn sign_and_send_transaction_impl( action: Action, data: Vec, gas: U256, -) -> Result { +) -> Result { let mut status = ctx.log.status_handle(); macro_rules! tags { () => { &[&"sign-and-send"] }; } - let _nonce_lock = NONCE_LOCK - .lock(|start, now| { - if ctx.is_stopping() { - return ERR!("MM is stopping, aborting sign_and_send_transaction_impl in NONCE_LOCK"); - } - if start < now { - status.status(tags!(), "Waiting for NONCE_LOCK…") - } - Ok(0.5) - }) - .await; + let _nonce_lock = coin.nonce_lock.lock().await; status.status(tags!(), "get_addr_nonce…"); - let nonce = try_s!( + let nonce = try_tx_s!( get_addr_nonce(coin.my_address, coin.web3_instances.clone()) .compat() .await ); status.status(tags!(), "get_gas_price…"); - let gas_price = try_s!(coin.get_gas_price().compat().await); + let gas_price = try_tx_s!(coin.get_gas_price().compat().await); let tx = UnSignedEthTx { nonce, gas_price, @@ -1332,14 +1410,17 @@ async fn sign_and_send_transaction_impl( let signed = tx.sign(coin.key_pair.secret(), coin.chain_id); let bytes = web3::types::Bytes(rlp::encode(&signed).to_vec()); status.status(tags!(), "send_raw_transaction…"); - try_s!( + + try_tx_s!( coin.web3 .eth() .send_raw_transaction(bytes) .map_err(|e| ERRL!("{}", e)) .compat() - .await + .await, + signed ); + status.status(tags!(), "get_addr_nonce…"); loop { // Check every second till ETH nodes recognize that nonce is increased @@ -1352,7 +1433,7 @@ async fn sign_and_send_transaction_impl( { Ok(n) => n, Err(e) => { - log!("Error " [e] " getting " [coin.ticker()] " " [coin.my_address] " nonce"); + error!("Error getting {} {} nonce: {}", coin.ticker(), coin.my_address, e); // we can just keep looping in case of error hoping it will go away continue; }, @@ -1691,23 +1772,15 @@ impl EthCoin { coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: trace.block_number, - tx_hash: BytesJson(raw.hash.to_vec()), + tx_hash: format!("{:02x}", BytesJson(raw.hash.to_vec())), tx_hex: BytesJson(rlp::encode(&raw)), internal_id, timestamp: block.timestamp.into(), kmd_rewards: None, + transaction_type: Default::default(), }; existing_history.push(details); - existing_history.sort_unstable_by(|a, b| { - if a.block_height == 0 { - Ordering::Less - } else if b.block_height == 0 { - Ordering::Greater - } else { - b.block_height.cmp(&a.block_height) - } - }); if let Err(e) = self.save_history_to_file(ctx, existing_history.clone()).compat().await { ctx.log.log( @@ -2065,23 +2138,16 @@ impl EthCoin { coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: block_number.into(), - tx_hash: BytesJson(raw.hash.to_vec()), + tx_hash: format!("{:02x}", BytesJson(raw.hash.to_vec())), tx_hex: BytesJson(rlp::encode(&raw)), internal_id: BytesJson(internal_id.to_vec()), timestamp: block.timestamp.into(), kmd_rewards: None, + transaction_type: Default::default(), }; existing_history.push(details); - existing_history.sort_unstable_by(|a, b| { - if a.block_height == 0 { - Ordering::Less - } else if b.block_height == 0 { - Ordering::Greater - } else { - b.block_height.cmp(&a.block_height) - } - }); + if let Err(e) = self.save_history_to_file(ctx, existing_history).compat().await { ctx.log.log( "", @@ -2113,7 +2179,7 @@ impl EthCoin { #[cfg_attr(test, mockable)] impl EthCoin { fn sign_and_send_transaction(&self, value: U256, action: Action, data: Vec, gas: U256) -> EthTxFut { - let ctx = try_fus!(MmArc::from_weak(&self.ctx).ok_or("!ctx")); + let ctx = try_tx_fus!(MmArc::from_weak(&self.ctx).ok_or("!ctx")); let fut = Box::pin(sign_and_send_transaction_impl( ctx, self.clone(), @@ -2132,9 +2198,9 @@ impl EthCoin { platform: _, token_addr, } => { - let abi = try_fus!(Contract::load(ERC20_ABI.as_bytes())); - let function = try_fus!(abi.function("transfer")); - let data = try_fus!(function.encode_input(&[Token::Address(address), Token::Uint(value)])); + let abi = try_tx_fus!(Contract::load(ERC20_ABI.as_bytes())); + let function = try_tx_fus!(abi.function("transfer")); + let data = try_tx_fus!(function.encode_input(&[Token::Address(address), Token::Uint(value)])); self.sign_and_send_transaction(0.into(), Action::Call(*token_addr), data, U256::from(210_000)) }, } @@ -2151,8 +2217,8 @@ impl EthCoin { ) -> EthTxFut { match &self.coin_type { EthCoinType::Eth => { - let function = try_fus!(SWAP_CONTRACT.function("ethPayment")); - let data = try_fus!(function.encode_input(&[ + let function = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); + let data = try_tx_fus!(function.encode_input(&[ Token::FixedBytes(id), Token::Address(receiver_addr), Token::FixedBytes(secret_hash.to_vec()), @@ -2164,10 +2230,12 @@ impl EthCoin { platform: _, token_addr, } => { - let allowance_fut = self.allowance(swap_contract_address).map_err(|e| ERRL!("{}", e)); + let allowance_fut = self + .allowance(swap_contract_address) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e))); - let function = try_fus!(SWAP_CONTRACT.function("erc20Payment")); - let data = try_fus!(function.encode_input(&[ + let function = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + let data = try_tx_fus!(function.encode_input(&[ Token::FixedBytes(id), Token::Uint(value), Token::Address(*token_addr), @@ -2209,144 +2277,160 @@ impl EthCoin { swap_contract_address: Address, secret: &[u8], ) -> EthTxFut { - let spend_func = try_fus!(SWAP_CONTRACT.function("receiverSpend")); + let spend_func = try_tx_fus!(SWAP_CONTRACT.function("receiverSpend")); let clone = self.clone(); let secret_vec = secret.to_vec(); match self.coin_type { EthCoinType::Eth => { - let payment_func = try_fus!(SWAP_CONTRACT.function("ethPayment")); - let decoded = try_fus!(payment_func.decode_input(&payment.data)); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new(state_f.and_then(move |state| -> EthTxFut { - if state != PAYMENT_STATE_SENT.into() { - return Box::new(futures01::future::err(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - ))); - } + Box::new( + state_f + .map_err(TransactionErr::Plain) + .and_then(move |state| -> EthTxFut { + if state != PAYMENT_STATE_SENT.into() { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + )))); + } - let value = payment.value; - let data = try_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(value), - Token::FixedBytes(secret_vec), - Token::Address(Address::default()), - Token::Address(payment.sender()), - ])); - - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(150_000), - ) - })) + let value = payment.value; + let data = try_tx_fus!(spend_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(value), + Token::FixedBytes(secret_vec), + Token::Address(Address::default()), + Token::Address(payment.sender()), + ])); + + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(150_000), + ) + }), + ) }, EthCoinType::Erc20 { platform: _, token_addr, } => { - let payment_func = try_fus!(SWAP_CONTRACT.function("erc20Payment")); - let decoded = try_fus!(payment_func.decode_input(&payment.data)); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new(state_f.and_then(move |state| -> EthTxFut { - if state != PAYMENT_STATE_SENT.into() { - return Box::new(futures01::future::err(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - ))); - } - let data = try_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - Token::FixedBytes(secret_vec), - Token::Address(token_addr), - Token::Address(payment.sender()), - ])); - - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(150_000), - ) - })) + Box::new( + state_f + .map_err(TransactionErr::Plain) + .and_then(move |state| -> EthTxFut { + if state != PAYMENT_STATE_SENT.into() { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + )))); + } + let data = try_tx_fus!(spend_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + Token::FixedBytes(secret_vec), + Token::Address(token_addr), + Token::Address(payment.sender()), + ])); + + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(150_000), + ) + }), + ) }, } } fn refund_hash_time_locked_payment(&self, swap_contract_address: Address, payment: SignedEthTx) -> EthTxFut { - let refund_func = try_fus!(SWAP_CONTRACT.function("senderRefund")); + let refund_func = try_tx_fus!(SWAP_CONTRACT.function("senderRefund")); let clone = self.clone(); match self.coin_type { EthCoinType::Eth => { - let payment_func = try_fus!(SWAP_CONTRACT.function("ethPayment")); - let decoded = try_fus!(payment_func.decode_input(&payment.data)); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new(state_f.and_then(move |state| -> EthTxFut { - if state != PAYMENT_STATE_SENT.into() { - return Box::new(futures01::future::err(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - ))); - } + Box::new( + state_f + .map_err(TransactionErr::Plain) + .and_then(move |state| -> EthTxFut { + if state != PAYMENT_STATE_SENT.into() { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + )))); + } - let value = payment.value; - let data = try_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(value), - decoded[2].clone(), - Token::Address(Address::default()), - decoded[1].clone(), - ])); - - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(150_000), - ) - })) + let value = payment.value; + let data = try_tx_fus!(refund_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(value), + decoded[2].clone(), + Token::Address(Address::default()), + decoded[1].clone(), + ])); + + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(150_000), + ) + }), + ) }, EthCoinType::Erc20 { platform: _, token_addr, } => { - let payment_func = try_fus!(SWAP_CONTRACT.function("erc20Payment")); - let decoded = try_fus!(payment_func.decode_input(&payment.data)); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new(state_f.and_then(move |state| -> EthTxFut { - if state != PAYMENT_STATE_SENT.into() { - return Box::new(futures01::future::err(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - ))); - } + Box::new( + state_f + .map_err(TransactionErr::Plain) + .and_then(move |state| -> EthTxFut { + if state != PAYMENT_STATE_SENT.into() { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + )))); + } - let data = try_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - decoded[4].clone(), - Token::Address(token_addr), - decoded[3].clone(), - ])); - - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(150_000), - ) - })) + let data = try_tx_fus!(refund_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + decoded[4].clone(), + Token::Address(token_addr), + decoded[3].clone(), + ])); + + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(150_000), + ) + }), + ) }, } } @@ -2466,13 +2550,13 @@ impl EthCoin { let coin = self.clone(); let fut = async move { let token_addr = match coin.coin_type { - EthCoinType::Eth => return ERR!("'approve' is expected to be call for ERC20 coins only"), + EthCoinType::Eth => return TX_PLAIN_ERR!("'approve' is expected to be call for ERC20 coins only"), EthCoinType::Erc20 { token_addr, .. } => token_addr, }; - let function = try_s!(ERC20_CONTRACT.function("approve")); - let data = try_s!(function.encode_input(&[Token::Address(spender), Token::Uint(amount)])); + let function = try_tx_s!(ERC20_CONTRACT.function("approve")); + let data = try_tx_s!(function.encode_input(&[Token::Address(spender), Token::Uint(amount)])); - let gas_limit = try_s!( + let gas_limit = try_tx_s!( coin.estimate_gas_for_contract_call(token_addr, Bytes::from(data.clone())) .compat() .await @@ -2481,7 +2565,6 @@ impl EthCoin { coin.sign_and_send_transaction(0.into(), Action::Call(token_addr), data, gas_limit) .compat() .await - .map_err(|e| ERRL!("{}", e)) }; Box::new(fut.boxed().compat()) } @@ -2687,7 +2770,7 @@ impl EthCoin { ) } - fn search_for_swap_tx_spend( + async fn search_for_swap_tx_spend( &self, tx: &[u8], swap_contract_address: Address, @@ -2708,7 +2791,7 @@ impl EthCoin { _ => panic!(), }; - let mut current_block = try_s!(self.current_block().wait()); + let mut current_block = try_s!(self.current_block().compat().await); if current_block < search_from_block { current_block = search_from_block; } @@ -2718,19 +2801,26 @@ impl EthCoin { loop { let to_block = current_block.min(from_block + self.logs_block_range); - let spend_events = try_s!(self.spend_events(swap_contract_address, from_block, to_block).wait()); + let spend_events = try_s!( + self.spend_events(swap_contract_address, from_block, to_block) + .compat() + .await + ); let found = spend_events.iter().find(|event| &event.data.0[..32] == id.as_slice()); if let Some(event) = found { match event.transaction_hash { Some(tx_hash) => { - let transaction = match try_s!(self.web3.eth().transaction(TransactionId::Hash(tx_hash)).wait()) - { - Some(t) => t, - None => { - return ERR!("Found ReceiverSpent event, but transaction {:02x} is missing", tx_hash) - }, - }; + let transaction = + match try_s!(self.web3.eth().transaction(TransactionId::Hash(tx_hash)).compat().await) { + Some(t) => t, + None => { + return ERR!( + "Found ReceiverSpent event, but transaction {:02x} is missing", + tx_hash + ) + }, + }; return Ok(Some(FoundSwapTxSpend::Spent(TransactionEnum::from(try_s!( signed_tx_from_web3_tx(transaction) @@ -2740,19 +2830,26 @@ impl EthCoin { } } - let refund_events = try_s!(self.refund_events(swap_contract_address, from_block, to_block).wait()); + let refund_events = try_s!( + self.refund_events(swap_contract_address, from_block, to_block) + .compat() + .await + ); let found = refund_events.iter().find(|event| &event.data.0[..32] == id.as_slice()); if let Some(event) = found { match event.transaction_hash { Some(tx_hash) => { - let transaction = match try_s!(self.web3.eth().transaction(TransactionId::Hash(tx_hash)).wait()) - { - Some(t) => t, - None => { - return ERR!("Found SenderRefunded event, but transaction {:02x} is missing", tx_hash) - }, - }; + let transaction = + match try_s!(self.web3.eth().transaction(TransactionId::Hash(tx_hash)).compat().await) { + Some(t) => t, + None => { + return ERR!( + "Found SenderRefunded event, but transaction {:02x} is missing", + tx_hash + ) + }, + }; return Ok(Some(FoundSwapTxSpend::Refunded(TransactionEnum::from(try_s!( signed_tx_from_web3_tx(transaction) @@ -2852,12 +2949,16 @@ impl EthTxFeeDetails { } } +#[async_trait] impl MmCoin for EthCoin { fn is_asset_chain(&self) -> bool { false } + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { + Box::new(get_raw_transaction_impl(self.clone(), req).boxed().compat()) + } + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { - let ctx = try_f!(MmArc::from_weak(&self.ctx).or_mm_err(|| WithdrawError::InternalError("!ctx".to_owned()))); - Box::new(Box::pin(withdraw_impl(ctx, self.clone(), req)).compat()) + Box::new(Box::pin(withdraw_impl(self.clone(), req)).compat()) } fn decimals(&self) -> u8 { self.decimals } @@ -2926,58 +3027,57 @@ impl MmCoin for EthCoin { ) } - fn get_sender_trade_fee(&self, value: TradePreimageValue, stage: FeeApproxStage) -> TradePreimageFut { - let coin = self.clone(); - let fut = async move { - let gas_price = coin.get_gas_price().compat().await?; - let gas_price = increase_gas_price_by_stage(gas_price, &stage); - let gas_limit = match coin.coin_type { - EthCoinType::Eth => { - // this gas_limit includes gas for `ethPayment` and `senderRefund` contract calls - U256::from(300_000) - }, - EthCoinType::Erc20 { token_addr, .. } => { - let value = match value { - TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => { - wei_from_big_decimal(&value, coin.decimals)? - }, - }; - let allowed = coin.allowance(coin.swap_contract_address).compat().await?; - if allowed < value { - // estimate gas for the `approve` contract call - - // Pass a dummy spender. Let's use `my_address`. - let spender = coin.my_address; - let approve_function = ERC20_CONTRACT.function("approve")?; - let approve_data = - approve_function.encode_input(&[Token::Address(spender), Token::Uint(value)])?; - let approve_gas_limit = coin - .estimate_gas_for_contract_call(token_addr, Bytes::from(approve_data)) - .compat() - .await?; + async fn get_sender_trade_fee( + &self, + value: TradePreimageValue, + stage: FeeApproxStage, + ) -> TradePreimageResult { + let gas_price = self.get_gas_price().compat().await?; + let gas_price = increase_gas_price_by_stage(gas_price, &stage); + let gas_limit = match self.coin_type { + EthCoinType::Eth => { + // this gas_limit includes gas for `ethPayment` and `senderRefund` contract calls + U256::from(300_000) + }, + EthCoinType::Erc20 { token_addr, .. } => { + let value = match value { + TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => { + wei_from_big_decimal(&value, self.decimals)? + }, + }; + let allowed = self.allowance(self.swap_contract_address).compat().await?; + if allowed < value { + // estimate gas for the `approve` contract call + + // Pass a dummy spender. Let's use `my_address`. + let spender = self.my_address; + let approve_function = ERC20_CONTRACT.function("approve")?; + let approve_data = approve_function.encode_input(&[Token::Address(spender), Token::Uint(value)])?; + let approve_gas_limit = self + .estimate_gas_for_contract_call(token_addr, Bytes::from(approve_data)) + .compat() + .await?; - // this gas_limit includes gas for `approve`, `erc20Payment` and `senderRefund` contract calls - U256::from(300_000) + approve_gas_limit - } else { - // this gas_limit includes gas for `erc20Payment` and `senderRefund` contract calls - U256::from(300_000) - } - }, - }; + // this gas_limit includes gas for `approve`, `erc20Payment` and `senderRefund` contract calls + U256::from(300_000) + approve_gas_limit + } else { + // this gas_limit includes gas for `erc20Payment` and `senderRefund` contract calls + U256::from(300_000) + } + }, + }; - let total_fee = gas_limit * gas_price; - let amount = u256_to_big_decimal(total_fee, 18)?; - let fee_coin = match &coin.coin_type { - EthCoinType::Eth => &coin.ticker, - EthCoinType::Erc20 { platform, .. } => platform, - }; - Ok(TradeFee { - coin: fee_coin.into(), - amount: amount.into(), - paid_from_trading_vol: false, - }) + let total_fee = gas_limit * gas_price; + let amount = u256_to_big_decimal(total_fee, 18)?; + let fee_coin = match &self.coin_type { + EthCoinType::Eth => &self.ticker, + EthCoinType::Erc20 { platform, .. } => platform, }; - Box::new(fut.boxed().compat()) + Ok(TradeFee { + coin: fee_coin.into(), + amount: amount.into(), + paid_from_trading_vol: false, + }) } fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { @@ -3000,65 +3100,61 @@ impl MmCoin for EthCoin { Box::new(fut.boxed().compat()) } - fn get_fee_to_send_taker_fee( + async fn get_fee_to_send_taker_fee( &self, dex_fee_amount: BigDecimal, stage: FeeApproxStage, - ) -> TradePreimageFut { - let coin = self.clone(); - let fut = async move { - let dex_fee_amount = wei_from_big_decimal(&dex_fee_amount, coin.decimals)?; - - // pass the dummy params - let to_addr = addr_from_raw_pubkey(&DEX_FEE_ADDR_RAW_PUBKEY) - .expect("addr_from_raw_pubkey should never fail with DEX_FEE_ADDR_RAW_PUBKEY"); - let (eth_value, data, call_addr, fee_coin) = match &coin.coin_type { - EthCoinType::Eth => (dex_fee_amount, Vec::new(), &to_addr, &coin.ticker), - EthCoinType::Erc20 { platform, token_addr } => { - let function = ERC20_CONTRACT.function("transfer")?; - let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(dex_fee_amount)])?; - (0.into(), data, token_addr, platform) - }, - }; - - let gas_price = coin.get_gas_price().compat().await?; - let gas_price = increase_gas_price_by_stage(gas_price, &stage); - let estimate_gas_req = CallRequest { - value: Some(eth_value), - data: Some(data.clone().into()), - from: Some(coin.my_address), - to: *call_addr, - gas: None, - // gas price must be supplied because some smart contracts base their - // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 - gas_price: Some(gas_price), - }; + ) -> TradePreimageResult { + let dex_fee_amount = wei_from_big_decimal(&dex_fee_amount, self.decimals)?; + + // pass the dummy params + let to_addr = addr_from_raw_pubkey(&DEX_FEE_ADDR_RAW_PUBKEY) + .expect("addr_from_raw_pubkey should never fail with DEX_FEE_ADDR_RAW_PUBKEY"); + let (eth_value, data, call_addr, fee_coin) = match &self.coin_type { + EthCoinType::Eth => (dex_fee_amount, Vec::new(), &to_addr, &self.ticker), + EthCoinType::Erc20 { platform, token_addr } => { + let function = ERC20_CONTRACT.function("transfer")?; + let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(dex_fee_amount)])?; + (0.into(), data, token_addr, platform) + }, + }; - // Please note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. - // Ideally we should determine the case when we have the insufficient balance and return `TradePreimageError::NotSufficientBalance` error. - let gas_limit = coin.estimate_gas(estimate_gas_req).compat().await?; - let total_fee = gas_limit * gas_price; - let amount = u256_to_big_decimal(total_fee, 18)?; - Ok(TradeFee { - coin: fee_coin.into(), - amount: amount.into(), - paid_from_trading_vol: false, - }) + let gas_price = self.get_gas_price().compat().await?; + let gas_price = increase_gas_price_by_stage(gas_price, &stage); + let estimate_gas_req = CallRequest { + value: Some(eth_value), + data: Some(data.clone().into()), + from: Some(self.my_address), + to: *call_addr, + gas: None, + // gas price must be supplied because some smart contracts base their + // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 + gas_price: Some(gas_price), }; - Box::new(fut.boxed().compat()) + + // Please note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. + // Ideally we should determine the case when we have the insufficient balance and return `TradePreimageError::NotSufficientBalance` error. + let gas_limit = self.estimate_gas(estimate_gas_req).compat().await?; + let total_fee = gas_limit * gas_price; + let amount = u256_to_big_decimal(total_fee, 18)?; + Ok(TradeFee { + coin: fee_coin.into(), + amount: amount.into(), + paid_from_trading_vol: false, + }) } - fn required_confirmations(&self) -> u64 { self.required_confirmations.load(AtomicOrderding::Relaxed) } + fn required_confirmations(&self) -> u64 { self.required_confirmations.load(AtomicOrdering::Relaxed) } fn requires_notarization(&self) -> bool { false } fn set_required_confirmations(&self, confirmations: u64) { self.required_confirmations - .store(confirmations, AtomicOrderding::Relaxed); + .store(confirmations, AtomicOrdering::Relaxed); } fn set_requires_notarization(&self, _requires_nota: bool) { - log!("Warning: set_requires_notarization doesn't take any effect on ETH/ERC20 coins"); + warn!("set_requires_notarization doesn't take any effect on ETH/ERC20 coins"); } fn swap_contract_address(&self) -> Option { @@ -3256,6 +3352,9 @@ fn rpc_event_handlers_for_eth_transport(ctx: &MmArc, ticker: String) -> Vec Arc> { Arc::new(AsyncMutex::new(())) } + pub async fn eth_coin_from_conf_and_request( ctx: &MmArc, ticker: &str, @@ -3297,7 +3396,7 @@ pub async fn eth_coin_from_conf_and_request( let version = match web3.web3().client_version().compat().await { Ok(v) => v, Err(e) => { - log!("Couldn't get client version for url " (url) ", " (e)); + error!("Couldn't get client version for url {}: {}", url, e); continue; }, }; @@ -3337,9 +3436,11 @@ pub async fn eth_coin_from_conf_and_request( .into(); if req["requires_notarization"].as_bool().is_some() { - log!("Warning: requires_notarization doesn't take any effect on ETH/ERC20 coins"); + warn!("requires_notarization doesn't take any effect on ETH/ERC20 coins"); } + let sign_message_prefix: Option = json::from_value(conf["sign_message_prefix"].clone()).unwrap_or(None); + let initial_history_state = if req["tx_history"].as_bool().unwrap_or(false) { HistorySyncState::NotStarted } else { @@ -3350,10 +3451,20 @@ pub async fn eth_coin_from_conf_and_request( let gas_station_policy: GasStationPricePolicy = json::from_value(req["gas_station_policy"].clone()).unwrap_or_default(); + let key_lock = match &coin_type { + EthCoinType::Eth => String::from(ticker), + EthCoinType::Erc20 { ref platform, .. } => String::from(platform), + }; + + let mut map = NONCE_LOCK.lock().unwrap(); + + let nonce_lock = map.entry(key_lock).or_insert_with(new_nonce_lock).clone(); + let coin = EthCoinImpl { key_pair, my_address, coin_type, + sign_message_prefix, swap_contract_address, fallback_swap_contract, decimals, @@ -3368,6 +3479,7 @@ pub async fn eth_coin_from_conf_and_request( required_confirmations, chain_id: conf["chain_id"].as_u64(), logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), + nonce_lock, }; Ok(EthCoin(Arc::new(coin))) } @@ -3381,8 +3493,8 @@ fn checksum_address(addr: &str) -> String { } let mut hasher = Keccak256::default(); - hasher.input(&addr); - let hash = hasher.result(); + hasher.update(&addr); + let hash = hasher.finalize(); let mut result: String = "0x".into(); for (i, c) in addr.chars().enumerate() { if c.is_digit(10) { @@ -3435,7 +3547,7 @@ fn get_addr_nonce(addr: Address, web3s: Vec) -> Box Some(n), Err(e) => { - log!("Error " (e) " when getting nonce for addr " [addr]); + error!("Error getting nonce for addr {:?}: {}", addr, e); None }, }) @@ -3452,7 +3564,7 @@ fn get_addr_nonce(addr: Address, web3s: Vec) -> Box [u8; 33] { #[test] fn validate_dex_fee_eth_confirmed_before_min_block() { - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, vec!["http://eth1.cipig.net:8555".into()], None); + let (_ctx, coin) = eth_coin_for_test( + EthCoinType::Eth, + vec!["https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b".into()], + None, + ); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f let tx = coin @@ -1065,7 +1084,14 @@ fn validate_dex_fee_eth_confirmed_before_min_block() { let tx = tx.into(); let amount: BigDecimal = "0.000526435076465".parse().unwrap(); let validate_err = coin - .validate_fee(&tx, &compressed_public, &*DEX_FEE_ADDR_RAW_PUBKEY, &amount, 11784793) + .validate_fee( + &tx, + &compressed_public, + &*DEX_FEE_ADDR_RAW_PUBKEY, + &amount, + 11784793, + &[], + ) .wait() .unwrap_err(); assert!(validate_err.contains("confirmed before min_block")); @@ -1098,7 +1124,14 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { let tx = tx.into(); let amount: BigDecimal = "5.548262548262548262".parse().unwrap(); let validate_err = coin - .validate_fee(&tx, &compressed_public, &*DEX_FEE_ADDR_RAW_PUBKEY, &amount, 11823975) + .validate_fee( + &tx, + &compressed_public, + &*DEX_FEE_ADDR_RAW_PUBKEY, + &amount, + 11823975, + &[], + ) .wait() .unwrap_err(); assert!(validate_err.contains("confirmed before min_block")); @@ -1169,6 +1202,7 @@ fn test_negotiate_swap_contract_addr_has_fallback() { } #[test] +#[ignore] fn polygon_check_if_my_payment_sent() { let ctx = MmCtxBuilder::new().into_mm_arc(); let conf = json!({ @@ -1188,7 +1222,7 @@ fn polygon_check_if_my_payment_sent() { let request = json!({ "method": "enable", "coin": "MATIC", - "urls": ["https://polygon-rpc.com"], + "urls": ["https://polygon-mainnet.g.alchemy.com/v2/9YYl6iMLmXXLoflMPHnMTC4Dcm2L2tFH"], "swap_contract_address": "0x9130b257d37a52e52f21054c4da3450c72f595ce", }); @@ -1208,10 +1242,102 @@ fn polygon_check_if_my_payment_sent() { let secret_hash = hex::decode("fc33114b389f0ee1212abf2867e99e89126f4860").unwrap(); let swap_contract_address = "9130b257d37a52e52f21054c4da3450c72f595ce".into(); let my_payment = coin - .check_if_my_payment_sent(1638764369, &[], &secret_hash, 22185109, &Some(swap_contract_address)) + .check_if_my_payment_sent( + 1638764369, + &[], + &secret_hash, + 22185109, + &Some(swap_contract_address), + &[], + ) .wait() .unwrap() .unwrap(); let expected_hash = BytesJson::from("69a20008cea0c15ee483b5bbdff942752634aa072dfd2ff715fe87eec302de11"); assert_eq!(expected_hash, my_payment.tx_hash()); } + +#[test] +fn test_message_hash() { + let key_pair = KeyPair::from_secret_slice( + &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), + ) + .unwrap(); + let transport = Web3Transport::new(vec!["http://195.201.0.6:8545".into()]).unwrap(); + let web3 = Web3::new(transport); + let ctx = MmCtxBuilder::new().into_mm_arc(); + let coin = EthCoin(Arc::new(EthCoinImpl { + ticker: "ETH".into(), + coin_type: EthCoinType::Eth, + my_address: key_pair.address(), + sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), + key_pair, + swap_contract_address: Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"), + fallback_swap_contract: None, + web3_instances: vec![Web3Instance { + web3: web3.clone(), + is_parity: true, + }], + web3, + decimals: 18, + gas_station_url: None, + gas_station_decimals: ETH_GAS_STATION_DECIMALS, + gas_station_policy: GasStationPricePolicy::MeanAverageFast, + history_sync_state: Mutex::new(HistorySyncState::NotStarted), + ctx: ctx.weak(), + required_confirmations: 1.into(), + chain_id: None, + logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, + nonce_lock: new_nonce_lock(), + })); + + let message_hash = coin.sign_message_hash("test").unwrap(); + assert_eq!( + hex::encode(message_hash), + "4a5c5d454721bbbb25540c3317521e71c373ae36458f960d2ad46ef088110e95" + ); +} + +#[test] +fn test_sign_verify_message() { + let key_pair = KeyPair::from_secret_slice( + &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), + ) + .unwrap(); + let transport = Web3Transport::new(vec!["http://195.201.0.6:8545".into()]).unwrap(); + let web3 = Web3::new(transport); + let ctx = MmCtxBuilder::new().into_mm_arc(); + let coin = EthCoin(Arc::new(EthCoinImpl { + ticker: "ETH".into(), + coin_type: EthCoinType::Eth, + my_address: key_pair.address(), + sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), + key_pair, + swap_contract_address: Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"), + fallback_swap_contract: None, + web3_instances: vec![Web3Instance { + web3: web3.clone(), + is_parity: true, + }], + web3, + decimals: 18, + gas_station_url: None, + gas_station_decimals: ETH_GAS_STATION_DECIMALS, + gas_station_policy: GasStationPricePolicy::MeanAverageFast, + history_sync_state: Mutex::new(HistorySyncState::NotStarted), + ctx: ctx.weak(), + required_confirmations: 1.into(), + chain_id: None, + logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, + nonce_lock: new_nonce_lock(), + })); + + let message = "test"; + let signature = coin.sign_message(message).unwrap(); + assert_eq!(signature, "0xcdf11a9c4591fb7334daa4b21494a2590d3f7de41c7d2b333a5b61ca59da9b311b492374cc0ba4fbae53933260fa4b1c18f15d95b694629a7b0620eec77a938600"); + + let is_valid = coin + .verify_message(&signature, message, "0xbAB36286672fbdc7B250804bf6D14Be0dF69fa29") + .unwrap(); + assert!(is_valid); +} diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 1574b780f1..ca1f3f8362 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::lp_coininit; -use common::mm_ctx::MmCtxBuilder; +use crypto::CryptoCtx; +use mm2_core::mm_ctx::MmCtxBuilder; use wasm_bindgen_test::*; use web_sys::console; @@ -26,6 +27,7 @@ async fn test_send() { ticker: "ETH".into(), coin_type: EthCoinType::Eth, my_address: key_pair.address(), + sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), key_pair, swap_contract_address: Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"), fallback_swap_contract: None, @@ -43,6 +45,7 @@ async fn test_send() { required_confirmations: 1.into(), chain_id: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, + nonce_lock: new_nonce_lock(), })); let tx = coin .send_maker_payment( @@ -51,6 +54,7 @@ async fn test_send() { &[1; 20], "0.001".parse().unwrap(), &None, + &[], ) .compat() .await; @@ -62,10 +66,6 @@ async fn test_send() { #[wasm_bindgen_test] async fn test_init_eth_coin() { - use common::privkey::key_pair_from_seed; - - let key_pair = - key_pair_from_seed("spice describe gravity federal blast come thank unfair canal monkey style afraid").unwrap(); let conf = json!({ "coins": [{ "coin": "ETH", @@ -78,10 +78,13 @@ async fn test_init_eth_coin() { "mm2": 1 }] }); - let ctx = MmCtxBuilder::new() - .with_conf(conf) - .with_secp256k1_key_pair(key_pair) - .into_mm_arc(); + + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); + CryptoCtx::init_with_iguana_passphrase( + ctx.clone(), + "spice describe gravity federal blast come thank unfair canal monkey style afraid", + ) + .unwrap(); let req = json!({ "urls":["http://195.201.0.6:8565"], diff --git a/mm2src/coins/eth/web3_transport.rs b/mm2src/coins/eth/web3_transport.rs index 5635e48a60..5b0b229a57 100644 --- a/mm2src/coins/eth/web3_transport.rs +++ b/mm2src/coins/eth/web3_transport.rs @@ -57,9 +57,9 @@ impl EthFeeHistoryNamespace { /// Parse bytes RPC response into `Result`. /// Implementation copied from Web3 HTTP transport #[cfg(not(target_arch = "wasm32"))] -fn single_response>(response: T) -> Result { - let response = - serde_json::from_slice(&*response).map_err(|e| Error::from(ErrorKind::InvalidResponse(format!("{}", e))))?; +fn single_response>(response: T, rpc_url: &str) -> Result { + let response = serde_json::from_slice(&*response) + .map_err(|e| Error::from(ErrorKind::InvalidResponse(format!("{}: {}", rpc_url, e))))?; match response { Response::Single(output) => to_result_from_output(output), @@ -114,7 +114,7 @@ impl Future for SendFuture { fn poll(&mut self) -> Poll { self.0.poll() } } -unsafe impl Send for SendFuture {} +unsafe impl Send for SendFuture where T: Send {} unsafe impl Sync for SendFuture {} impl Transport for Web3Transport { @@ -150,10 +150,13 @@ async fn send_request( event_handlers: Vec, ) -> Result { use common::executor::Timer; - use common::wio::slurp_reqʹ; + use common::log::warn; use futures::future::{select, Either}; use gstuff::binprint; use http::header::HeaderValue; + use mm2_net::transport::slurp_req; + + const REQUEST_TIMEOUT_S: f64 = 60.; let mut errors = Vec::new(); for uri in uris.iter() { @@ -165,13 +168,15 @@ async fn send_request( *req.uri_mut() = uri.clone(); req.headers_mut() .insert(http::header::CONTENT_TYPE, HeaderValue::from_static("application/json")); - let timeout = Timer::sleep(60.); - let req = Box::pin(slurp_reqʹ(req)); + let timeout = Timer::sleep(REQUEST_TIMEOUT_S); + let req = Box::pin(slurp_req(req)); let rc = select(req, timeout).await; let res = match rc { Either::Left((r, _t)) => r, Either::Right((_t, _r)) => { - errors.push(ERRL!("timeout")); + let error = ERRL!("Error requesting '{}': {}s timeout expired", uri, REQUEST_TIMEOUT_S); + warn!("{}", error); + errors.push(error); continue; }, }; @@ -179,7 +184,7 @@ async fn send_request( let (status, _headers, body) = match res { Ok(r) => r, Err(err) => { - errors.push(err); + errors.push(err.to_string()); continue; }, }; @@ -187,17 +192,18 @@ async fn send_request( event_handlers.on_incoming_response(&body); if !status.is_success() { - errors.push(ERRL!("!200: {}, {}", status, binprint(&body, b'.'))); + errors.push(ERRL!( + "Server '{}' response !200: {}, {}", + uri, + status, + binprint(&body, b'.') + )); continue; } - return single_response(body); + return single_response(body, &uri.to_string()); } - Err(ErrorKind::Transport(fomat!( - "request " [request] " failed: " - for err in errors {(err)} sep {"; "} - )) - .into()) + Err(request_failed_error(&request, &errors)) } #[cfg(target_arch = "wasm32")] @@ -210,7 +216,7 @@ async fn send_request( let mut transport_errors = Vec::new(); for uri in uris { - match send_request_once(&request_payload, &uri, &event_handlers).await { + match send_request_once(request_payload.clone(), &uri, &event_handlers).await { Ok(response_json) => return Ok(response_json), Err(Error(ErrorKind::Transport(e), _)) => { transport_errors.push(e.to_string()); @@ -219,23 +225,16 @@ async fn send_request( } } - Err(ErrorKind::Transport(fomat!( - "request " [request] " failed: " - for err in transport_errors {(err)} sep {"; "} - )) - .into()) + Err(request_failed_error(&request, &transport_errors)) } #[cfg(target_arch = "wasm32")] async fn send_request_once( - request_payload: &String, + request_payload: String, uri: &http::Uri, event_handlers: &Vec, ) -> Result { - use wasm_bindgen::prelude::*; - use wasm_bindgen::JsCast; - use wasm_bindgen_futures::JsFuture; - use web_sys::{Request, RequestInit, RequestMode, Response as JsResponse}; + use mm2_net::wasm_http::FetchRequest; macro_rules! try_or { ($exp:expr, $errkind:ident) => { @@ -246,39 +245,29 @@ async fn send_request_once( }; } - let window = web_sys::window().expect("!window"); - // account for outgoing traffic event_handlers.on_outgoing_request(request_payload.as_bytes()); - let mut opts = RequestInit::new(); - opts.method("POST"); - opts.mode(RequestMode::Cors); - opts.body(Some(&JsValue::from_str(&request_payload))); - - let request = try_or!(Request::new_with_str_and_init(&uri.to_string(), &opts), Transport); - - request.headers().set("Accept", "application/json").unwrap(); - request.headers().set("Content-Type", "application/json").unwrap(); - - let request_promise = window.fetch_with_request(&request); - - let future = JsFuture::from(request_promise); - let resp_value = try_or!(future.await, Transport); - let js_response: JsResponse = try_or!(resp_value.dyn_into(), Transport); - - let resp_txt_fut = try_or!(js_response.text(), Transport); - let resp_txt = try_or!(JsFuture::from(resp_txt_fut).await, Transport); + let result = FetchRequest::post(&uri.to_string()) + .cors() + .body_utf8(request_payload) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .request_str() + .await; + let (status_code, response_str) = try_or!(result, Transport); + if !status_code.is_success() { + return Err(Error::from(ErrorKind::Transport(ERRL!( + "!200: {}, {}", + status_code, + response_str + )))); + } - let resp_str = resp_txt.as_string().ok_or_else(|| { - Error::from(ErrorKind::Transport(ERRL!( - "Expected a UTF-8 string JSON, found {:?}", - resp_txt - ))) - })?; - event_handlers.on_incoming_response(resp_str.as_bytes()); + // account for incoming traffic + event_handlers.on_incoming_response(response_str.as_bytes()); - let response: Response = try_or!(serde_json::from_str(&resp_str), InvalidResponse); + let response: Response = try_or!(serde_json::from_str(&response_str), InvalidResponse); match response { Response::Single(output) => to_result_from_output(output), Response::Batch(_) => Err(Error::from(ErrorKind::InvalidResponse( @@ -286,3 +275,9 @@ async fn send_request_once( ))), } } + +fn request_failed_error(request: &Call, errors: &[String]) -> Error { + let errors = errors.join("; "); + let error = format!("request {:?} failed: {}", request, errors); + Error::from(ErrorKind::Transport(error)) +} diff --git a/mm2src/coins/for_tests/ZOMBIE_CACHE.db b/mm2src/coins/for_tests/ZOMBIE_CACHE.db new file mode 100644 index 0000000000..36f086ff5f Binary files /dev/null and b/mm2src/coins/for_tests/ZOMBIE_CACHE.db differ diff --git a/mm2src/coins/for_tests/tBCH_tx_history_fixtures.json b/mm2src/coins/for_tests/tBCH_tx_history_fixtures.json new file mode 100644 index 0000000000..6949461b3e --- /dev/null +++ b/mm2src/coins/for_tests/tBCH_tx_history_fixtures.json @@ -0,0 +1,246 @@ +[ +{"tx_hex":"0100000001ce59a734f33811afcc00c19dcb12202ed00067a50efed80424fabd2b723678c0020000006b483045022100ec1fecff9c60fb7e821b9a412bd8c4ce4a757c68287f9cf9e0f461165492d6530220222f020dd05d65ba35cddd0116c99255612ec90d63019bb1cea45e2cf09a62a94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914953b3909ff6aa269f85da34c132a92424440e18e879decad00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acd1215c61","tx_hash":"6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pz2nkwgfla42y60ctk35cye2jfpygs8p3csm2zkhzd","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.11400301","spent_by_me":"0.11400301","received_by_me":"0.11398301","my_balance_change":"-0.00002000","block_height":0,"timestamp":0,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000001ce59a734f33811afcc00c19dcb12202ed00067a50efed80424fabd2b723678c0020000006b483045022100ec1fecff9c60fb7e821b9a412bd8c4ce4a757c68287f9cf9e0f461165492d6530220222f020dd05d65ba35cddd0116c99255612ec90d63019bb1cea45e2cf09a62a94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914953b3909ff6aa269f85da34c132a92424440e18e879decad00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acd1215c61","tx_hash":"6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69","from":[],"to":["slptest:pz2nkwgfla42y60ctk35cye2jfpygs8p3ct0devqss"],"total_amount":"0","spent_by_me":"0","received_by_me":"0","my_balance_change":"0","block_height":0,"timestamp":0,"fee_details":null,"coin":"tBCH","internal_id":"421b9d5aed93ab2a20fb41d6a30d85ee6e315a506a6ac521eafaec4cb09739df","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000001c0b02bc3ded9a8684fcd729e19e6c53a5cf1467106554273b1b2684229771809000000006a4730440220607616e5ed18ac83461c8b3c1c258e5245efe936d3b37d2e60b667ec6accd47602204e2c55ba355f58206be3e30bb5aebabc72ce7878306c41a319156a67094eea494121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a914953b3909ff6aa269f85da34c132a92424440e18e88ac6df4ad00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88accc1d5c61","tx_hash":"c07836722bbdfa2404d8fe0ea56700d02e2012cb9dc100ccaf1138f334a759ce","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qz2nkwgfla42y60ctk35cye2jfpygs8p3c87hd35es","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.11402301","spent_by_me":"0.11402301","received_by_me":"0.11400301","my_balance_change":"-0.00002000","block_height":1468165,"timestamp":1633426943,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"c07836722bbdfa2404d8fe0ea56700d02e2012cb9dc100ccaf1138f334a759ce","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000001c0b02bc3ded9a8684fcd729e19e6c53a5cf1467106554273b1b2684229771809000000006a4730440220607616e5ed18ac83461c8b3c1c258e5245efe936d3b37d2e60b667ec6accd47602204e2c55ba355f58206be3e30bb5aebabc72ce7878306c41a319156a67094eea494121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a914953b3909ff6aa269f85da34c132a92424440e18e88ac6df4ad00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88accc1d5c61","tx_hash":"c07836722bbdfa2404d8fe0ea56700d02e2012cb9dc100ccaf1138f334a759ce","from":[],"to":["slptest:qz2nkwgfla42y60ctk35cye2jfpygs8p3cu2sktrtd"],"total_amount":"0","spent_by_me":"0","received_by_me":"0","my_balance_change":"0","block_height":1468165,"timestamp":1633426943,"fee_details":null,"coin":"tBCH","internal_id":"8c18978529a68bb9eb0cd4b3488775ef2dcfa473baab23158174c4a8b68a03e8","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000001a789174935fb6af2d669b52d03e4dc37dbf0d6afced2d598c54bb692c02367d7010000006b483045022100c3fee0b751f098debd3cfd6befdcd7210f192840143655dee95157c20c9731920220548dab4ddaabd4da92c838d036853a97d6ab98af6d4d8894f82f261c9ed14aa34121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff013dfcad00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acda865161","tx_hash":"091877294268b2b1734255067146f15c3ac5e6199e72cd4f68a8d9dec32bb0c0","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.11403301","spent_by_me":"0.11403301","received_by_me":"0.11402301","my_balance_change":"-0.00001000","block_height":1467009,"timestamp":1632733367,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"091877294268b2b1734255067146f15c3ac5e6199e72cd4f68a8d9dec32bb0c0","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000190e35c09c83b5818b441c18a2d5ec54734851e5581fb21bde7936e77c6c3dca8030000006b483045022100e6b1415cbd81f2d04360597fba65965bc77ab5a972f5b8f8d5c0f1b1912923c402206a63f305f03e9c49ffba6c71c7a76ef60631f67dce7631f673a0e8485b86898d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff020000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e82500ae00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac62715161","tx_hash":"d76723c092b64bc598d5d2ceafd6f0db37dce4032db569d6f26afb35491789a7","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.11404301","spent_by_me":"0.11404301","received_by_me":"0.11403301","my_balance_change":"-0.00001000","block_height":1467000,"timestamp":1632728165,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"d76723c092b64bc598d5d2ceafd6f0db37dce4032db569d6f26afb35491789a7","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000190e35c09c83b5818b441c18a2d5ec54734851e5581fb21bde7936e77c6c3dca8030000006b483045022100e6b1415cbd81f2d04360597fba65965bc77ab5a972f5b8f8d5c0f1b1912923c402206a63f305f03e9c49ffba6c71c7a76ef60631f67dce7631f673a0e8485b86898d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff020000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e82500ae00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac62715161","tx_hash":"d76723c092b64bc598d5d2ceafd6f0db37dce4032db569d6f26afb35491789a7","from":[],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0","spent_by_me":"0","received_by_me":"0.1","my_balance_change":"0.1","block_height":1467000,"timestamp":1632728165,"fee_details":null,"coin":"tBCH","internal_id":"045fa17faacd5e2b420266cca1265b37549aa45e140a30f0b92da7fb54ac3cdf","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000027f6af57604a18438921d5a1ce0c62af7e6f372fdf8c31a654796903f613145e6020000006a47304402200657110d9cad55a3cddb6515006a0263e93d4017f9eded90196d7ef51783cd3d022046b40ceccd9e5edd14a9651db8ea1a2a8a7bd5a8d49a2e5c37cc725763b95ae04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff5a181e6feadb8db84c6aebea0cd2006a09f1981a0d88df2fa6a3fc1350ecc9e5010000006b483045022100d3600f5b4db791d4385a205bc1b27723bf950fdca15471e23577ab4244e1b51802203584b8d66324a29117699a72cb0ae40989fd22fdd3aecb1a1228445ccce359914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000271008000000000000ceeee8030000000000001976a914eed5d3ad264ffc68fc0a6454e1696a30d8f405be88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0d04ae00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2f3e1261","tx_hash":"a8dcc3c6776e93e7bd21fb81551e853447c55e2d8ac141b418583bc8095ce390","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qrhdt5adye8lc68upfj9fctfdgcd3aq9hctf8ft6md","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.11407782","spent_by_me":"0.11407782","received_by_me":"0.11405301","my_balance_change":"-0.00002481","block_height":1460099,"timestamp":1628586152,"fee_details":{"type":"Utxo","amount":"0.00001481"},"coin":"tBCH","internal_id":"a8dcc3c6776e93e7bd21fb81551e853447c55e2d8ac141b418583bc8095ce390","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000027f6af57604a18438921d5a1ce0c62af7e6f372fdf8c31a654796903f613145e6020000006a47304402200657110d9cad55a3cddb6515006a0263e93d4017f9eded90196d7ef51783cd3d022046b40ceccd9e5edd14a9651db8ea1a2a8a7bd5a8d49a2e5c37cc725763b95ae04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff5a181e6feadb8db84c6aebea0cd2006a09f1981a0d88df2fa6a3fc1350ecc9e5010000006b483045022100d3600f5b4db791d4385a205bc1b27723bf950fdca15471e23577ab4244e1b51802203584b8d66324a29117699a72cb0ae40989fd22fdd3aecb1a1228445ccce359914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000271008000000000000ceeee8030000000000001976a914eed5d3ad264ffc68fc0a6454e1696a30d8f405be88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0d04ae00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2f3e1261","tx_hash":"a8dcc3c6776e93e7bd21fb81551e853447c55e2d8ac141b418583bc8095ce390","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qrhdt5adye8lc68upfj9fctfdgcd3aq9hcsaqj3dfs","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.2974","spent_by_me":"6.2974","received_by_me":"5.2974","my_balance_change":"-1.0000","block_height":1460099,"timestamp":1628586152,"fee_details":null,"coin":"tBCH","internal_id":"20976b31d1e0a5257bf983bdcbbb4230cc465197cc6dd886a9cf2e17b1b9acb0","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000017f6af57604a18438921d5a1ce0c62af7e6f372fdf8c31a654796903f613145e6030000006b483045022100c335dd0f22e047b806a9d84e02b70aab609093e960888f6f1878e605a173e3da02201c274ce4983d8e519a47c4bd17aeca897b084954ce7a9d77033100e06aa999304121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff0280969800000000001976a914eed5d3ad264ffc68fc0a6454e1696a30d8f405be88acbe0dae00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7a361261","tx_hash":"e5c9ec5013fca3a62fdf880d1a98f1096a00d20ceaeb6a4cb88ddbea6f1e185a","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qrhdt5adye8lc68upfj9fctfdgcd3aq9hctf8ft6md","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21407847","spent_by_me":"0.21407847","received_by_me":"0.11406782","my_balance_change":"-0.10001065","block_height":1460096,"timestamp":1628584118,"fee_details":{"type":"Utxo","amount":"0.00001065"},"coin":"tBCH","internal_id":"e5c9ec5013fca3a62fdf880d1a98f1096a00d20ceaeb6a4cb88ddbea6f1e185a","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002ebc10f58f220ec1bad5d634684ae649aa7bdd2f9c9081d36e5384e579caa95c2020000006a4730440220639ac218f572520c7d8addae74be6bfdefa9c86bc91474b6dedd7e117d232085022015a92f45f9ae5cee08c188e01fc614b77c461a41733649a55abfcc3e7ca207444121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffebc10f58f220ec1bad5d634684ae649aa7bdd2f9c9081d36e5384e579caa95c2030000006a47304402204c27a2c04df44f34bd71ec69cc0a24291a96f265217473affb3c3fce2dbd937202202c2ad2e6cfaac3901c807d9b048ccb2b5e7b0dbd922f2066e637f6bbf459313a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000000f5fee80300000000000017a9146569d9a853a1934c642223a9432f18c3b3f2a64b87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac67a84601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac87caee60","tx_hash":"e64531613f909647651ac3f8fd72f3e6f72ac6e01c5a1d923884a10476f56a7f","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:ppjknkdg2wsexnryyg36jse0rrpm8u4xfv9hwa0rgl","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21411847","spent_by_me":"0.21411847","received_by_me":"0.21408847","my_balance_change":"-0.00003000","block_height":1456230,"timestamp":1626262632,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"e64531613f909647651ac3f8fd72f3e6f72ac6e01c5a1d923884a10476f56a7f","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002ebc10f58f220ec1bad5d634684ae649aa7bdd2f9c9081d36e5384e579caa95c2020000006a4730440220639ac218f572520c7d8addae74be6bfdefa9c86bc91474b6dedd7e117d232085022015a92f45f9ae5cee08c188e01fc614b77c461a41733649a55abfcc3e7ca207444121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffebc10f58f220ec1bad5d634684ae649aa7bdd2f9c9081d36e5384e579caa95c2030000006a47304402204c27a2c04df44f34bd71ec69cc0a24291a96f265217473affb3c3fce2dbd937202202c2ad2e6cfaac3901c807d9b048ccb2b5e7b0dbd922f2066e637f6bbf459313a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000000f5fee80300000000000017a9146569d9a853a1934c642223a9432f18c3b3f2a64b87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac67a84601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac87caee60","tx_hash":"e64531613f909647651ac3f8fd72f3e6f72ac6e01c5a1d923884a10476f56a7f","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:ppjknkdg2wsexnryyg36jse0rrpm8u4xfv7rfx456z","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.3974","spent_by_me":"6.3974","received_by_me":"6.2974","my_balance_change":"-0.1000","block_height":1456230,"timestamp":1626262632,"fee_details":null,"coin":"tBCH","internal_id":"babe9bd0dc1495dff0920da14a76311b744daadc9d01314f8bd4e2438c6b183b","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002f418c909e5baba0708643146d6f1b0b77af9d3fa7f199c56a0e61b230d3dbcee020000006a47304402201367fc23d4acf145c1aa9b7b4111db42a01bf9e8d1555f09b99bfc0e3a26cac7022053dd8f2d617e0acf132f3930b5e91515fd997e07e04e945a2066f09c63739ce24121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cfffffffff418c909e5baba0708643146d6f1b0b77af9d3fa7f199c56a0e61b230d3dbcee030000006a4730440220010f1d0f1d6246575cbb3d666b453bdd1515f3d9f6cabfb4a7907df50b8aee4f022066c56d4bb7126882c2044d2c777f5c78a23a7a36a9709b735e69d2bd3946c92f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000000f9e6e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1fb44601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac85caee60","tx_hash":"c295aa9c574e38e5361d08c9f9d2bda79a64ae8446635dad1bec20f2580fc1eb","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21414847","spent_by_me":"0.21414847","received_by_me":"0.21411847","my_balance_change":"-0.00003000","block_height":1456230,"timestamp":1626262632,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"c295aa9c574e38e5361d08c9f9d2bda79a64ae8446635dad1bec20f2580fc1eb","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002f418c909e5baba0708643146d6f1b0b77af9d3fa7f199c56a0e61b230d3dbcee020000006a47304402201367fc23d4acf145c1aa9b7b4111db42a01bf9e8d1555f09b99bfc0e3a26cac7022053dd8f2d617e0acf132f3930b5e91515fd997e07e04e945a2066f09c63739ce24121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cfffffffff418c909e5baba0708643146d6f1b0b77af9d3fa7f199c56a0e61b230d3dbcee030000006a4730440220010f1d0f1d6246575cbb3d666b453bdd1515f3d9f6cabfb4a7907df50b8aee4f022066c56d4bb7126882c2044d2c777f5c78a23a7a36a9709b735e69d2bd3946c92f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000000f9e6e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1fb44601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac85caee60","tx_hash":"c295aa9c574e38e5361d08c9f9d2bda79a64ae8446635dad1bec20f2580fc1eb","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.3975","spent_by_me":"6.3975","received_by_me":"6.3974","my_balance_change":"-0.0001","block_height":1456230,"timestamp":1626262632,"fee_details":null,"coin":"tBCH","internal_id":"433b641bc89e1b59c22717918583c60ec98421805c8e85b064691705d9aeb970","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000270fba4f0921a57c550bfe911fa436757cc65f56825f2ff0581aed9775a72fb9c020000006b483045022100c0b9faeb97307ed33db1ac5c5b1b189d7b18267676a3e01d65a48d310651779302205e5be1e1d5ebfeed6994f52c1cc1af2908ff2d9d9b7eb846c7e715e9673e228a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff70fba4f0921a57c550bfe911fa436757cc65f56825f2ff0581aed9775a72fb9c030000006b483045022100965eed3d4152262adbacc52f928b59385480cd4ef44e899d2064a6f38503794902205cebbf1abaab0c116f4b63417cdf6a13dd681cf75e520e26caa7257f9064c6454121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000000f9e7e80300000000000017a914fe9318c279369c68cb240c88ef2c2df18cea63e087e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acd7bf4601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc3c7ee60","tx_hash":"eebc3d0d231be6a0569c197ffad3f97ab7b0f1d64631640807babae509c918f4","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:prlfxxxz0ymfc6xtysxg3mev9hcce6nruq9p6sq73k","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21417847","spent_by_me":"0.21417847","received_by_me":"0.21414847","my_balance_change":"-0.00003000","block_height":1456229,"timestamp":1626261653,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"eebc3d0d231be6a0569c197ffad3f97ab7b0f1d64631640807babae509c918f4","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000270fba4f0921a57c550bfe911fa436757cc65f56825f2ff0581aed9775a72fb9c020000006b483045022100c0b9faeb97307ed33db1ac5c5b1b189d7b18267676a3e01d65a48d310651779302205e5be1e1d5ebfeed6994f52c1cc1af2908ff2d9d9b7eb846c7e715e9673e228a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff70fba4f0921a57c550bfe911fa436757cc65f56825f2ff0581aed9775a72fb9c030000006b483045022100965eed3d4152262adbacc52f928b59385480cd4ef44e899d2064a6f38503794902205cebbf1abaab0c116f4b63417cdf6a13dd681cf75e520e26caa7257f9064c6454121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000000f9e7e80300000000000017a914fe9318c279369c68cb240c88ef2c2df18cea63e087e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acd7bf4601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc3c7ee60","tx_hash":"eebc3d0d231be6a0569c197ffad3f97ab7b0f1d64631640807babae509c918f4","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:prlfxxxz0ymfc6xtysxg3mev9hcce6nruq74at6frt","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.4975","spent_by_me":"6.4975","received_by_me":"6.3975","my_balance_change":"-0.1000","block_height":1456229,"timestamp":1626261653,"fee_details":null,"coin":"tBCH","internal_id":"cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000026fd35b26526cebfc7df0b229bde298b04aa853c0499c4c54c12df0118ed84052020000006a47304402203bd00a6a61434abb4fe4359aad6991e28eeb224fc1f4524309bebca89fbe5807022062a38c4e1da944942c6722141df6bae9c44c07ed52fd736e1f8c75e15aaf99f44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff6fd35b26526cebfc7df0b229bde298b04aa853c0499c4c54c12df0118ed84052030000006b483045022100909a8d0be19172c68c2889f240b1531d4a921bf9df8d081fce172d95c7a971cd02206ab0de0ed044b002341e72678106c558cd5e8565352fd78dcdf88a574c409a754121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000000fdcfe8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8fcb4601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc0c7ee60","tx_hash":"9cfb725a77d9ae8105fff22568f565cc576743fa11e9bf50c5571a92f0a4fb70","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21420847","spent_by_me":"0.21420847","received_by_me":"0.21417847","my_balance_change":"-0.00003000","block_height":1456229,"timestamp":1626261653,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"9cfb725a77d9ae8105fff22568f565cc576743fa11e9bf50c5571a92f0a4fb70","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000026fd35b26526cebfc7df0b229bde298b04aa853c0499c4c54c12df0118ed84052020000006a47304402203bd00a6a61434abb4fe4359aad6991e28eeb224fc1f4524309bebca89fbe5807022062a38c4e1da944942c6722141df6bae9c44c07ed52fd736e1f8c75e15aaf99f44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff6fd35b26526cebfc7df0b229bde298b04aa853c0499c4c54c12df0118ed84052030000006b483045022100909a8d0be19172c68c2889f240b1531d4a921bf9df8d081fce172d95c7a971cd02206ab0de0ed044b002341e72678106c558cd5e8565352fd78dcdf88a574c409a754121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000000fdcfe8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8fcb4601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc0c7ee60","tx_hash":"9cfb725a77d9ae8105fff22568f565cc576743fa11e9bf50c5571a92f0a4fb70","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.4976","spent_by_me":"6.4976","received_by_me":"6.4975","my_balance_change":"-0.0001","block_height":1456229,"timestamp":1626261653,"fee_details":null,"coin":"tBCH","internal_id":"1c1e68357cf5a6dacb53881f13aa5d2048fe0d0fab24b76c9ec48f53884bed97","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000024aaac282f81cc302e86957d19acd73bdf2757a2ad84b9377ef8dcab593a34545020000006a4730440220772cb5f66b9eb22a48d9aa65711f121a7d074cc645c7bd32e6068f28a7f67e16022070b627a64c0b81c8f3a25d398da12707488cdc8b1782629d2b1cb4c6e17f2b0a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff4aaac282f81cc302e86957d19acd73bdf2757a2ad84b9377ef8dcab593a34545030000006a473044022074bac9d2396dc7b9e44fd83a722620b2f553404d94356267debb421027bed67e02200513bc889699dab01a7abc0248a8ecdcabbceaac119b453fe6b553848d5178534121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000000fdd0e80300000000000017a914fbda7aee561fd22fd76c6c29b712aee35b8dc44187e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac47d74601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7a5aed60","tx_hash":"5240d88e11f02dc1544c9c49c053a84ab098e2bd29b2f07dfceb6c52265bd36f","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:praa57hw2c0ayt7hd3kzndcj4m34hrwygy4jffn8sy","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21423847","spent_by_me":"0.21423847","received_by_me":"0.21420847","my_balance_change":"-0.00003000","block_height":1456064,"timestamp":1626168435,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"5240d88e11f02dc1544c9c49c053a84ab098e2bd29b2f07dfceb6c52265bd36f","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000024aaac282f81cc302e86957d19acd73bdf2757a2ad84b9377ef8dcab593a34545020000006a4730440220772cb5f66b9eb22a48d9aa65711f121a7d074cc645c7bd32e6068f28a7f67e16022070b627a64c0b81c8f3a25d398da12707488cdc8b1782629d2b1cb4c6e17f2b0a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff4aaac282f81cc302e86957d19acd73bdf2757a2ad84b9377ef8dcab593a34545030000006a473044022074bac9d2396dc7b9e44fd83a722620b2f553404d94356267debb421027bed67e02200513bc889699dab01a7abc0248a8ecdcabbceaac119b453fe6b553848d5178534121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000000fdd0e80300000000000017a914fbda7aee561fd22fd76c6c29b712aee35b8dc44187e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac47d74601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7a5aed60","tx_hash":"5240d88e11f02dc1544c9c49c053a84ab098e2bd29b2f07dfceb6c52265bd36f","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:praa57hw2c0ayt7hd3kzndcj4m34hrwygywxwjfsze","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.5976","spent_by_me":"6.5976","received_by_me":"6.4976","my_balance_change":"-0.1000","block_height":1456064,"timestamp":1626168435,"fee_details":null,"coin":"tBCH","internal_id":"c4304b5ef4f1b88ed4939534a8ca9eca79f592939233174ae08002e8454e3f06","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002de1e81fdbb6766cfbd5feed3f71845fb9980df3d3029d07012a48b6eef48552a020000006a473044022064459a789f9dc6efbf1464922bb4e778b434afdc9a4034f587776169d2e2a966022031f38a543b67354263d865371a931f6fd9c9a70fa336e563d1aa71579376f6004121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffde1e81fdbb6766cfbd5feed3f71845fb9980df3d3029d07012a48b6eef48552a030000006b483045022100f1f35326636053589c9dcbe5d60e9ab95d04076ba5563862cd00b39e6af9326f02201f31c95858d534298aa5a5044d744cb9f6ee5a057d0177735e3b8b3ebd36c26d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000101b8e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acffe24601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac765aed60","tx_hash":"4545a393b5ca8def77934bd82a7a75f2bd73cd9ad15769e802c31cf882c2aa4a","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21426847","spent_by_me":"0.21426847","received_by_me":"0.21423847","my_balance_change":"-0.00003000","block_height":1456064,"timestamp":1626168435,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"4545a393b5ca8def77934bd82a7a75f2bd73cd9ad15769e802c31cf882c2aa4a","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002de1e81fdbb6766cfbd5feed3f71845fb9980df3d3029d07012a48b6eef48552a020000006a473044022064459a789f9dc6efbf1464922bb4e778b434afdc9a4034f587776169d2e2a966022031f38a543b67354263d865371a931f6fd9c9a70fa336e563d1aa71579376f6004121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffde1e81fdbb6766cfbd5feed3f71845fb9980df3d3029d07012a48b6eef48552a030000006b483045022100f1f35326636053589c9dcbe5d60e9ab95d04076ba5563862cd00b39e6af9326f02201f31c95858d534298aa5a5044d744cb9f6ee5a057d0177735e3b8b3ebd36c26d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000101b8e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acffe24601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac765aed60","tx_hash":"4545a393b5ca8def77934bd82a7a75f2bd73cd9ad15769e802c31cf882c2aa4a","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.5977","spent_by_me":"6.5977","received_by_me":"6.5976","my_balance_change":"-0.0001","block_height":1456064,"timestamp":1626168435,"fee_details":null,"coin":"tBCH","internal_id":"b0035434a1e7be5af2ed991ee2a21a90b271c5852a684a0b7d315c5a770d1b1c","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000029ee15b0b97c5fd25baafbf7375caab4110a74653566c902f15c62383a445f013020000006b483045022100b738bb36ad5dd6d35f2c21d1817a77df2830e27c40d5bd950460542ed1382d580220740f189c29ff9a8acb488db22be5c04ce06c33f44911f742236389cd62acf06c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff9ee15b0b97c5fd25baafbf7375caab4110a74653566c902f15c62383a445f013030000006b4830450221008278cab9ea89412af4366b7cf13223a66c75f1b99b8d2426ab45874bb76fda020220153585a9c531f53eff0a1de2285159aa60d882d9b0f8c4b81abafa06608d15c54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000101b9e80300000000000017a914fd3dee29c96afa812f7fad76a04a2499d1555fac87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb7ee4601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca745ed60","tx_hash":"2a5548ef6e8ba41270d029303ddf8099fb4518f7d3ee5fbdcf6667bbfd811ede","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pr7nmm3fe9404qf007khdgz2yjvaz42l4sqdkmt79k","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21429847","spent_by_me":"0.21429847","received_by_me":"0.21426847","my_balance_change":"-0.00003000","block_height":1456060,"timestamp":1626163734,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"2a5548ef6e8ba41270d029303ddf8099fb4518f7d3ee5fbdcf6667bbfd811ede","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000029ee15b0b97c5fd25baafbf7375caab4110a74653566c902f15c62383a445f013020000006b483045022100b738bb36ad5dd6d35f2c21d1817a77df2830e27c40d5bd950460542ed1382d580220740f189c29ff9a8acb488db22be5c04ce06c33f44911f742236389cd62acf06c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff9ee15b0b97c5fd25baafbf7375caab4110a74653566c902f15c62383a445f013030000006b4830450221008278cab9ea89412af4366b7cf13223a66c75f1b99b8d2426ab45874bb76fda020220153585a9c531f53eff0a1de2285159aa60d882d9b0f8c4b81abafa06608d15c54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000101b9e80300000000000017a914fd3dee29c96afa812f7fad76a04a2499d1555fac87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb7ee4601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca745ed60","tx_hash":"2a5548ef6e8ba41270d029303ddf8099fb4518f7d3ee5fbdcf6667bbfd811ede","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pr7nmm3fe9404qf007khdgz2yjvaz42l4sme3q3fht","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.6977","spent_by_me":"6.6977","received_by_me":"6.5977","my_balance_change":"-0.1000","block_height":1456060,"timestamp":1626163734,"fee_details":null,"coin":"tBCH","internal_id":"bd9e35d35e4296a53693074c51e92458a5982bf60e966c919be6d05958f4c406","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000203d79bcfeb47659aeabf17f71043fb9ce7f985edf4a20d3244673f737943fce6020000006a4730440220340bed348f70c0fb7362d7b37451ae6a604116140172e866edead7200eb92693022027880c0aa2bbc2eaa3834238d4e04f4675200d4424e9bec21e1d0c6bb3832c594121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff03d79bcfeb47659aeabf17f71043fb9ce7f985edf4a20d3244673f737943fce6030000006a473044022042452a36d3d44bc85f77881a1bb6a55120a269c95544ca0a0951492e439bc0f1022020a375bf7f34f95148b8b9c55129a995805cc35e84c5c3c89603a0d5f181be694121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000105a1e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6ffa4601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca645ed60","tx_hash":"13f045a48323c6152f906c565346a71041abca7573bfafba25fdc5970b5be19e","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21432847","spent_by_me":"0.21432847","received_by_me":"0.21429847","my_balance_change":"-0.00003000","block_height":1456060,"timestamp":1626163734,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"13f045a48323c6152f906c565346a71041abca7573bfafba25fdc5970b5be19e","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000203d79bcfeb47659aeabf17f71043fb9ce7f985edf4a20d3244673f737943fce6020000006a4730440220340bed348f70c0fb7362d7b37451ae6a604116140172e866edead7200eb92693022027880c0aa2bbc2eaa3834238d4e04f4675200d4424e9bec21e1d0c6bb3832c594121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff03d79bcfeb47659aeabf17f71043fb9ce7f985edf4a20d3244673f737943fce6030000006a473044022042452a36d3d44bc85f77881a1bb6a55120a269c95544ca0a0951492e439bc0f1022020a375bf7f34f95148b8b9c55129a995805cc35e84c5c3c89603a0d5f181be694121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000105a1e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6ffa4601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca645ed60","tx_hash":"13f045a48323c6152f906c565346a71041abca7573bfafba25fdc5970b5be19e","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.6978","spent_by_me":"6.6978","received_by_me":"6.6977","my_balance_change":"-0.0001","block_height":1456060,"timestamp":1626163734,"fee_details":null,"coin":"tBCH","internal_id":"3c76ca7ab4290a09683d13794e8636e2071344beb7364ab043ce8c2dd14514a8","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002dc7d15bb62351eabe269d01499d2a1c00bee0705eaa1cef29cf8d04a8bcd184d020000006b483045022100d113d6de671c750ef79bc60f14da46a9e255e33e3f0b85fb37533706a63826390220777565b655e7e28d6f6e12a89a4d5c6e6a56f47d6b4c5f2aef418b4d7a8710a74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffdc7d15bb62351eabe269d01499d2a1c00bee0705eaa1cef29cf8d04a8bcd184d030000006a47304402204f63572b779323619eb16c40db07360346bd5f60ee90075c97e4db40ea59aed602206c7753de0f58417caf6fc92a6bec8fefcad4ae410bb73d9206719244ff53834f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000105a2e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac27064701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc336ec60","tx_hash":"e6fc4379733f6744320da2f4ed85f9e79cfb4310f717bfea9a6547ebcf9bd703","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21435847","spent_by_me":"0.21435847","received_by_me":"0.21432847","my_balance_change":"-0.00003000","block_height":1455944,"timestamp":1626094075,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"e6fc4379733f6744320da2f4ed85f9e79cfb4310f717bfea9a6547ebcf9bd703","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002dc7d15bb62351eabe269d01499d2a1c00bee0705eaa1cef29cf8d04a8bcd184d020000006b483045022100d113d6de671c750ef79bc60f14da46a9e255e33e3f0b85fb37533706a63826390220777565b655e7e28d6f6e12a89a4d5c6e6a56f47d6b4c5f2aef418b4d7a8710a74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffdc7d15bb62351eabe269d01499d2a1c00bee0705eaa1cef29cf8d04a8bcd184d030000006a47304402204f63572b779323619eb16c40db07360346bd5f60ee90075c97e4db40ea59aed602206c7753de0f58417caf6fc92a6bec8fefcad4ae410bb73d9206719244ff53834f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000105a2e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac27064701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc336ec60","tx_hash":"e6fc4379733f6744320da2f4ed85f9e79cfb4310f717bfea9a6547ebcf9bd703","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.6979","spent_by_me":"6.6979","received_by_me":"6.6978","my_balance_change":"-0.0001","block_height":1455944,"timestamp":1626094075,"fee_details":null,"coin":"tBCH","internal_id":"d7b73b1e45e78f5738760355cf643bb0ce7f84a87e51dfed3ddcd999f5a1892a","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000026d51c211fbbb091312efc8f4051666807e13402102f90aaccac7be62fb5958de020000006b483045022100e660ebcb57c4a2d7aa67d02be84e7347a50e277544f734ca7edd58a76b8ded1c022069153d86b2df7663c79c79d6411c1edb287595b0e96557af353fce8ca9b4291d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff6d51c211fbbb091312efc8f4051666807e13402102f90aaccac7be62fb5958de030000006a47304402204b0ce7a6baf38d93b70ffd069a96e714d07668ee8c79a0df5ff37ecf5e3f6490022039b32d03755772032b513311bdcc5fcd5aadc1ec863fe31bfecddad204eb1a324121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000001098be8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac971d4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc52fec60","tx_hash":"bc2fc4aa6996da2267fd0f8cc18d3752d290dad15d2291c887351b18ffd10cb0","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21441847","spent_by_me":"0.21441847","received_by_me":"0.21438847","my_balance_change":"-0.00003000","block_height":1455942,"timestamp":1626092520,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"bc2fc4aa6996da2267fd0f8cc18d3752d290dad15d2291c887351b18ffd10cb0","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000026d51c211fbbb091312efc8f4051666807e13402102f90aaccac7be62fb5958de020000006b483045022100e660ebcb57c4a2d7aa67d02be84e7347a50e277544f734ca7edd58a76b8ded1c022069153d86b2df7663c79c79d6411c1edb287595b0e96557af353fce8ca9b4291d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff6d51c211fbbb091312efc8f4051666807e13402102f90aaccac7be62fb5958de030000006a47304402204b0ce7a6baf38d93b70ffd069a96e714d07668ee8c79a0df5ff37ecf5e3f6490022039b32d03755772032b513311bdcc5fcd5aadc1ec863fe31bfecddad204eb1a324121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000001098be8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac971d4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc52fec60","tx_hash":"bc2fc4aa6996da2267fd0f8cc18d3752d290dad15d2291c887351b18ffd10cb0","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.798","spent_by_me":"6.798","received_by_me":"6.7979","my_balance_change":"-0.0001","block_height":1455942,"timestamp":1626092520,"fee_details":null,"coin":"tBCH","internal_id":"2a5f6789a28e62ebfcac80e1e371c87ac1c8a363ee844b65e52fe603633a12fe","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002b00cd1ff181b3587c891225dd1da90d252378dc18c0ffd6722da9669aac42fbc020000006a47304402206a76876825e83613dea316a03db8df83cdf198d11da60e601fb2e436cbb813ea02206f5450aff9dfa35c9f3bffef649474d4b974746f822ba2f674907601872a7dc94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb00cd1ff181b3587c891225dd1da90d252378dc18c0ffd6722da9669aac42fbc030000006b483045022100ce05de693f51b977545a59aff4eb9f8293c4590ae5a69ac823a2a92d5d7dc33502206271a537f50713f6d79800977a9695023ddcae6a9758642c2397b57991de19224121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000105a3e80300000000000017a914bf0e3e4395363c21f2ae5f8514d6e0636d64753b87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdf114701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc82fec60","tx_hash":"4d18cd8b4ad0f89cf2cea1ea0507ee0bc0a1d29914d069e2ab1e3562bb157ddc","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzlsu0jrj5mrcg0j4e0c29xkup3k6er48vw7yxjdtl","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21438847","spent_by_me":"0.21438847","received_by_me":"0.21435847","my_balance_change":"-0.00003000","block_height":1455942,"timestamp":1626092520,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"4d18cd8b4ad0f89cf2cea1ea0507ee0bc0a1d29914d069e2ab1e3562bb157ddc","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002b00cd1ff181b3587c891225dd1da90d252378dc18c0ffd6722da9669aac42fbc020000006a47304402206a76876825e83613dea316a03db8df83cdf198d11da60e601fb2e436cbb813ea02206f5450aff9dfa35c9f3bffef649474d4b974746f822ba2f674907601872a7dc94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb00cd1ff181b3587c891225dd1da90d252378dc18c0ffd6722da9669aac42fbc030000006b483045022100ce05de693f51b977545a59aff4eb9f8293c4590ae5a69ac823a2a92d5d7dc33502206271a537f50713f6d79800977a9695023ddcae6a9758642c2397b57991de19224121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000105a3e80300000000000017a914bf0e3e4395363c21f2ae5f8514d6e0636d64753b87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdf114701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc82fec60","tx_hash":"4d18cd8b4ad0f89cf2cea1ea0507ee0bc0a1d29914d069e2ab1e3562bb157ddc","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzlsu0jrj5mrcg0j4e0c29xkup3k6er48v42rag6ez","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.7979","spent_by_me":"6.7979","received_by_me":"6.6979","my_balance_change":"-0.1000","block_height":1455942,"timestamp":1626092520,"fee_details":null,"coin":"tBCH","internal_id":"eb51cc74f9ed8b567ed7e2d5e93f94e34846407fccc8b4c82e02ff92bfc69bd6","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002174c10d97ce1db602e2ec12a79566029a58290b7f679d349bd535a0b66f23903020000006b483045022100fec07d643397cf597d7850158605d57123c9436167ac7ec4ba9a87f618bf26a2022057f5999ef200e63995cf76f238cf1efdfdbc579009685e394d43821a0498e1d74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff174c10d97ce1db602e2ec12a79566029a58290b7f679d349bd535a0b66f23903030000006a47304402202d292987b9de77aaa624c51518dfc4715b3f86329bb10c61f20bffe07d8c76a9022005c93c381c8d9af5569e300a8b3fea1a00d6d5256b5c2fb11835f4d81747f88d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000001098ce80300000000000017a9144a016e56ef1c3846f3636da1938cdcff489d267f87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4f294701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acfa1bec60","tx_hash":"de5859fb62bec7caac0af9022140137e80661605f4c8ef121309bbfb11c2516d","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pp9qzmjkauwrs3hnvdk6ryuvmnl538fx0ujw2qtywk","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21444847","spent_by_me":"0.21444847","received_by_me":"0.21441847","my_balance_change":"-0.00003000","block_height":1455932,"timestamp":1626086540,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"de5859fb62bec7caac0af9022140137e80661605f4c8ef121309bbfb11c2516d","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002174c10d97ce1db602e2ec12a79566029a58290b7f679d349bd535a0b66f23903020000006b483045022100fec07d643397cf597d7850158605d57123c9436167ac7ec4ba9a87f618bf26a2022057f5999ef200e63995cf76f238cf1efdfdbc579009685e394d43821a0498e1d74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff174c10d97ce1db602e2ec12a79566029a58290b7f679d349bd535a0b66f23903030000006a47304402202d292987b9de77aaa624c51518dfc4715b3f86329bb10c61f20bffe07d8c76a9022005c93c381c8d9af5569e300a8b3fea1a00d6d5256b5c2fb11835f4d81747f88d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000001098ce80300000000000017a9144a016e56ef1c3846f3636da1938cdcff489d267f87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4f294701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acfa1bec60","tx_hash":"de5859fb62bec7caac0af9022140137e80661605f4c8ef121309bbfb11c2516d","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pp9qzmjkauwrs3hnvdk6ryuvmnl538fx0uf6dm3nut","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.898","spent_by_me":"6.898","received_by_me":"6.798","my_balance_change":"-0.100","block_height":1455932,"timestamp":1626086540,"fee_details":null,"coin":"tBCH","internal_id":"0f14bb119589b11dae2554544265ba60d3558ae5e73d4ca73bfae3abafaf1188","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000025e3205d6a4a8c7b823572c2cb7c5f3172d7986f1bfed2b04321d3dcf44b22f7f020000006a47304402207b4ebcbbb8dc10a288397b9c0a25d54f59e85df648bd32f56e5f367579819142022051bfdacb030374cc04fb7436c877114b3d824e7e55b7a34ddfab05fe68092e8c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff5e3205d6a4a8c7b823572c2cb7c5f3172d7986f1bfed2b04321d3dcf44b22f7f030000006a473044022052a24a37a1bc67772c12c8426c91f4fd6a69d7066f83908b14b2ea10dac2e65102206f7858e0681f736303d23b6fe3f178e6b9785f30957028f5ffe5b29d702ae0734121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000010d74e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac07354701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aced1bec60","tx_hash":"0339f2660b5a53bd49d379f6b79082a5296056792ac12e2e60dbe17cd9104c17","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21447847","spent_by_me":"0.21447847","received_by_me":"0.21444847","my_balance_change":"-0.00003000","block_height":1455932,"timestamp":1626086540,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"0339f2660b5a53bd49d379f6b79082a5296056792ac12e2e60dbe17cd9104c17","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000025e3205d6a4a8c7b823572c2cb7c5f3172d7986f1bfed2b04321d3dcf44b22f7f020000006a47304402207b4ebcbbb8dc10a288397b9c0a25d54f59e85df648bd32f56e5f367579819142022051bfdacb030374cc04fb7436c877114b3d824e7e55b7a34ddfab05fe68092e8c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff5e3205d6a4a8c7b823572c2cb7c5f3172d7986f1bfed2b04321d3dcf44b22f7f030000006a473044022052a24a37a1bc67772c12c8426c91f4fd6a69d7066f83908b14b2ea10dac2e65102206f7858e0681f736303d23b6fe3f178e6b9785f30957028f5ffe5b29d702ae0734121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000010d74e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac07354701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aced1bec60","tx_hash":"0339f2660b5a53bd49d379f6b79082a5296056792ac12e2e60dbe17cd9104c17","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.8981","spent_by_me":"6.8981","received_by_me":"6.898","my_balance_change":"-0.0001","block_height":1455932,"timestamp":1626086540,"fee_details":null,"coin":"tBCH","internal_id":"2b2c364bcbda16dc1c91f5939fc57fda12a350954cbf6cedbc7c233713f4740a","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002bf7ec5e2f89089897ba9a327e5b1d45c8446193cbc9eedb90d2f4c5e83329f45020000006a473044022073e4c2b8c72d5597feaa4aac2d3122e30f6dd2e68d627fb7dc7e220acd70ea6c02206de3aec2dc49aff8b06c9575938379f5077db441a5c843a4d8bb7e326ec509f84121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffbf7ec5e2f89089897ba9a327e5b1d45c8446193cbc9eedb90d2f4c5e83329f45030000006b483045022100b1df08b3be7aad4a5b1e3abf3643f8bfb54b1c7c625a1710e9860439a1d7df1f022034d24cdbe29b1148eeaa0555823385e29f8acb305ec671907bd4ac9ef950d3304121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000010d75e80300000000000017a914eb39dbf19aedc8fb22467ce18fafcd97cb8cc2b187e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acbf404701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0c03ec60","tx_hash":"7f2fb244cf3d1d32042bedbff186792d17f3c5b72c2c5723b8c7a8a4d605325e","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pr4nnkl3ntku37ezge7wrra0ektuhrxzkyk9gpw5ce","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21450847","spent_by_me":"0.21450847","received_by_me":"0.21447847","my_balance_change":"-0.00003000","block_height":1455920,"timestamp":1626080445,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"7f2fb244cf3d1d32042bedbff186792d17f3c5b72c2c5723b8c7a8a4d605325e","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002bf7ec5e2f89089897ba9a327e5b1d45c8446193cbc9eedb90d2f4c5e83329f45020000006a473044022073e4c2b8c72d5597feaa4aac2d3122e30f6dd2e68d627fb7dc7e220acd70ea6c02206de3aec2dc49aff8b06c9575938379f5077db441a5c843a4d8bb7e326ec509f84121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffbf7ec5e2f89089897ba9a327e5b1d45c8446193cbc9eedb90d2f4c5e83329f45030000006b483045022100b1df08b3be7aad4a5b1e3abf3643f8bfb54b1c7c625a1710e9860439a1d7df1f022034d24cdbe29b1148eeaa0555823385e29f8acb305ec671907bd4ac9ef950d3304121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000010d75e80300000000000017a914eb39dbf19aedc8fb22467ce18fafcd97cb8cc2b187e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acbf404701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0c03ec60","tx_hash":"7f2fb244cf3d1d32042bedbff186792d17f3c5b72c2c5723b8c7a8a4d605325e","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pr4nnkl3ntku37ezge7wrra0ektuhrxzkyd3065r2y","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.9981","spent_by_me":"6.9981","received_by_me":"6.8981","my_balance_change":"-0.1000","block_height":1455920,"timestamp":1626080445,"fee_details":null,"coin":"tBCH","internal_id":"c97ea51d4c9fef38bcef7f5f8889187233c99907f087ae4fd27e9a3305e493f4","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002528acbbae8d79a7dcef6ddf53eddbab835880c88522d79fdf4f1512ab6685382020000006b483045022100c6a0be60fcb959f94980bc6ca0913793cbf17e9c84f79ef692daf4e603a4124402204c4ff28b0476a0e9363d0251468314499ffccebce9ddd8f2286d4220a5e3b0a34121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff528acbbae8d79a7dcef6ddf53eddbab835880c88522d79fdf4f1512ab6685382030000006b483045022100f1189b76a0f58d5ea2e75420dd6277d2030ef2aa7e1bdc301715870c7860e2a60220239d1d7b6678427c50d6e6e7f2a69d87c54c75be1578a4b3c8b9b246ef164eb04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000001115de8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac774c4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acff02ec60","tx_hash":"459f32835e4c2f0db9ed9ebc3c1946845cd4b1e527a3a97b898990f8e2c57ebf","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21453847","spent_by_me":"0.21453847","received_by_me":"0.21450847","my_balance_change":"-0.00003000","block_height":1455920,"timestamp":1626080445,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"459f32835e4c2f0db9ed9ebc3c1946845cd4b1e527a3a97b898990f8e2c57ebf","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002528acbbae8d79a7dcef6ddf53eddbab835880c88522d79fdf4f1512ab6685382020000006b483045022100c6a0be60fcb959f94980bc6ca0913793cbf17e9c84f79ef692daf4e603a4124402204c4ff28b0476a0e9363d0251468314499ffccebce9ddd8f2286d4220a5e3b0a34121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff528acbbae8d79a7dcef6ddf53eddbab835880c88522d79fdf4f1512ab6685382030000006b483045022100f1189b76a0f58d5ea2e75420dd6277d2030ef2aa7e1bdc301715870c7860e2a60220239d1d7b6678427c50d6e6e7f2a69d87c54c75be1578a4b3c8b9b246ef164eb04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000001115de8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac774c4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acff02ec60","tx_hash":"459f32835e4c2f0db9ed9ebc3c1946845cd4b1e527a3a97b898990f8e2c57ebf","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"6.9982","spent_by_me":"6.9982","received_by_me":"6.9981","my_balance_change":"-0.0001","block_height":1455920,"timestamp":1626080445,"fee_details":null,"coin":"tBCH","internal_id":"622eded551f4ab8052e462d9b13a5826b46d0850117b5309f7cf35d4b2dd7d20","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002b8c5dc25ebdbf124674e408d85367c8454a68ea3dbae0a8bf4e3039c4ebef0e6020000006b483045022100b6ca21e41daa73b6b6a0a053b5b6a72156c5af5258122e56a1c7fe0cd49695cb02202d2e09e50c874bc711f4fabf0d469846a5fdb599731720dffc9e9ec4521023904121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb8c5dc25ebdbf124674e408d85367c8454a68ea3dbae0a8bf4e3039c4ebef0e6030000006a47304402205f486aad621801ada4206fdef983224ed4a10e1d85785929870bfbf0ab7c939202201a37f499351f69a7ffe119b5f73d8e03cc746bfafbfb90d950bb1d4ffcfde4364121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000011546e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace7634701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3cf7e760","tx_hash":"c156cb702b0dbe463f64a354386169dd1a0b3808c904c27db8ed0256ced2c37e","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21459847","spent_by_me":"0.21459847","received_by_me":"0.21456847","my_balance_change":"-0.00003000","block_height":1455481,"timestamp":1625814954,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"c156cb702b0dbe463f64a354386169dd1a0b3808c904c27db8ed0256ced2c37e","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002b8c5dc25ebdbf124674e408d85367c8454a68ea3dbae0a8bf4e3039c4ebef0e6020000006b483045022100b6ca21e41daa73b6b6a0a053b5b6a72156c5af5258122e56a1c7fe0cd49695cb02202d2e09e50c874bc711f4fabf0d469846a5fdb599731720dffc9e9ec4521023904121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb8c5dc25ebdbf124674e408d85367c8454a68ea3dbae0a8bf4e3039c4ebef0e6030000006a47304402205f486aad621801ada4206fdef983224ed4a10e1d85785929870bfbf0ab7c939202201a37f499351f69a7ffe119b5f73d8e03cc746bfafbfb90d950bb1d4ffcfde4364121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000011546e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace7634701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3cf7e760","tx_hash":"c156cb702b0dbe463f64a354386169dd1a0b3808c904c27db8ed0256ced2c37e","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.0983","spent_by_me":"7.0983","received_by_me":"7.0982","my_balance_change":"-0.0001","block_height":1455481,"timestamp":1625814954,"fee_details":null,"coin":"tBCH","internal_id":"01108977dad384b3495ed3ee6d07401785431326784102c7f8b4e33c7fceedda","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000027ec3d2ce5602edb87dc204c908380b1add69613854a3643f46be0d2b70cb56c1020000006b4830450221008e47c2c07a9c4856fdec40a13abbbb87dab56706917e5314278549686febc43b022042e07e4b477ac5f29fa6c7ee2a97c73901d8861ad1c299a718c4c92ed5a2d1804121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff7ec3d2ce5602edb87dc204c908380b1add69613854a3643f46be0d2b70cb56c1030000006a473044022017821ab2ae4d31e6eebcf3e4575e06e8180ad03c17207f339d7b5441d85e50aa02200f78eb7687e600348742129ade40099e33902b30f638ac5574570be09b8c81c14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000001115ee80300000000000017a914b7e0007cf03669a5b4ae5268617432b030dc9d5187e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2f584701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3ef7e760","tx_hash":"825368b62a51f1f4fd792d52880c8835b8badd3ef5ddf6ce7d9ad7e8bacb8a52","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzm7qqru7qmxnfd54efxsct5x2crphya2yx0cn77dm","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21456847","spent_by_me":"0.21456847","received_by_me":"0.21453847","my_balance_change":"-0.00003000","block_height":1455481,"timestamp":1625814954,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"825368b62a51f1f4fd792d52880c8835b8badd3ef5ddf6ce7d9ad7e8bacb8a52","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000027ec3d2ce5602edb87dc204c908380b1add69613854a3643f46be0d2b70cb56c1020000006b4830450221008e47c2c07a9c4856fdec40a13abbbb87dab56706917e5314278549686febc43b022042e07e4b477ac5f29fa6c7ee2a97c73901d8861ad1c299a718c4c92ed5a2d1804121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff7ec3d2ce5602edb87dc204c908380b1add69613854a3643f46be0d2b70cb56c1030000006a473044022017821ab2ae4d31e6eebcf3e4575e06e8180ad03c17207f339d7b5441d85e50aa02200f78eb7687e600348742129ade40099e33902b30f638ac5574570be09b8c81c14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000001115ee80300000000000017a914b7e0007cf03669a5b4ae5268617432b030dc9d5187e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2f584701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3ef7e760","tx_hash":"825368b62a51f1f4fd792d52880c8835b8badd3ef5ddf6ce7d9ad7e8bacb8a52","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzm7qqru7qmxnfd54efxsct5x2crphya2yamlgyflx","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.0982","spent_by_me":"7.0982","received_by_me":"6.9982","my_balance_change":"-0.1000","block_height":1455481,"timestamp":1625814954,"fee_details":null,"coin":"tBCH","internal_id":"70dd3998d70330772870f23db91beeee9d52f7dc9de80c4e4a5369fa1c244a7b","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000020a54c63de908f7db54cbda8595ba47b616205d919e27b0534f784ac564ba313a020000006b483045022100daa11f5ece3150332a4283ee221450948ed35d261441dcedcf41823edc9357d80220474574926bd3002cadbc98ee2d08d9f4fb031376cf47d920fa70fee717c87eff4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff0a54c63de908f7db54cbda8595ba47b616205d919e27b0534f784ac564ba313a030000006b483045022100afd96dfe543efe8d84b5e1c05216db0bf4269d4f3986c8c04f3ec344079d2b330220498b5d147b9901b60aee2d7afb25f9acf5fe8b81616b70bb07dc4f6a6125a96b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000011547e80300000000000017a91472a1f30385c63ee266b6955d6edfdb10596de93087e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f6f4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb80de760","tx_hash":"e6f0be4e9c03e3f48b0aaedba38ea654847c36858d404e6724f1dbeb25dcc5b8","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:ppe2rucrshrracnxk6246mklmvg9jm0fxqvlx3y9ty","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21462847","spent_by_me":"0.21462847","received_by_me":"0.21459847","my_balance_change":"-0.00003000","block_height":1455413,"timestamp":1625755850,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"e6f0be4e9c03e3f48b0aaedba38ea654847c36858d404e6724f1dbeb25dcc5b8","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000020a54c63de908f7db54cbda8595ba47b616205d919e27b0534f784ac564ba313a020000006b483045022100daa11f5ece3150332a4283ee221450948ed35d261441dcedcf41823edc9357d80220474574926bd3002cadbc98ee2d08d9f4fb031376cf47d920fa70fee717c87eff4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff0a54c63de908f7db54cbda8595ba47b616205d919e27b0534f784ac564ba313a030000006b483045022100afd96dfe543efe8d84b5e1c05216db0bf4269d4f3986c8c04f3ec344079d2b330220498b5d147b9901b60aee2d7afb25f9acf5fe8b81616b70bb07dc4f6a6125a96b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000011547e80300000000000017a91472a1f30385c63ee266b6955d6edfdb10596de93087e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f6f4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb80de760","tx_hash":"e6f0be4e9c03e3f48b0aaedba38ea654847c36858d404e6724f1dbeb25dcc5b8","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:ppe2rucrshrracnxk6246mklmvg9jm0fxqhtp27jee","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.1983","spent_by_me":"7.1983","received_by_me":"7.0983","my_balance_change":"-0.1000","block_height":1455413,"timestamp":1625755850,"fee_details":null,"coin":"tBCH","internal_id":"ce6c7660410cc159b0d57509238189383d47ca5e1a154eea457b5615b3655833","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000263a5fab0683011ee4d18541e2aed8fb190f26fe08d216e88e247f602acc6eba7020000006b483045022100e78ce23da10343611783990aecdc833e68d6a41389af52c44be92c4a78ddc9db022058833c1c05deff16327cf48be706a5da3ed4227bf946463516409deb3cb8d86b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff63a5fab0683011ee4d18541e2aed8fb190f26fe08d216e88e247f602acc6eba7030000006b483045022100a232e5521febc0fb36bb01f50657662cc7297e27ea278067ade677fe98796cfd022015962e70f0c5a079655ecf485f42f3a28330eaf5fcad045bdd853da45394cda54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000001192fe8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac577b4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb50de760","tx_hash":"3a31ba64c54a784f53b0279e915d2016b647ba9585dacb54dbf708e93dc6540a","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21465847","spent_by_me":"0.21465847","received_by_me":"0.21462847","my_balance_change":"-0.00003000","block_height":1455413,"timestamp":1625755850,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"3a31ba64c54a784f53b0279e915d2016b647ba9585dacb54dbf708e93dc6540a","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000263a5fab0683011ee4d18541e2aed8fb190f26fe08d216e88e247f602acc6eba7020000006b483045022100e78ce23da10343611783990aecdc833e68d6a41389af52c44be92c4a78ddc9db022058833c1c05deff16327cf48be706a5da3ed4227bf946463516409deb3cb8d86b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff63a5fab0683011ee4d18541e2aed8fb190f26fe08d216e88e247f602acc6eba7030000006b483045022100a232e5521febc0fb36bb01f50657662cc7297e27ea278067ade677fe98796cfd022015962e70f0c5a079655ecf485f42f3a28330eaf5fcad045bdd853da45394cda54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000001192fe8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac577b4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb50de760","tx_hash":"3a31ba64c54a784f53b0279e915d2016b647ba9585dacb54dbf708e93dc6540a","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.1984","spent_by_me":"7.1984","received_by_me":"7.1983","my_balance_change":"-0.0001","block_height":1455413,"timestamp":1625755850,"fee_details":null,"coin":"tBCH","internal_id":"2c2a602e6fbbb97045ba95afb534479a59d1cf179a36959070c67d4ef9e92d56","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002bb93d534bfa95c5a746c22671ef5e9f7dae48b414c46d7885862e50887fcdc2b020000006a4730440220474ee418db2760c703016d01f8cd1440d36a45ae3eb5bea1d8ec257f22e34a0802201516a8b32af513c3914839d1bb6871ffe4b77928070444bd0e7ca1033ef53b1d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffbb93d534bfa95c5a746c22671ef5e9f7dae48b414c46d7885862e50887fcdc2b030000006a47304402203627759d399bb8ca178926fc93412bce1f78e2c6c43803e33db4eb79a4f6c7fa022016a1c2a54df3503a9749ac97f482b819a236cadc1273fc8dd70a9e2ef69193ce4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000011930e80300000000000017a914cdad3b10b52f6e13c606cdca160bc8f510c48e5187e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0f874701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5eb0e660","tx_hash":"a7ebc6ac02f647e2886e218de06ff290b18fed2a1e54184dee113068b0faa563","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:prx66wcsk5hkuy7xqmxu59ster63p3yw2ywmgzqrqz","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21468847","spent_by_me":"0.21468847","received_by_me":"0.21465847","my_balance_change":"-0.00003000","block_height":1455393,"timestamp":1625732178,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"a7ebc6ac02f647e2886e218de06ff290b18fed2a1e54184dee113068b0faa563","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002bb93d534bfa95c5a746c22671ef5e9f7dae48b414c46d7885862e50887fcdc2b020000006a4730440220474ee418db2760c703016d01f8cd1440d36a45ae3eb5bea1d8ec257f22e34a0802201516a8b32af513c3914839d1bb6871ffe4b77928070444bd0e7ca1033ef53b1d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffbb93d534bfa95c5a746c22671ef5e9f7dae48b414c46d7885862e50887fcdc2b030000006a47304402203627759d399bb8ca178926fc93412bce1f78e2c6c43803e33db4eb79a4f6c7fa022016a1c2a54df3503a9749ac97f482b819a236cadc1273fc8dd70a9e2ef69193ce4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000011930e80300000000000017a914cdad3b10b52f6e13c606cdca160bc8f510c48e5187e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0f874701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5eb0e660","tx_hash":"a7ebc6ac02f647e2886e218de06ff290b18fed2a1e54184dee113068b0faa563","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:prx66wcsk5hkuy7xqmxu59ster63p3yw2y400e65jl","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.2984","spent_by_me":"7.2984","received_by_me":"7.1984","my_balance_change":"-0.1000","block_height":1455393,"timestamp":1625732178,"fee_details":null,"coin":"tBCH","internal_id":"f4e1812cc0095cece6d05ec831d13c8ed9e58f546ad05bf41e28c37f2e4cf085","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002d01df8b7a9a203a5d1b1b797d339f69ff0f3c929e638758948f1497599f3ef7d020000006a47304402200e2dfab7c1cea057f825c65b5158c33e4bcefcce0d492cddc220fb63dc4d9bda022016048782f9b321406f8991c6b38562bf240626cbc35825a2a34f7e1bd49848024121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffd01df8b7a9a203a5d1b1b797d339f69ff0f3c929e638758948f1497599f3ef7d030000006b483045022100de0917450e56164d8cca69bc0e29f070eafd0b0b4239407ffbed73b673a2843d022062406ce0bb6c13b916a24cbb781aa44d6ac4f3db28a6d8b22ed06028f060f69d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000011d18e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc7924701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5bb0e660","tx_hash":"2bdcfc8708e5625888d7464c418be4daf7e9f51e67226c745a5ca9bf34d593bb","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21471847","spent_by_me":"0.21471847","received_by_me":"0.21468847","my_balance_change":"-0.00003000","block_height":1455393,"timestamp":1625732178,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"2bdcfc8708e5625888d7464c418be4daf7e9f51e67226c745a5ca9bf34d593bb","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002d01df8b7a9a203a5d1b1b797d339f69ff0f3c929e638758948f1497599f3ef7d020000006a47304402200e2dfab7c1cea057f825c65b5158c33e4bcefcce0d492cddc220fb63dc4d9bda022016048782f9b321406f8991c6b38562bf240626cbc35825a2a34f7e1bd49848024121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffd01df8b7a9a203a5d1b1b797d339f69ff0f3c929e638758948f1497599f3ef7d030000006b483045022100de0917450e56164d8cca69bc0e29f070eafd0b0b4239407ffbed73b673a2843d022062406ce0bb6c13b916a24cbb781aa44d6ac4f3db28a6d8b22ed06028f060f69d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000011d18e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc7924701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5bb0e660","tx_hash":"2bdcfc8708e5625888d7464c418be4daf7e9f51e67226c745a5ca9bf34d593bb","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.2985","spent_by_me":"7.2985","received_by_me":"7.2984","my_balance_change":"-0.0001","block_height":1455393,"timestamp":1625732178,"fee_details":null,"coin":"tBCH","internal_id":"536ba3818af4beb7eff054adc313c01a21cbca40dc480a2040e9f165af5d3ffd","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002846a1b801449b070e701db8d41c905fcad7db3fd7645987903c2a8b348ffe47f020000006b483045022100ecdcb4e81e0976693c7974029f74ebe0e2697f8547f4ef06130fa6d89519720f02203e794f2d66b6549a088f49d3d18ffbf8d46166d01af35fa2d03b3fedb6721ed64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff846a1b801449b070e701db8d41c905fcad7db3fd7645987903c2a8b348ffe47f030000006b483045022100f7db307d111357b6926564728a52a83b714c5a850e1a8fcb081cb6499929e2f702200fd5a171f9a06ec03ad844ee5513b72e1f6f6def1ddd0360f20f6161cae46be44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000011d19e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7f9e4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace828df60","tx_hash":"7deff3997549f148897538e629c9f3f09ff639d397b7b1d1a503a2a9b7f81dd0","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21474847","spent_by_me":"0.21474847","received_by_me":"0.21471847","my_balance_change":"-0.00003000","block_height":1454520,"timestamp":1625238393,"fee_details":{"type":"Utxo","amount":"0.00002"},"coin":"tBCH","internal_id":"7deff3997549f148897538e629c9f3f09ff639d397b7b1d1a503a2a9b7f81dd0","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002846a1b801449b070e701db8d41c905fcad7db3fd7645987903c2a8b348ffe47f020000006b483045022100ecdcb4e81e0976693c7974029f74ebe0e2697f8547f4ef06130fa6d89519720f02203e794f2d66b6549a088f49d3d18ffbf8d46166d01af35fa2d03b3fedb6721ed64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff846a1b801449b070e701db8d41c905fcad7db3fd7645987903c2a8b348ffe47f030000006b483045022100f7db307d111357b6926564728a52a83b714c5a850e1a8fcb081cb6499929e2f702200fd5a171f9a06ec03ad844ee5513b72e1f6f6def1ddd0360f20f6161cae46be44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000011d19e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7f9e4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace828df60","tx_hash":"7deff3997549f148897538e629c9f3f09ff639d397b7b1d1a503a2a9b7f81dd0","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.2986","spent_by_me":"7.2986","received_by_me":"7.2985","my_balance_change":"-0.0001","block_height":1454520,"timestamp":1625238393,"fee_details":null,"coin":"tBCH","internal_id":"d0d0d27da94208523d7aa15e69c01e26aaf2e57d3babb59cb33c2a12312139c9","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002bc2f060f95f49adebada3a716b8b41f8ae910116a0a785025424083bd9f4a227020000006a47304402200801537c34845451ee32d55d92d067692d5a7e90b42e40b494b110aa600a7512022045812131c23b0372cc1f08e86fe09511aa84ded6c5bc7785f7af1dff1d01bf5a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffbc2f060f95f49adebada3a716b8b41f8ae910116a0a785025424083bd9f4a227030000006a473044022057c3ca0a94ffd218809c362244777aceff638dd673d492dc305e6bf4d0c8eeab02205f3863339664b1cbe6aa15d70b959b30646e581f9d7b009afc7b0cd2df839ea54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000124ebe8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca7c14701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac63ccde60","tx_hash":"ec8eed8490565c17685f8ad2a0399b67af0433c2b16d1ce53c5e1c3a0f416f60","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21482847","spent_by_me":"0.21482847","received_by_me":"0.21480847","my_balance_change":"-0.00002000","block_height":1454479,"timestamp":1625215221,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"ec8eed8490565c17685f8ad2a0399b67af0433c2b16d1ce53c5e1c3a0f416f60","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002bc2f060f95f49adebada3a716b8b41f8ae910116a0a785025424083bd9f4a227020000006a47304402200801537c34845451ee32d55d92d067692d5a7e90b42e40b494b110aa600a7512022045812131c23b0372cc1f08e86fe09511aa84ded6c5bc7785f7af1dff1d01bf5a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffbc2f060f95f49adebada3a716b8b41f8ae910116a0a785025424083bd9f4a227030000006a473044022057c3ca0a94ffd218809c362244777aceff638dd673d492dc305e6bf4d0c8eeab02205f3863339664b1cbe6aa15d70b959b30646e581f9d7b009afc7b0cd2df839ea54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000124ebe8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca7c14701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac63ccde60","tx_hash":"ec8eed8490565c17685f8ad2a0399b67af0433c2b16d1ce53c5e1c3a0f416f60","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.4988","spent_by_me":"7.4988","received_by_me":"7.4987","my_balance_change":"-0.0001","block_height":1454479,"timestamp":1625215221,"fee_details":null,"coin":"tBCH","internal_id":"74a914ba7cdd9164cbb36cc26d3af17bf4c54619e1d3118b542cef44352a61a2","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002624c73d2d461f17c8c65240185ebc5f8c5e7864cbebc7e9cb837c58712d51558020000006b483045022100c95e3beb4f2b4ba1880de7631cbd0570e5b5cc4b53cc6d72f505db4db119ca8f02205389a8eb3fef0a3986accdb86a9fabf83968ddc91f5aed76e18abb58a05ae92f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff624c73d2d461f17c8c65240185ebc5f8c5e7864cbebc7e9cb837c58712d51558030000006b483045022100e30e6e76b0e201d607ec23c12d52d0e68cb7c524b4ce30dbdebb3e25256feb4802207fa3ca01ed8a8e304ba4bdf9229ca3159210949dca22bf45d8b17c27e1f5014c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000011d1ae80300000000000017a914a532cabaee0701e1e1db193683e3cda43161612b87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac37aa4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac32cede60","tx_hash":"7fe4ff48b3a8c20379984576fdb37dadfc05c9418ddb01e770b04914801b6a84","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzjn9j46acrsrc0pmvvndqlrekjrzctp9vpmhqt026","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21476847","spent_by_me":"0.21476847","received_by_me":"0.21474847","my_balance_change":"-0.00002000","block_height":1454479,"timestamp":1625215221,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"7fe4ff48b3a8c20379984576fdb37dadfc05c9418ddb01e770b04914801b6a84","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002624c73d2d461f17c8c65240185ebc5f8c5e7864cbebc7e9cb837c58712d51558020000006b483045022100c95e3beb4f2b4ba1880de7631cbd0570e5b5cc4b53cc6d72f505db4db119ca8f02205389a8eb3fef0a3986accdb86a9fabf83968ddc91f5aed76e18abb58a05ae92f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff624c73d2d461f17c8c65240185ebc5f8c5e7864cbebc7e9cb837c58712d51558030000006b483045022100e30e6e76b0e201d607ec23c12d52d0e68cb7c524b4ce30dbdebb3e25256feb4802207fa3ca01ed8a8e304ba4bdf9229ca3159210949dca22bf45d8b17c27e1f5014c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000011d1ae80300000000000017a914a532cabaee0701e1e1db193683e3cda43161612b87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac37aa4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac32cede60","tx_hash":"7fe4ff48b3a8c20379984576fdb37dadfc05c9418ddb01e770b04914801b6a84","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzjn9j46acrsrc0pmvvndqlrekjrzctp9v60sm3cc8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.3986","spent_by_me":"7.3986","received_by_me":"7.2986","my_balance_change":"-0.1000","block_height":1454479,"timestamp":1625215221,"fee_details":null,"coin":"tBCH","internal_id":"0a0a509bc58762751e9c55d683d829c25a02d8a2e2882bc9477ebb52444300f6","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000276aea71963b604d2232ca7b6e2fc0ba9a584e21c199cfef3b1cf802fa8a55552020000006b483045022100c1689ac6f5bf90ed8d70612fff5fa275eecbbf1cbaa7768e3f56e610d1af2bca022069e5ffc3c74878b5c9addf0f8730514d14047c0cbdb74aebabcb6e6f2768854b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff76aea71963b604d2232ca7b6e2fc0ba9a584e21c199cfef3b1cf802fa8a55552030000006a47304402205f460fa1e3b82b364551446ad60aa218e77ab04af3c9db72eeb0dbbea2f5fe9a02206a5dcba75809dda38e370c4f8b1d0de3f7b44e34f3d115c59981e329dce9d9274121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000012102e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac07b24701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac30cede60","tx_hash":"5815d51287c537b89c7ebcbe4c86e7c5f8c5eb850124658c7cf161d4d2734c62","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21478847","spent_by_me":"0.21478847","received_by_me":"0.21476847","my_balance_change":"-0.00002000","block_height":1454479,"timestamp":1625215221,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"5815d51287c537b89c7ebcbe4c86e7c5f8c5eb850124658c7cf161d4d2734c62","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000276aea71963b604d2232ca7b6e2fc0ba9a584e21c199cfef3b1cf802fa8a55552020000006b483045022100c1689ac6f5bf90ed8d70612fff5fa275eecbbf1cbaa7768e3f56e610d1af2bca022069e5ffc3c74878b5c9addf0f8730514d14047c0cbdb74aebabcb6e6f2768854b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff76aea71963b604d2232ca7b6e2fc0ba9a584e21c199cfef3b1cf802fa8a55552030000006a47304402205f460fa1e3b82b364551446ad60aa218e77ab04af3c9db72eeb0dbbea2f5fe9a02206a5dcba75809dda38e370c4f8b1d0de3f7b44e34f3d115c59981e329dce9d9274121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000012102e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac07b24701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac30cede60","tx_hash":"5815d51287c537b89c7ebcbe4c86e7c5f8c5eb850124658c7cf161d4d2734c62","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.3987","spent_by_me":"7.3987","received_by_me":"7.3986","my_balance_change":"-0.0001","block_height":1454479,"timestamp":1625215221,"fee_details":null,"coin":"tBCH","internal_id":"cd72e89cb7d7ef8311f019acf44a0d6fc4e2c3f3682e103ded1be9b9e95e95b7","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002606f410f3a1c5e3ce51c6db1c23304af679b39a0d28a5f68175c569084ed8eec020000006b483045022100b52e86356200712da855e10b02ef62148bb05f26eb6ff8a851a6765b12d4f7f502206df1ba9aa53ee0e9d49e3c5f63f250045bc73d16d66b0387dcd36666e03162f74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff606f410f3a1c5e3ce51c6db1c23304af679b39a0d28a5f68175c569084ed8eec030000006a47304402207ce91e1cf6ace3165e66018123b17d7fcd11bfcf75c2e529916746123c899359022072b40bcebfd4f535de42f837c762b4723ecb3d98213258af4d35dcc6412fcb404121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000012103e80300000000000017a9149a1411ace5b6c6a4b683a88cfedb343d2e7f1c7487e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acd7b94701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac65ccde60","tx_hash":"5255a5a82f80cfb1f3fe9c191ce284a5a90bfce2b6a72c23d204b66319a7ae76","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzdpgydvukmvdf9ksw5gelkmxs7julcuwse7d2n3n2","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21480847","spent_by_me":"0.21480847","received_by_me":"0.21478847","my_balance_change":"-0.00002000","block_height":1454479,"timestamp":1625215221,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"5255a5a82f80cfb1f3fe9c191ce284a5a90bfce2b6a72c23d204b66319a7ae76","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002606f410f3a1c5e3ce51c6db1c23304af679b39a0d28a5f68175c569084ed8eec020000006b483045022100b52e86356200712da855e10b02ef62148bb05f26eb6ff8a851a6765b12d4f7f502206df1ba9aa53ee0e9d49e3c5f63f250045bc73d16d66b0387dcd36666e03162f74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff606f410f3a1c5e3ce51c6db1c23304af679b39a0d28a5f68175c569084ed8eec030000006a47304402207ce91e1cf6ace3165e66018123b17d7fcd11bfcf75c2e529916746123c899359022072b40bcebfd4f535de42f837c762b4723ecb3d98213258af4d35dcc6412fcb404121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000012103e80300000000000017a9149a1411ace5b6c6a4b683a88cfedb343d2e7f1c7487e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acd7b94701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac65ccde60","tx_hash":"5255a5a82f80cfb1f3fe9c191ce284a5a90bfce2b6a72c23d204b66319a7ae76","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzdpgydvukmvdf9ksw5gelkmxs7julcuwsz223fxph","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.4987","spent_by_me":"7.4987","received_by_me":"7.3987","my_balance_change":"-0.1000","block_height":1454479,"timestamp":1625215221,"fee_details":null,"coin":"tBCH","internal_id":"ad9ec7b0c12fed34a7f859abfc18cdcad5bb046e4a20c6d52fb7e43efaecf195","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002381f4c7570e528446c92c2b9d28cce1d457ddeb8d6ae5590995852592b2ba8ab020000006b483045022100fba4e3d9bfc56c6ece835cec5ee14178918065241389661fd89d7db1eea357a302207e892529c9e3324f5d11b0492484f871dab3457631aa55cc1d72ceb4078717064121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff381f4c7570e528446c92c2b9d28cce1d457ddeb8d6ae5590995852592b2ba8ab030000006a47304402203907a215422e6816403301d2829b11b5d984ee89c55a7a612d4321b418c61d7702202a59c70515751d320d27e0d4d4b058b12f54a6d992e739829cfbd4b29faa2e0d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000128d4e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac47d14701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb4c9de60","tx_hash":"875d961219c90391427eaf86ca6ad1d1f67d787cb004c39fe9fe284c6191c258","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21486847","spent_by_me":"0.21486847","received_by_me":"0.21484847","my_balance_change":"-0.00002000","block_height":1454477,"timestamp":1625213445,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"875d961219c90391427eaf86ca6ad1d1f67d787cb004c39fe9fe284c6191c258","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002381f4c7570e528446c92c2b9d28cce1d457ddeb8d6ae5590995852592b2ba8ab020000006b483045022100fba4e3d9bfc56c6ece835cec5ee14178918065241389661fd89d7db1eea357a302207e892529c9e3324f5d11b0492484f871dab3457631aa55cc1d72ceb4078717064121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff381f4c7570e528446c92c2b9d28cce1d457ddeb8d6ae5590995852592b2ba8ab030000006a47304402203907a215422e6816403301d2829b11b5d984ee89c55a7a612d4321b418c61d7702202a59c70515751d320d27e0d4d4b058b12f54a6d992e739829cfbd4b29faa2e0d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000128d4e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac47d14701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb4c9de60","tx_hash":"875d961219c90391427eaf86ca6ad1d1f67d787cb004c39fe9fe284c6191c258","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.5989","spent_by_me":"7.5989","received_by_me":"7.5988","my_balance_change":"-0.0001","block_height":1454477,"timestamp":1625213445,"fee_details":null,"coin":"tBCH","internal_id":"7913669d09ec17078156a5765f4a356d3186a34eedff2f8a51fabfc4c28e955c","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000258c291614c28fee99fc304b07c787df6d1d16aca86af7e429103c91912965d87020000006a47304402204d2bbeb9a5defa77ef04a6ec115c925e76dee55203df6c4714c5b2122ed81a46022052f696df673dff637ee2db36b1f760c5d58a9dbf74a8ec5e8d976b430ca49d334121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff58c291614c28fee99fc304b07c787df6d1d16aca86af7e429103c91912965d87030000006b483045022100a6d6b9ec4023662628401bf524ac2713d8579026a275cf346c10843eadd7f141022074721fff37a3665a80c0f7324f0499f686cf73ad827c6ff9acea3bf99080448a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000124ece80300000000000017a914a85daa483c80249a83beaa8defdff18c4e8b5b1287e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac77c94701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc6c9de60","tx_hash":"27a2f4d93b0824540285a7a0160191aef8418b6b713adabade9af4950f062fbc","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pz59m2jg8jqzfx5rh64gmm7l7xxyaz6mzgqzr8dnzs","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21484847","spent_by_me":"0.21484847","received_by_me":"0.21482847","my_balance_change":"-0.00002000","block_height":1454477,"timestamp":1625213445,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"27a2f4d93b0824540285a7a0160191aef8418b6b713adabade9af4950f062fbc","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000258c291614c28fee99fc304b07c787df6d1d16aca86af7e429103c91912965d87020000006a47304402204d2bbeb9a5defa77ef04a6ec115c925e76dee55203df6c4714c5b2122ed81a46022052f696df673dff637ee2db36b1f760c5d58a9dbf74a8ec5e8d976b430ca49d334121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff58c291614c28fee99fc304b07c787df6d1d16aca86af7e429103c91912965d87030000006b483045022100a6d6b9ec4023662628401bf524ac2713d8579026a275cf346c10843eadd7f141022074721fff37a3665a80c0f7324f0499f686cf73ad827c6ff9acea3bf99080448a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000124ece80300000000000017a914a85daa483c80249a83beaa8defdff18c4e8b5b1287e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac77c94701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc6c9de60","tx_hash":"27a2f4d93b0824540285a7a0160191aef8418b6b713adabade9af4950f062fbc","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pz59m2jg8jqzfx5rh64gmm7l7xxyaz6mzgmkyuhysd","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.5988","spent_by_me":"7.5988","received_by_me":"7.4988","my_balance_change":"-0.1000","block_height":1454477,"timestamp":1625213445,"fee_details":null,"coin":"tBCH","internal_id":"df5314098d5124ac85f35adec6243b1c6222b08d1b4592dcb6a3be5dd5eb92df","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002b14ff412501d21f232f57f6f712feb4499a58cf614b88834e6a730d36661dd17020000006a473044022078af5d9b47d6f1c17fce8dacb078e8a83fcc1451ad5fcfb0bd6a2208541bcc610220471178547c4c0136bcd0d43bc922b82fe97b9bcaf00f9e790a7396eb7a857b6b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb14ff412501d21f232f57f6f712feb4499a58cf614b88834e6a730d36661dd17030000006b483045022100d4152ffdb6a5cd0cc821a0210ada7f505d05439a2bc7210a6e6dbeb520b5bb5602202a1f0d1587b92bd518e9272c4d89002245f2c431c3b624fc14acd7358c686ae14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000128d5e80300000000000017a914b0deb23cc680b5916cff0e43a6c3cb40d470a95287e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac17d94701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdebfde60","tx_hash":"aba82b2b595258999055aed6b8de7d451dce8cd2b9c2926c4428e570754c1f38","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzcdav3uc6qttytvlu8y8fkredqdgu9f2gq6epdgy3","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21488847","spent_by_me":"0.21488847","received_by_me":"0.21486847","my_balance_change":"-0.00002000","block_height":1454471,"timestamp":1625210966,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"aba82b2b595258999055aed6b8de7d451dce8cd2b9c2926c4428e570754c1f38","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002b14ff412501d21f232f57f6f712feb4499a58cf614b88834e6a730d36661dd17020000006a473044022078af5d9b47d6f1c17fce8dacb078e8a83fcc1451ad5fcfb0bd6a2208541bcc610220471178547c4c0136bcd0d43bc922b82fe97b9bcaf00f9e790a7396eb7a857b6b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb14ff412501d21f232f57f6f712feb4499a58cf614b88834e6a730d36661dd17030000006b483045022100d4152ffdb6a5cd0cc821a0210ada7f505d05439a2bc7210a6e6dbeb520b5bb5602202a1f0d1587b92bd518e9272c4d89002245f2c431c3b624fc14acd7358c686ae14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000128d5e80300000000000017a914b0deb23cc680b5916cff0e43a6c3cb40d470a95287e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac17d94701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdebfde60","tx_hash":"aba82b2b595258999055aed6b8de7d451dce8cd2b9c2926c4428e570754c1f38","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzcdav3uc6qttytvlu8y8fkredqdgu9f2gmw76hlkv","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.6989","spent_by_me":"7.6989","received_by_me":"7.5989","my_balance_change":"-0.1000","block_height":1454471,"timestamp":1625210966,"fee_details":null,"coin":"tBCH","internal_id":"3941fda591fbabaa4b2b9bb5bd5d774e3e8d417e7b19a092ab2dfd2124d70277","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002b264484d1a7bbd70b3b241631097807996523c717fc780e9db2cc9567c84504c020000006a47304402205125b6b70030809178251997c95efdfcfbafe28eac68b6f571348c24d705fcfd02206ca33eeca12657574bbddde106e9b3073cb7fd44ad9818bc14dc451fd2b65d704121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb264484d1a7bbd70b3b241631097807996523c717fc780e9db2cc9567c84504c030000006b483045022100f8eddf3d5e9b5cff19735e5f61e7198228bd35a56ace386a2b62c559263967e602205e89690bbe2c93bc92baafe31a60d2435e434a34c5aed89e04be7af2f80b687e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000012cbde8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace7e04701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdabfde60","tx_hash":"17dd6166d330a7e63488b814f68ca59944eb2f716f7ff532f2211d5012f44fb1","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21490847","spent_by_me":"0.21490847","received_by_me":"0.21488847","my_balance_change":"-0.00002000","block_height":1454471,"timestamp":1625210966,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"17dd6166d330a7e63488b814f68ca59944eb2f716f7ff532f2211d5012f44fb1","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002b264484d1a7bbd70b3b241631097807996523c717fc780e9db2cc9567c84504c020000006a47304402205125b6b70030809178251997c95efdfcfbafe28eac68b6f571348c24d705fcfd02206ca33eeca12657574bbddde106e9b3073cb7fd44ad9818bc14dc451fd2b65d704121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb264484d1a7bbd70b3b241631097807996523c717fc780e9db2cc9567c84504c030000006b483045022100f8eddf3d5e9b5cff19735e5f61e7198228bd35a56ace386a2b62c559263967e602205e89690bbe2c93bc92baafe31a60d2435e434a34c5aed89e04be7af2f80b687e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000012cbde8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace7e04701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdabfde60","tx_hash":"17dd6166d330a7e63488b814f68ca59944eb2f716f7ff532f2211d5012f44fb1","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.699","spent_by_me":"7.699","received_by_me":"7.6989","my_balance_change":"-0.0001","block_height":1454471,"timestamp":1625210966,"fee_details":null,"coin":"tBCH","internal_id":"e3e26682d20fbfb643e1a74e9d0f87d70b06f1900b9df3033dab6b76bbce34d3","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002745e24ccb72de11bb5b1a1232f0847e777bd1d9cd075356122864e029c6a7e4c020000006a47304402204303e231d3b6b8bce817c6db01e5800aec588d0e50d9d3786a66c69daf1190f502205805d5ace2e7a143b94899529dbe02b004152026891c1c616bb64d982e8ae4984121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff745e24ccb72de11bb5b1a1232f0847e777bd1d9cd075356122864e029c6a7e4c030000006b483045022100f386e5b02696878c7a1f34b3b5f6ecf83aabb4ebf0c9831bcb5c2ef52fbae918022018c0c90f3fb35aaa666c355d40f295d5542442398d31f3bc540b334349ceeb284121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000003dee8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9fec4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac93a9dd60","tx_hash":"b3427413c0f84d9eca4bfba66133ff781e7569f69f451f20786be7d2df5e39b6","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21493847","spent_by_me":"0.21493847","received_by_me":"0.21491847","my_balance_change":"-0.00002000","block_height":1454353,"timestamp":1625139879,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"b3427413c0f84d9eca4bfba66133ff781e7569f69f451f20786be7d2df5e39b6","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002745e24ccb72de11bb5b1a1232f0847e777bd1d9cd075356122864e029c6a7e4c020000006a47304402204303e231d3b6b8bce817c6db01e5800aec588d0e50d9d3786a66c69daf1190f502205805d5ace2e7a143b94899529dbe02b004152026891c1c616bb64d982e8ae4984121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff745e24ccb72de11bb5b1a1232f0847e777bd1d9cd075356122864e029c6a7e4c030000006b483045022100f386e5b02696878c7a1f34b3b5f6ecf83aabb4ebf0c9831bcb5c2ef52fbae918022018c0c90f3fb35aaa666c355d40f295d5542442398d31f3bc540b334349ceeb284121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000003dee8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9fec4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac93a9dd60","tx_hash":"b3427413c0f84d9eca4bfba66133ff781e7569f69f451f20786be7d2df5e39b6","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.0991","spent_by_me":"0.0991","received_by_me":"0.099","my_balance_change":"-0.0001","block_height":1454353,"timestamp":1625139879,"fee_details":null,"coin":"tBCH","internal_id":"4f213288582062897888cfe263628e2aa1ab9d90562da1a8f6951dd6f57e942a","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000003b6395edfd2e76b78201f459ff669751e78ff3361a6fb4bca9e4df8c0137442b3020000006a473044022004c90086c6990d9764b5ed20c7e0edf9ba10328ff4536bd088bc24b851f9929402202f7c9d525e79c85bd2734d3de06432c1464f392c7fc5482c50b8ae2e9ff9f4734121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffafece59188497a02c4aca4a05c8da14b545fd9ceed94d4f44e74a3c10759a535020000006a47304402203a14293b5b7cae47f2bfcb47c4323d029669fd57505cd7085877deb1f839829702203957e8624a0d1abc8dfd44d6443e61146469031f474eff7de342bb42626767814121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb6395edfd2e76b78201f459ff669751e78ff3361a6fb4bca9e4df8c0137442b3030000006b4830450221008f0a6397407f57ea7a78d413e80c5249739a3694bbccb85b61cf8b29f1a0fc940220362b2fcbded953c8dc38dcb320b2f69ade2b0a17be7f89a6da1290465b6fa7364121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000012cbee80300000000000017a9149e8513db6a052a73d09f3db084d76d666b7d3b0c87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb7e84701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac97a9dd60","tx_hash":"4c50847c56c92cdbe980c77f713c5296798097106341b2b370bd7b1a4d4864b2","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pz0g2y7mdgzj5u7snu7mppxhd4nxklfmpseptch3ch","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21492847","spent_by_me":"0.21492847","received_by_me":"0.21490847","my_balance_change":"-0.00002000","block_height":1454353,"timestamp":1625139879,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"4c50847c56c92cdbe980c77f713c5296798097106341b2b370bd7b1a4d4864b2","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000003b6395edfd2e76b78201f459ff669751e78ff3361a6fb4bca9e4df8c0137442b3020000006a473044022004c90086c6990d9764b5ed20c7e0edf9ba10328ff4536bd088bc24b851f9929402202f7c9d525e79c85bd2734d3de06432c1464f392c7fc5482c50b8ae2e9ff9f4734121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffafece59188497a02c4aca4a05c8da14b545fd9ceed94d4f44e74a3c10759a535020000006a47304402203a14293b5b7cae47f2bfcb47c4323d029669fd57505cd7085877deb1f839829702203957e8624a0d1abc8dfd44d6443e61146469031f474eff7de342bb42626767814121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb6395edfd2e76b78201f459ff669751e78ff3361a6fb4bca9e4df8c0137442b3030000006b4830450221008f0a6397407f57ea7a78d413e80c5249739a3694bbccb85b61cf8b29f1a0fc940220362b2fcbded953c8dc38dcb320b2f69ade2b0a17be7f89a6da1290465b6fa7364121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000012cbee80300000000000017a9149e8513db6a052a73d09f3db084d76d666b7d3b0c87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb7e84701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac97a9dd60","tx_hash":"4c50847c56c92cdbe980c77f713c5296798097106341b2b370bd7b1a4d4864b2","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pz0g2y7mdgzj5u7snu7mppxhd4nxklfmpsz4vrdx22","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"7.799","spent_by_me":"7.799","received_by_me":"7.699","my_balance_change":"-0.100","block_height":1454353,"timestamp":1625139879,"fee_details":null,"coin":"tBCH","internal_id":"f753153240944a31b59dc5918baaa8135ca06f82209e98b1ef258b8b084500d5","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002b6fa38deca7b73a3f32973b02228316240889b837e4ac086d4038f714b983248020000006a4730440220370c51125798e3ec8fb69575dc28d348f328f8ecf472360219bb72738ba511ec0220590df2673525d0bf62b25a2060626cd1802189895e3d649b7f192b8c81155b054121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb6fa38deca7b73a3f32973b02228316240889b837e4ac086d4038f714b983248030000006b483045022100c22856aeffa0c2adee96e7e5a17b743bd70f6830985d846f046e3d0da0e44f7802201a59294c8e5dc51e5049744d773e08c8626e9446ba48095f03b36b5353b576ae4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000003dfe80300000000000017a91412009a5b3c6cea48a56aa118c7ce956de1c0144c87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6ff44701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdfa5dd60","tx_hash":"4c7e6a9c024e8622613575d09c1dbd77e747082f23a1b1b51be12db7cc245e74","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pqfqpxjm83kw5j99d2s3337wj4k7rsq5fsngexvak8","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21495847","spent_by_me":"0.21495847","received_by_me":"0.21493847","my_balance_change":"-0.00002000","block_height":1454352,"timestamp":1625138664,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"4c7e6a9c024e8622613575d09c1dbd77e747082f23a1b1b51be12db7cc245e74","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002b6fa38deca7b73a3f32973b02228316240889b837e4ac086d4038f714b983248020000006a4730440220370c51125798e3ec8fb69575dc28d348f328f8ecf472360219bb72738ba511ec0220590df2673525d0bf62b25a2060626cd1802189895e3d649b7f192b8c81155b054121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb6fa38deca7b73a3f32973b02228316240889b837e4ac086d4038f714b983248030000006b483045022100c22856aeffa0c2adee96e7e5a17b743bd70f6830985d846f046e3d0da0e44f7802201a59294c8e5dc51e5049744d773e08c8626e9446ba48095f03b36b5353b576ae4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000003dfe80300000000000017a91412009a5b3c6cea48a56aa118c7ce956de1c0144c87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6ff44701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdfa5dd60","tx_hash":"4c7e6a9c024e8622613575d09c1dbd77e747082f23a1b1b51be12db7cc245e74","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pqfqpxjm83kw5j99d2s3337wj4k7rsq5fsgu7ak2y6","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1991","spent_by_me":"0.1991","received_by_me":"0.0991","my_balance_change":"-0.1000","block_height":1454352,"timestamp":1625138664,"fee_details":null,"coin":"tBCH","internal_id":"54e9e51fab97edde4696049c04ee322711920e792047fda1dbe9590884ab312c","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002606e9b9101d8cf579a7c10992ab425111295388d81b3bce466d6a028ead11b21020000006a473044022100f6bc368f8a049aaa177f43c5cc2b3426f59e0ae05778f225ba312862e4efc0a6021f4435be0843850fbfaabe415ed11db44551019d72e25528d2a75d6a80962fd64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff606e9b9101d8cf579a7c10992ab425111295388d81b3bce466d6a028ead11b21030000006b483045022100f450a3eed8521ad8e06c9b7403739653f6ff6d4ea259ffd0b936bb8d0329a527022033006270d50b46f542e0d1a77b9050fc37d0ddbe89180a0aa64fd51645f397244121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000007c7e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3ffc4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdba5dd60","tx_hash":"4832984b718f03d486c04a7e839b884062312822b07329f3a3737bcade38fab6","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21497847","spent_by_me":"0.21497847","received_by_me":"0.21495847","my_balance_change":"-0.00002000","block_height":1454352,"timestamp":1625138664,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"4832984b718f03d486c04a7e839b884062312822b07329f3a3737bcade38fab6","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002606e9b9101d8cf579a7c10992ab425111295388d81b3bce466d6a028ead11b21020000006a473044022100f6bc368f8a049aaa177f43c5cc2b3426f59e0ae05778f225ba312862e4efc0a6021f4435be0843850fbfaabe415ed11db44551019d72e25528d2a75d6a80962fd64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff606e9b9101d8cf579a7c10992ab425111295388d81b3bce466d6a028ead11b21030000006b483045022100f450a3eed8521ad8e06c9b7403739653f6ff6d4ea259ffd0b936bb8d0329a527022033006270d50b46f542e0d1a77b9050fc37d0ddbe89180a0aa64fd51645f397244121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000007c7e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3ffc4701000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdba5dd60","tx_hash":"4832984b718f03d486c04a7e839b884062312822b07329f3a3737bcade38fab6","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1992","spent_by_me":"0.1992","received_by_me":"0.1991","my_balance_change":"-0.0001","block_height":1454352,"timestamp":1625138664,"fee_details":null,"coin":"tBCH","internal_id":"7fe7d452f05a91f3fa0cce061cea66d9dde340ef6827ea138b46c23c30b1817f","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002484f0fe857e30fa9650ea31f9a985e7f5fd446d782ab133e999b2e755f8d78aa020000006a473044022028197c9ccc954c6a8dc9943d8c0567d1cb03012fb424e1bf926184fa2348818802206737f2eaccbaeec3db04593d151e08e98e4072a38c17225e5e8e57f8df207eb44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff484f0fe857e30fa9650ea31f9a985e7f5fd446d782ab133e999b2e755f8d78aa030000006b483045022100fdd6886519021be6ecd9223adcd281ecfa30c0d63e4db3d04868f4321f47a32f02202069e3916205c6b286b7baf307fc697118936d51d6b58b4353279474d4c7849f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000000bb0e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdf0b4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4f53dc60","tx_hash":"6b386c5d39cf82738fdf29ef330b0583e3ce9503a98ca2474fde2c539a346e28","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21501847","spent_by_me":"0.21501847","received_by_me":"0.21499847","my_balance_change":"-0.00002000","block_height":1454208,"timestamp":1625052599,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"6b386c5d39cf82738fdf29ef330b0583e3ce9503a98ca2474fde2c539a346e28","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002484f0fe857e30fa9650ea31f9a985e7f5fd446d782ab133e999b2e755f8d78aa020000006a473044022028197c9ccc954c6a8dc9943d8c0567d1cb03012fb424e1bf926184fa2348818802206737f2eaccbaeec3db04593d151e08e98e4072a38c17225e5e8e57f8df207eb44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff484f0fe857e30fa9650ea31f9a985e7f5fd446d782ab133e999b2e755f8d78aa030000006b483045022100fdd6886519021be6ecd9223adcd281ecfa30c0d63e4db3d04868f4321f47a32f02202069e3916205c6b286b7baf307fc697118936d51d6b58b4353279474d4c7849f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000000bb0e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdf0b4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4f53dc60","tx_hash":"6b386c5d39cf82738fdf29ef330b0583e3ce9503a98ca2474fde2c539a346e28","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.2993","spent_by_me":"0.2993","received_by_me":"0.2992","my_balance_change":"-0.0001","block_height":1454208,"timestamp":1625052599,"fee_details":null,"coin":"tBCH","internal_id":"542af7b65cb4a844fa59e8acf48bbae2df00dfb298d25a6a53addc7121f336c1","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002286e349a532cde4f47a28ca90395cee383050b33ef29df8f7382cf395d6c386b020000006a47304402204471099da0be58324ccfd44f6d2174a7ae2e52d83d4361ec36224ebba1e90f6b022045e601f05e215c09bf9f5bef4cbec8405a1f811b7be65938bca49307d4a012d04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff286e349a532cde4f47a28ca90395cee383050b33ef29df8f7382cf395d6c386b030000006a4730440220711074ad3cd3e52be20dc880a2f0209f5f026af3d7e8634d831815f75177180a02201cc04718debe4d3ec4d57d5ef653e985bacd0394a6788f3ce0a11f90bd3c1a164121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000007c8e80300000000000017a9148898ff6101d77e1cb7a6f83bf7842b79a766c77287e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0f044801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5353dc60","tx_hash":"211bd1ea28a0d666e4bcb3818d3895121125b42a99107c9a57cfd801919b6e60","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzyf3lmpq8thu89h5murhauy9du6wek8wgjmxqakvv","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21499847","spent_by_me":"0.21499847","received_by_me":"0.21497847","my_balance_change":"-0.00002000","block_height":1454208,"timestamp":1625052599,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"211bd1ea28a0d666e4bcb3818d3895121125b42a99107c9a57cfd801919b6e60","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002286e349a532cde4f47a28ca90395cee383050b33ef29df8f7382cf395d6c386b020000006a47304402204471099da0be58324ccfd44f6d2174a7ae2e52d83d4361ec36224ebba1e90f6b022045e601f05e215c09bf9f5bef4cbec8405a1f811b7be65938bca49307d4a012d04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff286e349a532cde4f47a28ca90395cee383050b33ef29df8f7382cf395d6c386b030000006a4730440220711074ad3cd3e52be20dc880a2f0209f5f026af3d7e8634d831815f75177180a02201cc04718debe4d3ec4d57d5ef653e985bacd0394a6788f3ce0a11f90bd3c1a164121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000007c8e80300000000000017a9148898ff6101d77e1cb7a6f83bf7842b79a766c77287e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0f044801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5353dc60","tx_hash":"211bd1ea28a0d666e4bcb3818d3895121125b42a99107c9a57cfd801919b6e60","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzyf3lmpq8thu89h5murhauy9du6wek8wgf0pm8p73","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.2992","spent_by_me":"0.2992","received_by_me":"0.1992","my_balance_change":"-0.1000","block_height":1454208,"timestamp":1625052599,"fee_details":null,"coin":"tBCH","internal_id":"a9522765d09e3d1fb53377f8bb6bb032bef17d8ce3e0183a075502134bcea9de","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002fd24acecb2c35ae5420799d8da8b45cfa892479bd9fd2ec1153f1696390a7908020000006b483045022100b1b53abea221bf3dffb9ffc29a83a9808fbd52d42148b52b5c2d60b63879d5e60220557a4b60f5d13144ad53c5be8c025080167725800bf41ccbe63075526714ee8f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cfffffffffd24acecb2c35ae5420799d8da8b45cfa892479bd9fd2ec1153f1696390a7908030000006a47304402206c3ae471148a8963a9d260ed6cc341658e774e65d6e55bd3bd6837d9854cbce60220241b4222bf42096ba190f0243982058ec03e3beab52e9d4801cf4e5bd153107a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000000bb1e80300000000000017a914ba3828c392666b5f7a0d1740c09903bbcf03794387e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acaf134801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdf09db60","tx_hash":"aa788d5f752e9b993e13ab82d746d45f7f5e989a1fa30e65a90fe357e80f4f48","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzars2xrjfnxkhm6p5t5psyeqwau7qmegvmp67a2g2","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21503847","spent_by_me":"0.21503847","received_by_me":"0.21501847","my_balance_change":"-0.00002000","block_height":1454069,"timestamp":1624968647,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"aa788d5f752e9b993e13ab82d746d45f7f5e989a1fa30e65a90fe357e80f4f48","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002fd24acecb2c35ae5420799d8da8b45cfa892479bd9fd2ec1153f1696390a7908020000006b483045022100b1b53abea221bf3dffb9ffc29a83a9808fbd52d42148b52b5c2d60b63879d5e60220557a4b60f5d13144ad53c5be8c025080167725800bf41ccbe63075526714ee8f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cfffffffffd24acecb2c35ae5420799d8da8b45cfa892479bd9fd2ec1153f1696390a7908030000006a47304402206c3ae471148a8963a9d260ed6cc341658e774e65d6e55bd3bd6837d9854cbce60220241b4222bf42096ba190f0243982058ec03e3beab52e9d4801cf4e5bd153107a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000000bb1e80300000000000017a914ba3828c392666b5f7a0d1740c09903bbcf03794387e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acaf134801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdf09db60","tx_hash":"aa788d5f752e9b993e13ab82d746d45f7f5e989a1fa30e65a90fe357e80f4f48","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzars2xrjfnxkhm6p5t5psyeqwau7qmegvq4a98a6h","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.3993","spent_by_me":"0.3993","received_by_me":"0.2993","my_balance_change":"-0.1000","block_height":1454069,"timestamp":1624968647,"fee_details":null,"coin":"tBCH","internal_id":"d0c497e3523a4616fdb3fe23e07381edd1de5d0a368daf42f222b11553d65e87","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002719f36a33aec61ba5833511b9348436d4823996e03a00842595f7840df2b3648020000006a473044022071d1f37e21109d5c75cc3fa07a84a53259528d3acabbe885e6b9210637f14c3802205b87daecbcb9a45c690e8ea84b9fa7a30879d7d3b86747732e90e4d734f287fd4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff719f36a33aec61ba5833511b9348436d4823996e03a00842595f7840df2b3648030000006b4830450221008581c9b36c3f7041051fbf22eafa2032a9dca65d6e9500d3636df1b4777a415502201a22260b15675f9e0983d4b246c37579dc482878237a49e2c897f4802c24f02f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000000f99e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7f1b4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdc09db60","tx_hash":"08790a3996163f15c12efdd99b4792a8cf458bdad8990742e55ac3b2ecac24fd","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21505847","spent_by_me":"0.21505847","received_by_me":"0.21503847","my_balance_change":"-0.00002000","block_height":1454069,"timestamp":1624968647,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"08790a3996163f15c12efdd99b4792a8cf458bdad8990742e55ac3b2ecac24fd","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002719f36a33aec61ba5833511b9348436d4823996e03a00842595f7840df2b3648020000006a473044022071d1f37e21109d5c75cc3fa07a84a53259528d3acabbe885e6b9210637f14c3802205b87daecbcb9a45c690e8ea84b9fa7a30879d7d3b86747732e90e4d734f287fd4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff719f36a33aec61ba5833511b9348436d4823996e03a00842595f7840df2b3648030000006b4830450221008581c9b36c3f7041051fbf22eafa2032a9dca65d6e9500d3636df1b4777a415502201a22260b15675f9e0983d4b246c37579dc482878237a49e2c897f4802c24f02f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000000f99e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7f1b4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdc09db60","tx_hash":"08790a3996163f15c12efdd99b4792a8cf458bdad8990742e55ac3b2ecac24fd","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.3994","spent_by_me":"0.3994","received_by_me":"0.3993","my_balance_change":"-0.0001","block_height":1454069,"timestamp":1624968647,"fee_details":null,"coin":"tBCH","internal_id":"a940cbe5c7e40fcd2a933029ae6b6106e5ebb379e3c5092cfbf09051d023a191","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002ca93a81b3cc4d41793169a7698479a41c0598f53b27159db11eba1ec4c6b7564020000006b483045022100ef7f948d86a99993fdb352718244c0f3286225063ac4287a8263b8138800b7cd02200d8667ec71cab66ef0e2bc39ab39bc6f5beea33020b2ed496f8997acc31a472e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffca93a81b3cc4d41793169a7698479a41c0598f53b27159db11eba1ec4c6b7564030000006b483045022100fd1a44ff9c079add9233e806215ce64108faafbd057a7317f9d6604625d5eb85022006b5c1650fe8c245a2303d516a90bb843de811c4f30819c8efbbc54527acabaa4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000001382e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1f2b4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2007db60","tx_hash":"d5b0c5505d128e1763070a1eccf0f6f8112c5a19d3d099d930c3fbcfde947310","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21509847","spent_by_me":"0.21509847","received_by_me":"0.21507847","my_balance_change":"-0.00002000","block_height":1454067,"timestamp":1624967412,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"d5b0c5505d128e1763070a1eccf0f6f8112c5a19d3d099d930c3fbcfde947310","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002ca93a81b3cc4d41793169a7698479a41c0598f53b27159db11eba1ec4c6b7564020000006b483045022100ef7f948d86a99993fdb352718244c0f3286225063ac4287a8263b8138800b7cd02200d8667ec71cab66ef0e2bc39ab39bc6f5beea33020b2ed496f8997acc31a472e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffca93a81b3cc4d41793169a7698479a41c0598f53b27159db11eba1ec4c6b7564030000006b483045022100fd1a44ff9c079add9233e806215ce64108faafbd057a7317f9d6604625d5eb85022006b5c1650fe8c245a2303d516a90bb843de811c4f30819c8efbbc54527acabaa4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000001382e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1f2b4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2007db60","tx_hash":"d5b0c5505d128e1763070a1eccf0f6f8112c5a19d3d099d930c3fbcfde947310","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.4995","spent_by_me":"0.4995","received_by_me":"0.4994","my_balance_change":"-0.0001","block_height":1454067,"timestamp":1624967412,"fee_details":null,"coin":"tBCH","internal_id":"b7da4342782f017c6d2b2137685669002d8110be6c0871dc50b0faeec1aa060f","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002107394decffbc330d999d0d3195a2c11f8f6f0cc1e0a0763178e125d50c5b0d5020000006a473044022064d75b688e8b0abed68fefb7b4bca0b48fe5e1d6509a7845f0916d3588f5ea980220663f2d457a96fabbfd7ba96d04eb5174227c2431db8e45e3eac7c3c832ed61164121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff107394decffbc330d999d0d3195a2c11f8f6f0cc1e0a0763178e125d50c5b0d5030000006a47304402204dc0aff8fea6834b79d9e0672faa00eed90c5dc6f8668fa24ae514e64630010902203aab6b0b68082201c6a783e5e01f13a0ccb1e6ff5a2be26418dbf2a55bbea90d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000000f9ae80300000000000017a91465d345bf85c86b496f4a0adb5158087bfdd41afd87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4f234801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2207db60","tx_hash":"48362bdf40785f594208a0036e9923486d4348931b513358ba61ec3aa3369f71","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:ppjax3dlshyxkjt0fg9dk52cppalm4q6l5v6k2s8qh","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21507847","spent_by_me":"0.21507847","received_by_me":"0.21505847","my_balance_change":"-0.00002000","block_height":1454067,"timestamp":1624967412,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"48362bdf40785f594208a0036e9923486d4348931b513358ba61ec3aa3369f71","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002107394decffbc330d999d0d3195a2c11f8f6f0cc1e0a0763178e125d50c5b0d5020000006a473044022064d75b688e8b0abed68fefb7b4bca0b48fe5e1d6509a7845f0916d3588f5ea980220663f2d457a96fabbfd7ba96d04eb5174227c2431db8e45e3eac7c3c832ed61164121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff107394decffbc330d999d0d3195a2c11f8f6f0cc1e0a0763178e125d50c5b0d5030000006a47304402204dc0aff8fea6834b79d9e0672faa00eed90c5dc6f8668fa24ae514e64630010902203aab6b0b68082201c6a783e5e01f13a0ccb1e6ff5a2be26418dbf2a55bbea90d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000000f9ae80300000000000017a91465d345bf85c86b496f4a0adb5158087bfdd41afd87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4f234801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2207db60","tx_hash":"48362bdf40785f594208a0036e9923486d4348931b513358ba61ec3aa3369f71","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:ppjax3dlshyxkjt0fg9dk52cppalm4q6l5hw332sj2","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.4994","spent_by_me":"0.4994","received_by_me":"0.3994","my_balance_change":"-0.1000","block_height":1454067,"timestamp":1624967412,"fee_details":null,"coin":"tBCH","internal_id":"7252512b3bc9266a2b975ef7824ebec128b38094596ae0e9b8c88cb233546d22","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002c768520d2e03203f3a5a4f55e92d62fcfb70f84943333c0d8f8ccbfadcba4943020000006b483045022100d9cfb5b8ab11bbe54f9565b0c60ddebbe0e89b7f80a96b27eb2614790d551ab8022064badd7ae055c1584fd2b9df63ed1b647eb51c52d68317a4e338b614f9175cf94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffc768520d2e03203f3a5a4f55e92d62fcfb70f84943333c0d8f8ccbfadcba4943030000006a473044022028e8ed10835b13d1982eab1af2da309fef19ee7837960ac3b73b8e4e7af51b9b02204b0d4c7c959dedccaf52b958cf665dda02c378a45912892ca5b0a96d3c47c6634121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000000176be8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acbf3a4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acf5c9d960","tx_hash":"80f4eec1d8b306695210ee47bb533e0bf7c8f7d4bfa9d16dae5045beea0806af","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21513847","spent_by_me":"0.21513847","received_by_me":"0.21511847","my_balance_change":"-0.00002000","block_height":1453937,"timestamp":1624886141,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"80f4eec1d8b306695210ee47bb533e0bf7c8f7d4bfa9d16dae5045beea0806af","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002c768520d2e03203f3a5a4f55e92d62fcfb70f84943333c0d8f8ccbfadcba4943020000006b483045022100d9cfb5b8ab11bbe54f9565b0c60ddebbe0e89b7f80a96b27eb2614790d551ab8022064badd7ae055c1584fd2b9df63ed1b647eb51c52d68317a4e338b614f9175cf94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffc768520d2e03203f3a5a4f55e92d62fcfb70f84943333c0d8f8ccbfadcba4943030000006a473044022028e8ed10835b13d1982eab1af2da309fef19ee7837960ac3b73b8e4e7af51b9b02204b0d4c7c959dedccaf52b958cf665dda02c378a45912892ca5b0a96d3c47c6634121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb708000000000000000108000000000000176be8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acbf3a4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acf5c9d960","tx_hash":"80f4eec1d8b306695210ee47bb533e0bf7c8f7d4bfa9d16dae5045beea0806af","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.5996","spent_by_me":"0.5996","received_by_me":"0.5995","my_balance_change":"-0.0001","block_height":1453937,"timestamp":1624886141,"fee_details":null,"coin":"tBCH","internal_id":"fc2805af1ab0a96830e54451dcc701bc4f9fc42988fe3508710f814b1719dfb6","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002af0608eabe4550ae6dd1a9bfd4f7c8f70b3e53bb47ee10526906b3d8c1eef480020000006a47304402205926028c225d9cfe0b848338ab9d7aa9b307dc894a14a32017927c6df78333a202205b93595f0d8f60425f81015ee93a131bf8b7cb7838f77dc174aedb4de593c0a44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffaf0608eabe4550ae6dd1a9bfd4f7c8f70b3e53bb47ee10526906b3d8c1eef480030000006a47304402207556d56d124a9f3d14eded152e944e33335a36c2f517899fdee50d40ec2e0a92022048a1d9b3ef72dd89595a900dbe7a2416151593711e22af2a7dc6d2bffa10053d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000001383e80300000000000017a914e14c86f86b702a81293c9b4f044fefaec74b717887e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acef324801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acf8c9d960","tx_hash":"64756b4ceca1eb11db5971b2538f59c0419a4798769a169317d4c43c1ba893ca","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:prs5ephcddcz4qff8jd57pz0a7hvwjm30qfrrgsyld","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21511847","spent_by_me":"0.21511847","received_by_me":"0.21509847","my_balance_change":"-0.00002000","block_height":1453937,"timestamp":1624886141,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"64756b4ceca1eb11db5971b2538f59c0419a4798769a169317d4c43c1ba893ca","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002af0608eabe4550ae6dd1a9bfd4f7c8f70b3e53bb47ee10526906b3d8c1eef480020000006a47304402205926028c225d9cfe0b848338ab9d7aa9b307dc894a14a32017927c6df78333a202205b93595f0d8f60425f81015ee93a131bf8b7cb7838f77dc174aedb4de593c0a44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffaf0608eabe4550ae6dd1a9bfd4f7c8f70b3e53bb47ee10526906b3d8c1eef480030000006a47304402207556d56d124a9f3d14eded152e944e33335a36c2f517899fdee50d40ec2e0a92022048a1d9b3ef72dd89595a900dbe7a2416151593711e22af2a7dc6d2bffa10053d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000001383e80300000000000017a914e14c86f86b702a81293c9b4f044fefaec74b717887e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acef324801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acf8c9d960","tx_hash":"64756b4ceca1eb11db5971b2538f59c0419a4798769a169317d4c43c1ba893ca","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:prs5ephcddcz4qff8jd57pz0a7hvwjm30qjhyn2nds","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.5995","spent_by_me":"0.5995","received_by_me":"0.4995","my_balance_change":"-0.1000","block_height":1453937,"timestamp":1624886141,"fee_details":null,"coin":"tBCH","internal_id":"37c0458520cd8c90b00b39efaf39d26bd9da8ad8f24220627d74b01fc2c928d1","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000244dfc25d083a84dd6f6b7e20e1df5125ee14e3dabfa14633e4d1b6e388f8172f020000006a47304402207712ee78491d287bc451bcd8065a56c514db29dbbd194dea21dd52646a51f2ae02206c37d7d5d80511d8d302bd3e9be9880faac730c906d9ed26cbe6e557f9fdb2174121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff44dfc25d083a84dd6f6b7e20e1df5125ee14e3dabfa14633e4d1b6e388f8172f030000006a47304402201a244bf563679202b2aa5237d00227423463102b8d1d402b7b15694e065b6d5102201f26706918081fd99a34966bc9b51c3d244fbd56ac78df3fd64e3b601904ce8f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000001b54e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5f4a4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace3c2d960","tx_hash":"ddcd0c6edba3462b3b7cc52108d8a3efc5f81e0eaea99c62efdb2577c26acf75","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21517847","spent_by_me":"0.21517847","received_by_me":"0.21515847","my_balance_change":"-0.00002000","block_height":1453933,"timestamp":1624884016,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"ddcd0c6edba3462b3b7cc52108d8a3efc5f81e0eaea99c62efdb2577c26acf75","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000244dfc25d083a84dd6f6b7e20e1df5125ee14e3dabfa14633e4d1b6e388f8172f020000006a47304402207712ee78491d287bc451bcd8065a56c514db29dbbd194dea21dd52646a51f2ae02206c37d7d5d80511d8d302bd3e9be9880faac730c906d9ed26cbe6e557f9fdb2174121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff44dfc25d083a84dd6f6b7e20e1df5125ee14e3dabfa14633e4d1b6e388f8172f030000006a47304402201a244bf563679202b2aa5237d00227423463102b8d1d402b7b15694e065b6d5102201f26706918081fd99a34966bc9b51c3d244fbd56ac78df3fd64e3b601904ce8f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000001b54e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5f4a4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace3c2d960","tx_hash":"ddcd0c6edba3462b3b7cc52108d8a3efc5f81e0eaea99c62efdb2577c26acf75","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.6997","spent_by_me":"0.6997","received_by_me":"0.6996","my_balance_change":"-0.0001","block_height":1453933,"timestamp":1624884016,"fee_details":null,"coin":"tBCH","internal_id":"56c200ecd4f819f46e0954f87fb829cac1a2be27f10922eff982f68145c74cdc","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000275cf6ac27725dbef629ca9ae0e1ef8c5efa3d80821c57c3b2b46a3db6e0ccddd020000006a47304402204161d535f2494050dc10c1cd24ff0bcfe8c5df9e6389ad01eb4dffdf6478cdc402205ef8737bc6b18f1171237cbc46517ff362d6760738a228ee147a6ea7c22c5a784121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff75cf6ac27725dbef629ca9ae0e1ef8c5efa3d80821c57c3b2b46a3db6e0ccddd030000006a47304402203d2783dd35d55ebc0522d0551b1f50f3c2d694b5b77abfc8e8ca18efea3d3eeb022019a6fa027b4f6be432ae318b522f3169c6d0c3434f59af69c24adf34e0b906a04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000000176ce80300000000000017a9145bbccc15f66b9c23a09fedd529e62fdcb40737bd87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8f424801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace8c2d960","tx_hash":"4349badcfacb8c8f0d3c334349f870fbfc622de9554f5a3a3f20032e0d5268c7","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:ppdmenq47e4ecgaqnlka220x9lwtgpehh5f9g8tku6","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21515847","spent_by_me":"0.21515847","received_by_me":"0.21513847","my_balance_change":"-0.00002000","block_height":1453933,"timestamp":1624884016,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"4349badcfacb8c8f0d3c334349f870fbfc622de9554f5a3a3f20032e0d5268c7","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000275cf6ac27725dbef629ca9ae0e1ef8c5efa3d80821c57c3b2b46a3db6e0ccddd020000006a47304402204161d535f2494050dc10c1cd24ff0bcfe8c5df9e6389ad01eb4dffdf6478cdc402205ef8737bc6b18f1171237cbc46517ff362d6760738a228ee147a6ea7c22c5a784121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff75cf6ac27725dbef629ca9ae0e1ef8c5efa3d80821c57c3b2b46a3db6e0ccddd030000006a47304402203d2783dd35d55ebc0522d0551b1f50f3c2d694b5b77abfc8e8ca18efea3d3eeb022019a6fa027b4f6be432ae318b522f3169c6d0c3434f59af69c24adf34e0b906a04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000000176ce80300000000000017a9145bbccc15f66b9c23a09fedd529e62fdcb40737bd87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8f424801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace8c2d960","tx_hash":"4349badcfacb8c8f0d3c334349f870fbfc622de9554f5a3a3f20032e0d5268c7","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:ppdmenq47e4ecgaqnlka220x9lwtgpehh5j30u3pw8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.6996","spent_by_me":"0.6996","received_by_me":"0.5996","my_balance_change":"-0.1000","block_height":1453933,"timestamp":1624884016,"fee_details":null,"coin":"tBCH","internal_id":"89a6f9c88e1eab39e65d2cb5ee152f7d99d5cff386b71615e838c6ca2b7f9d9a","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002c808ec40b3b5efbfd4f44da81812af98b42f50d246f18ac21862575087f3820d020000006a4730440220508fac4b539cde4afc40a1acb8d36977254f14f38d224a3d4b16e6d6a9e6bedc022021cb03286f6eede49727ba6e7ca3fd152ac16414e2caf648d0542914acf163e44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffc808ec40b3b5efbfd4f44da81812af98b42f50d246f18ac21862575087f3820d030000006a47304402200e08f25dae26dbd0c4e001478c3e5c046d52358da8eb15524e8b183de42ced1d022028961d5639337864351f40bd7f50f7b88fe649bb9e34b69ba3d7cde7a8c0f58e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000001b55e80300000000000017a9146c7e381da62699d3e1986a101d91046d4affa0a887e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2f524801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6c99d960","tx_hash":"2f17f888e3b6d1e43346a1bfdae314ee2551dfe1207e6b6fdd843a085dc2df44","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:ppk8uwqa5cnfn5lpnp4pq8v3q3k54laq4qpxj823qc","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21519847","spent_by_me":"0.21519847","received_by_me":"0.21517847","my_balance_change":"-0.00002000","block_height":1453914,"timestamp":1624873371,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"2f17f888e3b6d1e43346a1bfdae314ee2551dfe1207e6b6fdd843a085dc2df44","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002c808ec40b3b5efbfd4f44da81812af98b42f50d246f18ac21862575087f3820d020000006a4730440220508fac4b539cde4afc40a1acb8d36977254f14f38d224a3d4b16e6d6a9e6bedc022021cb03286f6eede49727ba6e7ca3fd152ac16414e2caf648d0542914acf163e44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffc808ec40b3b5efbfd4f44da81812af98b42f50d246f18ac21862575087f3820d030000006a47304402200e08f25dae26dbd0c4e001478c3e5c046d52358da8eb15524e8b183de42ced1d022028961d5639337864351f40bd7f50f7b88fe649bb9e34b69ba3d7cde7a8c0f58e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000001b55e80300000000000017a9146c7e381da62699d3e1986a101d91046d4affa0a887e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2f524801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6c99d960","tx_hash":"2f17f888e3b6d1e43346a1bfdae314ee2551dfe1207e6b6fdd843a085dc2df44","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:ppk8uwqa5cnfn5lpnp4pq8v3q3k54laq4q6j4usxj9","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.7997","spent_by_me":"0.7997","received_by_me":"0.6997","my_balance_change":"-0.1000","block_height":1453914,"timestamp":1624873371,"fee_details":null,"coin":"tBCH","internal_id":"d127f5c07cf70753f059e039900e0b7c918bf211dce31f193c058b6ae44ef964","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002b249bb0a56d954fb1f1f737365df288f61dae8fbf8c60f7a00455bfb0b1635e9020000006a4730440220367a94a77ea8d8ba3fb401cd27204e163490d8186be8447ec57dea6f28cb983f022018b9de29b3c58686d7b76d3c73b1ccd5fe0821dbf85ce0c22dd02422115aac224121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb249bb0a56d954fb1f1f737365df288f61dae8fbf8c60f7a00455bfb0b1635e9030000006b483045022100c5acda6db723a488b5762c69e2030a5513173c49fa2b14db301c50cd8fab03490220257e50ada7fc92ff2071b57d05185aaa458d7d270b9a5c44324f0ac6b95976f94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000001f3de8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acff594801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6999d960","tx_hash":"0d82f38750576218c28af146d2502fb498af1218a84df4d4bfefb5b340ec08c8","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21521847","spent_by_me":"0.21521847","received_by_me":"0.21519847","my_balance_change":"-0.00002000","block_height":1453914,"timestamp":1624873371,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"0d82f38750576218c28af146d2502fb498af1218a84df4d4bfefb5b340ec08c8","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002b249bb0a56d954fb1f1f737365df288f61dae8fbf8c60f7a00455bfb0b1635e9020000006a4730440220367a94a77ea8d8ba3fb401cd27204e163490d8186be8447ec57dea6f28cb983f022018b9de29b3c58686d7b76d3c73b1ccd5fe0821dbf85ce0c22dd02422115aac224121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb249bb0a56d954fb1f1f737365df288f61dae8fbf8c60f7a00455bfb0b1635e9030000006b483045022100c5acda6db723a488b5762c69e2030a5513173c49fa2b14db301c50cd8fab03490220257e50ada7fc92ff2071b57d05185aaa458d7d270b9a5c44324f0ac6b95976f94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000001f3de8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acff594801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6999d960","tx_hash":"0d82f38750576218c28af146d2502fb498af1218a84df4d4bfefb5b340ec08c8","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.7998","spent_by_me":"0.7998","received_by_me":"0.7997","my_balance_change":"-0.0001","block_height":1453914,"timestamp":1624873371,"fee_details":null,"coin":"tBCH","internal_id":"c747951c681709645102156e2e289823482513014433a5db0d4938c66e49236e","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002736cf584f877ec7b6b95974bc461a9cfb9f126655b5d335471683154cc6cf4c5020000006a47304402206be99fe56a98e7a8c2ffe6f2d05c5c1f46a6577064b84d27d45fe0e959f6e77402201c512629313b48cd4df873222aa49046ae9a3a6e34e359d10d4308cb40438fba4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff736cf584f877ec7b6b95974bc461a9cfb9f126655b5d335471683154cc6cf4c5030000006a473044022020d774d045bbe3dce5b04af836f6a5629c6c4ce75b0b5ba8a1da0ae9a4ecc0530220522f86d20c9e4142e40f9a9c8d25db16fde91d4a0ad6f6ff2107e201386131b64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000001f3ee80300000000000017a914b0ca1fea17cf522c7e858416093fc6d95e55824087e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88accf614801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8c83d460","tx_hash":"e935160bfb5b45007a0fc6f8fbe8da618f28df6573731f1ffb54d9560abb49b2","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzcv58l2zl84ytr7skzpvzflcmv4u4vzgqc2sdq3kz","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21523847","spent_by_me":"0.21523847","received_by_me":"0.21521847","my_balance_change":"-0.00002000","block_height":1453354,"timestamp":1624540312,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"e935160bfb5b45007a0fc6f8fbe8da618f28df6573731f1ffb54d9560abb49b2","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002736cf584f877ec7b6b95974bc461a9cfb9f126655b5d335471683154cc6cf4c5020000006a47304402206be99fe56a98e7a8c2ffe6f2d05c5c1f46a6577064b84d27d45fe0e959f6e77402201c512629313b48cd4df873222aa49046ae9a3a6e34e359d10d4308cb40438fba4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff736cf584f877ec7b6b95974bc461a9cfb9f126655b5d335471683154cc6cf4c5030000006a473044022020d774d045bbe3dce5b04af836f6a5629c6c4ce75b0b5ba8a1da0ae9a4ecc0530220522f86d20c9e4142e40f9a9c8d25db16fde91d4a0ad6f6ff2107e201386131b64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000001f3ee80300000000000017a914b0ca1fea17cf522c7e858416093fc6d95e55824087e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88accf614801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8c83d460","tx_hash":"e935160bfb5b45007a0fc6f8fbe8da618f28df6573731f1ffb54d9560abb49b2","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzcv58l2zl84ytr7skzpvzflcmv4u4vzgqr7hk6xyl","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.8998","spent_by_me":"0.8998","received_by_me":"0.7998","my_balance_change":"-0.1000","block_height":1453354,"timestamp":1624540312,"fee_details":null,"coin":"tBCH","internal_id":"9145538486e5c0fec6ec12f8a93b6769a4ec31bdd197777ca9e2b9c96a5a5d8e","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000232809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b020000006a473044022057c88d815fa563eda8ef7d0dd5c522f4501ffa6110df455b151b31609f149c22022048fecfc9b16e983fbfd05b0d2b7c011c3dbec542577fa00cd9bd192b81961f8e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff32809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b030000006a4730440220539e1204d2805c0474111a1f233ff82c0ab06e6e2bfc0cbe4975eacae64a0b1f02200ec83d32c2180f5567d0f760e85f1efc99d9341cfebd86c9a334310f6d4381494121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000002326e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f694801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8983d460","tx_hash":"c5f46ccc5431687154335d5b6526f1b9cfa961c44b97956b7bec77f884f56c73","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21525847","spent_by_me":"0.21525847","received_by_me":"0.21523847","my_balance_change":"-0.00002000","block_height":1453354,"timestamp":1624540312,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"c5f46ccc5431687154335d5b6526f1b9cfa961c44b97956b7bec77f884f56c73","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000232809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b020000006a473044022057c88d815fa563eda8ef7d0dd5c522f4501ffa6110df455b151b31609f149c22022048fecfc9b16e983fbfd05b0d2b7c011c3dbec542577fa00cd9bd192b81961f8e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff32809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b030000006a4730440220539e1204d2805c0474111a1f233ff82c0ab06e6e2bfc0cbe4975eacae64a0b1f02200ec83d32c2180f5567d0f760e85f1efc99d9341cfebd86c9a334310f6d4381494121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000002326e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f694801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8983d460","tx_hash":"c5f46ccc5431687154335d5b6526f1b9cfa961c44b97956b7bec77f884f56c73","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.8999","spent_by_me":"0.8999","received_by_me":"0.8998","my_balance_change":"-0.0001","block_height":1453354,"timestamp":1624540312,"fee_details":null,"coin":"tBCH","internal_id":"070c126f57e38edc060ae9636aab5ed1fbf0fcd29f288863aeba8aaacf629232","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002f6859c372f562f72b9935499310c50707a6630d6550e8220227beb6666718b91010000006a4730440220566c9f2184a1c7911449f81ec78255a103c46dff96d4912491fae7d96c606a6b02202213eb87ed44145ff2afb83d177a9b5d8a8555b23bc8fb425dbfd037fe9b290e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cfffffffff6859c372f562f72b9935499310c50707a6630d6550e8220227beb6666718b91020000006b483045022100a2e4426090de97c9a0e3d78d6146bc8d56deb3124e0c37e27ae63cc8ffad531402204d3ed43b7a98e6b003bb2487ead570c9aeb5c0398e1ffdc712ac37ee1ad86cf84121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000003e7e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac57754801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0482d460","tx_hash":"26516535a6f61f750522ae7e459133b0aad0ba41aa04dbb384d037290a242e19","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58uu0a8dg36","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21528847","spent_by_me":"0.21528847","received_by_me":"0.21526847","my_balance_change":"-0.00002000","block_height":1453354,"timestamp":1624540312,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"26516535a6f61f750522ae7e459133b0aad0ba41aa04dbb384d037290a242e19","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002f6859c372f562f72b9935499310c50707a6630d6550e8220227beb6666718b91010000006a4730440220566c9f2184a1c7911449f81ec78255a103c46dff96d4912491fae7d96c606a6b02202213eb87ed44145ff2afb83d177a9b5d8a8555b23bc8fb425dbfd037fe9b290e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cfffffffff6859c372f562f72b9935499310c50707a6630d6550e8220227beb6666718b91020000006b483045022100a2e4426090de97c9a0e3d78d6146bc8d56deb3124e0c37e27ae63cc8ffad531402204d3ed43b7a98e6b003bb2487ead570c9aeb5c0398e1ffdc712ac37ee1ad86cf84121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000000010800000000000003e7e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac57754801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0482d460","tx_hash":"26516535a6f61f750522ae7e459133b0aad0ba41aa04dbb384d037290a242e19","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:qr9pupr5t6x2p3sd33vgz5ca2xlvgur58u8m6uhlr8","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1","spent_by_me":"0.1","received_by_me":"0.0999","my_balance_change":"-0.0001","block_height":1453354,"timestamp":1624540312,"fee_details":null,"coin":"tBCH","internal_id":"3494cf5ac3a03a85f03d73bacce5a331cfaafd735e4522eed3cc405f77f351bd","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000003192e240a2937d084b3db04aa41bad0aab03391457eae2205751ff6a635655126020000006a47304402205df1f20c02835ab8b7f687d2bcdb028dffc6b1772b3813e4cc75475fba4def6f0220131559457a20df685137fa249e72428e8e7d2d27968df351a7044dab8e7778a94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff2682816d0706d48c45fbfa4a4946e6cdad9ddc3e2f61b4c848f474ab49a2a0bd020000006a473044022034b496e18d8f4255ee840c9f97102b59a43a6fc50f2c18248eed1f932c30d247022005053a85b80af9539eed1232a8bc389c6d1b088587366b4afe8fd9729fc3ef034121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff192e240a2937d084b3db04aa41bad0aab03391457eae2205751ff6a635655126030000006b48304502210098fc91a689f922c16614dd548639711c47a5ca6efe8aa0603a4ba192e2e394850220586b571f9eee789746e64d8a270c18e6e246cb95d5d5ce88cbac69581baa20dd4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000002327e80300000000000017a914600760da42613ec484d739a52c672aa4dc3b29dd87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6f714801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0882d460","tx_hash":"0ba1b91abbfceaa0777424165edb2928dace87d59669c913989950da31968032","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:ppsqwcx6gfsna3yy6uu62tr892jdcwefm5qy04gp3q","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21527847","spent_by_me":"0.21527847","received_by_me":"0.21525847","my_balance_change":"-0.00002000","block_height":1453354,"timestamp":1624540312,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"0ba1b91abbfceaa0777424165edb2928dace87d59669c913989950da31968032","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000003192e240a2937d084b3db04aa41bad0aab03391457eae2205751ff6a635655126020000006a47304402205df1f20c02835ab8b7f687d2bcdb028dffc6b1772b3813e4cc75475fba4def6f0220131559457a20df685137fa249e72428e8e7d2d27968df351a7044dab8e7778a94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff2682816d0706d48c45fbfa4a4946e6cdad9ddc3e2f61b4c848f474ab49a2a0bd020000006a473044022034b496e18d8f4255ee840c9f97102b59a43a6fc50f2c18248eed1f932c30d247022005053a85b80af9539eed1232a8bc389c6d1b088587366b4afe8fd9729fc3ef034121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff192e240a2937d084b3db04aa41bad0aab03391457eae2205751ff6a635655126030000006b48304502210098fc91a689f922c16614dd548639711c47a5ca6efe8aa0603a4ba192e2e394850220586b571f9eee789746e64d8a270c18e6e246cb95d5d5ce88cbac69581baa20dd4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000002327e80300000000000017a914600760da42613ec484d739a52c672aa4dc3b29dd87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6f714801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0882d460","tx_hash":"0ba1b91abbfceaa0777424165edb2928dace87d59669c913989950da31968032","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:ppsqwcx6gfsna3yy6uu62tr892jdcwefm5msgwjkra","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.9999","spent_by_me":"0.9999","received_by_me":"0.8999","my_balance_change":"-0.1000","block_height":1453354,"timestamp":1624540312,"fee_details":null,"coin":"tBCH","internal_id":"8ac5a09bd55cd5875e57bf6b2ad46ded395e038edc13e69670720884602e9d52","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000023b73ed4180f25328865e326356890047e448911b14ea013cc086e3ad73b57b0301000000d8483045022100ba5cfb6dfe65296dad41023252593c99776c5ae34526f93029058db8463604ca0220500d93700e67702b60c10d88188b818b001dea81a8a13dd7ce678643fb89237a41200000000000000000000000000000000000000000000000000000000000000000004c6b6304c09dc860b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff3b73ed4180f25328865e326356890047e448911b14ea013cc086e3ad73b57b03020000006a47304402204329285e2fcbb6f5ff1bd6d23eaee132e0e0494cdc3ec05092a9782e866e4aee02202395943dd6e11fe2e13c79873c5e47c1ef986f78e97dc882adb37ee2424de9f14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac277d4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc09dc860","tx_hash":"918b716666eb7b2220820e55d630667a70500c31995493b9722f562f379c85f6","from":["bchtest:prlh6yhf8tlwnnvyk3klaath66dq3gd27yvc5anfy4","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21529847","spent_by_me":"0.21528847","received_by_me":"0.21528847","my_balance_change":"0.00000000","block_height":1452134,"timestamp":1623760650,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"918b716666eb7b2220820e55d630667a70500c31995493b9722f562f379c85f6","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000023b73ed4180f25328865e326356890047e448911b14ea013cc086e3ad73b57b0301000000d8483045022100ba5cfb6dfe65296dad41023252593c99776c5ae34526f93029058db8463604ca0220500d93700e67702b60c10d88188b818b001dea81a8a13dd7ce678643fb89237a41200000000000000000000000000000000000000000000000000000000000000000004c6b6304c09dc860b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff3b73ed4180f25328865e326356890047e448911b14ea013cc086e3ad73b57b03020000006a47304402204329285e2fcbb6f5ff1bd6d23eaee132e0e0494cdc3ec05092a9782e866e4aee02202395943dd6e11fe2e13c79873c5e47c1ef986f78e97dc882adb37ee2424de9f14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac277d4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc09dc860","tx_hash":"918b716666eb7b2220820e55d630667a70500c31995493b9722f562f379c85f6","from":["slptest:prlh6yhf8tlwnnvyk3klaath66dq3gd27yhvnxf7kg"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1","spent_by_me":"0","received_by_me":"0.1","my_balance_change":"0.1","block_height":1452134,"timestamp":1623760650,"fee_details":null,"coin":"tBCH","internal_id":"7e23adb9b707f842ce5c633cca58f5a4c3ec15fca31278d7576380429543b3b1","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002a2685746afd55241c7860e4fb9ad7fbc2d1be032738dc1209f2188d2a07e70a1010000006a47304402207ba49344eee4c5d03b349cd20498e6b326fda219d2bc235fd0077405f89c3ab4022010a40351ce3337386bb28acfce5ae5abb63965b5daddbfcf0b6abe7110dc2c164121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffa2685746afd55241c7860e4fb9ad7fbc2d1be032738dc1209f2188d2a07e70a1020000006a4730440220259fcb86b2cdd3d41d52a1f04eea77491d1c87ffd3f4c1be99b9a3858ee90e46022063728ab5b0c9e7e0ac4d6e99c6549dc57b6804fae6bd78257d60f3f0483461e14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914ff7d12e93afee9cd84b46dfef577d69a08a1aaf1870f814801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc09dc860","tx_hash":"037bb573ade386c03c01ea141b9148e44700895663325e862853f28041ed733b","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:prlh6yhf8tlwnnvyk3klaath66dq3gd27yvc5anfy4","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21530847","spent_by_me":"0.21530847","received_by_me":"0.21528847","my_balance_change":"-0.00002000","block_height":1452134,"timestamp":1623760650,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"037bb573ade386c03c01ea141b9148e44700895663325e862853f28041ed733b","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002a2685746afd55241c7860e4fb9ad7fbc2d1be032738dc1209f2188d2a07e70a1010000006a47304402207ba49344eee4c5d03b349cd20498e6b326fda219d2bc235fd0077405f89c3ab4022010a40351ce3337386bb28acfce5ae5abb63965b5daddbfcf0b6abe7110dc2c164121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffa2685746afd55241c7860e4fb9ad7fbc2d1be032738dc1209f2188d2a07e70a1020000006a4730440220259fcb86b2cdd3d41d52a1f04eea77491d1c87ffd3f4c1be99b9a3858ee90e46022063728ab5b0c9e7e0ac4d6e99c6549dc57b6804fae6bd78257d60f3f0483461e14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914ff7d12e93afee9cd84b46dfef577d69a08a1aaf1870f814801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc09dc860","tx_hash":"037bb573ade386c03c01ea141b9148e44700895663325e862853f28041ed733b","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:prlh6yhf8tlwnnvyk3klaath66dq3gd27yhvnxf7kg"],"total_amount":"0.1","spent_by_me":"0.1","received_by_me":"0","my_balance_change":"-0.1","block_height":1452134,"timestamp":1623760650,"fee_details":null,"coin":"tBCH","internal_id":"0e7bf55e1c0fd31ece3febcffba8366d1fde3f87c5457e55c32edcc899a2055c","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002410a8a85384fbbe8648b3a85cc20065aa9ae5b46bfbd67eb9a8e404c58f71177010000006a473044022029550c628335be94cf7366b40bc60da2b9157a3e71a10d50af259e0d98b52ea702205ff9ed3eab4255cb6c4d5cd910edb9e0c7460d3e6747e6e3994533753d7a89504121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff410a8a85384fbbe8648b3a85cc20065aa9ae5b46bfbd67eb9a8e404c58f71177020000006a47304402206dccbd9ef82d300f3f4af4fe625c67aaf8254851e5515413c6e166f0b536e47a02206c1d7b7142c2204adc31ff6f037f2ed497baf7905df832915dc58ccb897917344121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914c3699d1d972d8ec7db7baf37b6493126fc364c2187af904801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac738dc860","tx_hash":"e80d66fd0b4b8cba533789719f06341ee0715e32068a41e4198b0cc697280740","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:prpkn8gajukca37m0whn0djfxyn0cdjvyytta8w7m6","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21534847","spent_by_me":"0.21534847","received_by_me":"0.21532847","my_balance_change":"-0.00002000","block_height":1452131,"timestamp":1623756980,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"e80d66fd0b4b8cba533789719f06341ee0715e32068a41e4198b0cc697280740","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002410a8a85384fbbe8648b3a85cc20065aa9ae5b46bfbd67eb9a8e404c58f71177010000006a473044022029550c628335be94cf7366b40bc60da2b9157a3e71a10d50af259e0d98b52ea702205ff9ed3eab4255cb6c4d5cd910edb9e0c7460d3e6747e6e3994533753d7a89504121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff410a8a85384fbbe8648b3a85cc20065aa9ae5b46bfbd67eb9a8e404c58f71177020000006a47304402206dccbd9ef82d300f3f4af4fe625c67aaf8254851e5515413c6e166f0b536e47a02206c1d7b7142c2204adc31ff6f037f2ed497baf7905df832915dc58ccb897917344121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914c3699d1d972d8ec7db7baf37b6493126fc364c2187af904801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac738dc860","tx_hash":"e80d66fd0b4b8cba533789719f06341ee0715e32068a41e4198b0cc697280740","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:prpkn8gajukca37m0whn0djfxyn0cdjvyysl6u5ff8"],"total_amount":"0.1","spent_by_me":"0.1","received_by_me":"0","my_balance_change":"-0.1","block_height":1452131,"timestamp":1623756980,"fee_details":null,"coin":"tBCH","internal_id":"ea1da4ab1b0970b3f05bda183a7bb7e2d13abfb4b0d732e93072cd65d96aa4a0","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002aab749c051de90b293772ca8b89aa793dd53896d764499ab61f7a945bebc9a0101000000d747304402205cbf6b72120cb686b8723c07b144ecd64f6dd264ab633c4e3e4f15c35bcf7dc902206e1a87cfec14807ccd8e721153de284c928d9f8c4c5b3a329d9b3a9a642bec9641200000000000000000000000000000000000000000000000000000000000000000004c6b63046390c860b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffffaab749c051de90b293772ca8b89aa793dd53896d764499ab61f7a945bebc9a01020000006b483045022100a1fea0fb751ca2509525ddfa65b3bac1bd27a93986dd4466dab0dd07810364dc02207428be9d24d414048111488e9ea0bea7f453a866181df8fd70fba13ae3fe7cb14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acf7844801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6390c860","tx_hash":"a1707ea0d288219f20c18d7332e01b2dbc7fadb94f0e86c74152d5af465768a2","from":["bchtest:pr805s2lsjgc5prldg8tjtu720rdld6lqssjl22saf","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21531847","spent_by_me":"0.21530847","received_by_me":"0.21530847","my_balance_change":"0.00000000","block_height":1452131,"timestamp":1623756980,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"a1707ea0d288219f20c18d7332e01b2dbc7fadb94f0e86c74152d5af465768a2","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002aab749c051de90b293772ca8b89aa793dd53896d764499ab61f7a945bebc9a0101000000d747304402205cbf6b72120cb686b8723c07b144ecd64f6dd264ab633c4e3e4f15c35bcf7dc902206e1a87cfec14807ccd8e721153de284c928d9f8c4c5b3a329d9b3a9a642bec9641200000000000000000000000000000000000000000000000000000000000000000004c6b63046390c860b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffffaab749c051de90b293772ca8b89aa793dd53896d764499ab61f7a945bebc9a01020000006b483045022100a1fea0fb751ca2509525ddfa65b3bac1bd27a93986dd4466dab0dd07810364dc02207428be9d24d414048111488e9ea0bea7f453a866181df8fd70fba13ae3fe7cb14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acf7844801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6390c860","tx_hash":"a1707ea0d288219f20c18d7332e01b2dbc7fadb94f0e86c74152d5af465768a2","from":["slptest:pr805s2lsjgc5prldg8tjtu720rdld6lqstxc3s805"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1","spent_by_me":"0","received_by_me":"0.1","my_balance_change":"0.1","block_height":1452131,"timestamp":1623756980,"fee_details":null,"coin":"tBCH","internal_id":"beed6e74f8d6818b97e93fa6af50922621041be59129a8646e981c62e112b657","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000240072897c60c8b19e4418a06325e71e01e34069f71893753ba8c4b0bfd660de801000000d7473044022067bf6d382b26ea5010a02822e1e4ad3983450fef71dc90713fdd71cda537f45202202845eba1b72e172ca8f4e8e1cef297517ae13ee1e23b6bd587e5e3c7709dfe4e41200000000000000000000000000000000000000000000000000000000000000000004c6b6304728dc860b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff40072897c60c8b19e4418a06325e71e01e34069f71893753ba8c4b0bfd660de8020000006a47304402206e7e7e013e58c05b9811072050957661afa2e769451dfd45bab866fe80bd330102203e6c9aeeeb3c4e693404140e33fc9e9c0360188d7e569c968768f30e0c5035f04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc78c4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac728dc860","tx_hash":"0d14600f4054dd60cfc51625010c31f1cc1300f8f22e6e9865120f5ec0294616","from":["bchtest:prpkn8gajukca37m0whn0djfxyn0cdjvyytta8w7m6","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21533847","spent_by_me":"0.21532847","received_by_me":"0.21532847","my_balance_change":"0.00000000","block_height":1452131,"timestamp":1623756980,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"0d14600f4054dd60cfc51625010c31f1cc1300f8f22e6e9865120f5ec0294616","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000240072897c60c8b19e4418a06325e71e01e34069f71893753ba8c4b0bfd660de801000000d7473044022067bf6d382b26ea5010a02822e1e4ad3983450fef71dc90713fdd71cda537f45202202845eba1b72e172ca8f4e8e1cef297517ae13ee1e23b6bd587e5e3c7709dfe4e41200000000000000000000000000000000000000000000000000000000000000000004c6b6304728dc860b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff40072897c60c8b19e4418a06325e71e01e34069f71893753ba8c4b0bfd660de8020000006a47304402206e7e7e013e58c05b9811072050957661afa2e769451dfd45bab866fe80bd330102203e6c9aeeeb3c4e693404140e33fc9e9c0360188d7e569c968768f30e0c5035f04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc78c4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac728dc860","tx_hash":"0d14600f4054dd60cfc51625010c31f1cc1300f8f22e6e9865120f5ec0294616","from":["slptest:prpkn8gajukca37m0whn0djfxyn0cdjvyysl6u5ff8"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1","spent_by_me":"0","received_by_me":"0.1","my_balance_change":"0.1","block_height":1452131,"timestamp":1623756980,"fee_details":null,"coin":"tBCH","internal_id":"06756d80c727a8b9622d2c71ce711e951770ac4ce941be05956d46b1ae66dc35","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002164629c05e0f1265986e2ef2f80013ccf1310c012516c5cf60dd54400f60140d010000006b483045022100c8c22ac7e4788adaa1907771092a54280120969aa38ed3076a4fcb197134ced9022015f50a4866f85ce1b8d069e344a885836c33c2afdb53632ff407114987c9462f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff164629c05e0f1265986e2ef2f80013ccf1310c012516c5cf60dd54400f60140d020000006b483045022100f7fef47051925d395ab5dd237322ed3ef1070e91375e4f49d85e6ecd2a8bc01702200727b678032300035b21c96a69298daedaecf434b82df0edd09cfe21ab7cd0234121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914cefa415f84918a047f6a0eb92f9e53c6dfb75f0487df884801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6490c860","tx_hash":"019abcbe45a9f761ab9944766d8953dd93a79ab8a82c7793b290de51c049b7aa","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pr805s2lsjgc5prldg8tjtu720rdld6lqssjl22saf","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21532847","spent_by_me":"0.21532847","received_by_me":"0.21530847","my_balance_change":"-0.00002000","block_height":1452131,"timestamp":1623756980,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"019abcbe45a9f761ab9944766d8953dd93a79ab8a82c7793b290de51c049b7aa","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002164629c05e0f1265986e2ef2f80013ccf1310c012516c5cf60dd54400f60140d010000006b483045022100c8c22ac7e4788adaa1907771092a54280120969aa38ed3076a4fcb197134ced9022015f50a4866f85ce1b8d069e344a885836c33c2afdb53632ff407114987c9462f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff164629c05e0f1265986e2ef2f80013ccf1310c012516c5cf60dd54400f60140d020000006b483045022100f7fef47051925d395ab5dd237322ed3ef1070e91375e4f49d85e6ecd2a8bc01702200727b678032300035b21c96a69298daedaecf434b82df0edd09cfe21ab7cd0234121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914cefa415f84918a047f6a0eb92f9e53c6dfb75f0487df884801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6490c860","tx_hash":"019abcbe45a9f761ab9944766d8953dd93a79ab8a82c7793b290de51c049b7aa","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pr805s2lsjgc5prldg8tjtu720rdld6lqstxc3s805"],"total_amount":"0.1","spent_by_me":"0.1","received_by_me":"0","my_balance_change":"-0.1","block_height":1452131,"timestamp":1623756980,"fee_details":null,"coin":"tBCH","internal_id":"c28747759edb023a57cf8cb86259548d3f1aa48b26683924d2e64f13811738d6","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002e34e90e127bbe6740a13a079a6cf19dab7f8f8e6bf84796487e55504056e0c3801000000d7473044022059220530aca5131991b9c7cee76bd1d57530c53e9418132e56f131f1f33efc4a022022d87c69f635a1d8103ddc5d76a5d0a85255c9b2672ad5daee0b458826a4682d41200000000000000000000000000000000000000000000000000000000000000000004c6b6304a001c260b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffffe34e90e127bbe6740a13a079a6cf19dab7f8f8e6bf84796487e55504056e0c38020000006a47304402201d6a1ad6094ebb7cd7e7921ccc49fce2e628ab6c510149f69505711c60351365022021c7f3bcad9ae1fd75f2e65e13dba4bd6bf7af5850566f74d9dbd48b8fe466914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac97944801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca001c260","tx_hash":"7711f7584c408e9aeb67bdbf465baea95a0620cc853a8b64e8bb4f38858a0a41","from":["bchtest:pzdlyeepfu7a85yjqs8kjhmqmtt84a2w056yc2ccax","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21535847","spent_by_me":"0.21534847","received_by_me":"0.21534847","my_balance_change":"0.00000000","block_height":1449202,"timestamp":1623327926,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"7711f7584c408e9aeb67bdbf465baea95a0620cc853a8b64e8bb4f38858a0a41","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002e34e90e127bbe6740a13a079a6cf19dab7f8f8e6bf84796487e55504056e0c3801000000d7473044022059220530aca5131991b9c7cee76bd1d57530c53e9418132e56f131f1f33efc4a022022d87c69f635a1d8103ddc5d76a5d0a85255c9b2672ad5daee0b458826a4682d41200000000000000000000000000000000000000000000000000000000000000000004c6b6304a001c260b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffffe34e90e127bbe6740a13a079a6cf19dab7f8f8e6bf84796487e55504056e0c38020000006a47304402201d6a1ad6094ebb7cd7e7921ccc49fce2e628ab6c510149f69505711c60351365022021c7f3bcad9ae1fd75f2e65e13dba4bd6bf7af5850566f74d9dbd48b8fe466914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac97944801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca001c260","tx_hash":"7711f7584c408e9aeb67bdbf465baea95a0620cc853a8b64e8bb4f38858a0a41","from":["slptest:pzdlyeepfu7a85yjqs8kjhmqmtt84a2w05psl3z00m"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1","spent_by_me":"0","received_by_me":"0.1","my_balance_change":"0.1","block_height":1449202,"timestamp":1623327926,"fee_details":null,"coin":"tBCH","internal_id":"8ed0dd4cdb558f5d425d577676b8795c109cef7b27e5d9744ea6cc0086009bb1","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002cf434aca56ad8f23162765f944782ba76cd2694307e39a0ce00a401f7f83baef010000006a473044022058f4e2b7306b063cf97af4dd9fcdcbd45feed4160a418708f3e62539499404f7022050fc3bc7a13e634c4dc41657474d0629e576d7b4e0c5b8ef8623b77afcff996f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffcf434aca56ad8f23162765f944782ba76cd2694307e39a0ce00a401f7f83baef020000006b483045022100c638bc57150d9f53048f25ab5c9799af31d9f6ede98d9d04982bfa2f1730a14f02200af28c72f35ae09111f9d7047776a6571d57de532ebe1315b62b9086793301224121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a9149bf267214f3dd3d092040f695f60dad67af54e7d877f984801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca001c260","tx_hash":"380c6e050455e587647984bfe6f8f8b7da19cfa679a0130a74e6bb27e1904ee3","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzdlyeepfu7a85yjqs8kjhmqmtt84a2w056yc2ccax","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21536847","spent_by_me":"0.21536847","received_by_me":"0.21534847","my_balance_change":"-0.00002000","block_height":1449202,"timestamp":1623327926,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"380c6e050455e587647984bfe6f8f8b7da19cfa679a0130a74e6bb27e1904ee3","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002cf434aca56ad8f23162765f944782ba76cd2694307e39a0ce00a401f7f83baef010000006a473044022058f4e2b7306b063cf97af4dd9fcdcbd45feed4160a418708f3e62539499404f7022050fc3bc7a13e634c4dc41657474d0629e576d7b4e0c5b8ef8623b77afcff996f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffcf434aca56ad8f23162765f944782ba76cd2694307e39a0ce00a401f7f83baef020000006b483045022100c638bc57150d9f53048f25ab5c9799af31d9f6ede98d9d04982bfa2f1730a14f02200af28c72f35ae09111f9d7047776a6571d57de532ebe1315b62b9086793301224121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a9149bf267214f3dd3d092040f695f60dad67af54e7d877f984801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca001c260","tx_hash":"380c6e050455e587647984bfe6f8f8b7da19cfa679a0130a74e6bb27e1904ee3","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzdlyeepfu7a85yjqs8kjhmqmtt84a2w05psl3z00m"],"total_amount":"0.1","spent_by_me":"0.1","received_by_me":"0","my_balance_change":"-0.1","block_height":1449202,"timestamp":1623327926,"fee_details":null,"coin":"tBCH","internal_id":"3564c09cca002561f56b05abd63500018bd7b2633fac3605f28a40cba003dacf","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000256333aaa3bd3f67ad5c0dd1f8b999d58766bab0416e149f7111ac0c4b448344301000000d747304402203bcd71f0b8a61dba3e1ad5cc4833873343235f429acd2f5108bccd2a78fd4c1c02201f10102ee2f7c968dd19700c23b193f55d587c3e7add9c775d127786ef9eb26b41200000000000000000000000000000000000000000000000000000000000000000004c6b6304c358bf60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff56333aaa3bd3f67ad5c0dd1f8b999d58766bab0416e149f7111ac0c4b4483443020000006b483045022100b017d816fe381c0fb200cc154117d1f65ddd668800a75d5fa6099bd16f23be1e02200e0635852d63580259a2479b88065c5e75d61eb6af45eb6dff42ef3654fafba44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac679c4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc358bf60","tx_hash":"efba837f1f400ae00c9ae3074369d26ca72b7844f9652716238fad56ca4a43cf","from":["bchtest:pzgwyss0ajdyvfxgjelqu85fy9yx2yfk9u4m7ad4zy","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21537847","spent_by_me":"0.21536847","received_by_me":"0.21536847","my_balance_change":"0.00000000","block_height":1449059,"timestamp":1623153240,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"efba837f1f400ae00c9ae3074369d26ca72b7844f9652716238fad56ca4a43cf","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000256333aaa3bd3f67ad5c0dd1f8b999d58766bab0416e149f7111ac0c4b448344301000000d747304402203bcd71f0b8a61dba3e1ad5cc4833873343235f429acd2f5108bccd2a78fd4c1c02201f10102ee2f7c968dd19700c23b193f55d587c3e7add9c775d127786ef9eb26b41200000000000000000000000000000000000000000000000000000000000000000004c6b6304c358bf60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff56333aaa3bd3f67ad5c0dd1f8b999d58766bab0416e149f7111ac0c4b4483443020000006b483045022100b017d816fe381c0fb200cc154117d1f65ddd668800a75d5fa6099bd16f23be1e02200e0635852d63580259a2479b88065c5e75d61eb6af45eb6dff42ef3654fafba44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac679c4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc358bf60","tx_hash":"efba837f1f400ae00c9ae3074369d26ca72b7844f9652716238fad56ca4a43cf","from":["slptest:pzgwyss0ajdyvfxgjelqu85fy9yx2yfk9uw0exhzse"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1","spent_by_me":"0","received_by_me":"0.1","my_balance_change":"0.1","block_height":1449059,"timestamp":1623153240,"fee_details":null,"coin":"tBCH","internal_id":"7a87dc602fb74b66ffdadc35efb67cdd4688c4c57e5bdbd4f4335d25eceee309","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000025bda96aeabdc64916e14920236d4df010ad6aa009ed77375370a0450131fd211010000006b483045022100ad6f91abd961da2f87bd60911dc937bcbb0cab9c054d89c5f93fc3ed755a158402203eac6c254cfb808b42e449f3a395b0f7cf4566e45ccc2b5a752c9fb52d95cc934121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff5bda96aeabdc64916e14920236d4df010ad6aa009ed77375370a0450131fd211020000006a47304402203db71882057cb8f590954d15f740f70f30ee3db57af1c6822d76dfa0d4c14742022008767a3df6a2bfef875b5922a4131e884ad99208a4a31b6dadc9c11517ee7a044121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a91490e2420fec9a4624c8967e0e1e8921486511362f874fa04801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc358bf60","tx_hash":"433448b4c4c01a11f749e11604ab6b76589d998b1fddc0d57af6d33baa3a3356","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzgwyss0ajdyvfxgjelqu85fy9yx2yfk9u4m7ad4zy","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21538847","spent_by_me":"0.21538847","received_by_me":"0.21536847","my_balance_change":"-0.00002000","block_height":1449059,"timestamp":1623153240,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"433448b4c4c01a11f749e11604ab6b76589d998b1fddc0d57af6d33baa3a3356","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000025bda96aeabdc64916e14920236d4df010ad6aa009ed77375370a0450131fd211010000006b483045022100ad6f91abd961da2f87bd60911dc937bcbb0cab9c054d89c5f93fc3ed755a158402203eac6c254cfb808b42e449f3a395b0f7cf4566e45ccc2b5a752c9fb52d95cc934121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff5bda96aeabdc64916e14920236d4df010ad6aa009ed77375370a0450131fd211020000006a47304402203db71882057cb8f590954d15f740f70f30ee3db57af1c6822d76dfa0d4c14742022008767a3df6a2bfef875b5922a4131e884ad99208a4a31b6dadc9c11517ee7a044121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a91490e2420fec9a4624c8967e0e1e8921486511362f874fa04801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc358bf60","tx_hash":"433448b4c4c01a11f749e11604ab6b76589d998b1fddc0d57af6d33baa3a3356","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzgwyss0ajdyvfxgjelqu85fy9yx2yfk9uw0exhzse"],"total_amount":"0.1","spent_by_me":"0.1","received_by_me":"0","my_balance_change":"-0.1","block_height":1449059,"timestamp":1623153240,"fee_details":null,"coin":"tBCH","internal_id":"b590067f740be1f064fe3914a94d20daa3c6030e96a121a92a48733f6838926a","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000261c03a8fa3713a1c69f5d72e49535bda965fee2b108b817a2963a209107eb176010000006a4730440220562c4ceee979119d1c2195369bbe62aba1e3fc44c1f5db2c8ae26b767feae8df0220331550ba31fedc8904e25ff2d7387f33d19d9c9fd91749ce00e4992ea75a1d484121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff61c03a8fa3713a1c69f5d72e49535bda965fee2b108b817a2963a209107eb176020000006a4730440220550ae9fa5bb1c12e7272eef9a32f3664fca340199826de16323100e11138a8c6022058b2a7c1bebeed29e436f5b7d5a0a7794830ae5c210e986a87083dbfca59e0da4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000002328e80300000000000017a914fb01b4ac662ae5b17ef686d78d353c28f26c228c87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1fa84801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9e50bf60","tx_hash":"bda0a249ab74f448c8b4612f3edc9dadcde646494afafb458cd406076d818226","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:prasrd9vvc4wtvt776rd0rf48s50ympz3s24vepcad","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21541847","spent_by_me":"0.21541847","received_by_me":"0.21539847","my_balance_change":"-0.00002000","block_height":1449057,"timestamp":1623150810,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"bda0a249ab74f448c8b4612f3edc9dadcde646494afafb458cd406076d818226","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000261c03a8fa3713a1c69f5d72e49535bda965fee2b108b817a2963a209107eb176010000006a4730440220562c4ceee979119d1c2195369bbe62aba1e3fc44c1f5db2c8ae26b767feae8df0220331550ba31fedc8904e25ff2d7387f33d19d9c9fd91749ce00e4992ea75a1d484121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff61c03a8fa3713a1c69f5d72e49535bda965fee2b108b817a2963a209107eb176020000006a4730440220550ae9fa5bb1c12e7272eef9a32f3664fca340199826de16323100e11138a8c6022058b2a7c1bebeed29e436f5b7d5a0a7794830ae5c210e986a87083dbfca59e0da4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000002328e80300000000000017a914fb01b4ac662ae5b17ef686d78d353c28f26c228c87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1fa84801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9e50bf60","tx_hash":"bda0a249ab74f448c8b4612f3edc9dadcde646494afafb458cd406076d818226","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:prasrd9vvc4wtvt776rd0rf48s50ympz3s3ptzm00s","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"1","received_by_me":"0.9","my_balance_change":"-0.1","block_height":1449057,"timestamp":1623150810,"fee_details":null,"coin":"tBCH","internal_id":"b87b0c4a4487dd7f0727fb1484b7bef702d865e438afb4c74a784b5d369a8d30","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000022682816d0706d48c45fbfa4a4946e6cdad9ddc3e2f61b4c848f474ab49a2a0bd01000000d747304402201b17877e953cf30a4c6f454f6b5bb848b8e22d63314faf10d6dd84eb58890c1c022027a710c07477a35f8a676dca775b60cf60d8ff50fb9435d8dffd7f5cb329db7441200000000000000000000000000000000000000000000000000000000000000000004c6b63049e50bf60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff2682816d0706d48c45fbfa4a4946e6cdad9ddc3e2f61b4c848f474ab49a2a0bd030000006b483045022100b3cc1ff2e40d708111e822dc44a0531f635268815a33907946abd6e9e463bc0902203699ed5e35bd96151959d99081ffc00a4284b2fb6796d99f2b1e755add919ddd4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac37a44801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9e50bf60","tx_hash":"11d21f1350040a377573d79e00aad60a01dfd4360292146e9164dcabae96da5b","from":["bchtest:prasrd9vvc4wtvt776rd0rf48s50ympz3s24vepcad","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21539847","spent_by_me":"0.21538847","received_by_me":"0.21538847","my_balance_change":"0.00000000","block_height":1449057,"timestamp":1623150810,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"11d21f1350040a377573d79e00aad60a01dfd4360292146e9164dcabae96da5b","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000022682816d0706d48c45fbfa4a4946e6cdad9ddc3e2f61b4c848f474ab49a2a0bd01000000d747304402201b17877e953cf30a4c6f454f6b5bb848b8e22d63314faf10d6dd84eb58890c1c022027a710c07477a35f8a676dca775b60cf60d8ff50fb9435d8dffd7f5cb329db7441200000000000000000000000000000000000000000000000000000000000000000004c6b63049e50bf60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff2682816d0706d48c45fbfa4a4946e6cdad9ddc3e2f61b4c848f474ab49a2a0bd030000006b483045022100b3cc1ff2e40d708111e822dc44a0531f635268815a33907946abd6e9e463bc0902203699ed5e35bd96151959d99081ffc00a4284b2fb6796d99f2b1e755add919ddd4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac37a44801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9e50bf60","tx_hash":"11d21f1350040a377573d79e00aad60a01dfd4360292146e9164dcabae96da5b","from":["slptest:prasrd9vvc4wtvt776rd0rf48s50ympz3s3ptzm00s"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1","spent_by_me":"0","received_by_me":"0.1","my_balance_change":"0.1","block_height":1449057,"timestamp":1623150810,"fee_details":null,"coin":"tBCH","internal_id":"ad73b00f1f0bf7635e480573b31574f38a257cac39b0a9a8134ac53a16f5e980","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000247721382b1cbadd5a935b7ea5054ee9c47ef93f8a1c804ea8228057ca5f5625b01000000b6473044022007e1a1e08c86c4d67bc48b468ee1bff60204135e35b14b72dc46c27a34600097022026205ef3706f44094d464d47a54bc0f2a21762abaff4f9aa34527a2f8232baf941514c6b63041b24bf60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821010101010101010101010101010101010101010101010101010101010101010101ac68feffffff47721382b1cbadd5a935b7ea5054ee9c47ef93f8a1c804ea8228057ca5f5625b020000006b483045022100906ae8f5b2e75571ec7442264f2b775731ca98d1e56bda6671ef869d1ae0156902200d1367b5adaec03158633a6380f60aa3a6a6a668320b333eb2f0efb3635684d84121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acefaf4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace625bf60","tx_hash":"76b17e1009a263297a818b102bee5f96da5b53492ed7f5691c3a71a38f3ac061","from":["bchtest:pp2zukdgpr7u6x45v8kg30hc8c9mndvavs30ttm4v3","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21542847","spent_by_me":"0.21541847","received_by_me":"0.21541847","my_balance_change":"0.00000000","block_height":1449054,"timestamp":1623147145,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"76b17e1009a263297a818b102bee5f96da5b53492ed7f5691c3a71a38f3ac061","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000247721382b1cbadd5a935b7ea5054ee9c47ef93f8a1c804ea8228057ca5f5625b01000000b6473044022007e1a1e08c86c4d67bc48b468ee1bff60204135e35b14b72dc46c27a34600097022026205ef3706f44094d464d47a54bc0f2a21762abaff4f9aa34527a2f8232baf941514c6b63041b24bf60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821010101010101010101010101010101010101010101010101010101010101010101ac68feffffff47721382b1cbadd5a935b7ea5054ee9c47ef93f8a1c804ea8228057ca5f5625b020000006b483045022100906ae8f5b2e75571ec7442264f2b775731ca98d1e56bda6671ef869d1ae0156902200d1367b5adaec03158633a6380f60aa3a6a6a668320b333eb2f0efb3635684d84121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acefaf4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace625bf60","tx_hash":"76b17e1009a263297a818b102bee5f96da5b53492ed7f5691c3a71a38f3ac061","from":["slptest:pp2zukdgpr7u6x45v8kg30hc8c9mndvavs2mvspz7v"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1449054,"timestamp":1623147145,"fee_details":null,"coin":"tBCH","internal_id":"c2ebf21255a77ea055e56b796c8aea5346f09de21c04acce3256775b3b78aeb0","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002addbd6e7f33432871f2c5a0a568b85f4ef6aca3ebc1cfe2810ba33def3f9d355010000006a473044022013ddc4993f9466d220b53ef2716bd53cdb8fdfecb54e69192013cf6c10c8af4802203fff4654ea16c31f313d2013b958974c3e2f57c888351b0b76266ba82d798ff74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffaddbd6e7f33432871f2c5a0a568b85f4ef6aca3ebc1cfe2810ba33def3f9d355020000006a4730440220332a1361aa1a5980796f9c65d901087ce799589e94c5c581664f056dde63e17b022028c5c82bb173c8e51995f4325a0f56ffe1b07be8901ae395facdcc5c19b2371e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914542e59a808fdcd1ab461ec88bef83e0bb9b59d6487d7b34801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3c40bf60","tx_hash":"5b62f5a57c052882ea04c8a1f893ef479cee5450eab735a9d5adcbb182137247","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pp2zukdgpr7u6x45v8kg30hc8c9mndvavs30ttm4v3","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21543847","spent_by_me":"0.21543847","received_by_me":"0.21541847","my_balance_change":"-0.00002000","block_height":1449054,"timestamp":1623147145,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"5b62f5a57c052882ea04c8a1f893ef479cee5450eab735a9d5adcbb182137247","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002addbd6e7f33432871f2c5a0a568b85f4ef6aca3ebc1cfe2810ba33def3f9d355010000006a473044022013ddc4993f9466d220b53ef2716bd53cdb8fdfecb54e69192013cf6c10c8af4802203fff4654ea16c31f313d2013b958974c3e2f57c888351b0b76266ba82d798ff74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffaddbd6e7f33432871f2c5a0a568b85f4ef6aca3ebc1cfe2810ba33def3f9d355020000006a4730440220332a1361aa1a5980796f9c65d901087ce799589e94c5c581664f056dde63e17b022028c5c82bb173c8e51995f4325a0f56ffe1b07be8901ae395facdcc5c19b2371e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914542e59a808fdcd1ab461ec88bef83e0bb9b59d6487d7b34801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3c40bf60","tx_hash":"5b62f5a57c052882ea04c8a1f893ef479cee5450eab735a9d5adcbb182137247","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pp2zukdgpr7u6x45v8kg30hc8c9mndvavs2mvspz7v"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1449054,"timestamp":1623147145,"fee_details":null,"coin":"tBCH","internal_id":"2ed101b6d14b4837dcc630be904fb8985ee7a3fdd8fd26909784fdbfb10cf64a","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002afece59188497a02c4aca4a05c8da14b545fd9ceed94d4f44e74a3c10759a53501000000b7483045022100f00a64f90282a063d887a4a8cbc06adcad7c437f85dcee8c8cd95371bf7664eb02203a5a0a8ccf15c43c0f3f80bb0fa762a975d810937cab93070f9062fdab01954741514c6b6304c71ebf60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821010101010101010101010101010101010101010101010101010101010101010101ac68feffffffafece59188497a02c4aca4a05c8da14b545fd9ceed94d4f44e74a3c10759a535030000006a47304402207b48da2aaae3600782d847b9bdc1a463ac84db39c72c5ed2ccb4ef62547adf21022079736eb9c4554c4e6b323ae78414d54b5122062ac3798b44664210d88fe20af24121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acbfb74801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1821bf60","tx_hash":"55d3f9f3de33ba1028fe1cbc3eca6aeff4858b560a5a2c1f873234f3e7d6dbad","from":["bchtest:pp422sn4fflcuvfvkfaqnrkwl7tjkm75lqwvyk3s9g","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21544847","spent_by_me":"0.21543847","received_by_me":"0.21543847","my_balance_change":"0.00000000","block_height":1449053,"timestamp":1623145925,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"55d3f9f3de33ba1028fe1cbc3eca6aeff4858b560a5a2c1f873234f3e7d6dbad","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002afece59188497a02c4aca4a05c8da14b545fd9ceed94d4f44e74a3c10759a53501000000b7483045022100f00a64f90282a063d887a4a8cbc06adcad7c437f85dcee8c8cd95371bf7664eb02203a5a0a8ccf15c43c0f3f80bb0fa762a975d810937cab93070f9062fdab01954741514c6b6304c71ebf60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821010101010101010101010101010101010101010101010101010101010101010101ac68feffffffafece59188497a02c4aca4a05c8da14b545fd9ceed94d4f44e74a3c10759a535030000006a47304402207b48da2aaae3600782d847b9bdc1a463ac84db39c72c5ed2ccb4ef62547adf21022079736eb9c4554c4e6b323ae78414d54b5122062ac3798b44664210d88fe20af24121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acbfb74801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1821bf60","tx_hash":"55d3f9f3de33ba1028fe1cbc3eca6aeff4858b560a5a2c1f873234f3e7d6dbad","from":["slptest:pp422sn4fflcuvfvkfaqnrkwl7tjkm75lq4crdt8h4"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1449053,"timestamp":1623145925,"fee_details":null,"coin":"tBCH","internal_id":"d68d695eb3338366bb4382effdf295f311651e9f135b36777359f0083a13d661","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000225e1230d5c75a6eee27b9927c6b3157b0863f3877ee2e9a912c2a907664da37f010000006a47304402200960739cd5f8af37a9fd4001c6730d491c1deab1de01f3029c58296457783d240220261b14e3b5dde5d3ea58534425fba176697008ae137ba2540ffc4182616c9fdf4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff25e1230d5c75a6eee27b9927c6b3157b0863f3877ee2e9a912c2a907664da37f020000006b483045022100d9e2be197b9abfc5dd426f6ce38a2549d214897a7b93f4c59cf7ad07a64a0b8802202bb44e708df28d3d6f9eff46ce951b206f1551d605f6dff8dd5d069f3f92e4f04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914dce608c2df96812f8ff8246d9abfec0249204bca878fbf4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace73abf60","tx_hash":"4238cf14335697e3512af0ec047fdcb58de45dbed0d77b031877f0eb47d90ab5","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:prwwvzxzm7tgztu0lqjxmx4laspyjgztegks0enazx","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21546847","spent_by_me":"0.21546847","received_by_me":"0.21544847","my_balance_change":"-0.00002000","block_height":1449053,"timestamp":1623145925,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"4238cf14335697e3512af0ec047fdcb58de45dbed0d77b031877f0eb47d90ab5","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000225e1230d5c75a6eee27b9927c6b3157b0863f3877ee2e9a912c2a907664da37f010000006a47304402200960739cd5f8af37a9fd4001c6730d491c1deab1de01f3029c58296457783d240220261b14e3b5dde5d3ea58534425fba176697008ae137ba2540ffc4182616c9fdf4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff25e1230d5c75a6eee27b9927c6b3157b0863f3877ee2e9a912c2a907664da37f020000006b483045022100d9e2be197b9abfc5dd426f6ce38a2549d214897a7b93f4c59cf7ad07a64a0b8802202bb44e708df28d3d6f9eff46ce951b206f1551d605f6dff8dd5d069f3f92e4f04121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914dce608c2df96812f8ff8246d9abfec0249204bca878fbf4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace73abf60","tx_hash":"4238cf14335697e3512af0ec047fdcb58de45dbed0d77b031877f0eb47d90ab5","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:prwwvzxzm7tgztu0lqjxmx4laspyjgztegdygzf2sm"],"total_amount":"0.1","spent_by_me":"0.1","received_by_me":"0","my_balance_change":"-0.1","block_height":1449053,"timestamp":1623145925,"fee_details":null,"coin":"tBCH","internal_id":"926ec71808b36d4e794d0cc40305fa427eefe8b0dde82d72c4db96f51129656f","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000303897e15b3384ad328adfdf81f2a9c3b3aeca4e7f643eb331bc4afd9f36a28f3020000006a473044022048cc5e2103d3d02092dea7cf75bd1a87b09a0aedef205a35e6a7c83259823b4102203ccaabcd115b0f033c67f21ccbcca87ed5c5323c58278439af3b141e4fcd39d54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb62a5f43d321a4f4e6585e92415747bf38719582b99ea37e0bd3310ce2969a1a020000006b483045022100f4a3148fff695d5cb6839c67a2dcc392e5efc793cf1b7cb952e711a6b6649a4702203c6f675eb30782964e55dead027cdb87cf916f3b717a5aa0599b8ff001011b004121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb50ad947ebf07718037bd7d0be5de48db5dc7f04ecf02a51e397563314cf3842020000006a47304402204681615e42b599e0febd9b8107080903f03be146eefcc07efa243f929360bc2f02202a2afcfe72f45d06e8c48fca98fda5ee8005a6a0a35fa521f1444bf0ebb928574121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710080000000000012cc8e80300000000000017a9146aa542754a7f8e312cb27a098eceff972b6fd4f887e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca7bb4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace73abf60","tx_hash":"35a55907c1a3744ef4d494edced95f544ba18d5ca0a4acc4027a498891e5ecaf","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pp422sn4fflcuvfvkfaqnrkwl7tjkm75lqwvyk3s9g","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21546847","spent_by_me":"0.21546847","received_by_me":"0.21544847","my_balance_change":"-0.00002000","block_height":1449053,"timestamp":1623145925,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"35a55907c1a3744ef4d494edced95f544ba18d5ca0a4acc4027a498891e5ecaf","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000303897e15b3384ad328adfdf81f2a9c3b3aeca4e7f643eb331bc4afd9f36a28f3020000006a473044022048cc5e2103d3d02092dea7cf75bd1a87b09a0aedef205a35e6a7c83259823b4102203ccaabcd115b0f033c67f21ccbcca87ed5c5323c58278439af3b141e4fcd39d54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb62a5f43d321a4f4e6585e92415747bf38719582b99ea37e0bd3310ce2969a1a020000006b483045022100f4a3148fff695d5cb6839c67a2dcc392e5efc793cf1b7cb952e711a6b6649a4702203c6f675eb30782964e55dead027cdb87cf916f3b717a5aa0599b8ff001011b004121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb50ad947ebf07718037bd7d0be5de48db5dc7f04ecf02a51e397563314cf3842020000006a47304402204681615e42b599e0febd9b8107080903f03be146eefcc07efa243f929360bc2f02202a2afcfe72f45d06e8c48fca98fda5ee8005a6a0a35fa521f1444bf0ebb928574121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710080000000000012cc8e80300000000000017a9146aa542754a7f8e312cb27a098eceff972b6fd4f887e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88aca7bb4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace73abf60","tx_hash":"35a55907c1a3744ef4d494edced95f544ba18d5ca0a4acc4027a498891e5ecaf","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pp422sn4fflcuvfvkfaqnrkwl7tjkm75lq4crdt8h4","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"8.7","spent_by_me":"8.7","received_by_me":"7.7","my_balance_change":"-1.0","block_height":1449053,"timestamp":1623145925,"fee_details":null,"coin":"tBCH","internal_id":"6c2441a91cac06cb087ae55d216d52cbc6f8c4e47a955dc32829814325c64653","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002c26e8b5503cf67379fdf81adf2747ffafff43d50b746d19ca7e39612fc05ac92010000006a4730440220033a209e7072eaa46bed21a7d27c4d45f38f068ca855883bbc568dc89cc2fd15022053165e8b959535c5aa269794c18bf617e43499166ae2bf0d36d5ec2c1f2390204121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffc26e8b5503cf67379fdf81adf2747ffafff43d50b746d19ca7e39612fc05ac92020000006a47304402207bac287b5a8f6d2bd3ecfac57b7a992c56f8303fe89130efb4b42cb2472d92ff02206f196c89b7b1cbd9bd52a7c3f584d27871cd290c74c30c63f1fffb4052d542614121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a91469a0891adb04109c8d31e9fc6f27edc41e87206c875fc74801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac221dbe60","tx_hash":"b0dde35f1a1b9c98dd775c03e46afdf8072dcc68ad36cac7148ad9d2048d016c","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pp56pzg6mvzpp8ydx85lcme8ahzpapeqds2lhdxyta","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21548847","spent_by_me":"0.21548847","received_by_me":"0.21546847","my_balance_change":"-0.00002000","block_height":1448993,"timestamp":1623072673,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"b0dde35f1a1b9c98dd775c03e46afdf8072dcc68ad36cac7148ad9d2048d016c","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002c26e8b5503cf67379fdf81adf2747ffafff43d50b746d19ca7e39612fc05ac92010000006a4730440220033a209e7072eaa46bed21a7d27c4d45f38f068ca855883bbc568dc89cc2fd15022053165e8b959535c5aa269794c18bf617e43499166ae2bf0d36d5ec2c1f2390204121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffc26e8b5503cf67379fdf81adf2747ffafff43d50b746d19ca7e39612fc05ac92020000006a47304402207bac287b5a8f6d2bd3ecfac57b7a992c56f8303fe89130efb4b42cb2472d92ff02206f196c89b7b1cbd9bd52a7c3f584d27871cd290c74c30c63f1fffb4052d542614121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a91469a0891adb04109c8d31e9fc6f27edc41e87206c875fc74801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac221dbe60","tx_hash":"b0dde35f1a1b9c98dd775c03e46afdf8072dcc68ad36cac7148ad9d2048d016c","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pp56pzg6mvzpp8ydx85lcme8ahzpapeqds3tskuneq"],"total_amount":"0.1","spent_by_me":"0.1","received_by_me":"0","my_balance_change":"-0.1","block_height":1448993,"timestamp":1623072673,"fee_details":null,"coin":"tBCH","internal_id":"db5de208cc0504197c0314d2c65e3ac3d3f18e40b9e5ca96d5323652a768326f","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002e98601b61821a5369f9e5d74bed4ae624166ce77df427a8e420ddc46e2666a8801000000d74730440220413afe0ff2672516caae61ee09156ba2f09098c71c7a0c019bca7e15fcee59a8022027846c0fd3a1df05dc95cf28d81cc46b1e8b5783bcac758e1fc4b0e2bc786dfd41200000000000000000000000000000000000000000000000000000000000000000004c6b63049f1cbe60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffffe98601b61821a5369f9e5d74bed4ae624166ce77df427a8e420ddc46e2666a88020000006b483045022100eb6044621a663634bf559dbea3d0ecf9495cdcc833c7da3faba5cef2612d16c1022056215d69ed28c10b50f531e3bfeb79f751ede2ac5086d398d1ee91768e7bf6274121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac47cb4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f1cbe60","tx_hash":"92ac05fc1296e3a79cd146b7503df4fffa7f74f2ad81df9f3767cf03558b6ec2","from":["bchtest:pzj6q9rrdzr9wlrvlqznt8cuzlf7gfhlhc04n8kkkw","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21549847","spent_by_me":"0.21548847","received_by_me":"0.21548847","my_balance_change":"0.00000000","block_height":1448993,"timestamp":1623072673,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"92ac05fc1296e3a79cd146b7503df4fffa7f74f2ad81df9f3767cf03558b6ec2","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002e98601b61821a5369f9e5d74bed4ae624166ce77df427a8e420ddc46e2666a8801000000d74730440220413afe0ff2672516caae61ee09156ba2f09098c71c7a0c019bca7e15fcee59a8022027846c0fd3a1df05dc95cf28d81cc46b1e8b5783bcac758e1fc4b0e2bc786dfd41200000000000000000000000000000000000000000000000000000000000000000004c6b63049f1cbe60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffffe98601b61821a5369f9e5d74bed4ae624166ce77df427a8e420ddc46e2666a88020000006b483045022100eb6044621a663634bf559dbea3d0ecf9495cdcc833c7da3faba5cef2612d16c1022056215d69ed28c10b50f531e3bfeb79f751ede2ac5086d398d1ee91768e7bf6274121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac47cb4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f1cbe60","tx_hash":"92ac05fc1296e3a79cd146b7503df4fffa7f74f2ad81df9f3767cf03558b6ec2","from":["slptest:pzj6q9rrdzr9wlrvlqznt8cuzlf7gfhlhc5p5uvpyn"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1","spent_by_me":"0","received_by_me":"0.1","my_balance_change":"0.1","block_height":1448993,"timestamp":1623072673,"fee_details":null,"coin":"tBCH","internal_id":"2f16d6629b3da90140cb3bce4fc1149add1a5d43f2bda8ecb18032e909d64061","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000238edeb1b3f37607ef191934d0b4f56f98faa05df894aead091313c3d08289b0b010000006b483045022100c42500ba0e9275de19c2b48f38680a86fc7f0e7ec3e4597797fe2829a954b50c02204c456f45c54ab89714ded72cbd4b88679d09220f928b9eb85c16c75da89b287c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff38edeb1b3f37607ef191934d0b4f56f98faa05df894aead091313c3d08289b0b020000006b483045022100e83e62bb7500a163bb49a938aedf6bb8ba2d181732f4dd6b3503996299a6223b022025c6962467ea069fb28b3dc0473b448fe60c743ef60fdb365f4c72d87fa1512e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914a5a014636886577c6cf805359f1c17d3e426ffbe872fcf4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f1cbe60","tx_hash":"886a66e246dc0d428e7a42df77ce664162aed4be745d9e9f36a52118b60186e9","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzj6q9rrdzr9wlrvlqznt8cuzlf7gfhlhc04n8kkkw","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21550847","spent_by_me":"0.21550847","received_by_me":"0.21548847","my_balance_change":"-0.00002000","block_height":1448993,"timestamp":1623072673,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"886a66e246dc0d428e7a42df77ce664162aed4be745d9e9f36a52118b60186e9","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000238edeb1b3f37607ef191934d0b4f56f98faa05df894aead091313c3d08289b0b010000006b483045022100c42500ba0e9275de19c2b48f38680a86fc7f0e7ec3e4597797fe2829a954b50c02204c456f45c54ab89714ded72cbd4b88679d09220f928b9eb85c16c75da89b287c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff38edeb1b3f37607ef191934d0b4f56f98faa05df894aead091313c3d08289b0b020000006b483045022100e83e62bb7500a163bb49a938aedf6bb8ba2d181732f4dd6b3503996299a6223b022025c6962467ea069fb28b3dc0473b448fe60c743ef60fdb365f4c72d87fa1512e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914a5a014636886577c6cf805359f1c17d3e426ffbe872fcf4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f1cbe60","tx_hash":"886a66e246dc0d428e7a42df77ce664162aed4be745d9e9f36a52118b60186e9","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzj6q9rrdzr9wlrvlqznt8cuzlf7gfhlhc5p5uvpyn"],"total_amount":"0.1","spent_by_me":"0.1","received_by_me":"0","my_balance_change":"-0.1","block_height":1448993,"timestamp":1623072673,"fee_details":null,"coin":"tBCH","internal_id":"0f28bec12eadfbf239b019f2314ef9bf3d664d6ba0299ecf2c4b25fe853e0f84","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000026c018d04d2d98a14c7ca36ad68cc2d07f8fd6ae4035c77dd989c1b1a5fe3ddb001000000d8483045022100ac5ecbb037892a26e296c1e4c97f8e4b51cb46877a921248ca3f2af06d49d3a102202d70e0afc20c0e81ca18a54b125a676a3e67c892c6e817ae9f279ed0681e15d941200000000000000000000000000000000000000000000000000000000000000000004c6b6304211dbe60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff6c018d04d2d98a14c7ca36ad68cc2d07f8fd6ae4035c77dd989c1b1a5fe3ddb0020000006a47304402201eaa2b3c2a074cafd734a9deec5f649dcdef044f0cba1a1a3d30939cf024bd37022024927f9e61ec3f260fe496e4afbd3e6e262861dfe255305c9c86fd89409b464a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac77c34801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac211dbe60","tx_hash":"7fa34d6607a9c212a9e9e27e87f363087b15b3c627997be2eea6755c0d23e125","from":["bchtest:pp56pzg6mvzpp8ydx85lcme8ahzpapeqds2lhdxyta","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21547847","spent_by_me":"0.21546847","received_by_me":"0.21546847","my_balance_change":"0.00000000","block_height":1448993,"timestamp":1623072673,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"7fa34d6607a9c212a9e9e27e87f363087b15b3c627997be2eea6755c0d23e125","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000026c018d04d2d98a14c7ca36ad68cc2d07f8fd6ae4035c77dd989c1b1a5fe3ddb001000000d8483045022100ac5ecbb037892a26e296c1e4c97f8e4b51cb46877a921248ca3f2af06d49d3a102202d70e0afc20c0e81ca18a54b125a676a3e67c892c6e817ae9f279ed0681e15d941200000000000000000000000000000000000000000000000000000000000000000004c6b6304211dbe60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff6c018d04d2d98a14c7ca36ad68cc2d07f8fd6ae4035c77dd989c1b1a5fe3ddb0020000006a47304402201eaa2b3c2a074cafd734a9deec5f649dcdef044f0cba1a1a3d30939cf024bd37022024927f9e61ec3f260fe496e4afbd3e6e262861dfe255305c9c86fd89409b464a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac77c34801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac211dbe60","tx_hash":"7fa34d6607a9c212a9e9e27e87f363087b15b3c627997be2eea6755c0d23e125","from":["slptest:pp56pzg6mvzpp8ydx85lcme8ahzpapeqds3tskuneq"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1","spent_by_me":"0","received_by_me":"0.1","my_balance_change":"0.1","block_height":1448993,"timestamp":1623072673,"fee_details":null,"coin":"tBCH","internal_id":"280b66943031c696ec5917538f12140e4cc739442d5caf51c4ee665f02648150","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002030e0d7566fc734f1cb9e7eed0fe75fd5804e53c05a2252cd97cb67337479952020000006b483045022100acdc3c9c95ac4201ffac479ba605aca31473b28035e2111f4ddcd2a48459eb89022004d5a2846a2474a2ad7883253ba3eef61b78f8e4e9060ced5baa5b7a55a40a3f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030e0d7566fc734f1cb9e7eed0fe75fd5804e53c05a2252cd97cb67337479952030000006a473044022027e720db123438e86d6c22b039aea73a5ed861d1dd8511c745bdb73a07267f5402201f39d23b1f68cc56286b3fffe38d1f7bb60e560c8635a4aae08f3ce1e4e37e914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000001f40e80300000000000017a9143dc0ff80ccbabd473d3837493304b02ee07a179f87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acffd64801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4e17be60","tx_hash":"f3286af3d9afc41b33eb43f6e7a4ec3a3b9c2a1ff8fdad28d34a38b3157e8903","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pq7upluqejat63ea8qm5jvcykqhwq7shnu2zklyu30","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21553847","spent_by_me":"0.21553847","received_by_me":"0.21551847","my_balance_change":"-0.00002000","block_height":1448992,"timestamp":1623071467,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"f3286af3d9afc41b33eb43f6e7a4ec3a3b9c2a1ff8fdad28d34a38b3157e8903","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002030e0d7566fc734f1cb9e7eed0fe75fd5804e53c05a2252cd97cb67337479952020000006b483045022100acdc3c9c95ac4201ffac479ba605aca31473b28035e2111f4ddcd2a48459eb89022004d5a2846a2474a2ad7883253ba3eef61b78f8e4e9060ced5baa5b7a55a40a3f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030e0d7566fc734f1cb9e7eed0fe75fd5804e53c05a2252cd97cb67337479952030000006a473044022027e720db123438e86d6c22b039aea73a5ed861d1dd8511c745bdb73a07267f5402201f39d23b1f68cc56286b3fffe38d1f7bb60e560c8635a4aae08f3ce1e4e37e914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000001f40e80300000000000017a9143dc0ff80ccbabd473d3837493304b02ee07a179f87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acffd64801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4e17be60","tx_hash":"f3286af3d9afc41b33eb43f6e7a4ec3a3b9c2a1ff8fdad28d34a38b3157e8903","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pq7upluqejat63ea8qm5jvcykqhwq7shnu3k3y7trj","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.9","spent_by_me":"0.9","received_by_me":"0.8","my_balance_change":"-0.1","block_height":1448992,"timestamp":1623071467,"fee_details":null,"coin":"tBCH","internal_id":"a37998be419f60124b0f181de9f007a6de8af97acbe1f9a2205bdec4af09c4f2","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002473519679e2222927e2e4b500a9d6b4a16ce7ac6aa8d888c0888a5a47ff2c572010000006b483045022100b84086ebca7e9feedf83f1808e6126d43ff806cf027988150562bcf4f5857db40220231932caf6c852847879d77d01f94bb18e5e54005865c449ccb2b88fe77ec2b44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff473519679e2222927e2e4b500a9d6b4a16ce7ac6aa8d888c0888a5a47ff2c572020000006a473044022043ca455e96018b72aecd8279af8aba38f0a703fd6aae1c35ac10d20b953125b302203a1e5ce6c476f67c026ce9763d8847feceb7712bb5e6775bd64e74aaf6cce5a34121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000002328e80300000000000017a9148d3b2f3ede519beeb220fbd67ca9207f8b8652f487e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88accfde4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acf316be60","tx_hash":"5299473773b67cd92c25a2053ce50458fd75fed0eee7b91c4f73fc66750d0e03","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzxnkte7megehm4jyraavl9fyplchpjj7s8d9hpss3","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21555847","spent_by_me":"0.21555847","received_by_me":"0.21553847","my_balance_change":"-0.00002000","block_height":1448992,"timestamp":1623071467,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"5299473773b67cd92c25a2053ce50458fd75fed0eee7b91c4f73fc66750d0e03","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002473519679e2222927e2e4b500a9d6b4a16ce7ac6aa8d888c0888a5a47ff2c572010000006b483045022100b84086ebca7e9feedf83f1808e6126d43ff806cf027988150562bcf4f5857db40220231932caf6c852847879d77d01f94bb18e5e54005865c449ccb2b88fe77ec2b44121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff473519679e2222927e2e4b500a9d6b4a16ce7ac6aa8d888c0888a5a47ff2c572020000006a473044022043ca455e96018b72aecd8279af8aba38f0a703fd6aae1c35ac10d20b953125b302203a1e5ce6c476f67c026ce9763d8847feceb7712bb5e6775bd64e74aaf6cce5a34121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000002328e80300000000000017a9148d3b2f3ede519beeb220fbd67ca9207f8b8652f487e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88accfde4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acf316be60","tx_hash":"5299473773b67cd92c25a2053ce50458fd75fed0eee7b91c4f73fc66750d0e03","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzxnkte7megehm4jyraavl9fyplchpjj7suezvm8zv","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"1","received_by_me":"0.9","my_balance_change":"-0.1","block_height":1448992,"timestamp":1623071467,"fee_details":null,"coin":"tBCH","internal_id":"74177c389de71ea31757e5cf1f99741ebc103b340c135ba6944d5b28d872d21e","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000203897e15b3384ad328adfdf81f2a9c3b3aeca4e7f643eb331bc4afd9f36a28f301000000d8483045022100e079f6b6d627fe58ab7454e5501623cc2a205ec6b77ccb6e18ac468c13f7e090022038f39424c06a5b8ae80a58dfa197839050cb1e10985fb2ca31a202387ddb530b41200000000000000000000000000000000000000000000000000000000000000000004c6b63044e17be60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff03897e15b3384ad328adfdf81f2a9c3b3aeca4e7f643eb331bc4afd9f36a28f3030000006b483045022100b38320d57ce4bd33b4ec2aa3a3471aa063677d3e4f768ec532271e66df81334e02202a54df0d82c7a1efd57b853c29390a5a12a02dce935d3ad0d11a0b0b7739bee64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac17d34801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4e17be60","tx_hash":"0b9b28083d3c3191d0ea4a89df05aa8ff9564f0b4d9391f17e60373f1bebed38","from":["bchtest:pq7upluqejat63ea8qm5jvcykqhwq7shnu2zklyu30","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21551847","spent_by_me":"0.21550847","received_by_me":"0.21550847","my_balance_change":"0.00000000","block_height":1448992,"timestamp":1623071467,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"0b9b28083d3c3191d0ea4a89df05aa8ff9564f0b4d9391f17e60373f1bebed38","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000203897e15b3384ad328adfdf81f2a9c3b3aeca4e7f643eb331bc4afd9f36a28f301000000d8483045022100e079f6b6d627fe58ab7454e5501623cc2a205ec6b77ccb6e18ac468c13f7e090022038f39424c06a5b8ae80a58dfa197839050cb1e10985fb2ca31a202387ddb530b41200000000000000000000000000000000000000000000000000000000000000000004c6b63044e17be60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff03897e15b3384ad328adfdf81f2a9c3b3aeca4e7f643eb331bc4afd9f36a28f3030000006b483045022100b38320d57ce4bd33b4ec2aa3a3471aa063677d3e4f768ec532271e66df81334e02202a54df0d82c7a1efd57b853c29390a5a12a02dce935d3ad0d11a0b0b7739bee64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac17d34801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4e17be60","tx_hash":"0b9b28083d3c3191d0ea4a89df05aa8ff9564f0b4d9391f17e60373f1bebed38","from":["slptest:pq7upluqejat63ea8qm5jvcykqhwq7shnu3k3y7trj"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"0.1","spent_by_me":"0","received_by_me":"0.1","my_balance_change":"0.1","block_height":1448992,"timestamp":1623071467,"fee_details":null,"coin":"tBCH","internal_id":"76c92ad50b78118449cb01575ee8535b32703b56d72badfe90db45d855831bb8","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000024c40732416a3bf28aca49fdeaa3c74e4c37d44db9a1a8cab830fbd751284560a01000000d7473044022016083f68dae0cbe440d4c674ca3db9010e7cb0485f86a569ac5cf717d65f746c02202669fe140ba642a71a7fd15f3dc5c59d2f757ca12e49bb928ca695750d80bbc541200000000000000000000000000000000000000000000000000000000000000000004c6b6304240ebe60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff4c40732416a3bf28aca49fdeaa3c74e4c37d44db9a1a8cab830fbd751284560a020000006b483045022100afc972c131187f3dc00df05c07164499e5b587a1adc880fb3394d02b1c2df831022018dc3e7da4113e98ac847a9d1d372ebdf0895de8835c803547e41011e05ce0914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9fe64801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac240ebe60","tx_hash":"72c5f27fa4a588088c888daac67ace164a6b9d0a504b2e7e9222229e67193547","from":["bchtest:pzaaxmwyk9x6vfkk5y9vck2dk88vr5jd6yts8zusej","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21556847","spent_by_me":"0.21555847","received_by_me":"0.21555847","my_balance_change":"0.00000000","block_height":1448990,"timestamp":1623069027,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"72c5f27fa4a588088c888daac67ace164a6b9d0a504b2e7e9222229e67193547","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000024c40732416a3bf28aca49fdeaa3c74e4c37d44db9a1a8cab830fbd751284560a01000000d7473044022016083f68dae0cbe440d4c674ca3db9010e7cb0485f86a569ac5cf717d65f746c02202669fe140ba642a71a7fd15f3dc5c59d2f757ca12e49bb928ca695750d80bbc541200000000000000000000000000000000000000000000000000000000000000000004c6b6304240ebe60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff4c40732416a3bf28aca49fdeaa3c74e4c37d44db9a1a8cab830fbd751284560a020000006b483045022100afc972c131187f3dc00df05c07164499e5b587a1adc880fb3394d02b1c2df831022018dc3e7da4113e98ac847a9d1d372ebdf0895de8835c803547e41011e05ce0914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9fe64801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac240ebe60","tx_hash":"72c5f27fa4a588088c888daac67ace164a6b9d0a504b2e7e9222229e67193547","from":["slptest:pzaaxmwyk9x6vfkk5y9vck2dk88vr5jd6ysyqex8t0"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448990,"timestamp":1623069027,"fee_details":null,"coin":"tBCH","internal_id":"6fd296ec8afb8f471733829c61ad3338c58e66b568c2495bc10ba01c59d017e2","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000028d372281608c372e656dfb6c0e08605552a59fb63dc17cd15f63101904efe874010000006b483045022100be47ac47975dc15e952d822dfebae2fffd8a9a8d88b31700150fd199604c52ee02206e5405c8d173cf345485ce580800e040f0700f4ab3c73f689caf4705010497374121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff8d372281608c372e656dfb6c0e08605552a59fb63dc17cd15f63101904efe874020000006b483045022100cb771d6df08b9a1b1c29ba13bd7ef73f274df3f52de89845a2701a203b8b02e0022041444bf0f2ef0311511df4b65461880aba5c8108557050cc7e4dc68a4aa43edf4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914bbd36dc4b14da626d6a10acc594db1cec1d24dd18787ea4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac250ebe60","tx_hash":"0a56841275bd0f83ab8c1a9adb447dc3e4743caade9fa4ac28bfa3162473404c","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzaaxmwyk9x6vfkk5y9vck2dk88vr5jd6yts8zusej","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21557847","spent_by_me":"0.21557847","received_by_me":"0.21555847","my_balance_change":"-0.00002000","block_height":1448990,"timestamp":1623069027,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"0a56841275bd0f83ab8c1a9adb447dc3e4743caade9fa4ac28bfa3162473404c","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000028d372281608c372e656dfb6c0e08605552a59fb63dc17cd15f63101904efe874010000006b483045022100be47ac47975dc15e952d822dfebae2fffd8a9a8d88b31700150fd199604c52ee02206e5405c8d173cf345485ce580800e040f0700f4ab3c73f689caf4705010497374121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff8d372281608c372e656dfb6c0e08605552a59fb63dc17cd15f63101904efe874020000006b483045022100cb771d6df08b9a1b1c29ba13bd7ef73f274df3f52de89845a2701a203b8b02e0022041444bf0f2ef0311511df4b65461880aba5c8108557050cc7e4dc68a4aa43edf4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914bbd36dc4b14da626d6a10acc594db1cec1d24dd18787ea4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac250ebe60","tx_hash":"0a56841275bd0f83ab8c1a9adb447dc3e4743caade9fa4ac28bfa3162473404c","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzaaxmwyk9x6vfkk5y9vck2dk88vr5jd6ysyqex8t0"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448990,"timestamp":1623069027,"fee_details":null,"coin":"tBCH","internal_id":"de700a925438bd828eb789ce0da9243fa79bef20405d7d34328ef1ea9fb1ad14","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000020ced642178dd794991b49e680e3a1b7292b657b5f4496515b0f15008073da81701000000d747304402201133296646156e6de500f5d56b7c00ccfdbc3928b2bdd5646c8e4c16196e410f02205133ddf69d72c5db416c1d447bfd80f90d1d827bed80fe16b934212b9d41785041200000000000000000000000000000000000000000000000000000000000000000004c6b63049a0bbe60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff0ced642178dd794991b49e680e3a1b7292b657b5f4496515b0f15008073da817020000006b483045022100db1eb4b1cd1b606f131ac77ef0e4c5c24401e03e1d95d8b9f01c9dbace485bad02203b0281aba051f7ac2e1fece64c499b78bb928efff589d7286df6f605b14086204121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6fee4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9a0bbe60","tx_hash":"74e8ef041910635fd17cc13db69fa5525560080e6cfb6d652e378c608122378d","from":["bchtest:ppfk6n4rhvnfyxemegc7whsxmuqqcgc9g502thwzj7","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21558847","spent_by_me":"0.21557847","received_by_me":"0.21557847","my_balance_change":"0.00000000","block_height":1448989,"timestamp":1623067807,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"74e8ef041910635fd17cc13db69fa5525560080e6cfb6d652e378c608122378d","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000020ced642178dd794991b49e680e3a1b7292b657b5f4496515b0f15008073da81701000000d747304402201133296646156e6de500f5d56b7c00ccfdbc3928b2bdd5646c8e4c16196e410f02205133ddf69d72c5db416c1d447bfd80f90d1d827bed80fe16b934212b9d41785041200000000000000000000000000000000000000000000000000000000000000000004c6b63049a0bbe60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff0ced642178dd794991b49e680e3a1b7292b657b5f4496515b0f15008073da817020000006b483045022100db1eb4b1cd1b606f131ac77ef0e4c5c24401e03e1d95d8b9f01c9dbace485bad02203b0281aba051f7ac2e1fece64c499b78bb928efff589d7286df6f605b14086204121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6fee4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9a0bbe60","tx_hash":"74e8ef041910635fd17cc13db69fa5525560080e6cfb6d652e378c608122378d","from":["slptest:ppfk6n4rhvnfyxemegc7whsxmuqqcgc9g557vv54qr"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448989,"timestamp":1623067807,"fee_details":null,"coin":"tBCH","internal_id":"ebc8206ac816600285c0cc362e31a8bfaac3df56d5fccd053bf37d5953011e87","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000021c213665e6720e83cbf8eb2c9c57669e40f404b848e21627ef02a7fbc061c60e010000006a47304402201238eee290c2a687b48b4f4d2568b97c37e19b7e5ca2a2fb567895b7448d00c602203c3bc584f4bc77d57b9801067ee6f3fbc766ecd79f64bbb153fde68ed33bef734121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff1c213665e6720e83cbf8eb2c9c57669e40f404b848e21627ef02a7fbc061c60e020000006a473044022015fe771d1ec5cc545c41e52fbd278ab32a8a89c69013db52d2523e5e38228a4a022008d25953117ca9cfe8bf5ecf406b3e9a7dd34f6c615d6b7b3d5c71d57ac569a14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914536d4ea3bb26921b3bca31e75e06df000c2305458757f24801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9a0bbe60","tx_hash":"17a83d070850f1b0156549f4b557b692721b3a0e689eb4914979dd782164ed0c","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:ppfk6n4rhvnfyxemegc7whsxmuqqcgc9g502thwzj7","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21559847","spent_by_me":"0.21559847","received_by_me":"0.21557847","my_balance_change":"-0.00002000","block_height":1448989,"timestamp":1623067807,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"17a83d070850f1b0156549f4b557b692721b3a0e689eb4914979dd782164ed0c","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000021c213665e6720e83cbf8eb2c9c57669e40f404b848e21627ef02a7fbc061c60e010000006a47304402201238eee290c2a687b48b4f4d2568b97c37e19b7e5ca2a2fb567895b7448d00c602203c3bc584f4bc77d57b9801067ee6f3fbc766ecd79f64bbb153fde68ed33bef734121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff1c213665e6720e83cbf8eb2c9c57669e40f404b848e21627ef02a7fbc061c60e020000006a473044022015fe771d1ec5cc545c41e52fbd278ab32a8a89c69013db52d2523e5e38228a4a022008d25953117ca9cfe8bf5ecf406b3e9a7dd34f6c615d6b7b3d5c71d57ac569a14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914536d4ea3bb26921b3bca31e75e06df000c2305458757f24801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9a0bbe60","tx_hash":"17a83d070850f1b0156549f4b557b692721b3a0e689eb4914979dd782164ed0c","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:ppfk6n4rhvnfyxemegc7whsxmuqqcgc9g557vv54qr"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448989,"timestamp":1623067807,"fee_details":null,"coin":"tBCH","internal_id":"e6a9f22657479057367138f2c5668140033b7cb090338f91fa6ed858119c075f","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002bec21db1d300d9ef27d50618842e958b8137723e29cea3864716a8348e2ab566010000006a47304402206a8f3705675d6ee112a64611a70830eeb60a6674cbb17b6c322e827077b6f7a50220520bb3454ecd4174939b0e41d661ce6eea5dbae36ad65e1ae4f9c4dd568d1a914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffbec21db1d300d9ef27d50618842e958b8137723e29cea3864716a8348e2ab566020000006a47304402205188db5d2e9bade41b204fd7e7a244aac4cba6d276c5671ee87ad2afa65d5df00220442cf7f68492597303de5ba6d5b77666eb673374ec6ca2b887a7be1cbba268e64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914746fc9a5f758c8a422484f24b45f538cc0e26d6a8727fa4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb8ffbd60","tx_hash":"8df08337cb7755ee1d9d7e4f0015b3dce123e8db9cb04c9adf11d753fb3a6891","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pp6xljd97avv3fpzfp8jfdzl2wxvpcnddgkk85xs4n","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21561847","spent_by_me":"0.21561847","received_by_me":"0.21559847","my_balance_change":"-0.00002000","block_height":1448987,"timestamp":1623065357,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"8df08337cb7755ee1d9d7e4f0015b3dce123e8db9cb04c9adf11d753fb3a6891","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002bec21db1d300d9ef27d50618842e958b8137723e29cea3864716a8348e2ab566010000006a47304402206a8f3705675d6ee112a64611a70830eeb60a6674cbb17b6c322e827077b6f7a50220520bb3454ecd4174939b0e41d661ce6eea5dbae36ad65e1ae4f9c4dd568d1a914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffbec21db1d300d9ef27d50618842e958b8137723e29cea3864716a8348e2ab566020000006a47304402205188db5d2e9bade41b204fd7e7a244aac4cba6d276c5671ee87ad2afa65d5df00220442cf7f68492597303de5ba6d5b77666eb673374ec6ca2b887a7be1cbba268e64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914746fc9a5f758c8a422484f24b45f538cc0e26d6a8727fa4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb8ffbd60","tx_hash":"8df08337cb7755ee1d9d7e4f0015b3dce123e8db9cb04c9adf11d753fb3a6891","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pp6xljd97avv3fpzfp8jfdzl2wxvpcnddgdzq0u88w"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448987,"timestamp":1623065357,"fee_details":null,"coin":"tBCH","internal_id":"29e5a4ee599d058989e0aa3ea35963f039e6e505a01bd9140238fad1fb358a19","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000291683afb53d711df9a4cb09cdbe823e1dcb315004f7e9d1dee5577cb3783f08d01000000d84830450221008ab8f20ca1d670c169baec53044c51062539e4d9b144ff6a1019ad43497f9b8e02204b7081af2f26779b61368ec4f90157b2e4ed073c46c50b3080234bbab0e73c1a41200000000000000000000000000000000000000000000000000000000000000000004c6b6304b7ffbd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff91683afb53d711df9a4cb09cdbe823e1dcb315004f7e9d1dee5577cb3783f08d020000006b48304502210093e395ebd62a80a756476069124b41cc33adbfcc9a3961cd7aaee9843649593d02207fc1b1a16cebf63e2fb6f85375c7ee6de07f266d1239f69a64c0adbd7fc15be74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3ff64801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb7ffbd60","tx_hash":"0ec661c0fba702ef2716e248b804f4409e66579c2cebf8cb830e72e66536211c","from":["bchtest:pp6xljd97avv3fpzfp8jfdzl2wxvpcnddgkk85xs4n","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21560847","spent_by_me":"0.21559847","received_by_me":"0.21559847","my_balance_change":"0.00000000","block_height":1448987,"timestamp":1623065357,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"0ec661c0fba702ef2716e248b804f4409e66579c2cebf8cb830e72e66536211c","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000291683afb53d711df9a4cb09cdbe823e1dcb315004f7e9d1dee5577cb3783f08d01000000d84830450221008ab8f20ca1d670c169baec53044c51062539e4d9b144ff6a1019ad43497f9b8e02204b7081af2f26779b61368ec4f90157b2e4ed073c46c50b3080234bbab0e73c1a41200000000000000000000000000000000000000000000000000000000000000000004c6b6304b7ffbd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff91683afb53d711df9a4cb09cdbe823e1dcb315004f7e9d1dee5577cb3783f08d020000006b48304502210093e395ebd62a80a756476069124b41cc33adbfcc9a3961cd7aaee9843649593d02207fc1b1a16cebf63e2fb6f85375c7ee6de07f266d1239f69a64c0adbd7fc15be74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3ff64801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb7ffbd60","tx_hash":"0ec661c0fba702ef2716e248b804f4409e66579c2cebf8cb830e72e66536211c","from":["slptest:pp6xljd97avv3fpzfp8jfdzl2wxvpcnddgdzq0u88w"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448987,"timestamp":1623065357,"fee_details":null,"coin":"tBCH","internal_id":"5369720668b62322dc28e81b6dcfe0e02f766376015ce8ff903955a867ceebaf","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002e8cb661f49a212f4a7daa46bb5bba024859dd520767e1e6b7b8dbcee4c843695010000006a47304402203384a1aa2e3366006ed1460541b84c9b3790f0b16b2ea1611d470bc646d2a27a022062633c7ffe5286ef56d83468a0d39e180a0a001723dd76a75d364bf19f697db64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffe8cb661f49a212f4a7daa46bb5bba024859dd520767e1e6b7b8dbcee4c843695020000006a473044022075372ba8c37fdd8a9ecfe3d821b90893d0fb3095284e928e9f75751fd492fb9a02202405dbbfffec129afe195c224b37ce0179e5ad999d65af37f8d9705745c8302b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914460dd524ea15822352e3e817380114f96ddbdd6987f7014901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acebf1bd60","tx_hash":"8dbcd953732a1f5ab474d96913bc565858c60fb96cdea1be3d88f85cd335ade7","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pprqm4fyag2cyg6ju05pwwqpznukmk7adypch6rq6t","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21563847","spent_by_me":"0.21563847","received_by_me":"0.21561847","my_balance_change":"-0.00002000","block_height":1448984,"timestamp":1623061716,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"8dbcd953732a1f5ab474d96913bc565858c60fb96cdea1be3d88f85cd335ade7","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002e8cb661f49a212f4a7daa46bb5bba024859dd520767e1e6b7b8dbcee4c843695010000006a47304402203384a1aa2e3366006ed1460541b84c9b3790f0b16b2ea1611d470bc646d2a27a022062633c7ffe5286ef56d83468a0d39e180a0a001723dd76a75d364bf19f697db64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffe8cb661f49a212f4a7daa46bb5bba024859dd520767e1e6b7b8dbcee4c843695020000006a473044022075372ba8c37fdd8a9ecfe3d821b90893d0fb3095284e928e9f75751fd492fb9a02202405dbbfffec129afe195c224b37ce0179e5ad999d65af37f8d9705745c8302b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914460dd524ea15822352e3e817380114f96ddbdd6987f7014901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acebf1bd60","tx_hash":"8dbcd953732a1f5ab474d96913bc565858c60fb96cdea1be3d88f85cd335ade7","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pprqm4fyag2cyg6ju05pwwqpznukmk7ady6vspehgk"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448984,"timestamp":1623061716,"fee_details":null,"coin":"tBCH","internal_id":"d17b4dcf7a76093fb6043b2d3b689b3dcd6d96f563a986b44c25b6c808634982","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002e7ad35d35cf8883dbea1de6cb90fc6585856bc1369d974b45a1f2a7353d9bc8d01000000d7473044022006fc5db60f583c351d96f5844025018d8b65a39cff8c6d13c54f2a5d338c74af0220324b0a60acc95cbf8bf5d708d6e5e649da291d8163d967dcdbb1f382c8cbac7941200000000000000000000000000000000000000000000000000000000000000000004c6b6304ebf1bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffffe7ad35d35cf8883dbea1de6cb90fc6585856bc1369d974b45a1f2a7353d9bc8d020000006b483045022100e7d8a8e7b3169a2e4f7c3955a8818e13206e9eb2b870e870fde814a9b96e5d0402204141202de05dcf0c491f3118b4a3dfb125cdd21b8760cf82383dabe1691787a34121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0ffe4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acebf1bd60","tx_hash":"66b52a8e34a8164786a3ce293e7237818b952e841806d527efd900d3b11dc2be","from":["bchtest:pprqm4fyag2cyg6ju05pwwqpznukmk7adypch6rq6t","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21562847","spent_by_me":"0.21561847","received_by_me":"0.21561847","my_balance_change":"0.00000000","block_height":1448984,"timestamp":1623061716,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"66b52a8e34a8164786a3ce293e7237818b952e841806d527efd900d3b11dc2be","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002e7ad35d35cf8883dbea1de6cb90fc6585856bc1369d974b45a1f2a7353d9bc8d01000000d7473044022006fc5db60f583c351d96f5844025018d8b65a39cff8c6d13c54f2a5d338c74af0220324b0a60acc95cbf8bf5d708d6e5e649da291d8163d967dcdbb1f382c8cbac7941200000000000000000000000000000000000000000000000000000000000000000004c6b6304ebf1bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffffe7ad35d35cf8883dbea1de6cb90fc6585856bc1369d974b45a1f2a7353d9bc8d020000006b483045022100e7d8a8e7b3169a2e4f7c3955a8818e13206e9eb2b870e870fde814a9b96e5d0402204141202de05dcf0c491f3118b4a3dfb125cdd21b8760cf82383dabe1691787a34121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac0ffe4801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acebf1bd60","tx_hash":"66b52a8e34a8164786a3ce293e7237818b952e841806d527efd900d3b11dc2be","from":["slptest:pprqm4fyag2cyg6ju05pwwqpznukmk7ady6vspehgk"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448984,"timestamp":1623061716,"fee_details":null,"coin":"tBCH","internal_id":"fe0ae7a5662b7f83ae3e6537a2938f2cd29c0ba6844eb23a259a61a49d024ccf","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000261d6cd1c6b14fc8f790ad2c430b163f9547c3a8f1afb65892d264726227cfe3101000000d8483045022100e07f1c59fd6305f249dd7f12f0d8db8257086236783f3980c1756e6e5f9e2429022059dd0a797eb56b548cee46e786aa3bf56a2a49f2f31e87cd837437160caea25a41200000000000000000000000000000000000000000000000000000000000000000004c6b630412edbd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff61d6cd1c6b14fc8f790ad2c430b163f9547c3a8f1afb65892d264726227cfe31020000006b483045022100ebdd5374355658a2792db69ecd10333f2b3e242c265e22a59f401f7222d3e61e02205e9a3e0ac55f430bfbcbde0b87d6ea77ff0b6a0733ed3512c940b6f56e8470bf4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7f154901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac12edbd60","tx_hash":"ba6acd2ab2d7bd2d57c8e01d5bb46ca94017633ab4ef3f492f65ab14e3c44666","from":["bchtest:pqy2nmkf0z4a496j9vusgrl9racx07fydq8qak75mv","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21568847","spent_by_me":"0.21567847","received_by_me":"0.21567847","my_balance_change":"0.00000000","block_height":1448983,"timestamp":1623060512,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"ba6acd2ab2d7bd2d57c8e01d5bb46ca94017633ab4ef3f492f65ab14e3c44666","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000261d6cd1c6b14fc8f790ad2c430b163f9547c3a8f1afb65892d264726227cfe3101000000d8483045022100e07f1c59fd6305f249dd7f12f0d8db8257086236783f3980c1756e6e5f9e2429022059dd0a797eb56b548cee46e786aa3bf56a2a49f2f31e87cd837437160caea25a41200000000000000000000000000000000000000000000000000000000000000000004c6b630412edbd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff61d6cd1c6b14fc8f790ad2c430b163f9547c3a8f1afb65892d264726227cfe31020000006b483045022100ebdd5374355658a2792db69ecd10333f2b3e242c265e22a59f401f7222d3e61e02205e9a3e0ac55f430bfbcbde0b87d6ea77ff0b6a0733ed3512c940b6f56e8470bf4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7f154901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac12edbd60","tx_hash":"ba6acd2ab2d7bd2d57c8e01d5bb46ca94017633ab4ef3f492f65ab14e3c44666","from":["slptest:pqy2nmkf0z4a496j9vusgrl9racx07fydqu56dyrf3"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448983,"timestamp":1623060512,"fee_details":null,"coin":"tBCH","internal_id":"2e327fb6476cbfcdfcaa629a145c363ce42139148b7afb6bb10c96603e43e22d","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002d2e5e3432e407641b5754143c8cb18333c03c5e659528ab6f89567d8d7dd012f010000006b483045022100adf485b76d97f1679fbf5a8354fa25e6fcb56ce6fdd658e326862f58a05d7d1f022001d2a5cc3cf29ea5bd72fd9218c0842f889d137a20a78e18bd4aae2c93d2dee64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffd2e5e3432e407641b5754143c8cb18333c03c5e659528ab6f89567d8d7dd012f020000006a47304402207ca5780c73ab3a1f12a51b413519865bc68b10359fd99676f26bb805c6be40ba0220099cb4d444782bd3dc035adc304b264bb392d9c3285dac91d51d937869be841a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a91422142d4b9ae623e9892b05b9dbc6e4c649b65b0387c7094901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac88efbd60","tx_hash":"a3cf3e996893e8b6657adca71368c03bd4839f8961a7e7e55d86f0dba5576ae9","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pq3pgt2tntnz86vf9vzmnk7xunryndjmqvczt7agdp","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21565847","spent_by_me":"0.21565847","received_by_me":"0.21563847","my_balance_change":"-0.00002000","block_height":1448983,"timestamp":1623060512,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"a3cf3e996893e8b6657adca71368c03bd4839f8961a7e7e55d86f0dba5576ae9","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002d2e5e3432e407641b5754143c8cb18333c03c5e659528ab6f89567d8d7dd012f010000006b483045022100adf485b76d97f1679fbf5a8354fa25e6fcb56ce6fdd658e326862f58a05d7d1f022001d2a5cc3cf29ea5bd72fd9218c0842f889d137a20a78e18bd4aae2c93d2dee64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffd2e5e3432e407641b5754143c8cb18333c03c5e659528ab6f89567d8d7dd012f020000006a47304402207ca5780c73ab3a1f12a51b413519865bc68b10359fd99676f26bb805c6be40ba0220099cb4d444782bd3dc035adc304b264bb392d9c3285dac91d51d937869be841a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a91422142d4b9ae623e9892b05b9dbc6e4c649b65b0387c7094901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac88efbd60","tx_hash":"a3cf3e996893e8b6657adca71368c03bd4839f8961a7e7e55d86f0dba5576ae9","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pq3pgt2tntnz86vf9vzmnk7xunryndjmqvrkv98llu"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448983,"timestamp":1623060512,"fee_details":null,"coin":"tBCH","internal_id":"ee9abad0053f22f6910db1cd2520d5c1e416209b9ce89afe72d34cf911c17f60","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002e96a57a5dbf0865de5e7a761899f83d43bc06813a7dc7a65b6e89368993ecfa301000000d8483045022100c927451bc5eeba0b6a0e186000c916c8230add92088ebd6cf13d3457ff73001b02202bef90393de9eac6db17078fdf2300cc677fe4471c57d855ebaa164a04c82c2a41200000000000000000000000000000000000000000000000000000000000000000004c6b630488efbd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffffe96a57a5dbf0865de5e7a761899f83d43bc06813a7dc7a65b6e89368993ecfa3020000006a473044022055d30fd50898767ec6589c749862534293286014face1181d966a079fd2255f2022003afbce939dd8f42962c039a3c67dace0a2f473163707b2323bece1fa216c22e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdf054901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac88efbd60","tx_hash":"9536844ceebc8d7b6b1e7e7620d59d8524a0bbb56ba4daa7f412a2491f66cbe8","from":["bchtest:pq3pgt2tntnz86vf9vzmnk7xunryndjmqvczt7agdp","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21564847","spent_by_me":"0.21563847","received_by_me":"0.21563847","my_balance_change":"0.00000000","block_height":1448983,"timestamp":1623060512,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"9536844ceebc8d7b6b1e7e7620d59d8524a0bbb56ba4daa7f412a2491f66cbe8","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002e96a57a5dbf0865de5e7a761899f83d43bc06813a7dc7a65b6e89368993ecfa301000000d8483045022100c927451bc5eeba0b6a0e186000c916c8230add92088ebd6cf13d3457ff73001b02202bef90393de9eac6db17078fdf2300cc677fe4471c57d855ebaa164a04c82c2a41200000000000000000000000000000000000000000000000000000000000000000004c6b630488efbd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffffe96a57a5dbf0865de5e7a761899f83d43bc06813a7dc7a65b6e89368993ecfa3020000006a473044022055d30fd50898767ec6589c749862534293286014face1181d966a079fd2255f2022003afbce939dd8f42962c039a3c67dace0a2f473163707b2323bece1fa216c22e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdf054901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac88efbd60","tx_hash":"9536844ceebc8d7b6b1e7e7620d59d8524a0bbb56ba4daa7f412a2491f66cbe8","from":["slptest:pq3pgt2tntnz86vf9vzmnk7xunryndjmqvrkv98llu"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448983,"timestamp":1623060512,"fee_details":null,"coin":"tBCH","internal_id":"f2e367fe86013767afdbf17dd1fdd00bd99b1bd3b35cafc1f2491c999f73d328","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000026646c4e314ab652f493fefb43a631740a96cb45b1de0c8572dbdd7b22acd6aba010000006a4730440220751dde7ce8fa749bfd1d876cb389751b679a1273cd146a441323d72e5fc64aaf0220280340a6a1255f8b2964832f1f6ba587e820672ecf83c2725d785c5c6249c2704121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff6646c4e314ab652f493fefb43a631740a96cb45b1de0c8572dbdd7b22acd6aba020000006a47304402202bd479f7d6270ac3de8022fcff1b7febe4fd1e7c19bdb14ac07738f376baa0aa0220320fc182f01fd707e14ccc52f587371b05e949e1e185e08d57409aa28694dd7b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a91459be927343984e5202ebb0fcb4615e1d24d0db788797114901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac85eebd60","tx_hash":"4739a8e36f94b8e53d8250fb42a0e1ec9de509cffe0990ba43e8f791ba5a87fb","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:ppvmaynngwvyu5szawc0edrptcwjf5xm0qp2l20gfx","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21567847","spent_by_me":"0.21567847","received_by_me":"0.21565847","my_balance_change":"-0.00002000","block_height":1448983,"timestamp":1623060512,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"4739a8e36f94b8e53d8250fb42a0e1ec9de509cffe0990ba43e8f791ba5a87fb","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000026646c4e314ab652f493fefb43a631740a96cb45b1de0c8572dbdd7b22acd6aba010000006a4730440220751dde7ce8fa749bfd1d876cb389751b679a1273cd146a441323d72e5fc64aaf0220280340a6a1255f8b2964832f1f6ba587e820672ecf83c2725d785c5c6249c2704121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff6646c4e314ab652f493fefb43a631740a96cb45b1de0c8572dbdd7b22acd6aba020000006a47304402202bd479f7d6270ac3de8022fcff1b7febe4fd1e7c19bdb14ac07738f376baa0aa0220320fc182f01fd707e14ccc52f587371b05e949e1e185e08d57409aa28694dd7b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a91459be927343984e5202ebb0fcb4615e1d24d0db788797114901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac85eebd60","tx_hash":"4739a8e36f94b8e53d8250fb42a0e1ec9de509cffe0990ba43e8f791ba5a87fb","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:ppvmaynngwvyu5szawc0edrptcwjf5xm0q67c34lmm"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448983,"timestamp":1623060512,"fee_details":null,"coin":"tBCH","internal_id":"b3f2a55c23fa55c78ae94db413436fc454ec0935a4292b1adfcce86d5547dd4f","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002c66715333dee76a1e1deb51c0aab6b95f0b2ef5c39f65550f3c76c1d90e873ed010000006a47304402202f0b32c9ec246f34bab48302ce275a705f526054547dbd5a9b1e75676e682d76022009c446f734ed582d4cc2bad5b4ab4b17078edd7ec89b69edac62b37ffa4cfb9f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffc66715333dee76a1e1deb51c0aab6b95f0b2ef5c39f65550f3c76c1d90e873ed020000006a47304402202b9306f536fecf767a0d6c0f5ef064eaba177a10f764dca1405a42a0a8e2789002203e7366a9f1a25666fc44859f70f7b858b70747622f9694059a60e80d8355c4924121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a91408a9eec978abda97522b39040fe51f7067f924688767194901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac12edbd60","tx_hash":"31fe7c222647262d8965fb1a8f3a7c54f963b130c4d20a798ffc146b1ccdd661","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pqy2nmkf0z4a496j9vusgrl9racx07fydq8qak75mv","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21569847","spent_by_me":"0.21569847","received_by_me":"0.21567847","my_balance_change":"-0.00002000","block_height":1448983,"timestamp":1623060512,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"31fe7c222647262d8965fb1a8f3a7c54f963b130c4d20a798ffc146b1ccdd661","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002c66715333dee76a1e1deb51c0aab6b95f0b2ef5c39f65550f3c76c1d90e873ed010000006a47304402202f0b32c9ec246f34bab48302ce275a705f526054547dbd5a9b1e75676e682d76022009c446f734ed582d4cc2bad5b4ab4b17078edd7ec89b69edac62b37ffa4cfb9f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffc66715333dee76a1e1deb51c0aab6b95f0b2ef5c39f65550f3c76c1d90e873ed020000006a47304402202b9306f536fecf767a0d6c0f5ef064eaba177a10f764dca1405a42a0a8e2789002203e7366a9f1a25666fc44859f70f7b858b70747622f9694059a60e80d8355c4924121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a91408a9eec978abda97522b39040fe51f7067f924688767194901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac12edbd60","tx_hash":"31fe7c222647262d8965fb1a8f3a7c54f963b130c4d20a798ffc146b1ccdd661","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pqy2nmkf0z4a496j9vusgrl9racx07fydqu56dyrf3"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448983,"timestamp":1623060512,"fee_details":null,"coin":"tBCH","internal_id":"f29d3076b7f68e91cee0b33de0e825871b7ae1f31997fefac7b6f6305f7c9108","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002fb875aba91f7e843ba9009fecf09e59dece1a042fb50823de5b8946fe3a8394701000000d747304402205304ccb5f1cfeee3181c6d5f41f2a3e3ae7da1756776ed627f15035fd67bed4602200beffdd4aec720c93d679123d9e6acfb3c5ec6251bdcb6b7b48f9c08888935b441200000000000000000000000000000000000000000000000000000000000000000004c6b630485eebd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68fffffffffb875aba91f7e843ba9009fecf09e59dece1a042fb50823de5b8946fe3a83947020000006a473044022048fe68b727125b7e9154319db07bc60090b2e99a8edb90bebd867c2e1c0891bb022010b442ca3bea131da2a14ee8f629140ff29a7a177b9c20802316a418f2c5671c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acaf0d4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac85eebd60","tx_hash":"2f01ddd7d86795f8b68a5259e6c5033c3318cbc8434175b54176402e43e3e5d2","from":["bchtest:ppvmaynngwvyu5szawc0edrptcwjf5xm0qp2l20gfx","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21566847","spent_by_me":"0.21565847","received_by_me":"0.21565847","my_balance_change":"0.00000000","block_height":1448983,"timestamp":1623060512,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"2f01ddd7d86795f8b68a5259e6c5033c3318cbc8434175b54176402e43e3e5d2","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002fb875aba91f7e843ba9009fecf09e59dece1a042fb50823de5b8946fe3a8394701000000d747304402205304ccb5f1cfeee3181c6d5f41f2a3e3ae7da1756776ed627f15035fd67bed4602200beffdd4aec720c93d679123d9e6acfb3c5ec6251bdcb6b7b48f9c08888935b441200000000000000000000000000000000000000000000000000000000000000000004c6b630485eebd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68fffffffffb875aba91f7e843ba9009fecf09e59dece1a042fb50823de5b8946fe3a83947020000006a473044022048fe68b727125b7e9154319db07bc60090b2e99a8edb90bebd867c2e1c0891bb022010b442ca3bea131da2a14ee8f629140ff29a7a177b9c20802316a418f2c5671c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acaf0d4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac85eebd60","tx_hash":"2f01ddd7d86795f8b68a5259e6c5033c3318cbc8434175b54176402e43e3e5d2","from":["slptest:ppvmaynngwvyu5szawc0edrptcwjf5xm0q67c34lmm"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448983,"timestamp":1623060512,"fee_details":null,"coin":"tBCH","internal_id":"52d14fbbf5bccb69704797c22c72e42b2340c73d2429fec743f3887c4bada513","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000281b24f0023a60bf0e9e5f7890cdb262898cf6c2f669f1d1e80b7d775d45be93101000000d74730440220345a848e10f1c50e7c08af6a0083c223dfc326fb96aedc6aad6f42585ca8d253022045b335c86dc7f2ab04375f87d44ea6e551a66233aef79425b24649a7712cf3f441200000000000000000000000000000000000000000000000000000000000000000004c6b630483e7bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff81b24f0023a60bf0e9e5f7890cdb262898cf6c2f669f1d1e80b7d775d45be931020000006b483045022100feac49df391ab602ed381c7923d7d39b9b816886316a8ba809395510567e5238022008db6e325845bd8abe31d4ae84d8cb49f01de5df93e0d9b13778978bdc6bcec74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4f1d4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac83e7bd60","tx_hash":"ed73e8901d6cc7f35055f6395cefb2f0956bab0a1cb5dee1a176ee3d331567c6","from":["bchtest:pph4aex9zhqhkeax7c87k9jv5dwe0h8zx5g8v9pwdy","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21570847","spent_by_me":"0.21569847","received_by_me":"0.21569847","my_balance_change":"0.00000000","block_height":1448982,"timestamp":1623059292,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"ed73e8901d6cc7f35055f6395cefb2f0956bab0a1cb5dee1a176ee3d331567c6","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000281b24f0023a60bf0e9e5f7890cdb262898cf6c2f669f1d1e80b7d775d45be93101000000d74730440220345a848e10f1c50e7c08af6a0083c223dfc326fb96aedc6aad6f42585ca8d253022045b335c86dc7f2ab04375f87d44ea6e551a66233aef79425b24649a7712cf3f441200000000000000000000000000000000000000000000000000000000000000000004c6b630483e7bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff81b24f0023a60bf0e9e5f7890cdb262898cf6c2f669f1d1e80b7d775d45be931020000006b483045022100feac49df391ab602ed381c7923d7d39b9b816886316a8ba809395510567e5238022008db6e325845bd8abe31d4ae84d8cb49f01de5df93e0d9b13778978bdc6bcec74121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4f1d4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac83e7bd60","tx_hash":"ed73e8901d6cc7f35055f6395cefb2f0956bab0a1cb5dee1a176ee3d331567c6","from":["slptest:pph4aex9zhqhkeax7c87k9jv5dwe0h8zx5nnt7mele"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448982,"timestamp":1623059292,"fee_details":null,"coin":"tBCH","internal_id":"294bdf000824a966898c940e934fbbc6097fb97fbb87b27fdb4a46ffeaa2893a","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000020d2614aa2b88026aef1d7a478244aded4bc30cad7f97055b8e1c6f636e154339010000006b483045022100ff683b0c28bd5b92e12f1813ad89b0eba014ecdbf51cefaeb280531f94bdc4b302203457fa2af3f1935f47a316923d146693539514118c12aa95dea46fe8c08c09284121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff0d2614aa2b88026aef1d7a478244aded4bc30cad7f97055b8e1c6f636e154339020000006b483045022100dbbb1a39cae424103d68e2544c6ec9d518f0d29713179c8cdc3ef17033ad0b1a02201ae5974383dd18d9cb78838623cc7349a3b32055c8de7e27e1996aac3e1600574121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a91452401fa60edd8f41b54f16f895f59f74f100764c8707294901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acfce6bd60","tx_hash":"e1d52d89ea18f1e8481768ba644fb8c4b1aefa5a440567c037cf3165ee25ac51","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:ppfyq8axpmwc7sd4fut03904na60zqrkfszvct9qh4","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21573847","spent_by_me":"0.21573847","received_by_me":"0.21571847","my_balance_change":"-0.00002000","block_height":1448982,"timestamp":1623059292,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"e1d52d89ea18f1e8481768ba644fb8c4b1aefa5a440567c037cf3165ee25ac51","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000020d2614aa2b88026aef1d7a478244aded4bc30cad7f97055b8e1c6f636e154339010000006b483045022100ff683b0c28bd5b92e12f1813ad89b0eba014ecdbf51cefaeb280531f94bdc4b302203457fa2af3f1935f47a316923d146693539514118c12aa95dea46fe8c08c09284121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff0d2614aa2b88026aef1d7a478244aded4bc30cad7f97055b8e1c6f636e154339020000006b483045022100dbbb1a39cae424103d68e2544c6ec9d518f0d29713179c8cdc3ef17033ad0b1a02201ae5974383dd18d9cb78838623cc7349a3b32055c8de7e27e1996aac3e1600574121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a91452401fa60edd8f41b54f16f895f59f74f100764c8707294901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acfce6bd60","tx_hash":"e1d52d89ea18f1e8481768ba644fb8c4b1aefa5a440567c037cf3165ee25ac51","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:ppfyq8axpmwc7sd4fut03904na60zqrkfseclslh9g"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448982,"timestamp":1623059292,"fee_details":null,"coin":"tBCH","internal_id":"1c4c13bc66014629c3b740e4a0fdc6056c4d17a3ef6015d061dc75285f45f5e5","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000251ac25ee6531cf37c06705445afaaeb1c4b84f64ba681748e8f118ea892dd5e101000000d8483045022100bf505f3622712cc9eb28ce8b2e243eeea47dd151bce0608666b375e87c7ed80202207a00f813034214d1996ec5a4ef4ab1d37968473ac3d024b5df40ff4f98809f6e41200000000000000000000000000000000000000000000000000000000000000000004c6b6304fbe6bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff51ac25ee6531cf37c06705445afaaeb1c4b84f64ba681748e8f118ea892dd5e1020000006b4830450221009a71afff96779e6e3ac23a069d2dd617cbeb9aca9bfba05f6c3efaff24d417990220734e2f07d8659287bb6d87d83887796a9224cf7500929ddf0a506a611d3275b54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1f254901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acfbe6bd60","tx_hash":"804a357229dfd1513737da9350156051b7bf455dc4f47f3b55e5172cc84d1e7b","from":["bchtest:ppfyq8axpmwc7sd4fut03904na60zqrkfszvct9qh4","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21572847","spent_by_me":"0.21571847","received_by_me":"0.21571847","my_balance_change":"0.00000000","block_height":1448982,"timestamp":1623059292,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"804a357229dfd1513737da9350156051b7bf455dc4f47f3b55e5172cc84d1e7b","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000251ac25ee6531cf37c06705445afaaeb1c4b84f64ba681748e8f118ea892dd5e101000000d8483045022100bf505f3622712cc9eb28ce8b2e243eeea47dd151bce0608666b375e87c7ed80202207a00f813034214d1996ec5a4ef4ab1d37968473ac3d024b5df40ff4f98809f6e41200000000000000000000000000000000000000000000000000000000000000000004c6b6304fbe6bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff51ac25ee6531cf37c06705445afaaeb1c4b84f64ba681748e8f118ea892dd5e1020000006b4830450221009a71afff96779e6e3ac23a069d2dd617cbeb9aca9bfba05f6c3efaff24d417990220734e2f07d8659287bb6d87d83887796a9224cf7500929ddf0a506a611d3275b54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1f254901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acfbe6bd60","tx_hash":"804a357229dfd1513737da9350156051b7bf455dc4f47f3b55e5172cc84d1e7b","from":["slptest:ppfyq8axpmwc7sd4fut03904na60zqrkfseclslh9g"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448982,"timestamp":1623059292,"fee_details":null,"coin":"tBCH","internal_id":"8d0cd035534eed79ea612e11354e63a96a4e623e0ee6de5603e56b5fe530cdb6","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000027b1e4dc82c17e5553b7ff4c45d45bfb75160155093da373751d1df2972354a80010000006b483045022100db9096d5d187a83da35d0f548a106f3c124001559bfafe0ef672f6c4c40cf632022008f89189e5d31c694b9ef940afc1d4d141d4349df29ed9e5ec6b819bbc14a3954121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff7b1e4dc82c17e5553b7ff4c45d45bfb75160155093da373751d1df2972354a80020000006b483045022100aef72036d7324c61807b7517900df36090c6e0743106f15d380f3f3fba565bd8022048885301964f0f319cd96926d3cc62b073749af9701dd98aea9555e69e5ec0614121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a9146f5ee4c515c17b67a6f60feb164ca35d97dce2358737214901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac84e7bd60","tx_hash":"31e95bd475d7b7801e1d9f662f6ccf982826db0c89f7e5e9f00ba623004fb281","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pph4aex9zhqhkeax7c87k9jv5dwe0h8zx5g8v9pwdy","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21571847","spent_by_me":"0.21571847","received_by_me":"0.21569847","my_balance_change":"-0.00002000","block_height":1448982,"timestamp":1623059292,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"31e95bd475d7b7801e1d9f662f6ccf982826db0c89f7e5e9f00ba623004fb281","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000027b1e4dc82c17e5553b7ff4c45d45bfb75160155093da373751d1df2972354a80010000006b483045022100db9096d5d187a83da35d0f548a106f3c124001559bfafe0ef672f6c4c40cf632022008f89189e5d31c694b9ef940afc1d4d141d4349df29ed9e5ec6b819bbc14a3954121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff7b1e4dc82c17e5553b7ff4c45d45bfb75160155093da373751d1df2972354a80020000006b483045022100aef72036d7324c61807b7517900df36090c6e0743106f15d380f3f3fba565bd8022048885301964f0f319cd96926d3cc62b073749af9701dd98aea9555e69e5ec0614121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a9146f5ee4c515c17b67a6f60feb164ca35d97dce2358737214901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac84e7bd60","tx_hash":"31e95bd475d7b7801e1d9f662f6ccf982826db0c89f7e5e9f00ba623004fb281","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pph4aex9zhqhkeax7c87k9jv5dwe0h8zx5nnt7mele"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448982,"timestamp":1623059292,"fee_details":null,"coin":"tBCH","internal_id":"30468ca5f542169af50153c167ed01f1f269abd7dee0c6001e738d6c72b2135c","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002c9e8fe17429d092f4c707c6fa2b0e568bd76a3adee663ebdca841935d0e896f4010000006a4730440220648f29f63aef5e84c30b3af157d113a294fef830502b037ccbff924ef287931502206d42b2a31e7ac32487cfc127c07607fae056c83de05d113dec4c1697f3d162e24121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffc9e8fe17429d092f4c707c6fa2b0e568bd76a3adee663ebdca841935d0e896f4020000006b483045022100b8a0100b1c0f26a1011aa0057177578426f169345c731176a5e9f2e281bf00c602200682ae012ed909bfba19ca1982db297748c5fd83d013c655f98abee1465d851b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914173cf5e4b0bc1db5ebbf8a4ce14dad646b2d78298777404901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5be3bd60","tx_hash":"ad947c93d048fc0092c07e5b667dee6c6707955a11cd92d720c5781edc1db48d","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pqtnea0ykz7pmd0th79yec2d44jxkttc9y5uwsgrtg","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21579847","spent_by_me":"0.21579847","received_by_me":"0.21577847","my_balance_change":"-0.00002000","block_height":1448981,"timestamp":1623058067,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"ad947c93d048fc0092c07e5b667dee6c6707955a11cd92d720c5781edc1db48d","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002c9e8fe17429d092f4c707c6fa2b0e568bd76a3adee663ebdca841935d0e896f4010000006a4730440220648f29f63aef5e84c30b3af157d113a294fef830502b037ccbff924ef287931502206d42b2a31e7ac32487cfc127c07607fae056c83de05d113dec4c1697f3d162e24121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffc9e8fe17429d092f4c707c6fa2b0e568bd76a3adee663ebdca841935d0e896f4020000006b483045022100b8a0100b1c0f26a1011aa0057177578426f169345c731176a5e9f2e281bf00c602200682ae012ed909bfba19ca1982db297748c5fd83d013c655f98abee1465d851b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914173cf5e4b0bc1db5ebbf8a4ce14dad646b2d78298777404901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5be3bd60","tx_hash":"ad947c93d048fc0092c07e5b667dee6c6707955a11cd92d720c5781edc1db48d","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pqtnea0ykz7pmd0th79yec2d44jxkttc9y0gftj5e4"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448981,"timestamp":1623058067,"fee_details":null,"coin":"tBCH","internal_id":"77767aa5f40c48a27d22565d02aa669698372502695b8f89618463afc93c8480","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002e2516dc1e6ddf2f829ffe899d8bbdd72b276b0d1d818676368028457c696ba43010000006a47304402201f4afa08d87957ec2b6f5f524dbee3295c1134508a7b07654d0087f38dff466b022067553b4bbc1a7e41c4a66dad093414296c787897ebcb56a52464d70efe352ba94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffe2516dc1e6ddf2f829ffe899d8bbdd72b276b0d1d818676368028457c696ba43020000006b483045022100fab2c87fef91116a723de5c200f2dbc1b4d0f077e54195e299244840126e89e002207ffc10bb70038eda05f371e59e4d9c6938662f3fea878e4c7610ae3f75a0e8364121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a9141e5373a7ca04506a71701eaf73f1d219337cd79a87d7304901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7de4bd60","tx_hash":"6a7836c2c35548944a86a00dea0a6f070eac4611c1b0ee3bf6d6a398174d460a","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pq09xua8egz9q6n3wq027ul36gvnxlxhngh59889lt","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21575847","spent_by_me":"0.21575847","received_by_me":"0.21573847","my_balance_change":"-0.00002000","block_height":1448981,"timestamp":1623058067,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"6a7836c2c35548944a86a00dea0a6f070eac4611c1b0ee3bf6d6a398174d460a","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002e2516dc1e6ddf2f829ffe899d8bbdd72b276b0d1d818676368028457c696ba43010000006a47304402201f4afa08d87957ec2b6f5f524dbee3295c1134508a7b07654d0087f38dff466b022067553b4bbc1a7e41c4a66dad093414296c787897ebcb56a52464d70efe352ba94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffe2516dc1e6ddf2f829ffe899d8bbdd72b276b0d1d818676368028457c696ba43020000006b483045022100fab2c87fef91116a723de5c200f2dbc1b4d0f077e54195e299244840126e89e002207ffc10bb70038eda05f371e59e4d9c6938662f3fea878e4c7610ae3f75a0e8364121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a9141e5373a7ca04506a71701eaf73f1d219337cd79a87d7304901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7de4bd60","tx_hash":"6a7836c2c35548944a86a00dea0a6f070eac4611c1b0ee3bf6d6a398174d460a","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pq09xua8egz9q6n3wq027ul36gvnxlxhngvqzuajdk"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448981,"timestamp":1623058067,"fee_details":null,"coin":"tBCH","internal_id":"0dfb8bef02991b571367240d81e5d278c82ae74adf1ce55a07f74c53b0b6ed26","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002025b2359779f0f8de247f1e3636243e83f14bd03aa30fa20362fbdc505d89b0401000000b7483045022100aaf3dd9263301250ad7ba746e85d8e299df99db2915fc80d0165048170d13b1b02204f95ca4ed98ae452ed41e787bd6f88398937bfaaade1e78b75d1e5f5be7241ca41514c6b6304f8c7bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821010101010101010101010101010101010101010101010101010101010101010101ac68feffffff025b2359779f0f8de247f1e3636243e83f14bd03aa30fa20362fbdc505d89b04020000006a4730440220327e09c8eeba7bf952b75e3c0788b6b6d3340fcb75b87ca903a7405320404e02022075d5dc9283037ece5d6609481fa5dbc7bba5c7d136321298f468b1056ab90c0d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acbf344901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace3c9bd60","tx_hash":"43ba96c657840268636718d8d1b076b272ddbbd899e8ff29f8f2dde6c16d51e2","from":["bchtest:presvtclnyz57rsqc5l3xs0cs8xpftxy7vfjya7qww","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21576847","spent_by_me":"0.21575847","received_by_me":"0.21575847","my_balance_change":"0.00000000","block_height":1448981,"timestamp":1623058067,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"43ba96c657840268636718d8d1b076b272ddbbd899e8ff29f8f2dde6c16d51e2","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002025b2359779f0f8de247f1e3636243e83f14bd03aa30fa20362fbdc505d89b0401000000b7483045022100aaf3dd9263301250ad7ba746e85d8e299df99db2915fc80d0165048170d13b1b02204f95ca4ed98ae452ed41e787bd6f88398937bfaaade1e78b75d1e5f5be7241ca41514c6b6304f8c7bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821010101010101010101010101010101010101010101010101010101010101010101ac68feffffff025b2359779f0f8de247f1e3636243e83f14bd03aa30fa20362fbdc505d89b04020000006a4730440220327e09c8eeba7bf952b75e3c0788b6b6d3340fcb75b87ca903a7405320404e02022075d5dc9283037ece5d6609481fa5dbc7bba5c7d136321298f468b1056ab90c0d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acbf344901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace3c9bd60","tx_hash":"43ba96c657840268636718d8d1b076b272ddbbd899e8ff29f8f2dde6c16d51e2","from":["slptest:presvtclnyz57rsqc5l3xs0cs8xpftxy7vjxrxyhun"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448981,"timestamp":1623058067,"fee_details":null,"coin":"tBCH","internal_id":"711b7ec3e347fd49c511168f684caabd00f0edc29790dacb821fa329c654db7e","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000020a464d1798a3d6f63beeb0c11146ac0e076f0aea0da0864a944855c3c236786a01000000b6473044022050d0bf1e449ccb2499c5bbdb2ee4f4bf7b4c238fbe02581641c98fd715f5298b02205b67e08155ac2d64bbee8731d791fb2044e4a9de3c478ec6c2b956c7915f908a41514c6b63045cc8bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821010101010101010101010101010101010101010101010101010101010101010101ac68feffffff0a464d1798a3d6f63beeb0c11146ac0e076f0aea0da0864a944855c3c236786a020000006a47304402202b438547d1eb7118b35d89c38e0d8c8dd27469b6a055211f11527bb785c00741022019168de07cbdebf5bf51cd502460a723beae4862798b9f75cb4d5abaceda6e364121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acef2c4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace3c9bd60","tx_hash":"3943156e636f1c8e5b05977fad0cc34bedad4482477a1def6a02882baa14260d","from":["bchtest:pq09xua8egz9q6n3wq027ul36gvnxlxhngh59889lt","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21574847","spent_by_me":"0.21573847","received_by_me":"0.21573847","my_balance_change":"0.00000000","block_height":1448981,"timestamp":1623058067,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"3943156e636f1c8e5b05977fad0cc34bedad4482477a1def6a02882baa14260d","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000020a464d1798a3d6f63beeb0c11146ac0e076f0aea0da0864a944855c3c236786a01000000b6473044022050d0bf1e449ccb2499c5bbdb2ee4f4bf7b4c238fbe02581641c98fd715f5298b02205b67e08155ac2d64bbee8731d791fb2044e4a9de3c478ec6c2b956c7915f908a41514c6b63045cc8bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821010101010101010101010101010101010101010101010101010101010101010101ac68feffffff0a464d1798a3d6f63beeb0c11146ac0e076f0aea0da0864a944855c3c236786a020000006a47304402202b438547d1eb7118b35d89c38e0d8c8dd27469b6a055211f11527bb785c00741022019168de07cbdebf5bf51cd502460a723beae4862798b9f75cb4d5abaceda6e364121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acef2c4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace3c9bd60","tx_hash":"3943156e636f1c8e5b05977fad0cc34bedad4482477a1def6a02882baa14260d","from":["slptest:pq09xua8egz9q6n3wq027ul36gvnxlxhngvqzuajdk"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448981,"timestamp":1623058067,"fee_details":null,"coin":"tBCH","internal_id":"052a317c46538a9909ec3ee762b1858f14226b55634e4d31bc12aa490980c59f","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000028db41ddc1e78c520d792cd115a9507676cee7d665b7ec09200fc48d0937c94ad01000000b7483045022100af09669cf2c6a7d99466f4c91707608c31239e1fd617111990ba0ad64d80678502204b44003cf57ead30571643cc26eb25d9db117d810029b635c731c09be6d7582941514c6b63043bc7bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffff8db41ddc1e78c520d792cd115a9507676cee7d665b7ec09200fc48d0937c94ad020000006a4730440220172c864b5f0bb3c93029c4d9a86d1ce391810c5d9be92b4867d5ac50fe0bceb602204e5b01ed308a67f90c71f050319701ecea7095c357265b46d5bd707a795653004121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8f3c4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace3c9bd60","tx_hash":"1d909ab0da84973b2a8dece0b3b2c54d6d22e2fe1b2f2fb84b42203e23d45392","from":["bchtest:pqtnea0ykz7pmd0th79yec2d44jxkttc9y5uwsgrtg","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21578847","spent_by_me":"0.21577847","received_by_me":"0.21577847","my_balance_change":"0.00000000","block_height":1448981,"timestamp":1623058067,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"1d909ab0da84973b2a8dece0b3b2c54d6d22e2fe1b2f2fb84b42203e23d45392","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000028db41ddc1e78c520d792cd115a9507676cee7d665b7ec09200fc48d0937c94ad01000000b7483045022100af09669cf2c6a7d99466f4c91707608c31239e1fd617111990ba0ad64d80678502204b44003cf57ead30571643cc26eb25d9db117d810029b635c731c09be6d7582941514c6b63043bc7bd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffff8db41ddc1e78c520d792cd115a9507676cee7d665b7ec09200fc48d0937c94ad020000006a4730440220172c864b5f0bb3c93029c4d9a86d1ce391810c5d9be92b4867d5ac50fe0bceb602204e5b01ed308a67f90c71f050319701ecea7095c357265b46d5bd707a795653004121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8f3c4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ace3c9bd60","tx_hash":"1d909ab0da84973b2a8dece0b3b2c54d6d22e2fe1b2f2fb84b42203e23d45392","from":["slptest:pqtnea0ykz7pmd0th79yec2d44jxkttc9y0gftj5e4"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448981,"timestamp":1623058067,"fee_details":null,"coin":"tBCH","internal_id":"b8fdd342ce47c9560ea75eefd1596e8efebe0673d8251c5fe896a2ef71d96a7f","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000029253d4233e20424bb82f2f1bfee2226d4dc5b2b3e0ec8d2a3b9784dab09a901d010000006b48304502210080bdcb450dce368184393981092d80154510ebccc747049aa09797be08ec5714022020cf5412eb7ecb45a099cab997e8e04e9bc568bffa687ace37e35d5881c499214121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff9253d4233e20424bb82f2f1bfee2226d4dc5b2b3e0ec8d2a3b9784dab09a901d020000006b483045022100c561e126f60eaf9c34d5aa4a1b2103f083c95570ee9d9856cdd2f71f1911d43b022039df2709cd0d6af4ba3406479245e9b308558baa42e77abe5f261938370da8104121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914f3062f1f99054f0e00c53f1341f881cc14acc4f387a7384901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac19e4bd60","tx_hash":"049bd805c5bd2f3620fa30aa03bd143fe8436263e3f147e28d0f9f7759235b02","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:presvtclnyz57rsqc5l3xs0cs8xpftxy7vfjya7qww","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21577847","spent_by_me":"0.21577847","received_by_me":"0.21575847","my_balance_change":"-0.00002000","block_height":1448981,"timestamp":1623058067,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"049bd805c5bd2f3620fa30aa03bd143fe8436263e3f147e28d0f9f7759235b02","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000029253d4233e20424bb82f2f1bfee2226d4dc5b2b3e0ec8d2a3b9784dab09a901d010000006b48304502210080bdcb450dce368184393981092d80154510ebccc747049aa09797be08ec5714022020cf5412eb7ecb45a099cab997e8e04e9bc568bffa687ace37e35d5881c499214121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff9253d4233e20424bb82f2f1bfee2226d4dc5b2b3e0ec8d2a3b9784dab09a901d020000006b483045022100c561e126f60eaf9c34d5aa4a1b2103f083c95570ee9d9856cdd2f71f1911d43b022039df2709cd0d6af4ba3406479245e9b308558baa42e77abe5f261938370da8104121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914f3062f1f99054f0e00c53f1341f881cc14acc4f387a7384901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac19e4bd60","tx_hash":"049bd805c5bd2f3620fa30aa03bd143fe8436263e3f147e28d0f9f7759235b02","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:presvtclnyz57rsqc5l3xs0cs8xpftxy7vjxrxyhun"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448981,"timestamp":1623058067,"fee_details":null,"coin":"tBCH","internal_id":"a3d37a930f179f044d119037c2ca06e4d910e3e3ed85fec217bf8d9570da3b66","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000022e8f723af79d13f3d05c23aef11d9d094c246784dfe686807e25b11087bc1a8b01000000b6473044022064fdab4412e25cba07cfc3075a42af3dea7c8f84ccab8fd9819d4ef8370ebdc302203e7d03c11a057192a3fa223710d7324d77fb87e4b96cf9a1df109af430b8dc0b41514c6b630452babd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffff2e8f723af79d13f3d05c23aef11d9d094c246784dfe686807e25b11087bc1a8b020000006b483045022100b8e002014f60210662c647f25ca23d0af29563ca6cbaf375d24d7549c633ea2202201491fe1fd024ed99c09c8fe0a6c73663af424a42e26019a2c02085746b82a1a94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5f444901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac97bbbd60","tx_hash":"f496e8d0351984cabd3e66eeada376bd68e5b0a26f7c704c2f099d4217fee8c9","from":["bchtest:pp0mu82rlztc3mzdz7knpvk9m6t5yrkn5sewsqr9y3","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21580847","spent_by_me":"0.21579847","received_by_me":"0.21579847","my_balance_change":"0.00000000","block_height":1448978,"timestamp":1623054396,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"f496e8d0351984cabd3e66eeada376bd68e5b0a26f7c704c2f099d4217fee8c9","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000022e8f723af79d13f3d05c23aef11d9d094c246784dfe686807e25b11087bc1a8b01000000b6473044022064fdab4412e25cba07cfc3075a42af3dea7c8f84ccab8fd9819d4ef8370ebdc302203e7d03c11a057192a3fa223710d7324d77fb87e4b96cf9a1df109af430b8dc0b41514c6b630452babd60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffff2e8f723af79d13f3d05c23aef11d9d094c246784dfe686807e25b11087bc1a8b020000006b483045022100b8e002014f60210662c647f25ca23d0af29563ca6cbaf375d24d7549c633ea2202201491fe1fd024ed99c09c8fe0a6c73663af424a42e26019a2c02085746b82a1a94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac5f444901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac97bbbd60","tx_hash":"f496e8d0351984cabd3e66eeada376bd68e5b0a26f7c704c2f099d4217fee8c9","from":["slptest:pp0mu82rlztc3mzdz7knpvk9m6t5yrkn5sz6hmejkv"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448978,"timestamp":1623054396,"fee_details":null,"coin":"tBCH","internal_id":"15cae3ece8450ec511d4aae630a4ed808504e1eb79b9da4139ce444f580cb043","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000025511dba5b0ba801ae50d725217a41fb3329678a8f3bee3d4d3f4a2aeacf4cf59010000006a47304402201014b3fbd217d4007008ed9e02ca91b8af6c3b957eefc73dcdd32a1dbba5ebc10220536785bda7bf014b6624b261cced5e09f9db126b993ead405148f7dccc28fdf64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff5511dba5b0ba801ae50d725217a41fb3329678a8f3bee3d4d3f4a2aeacf4cf59020000006b483045022100e75c5bd3717f71f74ef425b59628e496c8268bb279e1d324f8d25106356382da022011f37dc96d083a20b41777b4e83fe95b89e8be7f28c40918afb9140ba6e5b6654121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a9145fbe1d43f89788ec4d17ad30b2c5de97420ed3a48747484901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac72d6bd60","tx_hash":"8b1abc8710b1257e8086e6df8467244c099d1df1ae235cd0f3139df73a728f2e","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pp0mu82rlztc3mzdz7knpvk9m6t5yrkn5sewsqr9y3","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21581847","spent_by_me":"0.21581847","received_by_me":"0.21579847","my_balance_change":"-0.00002000","block_height":1448978,"timestamp":1623054396,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"8b1abc8710b1257e8086e6df8467244c099d1df1ae235cd0f3139df73a728f2e","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000025511dba5b0ba801ae50d725217a41fb3329678a8f3bee3d4d3f4a2aeacf4cf59010000006a47304402201014b3fbd217d4007008ed9e02ca91b8af6c3b957eefc73dcdd32a1dbba5ebc10220536785bda7bf014b6624b261cced5e09f9db126b993ead405148f7dccc28fdf64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff5511dba5b0ba801ae50d725217a41fb3329678a8f3bee3d4d3f4a2aeacf4cf59020000006b483045022100e75c5bd3717f71f74ef425b59628e496c8268bb279e1d324f8d25106356382da022011f37dc96d083a20b41777b4e83fe95b89e8be7f28c40918afb9140ba6e5b6654121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a9145fbe1d43f89788ec4d17ad30b2c5de97420ed3a48747484901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac72d6bd60","tx_hash":"8b1abc8710b1257e8086e6df8467244c099d1df1ae235cd0f3139df73a728f2e","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pp0mu82rlztc3mzdz7knpvk9m6t5yrkn5sz6hmejkv"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448978,"timestamp":1623054396,"fee_details":null,"coin":"tBCH","internal_id":"c176bd7bbb36515a42c6b6df15006513d2e7cc5f60108e7bd2c11f84bf98fc00","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002b3ef43aa024a106908deacccd94302b9076d2c69498a71fcd23a4a6b60fc4e0a010000006a47304402201dd728a677e6ea0412014447ad9acf92fd87a8fb41583aef7647d5b00a5a0bb10220033b57ba2a54ceae4ff27b5018e8333918fae8a9439e8d75ae4cc9adb93f84b14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb3ef43aa024a106908deacccd94302b9076d2c69498a71fcd23a4a6b60fc4e0a020000006b483045022100fc577a2e09251aef54f5810f69a931d0fec7f0fca52714ae81354c4188d23e4a022022c3fc494ef78f9e7fde72a420ea9c910f6f2d4fb85f1c4a65fd5c2d3ef6a0714121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914396a533a23b3700a753359592b3374ade703a9818717504901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1132ba60","tx_hash":"d49e92afd58a1b654e975548588f8e410b6efd459dff902d62bd51d5f68bc12d","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pquk55e6ywehqzn4xdv4j2enwjk7wqafsygyvlrvl6","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21583847","spent_by_me":"0.21583847","received_by_me":"0.21581847","my_balance_change":"-0.00002000","block_height":1448783,"timestamp":1622816281,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"d49e92afd58a1b654e975548588f8e410b6efd459dff902d62bd51d5f68bc12d","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002b3ef43aa024a106908deacccd94302b9076d2c69498a71fcd23a4a6b60fc4e0a010000006a47304402201dd728a677e6ea0412014447ad9acf92fd87a8fb41583aef7647d5b00a5a0bb10220033b57ba2a54ceae4ff27b5018e8333918fae8a9439e8d75ae4cc9adb93f84b14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb3ef43aa024a106908deacccd94302b9076d2c69498a71fcd23a4a6b60fc4e0a020000006b483045022100fc577a2e09251aef54f5810f69a931d0fec7f0fca52714ae81354c4188d23e4a022022c3fc494ef78f9e7fde72a420ea9c910f6f2d4fb85f1c4a65fd5c2d3ef6a0714121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914396a533a23b3700a753359592b3374ade703a9818717504901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac1132ba60","tx_hash":"d49e92afd58a1b654e975548588f8e410b6efd459dff902d62bd51d5f68bc12d","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pquk55e6ywehqzn4xdv4j2enwjk7wqafsynstyemd8"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448783,"timestamp":1622816281,"fee_details":null,"coin":"tBCH","internal_id":"9a89ec96d924763c588d9d98a6289d4fccaa0575f6d6dd7834d17ea0d778d7d8","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000022dc18bf6d551bd622d90ff9d45fd6e0b418e8f584855974e651b8ad5af929ed401000000b7483045022100918eabe812b517bcaf3d1fa4bbea606d55128214aecf7e638e0c404038f838b702203bb0a8806fff558ad5203eb7162c8c9b4804742f6a3b13e072d77d7b1a1458d241514c6b6304f115ba60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffff2dc18bf6d551bd622d90ff9d45fd6e0b418e8f584855974e651b8ad5af929ed4020000006b48304502210088f1279901c508834246d9c710976424003c58ce97f0b669d663a3868f740cbf02201e239405ec8bba599e9fde8b5642b08f81afc829f835ff05626b01ae4d73199c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2f4c4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7b19ba60","tx_hash":"59cff4acaea2f4d3d4e3bef3a8789632b31fa41752720de51a80bab0a5db1155","from":["bchtest:pquk55e6ywehqzn4xdv4j2enwjk7wqafsygyvlrvl6","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21582847","spent_by_me":"0.21581847","received_by_me":"0.21581847","my_balance_change":"0.00000000","block_height":1448783,"timestamp":1622816281,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"59cff4acaea2f4d3d4e3bef3a8789632b31fa41752720de51a80bab0a5db1155","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000022dc18bf6d551bd622d90ff9d45fd6e0b418e8f584855974e651b8ad5af929ed401000000b7483045022100918eabe812b517bcaf3d1fa4bbea606d55128214aecf7e638e0c404038f838b702203bb0a8806fff558ad5203eb7162c8c9b4804742f6a3b13e072d77d7b1a1458d241514c6b6304f115ba60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffff2dc18bf6d551bd622d90ff9d45fd6e0b418e8f584855974e651b8ad5af929ed4020000006b48304502210088f1279901c508834246d9c710976424003c58ce97f0b669d663a3868f740cbf02201e239405ec8bba599e9fde8b5642b08f81afc829f835ff05626b01ae4d73199c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2f4c4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7b19ba60","tx_hash":"59cff4acaea2f4d3d4e3bef3a8789632b31fa41752720de51a80bab0a5db1155","from":["slptest:pquk55e6ywehqzn4xdv4j2enwjk7wqafsynstyemd8"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448783,"timestamp":1622816281,"fee_details":null,"coin":"tBCH","internal_id":"6e9b3bb36689832aa32b1f7a5f200dc6682cd9ad69e7321a2137eba07a9b93b7","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000020189e764ee43a90c8fa31b458741b569ed7501980dd6c1e07766c186872f9bc9010000006b483045022100ba98457a7cf571b70c0fc4e4ae7cfbefdaabf9704233e6f70058317435af42e10220630cf02dae2e705b17cb22fcd8b452159a136a83b7d8efa75ab37d7f88478f234121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff0189e764ee43a90c8fa31b458741b569ed7501980dd6c1e07766c186872f9bc9020000006a473044022024e40b8089dd60e3bf0847faaf68f8ae746bc677cf554ddbda97d04ca61e30e902207c9ef17a84e32f1fecd2164d47f330e74a978a3f56d7d9388cdff44cb8077b9f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914521da80525e5bd677da9ff9c60ef2ab67490454987e7574901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac862dba60","tx_hash":"50c387362d62d445be3cb15cd7f0fec9f87aafd0d23198a1dce8358b3648b7d4","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:ppfpm2q9yhjm6ema48lecc8092m8fyz9fyqr84njrs","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21585847","spent_by_me":"0.21585847","received_by_me":"0.21583847","my_balance_change":"-0.00002000","block_height":1448782,"timestamp":1622815061,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"50c387362d62d445be3cb15cd7f0fec9f87aafd0d23198a1dce8358b3648b7d4","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000020189e764ee43a90c8fa31b458741b569ed7501980dd6c1e07766c186872f9bc9010000006b483045022100ba98457a7cf571b70c0fc4e4ae7cfbefdaabf9704233e6f70058317435af42e10220630cf02dae2e705b17cb22fcd8b452159a136a83b7d8efa75ab37d7f88478f234121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff0189e764ee43a90c8fa31b458741b569ed7501980dd6c1e07766c186872f9bc9020000006a473044022024e40b8089dd60e3bf0847faaf68f8ae746bc677cf554ddbda97d04ca61e30e902207c9ef17a84e32f1fecd2164d47f330e74a978a3f56d7d9388cdff44cb8077b9f4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914521da80525e5bd677da9ff9c60ef2ab67490454987e7574901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac862dba60","tx_hash":"50c387362d62d445be3cb15cd7f0fec9f87aafd0d23198a1dce8358b3648b7d4","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:ppfpm2q9yhjm6ema48lecc8092m8fyz9fymhqwf93d"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448782,"timestamp":1622815061,"fee_details":null,"coin":"tBCH","internal_id":"ca81e22dc430e8556caf905bd4aa4330422732f61c2d39403c0850ff5655a282","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002d4b748368b35e8dca19831d2d0af7af8c9fef0d75cb13cbe45d4622d3687c35001000000b647304402207307b33a6eb87e7fbf4c8fb5b045521904d0b063ba95b9487119f10e449f305102204fdf0e4a64b139d7a6b7323e869e35d25c6723c2dfdff66125a24e13c256c12141514c6b63046511ba60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffffd4b748368b35e8dca19831d2d0af7af8c9fef0d75cb13cbe45d4622d3687c350020000006b483045022100b30abde7b4e72fc928f361c9e06d0ae57fe8c2f364f5ba5841e5fca38715fb98022023f4b239d54a8fc1f9e962a4c1be54848af10a8be663b88db6c95461051fb3584121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acff534901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6511ba60","tx_hash":"0a4efc606b4a3ad2fc718a49692c6d07b90243d9ccacde0869104a02aa43efb3","from":["bchtest:ppfpm2q9yhjm6ema48lecc8092m8fyz9fyqr84njrs","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21584847","spent_by_me":"0.21583847","received_by_me":"0.21583847","my_balance_change":"0.00000000","block_height":1448782,"timestamp":1622815061,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"0a4efc606b4a3ad2fc718a49692c6d07b90243d9ccacde0869104a02aa43efb3","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002d4b748368b35e8dca19831d2d0af7af8c9fef0d75cb13cbe45d4622d3687c35001000000b647304402207307b33a6eb87e7fbf4c8fb5b045521904d0b063ba95b9487119f10e449f305102204fdf0e4a64b139d7a6b7323e869e35d25c6723c2dfdff66125a24e13c256c12141514c6b63046511ba60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffffd4b748368b35e8dca19831d2d0af7af8c9fef0d75cb13cbe45d4622d3687c350020000006b483045022100b30abde7b4e72fc928f361c9e06d0ae57fe8c2f364f5ba5841e5fca38715fb98022023f4b239d54a8fc1f9e962a4c1be54848af10a8be663b88db6c95461051fb3584121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acff534901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6511ba60","tx_hash":"0a4efc606b4a3ad2fc718a49692c6d07b90243d9ccacde0869104a02aa43efb3","from":["slptest:ppfpm2q9yhjm6ema48lecc8092m8fyz9fymhqwf93d"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448782,"timestamp":1622815061,"fee_details":null,"coin":"tBCH","internal_id":"694e8bcc82f8e41f5c1fa38926dd525925ebadbd45597b69a73865f0002bdf0b","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000020cec14a95a69f699659517bcbac9d01fa0b6407fe117b03e7da9e59782e8b24801000000b6473044022025d7ab1fbdb6e21b6802f8888e2cf46b9367062c81bdc3477a31720a53e3bf1802205b6c7aaadc2ebf50262ee58754d47100211339f2d5cb081711139fe0646b1ff541514c6b63042d0dba60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffff0cec14a95a69f699659517bcbac9d01fa0b6407fe117b03e7da9e59782e8b248020000006a4730440220785a8f92aeb088579c1620b2767b94fdeaaf20d719c274259e81b03b68cf3dd202203f241a3a7859469590616a8672990f2f9ac570d5f7572971fb7339244fc1c1d34121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88accf5b4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2d0dba60","tx_hash":"c99b2f8786c16677e0c1d60d980175ed69b54187451ba38f0ca943ee64e78901","from":["bchtest:prqs2sj4f30er06gyu4cy3uz9we5tpg8jvvyv5vjfn","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21586847","spent_by_me":"0.21585847","received_by_me":"0.21585847","my_balance_change":"0.00000000","block_height":1448781,"timestamp":1622813841,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"c99b2f8786c16677e0c1d60d980175ed69b54187451ba38f0ca943ee64e78901","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000020cec14a95a69f699659517bcbac9d01fa0b6407fe117b03e7da9e59782e8b24801000000b6473044022025d7ab1fbdb6e21b6802f8888e2cf46b9367062c81bdc3477a31720a53e3bf1802205b6c7aaadc2ebf50262ee58754d47100211339f2d5cb081711139fe0646b1ff541514c6b63042d0dba60b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffff0cec14a95a69f699659517bcbac9d01fa0b6407fe117b03e7da9e59782e8b248020000006a4730440220785a8f92aeb088579c1620b2767b94fdeaaf20d719c274259e81b03b68cf3dd202203f241a3a7859469590616a8672990f2f9ac570d5f7572971fb7339244fc1c1d34121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88accf5b4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2d0dba60","tx_hash":"c99b2f8786c16677e0c1d60d980175ed69b54187451ba38f0ca943ee64e78901","from":["slptest:prqs2sj4f30er06gyu4cy3uz9we5tpg8jvhst0k9mw"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448781,"timestamp":1622813841,"fee_details":null,"coin":"tBCH","internal_id":"f8e7ddbdd4b2e14abd83f43e9b4e4764dca3ccfc91e0d5dd16b627f8da2b7b73","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000022e4b6fb0b5937a3b65db99737d73b3b431226593f8c56a78d0cb07d0e162c0d7010000006a473044022052335a84b7a8665bfc42b5c0e5c13f3adcf502f304df6971881e69203ea6b9a102201a56ebacf179206d5ac20f511fc2f29c400de8c6ed93255b7ca76faac90b953e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff2e4b6fb0b5937a3b65db99737d73b3b431226593f8c56a78d0cb07d0e162c0d7020000006a47304402201f8c809b5b2715553c43334aaec96885342153210efec415775c24572a757e3a022023408a0be6286e0dde33f2114b7c455b3b270f48475bdbfcdc4e2cd23fa247f94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914c10542554c5f91bf48272b8247822bb34585079387b75f4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4e29ba60","tx_hash":"48b2e88297e5a97d3eb017e17f40b6a01fd0c9babc17956599f6695aa914ec0c","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:prqs2sj4f30er06gyu4cy3uz9we5tpg8jvvyv5vjfn","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21587847","spent_by_me":"0.21587847","received_by_me":"0.21585847","my_balance_change":"-0.00002000","block_height":1448781,"timestamp":1622813841,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"48b2e88297e5a97d3eb017e17f40b6a01fd0c9babc17956599f6695aa914ec0c","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000022e4b6fb0b5937a3b65db99737d73b3b431226593f8c56a78d0cb07d0e162c0d7010000006a473044022052335a84b7a8665bfc42b5c0e5c13f3adcf502f304df6971881e69203ea6b9a102201a56ebacf179206d5ac20f511fc2f29c400de8c6ed93255b7ca76faac90b953e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff2e4b6fb0b5937a3b65db99737d73b3b431226593f8c56a78d0cb07d0e162c0d7020000006a47304402201f8c809b5b2715553c43334aaec96885342153210efec415775c24572a757e3a022023408a0be6286e0dde33f2114b7c455b3b270f48475bdbfcdc4e2cd23fa247f94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914c10542554c5f91bf48272b8247822bb34585079387b75f4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4e29ba60","tx_hash":"48b2e88297e5a97d3eb017e17f40b6a01fd0c9babc17956599f6695aa914ec0c","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:prqs2sj4f30er06gyu4cy3uz9we5tpg8jvhst0k9mw"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448781,"timestamp":1622813841,"fee_details":null,"coin":"tBCH","internal_id":"0bed4b0e9513781343bb20b835fe3046d23719142a7576713b391941e5252b17","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002bdb9bc1b680b76eceba6e56d8d738bdeeed122361424c42682478455201d054701000000b7483045022100ceb2aeeeed61be3ab66337458419d0b50dcf980bc3db2445f51ca407ed50ab11022000cd7ef15f715a8fb0d964d5ad4744358c2f65f9193cfb60666c29eaf95e253741514c6b6304b9feb960b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffffbdb9bc1b680b76eceba6e56d8d738bdeeed122361424c42682478455201d0547020000006b483045022100c486564b03dd6d4021aed95e38694ff8163d2a2c69853820372a783b001e0ed1022073d6679a324352d8dbca4d51837217b787ec49ff284b6703960a086e26dc253b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f634901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb9feb960","tx_hash":"d7c062e1d007cbd0786ac5f893652231b4b3737d7399db653b7a93b5b06f4b2e","from":["bchtest:pqgk69yyj6dzag4mdyur9lykye89ucz9vsddce5q2t","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21588847","spent_by_me":"0.21587847","received_by_me":"0.21587847","my_balance_change":"0.00000000","block_height":1448778,"timestamp":1622810181,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"d7c062e1d007cbd0786ac5f893652231b4b3737d7399db653b7a93b5b06f4b2e","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002bdb9bc1b680b76eceba6e56d8d738bdeeed122361424c42682478455201d054701000000b7483045022100ceb2aeeeed61be3ab66337458419d0b50dcf980bc3db2445f51ca407ed50ab11022000cd7ef15f715a8fb0d964d5ad4744358c2f65f9193cfb60666c29eaf95e253741514c6b6304b9feb960b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffffbdb9bc1b680b76eceba6e56d8d738bdeeed122361424c42682478455201d0547020000006b483045022100c486564b03dd6d4021aed95e38694ff8163d2a2c69853820372a783b001e0ed1022073d6679a324352d8dbca4d51837217b787ec49ff284b6703960a086e26dc253b4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f634901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acb9feb960","tx_hash":"d7c062e1d007cbd0786ac5f893652231b4b3737d7399db653b7a93b5b06f4b2e","from":["slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448778,"timestamp":1622810181,"fee_details":null,"coin":"tBCH","internal_id":"660d57aad6e7807ee99459a77ed6b526771db8567fff99ca055d652913555d08","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002826181fbca65995c3acb4ccc1153fe6aa2514e7fbc52124aa52fe77c861f1af5010000006a4730440220593ebbfb4ae66807d87fe201d216e2f66785fba9f1d0a7494fbc7c30a83cce8a022002c74fe866e5491fedbbe03116c094053bb85bc0d1fb2ab5df1e2f976d92d6ba4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff826181fbca65995c3acb4ccc1153fe6aa2514e7fbc52124aa52fe77c861f1af5020000006a4730440220780d63fbb982c0e1c2d06db19676e6cee1fc3c900ec9c4092f83160deb18c1bc022071ca86381164f0da02a5643c0e3d1b1c0ee5ceb26a19fb71abdaf16cbdfde8ae4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914116d1484969a2ea2bb693832fc96264e5e6045648787674901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acd91aba60","tx_hash":"47051d205584478226c424143622d1eede8b738d6de5a6ebec760b681bbcb9bd","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pqgk69yyj6dzag4mdyur9lykye89ucz9vsddce5q2t","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21589847","spent_by_me":"0.21589847","received_by_me":"0.21587847","my_balance_change":"-0.00002000","block_height":1448778,"timestamp":1622810181,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"47051d205584478226c424143622d1eede8b738d6de5a6ebec760b681bbcb9bd","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002826181fbca65995c3acb4ccc1153fe6aa2514e7fbc52124aa52fe77c861f1af5010000006a4730440220593ebbfb4ae66807d87fe201d216e2f66785fba9f1d0a7494fbc7c30a83cce8a022002c74fe866e5491fedbbe03116c094053bb85bc0d1fb2ab5df1e2f976d92d6ba4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff826181fbca65995c3acb4ccc1153fe6aa2514e7fbc52124aa52fe77c861f1af5020000006a4730440220780d63fbb982c0e1c2d06db19676e6cee1fc3c900ec9c4092f83160deb18c1bc022071ca86381164f0da02a5643c0e3d1b1c0ee5ceb26a19fb71abdaf16cbdfde8ae4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914116d1484969a2ea2bb693832fc96264e5e6045648787674901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acd91aba60","tx_hash":"47051d205584478226c424143622d1eede8b738d6de5a6ebec760b681bbcb9bd","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448778,"timestamp":1622810181,"fee_details":null,"coin":"tBCH","internal_id":"e46fa0836be0534f7799b2ef5b538551ea25b6f430b7e015a95731efb7a0cd4f","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002ba95a3a80f0f8396d5ed9c8a13b122c4a9f6b338fff2b483e78ee3d9d8b75b0601000000b6473044022031bb6d5f22d205eea401c64bb0e0bf9dc9cce5d4c237a627e4742e2cce91fc6a022035db7a3be1ea37df202fc3f1e054b5b62e2f79d3530fedbd3ba7b579d9e9e89e41514c6b630477d3b960b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffffba95a3a80f0f8396d5ed9c8a13b122c4a9f6b338fff2b483e78ee3d9d8b75b06020000006a4730440220677b3221e3e2ff3cffe61ee51ae8d11fb8bb420f2a152992e4e02242bdb49fbb022028855abb6aec4ad6007c078ef1f5e9ed7055e7486972200b1fce8089ed7fdee64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6f6b4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac77d3b960","tx_hash":"f51a1f867ce72fa54a1252bc7f4e51a26afe5311cc4ccb3a5c9965cafb816182","from":["bchtest:pztgwc96yenmmh3jdw7y2h9zk0wjspzldggnphckpq","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21590847","spent_by_me":"0.21589847","received_by_me":"0.21589847","my_balance_change":"0.00000000","block_height":1448769,"timestamp":1622799170,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"f51a1f867ce72fa54a1252bc7f4e51a26afe5311cc4ccb3a5c9965cafb816182","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002ba95a3a80f0f8396d5ed9c8a13b122c4a9f6b338fff2b483e78ee3d9d8b75b0601000000b6473044022031bb6d5f22d205eea401c64bb0e0bf9dc9cce5d4c237a627e4742e2cce91fc6a022035db7a3be1ea37df202fc3f1e054b5b62e2f79d3530fedbd3ba7b579d9e9e89e41514c6b630477d3b960b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffffba95a3a80f0f8396d5ed9c8a13b122c4a9f6b338fff2b483e78ee3d9d8b75b06020000006a4730440220677b3221e3e2ff3cffe61ee51ae8d11fb8bb420f2a152992e4e02242bdb49fbb022028855abb6aec4ad6007c078ef1f5e9ed7055e7486972200b1fce8089ed7fdee64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac6f6b4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac77d3b960","tx_hash":"f51a1f867ce72fa54a1252bc7f4e51a26afe5311cc4ccb3a5c9965cafb816182","from":["slptest:pztgwc96yenmmh3jdw7y2h9zk0wjspzldgn8xvzpna"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448769,"timestamp":1622799170,"fee_details":null,"coin":"tBCH","internal_id":"2b5965f90654c62f42f0d03bf97e2f4890064012006bbedebf6f7a6cbf23859a","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002cba67d2031c614825447da0b97ca6c4c0192b6eef757ffe3a79f2924a3657860010000006b483045022100d1bbead6d6e72564cebba4fefcf58897e7a959e39ecba5ce0634242c8cf4dd9f02203d946ff23259bbf3501e0a15aad32a64adc162f5cdf6a9c64ce08ae147f6b87c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffcba67d2031c614825447da0b97ca6c4c0192b6eef757ffe3a79f2924a3657860020000006a47304402202a0469e94bb829a314a10ddc0d9e8631e129615cb24ffb03093a1051fd4d4be00220072baeb82d9044edfaac6cc1f9a7614c9ab381f92801b0ec7e15740cc26dd6f54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914968760ba2667bdde326bbc455ca2b3dd28045f6a87576f4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac98efb960","tx_hash":"065bb7d8d9e38ee783b4f2ff38b3f6a9c422b1138a9cedd596830f0fa8a395ba","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pztgwc96yenmmh3jdw7y2h9zk0wjspzldggnphckpq","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21591847","spent_by_me":"0.21591847","received_by_me":"0.21589847","my_balance_change":"-0.00002000","block_height":1448769,"timestamp":1622799170,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"065bb7d8d9e38ee783b4f2ff38b3f6a9c422b1138a9cedd596830f0fa8a395ba","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002cba67d2031c614825447da0b97ca6c4c0192b6eef757ffe3a79f2924a3657860010000006b483045022100d1bbead6d6e72564cebba4fefcf58897e7a959e39ecba5ce0634242c8cf4dd9f02203d946ff23259bbf3501e0a15aad32a64adc162f5cdf6a9c64ce08ae147f6b87c4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffcba67d2031c614825447da0b97ca6c4c0192b6eef757ffe3a79f2924a3657860020000006a47304402202a0469e94bb829a314a10ddc0d9e8631e129615cb24ffb03093a1051fd4d4be00220072baeb82d9044edfaac6cc1f9a7614c9ab381f92801b0ec7e15740cc26dd6f54121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914968760ba2667bdde326bbc455ca2b3dd28045f6a87576f4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac98efb960","tx_hash":"065bb7d8d9e38ee783b4f2ff38b3f6a9c422b1138a9cedd596830f0fa8a395ba","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pztgwc96yenmmh3jdw7y2h9zk0wjspzldgn8xvzpna"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448769,"timestamp":1622799170,"fee_details":null,"coin":"tBCH","internal_id":"d852d3768df093a28c370803bb793b3cead3d7754a961a1d668ab36d96d599ee","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002b06721589d2b86bea464fa1ea0463e0ca7c34a740d1aa8f83dd2d5a29585f306010000006b48304502210095949388142ae76b11828ac056c54fbfe1639008a4150ea4448738e4211c884b02203d85bce130c61cea1a507e071a37b68fff046e1b25e6e1e285416959b6cdec374121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb06721589d2b86bea464fa1ea0463e0ca7c34a740d1aa8f83dd2d5a29585f306020000006a473044022041a2e3f5512d0187c64e8ba9f30f6b48c8a2295be5991b825f0754933fabb88902207be81190f92ef2dca4beff8872cdcaefd18dcd8de5d07b6d84b783c310e4b8b64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914bb8b9fc2957a4f3fa71ef1bb5c6d8736e426406387f77e4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9eeab960","tx_hash":"b15e1c55f9a5906d74142becffc53869c022ecea3748aa2028943195a93c456e","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pzach87zj4ay70a8rmcmkhrdsumwgfjqvv22hqa5n7","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21595847","spent_by_me":"0.21595847","received_by_me":"0.21593847","my_balance_change":"-0.00002000","block_height":1448768,"timestamp":1622797945,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"b15e1c55f9a5906d74142becffc53869c022ecea3748aa2028943195a93c456e","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002b06721589d2b86bea464fa1ea0463e0ca7c34a740d1aa8f83dd2d5a29585f306010000006b48304502210095949388142ae76b11828ac056c54fbfe1639008a4150ea4448738e4211c884b02203d85bce130c61cea1a507e071a37b68fff046e1b25e6e1e285416959b6cdec374121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffb06721589d2b86bea464fa1ea0463e0ca7c34a740d1aa8f83dd2d5a29585f306020000006a473044022041a2e3f5512d0187c64e8ba9f30f6b48c8a2295be5991b825f0754933fabb88902207be81190f92ef2dca4beff8872cdcaefd18dcd8de5d07b6d84b783c310e4b8b64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e80300000000000017a914bb8b9fc2957a4f3fa71ef1bb5c6d8736e426406387f77e4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9eeab960","tx_hash":"b15e1c55f9a5906d74142becffc53869c022ecea3748aa2028943195a93c456e","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pzach87zj4ay70a8rmcmkhrdsumwgfjqvv37sm8rpr"],"total_amount":"1","spent_by_me":"1","received_by_me":"0","my_balance_change":"-1","block_height":1448768,"timestamp":1622797945,"fee_details":null,"coin":"tBCH","internal_id":"5fcf9bd0019e3d35616b21f872888804d897f74dae2191b6fa3b734ee15aabf1","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000002b62a5f43d321a4f4e6585e92415747bf38719582b99ea37e0bd3310ce2969a1a01000000b647304402207a5be062e97d9db2426a531bd73320295f0a3a2b36f1c840415360daf37e9d69022051a31dc179df30f9193c1b433c87aaa00e89368cff2f852f8279e657867f1aa541514c6b63042fcfb960b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffffb62a5f43d321a4f4e6585e92415747bf38719582b99ea37e0bd3310ce2969a1a030000006b483045022100e9d76e8581c2f17d9131f3078f33c88d90016ad81f6842c19eba9a1eca07b08a0220091d90a4ec9e31d25cceaff5fb8625772af9f5925d57105d56fec23aacea6a6d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3f734901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2fcfb960","tx_hash":"607865a324299fa7e3ff57f7eeb692014c6cca970bda47548214c631207da6cb","from":["bchtest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu3d5sdzs2","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21592847","spent_by_me":"0.21591847","received_by_me":"0.21591847","my_balance_change":"0.00000000","block_height":1448768,"timestamp":1622797945,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"607865a324299fa7e3ff57f7eeb692014c6cca970bda47548214c631207da6cb","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000002b62a5f43d321a4f4e6585e92415747bf38719582b99ea37e0bd3310ce2969a1a01000000b647304402207a5be062e97d9db2426a531bd73320295f0a3a2b36f1c840415360daf37e9d69022051a31dc179df30f9193c1b433c87aaa00e89368cff2f852f8279e657867f1aa541514c6b63042fcfb960b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68feffffffb62a5f43d321a4f4e6585e92415747bf38719582b99ea37e0bd3310ce2969a1a030000006b483045022100e9d76e8581c2f17d9131f3078f33c88d90016ad81f6842c19eba9a1eca07b08a0220091d90a4ec9e31d25cceaff5fb8625772af9f5925d57105d56fec23aacea6a6d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac3f734901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2fcfb960","tx_hash":"607865a324299fa7e3ff57f7eeb692014c6cca970bda47548214c631207da6cb","from":["slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448768,"timestamp":1622797945,"fee_details":null,"coin":"tBCH","internal_id":"fc666307cafcbf29e4b95ccc261a24603c8168535283c6ed8243d4cd8c2543c8","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000022bf9a77501f51dc1c2aa5b0998c385813c18fa67649baa49e6490edb5f78a7af020000006a473044022051d38d2d762d868f00040584b5f7fccf396eccb5d4099ac952ea00f788c672f5022030d0d1a31e15a1935db5f079a898c586b360bc62259ed313156e4913fa6271514121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff6e453ca99531942820aa4837eaec22c06938c5ffec2b14746d90a5f9551c5eb1020000006b4830450221009645b0710ac1427a2deba4721d68caccf811bae1f52a29acac2c7b3a6f160ea202206b28cde5ce396abf4d5bb37104cc7d9aef12a9a1ea70aca79b4889d56ac31a2a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710080000000000013498e80300000000000017a91452d0e96a043c3c78ae34821731f77307ef68d46787e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac27774901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4febb960","tx_hash":"1a9a96e20c31d30b7ea39eb982957138bf475741925e58e6f4a421d3435f2ab6","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu3d5sdzs2","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21594847","spent_by_me":"0.21594847","received_by_me":"0.21592847","my_balance_change":"-0.00002000","block_height":1448768,"timestamp":1622797945,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"1a9a96e20c31d30b7ea39eb982957138bf475741925e58e6f4a421d3435f2ab6","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000022bf9a77501f51dc1c2aa5b0998c385813c18fa67649baa49e6490edb5f78a7af020000006a473044022051d38d2d762d868f00040584b5f7fccf396eccb5d4099ac952ea00f788c672f5022030d0d1a31e15a1935db5f079a898c586b360bc62259ed313156e4913fa6271514121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff6e453ca99531942820aa4837eaec22c06938c5ffec2b14746d90a5f9551c5eb1020000006b4830450221009645b0710ac1427a2deba4721d68caccf811bae1f52a29acac2c7b3a6f160ea202206b28cde5ce396abf4d5bb37104cc7d9aef12a9a1ea70aca79b4889d56ac31a2a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710080000000000013498e80300000000000017a91452d0e96a043c3c78ae34821731f77307ef68d46787e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac27774901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac4febb960","tx_hash":"1a9a96e20c31d30b7ea39eb982957138bf475741925e58e6f4a421d3435f2ab6","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"8.9","spent_by_me":"8.9","received_by_me":"7.9","my_balance_change":"-1.0","block_height":1448768,"timestamp":1622797945,"fee_details":null,"coin":"tBCH","internal_id":"fe78e04399219ef75271019f6d5db5d77179e9f310f8364604a6e4e05c4d7563","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"010000000259eb9eb93d9e2a571e44997132248b3e595422375f9f134395fdef1211633ecf020000006b483045022100c8abc9eed50341d8ab008efed4e56080a494999cdc53d173622be59e46e262ea02201077d04e01561af4b14adea0611a52134084385524fa37409c566cb98a39aa914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff59eb9eb93d9e2a571e44997132248b3e595422375f9f134395fdef1211633ecf030000006a473044022049dd947c0b5ccceb1920ef0bb297de0fe287c34a8d7d9ed39209776a191850fe022069d98060c395e61fac90bcecb800e981ab5903fe50eaa0e1073d9efdadf227ce4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710080000000000015ba8e80300000000000017a914169fdaecb872081837b47440f82052a0be7b2d8c87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc7864901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac00d2b960","tx_hash":"afa7785fdb0e49e649aa9b6467fa183c8185c398095baac2c11df50175a7f92b","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21598847","spent_by_me":"0.21598847","received_by_me":"0.21596847","my_balance_change":"-0.00002000","block_height":1448763,"timestamp":1622791834,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"afa7785fdb0e49e649aa9b6467fa183c8185c398095baac2c11df50175a7f92b","transaction_type":"StandardTransfer"}, +{"tx_hex":"010000000259eb9eb93d9e2a571e44997132248b3e595422375f9f134395fdef1211633ecf020000006b483045022100c8abc9eed50341d8ab008efed4e56080a494999cdc53d173622be59e46e262ea02201077d04e01561af4b14adea0611a52134084385524fa37409c566cb98a39aa914121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff59eb9eb93d9e2a571e44997132248b3e595422375f9f134395fdef1211633ecf030000006a473044022049dd947c0b5ccceb1920ef0bb297de0fe287c34a8d7d9ed39209776a191850fe022069d98060c395e61fac90bcecb800e981ab5903fe50eaa0e1073d9efdadf227ce4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710080000000000015ba8e80300000000000017a914169fdaecb872081837b47440f82052a0be7b2d8c87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acc7864901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac00d2b960","tx_hash":"afa7785fdb0e49e649aa9b6467fa183c8185c398095baac2c11df50175a7f92b","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3smapthuvm","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"9.9","spent_by_me":"9.9","received_by_me":"8.9","my_balance_change":"-1.0","block_height":1448763,"timestamp":1622791834,"fee_details":null,"coin":"tBCH","internal_id":"97108642f76be5a9f4be8f35ea4057d12240c680ff782badc4a54b32c86c3b92","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000022bf9a77501f51dc1c2aa5b0998c385813c18fa67649baa49e6490edb5f78a7af01000000d7473044022010073013a6fe700b0d8c81a027f32a0d0a35c14511a45f65c57d644531b7f0c602206d070b6fb1bb6d5b8c073e2e10fb7eca0386473b2c88839a99e61f54ec8d855b41200000000000000000000000000000000000000000000000000000000000000000004c6b6304ffd1b960b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff2bf9a77501f51dc1c2aa5b0998c385813c18fa67649baa49e6490edb5f78a7af030000006a47304402205278a8722609beb16cf872622224255327238f46b28b90650b8307ce634053ca022003b03a2b1abbe57f6031bcfad7b1ddb31e853dcc275de07728ba45e396bb0bd14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdf824901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac00d2b960","tx_hash":"06f38595a2d5d23df8a81a0d744ac3a70c3e46a01efa64a4be862b9d582167b0","from":["bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21596847","spent_by_me":"0.21595847","received_by_me":"0.21595847","my_balance_change":"0.00000000","block_height":1448763,"timestamp":1622791834,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"06f38595a2d5d23df8a81a0d744ac3a70c3e46a01efa64a4be862b9d582167b0","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000022bf9a77501f51dc1c2aa5b0998c385813c18fa67649baa49e6490edb5f78a7af01000000d7473044022010073013a6fe700b0d8c81a027f32a0d0a35c14511a45f65c57d644531b7f0c602206d070b6fb1bb6d5b8c073e2e10fb7eca0386473b2c88839a99e61f54ec8d855b41200000000000000000000000000000000000000000000000000000000000000000004c6b6304ffd1b960b17521036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc68821036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cac68ffffffff2bf9a77501f51dc1c2aa5b0998c385813c18fa67649baa49e6490edb5f78a7af030000006a47304402205278a8722609beb16cf872622224255327238f46b28b90650b8307ce634053ca022003b03a2b1abbe57f6031bcfad7b1ddb31e853dcc275de07728ba45e396bb0bd14121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000002710e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acdf824901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac00d2b960","tx_hash":"06f38595a2d5d23df8a81a0d744ac3a70c3e46a01efa64a4be862b9d582167b0","from":["slptest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3smapthuvm"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"1","spent_by_me":"0","received_by_me":"1","my_balance_change":"1","block_height":1448763,"timestamp":1622791834,"fee_details":null,"coin":"tBCH","internal_id":"adbef43a6d839be9edbc03620c9e2fdd5ef4292db33d8fee3e554b8beb55438b","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"01000000026eb9e68f3760e787a0924defa136b9d164b789cfb46ccf732c33a22ef29ccc0f010000006b483045022100fd0cba042d5834d2777a0fd1896ce55324375f636e914e654c6489218eb094250220401442f474bc86e63251a24f20c6704e2507b06ba88b84a539ad96a43bbc8d024121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff6eb9e68f3760e787a0924defa136b9d164b789cfb46ccf732c33a22ef29ccc0f020000006b483045022100cfe2f7fd47f9178f266c81a51f7dc69f5c16e397bb3f768383f7fff8123d15f002203582254c5f3eee2af4da7ee9600ef1e5a77c811d9353b23c85dd0f230dd83def4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000182b8e80300000000000017a9141fc203324c09e033000eb5914b6341b3dad3128087e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac978e4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2bbfb960","tx_hash":"cf3e631112effd9543139f5f372254593e8b24327199441e572a9e3db99eeb59","from":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"to":["bchtest:pq0uyqejfsy7qvcqp66ezjmrgxea45cjsqnw5tuq4p","bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21600847","spent_by_me":"0.21600847","received_by_me":"0.21598847","my_balance_change":"-0.00002000","block_height":1448759,"timestamp":1622786934,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"tBCH","internal_id":"cf3e631112effd9543139f5f372254593e8b24327199441e572a9e3db99eeb59","transaction_type":"StandardTransfer"}, +{"tx_hex":"01000000026eb9e68f3760e787a0924defa136b9d164b789cfb46ccf732c33a22ef29ccc0f010000006b483045022100fd0cba042d5834d2777a0fd1896ce55324375f636e914e654c6489218eb094250220401442f474bc86e63251a24f20c6704e2507b06ba88b84a539ad96a43bbc8d024121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff6eb9e68f3760e787a0924defa136b9d164b789cfb46ccf732c33a22ef29ccc0f020000006b483045022100cfe2f7fd47f9178f266c81a51f7dc69f5c16e397bb3f768383f7fff8123d15f002203582254c5f3eee2af4da7ee9600ef1e5a77c811d9353b23c85dd0f230dd83def4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e80800000000000182b8e80300000000000017a9141fc203324c09e033000eb5914b6341b3dad3128087e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac978e4901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2bbfb960","tx_hash":"cf3e631112effd9543139f5f372254593e8b24327199441e572a9e3db99eeb59","from":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"to":["slptest:pq0uyqejfsy7qvcqp66ezjmrgxea45cjsqg6nsxh8u","slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"10","spent_by_me":"10","received_by_me":"9.9","my_balance_change":"-0.1","block_height":1448759,"timestamp":1622786934,"fee_details":null,"coin":"tBCH","internal_id":"860c0638239fd474aa912ea59e59fa1293f10618248333ad15f6b71e0c3f0467","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}}, +{"tx_hex":"0100000007ef36129aeafbb5b35dbc2338ad48b73104ab776fa4c50bfc2ba5c4dafb76c4be020000006441cbdb096620d569f33a9d474f51736aab010c719f97b6c4603815cc92d11d1bd66c377b833e3c40c6205bbdbe9baa927e7f1280fa3e015fa17bd2c9c5e885b2a0412102d35051b984db235450829ea9a8e4dfde91e27be7ab7153224394eacd97046461feffffff133a04916fe4d25cc36598f7af4c3398f79b1aa3e85d47b857543bf0d1e1fdd2010000006441511f8178c54d56b178aa40293bb72b7556405b8fbd28d343160e8e9ea082710641028d7a1fd8b2c6b9e44cba10e210ab56fc531fc0fff5b79c3e00d810ced7f541210392a76e77f476e89b447c2f1fea192701f6c0fa2f49f04794294746997e8b3536feffffffd3cf85f86c071c3583f33d40c145e7ffd1cc039cfcb12faf2e326638f5fe0048010000006441a1885a66ac77008f5d487e37dba4fe4db222840a5efb3e4c4a7aeb6b7432ec5455e48941cf16ec55c90db91f63e9ad9b785eecb270b4d07de14384bee0fd2536412103dbd12a3753e7863223f8ac82f01ffa590714b8cdb42512153335025e5404dc94feffffffe319a50fda84c5bc87e92e16aebe83789aa7724f23e619087ab516d3f74e40a30100000064419c70094bef4954117c189692c184528946baf09cc5215eb2dd5103f08025f1e1ccb3efc4087b17535a7aa6fa1fd835c3b0e52159d01eba9f165be931f1962e7c4121032ab3389e0b58bbb6962dc804917293b995a25a44e6028e73894ecdfe5ff9f208feffffffa2621f1149fd3f230c051e1b210c6494f32fa398e8335d0894d56e9f2c8d955e010000006441c21f3a04f7c71dd04773fe862ec53193764b5b1eef81f5d3085e565ecb6a0d16a3a213b65ed2137f2f66701aab8f625a1a49b1f128ed87e5ce44b2c7d5363dc44121024b669a41202061b0cb954e17488f6c7191f54e2fe06304a59eaa903be19ff3d0feffffffdf196c6edbac91a9c3d73b385c98c93e01d7dc58e425e6d4d2a5e3164192017501000000644117e9fda726464c2ae70409ee983aacb4888f11335c9fa3ceedb5c2ca4f9c57ec516364c41e6c994691ef98df780e4d7540be9afb2fde5fea20d39d0432f65548412103eb08706246d042f3e76cdf9a87db6d7a3d2432a58043be35f3f6277a6898c1dffeffffff2cbbdd13b3577043cc175d77a82b08e27302703d6d6a0628a7e77a3aa4593ab7000000006441360b9cb442639b313f3c98755c7992b08462ed73e2869468372438af21fbf714e656f27238267125212423e0bd4fcb2dd8131ef3cc752bf234700df46d96203d41210359a24bddf1ebfb6bcf43dbbccc6f59d2bbc3bc836f896aaf28dd67ed3e210a51feffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000186a022020000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2d984901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac351b1600","tx_hash":"0fcc9cf22ea2332c73cf6cb4cf89b764d1b936a1ef4d92a087e760378fe6b96e","from":["bchtest:qp5fphvvj3pvrrv2awhm7dyu8xjueydapg3ju9kwmm","bchtest:qqayyhsx4p9am5gd54sjeelzpqxm0jncs5mgp452d8","bchtest:qqqxycuccwny828aqylsw8uugrpa33gelg8znan59t","bchtest:qr28js92h5gjv2dcau4twu8tma296kqt6yw9490acu","bchtest:qrrh9jq3zkagxnncyv60e94kv4jnt3yw9s63ykwahy","bchtest:qz7yzmqjfx0wa9hvyfjp7sdc8fkevk44svnvtuepnf","bchtest:qzlf6nyj7aag69l5h862w254ftd75anegcxecncczh"],"to":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"total_amount":"0.21601976","spent_by_me":"0","received_by_me":"0.21600847","my_balance_change":"0.21600847","block_height":1448758,"timestamp":1622785714,"fee_details":{"type":"Utxo","amount":"0.00001129"},"coin":"tBCH","internal_id":"0fcc9cf22ea2332c73cf6cb4cf89b764d1b936a1ef4d92a087e760378fe6b96e","transaction_type":"StandardTransfer"}, +{"tx_hex":"0100000007ef36129aeafbb5b35dbc2338ad48b73104ab776fa4c50bfc2ba5c4dafb76c4be020000006441cbdb096620d569f33a9d474f51736aab010c719f97b6c4603815cc92d11d1bd66c377b833e3c40c6205bbdbe9baa927e7f1280fa3e015fa17bd2c9c5e885b2a0412102d35051b984db235450829ea9a8e4dfde91e27be7ab7153224394eacd97046461feffffff133a04916fe4d25cc36598f7af4c3398f79b1aa3e85d47b857543bf0d1e1fdd2010000006441511f8178c54d56b178aa40293bb72b7556405b8fbd28d343160e8e9ea082710641028d7a1fd8b2c6b9e44cba10e210ab56fc531fc0fff5b79c3e00d810ced7f541210392a76e77f476e89b447c2f1fea192701f6c0fa2f49f04794294746997e8b3536feffffffd3cf85f86c071c3583f33d40c145e7ffd1cc039cfcb12faf2e326638f5fe0048010000006441a1885a66ac77008f5d487e37dba4fe4db222840a5efb3e4c4a7aeb6b7432ec5455e48941cf16ec55c90db91f63e9ad9b785eecb270b4d07de14384bee0fd2536412103dbd12a3753e7863223f8ac82f01ffa590714b8cdb42512153335025e5404dc94feffffffe319a50fda84c5bc87e92e16aebe83789aa7724f23e619087ab516d3f74e40a30100000064419c70094bef4954117c189692c184528946baf09cc5215eb2dd5103f08025f1e1ccb3efc4087b17535a7aa6fa1fd835c3b0e52159d01eba9f165be931f1962e7c4121032ab3389e0b58bbb6962dc804917293b995a25a44e6028e73894ecdfe5ff9f208feffffffa2621f1149fd3f230c051e1b210c6494f32fa398e8335d0894d56e9f2c8d955e010000006441c21f3a04f7c71dd04773fe862ec53193764b5b1eef81f5d3085e565ecb6a0d16a3a213b65ed2137f2f66701aab8f625a1a49b1f128ed87e5ce44b2c7d5363dc44121024b669a41202061b0cb954e17488f6c7191f54e2fe06304a59eaa903be19ff3d0feffffffdf196c6edbac91a9c3d73b385c98c93e01d7dc58e425e6d4d2a5e3164192017501000000644117e9fda726464c2ae70409ee983aacb4888f11335c9fa3ceedb5c2ca4f9c57ec516364c41e6c994691ef98df780e4d7540be9afb2fde5fea20d39d0432f65548412103eb08706246d042f3e76cdf9a87db6d7a3d2432a58043be35f3f6277a6898c1dffeffffff2cbbdd13b3577043cc175d77a82b08e27302703d6d6a0628a7e77a3aa4593ab7000000006441360b9cb442639b313f3c98755c7992b08462ed73e2869468372438af21fbf714e656f27238267125212423e0bd4fcb2dd8131ef3cc752bf234700df46d96203d41210359a24bddf1ebfb6bcf43dbbccc6f59d2bbc3bc836f896aaf28dd67ed3e210a51feffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000186a022020000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac2d984901000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac351b1600","tx_hash":"0fcc9cf22ea2332c73cf6cb4cf89b764d1b936a1ef4d92a087e760378fe6b96e","from":["slptest:qp5fphvvj3pvrrv2awhm7dyu8xjueydapg2xm7vefx","slptest:qqayyhsx4p9am5gd54sjeelzpqxm0jncs5quxwwal6","slptest:qqqxycuccwny828aqylsw8uugrpa33gelguk5xfrhk","slptest:qr28js92h5gjv2dcau4twu8tma296kqt6y43j7422p","slptest:qrrh9jq3zkagxnncyv60e94kv4jnt3yw9sp9rd529e","slptest:qz7yzmqjfx0wa9hvyfjp7sdc8fkevk44svgcv8rkp5"],"to":["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8"],"total_amount":"10","spent_by_me":"0","received_by_me":"10","my_balance_change":"10","block_height":1448758,"timestamp":1622785714,"fee_details":null,"coin":"tBCH","internal_id":"fe1e72fc17cda2ad5e8d52e73a65ca89d6ab364311d940f2f0600329ce40de7e","transaction_type":{"TokenTransfer":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"}} +] diff --git a/mm2src/coins/hd_pubkey.rs b/mm2src/coins/hd_pubkey.rs new file mode 100644 index 0000000000..a90accbc69 --- /dev/null +++ b/mm2src/coins/hd_pubkey.rs @@ -0,0 +1,208 @@ +use crate::hd_wallet::{HDWalletRpcError, NewAccountCreatingError}; +use async_trait::async_trait; +use crypto::hw_rpc_task::{HwConnectStatuses, TrezorRpcTaskConnectProcessor}; +use crypto::trezor::trezor_rpc_task::TrezorRpcTaskProcessor; +use crypto::trezor::utxo::TrezorUtxoCoin; +use crypto::trezor::{ProcessTrezorResponse, TrezorError, TrezorPinMatrix3x3Response, TrezorProcessingError}; +use crypto::{Bip32Error, CryptoCtx, CryptoInitError, DerivationPath, EcdsaCurve, HardwareWalletArc, HwError, + HwProcessingError, XPub}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandle}; +use std::convert::TryInto; + +#[derive(Clone)] +pub enum HDExtractPubkeyError { + HwContextNotInitialized, + CoinDoesntSupportTrezor, + RpcTaskError(RpcTaskError), + HardwareWalletError(HwError), + InvalidXpub(Bip32Error), + Internal(String), +} + +impl From for HDExtractPubkeyError { + fn from(e: CryptoInitError) -> Self { HDExtractPubkeyError::Internal(e.to_string()) } +} + +impl From for HDExtractPubkeyError { + fn from(e: TrezorError) -> Self { HDExtractPubkeyError::HardwareWalletError(HwError::from(e)) } +} + +impl From for HDExtractPubkeyError { + fn from(e: HwError) -> Self { HDExtractPubkeyError::HardwareWalletError(e) } +} + +impl From> for HDExtractPubkeyError { + fn from(e: TrezorProcessingError) -> Self { + match e { + TrezorProcessingError::TrezorError(trezor) => HDExtractPubkeyError::from(HwError::from(trezor)), + TrezorProcessingError::ProcessorError(rpc) => HDExtractPubkeyError::RpcTaskError(rpc), + } + } +} + +impl From> for HDExtractPubkeyError { + fn from(e: HwProcessingError) -> Self { + match e { + HwProcessingError::HwError(hw) => HDExtractPubkeyError::from(hw), + HwProcessingError::ProcessorError(rpc) => HDExtractPubkeyError::RpcTaskError(rpc), + } + } +} + +impl From for NewAccountCreatingError { + fn from(e: HDExtractPubkeyError) -> Self { + match e { + HDExtractPubkeyError::HwContextNotInitialized => NewAccountCreatingError::HwContextNotInitialized, + HDExtractPubkeyError::CoinDoesntSupportTrezor => NewAccountCreatingError::CoinDoesntSupportTrezor, + HDExtractPubkeyError::RpcTaskError(rpc) => NewAccountCreatingError::RpcTaskError(rpc), + HDExtractPubkeyError::HardwareWalletError(hw) => NewAccountCreatingError::HardwareWalletError(hw), + HDExtractPubkeyError::InvalidXpub(xpub) => { + NewAccountCreatingError::HardwareWalletError(HwError::InvalidXpub(xpub)) + }, + HDExtractPubkeyError::Internal(internal) => NewAccountCreatingError::Internal(internal), + } + } +} + +impl From for HDWalletRpcError { + fn from(e: HDExtractPubkeyError) -> Self { + match e { + HDExtractPubkeyError::HwContextNotInitialized => HDWalletRpcError::HwContextNotInitialized, + HDExtractPubkeyError::CoinDoesntSupportTrezor => HDWalletRpcError::CoinDoesntSupportTrezor, + HDExtractPubkeyError::RpcTaskError(rpc) => HDWalletRpcError::from(rpc), + HDExtractPubkeyError::HardwareWalletError(hw) => HDWalletRpcError::from(hw), + HDExtractPubkeyError::InvalidXpub(xpub) => HDWalletRpcError::from(HwError::InvalidXpub(xpub)), + HDExtractPubkeyError::Internal(internal) => HDWalletRpcError::Internal(internal), + } + } +} + +#[async_trait] +pub trait ExtractExtendedPubkey { + type ExtendedPublicKey; + + async fn extract_extended_pubkey( + &self, + xpub_extractor: &XPubExtractor, + derivation_path: DerivationPath, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Sync; +} + +#[async_trait] +pub trait HDXPubExtractor { + async fn extract_utxo_xpub( + &self, + trezor_utxo_coin: TrezorUtxoCoin, + derivation_path: DerivationPath, + ) -> MmResult; +} + +pub enum RpcTaskXPubExtractor<'task, Task: RpcTask> { + Trezor { + hw_ctx: HardwareWalletArc, + task_handle: &'task RpcTaskHandle, + statuses: HwConnectStatuses, + }, +} + +#[async_trait] +impl<'task, Task> HDXPubExtractor for RpcTaskXPubExtractor<'task, Task> +where + Task: RpcTask, + Task::UserAction: TryInto + Send, +{ + async fn extract_utxo_xpub( + &self, + trezor_utxo_coin: TrezorUtxoCoin, + derivation_path: DerivationPath, + ) -> MmResult { + match self { + RpcTaskXPubExtractor::Trezor { + hw_ctx, + task_handle, + statuses, + } => { + Self::extract_utxo_xpub_from_trezor(hw_ctx, task_handle, statuses, trezor_utxo_coin, derivation_path) + .await + }, + } + } +} + +impl<'task, Task> RpcTaskXPubExtractor<'task, Task> +where + Task: RpcTask, + Task::UserAction: TryInto + Send, +{ + pub fn new( + ctx: &MmArc, + task_handle: &'task RpcTaskHandle, + statuses: HwConnectStatuses, + ) -> MmResult, HDExtractPubkeyError> { + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| HDExtractPubkeyError::HwContextNotInitialized)?; + Ok(RpcTaskXPubExtractor::Trezor { + hw_ctx, + task_handle, + statuses, + }) + } + + /// Constructs an Xpub extractor without checking if the MarketMaker is initialized with a hardware wallet. + pub fn new_unchecked( + ctx: &MmArc, + task_handle: &'task RpcTaskHandle, + statuses: HwConnectStatuses, + ) -> XPubExtractorUnchecked> { + XPubExtractorUnchecked(Self::new(ctx, task_handle, statuses)) + } + + async fn extract_utxo_xpub_from_trezor( + hw_ctx: &HardwareWalletArc, + task_handle: &RpcTaskHandle, + statuses: &HwConnectStatuses, + trezor_coin: TrezorUtxoCoin, + derivation_path: DerivationPath, + ) -> MmResult { + let connect_processor = TrezorRpcTaskConnectProcessor::new(task_handle, statuses.clone()); + let trezor = hw_ctx.trezor(&connect_processor).await?; + let mut trezor_session = trezor.session().await?; + + let pubkey_processor = TrezorRpcTaskProcessor::new(task_handle, statuses.to_trezor_request_statuses()); + trezor_session + .get_public_key(derivation_path, trezor_coin, EcdsaCurve::Secp256k1) + .await? + .process(&pubkey_processor) + .await + .mm_err(HDExtractPubkeyError::from) + } +} + +/// This is a wrapper over `XPubExtractor`. The main goal of this structure is to allow construction of an Xpub extractor +/// even if HD wallet is not supported. But if someone tries to extract an Xpub despite HD wallet is not supported, +/// it fails with an inner `HDExtractPubkeyError` error. +pub struct XPubExtractorUnchecked(MmResult); + +#[async_trait] +impl HDXPubExtractor for XPubExtractorUnchecked +where + XPubExtractor: HDXPubExtractor + Send + Sync, +{ + async fn extract_utxo_xpub( + &self, + trezor_utxo_coin: TrezorUtxoCoin, + derivation_path: DerivationPath, + ) -> MmResult { + self.0 + .as_ref() + .map_err(Clone::clone)? + .extract_utxo_xpub(trezor_utxo_coin, derivation_path) + .await + } +} diff --git a/mm2src/coins/hd_wallet.rs b/mm2src/coins/hd_wallet.rs new file mode 100644 index 0000000000..cb365a01e5 --- /dev/null +++ b/mm2src/coins/hd_wallet.rs @@ -0,0 +1,549 @@ +use crate::coin_balance::HDAddressBalance; +use crate::hd_pubkey::HDXPubExtractor; +use crate::hd_wallet_storage::HDWalletStorageError; +use crate::{lp_coinfind_or_err, BalanceError, CoinFindError, CoinWithDerivationMethod, MmCoinEnum, + UnexpectedDerivationMethod, WithdrawError}; +use async_trait::async_trait; +use common::HttpStatusCode; +use crypto::{Bip32DerPathError, Bip32Error, Bip44Chain, Bip44DerPathError, Bip44DerivationPath, ChildNumber, + DerivationPath, HwError}; +use derive_more::Display; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc_task::RpcTaskError; +use serde::Serialize; +use std::collections::BTreeMap; +use std::time::Duration; + +pub use futures::lock::{MappedMutexGuard as AsyncMappedMutexGuard, Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; + +pub type HDAccountsMap = BTreeMap; +pub type HDAccountsMutex = AsyncMutex>; +pub type HDAccountsMut<'a, HDAccount> = AsyncMutexGuard<'a, HDAccountsMap>; +pub type HDAccountMut<'a, HDAccount> = AsyncMappedMutexGuard<'a, HDAccountsMap, HDAccount>; + +#[derive(Display)] +pub enum AddressDerivingError { + #[display(fmt = "BIP32 address deriving error: {}", _0)] + Bip32Error(Bip32Error), +} + +impl From for AddressDerivingError { + fn from(e: Bip32Error) -> Self { AddressDerivingError::Bip32Error(e) } +} + +impl From for BalanceError { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::Bip32Error(bip32) => BalanceError::Internal(bip32.to_string()), + } + } +} + +impl From for WithdrawError { + fn from(e: AddressDerivingError) -> Self { WithdrawError::UnexpectedFromAddress(e.to_string()) } +} + +pub enum NewAddressDerivingError { + AddressLimitReached { max_addresses_number: u32 }, + InvalidBip44Chain { chain: Bip44Chain }, + Bip32Error(Bip32Error), + WalletStorageError(HDWalletStorageError), +} + +impl From for NewAddressDerivingError { + fn from(e: Bip32Error) -> Self { NewAddressDerivingError::Bip32Error(e) } +} + +impl From for NewAddressDerivingError { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::Bip32Error(bip32) => NewAddressDerivingError::Bip32Error(bip32), + } + } +} + +impl From for NewAddressDerivingError { + fn from(e: InvalidBip44ChainError) -> Self { NewAddressDerivingError::InvalidBip44Chain { chain: e.chain } } +} + +impl From for NewAddressDerivingError { + fn from(e: AccountUpdatingError) -> Self { + match e { + AccountUpdatingError::AddressLimitReached { max_addresses_number } => { + NewAddressDerivingError::AddressLimitReached { max_addresses_number } + }, + AccountUpdatingError::InvalidBip44Chain(e) => NewAddressDerivingError::from(e), + AccountUpdatingError::WalletStorageError(storage) => NewAddressDerivingError::WalletStorageError(storage), + } + } +} + +#[derive(Display)] +pub enum NewAccountCreatingError { + #[display(fmt = "Hardware Wallet context is not initialized")] + HwContextNotInitialized, + #[display(fmt = "HD wallet is unavailable")] + HDWalletUnavailable, + #[display( + fmt = "Coin doesn't support Trezor hardware wallet. Please consider adding the 'trezor_coin' field to the coins config" + )] + CoinDoesntSupportTrezor, + RpcTaskError(RpcTaskError), + HardwareWalletError(HwError), + #[display(fmt = "Accounts limit reached. Max number of accounts: {}", max_accounts_number)] + AccountLimitReached { + max_accounts_number: u32, + }, + #[display(fmt = "Error saving HD account to storage: {}", _0)] + ErrorSavingAccountToStorage(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for NewAccountCreatingError { + fn from(e: Bip32DerPathError) -> Self { NewAccountCreatingError::Internal(Bip44DerPathError::from(e).to_string()) } +} + +impl From for NewAccountCreatingError { + fn from(e: HDWalletStorageError) -> Self { + match e { + HDWalletStorageError::ErrorSaving(e) | HDWalletStorageError::ErrorSerializing(e) => { + NewAccountCreatingError::ErrorSavingAccountToStorage(e) + }, + HDWalletStorageError::HDWalletUnavailable => NewAccountCreatingError::HDWalletUnavailable, + HDWalletStorageError::Internal(internal) => NewAccountCreatingError::Internal(internal), + other => NewAccountCreatingError::Internal(other.to_string()), + } + } +} + +impl From for HDWalletRpcError { + fn from(e: NewAccountCreatingError) -> Self { + match e { + NewAccountCreatingError::HwContextNotInitialized => HDWalletRpcError::HwContextNotInitialized, + NewAccountCreatingError::HDWalletUnavailable => HDWalletRpcError::CoinIsActivatedNotWithHDWallet, + NewAccountCreatingError::CoinDoesntSupportTrezor => HDWalletRpcError::CoinDoesntSupportTrezor, + NewAccountCreatingError::RpcTaskError(rpc) => HDWalletRpcError::from(rpc), + NewAccountCreatingError::HardwareWalletError(hw) => HDWalletRpcError::from(hw), + NewAccountCreatingError::AccountLimitReached { max_accounts_number } => { + HDWalletRpcError::AccountLimitReached { max_accounts_number } + }, + NewAccountCreatingError::ErrorSavingAccountToStorage(e) => { + let error = format!("Error uploading HD account info to the storage: {}", e); + HDWalletRpcError::WalletStorageError(error) + }, + NewAccountCreatingError::Internal(internal) => HDWalletRpcError::Internal(internal), + } + } +} + +/// Currently, we suppose that ETH/ERC20/QRC20 don't have [`Bip44Chain::Internal`] addresses. +#[derive(Display)] +#[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] +pub struct InvalidBip44ChainError { + pub chain: Bip44Chain, +} + +#[derive(Display)] +pub enum AccountUpdatingError { + AddressLimitReached { max_addresses_number: u32 }, + InvalidBip44Chain(InvalidBip44ChainError), + WalletStorageError(HDWalletStorageError), +} + +impl From for AccountUpdatingError { + fn from(e: InvalidBip44ChainError) -> Self { AccountUpdatingError::InvalidBip44Chain(e) } +} + +impl From for AccountUpdatingError { + fn from(e: HDWalletStorageError) -> Self { AccountUpdatingError::WalletStorageError(e) } +} + +impl From for BalanceError { + fn from(e: AccountUpdatingError) -> Self { + let error = e.to_string(); + match e { + AccountUpdatingError::AddressLimitReached { .. } | AccountUpdatingError::InvalidBip44Chain(_) => { + // Account updating is expected to be called after `address_id` and `chain` validation. + BalanceError::Internal(format!("Unexpected internal error: {}", error)) + }, + AccountUpdatingError::WalletStorageError(_) => BalanceError::WalletStorageError(error), + } + } +} + +#[derive(Clone, Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum HDWalletRpcError { + /* ----------- Trezor device errors ----------- */ + #[display(fmt = "Trezor device disconnected")] + TrezorDisconnected, + #[display(fmt = "Trezor internal error: {}", _0)] + HardwareWalletInternal(String), + #[display(fmt = "No Trezor device available")] + NoTrezorDeviceAvailable, + #[display(fmt = "Unexpected Hardware Wallet device: {}", _0)] + FoundUnexpectedDevice(String), + #[display( + fmt = "Coin doesn't support Trezor hardware wallet. Please consider adding the 'trezor_coin' field to the coins config" + )] + CoinDoesntSupportTrezor, + /* ----------- HD Wallet RPC error ------------ */ + #[display(fmt = "Hardware Wallet context is not initialized")] + HwContextNotInitialized, + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "RPC timed out {:?}", _0)] + Timeout(Duration), + #[display(fmt = "Coin is expected to be activated with the HD wallet derivation method")] + CoinIsActivatedNotWithHDWallet, + #[display(fmt = "HD account '{}' is not activated", account_id)] + UnknownAccount { account_id: u32 }, + #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] + InvalidBip44Chain { chain: Bip44Chain }, + #[display(fmt = "Error deriving an address: {}", _0)] + ErrorDerivingAddress(String), + #[display(fmt = "Accounts limit reached. Max number of accounts: {}", max_accounts_number)] + AccountLimitReached { max_accounts_number: u32 }, + #[display(fmt = "Addresses limit reached. Max number of addresses: {}", max_addresses_number)] + AddressLimitReached { max_addresses_number: u32 }, + #[display(fmt = "Electrum/Native RPC invalid response: {}", _0)] + RpcInvalidResponse(String), + #[display(fmt = "HD wallet storage error: {}", _0)] + WalletStorageError(String), + #[display(fmt = "Transport: {}", _0)] + Transport(String), + #[display(fmt = "Internal: {}", _0)] + Internal(String), +} + +impl From for HDWalletRpcError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => HDWalletRpcError::NoSuchCoin { coin }, + } + } +} + +impl From for HDWalletRpcError { + fn from(e: UnexpectedDerivationMethod) -> Self { + match e { + UnexpectedDerivationMethod::HDWalletUnavailable => HDWalletRpcError::CoinIsActivatedNotWithHDWallet, + unexpected_error => HDWalletRpcError::Internal(unexpected_error.to_string()), + } + } +} + +impl From for HDWalletRpcError { + fn from(e: BalanceError) -> Self { + match e { + BalanceError::Transport(transport) => HDWalletRpcError::Transport(transport), + BalanceError::InvalidResponse(rpc) => HDWalletRpcError::RpcInvalidResponse(rpc), + BalanceError::UnexpectedDerivationMethod(der_path) => HDWalletRpcError::from(der_path), + BalanceError::WalletStorageError(internal) | BalanceError::Internal(internal) => { + HDWalletRpcError::Internal(internal) + }, + } + } +} + +impl From for HDWalletRpcError { + fn from(e: InvalidBip44ChainError) -> Self { HDWalletRpcError::InvalidBip44Chain { chain: e.chain } } +} + +impl From for HDWalletRpcError { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::Bip32Error(bip32) => HDWalletRpcError::ErrorDerivingAddress(bip32.to_string()), + } + } +} + +impl From for HDWalletRpcError { + fn from(e: NewAddressDerivingError) -> HDWalletRpcError { + match e { + NewAddressDerivingError::AddressLimitReached { max_addresses_number } => { + HDWalletRpcError::AddressLimitReached { max_addresses_number } + }, + NewAddressDerivingError::InvalidBip44Chain { chain } => HDWalletRpcError::InvalidBip44Chain { chain }, + NewAddressDerivingError::Bip32Error(bip32) => HDWalletRpcError::Internal(bip32.to_string()), + NewAddressDerivingError::WalletStorageError(storage) => { + HDWalletRpcError::WalletStorageError(storage.to_string()) + }, + } + } +} + +impl From for HDWalletRpcError { + fn from(e: RpcTaskError) -> Self { + let error = e.to_string(); + match e { + RpcTaskError::Canceled => HDWalletRpcError::Internal("Canceled".to_owned()), + RpcTaskError::Timeout(timeout) => HDWalletRpcError::Timeout(timeout), + RpcTaskError::NoSuchTask(_) | RpcTaskError::UnexpectedTaskStatus { .. } => { + HDWalletRpcError::Internal(error) + }, + RpcTaskError::Internal(internal) => HDWalletRpcError::Internal(internal), + } + } +} + +impl From for HDWalletRpcError { + fn from(e: HwError) -> Self { + let error = e.to_string(); + match e { + HwError::NoTrezorDeviceAvailable => HDWalletRpcError::NoTrezorDeviceAvailable, + HwError::FoundUnexpectedDevice { .. } => HDWalletRpcError::FoundUnexpectedDevice(error), + _ => HDWalletRpcError::HardwareWalletInternal(error), + } + } +} + +impl HttpStatusCode for HDWalletRpcError { + fn status_code(&self) -> StatusCode { + match self { + HDWalletRpcError::CoinDoesntSupportTrezor + | HDWalletRpcError::HwContextNotInitialized + | HDWalletRpcError::NoSuchCoin { .. } + | HDWalletRpcError::CoinIsActivatedNotWithHDWallet + | HDWalletRpcError::UnknownAccount { .. } + | HDWalletRpcError::InvalidBip44Chain { .. } + | HDWalletRpcError::ErrorDerivingAddress(_) + | HDWalletRpcError::AddressLimitReached { .. } + | HDWalletRpcError::AccountLimitReached { .. } => StatusCode::BAD_REQUEST, + HDWalletRpcError::TrezorDisconnected + | HDWalletRpcError::HardwareWalletInternal(_) + | HDWalletRpcError::NoTrezorDeviceAvailable + | HDWalletRpcError::FoundUnexpectedDevice(_) + | HDWalletRpcError::Timeout(_) + | HDWalletRpcError::Transport(_) + | HDWalletRpcError::RpcInvalidResponse(_) + | HDWalletRpcError::WalletStorageError(_) + | HDWalletRpcError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub struct HDAddress { + pub address: Address, + pub pubkey: Pubkey, + pub derivation_path: DerivationPath, +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct HDAddressId { + pub account_id: u32, + pub chain: Bip44Chain, + pub address_id: u32, +} + +impl From for HDAddressId { + fn from(der_path: Bip44DerivationPath) -> Self { + HDAddressId { + account_id: der_path.account_id(), + chain: der_path.chain(), + address_id: der_path.address_id(), + } + } +} + +#[async_trait] +pub trait HDWalletCoinOps { + type Address: Send + Sync; + type Pubkey: Send; + type HDWallet: HDWalletOps; + type HDAccount: HDAccountOps; + + /// Derives an address from the given info. + fn derive_address( + &self, + hd_account: &Self::HDAccount, + chain: Bip44Chain, + address_id: u32, + ) -> MmResult, AddressDerivingError>; + + /// Generates a new address and updates the corresponding number of used `hd_account` addresses. + async fn generate_new_address( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut Self::HDAccount, + chain: Bip44Chain, + ) -> MmResult, NewAddressDerivingError> { + let known_addresses_number = hd_account.known_addresses_number(chain)?; + // Address IDs start from 0, so the `known_addresses_number = last_known_address_id + 1`. + let new_address_id = known_addresses_number; + if new_address_id >= ChildNumber::HARDENED_FLAG { + return MmError::err(NewAddressDerivingError::AddressLimitReached { + max_addresses_number: ChildNumber::HARDENED_FLAG, + }); + } + let new_address = self + .derive_address(hd_account, chain, new_address_id) + .mm_err(NewAddressDerivingError::from)?; + self.set_known_addresses_number(hd_wallet, hd_account, chain, known_addresses_number + 1) + .await?; + Ok(new_address) + } + + /// Creates a new HD account, registers it within the given `hd_wallet` + /// and returns a mutable reference to the registered account. + async fn create_new_account<'a, XPubExtractor>( + &self, + hd_wallet: &'a Self::HDWallet, + xpub_extractor: &XPubExtractor, + ) -> MmResult, NewAccountCreatingError> + where + XPubExtractor: HDXPubExtractor + Sync; + + async fn set_known_addresses_number( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut Self::HDAccount, + chain: Bip44Chain, + new_known_addresses_number: u32, + ) -> MmResult<(), AccountUpdatingError>; +} + +#[async_trait] +pub trait HDWalletOps: Send + Sync { + type HDAccount: HDAccountOps + Clone + Send; + + fn coin_type(&self) -> u32; + + fn gap_limit(&self) -> u32; + + fn get_accounts_mutex(&self) -> &HDAccountsMutex; + + /// Returns a copy of an account by the given `account_id` if it's activated. + async fn get_account(&self, account_id: u32) -> Option { + let accounts = self.get_accounts_mutex().lock().await; + accounts.get(&account_id).cloned() + } + + /// Returns a mutable reference to an account by the given `account_id` if it's activated. + async fn get_account_mut(&self, account_id: u32) -> Option> { + let accounts = self.get_accounts_mutex().lock().await; + if !accounts.contains_key(&account_id) { + return None; + } + + Some(AsyncMutexGuard::map(accounts, |accounts| { + accounts + .get_mut(&account_id) + .expect("getting an element should never fail due to the checks above") + })) + } + + /// Returns copies of all activated accounts. + async fn get_accounts(&self) -> HDAccountsMap { self.get_accounts_mutex().lock().await.clone() } + + /// Returns a mutable reference to all activated accounts. + async fn get_accounts_mut(&self) -> HDAccountsMut<'_, Self::HDAccount> { self.get_accounts_mutex().lock().await } +} + +pub trait HDAccountOps: Send + Sync { + /// Returns a number of used addresses of this account + /// or an `InvalidBip44ChainError` error if the coin doesn't support the given `chain`. + fn known_addresses_number(&self, chain: Bip44Chain) -> MmResult; + + /// Returns a derivation path of this account. + fn account_derivation_path(&self) -> DerivationPath; + + /// Returns an index of this account. + fn account_id(&self) -> u32; + + /// Returns true if the given address is known at this time. + fn is_address_activated(&self, chain: Bip44Chain, address_id: u32) -> MmResult { + let is_activated = address_id < self.known_addresses_number(chain)?; + Ok(is_activated) + } +} + +#[derive(Deserialize)] +pub struct GetNewHDAddressRequest { + coin: String, + #[serde(flatten)] + params: GetNewHDAddressParams, +} + +#[derive(Deserialize)] +pub struct GetNewHDAddressParams { + account_id: u32, + chain: Bip44Chain, +} + +#[derive(Serialize)] +pub struct GetNewHDAddressResponse { + new_address: HDAddressBalance, +} + +#[async_trait] +pub trait HDWalletRpcOps { + async fn get_new_address_rpc( + &self, + params: GetNewHDAddressParams, + ) -> MmResult; +} + +pub async fn get_new_address( + ctx: MmArc, + req: GetNewHDAddressRequest, +) -> MmResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + match coin { + MmCoinEnum::UtxoCoin(utxo) => utxo.get_new_address_rpc(req.params).await, + MmCoinEnum::QtumCoin(qtum) => qtum.get_new_address_rpc(req.params).await, + _ => MmError::err(HDWalletRpcError::CoinIsActivatedNotWithHDWallet), + } +} + +pub mod common_impl { + use super::*; + use crate::coin_balance::HDWalletBalanceOps; + use crate::MarketCoinOps; + use crypto::RpcDerivationPath; + use std::fmt; + use std::ops::DerefMut; + + pub async fn get_new_address_rpc( + coin: &Coin, + params: GetNewHDAddressParams, + ) -> MmResult + where + Coin: HDWalletBalanceOps + + CoinWithDerivationMethod::HDWallet> + + MarketCoinOps + + Sync + + Send, + ::Address: fmt::Display, + { + let account_id = params.account_id; + let chain = params.chain; + + let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; + let mut hd_account = hd_wallet + .get_account_mut(params.account_id) + .await + .or_mm_err(|| HDWalletRpcError::UnknownAccount { account_id })?; + + let HDAddress { + address, + derivation_path, + .. + } = coin + .generate_new_address(hd_wallet, hd_account.deref_mut(), chain) + .await?; + let balance = coin.known_address_balance(&address).await?; + + Ok(GetNewHDAddressResponse { + new_address: HDAddressBalance { + address: address.to_string(), + derivation_path: RpcDerivationPath(derivation_path), + chain, + balance, + }, + }) + } +} diff --git a/mm2src/coins/hd_wallet_storage/mock_storage.rs b/mm2src/coins/hd_wallet_storage/mock_storage.rs new file mode 100644 index 0000000000..67e694ed44 --- /dev/null +++ b/mm2src/coins/hd_wallet_storage/mock_storage.rs @@ -0,0 +1,57 @@ +use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageInternalOps, HDWalletStorageResult}; +use async_trait::async_trait; +use mm2_core::mm_ctx::MmArc; +use mocktopus::macros::*; + +pub struct HDWalletMockStorage; + +#[async_trait] +#[mockable] +impl HDWalletStorageInternalOps for HDWalletMockStorage { + async fn init(_ctx: &MmArc) -> HDWalletStorageResult + where + Self: Sized, + { + unimplemented!() + } + + async fn load_accounts(&self, _wallet_id: HDWalletId) -> HDWalletStorageResult> { + unimplemented!() + } + + async fn load_account( + &self, + _wallet_id: HDWalletId, + _account_id: u32, + ) -> HDWalletStorageResult> { + unimplemented!() + } + + async fn update_external_addresses_number( + &self, + _wallet_id: HDWalletId, + _account_id: u32, + _new_external_addresses_number: u32, + ) -> HDWalletStorageResult<()> { + unimplemented!() + } + + async fn update_internal_addresses_number( + &self, + _wallet_id: HDWalletId, + _account_id: u32, + _new_internal_addresses_number: u32, + ) -> HDWalletStorageResult<()> { + unimplemented!() + } + + async fn upload_new_account( + &self, + _wallet_id: HDWalletId, + _account: HDAccountStorageItem, + ) -> HDWalletStorageResult<()> { + unimplemented!() + } + + async fn clear_accounts(&self, _wallet_id: HDWalletId) -> HDWalletStorageResult<()> { unimplemented!() } +} diff --git a/mm2src/coins/hd_wallet_storage/mod.rs b/mm2src/coins/hd_wallet_storage/mod.rs new file mode 100644 index 0000000000..95506f52fe --- /dev/null +++ b/mm2src/coins/hd_wallet_storage/mod.rs @@ -0,0 +1,585 @@ +use crate::hd_wallet::HDWalletCoinOps; +use async_trait::async_trait; +use crypto::{CryptoCtx, CryptoInitError, XPub}; +use derive_more::Display; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +#[cfg(test)] use mocktopus::macros::*; +use primitives::hash::H160; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::fmt::Formatter; +use std::ops::Deref; + +#[cfg(not(target_arch = "wasm32"))] mod sqlite_storage; +#[cfg(target_arch = "wasm32")] mod wasm_storage; + +#[cfg(test)] mod mock_storage; +#[cfg(test)] pub use mock_storage::HDWalletMockStorage; + +cfg_wasm32! { + use wasm_storage::HDWalletIndexedDbStorage as HDWalletStorageInstance; + + pub use wasm_storage::{HDWalletDb, HDWalletDbLocked}; +} + +cfg_native! { + use sqlite_storage::HDWalletSqliteStorage as HDWalletStorageInstance; +} + +pub type HDWalletStorageResult = MmResult; +type HDWalletStorageBoxed = Box; + +#[derive(Debug, Display)] +pub enum HDWalletStorageError { + #[display(fmt = "HD wallet not allowed")] + HDWalletUnavailable, + #[display(fmt = "HD account '{:?}':{} not found", wallet_id, account_id)] + HDAccountNotFound { wallet_id: HDWalletId, account_id: u32 }, + #[display(fmt = "Error saving changes in HD wallet storage: {}", _0)] + ErrorSaving(String), + #[display(fmt = "Error loading from HD wallet storage: {}", _0)] + ErrorLoading(String), + #[display(fmt = "Error deserializing a swap: {}", _0)] + ErrorDeserializing(String), + #[display(fmt = "Error serializing a swap: {}", _0)] + ErrorSerializing(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for HDWalletStorageError { + fn from(e: CryptoInitError) -> Self { HDWalletStorageError::Internal(e.to_string()) } +} + +impl HDWalletStorageError { + pub fn is_deserializing_err(&self) -> bool { matches!(self, HDWalletStorageError::ErrorDeserializing(_)) } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HDWalletId { + coin: String, + /// RIPEMD160(SHA256(x)) where x is a pubkey with which mm2 is launched. + /// It's expected to be equal to [`MmCtx::rmd160`]. + /// This property allows us to store DB items that are unique to each user (passphrase). + mm2_rmd160: String, + /// RIPEMD160(SHA256(x)) where x is a pubkey extracted from a Hardware Wallet device or passphrase. + /// This property allows us to store DB items that are unique to each Hardware Wallet device. + /// Please note it can be equal to [`HDWalletId::mm2_rmd160`] if mm2 is launched with a HD private key derived from a passphrase. + hd_wallet_rmd160: String, +} + +impl HDWalletId { + pub fn new(coin: String, mm2_rmd160: &H160, hd_wallet_rmd160: &H160) -> HDWalletId { + HDWalletId { + coin, + mm2_rmd160: display_rmd160(mm2_rmd160), + hd_wallet_rmd160: display_rmd160(hd_wallet_rmd160), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct HDAccountStorageItem { + pub account_id: u32, + pub account_xpub: XPub, + /// The number of addresses that we know have been used by the user. + pub external_addresses_number: u32, + pub internal_addresses_number: u32, +} + +#[async_trait] +#[cfg_attr(test, mockable)] +pub trait HDWalletStorageInternalOps { + async fn init(ctx: &MmArc) -> HDWalletStorageResult + where + Self: Sized; + + async fn load_accounts(&self, wallet_id: HDWalletId) -> HDWalletStorageResult>; + + async fn load_account( + &self, + wallet_id: HDWalletId, + account_id: u32, + ) -> HDWalletStorageResult>; + + async fn update_external_addresses_number( + &self, + wallet_id: HDWalletId, + account_id: u32, + new_external_addresses_number: u32, + ) -> HDWalletStorageResult<()>; + + async fn update_internal_addresses_number( + &self, + wallet_id: HDWalletId, + account_id: u32, + new_internal_addresses_number: u32, + ) -> HDWalletStorageResult<()>; + + async fn upload_new_account( + &self, + wallet_id: HDWalletId, + account: HDAccountStorageItem, + ) -> HDWalletStorageResult<()>; + + async fn clear_accounts(&self, wallet_id: HDWalletId) -> HDWalletStorageResult<()>; +} + +#[async_trait] +pub trait HDWalletCoinWithStorageOps: HDWalletCoinOps { + fn hd_wallet_storage<'a>(&self, hd_wallet: &'a Self::HDWallet) -> &'a HDWalletCoinStorage; + + async fn load_all_accounts(&self, hd_wallet: &Self::HDWallet) -> HDWalletStorageResult> { + let storage = self.hd_wallet_storage(hd_wallet); + storage.load_all_accounts().await + } + + async fn load_account( + &self, + hd_wallet: &Self::HDWallet, + account_id: u32, + ) -> HDWalletStorageResult> { + let storage = self.hd_wallet_storage(hd_wallet); + storage.load_account(account_id).await + } + + async fn update_external_addresses_number( + &self, + hd_wallet: &Self::HDWallet, + account_id: u32, + new_external_addresses_number: u32, + ) -> HDWalletStorageResult<()> { + let storage = self.hd_wallet_storage(hd_wallet); + storage + .update_external_addresses_number(account_id, new_external_addresses_number) + .await + } + + async fn update_internal_addresses_number( + &self, + hd_wallet: &Self::HDWallet, + account_id: u32, + new_internal_addresses_number: u32, + ) -> HDWalletStorageResult<()> { + let storage = self.hd_wallet_storage(hd_wallet); + storage + .update_internal_addresses_number(account_id, new_internal_addresses_number) + .await + } + + async fn upload_new_account( + &self, + hd_wallet: &Self::HDWallet, + account_info: HDAccountStorageItem, + ) -> HDWalletStorageResult<()> { + let storage = self.hd_wallet_storage(hd_wallet); + storage.upload_new_account(account_info).await + } + + async fn clear_accounts(&self, hd_wallet: &Self::HDWallet) -> HDWalletStorageResult<()> { + let storage = self.hd_wallet_storage(hd_wallet); + storage.clear_accounts().await + } +} + +/// The wrapper over the [`HDWalletStorage::inner`] database implementation. +/// It's associated with a specific mm2 user, HD wallet and coin. +pub struct HDWalletCoinStorage { + coin: String, + /// RIPEMD160(SHA256(x)) where x is a pubkey with which mm2 is launched. + /// It's expected to be equal to [`MmCtx::rmd160`]. + /// This property allows us to store DB items that are unique to each user (passphrase). + mm2_rmd160: H160, + /// RIPEMD160(SHA256(x)) where x is a pubkey extracted from a Hardware Wallet device or passphrase. + /// This property allows us to store DB items that are unique to each Hardware Wallet device. + /// Please note it can be equal to [`HDWalletId::mm2_rmd160`] if mm2 is launched with a HD private key derived from a passphrase. + hd_wallet_rmd160: H160, + inner: HDWalletStorageBoxed, +} + +impl fmt::Debug for HDWalletCoinStorage { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("HDWalletCoinStorage") + .field("coin", &self.coin) + .field("mm2_rmd160", &self.mm2_rmd160) + .field("hd_wallet_rmd160", &self.hd_wallet_rmd160) + .finish() + } +} + +#[cfg(test)] +impl Default for HDWalletCoinStorage { + fn default() -> Self { + HDWalletCoinStorage { + coin: String::default(), + mm2_rmd160: H160::default(), + hd_wallet_rmd160: H160::default(), + inner: Box::new(HDWalletMockStorage), + } + } +} + +impl HDWalletCoinStorage { + pub async fn init(ctx: &MmArc, coin: String) -> HDWalletStorageResult { + let inner = Box::new(HDWalletStorageInstance::init(ctx).await?); + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let hd_wallet_rmd160 = crypto_ctx + .hd_wallet_rmd160() + .or_mm_err(|| HDWalletStorageError::HDWalletUnavailable)?; + Ok(HDWalletCoinStorage { + coin, + mm2_rmd160: *ctx.rmd160(), + hd_wallet_rmd160, + inner, + }) + } + + #[cfg(any(test, target_arch = "wasm32"))] + pub async fn init_with_rmd160( + ctx: &MmArc, + coin: String, + mm2_rmd160: H160, + hd_wallet_rmd160: H160, + ) -> HDWalletStorageResult { + let inner = Box::new(HDWalletStorageInstance::init(ctx).await?); + Ok(HDWalletCoinStorage { + coin, + mm2_rmd160, + hd_wallet_rmd160, + inner, + }) + } + + pub fn wallet_id(&self) -> HDWalletId { + HDWalletId::new(self.coin.clone(), &self.mm2_rmd160, &self.hd_wallet_rmd160) + } + + pub async fn load_all_accounts(&self) -> HDWalletStorageResult> { + let wallet_id = self.wallet_id(); + self.inner.load_accounts(wallet_id).await + } + + async fn load_account(&self, account_id: u32) -> HDWalletStorageResult> { + let wallet_id = self.wallet_id(); + self.inner.load_account(wallet_id, account_id).await + } + + async fn update_external_addresses_number( + &self, + account_id: u32, + new_external_addresses_number: u32, + ) -> HDWalletStorageResult<()> { + let wallet_id = self.wallet_id(); + self.inner + .update_external_addresses_number(wallet_id, account_id, new_external_addresses_number) + .await + } + + async fn update_internal_addresses_number( + &self, + account_id: u32, + new_internal_addresses_number: u32, + ) -> HDWalletStorageResult<()> { + let wallet_id = self.wallet_id(); + self.inner + .update_internal_addresses_number(wallet_id, account_id, new_internal_addresses_number) + .await + } + + async fn upload_new_account(&self, account_info: HDAccountStorageItem) -> HDWalletStorageResult<()> { + let wallet_id = self.wallet_id(); + self.inner.upload_new_account(wallet_id, account_info).await + } + + pub async fn clear_accounts(&self) -> HDWalletStorageResult<()> { + let wallet_id = self.wallet_id(); + self.inner.clear_accounts(wallet_id).await + } +} + +fn display_rmd160(rmd160: &H160) -> String { hex::encode(rmd160.deref()) } + +#[cfg(any(test, target_arch = "wasm32"))] +mod tests { + use super::*; + use itertools::Itertools; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + use primitives::hash::H160; + + cfg_wasm32! { + use crate::hd_wallet_storage::wasm_storage::get_all_storage_items; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + } + + cfg_native! { + use crate::hd_wallet_storage::sqlite_storage::get_all_storage_items; + use common::block_on; + } + + async fn test_unique_wallets_impl() { + let rick_user0_device0_account0 = HDAccountStorageItem { + account_id: 0, + account_xpub: "xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ".to_owned(), + external_addresses_number: 1, + internal_addresses_number: 2, + }; + let rick_user0_device0_account1 = HDAccountStorageItem { + account_id: 1, + account_xpub: "xpub6DEHSksajpRPQq2FdGT6JoieiQZUpTZ3WZn8fcuLJhFVmtCpXbuXxp5aPzaokwcLV2V9LE55Dwt8JYkpuMv7jXKwmyD28WbHYjBH2zhbW2p".to_owned(), + external_addresses_number: 1, + internal_addresses_number: 2, + }; + let rick_user0_device1_account0 = HDAccountStorageItem { + account_id: 0, + account_xpub: "xpub6EuV33a2DXxAhoJTRTnr8qnysu81AA4YHpLY6o8NiGkEJ8KADJ35T64eJsStWsmRf1xXkEANVjXFXnaUKbRtFwuSPCLfDdZwYNZToh4LBCd".to_owned(), + external_addresses_number: 3, + internal_addresses_number: 4, + }; + let rick_user1_device0_account0 = HDAccountStorageItem { + account_id: 0, + account_xpub: "xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz".to_owned(), + external_addresses_number: 5, + internal_addresses_number: 6, + }; + let morty_user0_device0_account0 = HDAccountStorageItem { + account_id: 0, + account_xpub: "xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU".to_owned(), + external_addresses_number: 7, + internal_addresses_number: 8, + }; + + let ctx = mm_ctx_with_custom_db(); + let user0_rmd160 = H160::from("0000000000000000000000000000000000000000"); + let user1_rmd160 = H160::from("0000000000000000000000000000000000000001"); + let device0_rmd160 = H160::from("0000000000000000000000000000000000000020"); + let device1_rmd160 = H160::from("0000000000000000000000000000000000000030"); + + let rick_user0_device0_db = + HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), user0_rmd160, device0_rmd160) + .await + .expect("!HDWalletCoinStorage::new"); + let rick_user0_device1_db = + HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), user0_rmd160, device1_rmd160) + .await + .expect("!HDWalletCoinStorage::new"); + let rick_user1_device0_db = + HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), user1_rmd160, device0_rmd160) + .await + .expect("!HDWalletCoinStorage::new"); + let morty_user0_device0_db = + HDWalletCoinStorage::init_with_rmd160(&ctx, "MORTY".to_owned(), user0_rmd160, device0_rmd160) + .await + .expect("!HDWalletCoinStorage::new"); + + rick_user0_device0_db + .upload_new_account(rick_user0_device0_account0.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: RICK user=0 device=0 account=0"); + rick_user0_device0_db + .upload_new_account(rick_user0_device0_account1.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: RICK user=0 device=0 account=1"); + rick_user0_device1_db + .upload_new_account(rick_user0_device1_account0.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: RICK user=0 device=1 account=0"); + rick_user1_device0_db + .upload_new_account(rick_user1_device0_account0.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: RICK user=1 device=0 account=0"); + morty_user0_device0_db + .upload_new_account(morty_user0_device0_account0.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: MORTY user=0 device=0 account=0"); + + // All accounts must be in the only one database. + // Rows in the database must differ by only `coin`, `mm2_rmd160`, `hd_wallet_rmd160` and `account_id` values. + let all_accounts: Vec<_> = get_all_storage_items(&ctx) + .await + .into_iter() + .sorted_by(|x, y| x.external_addresses_number.cmp(&y.external_addresses_number)) + .collect(); + assert_eq!(all_accounts, vec![ + rick_user0_device0_account0.clone(), + rick_user0_device0_account1.clone(), + rick_user0_device1_account0.clone(), + rick_user1_device0_account0.clone(), + morty_user0_device0_account0.clone() + ]); + + let mut actual = rick_user0_device0_db + .load_all_accounts() + .await + .expect("HDWalletCoinStorage::load_all_accounts: RICK user=0 device=0"); + actual.sort_by(|x, y| x.account_id.cmp(&y.account_id)); + assert_eq!(actual, vec![rick_user0_device0_account0, rick_user0_device0_account1]); + + let actual = rick_user0_device1_db + .load_all_accounts() + .await + .expect("HDWalletCoinStorage::load_all_accounts: RICK user=0 device=1"); + assert_eq!(actual, vec![rick_user0_device1_account0]); + + let actual = rick_user1_device0_db + .load_all_accounts() + .await + .expect("HDWalletCoinStorage::load_all_accounts: RICK user=1 device=0"); + assert_eq!(actual, vec![rick_user1_device0_account0]); + + let actual = morty_user0_device0_db + .load_all_accounts() + .await + .expect("HDWalletCoinStorage::load_all_accounts: MORTY user=0 device=0"); + assert_eq!(actual, vec![morty_user0_device0_account0]); + } + + async fn test_delete_accounts_impl() { + let wallet0_account0 = HDAccountStorageItem { + account_id: 0, + account_xpub: "xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ".to_owned(), + external_addresses_number: 1, + internal_addresses_number: 2, + }; + let wallet0_account1 = HDAccountStorageItem { + account_id: 1, + account_xpub: "xpub6DEHSksajpRPQq2FdGT6JoieiQZUpTZ3WZn8fcuLJhFVmtCpXbuXxp5aPzaokwcLV2V9LE55Dwt8JYkpuMv7jXKwmyD28WbHYjBH2zhbW2p".to_owned(), + external_addresses_number: 1, + internal_addresses_number: 2, + }; + let wallet1_account0 = HDAccountStorageItem { + account_id: 0, + account_xpub: "xpub6EuV33a2DXxAhoJTRTnr8qnysu81AA4YHpLY6o8NiGkEJ8KADJ35T64eJsStWsmRf1xXkEANVjXFXnaUKbRtFwuSPCLfDdZwYNZToh4LBCd".to_owned(), + external_addresses_number: 3, + internal_addresses_number: 4, + }; + let wallet2_account0 = HDAccountStorageItem { + account_id: 0, + account_xpub: "xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz".to_owned(), + external_addresses_number: 5, + internal_addresses_number: 6, + }; + + let ctx = mm_ctx_with_custom_db(); + let user_rmd160 = H160::from("0000000000000000000000000000000000000000"); + let device0_rmd160 = H160::from("0000000000000000000000000000000000000010"); + let device1_rmd160 = H160::from("0000000000000000000000000000000000000020"); + let device2_rmd160 = H160::from("0000000000000000000000000000000000000030"); + + let wallet0_db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), user_rmd160, device0_rmd160) + .await + .expect("!HDWalletCoinStorage::new"); + let wallet1_db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), user_rmd160, device1_rmd160) + .await + .expect("!HDWalletCoinStorage::new"); + let wallet2_db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), user_rmd160, device2_rmd160) + .await + .expect("!HDWalletCoinStorage::new"); + + wallet0_db + .upload_new_account(wallet0_account0.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: RICK wallet=0 account=0"); + wallet0_db + .upload_new_account(wallet0_account1.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: RICK wallet=0 account=1"); + wallet1_db + .upload_new_account(wallet1_account0.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: RICK wallet=1 account=0"); + wallet2_db + .upload_new_account(wallet2_account0.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: RICK wallet=2 account=0"); + + wallet0_db + .clear_accounts() + .await + .expect("HDWalletCoinStorage::clear_accounts: RICK wallet=0"); + + // All accounts must be in the only one database. + // Rows in the database must differ by only `coin`, `mm2_rmd160`, `hd_wallet_rmd160` and `account_id` values. + let all_accounts: Vec<_> = get_all_storage_items(&ctx) + .await + .into_iter() + .sorted_by(|x, y| x.external_addresses_number.cmp(&y.external_addresses_number)) + .collect(); + assert_eq!(all_accounts, vec![wallet1_account0, wallet2_account0]); + } + + async fn test_update_account_impl() { + let mut account0 = HDAccountStorageItem { + account_id: 0, + account_xpub: "xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ".to_owned(), + external_addresses_number: 1, + internal_addresses_number: 2, + }; + let mut account1 = HDAccountStorageItem { + account_id: 1, + account_xpub: "xpub6DEHSksajpRPQq2FdGT6JoieiQZUpTZ3WZn8fcuLJhFVmtCpXbuXxp5aPzaokwcLV2V9LE55Dwt8JYkpuMv7jXKwmyD28WbHYjBH2zhbW2p".to_owned(), + external_addresses_number: 3, + internal_addresses_number: 4, + }; + + let ctx = mm_ctx_with_custom_db(); + let user_rmd160 = H160::from("0000000000000000000000000000000000000000"); + let device_rmd160 = H160::from("0000000000000000000000000000000000000010"); + + let db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), user_rmd160, device_rmd160) + .await + .expect("!HDWalletCoinStorage::new"); + + db.upload_new_account(account0.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: RICK wallet=0 account=0"); + db.upload_new_account(account1.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: RICK wallet=0 account=1"); + + db.update_internal_addresses_number(0, 5) + .await + .expect("!HDWalletCoinStorage::update_internal_addresses_number"); + db.update_external_addresses_number(1, 10) + .await + .expect("!HDWalletCoinStorage::update_external_addresses_number"); + + let actual: Vec<_> = db + .load_all_accounts() + .await + .expect("!HDWalletCoinStorage::load_all_accounts") + .into_iter() + .sorted_by(|x, y| x.external_addresses_number.cmp(&y.external_addresses_number)) + .collect(); + + account0.internal_addresses_number = 5; + account1.external_addresses_number = 10; + assert_eq!(actual, vec![account0, account1]); + } + + #[cfg(target_arch = "wasm32")] + #[wasm_bindgen_test] + async fn test_unique_wallets() { test_unique_wallets_impl().await } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn test_unique_wallets() { block_on(test_unique_wallets_impl()) } + + #[cfg(target_arch = "wasm32")] + #[wasm_bindgen_test] + async fn test_delete_accounts() { test_delete_accounts_impl().await } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn test_delete_accounts() { block_on(test_delete_accounts_impl()) } + + #[cfg(target_arch = "wasm32")] + #[wasm_bindgen_test] + async fn test_update_account() { test_update_account_impl().await } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn test_update_account() { block_on(test_update_account_impl()) } +} diff --git a/mm2src/coins/hd_wallet_storage/sqlite_storage.rs b/mm2src/coins/hd_wallet_storage/sqlite_storage.rs new file mode 100644 index 0000000000..8a061fddb5 --- /dev/null +++ b/mm2src/coins/hd_wallet_storage/sqlite_storage.rs @@ -0,0 +1,309 @@ +use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageError, HDWalletStorageInternalOps, + HDWalletStorageResult}; +use async_trait::async_trait; +use common::async_blocking; +use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Row, ToSql, NO_PARAMS}; +use db_common::sqlite::{SqliteConnShared, SqliteConnWeak}; +use derive_more::Display; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use std::convert::TryFrom; +use std::sync::MutexGuard; + +const CREATE_HD_ACCOUNT_TABLE: &str = "CREATE TABLE IF NOT EXISTS hd_account ( + coin VARCHAR(255) NOT NULL, + mm2_rmd160 VARCHAR(255) NOT NULL, + hd_wallet_rmd160 VARCHAR(255) NOT NULL, + account_id INTEGER NOT NULL, + account_xpub VARCHAR(255) NOT NULL, + external_addresses_number INTEGER NOT NULL, + internal_addresses_number INTEGER NOT NULL +);"; + +const INSERT_ACCOUNT: &str = "INSERT INTO hd_account + (coin, mm2_rmd160, hd_wallet_rmd160, account_id, account_xpub, external_addresses_number, internal_addresses_number) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);"; + +const DELETE_ACCOUNTS_BY_WALLET_ID: &str = + "DELETE FROM hd_account WHERE coin=?1 AND mm2_rmd160=?2 AND hd_wallet_rmd160=?3;"; + +const SELECT_ACCOUNT: &str = "SELECT account_id, account_xpub, external_addresses_number, internal_addresses_number + FROM hd_account + WHERE coin=?1 AND mm2_rmd160=?2 AND hd_wallet_rmd160=?3 AND account_id=?4;"; + +const SELECT_ACCOUNTS_BY_WALLET_ID: &str = + "SELECT account_id, account_xpub, external_addresses_number, internal_addresses_number + FROM hd_account + WHERE coin=?1 AND mm2_rmd160=?2 AND hd_wallet_rmd160=?3;"; + +/// The max number of SQL query params. +const PARAMS_CAPACITY: usize = 7; + +impl From for HDWalletStorageError { + fn from(e: SqlError) -> Self { + let error = e.to_string(); + match e { + SqlError::FromSqlConversionFailure(_, _, _) + | SqlError::IntegralValueOutOfRange(_, _) + | SqlError::InvalidColumnIndex(_) + | SqlError::InvalidColumnType(_, _, _) => HDWalletStorageError::ErrorDeserializing(error), + SqlError::Utf8Error(_) | SqlError::NulError(_) | SqlError::ToSqlConversionFailure(_) => { + HDWalletStorageError::ErrorSerializing(error) + }, + _ => HDWalletStorageError::Internal(error), + } + } +} + +impl TryFrom<&Row<'_>> for HDAccountStorageItem { + type Error = SqlError; + + fn try_from(row: &Row<'_>) -> Result { + Ok(HDAccountStorageItem { + account_id: row.get(0)?, + account_xpub: row.get(1)?, + external_addresses_number: row.get(2)?, + internal_addresses_number: row.get(3)?, + }) + } +} + +impl HDAccountStorageItem { + fn to_sql_params_with_wallet_id(&self, wallet_id: HDWalletId) -> Vec { + let mut params = Vec::with_capacity(PARAMS_CAPACITY); + wallet_id.fill_sql_params(&mut params); + self.fill_sql_params(&mut params); + params + } + + fn fill_sql_params(&self, params: &mut Vec) { + params.push(self.account_id.to_string()); + params.push(self.account_xpub.clone()); + params.push(self.external_addresses_number.to_string()); + params.push(self.internal_addresses_number.to_string()); + } +} + +impl HDWalletId { + fn to_sql_params(&self) -> Vec { + let mut params = Vec::with_capacity(PARAMS_CAPACITY); + self.fill_sql_params(&mut params); + params + } + + fn fill_sql_params(&self, params: &mut Vec) { + params.push(self.coin.clone()); + params.push(self.mm2_rmd160.clone()); + params.push(self.hd_wallet_rmd160.clone()); + } +} + +#[derive(Clone)] +pub struct HDWalletSqliteStorage { + conn: SqliteConnWeak, +} + +#[async_trait] +impl HDWalletStorageInternalOps for HDWalletSqliteStorage { + async fn init(ctx: &MmArc) -> HDWalletStorageResult + where + Self: Sized, + { + let shared = ctx + .sqlite_connection + .as_option() + .or_mm_err(|| HDWalletStorageError::Internal("'MmCtx::sqlite_connection' is not initialized".to_owned()))?; + let storage = HDWalletSqliteStorage { + conn: SqliteConnShared::downgrade(shared), + }; + storage.init_tables().await?; + Ok(storage) + } + + async fn load_accounts(&self, wallet_id: HDWalletId) -> HDWalletStorageResult> { + let selfi = self.clone(); + async_blocking(move || { + let conn_shared = selfi.get_shared_conn()?; + let conn = Self::lock_conn(&conn_shared)?; + + let mut statement = conn.prepare(SELECT_ACCOUNTS_BY_WALLET_ID)?; + + let params = wallet_id.to_sql_params(); + let rows = statement + .query_map(params, |row: &Row<'_>| HDAccountStorageItem::try_from(row))? + .collect::, _>>()?; + Ok(rows) + }) + .await + } + + async fn load_account( + &self, + wallet_id: HDWalletId, + account_id: u32, + ) -> HDWalletStorageResult> { + let selfi = self.clone(); + async_blocking(move || { + let conn_shared = selfi.get_shared_conn()?; + let conn = Self::lock_conn(&conn_shared)?; + + let mut params = wallet_id.to_sql_params(); + params.push(account_id.to_string()); + query_single_row(&conn, SELECT_ACCOUNT, params, |row: &Row<'_>| { + HDAccountStorageItem::try_from(row) + }) + .mm_err(HDWalletStorageError::from) + }) + .await + } + + async fn update_external_addresses_number( + &self, + wallet_id: HDWalletId, + account_id: u32, + new_external_addresses_number: u32, + ) -> HDWalletStorageResult<()> { + self.update_addresses_number( + UpdatingProperty::ExternalAddressesNumber, + wallet_id, + account_id, + new_external_addresses_number, + ) + .await + } + + async fn update_internal_addresses_number( + &self, + wallet_id: HDWalletId, + account_id: u32, + new_internal_addresses_number: u32, + ) -> HDWalletStorageResult<()> { + self.update_addresses_number( + UpdatingProperty::InternalAddressesNumber, + wallet_id, + account_id, + new_internal_addresses_number, + ) + .await + } + + async fn upload_new_account( + &self, + wallet_id: HDWalletId, + account: HDAccountStorageItem, + ) -> HDWalletStorageResult<()> { + let selfi = self.clone(); + async_blocking(move || { + let conn_shared = selfi.get_shared_conn()?; + let conn = Self::lock_conn(&conn_shared)?; + + let params = account.to_sql_params_with_wallet_id(wallet_id); + conn.execute(INSERT_ACCOUNT, params) + .map(|_| ()) + .map_to_mm(HDWalletStorageError::from) + }) + .await + } + + async fn clear_accounts(&self, wallet_id: HDWalletId) -> HDWalletStorageResult<()> { + let selfi = self.clone(); + async_blocking(move || { + let conn_shared = selfi.get_shared_conn()?; + let conn = Self::lock_conn(&conn_shared)?; + + let params = wallet_id.to_sql_params(); + conn.execute(DELETE_ACCOUNTS_BY_WALLET_ID, params) + .map(|_| ()) + .map_to_mm(HDWalletStorageError::from) + }) + .await + } +} + +impl HDWalletSqliteStorage { + fn get_shared_conn(&self) -> HDWalletStorageResult { + self.conn + .upgrade() + .or_mm_err(|| HDWalletStorageError::Internal("'HDWalletSqliteStorage::conn' doesn't exist".to_owned())) + } + + fn lock_conn(conn: &SqliteConnShared) -> HDWalletStorageResult> { + conn.lock() + .map_to_mm(|e| HDWalletStorageError::Internal(format!("Error locking sqlite connection: {}", e))) + } + + async fn init_tables(&self) -> HDWalletStorageResult<()> { + let conn_shared = self.get_shared_conn()?; + let conn = Self::lock_conn(&conn_shared)?; + conn.execute(CREATE_HD_ACCOUNT_TABLE, NO_PARAMS) + .map(|_| ()) + .map_to_mm(HDWalletStorageError::from) + } + + async fn update_addresses_number( + &self, + updating_property: UpdatingProperty, + wallet_id: HDWalletId, + account_id: u32, + new_addresses_number: u32, + ) -> HDWalletStorageResult<()> { + let sql = format!( + "UPDATE hd_account SET {}=?1 WHERE coin=?2 AND mm2_rmd160=?3 AND hd_wallet_rmd160=?4 AND account_id=?5;", + updating_property + ); + + let selfi = self.clone(); + async_blocking(move || { + let conn_shared = selfi.get_shared_conn()?; + let conn = Self::lock_conn(&conn_shared)?; + + let mut params = vec![new_addresses_number.to_string()]; + wallet_id.fill_sql_params(&mut params); + params.push(account_id.to_string()); + + conn.execute(&sql, params) + .map(|_| ()) + .map_to_mm(HDWalletStorageError::from) + }) + .await + } +} + +#[derive(Display)] +enum UpdatingProperty { + #[display(fmt = "external_addresses_number")] + ExternalAddressesNumber, + #[display(fmt = "internal_addresses_number")] + InternalAddressesNumber, +} + +/// TODO remove this when `db_common::query_single_row` is merged into `dev`. +fn query_single_row(conn: &Connection, query: &str, params: P, map_fn: F) -> MmResult, SqlError> +where + P: IntoIterator, + P::Item: ToSql, + F: FnOnce(&Row<'_>) -> Result, +{ + let maybe_result = conn.query_row(query, params, map_fn); + if let Err(SqlError::QueryReturnedNoRows) = maybe_result { + return Ok(None); + } + + let result = maybe_result?; + Ok(Some(result)) +} + +/// This function is used in `hd_wallet_storage::tests`. +#[cfg(test)] +pub(super) async fn get_all_storage_items(ctx: &MmArc) -> Vec { + const SELECT_ALL_ACCOUNTS: &str = + "SELECT account_id, account_xpub, external_addresses_number, internal_addresses_number FROM hd_account"; + + let conn = ctx.sqlite_connection(); + let mut statement = conn.prepare(SELECT_ALL_ACCOUNTS).unwrap(); + statement + .query_map(NO_PARAMS, |row: &Row<'_>| HDAccountStorageItem::try_from(row)) + .unwrap() + .collect::, _>>() + .unwrap() +} diff --git a/mm2src/coins/hd_wallet_storage/wasm_storage.rs b/mm2src/coins/hd_wallet_storage/wasm_storage.rs new file mode 100644 index 0000000000..fb9dbd2bbd --- /dev/null +++ b/mm2src/coins/hd_wallet_storage/wasm_storage.rs @@ -0,0 +1,341 @@ +use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageError, HDWalletStorageInternalOps, + HDWalletStorageResult}; +use crate::CoinsContext; +use async_trait::async_trait; +use crypto::XPub; +use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::cursor_prelude::*; +use mm2_db::indexed_db::{DbIdentifier, DbInstance, DbLocked, DbTable, DbTransactionError, DbUpgrader, IndexedDb, + IndexedDbBuilder, InitDbError, InitDbResult, ItemId, MultiIndex, OnUpgradeResult, SharedDb, + TableSignature, WeakDb}; +use mm2_err_handle::prelude::*; + +const DB_NAME: &str = "hd_wallet"; +const DB_VERSION: u32 = 1; +/// An index of the `HDAccountTable` table that consists of the following properties: +/// * coin - coin ticker +/// * mm2_rmd160 - RIPEMD160(SHA256(x)) where x is a pubkey with which mm2 is launched +/// * hd_wallet_rmd160 - RIPEMD160(SHA256(x)) where x is a pubkey extracted from a Hardware Wallet device or passphrase. +const WALLET_ID_INDEX: &str = "wallet_id"; +/// A **unique** index of the `HDAccountTable` table that consists of the following properties: +/// * coin - coin ticker +/// * mm2_rmd160 - RIPEMD160(SHA256(x)) where x is a pubkey with which mm2 is launched +/// * hd_wallet_rmd160 - RIPEMD160(SHA256(x)) where x is a pubkey extracted from a Hardware Wallet device or passphrase. +/// * account_id - HD account id +const WALLET_ACCOUNT_ID_INDEX: &str = "wallet_account_id"; + +pub type HDWalletDbLocked<'a> = DbLocked<'a, HDWalletDb>; + +impl From for HDWalletStorageError { + fn from(e: DbTransactionError) -> Self { + let desc = e.to_string(); + match e { + DbTransactionError::NoSuchTable { .. } + | DbTransactionError::ErrorCreatingTransaction(_) + | DbTransactionError::ErrorOpeningTable { .. } + | DbTransactionError::ErrorSerializingIndex { .. } + | DbTransactionError::MultipleItemsByUniqueIndex { .. } + | DbTransactionError::NoSuchIndex { .. } + | DbTransactionError::InvalidIndex { .. } + | DbTransactionError::UnexpectedState(_) + | DbTransactionError::TransactionAborted => HDWalletStorageError::Internal(desc), + DbTransactionError::ErrorDeserializingItem(_) => HDWalletStorageError::ErrorDeserializing(desc), + DbTransactionError::ErrorSerializingItem(_) => HDWalletStorageError::ErrorSerializing(desc), + DbTransactionError::ErrorGettingItems(_) | DbTransactionError::ErrorCountingItems(_) => { + HDWalletStorageError::ErrorLoading(desc) + }, + DbTransactionError::ErrorUploadingItem(_) | DbTransactionError::ErrorDeletingItems(_) => { + HDWalletStorageError::ErrorSaving(desc) + }, + } + } +} + +impl From for HDWalletStorageError { + fn from(e: CursorError) -> Self { + let stringified_error = e.to_string(); + match e { + // We don't expect that the `String` and `u32` types serialization to fail. + CursorError::ErrorSerializingIndexFieldValue {..} + // We don't expect that the `String` and `u32` types deserialization to fail. + | CursorError::ErrorDeserializingIndexValue {..} + | CursorError::ErrorOpeningCursor {..} + | CursorError::AdvanceError {..} + | CursorError::InvalidKeyRange {..} + | CursorError::TypeMismatch {..} + | CursorError::IncorrectNumberOfKeysPerIndex {..} + | CursorError::UnexpectedState(..) + | CursorError::IncorrectUsage {..} => HDWalletStorageError::Internal(stringified_error), + CursorError::ErrorDeserializingItem {..} => HDWalletStorageError::ErrorDeserializing(stringified_error), + } + } +} + +impl From for HDWalletStorageError { + fn from(e: InitDbError) -> Self { HDWalletStorageError::Internal(e.to_string()) } +} + +/// The table has the following individually non-unique indexes: `coin`, `mm2_rmd160`, `hd_wallet_rmd160`, `account_id`, +/// one non-unique multi-index `wallet_id` that consists of `coin`, `mm2_rmd160`, `hd_wallet_rmd160`, +/// and one unique multi-index `wallet_account_id` that consists of these four indexes in a row. +/// See [`HDAccountTable::on_update_needed`]. +#[derive(Deserialize, Serialize)] +pub struct HDAccountTable { + /// [`HDWalletId::coin`]. + /// Non-unique index that is used to fetch/remove items from the storage. + coin: String, + /// [`HDWalletId::mm2_rmd160`]. + /// Non-unique index that is used to fetch/remove items from the storage. + mm2_rmd160: String, + /// [`HDWalletId::hd_wallet_rmd160`]. + /// Non-unique index that is used to fetch/remove items from the storage. + hd_wallet_rmd160: String, + /// HD Account ID. + /// Non-unique index that is used to fetch/remove items from the storage. + account_id: u32, + account_xpub: XPub, + /// The number of addresses that we know have been used by the user. + external_addresses_number: u32, + internal_addresses_number: u32, +} + +impl TableSignature for HDAccountTable { + fn table_name() -> &'static str { "hd_account" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + match (old_version, new_version) { + (0, 1) => { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(WALLET_ID_INDEX, &["coin", "mm2_rmd160", "hd_wallet_rmd160"], false)?; + table.create_multi_index( + WALLET_ACCOUNT_ID_INDEX, + &["coin", "mm2_rmd160", "hd_wallet_rmd160", "account_id"], + true, + )?; + }, + _ => (), + } + Ok(()) + } +} + +impl HDAccountTable { + fn new(wallet_id: HDWalletId, account_info: HDAccountStorageItem) -> HDAccountTable { + HDAccountTable { + coin: wallet_id.coin, + mm2_rmd160: wallet_id.mm2_rmd160, + hd_wallet_rmd160: wallet_id.hd_wallet_rmd160, + account_id: account_info.account_id, + account_xpub: account_info.account_xpub, + external_addresses_number: account_info.external_addresses_number, + internal_addresses_number: account_info.internal_addresses_number, + } + } +} + +impl From for HDAccountStorageItem { + fn from(account: HDAccountTable) -> Self { + HDAccountStorageItem { + account_id: account.account_id, + account_xpub: account.account_xpub, + external_addresses_number: account.external_addresses_number, + internal_addresses_number: account.internal_addresses_number, + } + } +} + +pub struct HDWalletDb { + pub(crate) inner: IndexedDb, +} + +#[async_trait] +impl DbInstance for HDWalletDb { + fn db_name() -> &'static str { DB_NAME } + + async fn init(db_id: DbIdentifier) -> InitDbResult { + let inner = IndexedDbBuilder::new(db_id) + .with_version(DB_VERSION) + .with_table::() + .build() + .await?; + Ok(HDWalletDb { inner }) + } +} + +/// The wrapper over the [`CoinsContext::hd_wallet_db`] weak pointer. +pub struct HDWalletIndexedDbStorage { + db: WeakDb, +} + +#[async_trait] +impl HDWalletStorageInternalOps for HDWalletIndexedDbStorage { + async fn init(ctx: &MmArc) -> HDWalletStorageResult + where + Self: Sized, + { + let coins_ctx = CoinsContext::from_ctx(ctx).map_to_mm(HDWalletStorageError::Internal)?; + let db = SharedDb::downgrade(&coins_ctx.hd_wallet_db); + Ok(HDWalletIndexedDbStorage { db }) + } + + async fn load_accounts(&self, wallet_id: HDWalletId) -> HDWalletStorageResult> { + let shared_db = self.get_shared_db()?; + let locked_db = Self::lock_db(&shared_db).await?; + + let transaction = locked_db.inner.transaction().await?; + let table = transaction.table::().await?; + + let index_keys = MultiIndex::new(WALLET_ID_INDEX) + .with_value(wallet_id.coin)? + .with_value(wallet_id.mm2_rmd160)? + .with_value(wallet_id.hd_wallet_rmd160)?; + Ok(table + .get_items_by_multi_index(index_keys) + .await? + .into_iter() + .map(|(_item_id, item)| HDAccountStorageItem::from(item)) + .collect()) + } + + async fn load_account( + &self, + wallet_id: HDWalletId, + account_id: u32, + ) -> HDWalletStorageResult> { + let shared_db = self.get_shared_db()?; + let locked_db = Self::lock_db(&shared_db).await?; + + let transaction = locked_db.inner.transaction().await?; + let table = transaction.table::().await?; + + let maybe_account = Self::find_account(&table, wallet_id, account_id).await?; + match maybe_account { + Some((_account_item_id, account_item)) => Ok(Some(HDAccountStorageItem::from(account_item))), + None => Ok(None), + } + } + + async fn update_external_addresses_number( + &self, + wallet_id: HDWalletId, + account_id: u32, + new_external_addresses_number: u32, + ) -> HDWalletStorageResult<()> { + self.update_account(wallet_id, account_id, |account| { + account.external_addresses_number = new_external_addresses_number; + }) + .await + } + + async fn update_internal_addresses_number( + &self, + wallet_id: HDWalletId, + account_id: u32, + new_internal_addresses_number: u32, + ) -> HDWalletStorageResult<()> { + self.update_account(wallet_id, account_id, |account| { + account.internal_addresses_number = new_internal_addresses_number; + }) + .await + } + + async fn upload_new_account( + &self, + wallet_id: HDWalletId, + account: HDAccountStorageItem, + ) -> HDWalletStorageResult<()> { + let shared_db = self.get_shared_db()?; + let locked_db = Self::lock_db(&shared_db).await?; + + let transaction = locked_db.inner.transaction().await?; + let table = transaction.table::().await?; + + let new_account = HDAccountTable::new(wallet_id, account); + table + .add_item(&new_account) + .await + .map(|_| ()) + .mm_err(HDWalletStorageError::from) + } + + async fn clear_accounts(&self, wallet_id: HDWalletId) -> HDWalletStorageResult<()> { + let shared_db = self.get_shared_db()?; + let locked_db = Self::lock_db(&shared_db).await?; + + let transaction = locked_db.inner.transaction().await?; + let table = transaction.table::().await?; + + let index_keys = MultiIndex::new(WALLET_ID_INDEX) + .with_value(wallet_id.coin)? + .with_value(wallet_id.mm2_rmd160)? + .with_value(wallet_id.hd_wallet_rmd160)?; + table.delete_items_by_multi_index(index_keys).await?; + Ok(()) + } +} + +impl HDWalletIndexedDbStorage { + fn get_shared_db(&self) -> HDWalletStorageResult> { + self.db + .upgrade() + .or_mm_err(|| HDWalletStorageError::Internal("'HDWalletIndexedDbStorage::db' doesn't exist".to_owned())) + } + + async fn lock_db(db: &SharedDb) -> HDWalletStorageResult> { + db.get_or_initialize().await.mm_err(HDWalletStorageError::from) + } + + async fn find_account( + table: &DbTable<'_, HDAccountTable>, + wallet_id: HDWalletId, + account_id: u32, + ) -> HDWalletStorageResult> { + let index_keys = MultiIndex::new(WALLET_ACCOUNT_ID_INDEX) + .with_value(wallet_id.coin)? + .with_value(wallet_id.mm2_rmd160)? + .with_value(wallet_id.hd_wallet_rmd160)? + .with_value(account_id)?; + table + .get_item_by_unique_multi_index(index_keys) + .await + .mm_err(HDWalletStorageError::from) + } + + async fn update_account(&self, wallet_id: HDWalletId, account_id: u32, f: F) -> HDWalletStorageResult<()> + where + F: FnOnce(&mut HDAccountTable), + { + let shared_db = self.get_shared_db()?; + let locked_db = Self::lock_db(&shared_db).await?; + + let transaction = locked_db.inner.transaction().await?; + let table = transaction.table::().await?; + + let (account_item_id, mut account) = Self::find_account(&table, wallet_id.clone(), account_id) + .await? + .or_mm_err(|| HDWalletStorageError::HDAccountNotFound { wallet_id, account_id })?; + + // Apply `f` to `account` and upload the changes to the storage. + f(&mut account); + table + .replace_item(account_item_id, &account) + .await + .map(|_| ()) + .mm_err(HDWalletStorageError::from) + } +} + +/// This function is used in `hd_wallet_storage::tests`. +pub(super) async fn get_all_storage_items(ctx: &MmArc) -> Vec { + let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); + let db = coins_ctx.hd_wallet_db.get_or_initialize().await.unwrap(); + let transaction = db.inner.transaction().await.unwrap(); + let table = transaction.table::().await.unwrap(); + table + .get_all_items() + .await + .expect("Error getting items") + .into_iter() + .map(|(_item_id, item)| HDAccountStorageItem::from(item)) + .collect() +} diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs new file mode 100644 index 0000000000..08e2acbb00 --- /dev/null +++ b/mm2src/coins/lightning.rs @@ -0,0 +1,1585 @@ +pub mod ln_conf; +pub mod ln_errors; +mod ln_events; +mod ln_p2p; +mod ln_platform; +mod ln_serialization; +mod ln_utils; + +use super::{lp_coinfind_or_err, DerivationMethod, MmCoinEnum}; +use crate::utxo::rpc_clients::UtxoRpcClientEnum; +use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, UtxoTxBuilder}; +use crate::utxo::{sat_from_big_decimal, BlockchainNetwork, FeePolicy, GetUtxoListOps, UtxoTxGenerationOps}; +use crate::{BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, + NegotiateSwapContractAddrErr, RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, + SignatureError, SignatureResult, SwapOps, TradeFee, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TransactionEnum, TransactionFut, UnexpectedDerivationMethod, UtxoStandardCoin, + ValidateAddressResult, ValidatePaymentInput, VerificationError, VerificationResult, WithdrawError, + WithdrawFut, WithdrawRequest}; +use async_trait::async_trait; +use bitcoin::hashes::Hash; +use bitcoin_hashes::sha256::Hash as Sha256; +use bitcrypto::dhash256; +use bitcrypto::ChecksumType; +use chain::TransactionOutput; +use common::executor::spawn; +use common::log::{LogOnError, LogState}; +use common::{async_blocking, calc_total_pages, log, now_ms, ten, PagingOptionsEnum}; +use futures::{FutureExt, TryFutureExt}; +use futures01::Future; +use keys::{hash::H256, AddressHashEnum, CompactSignature, KeyPair, Private, Public}; +use lightning::chain::channelmonitor::Balance; +use lightning::chain::keysinterface::{KeysInterface, KeysManager, Recipient}; +use lightning::chain::Access; +use lightning::ln::channelmanager::{ChannelDetails, MIN_FINAL_CLTV_EXPIRY}; +use lightning::ln::{PaymentHash, PaymentPreimage}; +use lightning::routing::network_graph::{NetGraphMsgHandler, NetworkGraph}; +use lightning::util::config::UserConfig; +use lightning_background_processor::BackgroundProcessor; +use lightning_invoice::payment; +use lightning_invoice::utils::{create_invoice_from_channelmanager, DefaultRouter}; +use lightning_invoice::{Invoice, InvoiceDescription}; +use lightning_persister::storage::{ClosedChannelsFilter, DbStorage, FileSystemStorage, HTLCStatus, + NodesAddressesMapShared, PaymentInfo, PaymentType, PaymentsFilter, Scorer, + SqlChannelDetails}; +use lightning_persister::LightningPersister; +use ln_conf::{ChannelOptions, LightningCoinConf, LightningProtocolConf, PlatformCoinConfirmations}; +use ln_errors::{ClaimableBalancesError, ClaimableBalancesResult, CloseChannelError, CloseChannelResult, + ConnectToNodeError, ConnectToNodeResult, EnableLightningError, EnableLightningResult, + GenerateInvoiceError, GenerateInvoiceResult, GetChannelDetailsError, GetChannelDetailsResult, + GetPaymentDetailsError, GetPaymentDetailsResult, ListChannelsError, ListChannelsResult, + ListPaymentsError, ListPaymentsResult, OpenChannelError, OpenChannelResult, SendPaymentError, + SendPaymentResult}; +use ln_events::LightningEventHandler; +use ln_p2p::{connect_to_node, ConnectToNodeRes, PeerManager}; +use ln_platform::{h256_json_from_txid, Platform}; +use ln_serialization::{InvoiceForRPC, NodeAddress, PublicKeyForRPC}; +use ln_utils::{ChainMonitor, ChannelManager}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_net::ip_addr::myipaddr; +use mm2_number::{BigDecimal, MmNumber}; +use parking_lot::Mutex as PaMutex; +use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; +use script::{Builder, TransactionInputSigner}; +use secp256k1::PublicKey; +use serde::{Deserialize, Serialize}; +use serde_json::Value as Json; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::net::SocketAddr; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +type Router = DefaultRouter, Arc>; +type InvoicePayer = payment::InvoicePayer, Router, Arc>, Arc, E>; + +#[derive(Clone)] +pub struct LightningCoin { + pub platform: Arc, + pub conf: LightningCoinConf, + /// The lightning node peer manager that takes care of connecting to peers, etc.. + pub peer_manager: Arc, + /// The lightning node background processor that takes care of tasks that need to happen periodically + pub background_processor: Arc, + /// The lightning node channel manager which keeps track of the number of open channels and sends messages to the appropriate + /// channel, also tracks HTLC preimages and forwards onion packets appropriately. + pub channel_manager: Arc, + /// The lightning node chain monitor that takes care of monitoring the chain for transactions of interest. + pub chain_monitor: Arc, + /// The lightning node keys manager that takes care of signing invoices. + pub keys_manager: Arc, + /// The lightning node invoice payer. + pub invoice_payer: Arc>>, + /// The lightning node persister that takes care of writing/reading data from storage. + pub persister: Arc, + /// The mutex storing the addresses of the nodes that the lightning node has open channels with, + /// these addresses are used for reconnecting. + pub open_channels_nodes: NodesAddressesMapShared, +} + +impl fmt::Debug for LightningCoin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "LightningCoin {{ conf: {:?} }}", self.conf) } +} + +impl LightningCoin { + fn platform_coin(&self) -> &UtxoStandardCoin { &self.platform.coin } + + #[inline] + fn my_node_id(&self) -> String { self.channel_manager.get_our_node_id().to_string() } + + fn get_balance_msat(&self) -> (u64, u64) { + self.channel_manager + .list_channels() + .iter() + .fold((0, 0), |(spendable, unspendable), chan| { + if chan.is_usable { + ( + spendable + chan.outbound_capacity_msat, + unspendable + chan.balance_msat - chan.outbound_capacity_msat, + ) + } else { + (spendable, unspendable + chan.balance_msat) + } + }) + } + + fn pay_invoice(&self, invoice: Invoice) -> SendPaymentResult { + self.invoice_payer + .pay_invoice(&invoice) + .map_to_mm(|e| SendPaymentError::PaymentError(format!("{:?}", e)))?; + let payment_hash = PaymentHash((*invoice.payment_hash()).into_inner()); + let payment_type = PaymentType::OutboundPayment { + destination: *invoice.payee_pub_key().unwrap_or(&invoice.recover_payee_pub_key()), + }; + let description = match invoice.description() { + InvoiceDescription::Direct(d) => d.to_string(), + InvoiceDescription::Hash(h) => hex::encode(h.0.into_inner()), + }; + let payment_secret = Some(*invoice.payment_secret()); + Ok(PaymentInfo { + payment_hash, + payment_type, + description, + preimage: None, + secret: payment_secret, + amt_msat: invoice.amount_milli_satoshis(), + fee_paid_msat: None, + status: HTLCStatus::Pending, + created_at: now_ms() / 1000, + last_updated: now_ms() / 1000, + }) + } + + fn keysend( + &self, + destination: PublicKey, + amount_msat: u64, + final_cltv_expiry_delta: u32, + ) -> SendPaymentResult { + if final_cltv_expiry_delta < MIN_FINAL_CLTV_EXPIRY { + return MmError::err(SendPaymentError::CLTVExpiryError( + final_cltv_expiry_delta, + MIN_FINAL_CLTV_EXPIRY, + )); + } + let payment_preimage = PaymentPreimage(self.keys_manager.get_secure_random_bytes()); + self.invoice_payer + .pay_pubkey(destination, payment_preimage, amount_msat, final_cltv_expiry_delta) + .map_to_mm(|e| SendPaymentError::PaymentError(format!("{:?}", e)))?; + let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).into_inner()); + let payment_type = PaymentType::OutboundPayment { destination }; + + Ok(PaymentInfo { + payment_hash, + payment_type, + description: "".into(), + preimage: Some(payment_preimage), + secret: None, + amt_msat: Some(amount_msat), + fee_paid_msat: None, + status: HTLCStatus::Pending, + created_at: now_ms() / 1000, + last_updated: now_ms() / 1000, + }) + } + + async fn get_open_channels_by_filter( + &self, + filter: Option, + paging: PagingOptionsEnum, + limit: usize, + ) -> ListChannelsResult { + let mut total_open_channels: Vec = self + .channel_manager + .list_channels() + .into_iter() + .map(From::from) + .collect(); + + total_open_channels.sort_by(|a, b| a.rpc_channel_id.cmp(&b.rpc_channel_id)); + + let open_channels_filtered = if let Some(ref f) = filter { + total_open_channels + .into_iter() + .filter(|chan| apply_open_channel_filter(chan, f)) + .collect() + } else { + total_open_channels + }; + + let offset = match paging { + PagingOptionsEnum::PageNumber(page) => (page.get() - 1) * limit, + PagingOptionsEnum::FromId(rpc_id) => open_channels_filtered + .iter() + .position(|x| x.rpc_channel_id == rpc_id) + .map(|pos| pos + 1) + .unwrap_or_default(), + }; + + let total = open_channels_filtered.len(); + + let channels = if offset + limit <= total { + open_channels_filtered[offset..offset + limit].to_vec() + } else { + open_channels_filtered[offset..].to_vec() + }; + + Ok(GetOpenChannelsResult { + channels, + skipped: offset, + total, + }) + } +} + +#[async_trait] +// Todo: Implement this when implementing swaps for lightning as it's is used only for swaps +impl SwapOps for LightningCoin { + fn send_taker_fee(&self, _fee_addr: &[u8], _amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { unimplemented!() } + + fn send_maker_payment( + &self, + _time_lock: u32, + _taker_pub: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_taker_payment( + &self, + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_maker_spends_taker_payment( + &self, + _taker_payment_tx: &[u8], + _time_lock: u32, + _taker_pub: &[u8], + _secret: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_taker_spends_maker_payment( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_taker_refunds_payment( + &self, + _taker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_maker_refunds_payment( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _taker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn validate_fee( + &self, + _fee_tx: &TransactionEnum, + _expected_sender: &[u8], + _fee_addr: &[u8], + _amount: &BigDecimal, + _min_block_number: u64, + _uuid: &[u8], + ) -> Box + Send> { + unimplemented!() + } + + fn validate_maker_payment( + &self, + _input: ValidatePaymentInput, + ) -> Box + Send> { + unimplemented!() + } + + fn validate_taker_payment( + &self, + _input: ValidatePaymentInput, + ) -> Box + Send> { + unimplemented!() + } + + fn check_if_my_payment_sent( + &self, + _time_lock: u32, + _other_pub: &[u8], + _secret_hash: &[u8], + _search_from_block: u64, + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> Box, Error = String> + Send> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_my( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_other( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + fn extract_secret(&self, _secret_hash: &[u8], _spend_tx: &[u8]) -> Result, String> { unimplemented!() } + + fn negotiate_swap_contract_addr( + &self, + _other_side_address: Option<&[u8]>, + ) -> Result, MmError> { + unimplemented!() + } + + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { unimplemented!() } +} + +impl MarketCoinOps for LightningCoin { + fn ticker(&self) -> &str { &self.conf.ticker } + + fn my_address(&self) -> Result { Ok(self.my_node_id()) } + + fn get_public_key(&self) -> Result> { unimplemented!() } + + fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { + let mut _message_prefix = self.conf.sign_message_prefix.clone()?; + let prefixed_message = format!("{}{}", _message_prefix, message); + Some(dhash256(prefixed_message.as_bytes()).take()) + } + + fn sign_message(&self, message: &str) -> SignatureResult { + let message_hash = self.sign_message_hash(message).ok_or(SignatureError::PrefixNotFound)?; + let secret_key = self + .keys_manager + .get_node_secret(Recipient::Node) + .map_err(|_| SignatureError::InternalError("Error accessing node keys".to_string()))?; + let private = Private { + prefix: 239, + secret: H256::from(*secret_key.as_ref()), + compressed: true, + checksum_type: ChecksumType::DSHA256, + }; + let signature = private.sign_compact(&H256::from(message_hash))?; + Ok(zbase32::encode_full_bytes(&*signature)) + } + + fn verify_message(&self, signature: &str, message: &str, pubkey: &str) -> VerificationResult { + let message_hash = self + .sign_message_hash(message) + .ok_or(VerificationError::PrefixNotFound)?; + let signature = CompactSignature::from( + zbase32::decode_full_bytes_str(signature) + .map_err(|e| VerificationError::SignatureDecodingError(e.to_string()))?, + ); + let recovered_pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; + Ok(recovered_pubkey.to_string() == pubkey) + } + + fn my_balance(&self) -> BalanceFut { + let decimals = self.decimals(); + let (spendable_msat, unspendable_msat) = self.get_balance_msat(); + let my_balance = CoinBalance { + spendable: big_decimal_from_sat_unsigned(spendable_msat, decimals), + unspendable: big_decimal_from_sat_unsigned(unspendable_msat, decimals), + }; + Box::new(futures01::future::ok(my_balance)) + } + + fn base_coin_balance(&self) -> BalanceFut { + Box::new(self.platform_coin().my_balance().map(|res| res.spendable)) + } + + fn platform_ticker(&self) -> &str { self.platform_coin().ticker() } + + fn send_raw_tx(&self, _tx: &str) -> Box + Send> { + Box::new(futures01::future::err( + MmError::new( + "send_raw_tx is not supported for lightning, please use send_payment method instead.".to_string(), + ) + .to_string(), + )) + } + + fn send_raw_tx_bytes(&self, _tx: &[u8]) -> Box + Send> { + Box::new(futures01::future::err( + MmError::new( + "send_raw_tx is not supported for lightning, please use send_payment method instead.".to_string(), + ) + .to_string(), + )) + } + + // Todo: Implement this when implementing swaps for lightning as it's is used mainly for swaps + fn wait_for_confirmations( + &self, + _tx: &[u8], + _confirmations: u64, + _requires_nota: bool, + _wait_until: u64, + _check_every: u64, + ) -> Box + Send> { + unimplemented!() + } + + // Todo: Implement this when implementing swaps for lightning as it's is used mainly for swaps + fn wait_for_tx_spend( + &self, + _transaction: &[u8], + _wait_until: u64, + _from_block: u64, + _swap_contract_address: &Option, + ) -> TransactionFut { + unimplemented!() + } + + // Todo: Implement this when implementing swaps for lightning as it's is used mainly for swaps + fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result { unimplemented!() } + + fn current_block(&self) -> Box + Send> { Box::new(futures01::future::ok(0)) } + + fn display_priv_key(&self) -> Result { + Ok(self + .keys_manager + .get_node_secret(Recipient::Node) + .map_err(|_| "Unsupported recipient".to_string())? + .to_string()) + } + + // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps + fn min_tx_amount(&self) -> BigDecimal { unimplemented!() } + + // Todo: Implement this when implementing swaps for lightning as it's is used only for order matching/swaps + fn min_trading_vol(&self) -> MmNumber { unimplemented!() } +} + +#[async_trait] +impl MmCoin for LightningCoin { + fn is_asset_chain(&self) -> bool { false } + + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { + Box::new(self.platform_coin().get_raw_transaction(req)) + } + + fn withdraw(&self, _req: WithdrawRequest) -> WithdrawFut { + let fut = async move { + MmError::err(WithdrawError::InternalError( + "withdraw method is not supported for lightning, please use generate_invoice method instead.".into(), + )) + }; + Box::new(fut.boxed().compat()) + } + + fn decimals(&self) -> u8 { self.conf.decimals } + + fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { + Err(MmError::new("Address conversion is not available for LightningCoin".to_string()).to_string()) + } + + fn validate_address(&self, address: &str) -> ValidateAddressResult { + match PublicKey::from_str(address) { + Ok(_) => ValidateAddressResult { + is_valid: true, + reason: None, + }, + Err(e) => ValidateAddressResult { + is_valid: false, + reason: Some(format!("Error {} on parsing node public key", e)), + }, + } + } + + // Todo: Implement this when implementing payments history for lightning + fn process_history_loop(&self, _ctx: MmArc) -> Box + Send> { unimplemented!() } + + // Todo: Implement this when implementing payments history for lightning + fn history_sync_status(&self) -> HistorySyncState { unimplemented!() } + + // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps + fn get_trade_fee(&self) -> Box + Send> { unimplemented!() } + + // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps + async fn get_sender_trade_fee( + &self, + _value: TradePreimageValue, + _stage: FeeApproxStage, + ) -> TradePreimageResult { + unimplemented!() + } + + // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps + fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + + // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps + async fn get_fee_to_send_taker_fee( + &self, + _dex_fee_amount: BigDecimal, + _stage: FeeApproxStage, + ) -> TradePreimageResult { + unimplemented!() + } + + // Lightning payments are either pending, successful or failed. Once a payment succeeds there is no need to for confirmations + // unlike onchain transactions. + fn required_confirmations(&self) -> u64 { 0 } + + fn requires_notarization(&self) -> bool { false } + + fn set_required_confirmations(&self, _confirmations: u64) {} + + fn set_requires_notarization(&self, _requires_nota: bool) {} + + fn swap_contract_address(&self) -> Option { None } + + fn mature_confirmations(&self) -> Option { None } + + // Todo: Implement this when implementing order matching for lightning as it's is used only for order matching + fn coin_protocol_info(&self) -> Vec { unimplemented!() } + + // Todo: Implement this when implementing order matching for lightning as it's is used only for order matching + fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { unimplemented!() } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LightningParams { + // The listening port for the p2p LN node + pub listening_port: u16, + // Printable human-readable string to describe this node to other users. + pub node_name: [u8; 32], + // Node's RGB color. This is used for showing the node in a network graph with the desired color. + pub node_color: [u8; 3], + // Invoice Payer is initialized while starting the lightning node, and it requires the number of payment retries that + // it should do before considering a payment failed or partially failed. If not provided the number of retries will be 5 + // as this is a good default value. + pub payment_retries: Option, + // Node's backup path for channels and other data that requires backup. + pub backup_path: Option, +} + +pub async fn start_lightning( + ctx: &MmArc, + platform_coin: UtxoStandardCoin, + protocol_conf: LightningProtocolConf, + conf: LightningCoinConf, + params: LightningParams, +) -> EnableLightningResult { + // Todo: add support for Hardware wallets for funding transactions and spending spendable outputs (channel closing transactions) + if let DerivationMethod::HDWallet(_) = platform_coin.as_ref().derivation_method { + return MmError::err(EnableLightningError::UnsupportedMode( + "'start_lightning'".into(), + "iguana".into(), + )); + } + + let platform = Arc::new(Platform::new( + platform_coin.clone(), + protocol_conf.network.clone(), + protocol_conf.confirmations, + )); + + // Initialize the Logger + let logger = ctx.log.0.clone(); + + // Initialize Persister + let persister = ln_utils::init_persister(ctx, platform.clone(), conf.ticker.clone(), params.backup_path).await?; + + // Initialize the KeysManager + let keys_manager = ln_utils::init_keys_manager(ctx)?; + + // Initialize the NetGraphMsgHandler. This is used for providing routes to send payments over + let network_graph = Arc::new(persister.get_network_graph(protocol_conf.network.into()).await?); + spawn(ln_utils::persist_network_graph_loop( + persister.clone(), + network_graph.clone(), + )); + let network_gossip = Arc::new(NetGraphMsgHandler::new( + network_graph.clone(), + None::>, + logger.clone(), + )); + + // Initialize the ChannelManager + let (chain_monitor, channel_manager) = ln_utils::init_channel_manager( + platform.clone(), + logger.clone(), + persister.clone(), + keys_manager.clone(), + conf.clone().into(), + ) + .await?; + + // Initialize the PeerManager + let peer_manager = ln_p2p::init_peer_manager( + ctx.clone(), + params.listening_port, + channel_manager.clone(), + network_gossip.clone(), + keys_manager + .get_node_secret(Recipient::Node) + .map_to_mm(|_| EnableLightningError::UnsupportedMode("'start_lightning'".into(), "local node".into()))?, + logger.clone(), + ) + .await?; + + // Initialize the event handler + let event_handler = Arc::new(ln_events::LightningEventHandler::new( + // It's safe to use unwrap here for now until implementing Native Client for Lightning + platform.clone(), + channel_manager.clone(), + keys_manager.clone(), + persister.clone(), + )); + + // Initialize routing Scorer + let scorer = Arc::new(Mutex::new(persister.get_scorer(network_graph.clone()).await?)); + spawn(ln_utils::persist_scorer_loop(persister.clone(), scorer.clone())); + + // Create InvoicePayer + let router = DefaultRouter::new(network_graph, logger.clone(), keys_manager.get_secure_random_bytes()); + let invoice_payer = Arc::new(InvoicePayer::new( + channel_manager.clone(), + router, + scorer, + logger.clone(), + event_handler, + payment::RetryAttempts(params.payment_retries.unwrap_or(5)), + )); + + // Persist ChannelManager + // Note: if the ChannelManager is not persisted properly to disk, there is risk of channels force closing the next time LN starts up + let channel_manager_persister = persister.clone(); + let persist_channel_manager_callback = + move |node: &ChannelManager| channel_manager_persister.persist_manager(&*node); + + // Start Background Processing. Runs tasks periodically in the background to keep LN node operational. + // InvoicePayer will act as our event handler as it handles some of the payments related events before + // delegating it to LightningEventHandler. + let background_processor = Arc::new(BackgroundProcessor::start( + persist_channel_manager_callback, + invoice_payer.clone(), + chain_monitor.clone(), + channel_manager.clone(), + Some(network_gossip), + peer_manager.clone(), + logger, + )); + + // If channel_nodes_data file exists, read channels nodes data from disk and reconnect to channel nodes/peers if possible. + let open_channels_nodes = Arc::new(PaMutex::new( + ln_utils::get_open_channels_nodes_addresses(persister.clone(), channel_manager.clone()).await?, + )); + spawn(ln_p2p::connect_to_nodes_loop( + open_channels_nodes.clone(), + peer_manager.clone(), + )); + + // Broadcast Node Announcement + spawn(ln_p2p::ln_node_announcement_loop( + channel_manager.clone(), + params.node_name, + params.node_color, + params.listening_port, + )); + + Ok(LightningCoin { + platform, + conf, + peer_manager, + background_processor, + channel_manager, + chain_monitor, + keys_manager, + invoice_payer, + persister, + open_channels_nodes, + }) +} + +#[derive(Deserialize)] +pub struct ConnectToNodeRequest { + pub coin: String, + pub node_address: NodeAddress, +} + +/// Connect to a certain node on the lightning network. +pub async fn connect_to_lightning_node(ctx: MmArc, req: ConnectToNodeRequest) -> ConnectToNodeResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let ln_coin = match coin { + MmCoinEnum::LightningCoin(c) => c, + _ => return MmError::err(ConnectToNodeError::UnsupportedCoin(coin.ticker().to_string())), + }; + + let node_pubkey = req.node_address.pubkey; + let node_addr = req.node_address.addr; + let res = connect_to_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()).await?; + + // If a node that we have an open channel with changed it's address, "connect_to_lightning_node" + // can be used to reconnect to the new address while saving this new address for reconnections. + if let ConnectToNodeRes::ConnectedSuccessfully { .. } = res { + if let Entry::Occupied(mut entry) = ln_coin.open_channels_nodes.lock().entry(node_pubkey) { + entry.insert(node_addr); + } + ln_coin + .persister + .save_nodes_addresses(ln_coin.open_channels_nodes) + .await?; + } + + Ok(res.to_string()) +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(tag = "type", content = "value")] +pub enum ChannelOpenAmount { + Exact(BigDecimal), + Max, +} + +#[derive(Deserialize)] +pub struct OpenChannelRequest { + pub coin: String, + pub node_address: NodeAddress, + pub amount: ChannelOpenAmount, + /// The amount to push to the counterparty as part of the open, in milli-satoshi. Creates inbound liquidity for the channel. + /// By setting push_msat to a value, opening channel request will be equivalent to opening a channel then sending a payment with + /// the push_msat amount. + #[serde(default)] + pub push_msat: u64, + pub channel_options: Option, + pub counterparty_locktime: Option, + pub our_htlc_minimum_msat: Option, +} + +#[derive(Serialize)] +pub struct OpenChannelResponse { + rpc_channel_id: u64, + node_address: NodeAddress, +} + +/// Opens a channel on the lightning network. +pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let ln_coin = match coin { + MmCoinEnum::LightningCoin(c) => c, + _ => return MmError::err(OpenChannelError::UnsupportedCoin(coin.ticker().to_string())), + }; + + // Making sure that the node data is correct and that we can connect to it before doing more operations + let node_pubkey = req.node_address.pubkey; + let node_addr = req.node_address.addr; + connect_to_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()).await?; + + let platform_coin = ln_coin.platform_coin().clone(); + let decimals = platform_coin.as_ref().decimals; + let my_address = platform_coin.as_ref().derivation_method.iguana_or_err()?; + let (unspents, _) = platform_coin.get_unspent_ordered_list(my_address).await?; + let (value, fee_policy) = match req.amount.clone() { + ChannelOpenAmount::Max => ( + unspents.iter().fold(0, |sum, unspent| sum + unspent.value), + FeePolicy::DeductFromOutput(0), + ), + ChannelOpenAmount::Exact(v) => { + let value = sat_from_big_decimal(&v, decimals)?; + (value, FeePolicy::SendExact) + }, + }; + + // The actual script_pubkey will replace this before signing the transaction after receiving the required + // output script from the other node when the channel is accepted + let script_pubkey = + Builder::build_witness_script(&AddressHashEnum::WitnessScriptHash(Default::default())).to_bytes(); + let outputs = vec![TransactionOutput { value, script_pubkey }]; + + let mut tx_builder = UtxoTxBuilder::new(&platform_coin) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_fee_policy(fee_policy); + + let fee = platform_coin + .get_tx_fee() + .await + .map_err(|e| OpenChannelError::RpcError(e.to_string()))?; + tx_builder = tx_builder.with_fee(fee); + + let (unsigned, _) = tx_builder.build().await?; + + let amount_in_sat = unsigned.outputs[0].value; + let push_msat = req.push_msat; + let channel_manager = ln_coin.channel_manager.clone(); + + let mut conf = ln_coin.conf.clone(); + if let Some(options) = req.channel_options { + match conf.channel_options.as_mut() { + Some(o) => o.update(options), + None => conf.channel_options = Some(options), + } + } + + let mut user_config: UserConfig = conf.into(); + if let Some(locktime) = req.counterparty_locktime { + user_config.own_channel_config.our_to_self_delay = locktime; + } + if let Some(min) = req.our_htlc_minimum_msat { + user_config.own_channel_config.our_htlc_minimum_msat = min; + } + + let rpc_channel_id = ln_coin.persister.get_last_channel_rpc_id().await? as u64 + 1; + + let temp_channel_id = async_blocking(move || { + channel_manager + .create_channel(node_pubkey, amount_in_sat, push_msat, rpc_channel_id, Some(user_config)) + .map_to_mm(|e| OpenChannelError::FailureToOpenChannel(node_pubkey.to_string(), format!("{:?}", e))) + }) + .await?; + + { + let mut unsigned_funding_txs = ln_coin.platform.unsigned_funding_txs.lock(); + unsigned_funding_txs.insert(rpc_channel_id, unsigned); + } + + let pending_channel_details = SqlChannelDetails::new( + rpc_channel_id, + temp_channel_id, + node_pubkey, + true, + user_config.channel_options.announced_channel, + ); + + // Saving node data to reconnect to it on restart + ln_coin.open_channels_nodes.lock().insert(node_pubkey, node_addr); + ln_coin + .persister + .save_nodes_addresses(ln_coin.open_channels_nodes) + .await?; + + ln_coin.persister.add_channel_to_db(pending_channel_details).await?; + + Ok(OpenChannelResponse { + rpc_channel_id, + node_address: req.node_address, + }) +} + +#[derive(Deserialize)] +pub struct OpenChannelsFilter { + pub channel_id: Option, + pub counterparty_node_id: Option, + pub funding_tx: Option, + pub from_funding_value_sats: Option, + pub to_funding_value_sats: Option, + pub is_outbound: Option, + pub from_balance_msat: Option, + pub to_balance_msat: Option, + pub from_outbound_capacity_msat: Option, + pub to_outbound_capacity_msat: Option, + pub from_inbound_capacity_msat: Option, + pub to_inbound_capacity_msat: Option, + pub confirmed: Option, + pub is_usable: Option, + pub is_public: Option, +} + +fn apply_open_channel_filter(channel_details: &ChannelDetailsForRPC, filter: &OpenChannelsFilter) -> bool { + let is_channel_id = filter.channel_id.is_none() || Some(&channel_details.channel_id) == filter.channel_id.as_ref(); + + let is_counterparty_node_id = filter.counterparty_node_id.is_none() + || Some(&channel_details.counterparty_node_id) == filter.counterparty_node_id.as_ref(); + + let is_funding_tx = filter.funding_tx.is_none() || channel_details.funding_tx == filter.funding_tx; + + let is_from_funding_value_sats = + Some(&channel_details.funding_tx_value_sats) >= filter.from_funding_value_sats.as_ref(); + + let is_to_funding_value_sats = filter.to_funding_value_sats.is_none() + || Some(&channel_details.funding_tx_value_sats) <= filter.to_funding_value_sats.as_ref(); + + let is_outbound = filter.is_outbound.is_none() || Some(&channel_details.is_outbound) == filter.is_outbound.as_ref(); + + let is_from_balance_msat = Some(&channel_details.balance_msat) >= filter.from_balance_msat.as_ref(); + + let is_to_balance_msat = + filter.to_balance_msat.is_none() || Some(&channel_details.balance_msat) <= filter.to_balance_msat.as_ref(); + + let is_from_outbound_capacity_msat = + Some(&channel_details.outbound_capacity_msat) >= filter.from_outbound_capacity_msat.as_ref(); + + let is_to_outbound_capacity_msat = filter.to_outbound_capacity_msat.is_none() + || Some(&channel_details.outbound_capacity_msat) <= filter.to_outbound_capacity_msat.as_ref(); + + let is_from_inbound_capacity_msat = + Some(&channel_details.inbound_capacity_msat) >= filter.from_inbound_capacity_msat.as_ref(); + + let is_to_inbound_capacity_msat = filter.to_inbound_capacity_msat.is_none() + || Some(&channel_details.inbound_capacity_msat) <= filter.to_inbound_capacity_msat.as_ref(); + + let is_confirmed = filter.confirmed.is_none() || Some(&channel_details.confirmed) == filter.confirmed.as_ref(); + + let is_usable = filter.is_usable.is_none() || Some(&channel_details.is_usable) == filter.is_usable.as_ref(); + + let is_public = filter.is_public.is_none() || Some(&channel_details.is_public) == filter.is_public.as_ref(); + + is_channel_id + && is_counterparty_node_id + && is_funding_tx + && is_from_funding_value_sats + && is_to_funding_value_sats + && is_outbound + && is_from_balance_msat + && is_to_balance_msat + && is_from_outbound_capacity_msat + && is_to_outbound_capacity_msat + && is_from_inbound_capacity_msat + && is_to_inbound_capacity_msat + && is_confirmed + && is_usable + && is_public +} + +#[derive(Deserialize)] +pub struct ListOpenChannelsRequest { + pub coin: String, + pub filter: Option, + #[serde(default = "ten")] + limit: usize, + #[serde(default)] + paging_options: PagingOptionsEnum, +} + +#[derive(Clone, Serialize)] +pub struct ChannelDetailsForRPC { + pub rpc_channel_id: u64, + pub channel_id: H256Json, + pub counterparty_node_id: PublicKeyForRPC, + pub funding_tx: Option, + pub funding_tx_output_index: Option, + pub funding_tx_value_sats: u64, + /// True if the channel was initiated (and thus funded) by us. + pub is_outbound: bool, + pub balance_msat: u64, + pub outbound_capacity_msat: u64, + pub inbound_capacity_msat: u64, + // Channel is confirmed onchain, this means that funding_locked messages have been exchanged, + // the channel is not currently being shut down, and the required confirmation count has been reached. + pub confirmed: bool, + // Channel is confirmed and funding_locked messages have been exchanged, the peer is connected, + // and the channel is not currently negotiating a shutdown. + pub is_usable: bool, + // A publicly-announced channel. + pub is_public: bool, +} + +impl From for ChannelDetailsForRPC { + fn from(details: ChannelDetails) -> ChannelDetailsForRPC { + ChannelDetailsForRPC { + rpc_channel_id: details.user_channel_id, + channel_id: details.channel_id.into(), + counterparty_node_id: PublicKeyForRPC(details.counterparty.node_id), + funding_tx: details.funding_txo.map(|tx| h256_json_from_txid(tx.txid)), + funding_tx_output_index: details.funding_txo.map(|tx| tx.index), + funding_tx_value_sats: details.channel_value_satoshis, + is_outbound: details.is_outbound, + balance_msat: details.balance_msat, + outbound_capacity_msat: details.outbound_capacity_msat, + inbound_capacity_msat: details.inbound_capacity_msat, + confirmed: details.is_funding_locked, + is_usable: details.is_usable, + is_public: details.is_public, + } + } +} + +struct GetOpenChannelsResult { + pub channels: Vec, + pub skipped: usize, + pub total: usize, +} + +#[derive(Serialize)] +pub struct ListOpenChannelsResponse { + open_channels: Vec, + limit: usize, + skipped: usize, + total: usize, + total_pages: usize, + paging_options: PagingOptionsEnum, +} + +pub async fn list_open_channels_by_filter( + ctx: MmArc, + req: ListOpenChannelsRequest, +) -> ListChannelsResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let ln_coin = match coin { + MmCoinEnum::LightningCoin(c) => c, + _ => return MmError::err(ListChannelsError::UnsupportedCoin(coin.ticker().to_string())), + }; + + let result = ln_coin + .get_open_channels_by_filter(req.filter, req.paging_options.clone(), req.limit) + .await?; + + Ok(ListOpenChannelsResponse { + open_channels: result.channels, + limit: req.limit, + skipped: result.skipped, + total: result.total, + total_pages: calc_total_pages(result.total, req.limit), + paging_options: req.paging_options, + }) +} + +#[derive(Deserialize)] +pub struct ListClosedChannelsRequest { + pub coin: String, + pub filter: Option, + #[serde(default = "ten")] + limit: usize, + #[serde(default)] + paging_options: PagingOptionsEnum, +} + +#[derive(Serialize)] +pub struct ListClosedChannelsResponse { + closed_channels: Vec, + limit: usize, + skipped: usize, + total: usize, + total_pages: usize, + paging_options: PagingOptionsEnum, +} + +pub async fn list_closed_channels_by_filter( + ctx: MmArc, + req: ListClosedChannelsRequest, +) -> ListChannelsResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let ln_coin = match coin { + MmCoinEnum::LightningCoin(c) => c, + _ => return MmError::err(ListChannelsError::UnsupportedCoin(coin.ticker().to_string())), + }; + let closed_channels_res = ln_coin + .persister + .get_closed_channels_by_filter(req.filter, req.paging_options.clone(), req.limit) + .await?; + + Ok(ListClosedChannelsResponse { + closed_channels: closed_channels_res.channels, + limit: req.limit, + skipped: closed_channels_res.skipped, + total: closed_channels_res.total, + total_pages: calc_total_pages(closed_channels_res.total, req.limit), + paging_options: req.paging_options, + }) +} + +#[derive(Deserialize)] +pub struct GetChannelDetailsRequest { + pub coin: String, + pub rpc_channel_id: u64, +} + +#[derive(Serialize)] +#[serde(tag = "status", content = "details")] +pub enum GetChannelDetailsResponse { + Open(ChannelDetailsForRPC), + Closed(SqlChannelDetails), +} + +pub async fn get_channel_details( + ctx: MmArc, + req: GetChannelDetailsRequest, +) -> GetChannelDetailsResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let ln_coin = match coin { + MmCoinEnum::LightningCoin(c) => c, + _ => return MmError::err(GetChannelDetailsError::UnsupportedCoin(coin.ticker().to_string())), + }; + let channel_details = match ln_coin + .channel_manager + .list_channels() + .into_iter() + .find(|chan| chan.user_channel_id == req.rpc_channel_id) + { + Some(details) => GetChannelDetailsResponse::Open(details.into()), + None => GetChannelDetailsResponse::Closed( + ln_coin + .persister + .get_channel_from_db(req.rpc_channel_id) + .await? + .ok_or(GetChannelDetailsError::NoSuchChannel(req.rpc_channel_id))?, + ), + }; + + Ok(channel_details) +} + +#[derive(Deserialize)] +pub struct GenerateInvoiceRequest { + pub coin: String, + pub amount_in_msat: Option, + pub description: String, +} + +#[derive(Serialize)] +pub struct GenerateInvoiceResponse { + payment_hash: H256Json, + invoice: InvoiceForRPC, +} + +/// Generates an invoice (request for payment) that can be paid on the lightning network by another node using send_payment. +pub async fn generate_invoice( + ctx: MmArc, + req: GenerateInvoiceRequest, +) -> GenerateInvoiceResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let ln_coin = match coin { + MmCoinEnum::LightningCoin(c) => c, + _ => return MmError::err(GenerateInvoiceError::UnsupportedCoin(coin.ticker().to_string())), + }; + let open_channels_nodes = ln_coin.open_channels_nodes.lock().clone(); + for (node_pubkey, node_addr) in open_channels_nodes { + connect_to_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()) + .await + .error_log_with_msg(&format!( + "Channel with node: {} can't be used for invoice routing hints due to connection error.", + node_pubkey + )); + } + let network = ln_coin.platform.network.clone().into(); + let invoice = create_invoice_from_channelmanager( + &ln_coin.channel_manager, + ln_coin.keys_manager, + network, + req.amount_in_msat, + req.description.clone(), + )?; + let payment_hash = invoice.payment_hash().into_inner(); + let payment_info = PaymentInfo { + payment_hash: PaymentHash(payment_hash), + payment_type: PaymentType::InboundPayment, + description: req.description, + preimage: None, + secret: Some(*invoice.payment_secret()), + amt_msat: req.amount_in_msat, + fee_paid_msat: None, + status: HTLCStatus::Pending, + created_at: now_ms() / 1000, + last_updated: now_ms() / 1000, + }; + ln_coin.persister.add_or_update_payment_in_db(payment_info).await?; + Ok(GenerateInvoiceResponse { + payment_hash: payment_hash.into(), + invoice: invoice.into(), + }) +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum Payment { + #[serde(rename = "invoice")] + Invoice { invoice: InvoiceForRPC }, + #[serde(rename = "keysend")] + Keysend { + // The recieving node pubkey (node ID) + destination: PublicKeyForRPC, + // Amount to send in millisatoshis + amount_in_msat: u64, + // The number of blocks the payment will be locked for if not claimed by the destination, + // It's can be assumed that 6 blocks = 1 hour. We can claim the payment amount back after this cltv expires. + // Minmum value allowed is MIN_FINAL_CLTV_EXPIRY which is currently 24 for rust-lightning. + expiry: u32, + }, +} + +#[derive(Deserialize)] +pub struct SendPaymentReq { + pub coin: String, + pub payment: Payment, +} + +#[derive(Serialize)] +pub struct SendPaymentResponse { + payment_hash: H256Json, +} + +pub async fn send_payment(ctx: MmArc, req: SendPaymentReq) -> SendPaymentResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let ln_coin = match coin { + MmCoinEnum::LightningCoin(c) => c, + _ => return MmError::err(SendPaymentError::UnsupportedCoin(coin.ticker().to_string())), + }; + let open_channels_nodes = ln_coin.open_channels_nodes.lock().clone(); + for (node_pubkey, node_addr) in open_channels_nodes { + connect_to_node(node_pubkey, node_addr, ln_coin.peer_manager.clone()) + .await + .error_log_with_msg(&format!( + "Channel with node: {} can't be used to route this payment due to connection error.", + node_pubkey + )); + } + let payment_info = match req.payment { + Payment::Invoice { invoice } => ln_coin.pay_invoice(invoice.into())?, + Payment::Keysend { + destination, + amount_in_msat, + expiry, + } => ln_coin.keysend(destination.into(), amount_in_msat, expiry)?, + }; + ln_coin + .persister + .add_or_update_payment_in_db(payment_info.clone()) + .await?; + Ok(SendPaymentResponse { + payment_hash: payment_info.payment_hash.0.into(), + }) +} + +#[derive(Deserialize)] +pub struct PaymentsFilterForRPC { + pub payment_type: Option, + pub description: Option, + pub status: Option, + pub from_amount_msat: Option, + pub to_amount_msat: Option, + pub from_fee_paid_msat: Option, + pub to_fee_paid_msat: Option, + pub from_timestamp: Option, + pub to_timestamp: Option, +} + +impl From for PaymentsFilter { + fn from(filter: PaymentsFilterForRPC) -> Self { + PaymentsFilter { + payment_type: filter.payment_type.map(From::from), + description: filter.description, + status: filter.status, + from_amount_msat: filter.from_amount_msat, + to_amount_msat: filter.to_amount_msat, + from_fee_paid_msat: filter.from_fee_paid_msat, + to_fee_paid_msat: filter.to_fee_paid_msat, + from_timestamp: filter.from_timestamp, + to_timestamp: filter.to_timestamp, + } + } +} + +#[derive(Deserialize)] +pub struct ListPaymentsReq { + pub coin: String, + pub filter: Option, + #[serde(default = "ten")] + limit: usize, + #[serde(default)] + paging_options: PagingOptionsEnum, +} + +#[derive(Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum PaymentTypeForRPC { + #[serde(rename = "Outbound Payment")] + OutboundPayment { destination: PublicKeyForRPC }, + #[serde(rename = "Inbound Payment")] + InboundPayment, +} + +impl From for PaymentTypeForRPC { + fn from(payment_type: PaymentType) -> Self { + match payment_type { + PaymentType::OutboundPayment { destination } => PaymentTypeForRPC::OutboundPayment { + destination: PublicKeyForRPC(destination), + }, + PaymentType::InboundPayment => PaymentTypeForRPC::InboundPayment, + } + } +} + +impl From for PaymentType { + fn from(payment_type: PaymentTypeForRPC) -> Self { + match payment_type { + PaymentTypeForRPC::OutboundPayment { destination } => PaymentType::OutboundPayment { + destination: destination.into(), + }, + PaymentTypeForRPC::InboundPayment => PaymentType::InboundPayment, + } + } +} + +#[derive(Serialize)] +pub struct PaymentInfoForRPC { + payment_hash: H256Json, + payment_type: PaymentTypeForRPC, + description: String, + #[serde(skip_serializing_if = "Option::is_none")] + amount_in_msat: Option, + #[serde(skip_serializing_if = "Option::is_none")] + fee_paid_msat: Option, + status: HTLCStatus, + created_at: u64, + last_updated: u64, +} + +impl From for PaymentInfoForRPC { + fn from(info: PaymentInfo) -> Self { + PaymentInfoForRPC { + payment_hash: info.payment_hash.0.into(), + payment_type: info.payment_type.into(), + description: info.description, + amount_in_msat: info.amt_msat, + fee_paid_msat: info.fee_paid_msat, + status: info.status, + created_at: info.created_at, + last_updated: info.last_updated, + } + } +} + +#[derive(Serialize)] +pub struct ListPaymentsResponse { + payments: Vec, + limit: usize, + skipped: usize, + total: usize, + total_pages: usize, + paging_options: PagingOptionsEnum, +} + +pub async fn list_payments_by_filter(ctx: MmArc, req: ListPaymentsReq) -> ListPaymentsResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let ln_coin = match coin { + MmCoinEnum::LightningCoin(c) => c, + _ => return MmError::err(ListPaymentsError::UnsupportedCoin(coin.ticker().to_string())), + }; + let get_payments_res = ln_coin + .persister + .get_payments_by_filter( + req.filter.map(From::from), + req.paging_options.clone().map(|h| PaymentHash(h.0)), + req.limit, + ) + .await?; + + Ok(ListPaymentsResponse { + payments: get_payments_res.payments.into_iter().map(From::from).collect(), + limit: req.limit, + skipped: get_payments_res.skipped, + total: get_payments_res.total, + total_pages: calc_total_pages(get_payments_res.total, req.limit), + paging_options: req.paging_options, + }) +} + +#[derive(Deserialize)] +pub struct GetPaymentDetailsRequest { + pub coin: String, + pub payment_hash: H256Json, +} + +#[derive(Serialize)] +pub struct GetPaymentDetailsResponse { + payment_details: PaymentInfoForRPC, +} + +pub async fn get_payment_details( + ctx: MmArc, + req: GetPaymentDetailsRequest, +) -> GetPaymentDetailsResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let ln_coin = match coin { + MmCoinEnum::LightningCoin(c) => c, + _ => return MmError::err(GetPaymentDetailsError::UnsupportedCoin(coin.ticker().to_string())), + }; + + if let Some(payment_info) = ln_coin + .persister + .get_payment_from_db(PaymentHash(req.payment_hash.0)) + .await? + { + return Ok(GetPaymentDetailsResponse { + payment_details: payment_info.into(), + }); + } + + MmError::err(GetPaymentDetailsError::NoSuchPayment(req.payment_hash)) +} + +#[derive(Deserialize)] +pub struct CloseChannelReq { + pub coin: String, + pub channel_id: H256Json, + #[serde(default)] + pub force_close: bool, +} + +pub async fn close_channel(ctx: MmArc, req: CloseChannelReq) -> CloseChannelResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let ln_coin = match coin { + MmCoinEnum::LightningCoin(c) => c, + _ => return MmError::err(CloseChannelError::UnsupportedCoin(coin.ticker().to_string())), + }; + if req.force_close { + ln_coin + .channel_manager + .force_close_channel(&req.channel_id.0) + .map_to_mm(|e| CloseChannelError::CloseChannelError(format!("{:?}", e)))?; + } else { + ln_coin + .channel_manager + .close_channel(&req.channel_id.0) + .map_to_mm(|e| CloseChannelError::CloseChannelError(format!("{:?}", e)))?; + } + + Ok(format!("Initiated closing of channel: {:?}", req.channel_id)) +} + +/// Details about the balance(s) available for spending once the channel appears on chain. +#[derive(Serialize)] +pub enum ClaimableBalance { + /// The channel is not yet closed (or the commitment or closing transaction has not yet + /// appeared in a block). The given balance is claimable (less on-chain fees) if the channel is + /// force-closed now. + ClaimableOnChannelClose { + /// The amount available to claim, in satoshis, excluding the on-chain fees which will be + /// required to do so. + claimable_amount_satoshis: u64, + }, + /// The channel has been closed, and the given balance is ours but awaiting confirmations until + /// we consider it spendable. + ClaimableAwaitingConfirmations { + /// The amount available to claim, in satoshis, possibly excluding the on-chain fees which + /// were spent in broadcasting the transaction. + claimable_amount_satoshis: u64, + /// The height at which an [`Event::SpendableOutputs`] event will be generated for this + /// amount. + confirmation_height: u32, + }, + /// The channel has been closed, and the given balance should be ours but awaiting spending + /// transaction confirmation. If the spending transaction does not confirm in time, it is + /// possible our counterparty can take the funds by broadcasting an HTLC timeout on-chain. + /// + /// Once the spending transaction confirms, before it has reached enough confirmations to be + /// considered safe from chain reorganizations, the balance will instead be provided via + /// [`Balance::ClaimableAwaitingConfirmations`]. + ContentiousClaimable { + /// The amount available to claim, in satoshis, excluding the on-chain fees which will be + /// required to do so. + claimable_amount_satoshis: u64, + /// The height at which the counterparty may be able to claim the balance if we have not + /// done so. + timeout_height: u32, + }, + /// HTLCs which we sent to our counterparty which are claimable after a timeout (less on-chain + /// fees) if the counterparty does not know the preimage for the HTLCs. These are somewhat + /// likely to be claimed by our counterparty before we do. + MaybeClaimableHTLCAwaitingTimeout { + /// The amount available to claim, in satoshis, excluding the on-chain fees which will be + /// required to do so. + claimable_amount_satoshis: u64, + /// The height at which we will be able to claim the balance if our counterparty has not + /// done so. + claimable_height: u32, + }, +} + +impl From for ClaimableBalance { + fn from(balance: Balance) -> Self { + match balance { + Balance::ClaimableOnChannelClose { + claimable_amount_satoshis, + } => ClaimableBalance::ClaimableOnChannelClose { + claimable_amount_satoshis, + }, + Balance::ClaimableAwaitingConfirmations { + claimable_amount_satoshis, + confirmation_height, + } => ClaimableBalance::ClaimableAwaitingConfirmations { + claimable_amount_satoshis, + confirmation_height, + }, + Balance::ContentiousClaimable { + claimable_amount_satoshis, + timeout_height, + } => ClaimableBalance::ContentiousClaimable { + claimable_amount_satoshis, + timeout_height, + }, + Balance::MaybeClaimableHTLCAwaitingTimeout { + claimable_amount_satoshis, + claimable_height, + } => ClaimableBalance::MaybeClaimableHTLCAwaitingTimeout { + claimable_amount_satoshis, + claimable_height, + }, + } + } +} + +#[derive(Deserialize)] +pub struct ClaimableBalancesReq { + pub coin: String, + #[serde(default)] + pub include_open_channels_balances: bool, +} + +pub async fn get_claimable_balances( + ctx: MmArc, + req: ClaimableBalancesReq, +) -> ClaimableBalancesResult> { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let ln_coin = match coin { + MmCoinEnum::LightningCoin(c) => c, + _ => return MmError::err(ClaimableBalancesError::UnsupportedCoin(coin.ticker().to_string())), + }; + let ignored_channels = if req.include_open_channels_balances { + Vec::new() + } else { + ln_coin.channel_manager.list_channels() + }; + let claimable_balances = ln_coin + .chain_monitor + .get_claimable_balances(&ignored_channels.iter().collect::>()[..]) + .into_iter() + .map(From::from) + .collect(); + + Ok(claimable_balances) +} diff --git a/mm2src/coins/lightning/ln_conf.rs b/mm2src/coins/lightning/ln_conf.rs new file mode 100644 index 0000000000..f69a9bca10 --- /dev/null +++ b/mm2src/coins/lightning/ln_conf.rs @@ -0,0 +1,250 @@ +use crate::utxo::BlockchainNetwork; +use lightning::util::config::{ChannelConfig, ChannelHandshakeConfig, ChannelHandshakeLimits, UserConfig}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DefaultFeesAndConfirmations { + pub default_fee_per_kb: u64, + pub n_blocks: u32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PlatformCoinConfirmations { + pub background: DefaultFeesAndConfirmations, + pub normal: DefaultFeesAndConfirmations, + pub high_priority: DefaultFeesAndConfirmations, +} + +#[derive(Debug)] +pub struct LightningProtocolConf { + pub platform_coin_ticker: String, + pub network: BlockchainNetwork, + pub confirmations: PlatformCoinConfirmations, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct ChannelOptions { + /// Amount (in millionths of a satoshi) charged per satoshi for payments forwarded outbound + /// over the channel. + pub proportional_fee_in_millionths_sats: Option, + /// Amount (in milli-satoshi) charged for payments forwarded outbound over the channel, in + /// excess of proportional_fee_in_millionths_sats. + pub base_fee_msat: Option, + pub cltv_expiry_delta: Option, + /// Set to announce the channel publicly and notify all nodes that they can route via this + /// channel. + pub announced_channel: Option, + /// When set, we commit to an upfront shutdown_pubkey at channel open. + pub commit_upfront_shutdown_pubkey: Option, + /// Limit our total exposure to in-flight HTLCs which are burned to fees as they are too + /// small to claim on-chain. + pub max_dust_htlc_exposure_msat: Option, + /// The additional fee we're willing to pay to avoid waiting for the counterparty's + /// locktime to reclaim funds. + pub force_close_avoidance_max_fee_sats: Option, +} + +impl ChannelOptions { + pub fn update(&mut self, options: ChannelOptions) { + if let Some(fee) = options.proportional_fee_in_millionths_sats { + self.proportional_fee_in_millionths_sats = Some(fee); + } + + if let Some(fee) = options.base_fee_msat { + self.base_fee_msat = Some(fee); + } + + if let Some(expiry) = options.cltv_expiry_delta { + self.cltv_expiry_delta = Some(expiry); + } + + if let Some(announce) = options.announced_channel { + self.announced_channel = Some(announce); + } + + if let Some(commit) = options.commit_upfront_shutdown_pubkey { + self.commit_upfront_shutdown_pubkey = Some(commit); + } + + if let Some(dust) = options.max_dust_htlc_exposure_msat { + self.max_dust_htlc_exposure_msat = Some(dust); + } + + if let Some(fee) = options.force_close_avoidance_max_fee_sats { + self.force_close_avoidance_max_fee_sats = Some(fee); + } + } +} + +impl From for ChannelConfig { + fn from(options: ChannelOptions) -> Self { + let mut channel_config = ChannelConfig::default(); + + if let Some(fee) = options.proportional_fee_in_millionths_sats { + channel_config.forwarding_fee_proportional_millionths = fee; + } + + if let Some(fee) = options.base_fee_msat { + channel_config.forwarding_fee_base_msat = fee; + } + + if let Some(expiry) = options.cltv_expiry_delta { + channel_config.cltv_expiry_delta = expiry; + } + + if let Some(announce) = options.announced_channel { + channel_config.announced_channel = announce; + } + + if let Some(commit) = options.commit_upfront_shutdown_pubkey { + channel_config.commit_upfront_shutdown_pubkey = commit; + } + + if let Some(dust) = options.max_dust_htlc_exposure_msat { + channel_config.max_dust_htlc_exposure_msat = dust; + } + + if let Some(fee) = options.force_close_avoidance_max_fee_sats { + channel_config.force_close_avoidance_max_fee_satoshis = fee; + } + + channel_config + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct OurChannelsConfig { + /// Confirmations we will wait for before considering an inbound channel locked in. + pub inbound_channels_confirmations: Option, + /// The number of blocks we require our counterparty to wait to claim their money on chain + /// if they broadcast a revoked transaction. We have to be online at least once during this time to + /// punish our counterparty for broadcasting a revoked transaction. + /// We have to account also for the time to broadcast and confirm our transaction, + /// possibly with time in between to RBF (Replace-By-Fee) the spending transaction. + pub counterparty_locktime: Option, + /// The smallest value HTLC we will accept to process. The channel gets closed any time + /// our counterparty misbehaves by sending us an HTLC with a value smaller than this. + pub our_htlc_minimum_msat: Option, +} + +impl From for ChannelHandshakeConfig { + fn from(config: OurChannelsConfig) -> Self { + let mut channel_handshake_config = ChannelHandshakeConfig::default(); + + if let Some(confs) = config.inbound_channels_confirmations { + channel_handshake_config.minimum_depth = confs; + } + + if let Some(delay) = config.counterparty_locktime { + channel_handshake_config.our_to_self_delay = delay; + } + + if let Some(min) = config.our_htlc_minimum_msat { + channel_handshake_config.our_htlc_minimum_msat = min; + } + + channel_handshake_config + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct CounterpartyLimits { + /// Minimum allowed satoshis when an inbound channel is funded. + pub min_funding_sats: Option, + /// The remote node sets a limit on the minimum size of HTLCs we can send to them. This allows + /// us to limit the maximum minimum-size they can require. + pub max_htlc_minimum_msat: Option, + /// The remote node sets a limit on the maximum value of pending HTLCs to them at any given + /// time to limit their funds exposure to HTLCs. This allows us to set a minimum such value. + pub min_max_htlc_value_in_flight_msat: Option, + /// The remote node will require us to keep a certain amount in direct payment to ourselves at all + /// time, ensuring that we are able to be punished if we broadcast an old state. This allows us + /// to limit the amount which we will have to keep to ourselves (and cannot use for HTLCs). + pub max_channel_reserve_sats: Option, + /// The remote node sets a limit on the maximum number of pending HTLCs to them at any given + /// time. This allows us to set a minimum such value. + pub min_max_accepted_htlcs: Option, + /// This config allows us to set a limit on the maximum confirmations to wait before the outbound channel is usable. + pub outbound_channels_confirmations: Option, + /// Set to force an incoming channel to match our announced channel preference in ChannelOptions announced_channel. + pub force_announced_channel_preference: Option, + /// Set to the amount of time we're willing to wait to claim money back to us. + pub our_locktime_limit: Option, +} + +impl From for ChannelHandshakeLimits { + fn from(limits: CounterpartyLimits) -> Self { + let mut channel_handshake_limits = ChannelHandshakeLimits::default(); + + if let Some(sats) = limits.min_funding_sats { + channel_handshake_limits.min_funding_satoshis = sats; + } + + if let Some(msat) = limits.max_htlc_minimum_msat { + channel_handshake_limits.max_htlc_minimum_msat = msat; + } + + if let Some(msat) = limits.min_max_htlc_value_in_flight_msat { + channel_handshake_limits.min_max_htlc_value_in_flight_msat = msat; + } + + if let Some(sats) = limits.max_channel_reserve_sats { + channel_handshake_limits.max_channel_reserve_satoshis = sats; + } + + if let Some(min) = limits.min_max_accepted_htlcs { + channel_handshake_limits.min_max_accepted_htlcs = min; + } + + if let Some(confs) = limits.outbound_channels_confirmations { + channel_handshake_limits.max_minimum_depth = confs; + } + + if let Some(pref) = limits.force_announced_channel_preference { + channel_handshake_limits.force_announced_channel_preference = pref; + } + + if let Some(blocks) = limits.our_locktime_limit { + channel_handshake_limits.their_to_self_delay = blocks; + } + + channel_handshake_limits + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct LightningCoinConf { + #[serde(rename = "coin")] + pub ticker: String, + pub decimals: u8, + pub accept_inbound_channels: Option, + pub accept_forwards_to_priv_channels: Option, + pub channel_options: Option, + pub our_channels_config: Option, + pub counterparty_channel_config_limits: Option, + pub sign_message_prefix: Option, +} + +impl From for UserConfig { + fn from(conf: LightningCoinConf) -> Self { + let mut user_config = UserConfig::default(); + if let Some(config) = conf.our_channels_config { + user_config.own_channel_config = config.into(); + } + if let Some(limits) = conf.counterparty_channel_config_limits { + user_config.peer_channel_config_limits = limits.into(); + } + if let Some(options) = conf.channel_options { + user_config.channel_options = options.into(); + } + if let Some(accept_forwards) = conf.accept_forwards_to_priv_channels { + user_config.accept_forwards_to_priv_channels = accept_forwards; + } + if let Some(accept_inbound) = conf.accept_inbound_channels { + user_config.accept_inbound_channels = accept_inbound; + } + // This allows OpenChannelRequest event to be fired + user_config.manually_accept_inbound_channels = true; + + user_config + } +} diff --git a/mm2src/coins/lightning/ln_errors.rs b/mm2src/coins/lightning/ln_errors.rs new file mode 100644 index 0000000000..72b581f647 --- /dev/null +++ b/mm2src/coins/lightning/ln_errors.rs @@ -0,0 +1,561 @@ +use crate::utxo::rpc_clients::UtxoRpcError; +use crate::utxo::GenerateTxError; +use crate::{BalanceError, CoinFindError, NumConversError, PrivKeyNotAllowed, UnexpectedDerivationMethod}; +use bitcoin::consensus::encode; +use common::jsonrpc_client::JsonRpcError; +use common::HttpStatusCode; +use db_common::sqlite::rusqlite::Error as SqlError; +use derive_more::Display; +use http::StatusCode; +use lightning_invoice::SignOrCreationError; +use mm2_err_handle::prelude::*; +use rpc::v1::types::H256 as H256Json; +use utxo_signer::with_key_pair::UtxoSignWithKeyPairError; + +pub type EnableLightningResult = Result>; +pub type ConnectToNodeResult = Result>; +pub type OpenChannelResult = Result>; +pub type ListChannelsResult = Result>; +pub type GetChannelDetailsResult = Result>; +pub type GenerateInvoiceResult = Result>; +pub type SendPaymentResult = Result>; +pub type ListPaymentsResult = Result>; +pub type GetPaymentDetailsResult = Result>; +pub type CloseChannelResult = Result>; +pub type ClaimableBalancesResult = Result>; +pub type SaveChannelClosingResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum EnableLightningError { + #[display(fmt = "Invalid request: {}", _0)] + InvalidRequest(String), + #[display(fmt = "Invalid configuration: {}", _0)] + InvalidConfiguration(String), + #[display(fmt = "{} is only supported in {} mode", _0, _1)] + UnsupportedMode(String, String), + #[display(fmt = "I/O error {}", _0)] + IOError(String), + #[display(fmt = "Invalid address: {}", _0)] + InvalidAddress(String), + #[display(fmt = "Invalid path: {}", _0)] + InvalidPath(String), + #[display(fmt = "System time error {}", _0)] + SystemTimeError(String), + #[display(fmt = "Hash error {}", _0)] + HashError(String), + #[display(fmt = "RPC error {}", _0)] + RpcError(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), + ConnectToNodeError(String), +} + +impl HttpStatusCode for EnableLightningError { + fn status_code(&self) -> StatusCode { + match self { + EnableLightningError::InvalidRequest(_) | EnableLightningError::RpcError(_) => StatusCode::BAD_REQUEST, + EnableLightningError::UnsupportedMode(_, _) => StatusCode::NOT_IMPLEMENTED, + EnableLightningError::InvalidAddress(_) + | EnableLightningError::InvalidPath(_) + | EnableLightningError::SystemTimeError(_) + | EnableLightningError::IOError(_) + | EnableLightningError::HashError(_) + | EnableLightningError::ConnectToNodeError(_) + | EnableLightningError::InvalidConfiguration(_) + | EnableLightningError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for EnableLightningError { + fn from(err: std::io::Error) -> EnableLightningError { EnableLightningError::IOError(err.to_string()) } +} + +impl From for EnableLightningError { + fn from(err: SqlError) -> EnableLightningError { EnableLightningError::DbError(err.to_string()) } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ConnectToNodeError { + #[display(fmt = "Parse error: {}", _0)] + ParseError(String), + #[display(fmt = "Error connecting to node: {}", _0)] + ConnectionError(String), + #[display(fmt = "I/O error {}", _0)] + IOError(String), + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), +} + +impl HttpStatusCode for ConnectToNodeError { + fn status_code(&self) -> StatusCode { + match self { + ConnectToNodeError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + ConnectToNodeError::ParseError(_) + | ConnectToNodeError::IOError(_) + | ConnectToNodeError::ConnectionError(_) => StatusCode::INTERNAL_SERVER_ERROR, + ConnectToNodeError::NoSuchCoin(_) => StatusCode::PRECONDITION_REQUIRED, + } + } +} + +impl From for EnableLightningError { + fn from(err: ConnectToNodeError) -> EnableLightningError { + EnableLightningError::ConnectToNodeError(err.to_string()) + } +} + +impl From for ConnectToNodeError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => ConnectToNodeError::NoSuchCoin(coin), + } + } +} + +impl From for ConnectToNodeError { + fn from(err: std::io::Error) -> ConnectToNodeError { ConnectToNodeError::IOError(err.to_string()) } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum OpenChannelError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "Balance Error {}", _0)] + BalanceError(String), + #[display(fmt = "Invalid path: {}", _0)] + InvalidPath(String), + #[display(fmt = "Failure to open channel with node {}: {}", _0, _1)] + FailureToOpenChannel(String, String), + #[display(fmt = "RPC error {}", _0)] + RpcError(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), + #[display(fmt = "I/O error {}", _0)] + IOError(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), + ConnectToNodeError(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "Generate Tx Error {}", _0)] + GenerateTxErr(String), + #[display(fmt = "Error converting transaction: {}", _0)] + ConvertTxErr(String), + PrivKeyNotAllowed(String), +} + +impl HttpStatusCode for OpenChannelError { + fn status_code(&self) -> StatusCode { + match self { + OpenChannelError::UnsupportedCoin(_) + | OpenChannelError::RpcError(_) + | OpenChannelError::PrivKeyNotAllowed(_) => StatusCode::BAD_REQUEST, + OpenChannelError::FailureToOpenChannel(_, _) + | OpenChannelError::ConnectToNodeError(_) + | OpenChannelError::InternalError(_) + | OpenChannelError::GenerateTxErr(_) + | OpenChannelError::IOError(_) + | OpenChannelError::DbError(_) + | OpenChannelError::InvalidPath(_) + | OpenChannelError::ConvertTxErr(_) => StatusCode::INTERNAL_SERVER_ERROR, + OpenChannelError::NoSuchCoin(_) | OpenChannelError::BalanceError(_) => StatusCode::PRECONDITION_REQUIRED, + } + } +} + +impl From for OpenChannelError { + fn from(err: ConnectToNodeError) -> OpenChannelError { OpenChannelError::ConnectToNodeError(err.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => OpenChannelError::NoSuchCoin(coin), + } + } +} + +impl From for OpenChannelError { + fn from(e: BalanceError) -> Self { OpenChannelError::BalanceError(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: NumConversError) -> Self { OpenChannelError::InternalError(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: GenerateTxError) -> Self { OpenChannelError::GenerateTxErr(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: UtxoRpcError) -> Self { OpenChannelError::RpcError(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: UnexpectedDerivationMethod) -> Self { OpenChannelError::InternalError(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: UtxoSignWithKeyPairError) -> Self { OpenChannelError::InternalError(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(e: PrivKeyNotAllowed) -> Self { OpenChannelError::PrivKeyNotAllowed(e.to_string()) } +} + +impl From for OpenChannelError { + fn from(err: std::io::Error) -> OpenChannelError { OpenChannelError::IOError(err.to_string()) } +} + +impl From for OpenChannelError { + fn from(err: SqlError) -> OpenChannelError { OpenChannelError::DbError(err.to_string()) } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ListChannelsError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for ListChannelsError { + fn status_code(&self) -> StatusCode { + match self { + ListChannelsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + ListChannelsError::NoSuchCoin(_) => StatusCode::PRECONDITION_REQUIRED, + ListChannelsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for ListChannelsError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => ListChannelsError::NoSuchCoin(coin), + } + } +} + +impl From for ListChannelsError { + fn from(err: SqlError) -> ListChannelsError { ListChannelsError::DbError(err.to_string()) } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetChannelDetailsError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "Channel with rpc id: {} is not found", _0)] + NoSuchChannel(u64), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for GetChannelDetailsError { + fn status_code(&self) -> StatusCode { + match self { + GetChannelDetailsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + GetChannelDetailsError::NoSuchCoin(_) => StatusCode::PRECONDITION_REQUIRED, + GetChannelDetailsError::NoSuchChannel(_) => StatusCode::NOT_FOUND, + GetChannelDetailsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for GetChannelDetailsError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => GetChannelDetailsError::NoSuchCoin(coin), + } + } +} + +impl From for GetChannelDetailsError { + fn from(err: SqlError) -> GetChannelDetailsError { GetChannelDetailsError::DbError(err.to_string()) } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GenerateInvoiceError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "Invoice signing or creation error: {}", _0)] + SignOrCreationError(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for GenerateInvoiceError { + fn status_code(&self) -> StatusCode { + match self { + GenerateInvoiceError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + GenerateInvoiceError::NoSuchCoin(_) => StatusCode::PRECONDITION_REQUIRED, + GenerateInvoiceError::SignOrCreationError(_) | GenerateInvoiceError::DbError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + }, + } + } +} + +impl From for GenerateInvoiceError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => GenerateInvoiceError::NoSuchCoin(coin), + } + } +} + +impl From for GenerateInvoiceError { + fn from(e: SignOrCreationError) -> Self { GenerateInvoiceError::SignOrCreationError(e.to_string()) } +} + +impl From for GenerateInvoiceError { + fn from(err: SqlError) -> GenerateInvoiceError { GenerateInvoiceError::DbError(err.to_string()) } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum SendPaymentError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "Couldn't parse destination pubkey: {}", _0)] + NoRouteFound(String), + #[display(fmt = "Payment error: {}", _0)] + PaymentError(String), + #[display(fmt = "Final cltv expiry delta {} is below the required minimum of {}", _0, _1)] + CLTVExpiryError(u32, u32), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for SendPaymentError { + fn status_code(&self) -> StatusCode { + match self { + SendPaymentError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + SendPaymentError::NoSuchCoin(_) => StatusCode::PRECONDITION_REQUIRED, + SendPaymentError::PaymentError(_) + | SendPaymentError::NoRouteFound(_) + | SendPaymentError::CLTVExpiryError(_, _) + | SendPaymentError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for SendPaymentError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => SendPaymentError::NoSuchCoin(coin), + } + } +} + +impl From for SendPaymentError { + fn from(err: SqlError) -> SendPaymentError { SendPaymentError::DbError(err.to_string()) } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ListPaymentsError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for ListPaymentsError { + fn status_code(&self) -> StatusCode { + match self { + ListPaymentsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + ListPaymentsError::NoSuchCoin(_) => StatusCode::PRECONDITION_REQUIRED, + ListPaymentsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for ListPaymentsError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => ListPaymentsError::NoSuchCoin(coin), + } + } +} + +impl From for ListPaymentsError { + fn from(err: SqlError) -> ListPaymentsError { ListPaymentsError::DbError(err.to_string()) } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetPaymentDetailsError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "Payment with hash: {:?} is not found", _0)] + NoSuchPayment(H256Json), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for GetPaymentDetailsError { + fn status_code(&self) -> StatusCode { + match self { + GetPaymentDetailsError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + GetPaymentDetailsError::NoSuchCoin(_) => StatusCode::PRECONDITION_REQUIRED, + GetPaymentDetailsError::NoSuchPayment(_) => StatusCode::NOT_FOUND, + GetPaymentDetailsError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for GetPaymentDetailsError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => GetPaymentDetailsError::NoSuchCoin(coin), + } + } +} + +impl From for GetPaymentDetailsError { + fn from(err: SqlError) -> GetPaymentDetailsError { GetPaymentDetailsError::DbError(err.to_string()) } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum CloseChannelError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "Closing channel error: {}", _0)] + CloseChannelError(String), +} + +impl HttpStatusCode for CloseChannelError { + fn status_code(&self) -> StatusCode { + match self { + CloseChannelError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + CloseChannelError::NoSuchCoin(_) => StatusCode::PRECONDITION_REQUIRED, + CloseChannelError::CloseChannelError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for CloseChannelError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => CloseChannelError::NoSuchCoin(coin), + } + } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ClaimableBalancesError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), +} + +impl HttpStatusCode for ClaimableBalancesError { + fn status_code(&self) -> StatusCode { + match self { + ClaimableBalancesError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + ClaimableBalancesError::NoSuchCoin(_) => StatusCode::PRECONDITION_REQUIRED, + } + } +} + +impl From for ClaimableBalancesError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => ClaimableBalancesError::NoSuchCoin(coin), + } + } +} + +#[derive(Display)] +pub enum SaveChannelClosingError { + #[display(fmt = "DB error: {}", _0)] + DbError(String), + #[display(fmt = "Channel with rpc id {} not found in DB", _0)] + ChannelNotFound(u64), + #[display(fmt = "funding_generated_in_block is Null in DB")] + BlockHeightNull, + #[display(fmt = "Funding transaction hash is Null in DB")] + FundingTxNull, + #[display(fmt = "Error parsing funding transaction hash: {}", _0)] + FundingTxParseError(String), + #[display(fmt = "Error while waiting for the funding transaction to be spent: {}", _0)] + WaitForFundingTxSpendError(String), +} + +impl From for SaveChannelClosingError { + fn from(err: SqlError) -> SaveChannelClosingError { SaveChannelClosingError::DbError(err.to_string()) } +} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum GetTxError { + Rpc(UtxoRpcError), + TxDeserialization(encode::Error), +} + +impl From for GetTxError { + fn from(err: UtxoRpcError) -> GetTxError { GetTxError::Rpc(err) } +} + +impl From for GetTxError { + fn from(err: encode::Error) -> GetTxError { GetTxError::TxDeserialization(err) } +} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum GetHeaderError { + Rpc(JsonRpcError), + HeaderDeserialization(encode::Error), +} + +impl From for GetHeaderError { + fn from(err: JsonRpcError) -> GetHeaderError { GetHeaderError::Rpc(err) } +} + +impl From for GetHeaderError { + fn from(err: encode::Error) -> GetHeaderError { GetHeaderError::HeaderDeserialization(err) } +} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum FindWatchedOutputSpendError { + HashNotHeight, + DeserializationErr(encode::Error), + RpcError(String), + GetHeaderError(GetHeaderError), +} + +impl From for FindWatchedOutputSpendError { + fn from(err: JsonRpcError) -> Self { FindWatchedOutputSpendError::RpcError(err.to_string()) } +} + +impl From for FindWatchedOutputSpendError { + fn from(err: encode::Error) -> Self { FindWatchedOutputSpendError::DeserializationErr(err) } +} diff --git a/mm2src/coins/lightning/ln_events.rs b/mm2src/coins/lightning/ln_events.rs new file mode 100644 index 0000000000..3f898e4fa3 --- /dev/null +++ b/mm2src/coins/lightning/ln_events.rs @@ -0,0 +1,481 @@ +use super::*; +use crate::lightning::ln_errors::{SaveChannelClosingError, SaveChannelClosingResult}; +use bitcoin::blockdata::script::Script; +use bitcoin::blockdata::transaction::Transaction; +use common::executor::{spawn, Timer}; +use common::log::{error, info}; +use common::now_ms; +use core::time::Duration; +use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; +use lightning::chain::keysinterface::SpendableOutputDescriptor; +use lightning::util::events::{Event, EventHandler, PaymentPurpose}; +use rand::Rng; +use script::{Builder, SignatureVersion}; +use secp256k1::Secp256k1; +use std::convert::TryFrom; +use std::sync::Arc; +use utxo_signer::with_key_pair::sign_tx; + +const TRY_LOOP_INTERVAL: f64 = 60.; + +pub struct LightningEventHandler { + platform: Arc, + channel_manager: Arc, + keys_manager: Arc, + persister: Arc, +} + +impl EventHandler for LightningEventHandler { + fn handle_event(&self, event: &Event) { + match event { + Event::FundingGenerationReady { + temporary_channel_id, + channel_value_satoshis, + output_script, + user_channel_id, + } => self.handle_funding_generation_ready( + *temporary_channel_id, + *channel_value_satoshis, + output_script, + *user_channel_id, + ), + + Event::PaymentReceived { + payment_hash, + amt, + purpose, + } => self.handle_payment_received(*payment_hash, *amt, purpose), + + Event::PaymentSent { + payment_preimage, + payment_hash, + fee_paid_msat, + .. + } => self.handle_payment_sent(*payment_preimage, *payment_hash, *fee_paid_msat), + + Event::PaymentFailed { payment_hash, .. } => self.handle_payment_failed(*payment_hash), + + Event::PendingHTLCsForwardable { time_forwardable } => self.handle_pending_htlcs_forwards(*time_forwardable), + + Event::SpendableOutputs { outputs } => self.handle_spendable_outputs(outputs), + + // Todo: an RPC for total amount earned + Event::PaymentForwarded { fee_earned_msat, claim_from_onchain_tx } => info!( + "Received a fee of {} milli-satoshis for a successfully forwarded payment through our {} lightning node. Was the forwarded HTLC claimed by our counterparty via an on-chain transaction?: {}", + fee_earned_msat.unwrap_or_default(), + self.platform.coin.ticker(), + claim_from_onchain_tx, + ), + + Event::ChannelClosed { + channel_id, + user_channel_id, + reason, + } => self.handle_channel_closed(*channel_id, *user_channel_id, reason.to_string()), + + // Todo: Add spent UTXOs to RecentlySpentOutPoints if it's not discarded + Event::DiscardFunding { channel_id, transaction } => info!( + "Discarding funding tx: {} for channel {}", + transaction.txid().to_string(), + hex::encode(channel_id), + ), + + // Handling updating channel penalties after successfully routing a payment along a path is done by the InvoicePayer. + Event::PaymentPathSuccessful { + payment_id, + payment_hash, + path, + } => info!( + "Payment path: {:?}, successful for payment hash: {}, payment id: {}", + path.iter().map(|hop| hop.pubkey.to_string()).collect::>(), + payment_hash.map(|h| hex::encode(h.0)).unwrap_or_default(), + hex::encode(payment_id.0) + ), + + // Handling updating channel penalties after a payment fails to route through a channel is done by the InvoicePayer. + // Also abandoning or retrying a payment is handled by the InvoicePayer. + Event::PaymentPathFailed { + payment_hash, + rejected_by_dest, + all_paths_failed, + path, + .. + } => info!( + "Payment path: {:?}, failed for payment hash: {}, Was rejected by destination?: {}, All paths failed?: {}", + path.iter().map(|hop| hop.pubkey.to_string()).collect::>(), + hex::encode(payment_hash.0), + rejected_by_dest, + all_paths_failed, + ), + + Event::OpenChannelRequest { + temporary_channel_id, + counterparty_node_id, + funding_satoshis, + push_msat, + channel_type: _, + } => { + info!( + "Handling OpenChannelRequest from node: {} with funding value: {} and starting balance: {}", + counterparty_node_id, + funding_satoshis, + push_msat, + ); + if self.channel_manager.accept_inbound_channel(temporary_channel_id, 0).is_ok() { + // Todo: once the rust-lightning PR for user_channel_id in accept_inbound_channel is released + // use user_channel_id to get the funding tx here once the funding tx is available. + } + }, + } + } +} + +// Generates the raw funding transaction with one output equal to the channel value. +fn sign_funding_transaction( + user_channel_id: u64, + output_script: &Script, + platform: Arc, +) -> OpenChannelResult { + let coin = &platform.coin; + let mut unsigned = { + let unsigned_funding_txs = platform.unsigned_funding_txs.lock(); + unsigned_funding_txs + .get(&user_channel_id) + .ok_or_else(|| { + OpenChannelError::InternalError(format!( + "Unsigned funding tx not found for internal channel id: {}", + user_channel_id + )) + })? + .clone() + }; + unsigned.outputs[0].script_pubkey = output_script.to_bytes().into(); + + let my_address = coin.as_ref().derivation_method.iguana_or_err()?; + let key_pair = coin.as_ref().priv_key_policy.key_pair_or_err()?; + + let prev_script = Builder::build_p2pkh(&my_address.hash); + let signed = sign_tx( + unsigned, + key_pair, + prev_script, + SignatureVersion::WitnessV0, + coin.as_ref().conf.fork_id, + )?; + + Transaction::try_from(signed).map_to_mm(|e| OpenChannelError::ConvertTxErr(e.to_string())) +} + +async fn save_channel_closing_details( + persister: Arc, + platform: Arc, + user_channel_id: u64, + reason: String, +) -> SaveChannelClosingResult<()> { + persister + .update_channel_to_closed(user_channel_id, reason, now_ms() / 1000) + .await?; + + let channel_details = persister + .get_channel_from_db(user_channel_id) + .await? + .ok_or_else(|| MmError::new(SaveChannelClosingError::ChannelNotFound(user_channel_id)))?; + + let closing_tx_hash = platform.get_channel_closing_tx(channel_details).await?; + + persister.add_closing_tx_to_db(user_channel_id, closing_tx_hash).await?; + + Ok(()) +} + +impl LightningEventHandler { + pub fn new( + platform: Arc, + channel_manager: Arc, + keys_manager: Arc, + persister: Arc, + ) -> Self { + LightningEventHandler { + platform, + channel_manager, + keys_manager, + persister, + } + } + + fn handle_funding_generation_ready( + &self, + temporary_channel_id: [u8; 32], + channel_value_satoshis: u64, + output_script: &Script, + user_channel_id: u64, + ) { + info!( + "Handling FundingGenerationReady event for internal channel id: {}", + user_channel_id + ); + let funding_tx = match sign_funding_transaction(user_channel_id, output_script, self.platform.clone()) { + Ok(tx) => tx, + Err(e) => { + error!( + "Error generating funding transaction for internal channel id {}: {}", + user_channel_id, + e.to_string() + ); + return; + }, + }; + let funding_txid = funding_tx.txid(); + // Give the funding transaction back to LDK for opening the channel. + if let Err(e) = self + .channel_manager + .funding_transaction_generated(&temporary_channel_id, funding_tx) + { + error!("{:?}", e); + return; + } + let platform = self.platform.clone(); + let persister = self.persister.clone(); + spawn(async move { + let best_block_height = platform.best_block_height(); + persister + .add_funding_tx_to_db( + user_channel_id, + funding_txid.to_string(), + channel_value_satoshis, + best_block_height, + ) + .await + .error_log(); + }); + } + + fn handle_payment_received(&self, payment_hash: PaymentHash, amt: u64, purpose: &PaymentPurpose) { + info!( + "Handling PaymentReceived event for payment_hash: {}", + hex::encode(payment_hash.0) + ); + let (payment_preimage, payment_secret) = match purpose { + PaymentPurpose::InvoicePayment { + payment_preimage, + payment_secret, + } => match payment_preimage { + Some(preimage) => (*preimage, Some(*payment_secret)), + None => return, + }, + PaymentPurpose::SpontaneousPayment(preimage) => (*preimage, None), + }; + let status = match self.channel_manager.claim_funds(payment_preimage) { + true => { + info!( + "Received an amount of {} millisatoshis for payment hash {}", + amt, + hex::encode(payment_hash.0) + ); + HTLCStatus::Succeeded + }, + false => HTLCStatus::Failed, + }; + let persister = self.persister.clone(); + match purpose { + PaymentPurpose::InvoicePayment { .. } => spawn(async move { + if let Ok(Some(mut payment_info)) = persister + .get_payment_from_db(payment_hash) + .await + .error_log_passthrough() + { + payment_info.preimage = Some(payment_preimage); + payment_info.status = HTLCStatus::Succeeded; + payment_info.amt_msat = Some(amt); + payment_info.last_updated = now_ms() / 1000; + if let Err(e) = persister.add_or_update_payment_in_db(payment_info).await { + error!("Unable to update payment information in DB: {}", e); + } + } + }), + PaymentPurpose::SpontaneousPayment(_) => { + let payment_info = PaymentInfo { + payment_hash, + payment_type: PaymentType::InboundPayment, + description: "".into(), + preimage: Some(payment_preimage), + secret: payment_secret, + amt_msat: Some(amt), + fee_paid_msat: None, + status, + created_at: now_ms() / 1000, + last_updated: now_ms() / 1000, + }; + spawn(async move { + if let Err(e) = persister.add_or_update_payment_in_db(payment_info).await { + error!("Unable to update payment information in DB: {}", e); + } + }); + }, + } + } + + fn handle_payment_sent( + &self, + payment_preimage: PaymentPreimage, + payment_hash: PaymentHash, + fee_paid_msat: Option, + ) { + info!( + "Handling PaymentSent event for payment_hash: {}", + hex::encode(payment_hash.0) + ); + let persister = self.persister.clone(); + spawn(async move { + if let Ok(Some(mut payment_info)) = persister + .get_payment_from_db(payment_hash) + .await + .error_log_passthrough() + { + payment_info.preimage = Some(payment_preimage); + payment_info.status = HTLCStatus::Succeeded; + payment_info.fee_paid_msat = fee_paid_msat; + payment_info.last_updated = now_ms() / 1000; + let amt_msat = payment_info.amt_msat; + if let Err(e) = persister.add_or_update_payment_in_db(payment_info).await { + error!("Unable to update payment information in DB: {}", e); + } + info!( + "Successfully sent payment of {} millisatoshis with payment hash {}", + amt_msat.unwrap_or_default(), + hex::encode(payment_hash.0) + ); + } + }); + } + + fn handle_channel_closed(&self, channel_id: [u8; 32], user_channel_id: u64, reason: String) { + info!( + "Channel: {} closed for the following reason: {}", + hex::encode(channel_id), + reason + ); + let persister = self.persister.clone(); + let platform = self.platform.clone(); + // Todo: Handle inbound channels closure case after updating to latest version of rust-lightning + // as it has a new OpenChannelRequest event where we can give an inbound channel a user_channel_id + // other than 0 in sql + if user_channel_id != 0 { + spawn(async move { + if let Err(e) = save_channel_closing_details(persister, platform, user_channel_id, reason).await { + error!( + "Unable to update channel {} closing details in DB: {}", + user_channel_id, e + ); + } + }); + } + } + + fn handle_payment_failed(&self, payment_hash: PaymentHash) { + info!( + "Handling PaymentFailed event for payment_hash: {}", + hex::encode(payment_hash.0) + ); + let persister = self.persister.clone(); + spawn(async move { + if let Ok(Some(mut payment_info)) = persister + .get_payment_from_db(payment_hash) + .await + .error_log_passthrough() + { + payment_info.status = HTLCStatus::Failed; + payment_info.last_updated = now_ms() / 1000; + if let Err(e) = persister.add_or_update_payment_in_db(payment_info).await { + error!("Unable to update payment information in DB: {}", e); + } + } + }); + } + + fn handle_pending_htlcs_forwards(&self, time_forwardable: Duration) { + info!("Handling PendingHTLCsForwardable event!"); + let min_wait_time = time_forwardable.as_millis() as u32; + let channel_manager = self.channel_manager.clone(); + spawn(async move { + let millis_to_sleep = rand::thread_rng().gen_range(min_wait_time, min_wait_time * 5); + Timer::sleep_ms(millis_to_sleep).await; + channel_manager.process_pending_htlc_forwards(); + }); + } + + fn handle_spendable_outputs(&self, outputs: &[SpendableOutputDescriptor]) { + info!("Handling SpendableOutputs event!"); + let platform_coin = &self.platform.coin; + // Todo: add support for Hardware wallets for funding transactions and spending spendable outputs (channel closing transactions) + let my_address = match platform_coin.as_ref().derivation_method.iguana_or_err() { + Ok(addr) => addr, + Err(e) => { + error!("{}", e); + return; + }, + }; + let change_destination_script = Builder::build_witness_script(&my_address.hash).to_bytes().take().into(); + let feerate_sat_per_1000_weight = self.platform.get_est_sat_per_1000_weight(ConfirmationTarget::Normal); + let output_descriptors = &outputs.iter().collect::>(); + let spending_tx = match self.keys_manager.spend_spendable_outputs( + output_descriptors, + Vec::new(), + change_destination_script, + feerate_sat_per_1000_weight, + &Secp256k1::new(), + ) { + Ok(tx) => tx, + Err(_) => { + error!("Error spending spendable outputs"); + return; + }, + }; + + let claiming_tx_inputs_value = outputs.iter().fold(0, |sum, output| match output { + SpendableOutputDescriptor::StaticOutput { output, .. } => sum + output.value, + SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) => sum + descriptor.output.value, + SpendableOutputDescriptor::StaticPaymentOutput(descriptor) => sum + descriptor.output.value, + }); + let claiming_tx_outputs_value = spending_tx.output.iter().fold(0, |sum, txout| sum + txout.value); + if claiming_tx_inputs_value < claiming_tx_outputs_value { + error!( + "Claiming transaction input value {} can't be less than outputs value {}!", + claiming_tx_inputs_value, claiming_tx_outputs_value + ); + return; + } + let claiming_tx_fee = claiming_tx_inputs_value - claiming_tx_outputs_value; + let claiming_tx_fee_per_channel = (claiming_tx_fee as f64) / (outputs.len() as f64); + + for output in outputs { + let (closing_txid, claimed_balance) = match output { + SpendableOutputDescriptor::StaticOutput { outpoint, output } => { + (outpoint.txid.to_string(), output.value) + }, + SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) => { + (descriptor.outpoint.txid.to_string(), descriptor.output.value) + }, + SpendableOutputDescriptor::StaticPaymentOutput(descriptor) => { + (descriptor.outpoint.txid.to_string(), descriptor.output.value) + }, + }; + let claiming_txid = spending_tx.txid().to_string(); + let persister = self.persister.clone(); + spawn(async move { + ok_or_retry_after_sleep!( + persister + .add_claiming_tx_to_db( + closing_txid.clone(), + claiming_txid.clone(), + (claimed_balance as f64) - claiming_tx_fee_per_channel, + ) + .await, + TRY_LOOP_INTERVAL + ); + }); + + self.platform.broadcast_transaction(&spending_tx); + } + } +} diff --git a/mm2src/coins/lightning/ln_p2p.rs b/mm2src/coins/lightning/ln_p2p.rs new file mode 100644 index 0000000000..00bd5cdd5b --- /dev/null +++ b/mm2src/coins/lightning/ln_p2p.rs @@ -0,0 +1,200 @@ +use super::*; +use common::executor::{spawn, Timer}; +use common::log::LogState; +use derive_more::Display; +use lightning::chain::Access; +use lightning::ln::msgs::NetAddress; +use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler, SimpleArcPeerManager}; +use lightning::routing::network_graph::{NetGraphMsgHandler, NetworkGraph}; +use lightning_net_tokio::SocketDescriptor; +use lightning_persister::storage::NodesAddressesMapShared; +use mm2_net::ip_addr::fetch_external_ip; +use rand::RngCore; +use secp256k1::SecretKey; +use std::net::{IpAddr, Ipv4Addr}; +use tokio::net::TcpListener; + +const TRY_RECONNECTING_TO_NODE_INTERVAL: f64 = 60.; +const BROADCAST_NODE_ANNOUNCEMENT_INTERVAL: u64 = 600; + +type NetworkGossip = NetGraphMsgHandler, Arc, Arc>; + +pub type PeerManager = + SimpleArcPeerManager; + +#[derive(Display)] +pub enum ConnectToNodeRes { + #[display(fmt = "Already connected to node: {}@{}", pubkey, node_addr)] + AlreadyConnected { pubkey: PublicKey, node_addr: SocketAddr }, + #[display(fmt = "Connected successfully to node : {}@{}", pubkey, node_addr)] + ConnectedSuccessfully { pubkey: PublicKey, node_addr: SocketAddr }, +} + +pub async fn connect_to_node( + pubkey: PublicKey, + node_addr: SocketAddr, + peer_manager: Arc, +) -> ConnectToNodeResult { + if peer_manager.get_peer_node_ids().contains(&pubkey) { + return Ok(ConnectToNodeRes::AlreadyConnected { pubkey, node_addr }); + } + + let mut connection_closed_future = + match lightning_net_tokio::connect_outbound(Arc::clone(&peer_manager), pubkey, node_addr).await { + Some(fut) => Box::pin(fut), + None => { + return MmError::err(ConnectToNodeError::ConnectionError(format!( + "Failed to connect to node: {}", + pubkey + ))) + }, + }; + + loop { + // Make sure the connection is still established. + match futures::poll!(&mut connection_closed_future) { + std::task::Poll::Ready(_) => { + return MmError::err(ConnectToNodeError::ConnectionError(format!( + "Node {} disconnected before finishing the handshake", + pubkey + ))); + }, + std::task::Poll::Pending => {}, + } + + if peer_manager.get_peer_node_ids().contains(&pubkey) { + break; + } + + // Wait for the handshake to complete + Timer::sleep_ms(10).await; + } + + Ok(ConnectToNodeRes::ConnectedSuccessfully { pubkey, node_addr }) +} + +pub async fn connect_to_nodes_loop(open_channels_nodes: NodesAddressesMapShared, peer_manager: Arc) { + loop { + let open_channels_nodes = open_channels_nodes.lock().clone(); + for (pubkey, node_addr) in open_channels_nodes { + let peer_manager = peer_manager.clone(); + match connect_to_node(pubkey, node_addr, peer_manager.clone()).await { + Ok(res) => { + if let ConnectToNodeRes::ConnectedSuccessfully { .. } = res { + log::info!("{}", res.to_string()); + } + }, + Err(e) => log::error!("{}", e.to_string()), + } + } + + Timer::sleep(TRY_RECONNECTING_TO_NODE_INTERVAL).await; + } +} + +// TODO: add TOR address option +fn netaddress_from_ipaddr(addr: IpAddr, port: u16) -> Vec { + if addr == Ipv4Addr::new(0, 0, 0, 0) || addr == Ipv4Addr::new(127, 0, 0, 1) { + return Vec::new(); + } + let mut addresses = Vec::new(); + let address = match addr { + IpAddr::V4(addr) => NetAddress::IPv4 { + addr: u32::from(addr).to_be_bytes(), + port, + }, + IpAddr::V6(addr) => NetAddress::IPv6 { + addr: u128::from(addr).to_be_bytes(), + port, + }, + }; + addresses.push(address); + addresses +} + +pub async fn ln_node_announcement_loop( + channel_manager: Arc, + node_name: [u8; 32], + node_color: [u8; 3], + port: u16, +) { + loop { + // Right now if the node is behind NAT the external ip is fetched on every loop + // If the node does not announce a public IP, it will not be displayed on the network graph, + // and other nodes will not be able to open a channel with it. But it can open channels with other nodes. + let addresses = match fetch_external_ip().await { + Ok(ip) => { + log::debug!("Fetch real IP successfully: {}:{}", ip, port); + netaddress_from_ipaddr(ip, port) + }, + Err(e) => { + log::error!("Error while fetching external ip for node announcement: {}", e); + Timer::sleep(BROADCAST_NODE_ANNOUNCEMENT_INTERVAL as f64).await; + continue; + }, + }; + channel_manager.broadcast_node_announcement(node_color, node_name, addresses); + + Timer::sleep(BROADCAST_NODE_ANNOUNCEMENT_INTERVAL as f64).await; + } +} + +async fn ln_p2p_loop(peer_manager: Arc, listener: TcpListener) { + loop { + let peer_mgr = peer_manager.clone(); + let tcp_stream = match listener.accept().await { + Ok((stream, addr)) => { + log::debug!("New incoming lightning connection from node address: {}", addr); + stream + }, + Err(e) => { + log::error!("Error on accepting lightning connection: {}", e); + continue; + }, + }; + if let Ok(stream) = tcp_stream.into_std() { + spawn(async move { + lightning_net_tokio::setup_inbound(peer_mgr.clone(), stream).await; + }); + }; + } +} + +pub async fn init_peer_manager( + ctx: MmArc, + listening_port: u16, + channel_manager: Arc, + network_gossip: Arc, + node_secret: SecretKey, + logger: Arc, +) -> EnableLightningResult> { + // The set (possibly empty) of socket addresses on which this node accepts incoming connections. + // If the user wishes to preserve privacy, addresses should likely contain only Tor Onion addresses. + let listening_addr = myipaddr(ctx).await.map_to_mm(EnableLightningError::InvalidAddress)?; + // If the listening port is used start_lightning should return an error early + let listener = TcpListener::bind(format!("{}:{}", listening_addr, listening_port)) + .await + .map_to_mm(|e| EnableLightningError::IOError(e.to_string()))?; + + // ephemeral_random_data is used to derive per-connection ephemeral keys + let mut ephemeral_bytes = [0; 32]; + rand::thread_rng().fill_bytes(&mut ephemeral_bytes); + let lightning_msg_handler = MessageHandler { + chan_handler: channel_manager, + route_handler: network_gossip, + }; + + // IgnoringMessageHandler is used as custom message types (experimental and application-specific messages) is not needed + let peer_manager: Arc = Arc::new(PeerManager::new( + lightning_msg_handler, + node_secret, + &ephemeral_bytes, + logger, + Arc::new(IgnoringMessageHandler {}), + )); + + // Initialize p2p networking + spawn(ln_p2p_loop(peer_manager.clone(), listener)); + + Ok(peer_manager) +} diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs new file mode 100644 index 0000000000..dfc3f24554 --- /dev/null +++ b/mm2src/coins/lightning/ln_platform.rs @@ -0,0 +1,575 @@ +use super::*; +use crate::lightning::ln_errors::{FindWatchedOutputSpendError, GetHeaderError, GetTxError, SaveChannelClosingError, + SaveChannelClosingResult}; +use crate::utxo::rpc_clients::{electrum_script_hash, BestBlock as RpcBestBlock, BlockHashOrHeight, + ElectrumBlockHeader, ElectrumClient, ElectrumNonce, EstimateFeeMethod, + UtxoRpcClientEnum, UtxoRpcError}; +use crate::utxo::utxo_common; +use crate::utxo::utxo_standard::UtxoStandardCoin; +use crate::{MarketCoinOps, MmCoin}; +use bitcoin::blockdata::block::BlockHeader; +use bitcoin::blockdata::script::Script; +use bitcoin::blockdata::transaction::Transaction; +use bitcoin::consensus::encode::{deserialize, serialize_hex}; +use bitcoin::hash_types::{BlockHash, TxMerkleNode, Txid}; +use bitcoin_hashes::{sha256d, Hash}; +use common::executor::{spawn, Timer}; +use common::jsonrpc_client::JsonRpcErrorType; +use common::log::{debug, error, info}; +use futures::compat::Future01CompatExt; +use keys::hash::H256; +use lightning::chain::{chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}, + Confirm, Filter, WatchedOutput}; +use rpc::v1::types::H256 as H256Json; +use std::cmp; +use std::convert::TryFrom; +use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; + +const CHECK_FOR_NEW_BEST_BLOCK_INTERVAL: f64 = 60.; +const MIN_ALLOWED_FEE_PER_1000_WEIGHT: u32 = 253; +const TRY_LOOP_INTERVAL: f64 = 60.; + +#[inline] +pub fn h256_json_from_txid(txid: Txid) -> H256Json { H256Json::from(txid.as_hash().into_inner()).reversed() } + +struct TxWithBlockInfo { + tx: Transaction, + block_header: BlockHeader, + block_height: u64, +} + +async fn get_block_header(electrum_client: &ElectrumClient, height: u64) -> Result { + Ok(deserialize( + &electrum_client.blockchain_block_header(height).compat().await?, + )?) +} + +async fn find_watched_output_spend_with_header( + electrum_client: &ElectrumClient, + output: &WatchedOutput, +) -> Result, FindWatchedOutputSpendError> { + // from_block parameter is not used in find_output_spend for electrum clients + let utxo_client: UtxoRpcClientEnum = electrum_client.clone().into(); + let tx_hash = H256::from(output.outpoint.txid.as_hash().into_inner()); + let output_spend = match utxo_client + .find_output_spend( + tx_hash, + output.script_pubkey.as_ref(), + output.outpoint.index.into(), + BlockHashOrHeight::Hash(Default::default()), + ) + .compat() + .await + .map_err(FindWatchedOutputSpendError::RpcError)? + { + Some(output) => output, + None => return Ok(None), + }; + + let height = match output_spend.spent_in_block { + BlockHashOrHeight::Height(h) => h, + _ => return Err(FindWatchedOutputSpendError::HashNotHeight), + }; + let block_header = get_block_header(electrum_client, height as u64) + .await + .map_err(FindWatchedOutputSpendError::GetHeaderError)?; + let spending_tx = Transaction::try_from(output_spend.spending_tx)?; + + Ok(Some(TxWithBlockInfo { + tx: spending_tx, + block_header, + block_height: height as u64, + })) +} + +pub async fn get_best_header(best_header_listener: &ElectrumClient) -> EnableLightningResult { + best_header_listener + .blockchain_headers_subscribe() + .compat() + .await + .map_to_mm(|e| EnableLightningError::RpcError(e.to_string())) +} + +pub async fn update_best_block( + chain_monitor: &ChainMonitor, + channel_manager: &ChannelManager, + best_header: ElectrumBlockHeader, +) { + { + let (new_best_header, new_best_height) = match best_header { + ElectrumBlockHeader::V12(h) => { + let nonce = match h.nonce { + ElectrumNonce::Number(n) => n as u32, + ElectrumNonce::Hash(_) => { + return; + }, + }; + let prev_blockhash = match sha256d::Hash::from_slice(&h.prev_block_hash.0) { + Ok(h) => h, + Err(e) => { + error!("Error while parsing previous block hash for lightning node: {}", e); + return; + }, + }; + let merkle_root = match sha256d::Hash::from_slice(&h.merkle_root.0) { + Ok(h) => h, + Err(e) => { + error!("Error while parsing merkle root for lightning node: {}", e); + return; + }, + }; + ( + BlockHeader { + version: h.version as i32, + prev_blockhash: BlockHash::from_hash(prev_blockhash), + merkle_root: TxMerkleNode::from_hash(merkle_root), + time: h.timestamp as u32, + bits: h.bits as u32, + nonce, + }, + h.block_height as u32, + ) + }, + ElectrumBlockHeader::V14(h) => { + let block_header = match deserialize(&h.hex.into_vec()) { + Ok(header) => header, + Err(e) => { + error!("Block header deserialization error: {}", e.to_string()); + return; + }, + }; + (block_header, h.height as u32) + }, + }; + channel_manager.best_block_updated(&new_best_header, new_best_height); + chain_monitor.best_block_updated(&new_best_header, new_best_height); + } +} + +pub async fn ln_best_block_update_loop( + platform: Arc, + persister: Arc, + chain_monitor: Arc, + channel_manager: Arc, + best_header_listener: ElectrumClient, + best_block: RpcBestBlock, +) { + let mut current_best_block = best_block; + loop { + let best_header = ok_or_continue_after_sleep!(get_best_header(&best_header_listener).await, TRY_LOOP_INTERVAL); + if current_best_block != best_header.clone().into() { + platform.update_best_block_height(best_header.block_height()); + platform + .process_txs_unconfirmations(&chain_monitor, &channel_manager) + .await; + platform + .process_txs_confirmations(&best_header_listener, &persister, &chain_monitor, &channel_manager) + .await; + current_best_block = best_header.clone().into(); + update_best_block(&chain_monitor, &channel_manager, best_header).await; + } + Timer::sleep(CHECK_FOR_NEW_BEST_BLOCK_INTERVAL).await; + } +} + +struct ConfirmedTransactionInfo { + txid: Txid, + header: BlockHeader, + index: usize, + transaction: Transaction, + height: u32, +} + +impl ConfirmedTransactionInfo { + fn new(txid: Txid, header: BlockHeader, index: usize, transaction: Transaction, height: u32) -> Self { + ConfirmedTransactionInfo { + txid, + header, + index, + transaction, + height, + } + } +} + +pub struct Platform { + pub coin: UtxoStandardCoin, + /// Main/testnet/signet/regtest Needed for lightning node to know which network to connect to + pub network: BlockchainNetwork, + /// The best block height. + pub best_block_height: AtomicU64, + /// Default fees to and confirmation targets to be used for FeeEstimator. Default fees are used when the call for + /// estimate_fee_sat fails. + pub default_fees_and_confirmations: PlatformCoinConfirmations, + /// This cache stores the transactions that the LN node has interest in. + pub registered_txs: PaMutex>>, + /// This cache stores the outputs that the LN node has interest in. + pub registered_outputs: PaMutex>, + /// This cache stores transactions to be broadcasted once the other node accepts the channel + pub unsigned_funding_txs: PaMutex>, +} + +impl Platform { + #[inline] + pub fn new( + coin: UtxoStandardCoin, + network: BlockchainNetwork, + default_fees_and_confirmations: PlatformCoinConfirmations, + ) -> Self { + Platform { + coin, + network, + best_block_height: AtomicU64::new(0), + default_fees_and_confirmations, + registered_txs: PaMutex::new(HashMap::new()), + registered_outputs: PaMutex::new(Vec::new()), + unsigned_funding_txs: PaMutex::new(HashMap::new()), + } + } + + #[inline] + fn rpc_client(&self) -> &UtxoRpcClientEnum { &self.coin.as_ref().rpc_client } + + #[inline] + pub fn update_best_block_height(&self, new_height: u64) { + self.best_block_height.store(new_height, AtomicOrdering::Relaxed); + } + + #[inline] + pub fn best_block_height(&self) -> u64 { self.best_block_height.load(AtomicOrdering::Relaxed) } + + pub fn add_tx(&self, txid: Txid, script_pubkey: Script) { + let mut registered_txs = self.registered_txs.lock(); + registered_txs + .entry(txid) + .or_insert_with(HashSet::new) + .insert(script_pubkey); + } + + pub fn add_output(&self, output: WatchedOutput) { + let mut registered_outputs = self.registered_outputs.lock(); + registered_outputs.push(output); + } + + async fn get_tx_if_onchain(&self, txid: Txid) -> Result, GetTxError> { + let txid = h256_json_from_txid(txid); + match self + .rpc_client() + .get_transaction_bytes(&txid) + .compat() + .await + .map_err(|e| e.into_inner()) + { + Ok(bytes) => Ok(Some(deserialize(&bytes.into_vec())?)), + Err(err) => { + if let UtxoRpcError::ResponseParseError(ref json_err) = err { + if let JsonRpcErrorType::Response(_, json) = &json_err.error { + if let Some(message) = json["message"].as_str() { + if message.contains(utxo_common::NO_TX_ERROR_CODE) { + return Ok(None); + } + } + } + } + Err(err.into()) + }, + } + } + + async fn process_tx_for_unconfirmation(&self, txid: Txid, monitor: &T) + where + T: Confirm, + { + match self.get_tx_if_onchain(txid).await { + Ok(Some(_)) => {}, + Ok(None) => { + info!( + "Transaction {} is not found on chain. The transaction will be re-broadcasted.", + txid, + ); + monitor.transaction_unconfirmed(&txid); + }, + Err(e) => error!( + "Error while trying to check if the transaction {} is discarded or not :{:?}", + txid, e + ), + } + } + + pub async fn process_txs_unconfirmations(&self, chain_monitor: &ChainMonitor, channel_manager: &ChannelManager) { + // Retrieve channel manager transaction IDs to check the chain for un-confirmations + let channel_manager_relevant_txids = channel_manager.get_relevant_txids(); + for txid in channel_manager_relevant_txids { + self.process_tx_for_unconfirmation(txid, channel_manager).await; + } + + // Retrieve chain monitor transaction IDs to check the chain for un-confirmations + let chain_monitor_relevant_txids = chain_monitor.get_relevant_txids(); + for txid in chain_monitor_relevant_txids { + self.process_tx_for_unconfirmation(txid, chain_monitor).await; + } + } + + async fn get_confirmed_registered_txs(&self, client: &ElectrumClient) -> Vec { + let registered_txs = self.registered_txs.lock().clone(); + let mut confirmed_registered_txs = Vec::new(); + for (txid, scripts) in registered_txs { + if let Some(transaction) = + ok_or_continue_after_sleep!(self.get_tx_if_onchain(txid).await, TRY_LOOP_INTERVAL) + { + for (_, vout) in transaction.output.iter().enumerate() { + if scripts.contains(&vout.script_pubkey) { + let script_hash = hex::encode(electrum_script_hash(vout.script_pubkey.as_ref())); + let history = ok_or_retry_after_sleep!( + client.scripthash_get_history(&script_hash).compat().await, + TRY_LOOP_INTERVAL + ); + for item in history { + let rpc_txid = h256_json_from_txid(txid); + if item.tx_hash == rpc_txid && item.height > 0 { + let height = item.height as u64; + let header = + ok_or_retry_after_sleep!(get_block_header(client, height).await, TRY_LOOP_INTERVAL); + let index = ok_or_retry_after_sleep!( + client + .blockchain_transaction_get_merkle(rpc_txid, height) + .compat() + .await, + TRY_LOOP_INTERVAL + ) + .pos; + let confirmed_transaction_info = ConfirmedTransactionInfo::new( + txid, + header, + index, + transaction.clone(), + height as u32, + ); + confirmed_registered_txs.push(confirmed_transaction_info); + self.registered_txs.lock().remove(&txid); + } + } + } + } + } + } + confirmed_registered_txs + } + + async fn append_spent_registered_output_txs( + &self, + transactions_to_confirm: &mut Vec, + client: &ElectrumClient, + ) { + let mut outputs_to_remove = Vec::new(); + let registered_outputs = self.registered_outputs.lock().clone(); + for output in registered_outputs { + if let Some(tx_info) = ok_or_continue_after_sleep!( + find_watched_output_spend_with_header(client, &output).await, + TRY_LOOP_INTERVAL + ) { + if !transactions_to_confirm + .iter() + .any(|info| info.txid == tx_info.tx.txid()) + { + let rpc_txid = h256_json_from_txid(tx_info.tx.txid()); + let index = ok_or_retry_after_sleep!( + client + .blockchain_transaction_get_merkle(rpc_txid, tx_info.block_height) + .compat() + .await, + TRY_LOOP_INTERVAL + ) + .pos; + let confirmed_transaction_info = ConfirmedTransactionInfo::new( + tx_info.tx.txid(), + tx_info.block_header, + index, + tx_info.tx, + tx_info.block_height as u32, + ); + transactions_to_confirm.push(confirmed_transaction_info); + } + outputs_to_remove.push(output); + } + } + self.registered_outputs + .lock() + .retain(|output| !outputs_to_remove.contains(output)); + } + + pub async fn process_txs_confirmations( + &self, + client: &ElectrumClient, + persister: &LightningPersister, + chain_monitor: &ChainMonitor, + channel_manager: &ChannelManager, + ) { + let mut transactions_to_confirm = self.get_confirmed_registered_txs(client).await; + self.append_spent_registered_output_txs(&mut transactions_to_confirm, client) + .await; + + transactions_to_confirm.sort_by(|a, b| (a.height, a.index).cmp(&(b.height, b.index))); + + for confirmed_transaction_info in transactions_to_confirm { + let best_block_height = self.best_block_height(); + if let Err(e) = persister + .update_funding_tx_block_height( + confirmed_transaction_info.transaction.txid().to_string(), + best_block_height, + ) + .await + { + error!("Unable to update the funding tx block height in DB: {}", e); + } + channel_manager.transactions_confirmed( + &confirmed_transaction_info.header, + &[( + confirmed_transaction_info.index, + &confirmed_transaction_info.transaction, + )], + confirmed_transaction_info.height, + ); + chain_monitor.transactions_confirmed( + &confirmed_transaction_info.header, + &[( + confirmed_transaction_info.index, + &confirmed_transaction_info.transaction, + )], + confirmed_transaction_info.height, + ); + } + } + + pub async fn get_channel_closing_tx(&self, channel_details: SqlChannelDetails) -> SaveChannelClosingResult { + let from_block = channel_details + .funding_generated_in_block + .ok_or_else(|| MmError::new(SaveChannelClosingError::BlockHeightNull))?; + + let tx_id = channel_details + .funding_tx + .ok_or_else(|| MmError::new(SaveChannelClosingError::FundingTxNull))?; + + let tx_hash = + H256Json::from_str(&tx_id).map_to_mm(|e| SaveChannelClosingError::FundingTxParseError(e.to_string()))?; + + let funding_tx_bytes = ok_or_retry_after_sleep!( + self.rpc_client().get_transaction_bytes(&tx_hash).compat().await, + TRY_LOOP_INTERVAL + ); + + let closing_tx = self + .coin + .wait_for_tx_spend( + &funding_tx_bytes.into_vec(), + (now_ms() / 1000) + 3600, + from_block, + &None, + ) + .compat() + .await + .map_to_mm(|e| SaveChannelClosingError::WaitForFundingTxSpendError(e.get_plain_text_format()))?; + + let closing_tx_hash = format!("{:02x}", closing_tx.tx_hash()); + + Ok(closing_tx_hash) + } +} + +impl FeeEstimator for Platform { + // Gets estimated satoshis of fee required per 1000 Weight-Units. + fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 { + let platform_coin = &self.coin; + + let default_fee = match confirmation_target { + ConfirmationTarget::Background => self.default_fees_and_confirmations.background.default_fee_per_kb, + ConfirmationTarget::Normal => self.default_fees_and_confirmations.normal.default_fee_per_kb, + ConfirmationTarget::HighPriority => self.default_fees_and_confirmations.high_priority.default_fee_per_kb, + }; + + let conf = &platform_coin.as_ref().conf; + let n_blocks = match confirmation_target { + ConfirmationTarget::Background => self.default_fees_and_confirmations.background.n_blocks, + ConfirmationTarget::Normal => self.default_fees_and_confirmations.normal.n_blocks, + ConfirmationTarget::HighPriority => self.default_fees_and_confirmations.high_priority.n_blocks, + }; + let fee_per_kb = tokio::task::block_in_place(move || { + self.rpc_client() + .estimate_fee_sat( + platform_coin.decimals(), + // Todo: when implementing Native client detect_fee_method should be used for Native and + // EstimateFeeMethod::Standard for Electrum + &EstimateFeeMethod::Standard, + &conf.estimate_fee_mode, + n_blocks, + ) + .wait() + .unwrap_or(default_fee) + }); + // Must be no smaller than 253 (ie 1 satoshi-per-byte rounded up to ensure later round-downs don’t put us below 1 satoshi-per-byte). + // https://docs.rs/lightning/0.0.101/lightning/chain/chaininterface/trait.FeeEstimator.html#tymethod.get_est_sat_per_1000_weight + cmp::max((fee_per_kb as f64 / 4.0).ceil() as u32, MIN_ALLOWED_FEE_PER_1000_WEIGHT) + } +} + +impl BroadcasterInterface for Platform { + fn broadcast_transaction(&self, tx: &Transaction) { + let txid = tx.txid(); + let tx_hex = serialize_hex(tx); + debug!("Trying to broadcast transaction: {}", tx_hex); + let fut = self.coin.send_raw_tx(&tx_hex); + spawn(async move { + match fut.compat().await { + Ok(id) => info!("Transaction broadcasted successfully: {:?} ", id), + Err(e) => error!("Broadcast transaction {} failed: {}", txid, e), + } + }); + } +} + +impl Filter for Platform { + // Watches for this transaction on-chain + #[inline] + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { self.add_tx(*txid, script_pubkey.clone()); } + + // Watches for any transactions that spend this output on-chain + fn register_output(&self, output: WatchedOutput) -> Option<(usize, Transaction)> { + self.add_output(output.clone()); + + let block_hash = match output.block_hash { + Some(h) => H256Json::from(h.as_hash().into_inner()), + None => return None, + }; + + // Although this works for both native and electrum clients as the block hash is available, + // the filter interface which includes register_output and register_tx should be used for electrum clients only, + // this is the reason for initializing the filter as an option in the start_lightning function as it will be None + // when implementing lightning for native clients + let output_spend_info = tokio::task::block_in_place(move || { + let delay = TRY_LOOP_INTERVAL as u64; + ok_or_retry_after_sleep_sync!( + self.rpc_client() + .find_output_spend( + H256::from(output.outpoint.txid.as_hash().into_inner()), + output.script_pubkey.as_ref(), + output.outpoint.index.into(), + BlockHashOrHeight::Hash(block_hash), + ) + .wait(), + delay + ) + }); + + if let Some(info) = output_spend_info { + match Transaction::try_from(info.spending_tx) { + Ok(tx) => Some((info.input_index, tx)), + Err(e) => { + error!("Can't convert transaction error: {}", e.to_string()); + return None; + }, + }; + } + + None + } +} diff --git a/mm2src/coins/lightning/ln_serialization.rs b/mm2src/coins/lightning/ln_serialization.rs new file mode 100644 index 0000000000..82f0a700c7 --- /dev/null +++ b/mm2src/coins/lightning/ln_serialization.rs @@ -0,0 +1,202 @@ +use lightning_invoice::Invoice; +use secp256k1::PublicKey; +use serde::{de, Serialize, Serializer}; +use std::fmt; +use std::net::{SocketAddr, ToSocketAddrs}; +use std::str::FromStr; + +#[derive(Clone, Debug, PartialEq)] +pub struct InvoiceForRPC(Invoice); + +impl From for InvoiceForRPC { + fn from(i: Invoice) -> Self { InvoiceForRPC(i) } +} + +impl From for Invoice { + fn from(i: InvoiceForRPC) -> Self { i.0 } +} + +impl Serialize for InvoiceForRPC { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.0.to_string()) + } +} + +impl<'de> de::Deserialize<'de> for InvoiceForRPC { + fn deserialize>(deserializer: D) -> Result { + struct InvoiceForRPCVisitor; + + impl<'de> de::Visitor<'de> for InvoiceForRPCVisitor { + type Value = InvoiceForRPC; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a lightning invoice") + } + + fn visit_str(self, v: &str) -> Result { + let invoice = Invoice::from_str(v).map_err(|e| { + let err = format!("Could not parse lightning invoice from str {}, err {}", v, e); + de::Error::custom(err) + })?; + Ok(InvoiceForRPC(invoice)) + } + } + + deserializer.deserialize_str(InvoiceForRPCVisitor) + } +} + +// TODO: support connection to onion addresses +#[derive(Debug, PartialEq)] +pub struct NodeAddress { + pub pubkey: PublicKey, + pub addr: SocketAddr, +} + +impl Serialize for NodeAddress { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&format!("{}@{}", self.pubkey, self.addr)) + } +} + +impl<'de> de::Deserialize<'de> for NodeAddress { + fn deserialize>(deserializer: D) -> Result { + struct NodeAddressVisitor; + + impl<'de> de::Visitor<'de> for NodeAddressVisitor { + type Value = NodeAddress; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "pubkey@host:port") } + + fn visit_str(self, v: &str) -> Result { + let mut pubkey_and_addr = v.split('@'); + let pubkey_str = pubkey_and_addr.next().ok_or_else(|| { + let err = format!("Could not parse node address from str {}", v); + de::Error::custom(err) + })?; + let addr_str = pubkey_and_addr.next().ok_or_else(|| { + let err = format!("Could not parse node address from str {}", v); + de::Error::custom(err) + })?; + let pubkey = PublicKey::from_str(pubkey_str).map_err(|e| { + let err = format!("Could not parse node pubkey from str {}, err {}", pubkey_str, e); + de::Error::custom(err) + })?; + let addr = addr_str + .to_socket_addrs() + .map(|mut r| r.next()) + .map_err(|e| { + let err = format!("Could not parse socket address from str {}, err {}", addr_str, e); + de::Error::custom(err) + })? + .ok_or_else(|| { + let err = format!("Could not parse socket address from str {}", addr_str); + de::Error::custom(err) + })?; + Ok(NodeAddress { pubkey, addr }) + } + } + + deserializer.deserialize_str(NodeAddressVisitor) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PublicKeyForRPC(pub PublicKey); + +impl From for PublicKey { + fn from(p: PublicKeyForRPC) -> Self { p.0 } +} + +impl Serialize for PublicKeyForRPC { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.0.to_string()) + } +} + +impl<'de> de::Deserialize<'de> for PublicKeyForRPC { + fn deserialize>(deserializer: D) -> Result { + struct PublicKeyForRPCVisitor; + + impl<'de> de::Visitor<'de> for PublicKeyForRPCVisitor { + type Value = PublicKeyForRPC; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "a public key") } + + fn visit_str(self, v: &str) -> Result { + let pubkey = PublicKey::from_str(v).map_err(|e| { + let err = format!("Could not parse public key from str {}, err {}", v, e); + de::Error::custom(err) + })?; + Ok(PublicKeyForRPC(pubkey)) + } + } + + deserializer.deserialize_str(PublicKeyForRPCVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json as json; + + #[test] + fn test_invoice_for_rpc_serialize() { + let invoice_for_rpc = InvoiceForRPC(str::parse::("lntb20u1p3zqmvrpp52hej7trefx6y633aujj6nltjs8cf7lzyp78tfn5y5wpa3udk5tvqdp8xys9xcmpd3sjqsmgd9czq3njv9c8qatrvd5kumcxqrrsscqp79qy9qsqsp5ccy2qgmptg8dthxsjvw2c43uyvqkg6cqey3jpks4xf0tv7xfrqrq3xfnuffau2h2k8defphv2xsktzn2qj5n2l8d9l9zx64fg6jcmdg9kmpevneyyhfnzrpspqdrky8u7l4c6qdnquh8lnevswwrtcd9ypcq89ga09").unwrap()); + let expected = r#""lntb20u1p3zqmvrpp52hej7trefx6y633aujj6nltjs8cf7lzyp78tfn5y5wpa3udk5tvqdp8xys9xcmpd3sjqsmgd9czq3njv9c8qatrvd5kumcxqrrsscqp79qy9qsqsp5ccy2qgmptg8dthxsjvw2c43uyvqkg6cqey3jpks4xf0tv7xfrqrq3xfnuffau2h2k8defphv2xsktzn2qj5n2l8d9l9zx64fg6jcmdg9kmpevneyyhfnzrpspqdrky8u7l4c6qdnquh8lnevswwrtcd9ypcq89ga09""#; + let actual = json::to_string(&invoice_for_rpc).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn test_invoice_for_rpc_deserialize() { + let invoice_for_rpc = r#""lntb20u1p3zqmvrpp52hej7trefx6y633aujj6nltjs8cf7lzyp78tfn5y5wpa3udk5tvqdp8xys9xcmpd3sjqsmgd9czq3njv9c8qatrvd5kumcxqrrsscqp79qy9qsqsp5ccy2qgmptg8dthxsjvw2c43uyvqkg6cqey3jpks4xf0tv7xfrqrq3xfnuffau2h2k8defphv2xsktzn2qj5n2l8d9l9zx64fg6jcmdg9kmpevneyyhfnzrpspqdrky8u7l4c6qdnquh8lnevswwrtcd9ypcq89ga09""#; + let expected = InvoiceForRPC(str::parse::("lntb20u1p3zqmvrpp52hej7trefx6y633aujj6nltjs8cf7lzyp78tfn5y5wpa3udk5tvqdp8xys9xcmpd3sjqsmgd9czq3njv9c8qatrvd5kumcxqrrsscqp79qy9qsqsp5ccy2qgmptg8dthxsjvw2c43uyvqkg6cqey3jpks4xf0tv7xfrqrq3xfnuffau2h2k8defphv2xsktzn2qj5n2l8d9l9zx64fg6jcmdg9kmpevneyyhfnzrpspqdrky8u7l4c6qdnquh8lnevswwrtcd9ypcq89ga09").unwrap()); + let actual = json::from_str(invoice_for_rpc).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn test_node_address_serialize() { + let node_address = NodeAddress { + pubkey: PublicKey::from_str("038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9").unwrap(), + addr: SocketAddr::new("203.132.94.196".parse().unwrap(), 9735), + }; + let expected = r#""038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9@203.132.94.196:9735""#; + let actual = json::to_string(&node_address).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn test_node_address_deserialize() { + let node_address = + r#""038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9@203.132.94.196:9735""#; + let expected = NodeAddress { + pubkey: PublicKey::from_str("038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9").unwrap(), + addr: SocketAddr::new("203.132.94.196".parse().unwrap(), 9735), + }; + let actual: NodeAddress = json::from_str(node_address).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn test_public_key_for_rpc_serialize() { + let public_key_for_rpc = PublicKeyForRPC( + PublicKey::from_str("038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9").unwrap(), + ); + let expected = r#""038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9""#; + let actual = json::to_string(&public_key_for_rpc).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn test_public_key_for_rpc_deserialize() { + let public_key_for_rpc = r#""038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9""#; + let expected = PublicKeyForRPC( + PublicKey::from_str("038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9").unwrap(), + ); + let actual = json::from_str(public_key_for_rpc).unwrap(); + assert_eq!(expected, actual); + } +} diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs new file mode 100644 index 0000000000..05e4fd4f63 --- /dev/null +++ b/mm2src/coins/lightning/ln_utils.rs @@ -0,0 +1,270 @@ +use super::*; +use crate::lightning::ln_platform::{get_best_header, ln_best_block_update_loop, update_best_block}; +use crate::utxo::rpc_clients::BestBlock as RpcBestBlock; +use bitcoin::hash_types::BlockHash; +use bitcoin_hashes::{sha256d, Hash}; +use common::executor::{spawn, Timer}; +use common::log; +use common::log::LogState; +use lightning::chain::keysinterface::{InMemorySigner, KeysManager}; +use lightning::chain::{chainmonitor, BestBlock, Watch}; +use lightning::ln::channelmanager; +use lightning::ln::channelmanager::{ChainParameters, ChannelManagerReadArgs, SimpleArcChannelManager}; +use lightning::routing::network_graph::NetworkGraph; +use lightning::util::config::UserConfig; +use lightning::util::ser::ReadableArgs; +use lightning_persister::storage::{DbStorage, FileSystemStorage, NodesAddressesMap, Scorer}; +use lightning_persister::LightningPersister; +use mm2_core::mm_ctx::MmArc; +use std::fs::File; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::time::SystemTime; + +const NETWORK_GRAPH_PERSIST_INTERVAL: u64 = 600; +const SCORER_PERSIST_INTERVAL: u64 = 600; + +pub type ChainMonitor = chainmonitor::ChainMonitor< + InMemorySigner, + Arc, + Arc, + Arc, + Arc, + Arc, +>; + +pub type ChannelManager = SimpleArcChannelManager; + +#[inline] +fn ln_data_dir(ctx: &MmArc, ticker: &str) -> PathBuf { ctx.dbdir().join("LIGHTNING").join(ticker) } + +#[inline] +fn ln_data_backup_dir(ctx: &MmArc, path: Option, ticker: &str) -> Option { + path.map(|p| { + PathBuf::from(&p) + .join(&hex::encode(&**ctx.rmd160())) + .join("LIGHTNING") + .join(ticker) + }) +} + +pub async fn init_persister( + ctx: &MmArc, + platform: Arc, + ticker: String, + backup_path: Option, +) -> EnableLightningResult> { + let ln_data_dir = ln_data_dir(ctx, &ticker); + let ln_data_backup_dir = ln_data_backup_dir(ctx, backup_path, &ticker); + let persister = Arc::new(LightningPersister::new( + ticker.replace('-', "_"), + ln_data_dir, + ln_data_backup_dir, + ctx.sqlite_connection + .ok_or(MmError::new(EnableLightningError::DbError( + "sqlite_connection is not initialized".into(), + )))? + .clone(), + )); + let is_initialized = persister.is_fs_initialized().await?; + if !is_initialized { + persister.init_fs().await?; + } + let is_db_initialized = persister.is_db_initialized().await?; + if !is_db_initialized { + persister.init_db().await?; + } + + let closed_channels_without_closing_tx = persister.get_closed_channels_with_no_closing_tx().await?; + for channel_details in closed_channels_without_closing_tx { + let platform = platform.clone(); + let persister = persister.clone(); + let user_channel_id = channel_details.rpc_id; + spawn(async move { + if let Ok(closing_tx_hash) = platform + .get_channel_closing_tx(channel_details) + .await + .error_log_passthrough() + { + if let Err(e) = persister.add_closing_tx_to_db(user_channel_id, closing_tx_hash).await { + log::error!( + "Unable to update channel {} closing details in DB: {}", + user_channel_id, + e + ); + } + } + }); + } + + Ok(persister) +} + +pub fn init_keys_manager(ctx: &MmArc) -> EnableLightningResult> { + // The current time is used to derive random numbers from the seed where required, to ensure all random generation is unique across restarts. + let seed: [u8; 32] = ctx.secp256k1_key_pair().private().secret.into(); + let cur = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_to_mm(|e| EnableLightningError::SystemTimeError(e.to_string()))?; + + Ok(Arc::new(KeysManager::new(&seed, cur.as_secs(), cur.subsec_nanos()))) +} + +pub async fn init_channel_manager( + platform: Arc, + logger: Arc, + persister: Arc, + keys_manager: Arc, + user_config: UserConfig, +) -> EnableLightningResult<(Arc, Arc)> { + // Initialize the FeeEstimator. UtxoStandardCoin implements the FeeEstimator trait, so it'll act as our fee estimator. + let fee_estimator = platform.clone(); + + // Initialize the BroadcasterInterface. UtxoStandardCoin implements the BroadcasterInterface trait, so it'll act as our transaction + // broadcaster. + let broadcaster = platform.clone(); + + // Initialize the ChainMonitor + let chain_monitor: Arc = Arc::new(chainmonitor::ChainMonitor::new( + Some(platform.clone()), + broadcaster.clone(), + logger.clone(), + fee_estimator.clone(), + persister.clone(), + )); + + // Read ChannelMonitor state from disk, important for lightning node is restarting and has at least 1 channel + let mut channelmonitors = persister + .read_channelmonitors(keys_manager.clone()) + .map_to_mm(|e| EnableLightningError::IOError(e.to_string()))?; + + // This is used for Electrum only to prepare for chain synchronization + for (_, chan_mon) in channelmonitors.iter() { + chan_mon.load_outputs_to_watch(&platform); + } + + let rpc_client = match &platform.coin.as_ref().rpc_client { + UtxoRpcClientEnum::Electrum(c) => c.clone(), + UtxoRpcClientEnum::Native(_) => { + return MmError::err(EnableLightningError::UnsupportedMode( + "Lightning network".into(), + "electrum".into(), + )) + }, + }; + let best_header = get_best_header(&rpc_client).await?; + platform.update_best_block_height(best_header.block_height()); + let best_block = RpcBestBlock::from(best_header.clone()); + let best_block_hash = BlockHash::from_hash( + sha256d::Hash::from_slice(&best_block.hash.0).map_to_mm(|e| EnableLightningError::HashError(e.to_string()))?, + ); + let (channel_manager_blockhash, channel_manager) = { + if let Ok(mut f) = File::open(persister.manager_path()) { + let mut channel_monitor_mut_references = Vec::new(); + for (_, channel_monitor) in channelmonitors.iter_mut() { + channel_monitor_mut_references.push(channel_monitor); + } + // Read ChannelManager data from the file + let read_args = ChannelManagerReadArgs::new( + keys_manager.clone(), + fee_estimator.clone(), + chain_monitor.clone(), + broadcaster.clone(), + logger.clone(), + user_config, + channel_monitor_mut_references, + ); + <(BlockHash, ChannelManager)>::read(&mut f, read_args) + .map_to_mm(|e| EnableLightningError::IOError(e.to_string()))? + } else { + // Initialize the ChannelManager to starting a new node without history + let chain_params = ChainParameters { + network: platform.network.clone().into(), + best_block: BestBlock::new(best_block_hash, best_block.height as u32), + }; + let new_channel_manager = channelmanager::ChannelManager::new( + fee_estimator.clone(), + chain_monitor.clone(), + broadcaster.clone(), + logger.clone(), + keys_manager.clone(), + user_config, + chain_params, + ); + (best_block_hash, new_channel_manager) + } + }; + + let channel_manager: Arc = Arc::new(channel_manager); + + // Sync ChannelMonitors and ChannelManager to chain tip if the node is restarting and has open channels + if channel_manager_blockhash != best_block_hash { + platform + .process_txs_unconfirmations(&chain_monitor, &channel_manager) + .await; + platform + .process_txs_confirmations(&rpc_client, &persister, &chain_monitor, &channel_manager) + .await; + update_best_block(&chain_monitor, &channel_manager, best_header).await; + } + + // Give ChannelMonitors to ChainMonitor + for (_, channel_monitor) in channelmonitors.drain(..) { + let funding_outpoint = channel_monitor.get_funding_txo().0; + chain_monitor + .watch_channel(funding_outpoint, channel_monitor) + .map_to_mm(|e| EnableLightningError::IOError(format!("{:?}", e)))?; + } + + // Update best block whenever there's a new chain tip or a block has been newly disconnected + spawn(ln_best_block_update_loop( + // It's safe to use unwrap here for now until implementing Native Client for Lightning + platform, + persister.clone(), + chain_monitor.clone(), + channel_manager.clone(), + rpc_client.clone(), + best_block, + )); + + Ok((chain_monitor, channel_manager)) +} + +pub async fn persist_network_graph_loop(persister: Arc, network_graph: Arc) { + loop { + if let Err(e) = persister.save_network_graph(network_graph.clone()).await { + log::warn!( + "Failed to persist network graph error: {}, please check disk space and permissions", + e + ); + } + Timer::sleep(NETWORK_GRAPH_PERSIST_INTERVAL as f64).await; + } +} + +pub async fn persist_scorer_loop(persister: Arc, scorer: Arc>) { + loop { + if let Err(e) = persister.save_scorer(scorer.clone()).await { + log::warn!( + "Failed to persist scorer error: {}, please check disk space and permissions", + e + ); + } + Timer::sleep(SCORER_PERSIST_INTERVAL as f64).await; + } +} + +pub async fn get_open_channels_nodes_addresses( + persister: Arc, + channel_manager: Arc, +) -> EnableLightningResult { + let channels = channel_manager.list_channels(); + let mut nodes_addresses = persister.get_nodes_addresses().await?; + nodes_addresses.retain(|pubkey, _node_addr| { + channels + .iter() + .map(|chan| chan.counterparty.node_id) + .any(|node_id| node_id == *pubkey) + }); + Ok(nodes_addresses) +} diff --git a/mm2src/coins/lightning_background_processor/Cargo.toml b/mm2src/coins/lightning_background_processor/Cargo.toml new file mode 100644 index 0000000000..5710dcfc2c --- /dev/null +++ b/mm2src/coins/lightning_background_processor/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "lightning-background-processor" +version = "0.0.106" +authors = ["Valentine Wallace "] +license = "MIT OR Apache-2.0" +repository = "http://github.com/lightningdevkit/rust-lightning" +description = """ +Utilities to perform required background tasks for Rust Lightning. +""" +edition = "2018" + +[dependencies] +bitcoin = "0.27.1" +lightning = { git = "https://github.com/shamardy/rust-lightning", branch = "0.0.106", features = ["std"] } + +[dev-dependencies] +db_common = { path = "../../db_common" } +lightning = { git = "https://github.com/shamardy/rust-lightning", branch = "0.0.106", features = ["_test_utils"] } +lightning-invoice = { git = "https://github.com/shamardy/rust-lightning", branch = "0.0.106" } +lightning-persister = { version = "0.0.106", path = "../lightning_persister" } diff --git a/mm2src/coins/lightning_background_processor/src/lib.rs b/mm2src/coins/lightning_background_processor/src/lib.rs new file mode 100644 index 0000000000..4ca2fe9ad4 --- /dev/null +++ b/mm2src/coins/lightning_background_processor/src/lib.rs @@ -0,0 +1,950 @@ +//! Utilities that take care of tasks that (1) need to happen periodically to keep Rust-Lightning +//! running properly, and (2) either can or should be run in the background. See docs for +//! [`BackgroundProcessor`] for more details on the nitty-gritty. + +#[macro_use] extern crate lightning; + +use lightning::chain; +use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; +use lightning::chain::chainmonitor::{ChainMonitor, Persist}; +use lightning::chain::keysinterface::{KeysInterface, Sign}; +use lightning::ln::channelmanager::ChannelManager; +use lightning::ln::msgs::{ChannelMessageHandler, RoutingMessageHandler}; +use lightning::ln::peer_handler::{CustomMessageHandler, PeerManager, SocketDescriptor}; +use lightning::routing::network_graph::{NetGraphMsgHandler, NetworkGraph}; +use lightning::util::events::{Event, EventHandler, EventsProvider}; +use lightning::util::logger::Logger; +use std::ops::Deref; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + +/// `BackgroundProcessor` takes care of tasks that (1) need to happen periodically to keep +/// Rust-Lightning running properly, and (2) either can or should be run in the background. Its +/// responsibilities are: +/// * Processing [`Event`]s with a user-provided [`EventHandler`]. +/// * Monitoring whether the [`ChannelManager`] needs to be re-persisted to disk, and if so, +/// writing it to disk/backups by invoking the callback given to it at startup. +/// [`ChannelManager`] persistence should be done in the background. +/// * Calling [`ChannelManager::timer_tick_occurred`] and [`PeerManager::timer_tick_occurred`] +/// at the appropriate intervals. +/// * Calling [`NetworkGraph::remove_stale_channels`] (if a [`NetGraphMsgHandler`] is provided to +/// [`BackgroundProcessor::start`]). +/// +/// It will also call [`PeerManager::process_events`] periodically though this shouldn't be relied +/// upon as doing so may result in high latency. +/// +/// # Note +/// +/// If [`ChannelManager`] persistence fails and the persisted manager becomes out-of-date, then +/// there is a risk of channels force-closing on startup when the manager realizes it's outdated. +/// However, as long as [`ChannelMonitor`] backups are sound, no funds besides those used for +/// unilateral chain closure fees are at risk. +/// +/// [`ChannelMonitor`]: lightning::chain::channelmonitor::ChannelMonitor +/// [`Event`]: lightning::util::events::Event +#[must_use = "BackgroundProcessor will immediately stop on drop. It should be stored until shutdown."] +pub struct BackgroundProcessor { + stop_thread: Arc, + thread_handle: Option>>, +} + +#[cfg(not(test))] +const FRESHNESS_TIMER: u64 = 60; +#[cfg(test)] +const FRESHNESS_TIMER: u64 = 1; + +#[cfg(all(not(test), not(debug_assertions)))] +const PING_TIMER: u64 = 10; +/// Signature operations take a lot longer without compiler optimisations. +/// Increasing the ping timer allows for this but slower devices will be disconnected if the +/// timeout is reached. +#[cfg(all(not(test), debug_assertions))] +const PING_TIMER: u64 = 30; +#[cfg(test)] +const PING_TIMER: u64 = 1; + +/// Prune the network graph of stale entries hourly. +const NETWORK_PRUNE_TIMER: u64 = 60 * 60; + +/// Trait which handles persisting a [`ChannelManager`] to disk. +/// +/// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager +pub trait ChannelManagerPersister +where + M::Target: 'static + chain::Watch, + T::Target: 'static + BroadcasterInterface, + K::Target: 'static + KeysInterface, + F::Target: 'static + FeeEstimator, + L::Target: 'static + Logger, +{ + /// Persist the given [`ChannelManager`] to disk, returning an error if persistence failed + /// (which will cause the [`BackgroundProcessor`] which called this method to exit. + /// + /// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager + fn persist_manager(&self, channel_manager: &ChannelManager) -> Result<(), std::io::Error>; +} + +impl ChannelManagerPersister + for Fun +where + M::Target: 'static + chain::Watch, + T::Target: 'static + BroadcasterInterface, + K::Target: 'static + KeysInterface, + F::Target: 'static + FeeEstimator, + L::Target: 'static + Logger, + Fun: Fn(&ChannelManager) -> Result<(), std::io::Error>, +{ + fn persist_manager(&self, channel_manager: &ChannelManager) -> Result<(), std::io::Error> { + self(channel_manager) + } +} + +/// Decorates an [`EventHandler`] with common functionality provided by standard [`EventHandler`]s. +struct DecoratingEventHandler< + E: EventHandler, + N: Deref>, + G: Deref, + A: Deref, + L: Deref, +> where + A::Target: chain::Access, + L::Target: Logger, +{ + event_handler: E, + net_graph_msg_handler: Option, +} + +impl< + E: EventHandler, + N: Deref>, + G: Deref, + A: Deref, + L: Deref, + > EventHandler for DecoratingEventHandler +where + A::Target: chain::Access, + L::Target: Logger, +{ + fn handle_event(&self, event: &Event) { + if let Some(event_handler) = &self.net_graph_msg_handler { + event_handler.handle_event(event); + } + self.event_handler.handle_event(event); + } +} + +impl BackgroundProcessor { + /// Start a background thread that takes care of responsibilities enumerated in the [top-level + /// documentation]. + /// + /// The thread runs indefinitely unless the object is dropped, [`stop`] is called, or + /// `persist_manager` returns an error. In case of an error, the error is retrieved by calling + /// either [`join`] or [`stop`]. + /// + /// # Data Persistence + /// + /// `persist_manager` is responsible for writing out the [`ChannelManager`] to disk, and/or + /// uploading to one or more backup services. See [`ChannelManager::write`] for writing out a + /// [`ChannelManager`]. See [`LightningPersister::persist_manager`] for Rust-Lightning's + /// provided implementation. + /// + /// Typically, users should either implement [`ChannelManagerPersister`] to never return an + /// error or call [`join`] and handle any error that may arise. For the latter case, + /// `BackgroundProcessor` must be restarted by calling `start` again after handling the error. + /// + /// # Event Handling + /// + /// `event_handler` is responsible for handling events that users should be notified of (e.g., + /// payment failed). [`BackgroundProcessor`] may decorate the given [`EventHandler`] with common + /// functionality implemented by other handlers. + /// * [`NetGraphMsgHandler`] if given will update the [`NetworkGraph`] based on payment failures. + /// + /// [top-level documentation]: BackgroundProcessor + /// [`join`]: Self::join + /// [`stop`]: Self::stop + /// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager + /// [`ChannelManager::write`]: lightning::ln::channelmanager::ChannelManager#impl-Writeable + /// [`LightningPersister::persist_manager`]: lightning_persister::LightningPersister::persist_manager + /// [`NetworkGraph`]: lightning::routing::network_graph::NetworkGraph + pub fn start< + Signer: 'static + Sign, + CA: 'static + Deref + Send + Sync, + CF: 'static + Deref + Send + Sync, + CW: 'static + Deref + Send + Sync, + T: 'static + Deref + Send + Sync, + K: 'static + Deref + Send + Sync, + F: 'static + Deref + Send + Sync, + G: 'static + Deref + Send + Sync, + L: 'static + Deref + Send + Sync, + P: 'static + Deref + Send + Sync, + Descriptor: 'static + SocketDescriptor + Send + Sync, + CMH: 'static + Deref + Send + Sync, + RMH: 'static + Deref + Send + Sync, + EH: 'static + EventHandler + Send, + CMP: 'static + Send + ChannelManagerPersister, + M: 'static + Deref> + Send + Sync, + CM: 'static + Deref> + Send + Sync, + NG: 'static + Deref> + Send + Sync, + UMH: 'static + Deref + Send + Sync, + PM: 'static + Deref> + Send + Sync, + >( + persister: CMP, + event_handler: EH, + chain_monitor: M, + channel_manager: CM, + net_graph_msg_handler: Option, + peer_manager: PM, + logger: L, + ) -> Self + where + CA::Target: 'static + chain::Access, + CF::Target: 'static + chain::Filter, + CW::Target: 'static + chain::Watch, + T::Target: 'static + BroadcasterInterface, + K::Target: 'static + KeysInterface, + F::Target: 'static + FeeEstimator, + L::Target: 'static + Logger, + P::Target: 'static + Persist, + CMH::Target: 'static + ChannelMessageHandler, + RMH::Target: 'static + RoutingMessageHandler, + UMH::Target: 'static + CustomMessageHandler, + { + let stop_thread = Arc::new(AtomicBool::new(false)); + let stop_thread_clone = stop_thread.clone(); + let handle = thread::spawn(move || -> Result<(), std::io::Error> { + let event_handler = DecoratingEventHandler { + event_handler, + net_graph_msg_handler: net_graph_msg_handler.as_deref(), + }; + + log_trace!(logger, "Calling ChannelManager's timer_tick_occurred on startup"); + channel_manager.timer_tick_occurred(); + + let mut last_freshness_call = Instant::now(); + let mut last_ping_call = Instant::now(); + let mut last_prune_call = Instant::now(); + let mut have_pruned = false; + + loop { + peer_manager.process_events(); // Note that this may block on ChannelManager's locking + channel_manager.process_pending_events(&event_handler); + chain_monitor.process_pending_events(&event_handler); + + // We wait up to 100ms, but track how long it takes to detect being put to sleep, + // see `await_start`'s use below. + let await_start = Instant::now(); + let updates_available = channel_manager.await_persistable_update_timeout(Duration::from_millis(100)); + let await_time = await_start.elapsed(); + + if updates_available { + log_trace!(logger, "Persisting ChannelManager..."); + persister.persist_manager(&*channel_manager)?; + log_trace!(logger, "Done persisting ChannelManager."); + } + // Exit the loop if the background processor was requested to stop. + if stop_thread.load(Ordering::Acquire) { + log_trace!(logger, "Terminating background processor."); + break; + } + if last_freshness_call.elapsed().as_secs() > FRESHNESS_TIMER { + log_trace!(logger, "Calling ChannelManager's timer_tick_occurred"); + channel_manager.timer_tick_occurred(); + last_freshness_call = Instant::now(); + } + if await_time > Duration::from_secs(1) { + // On various platforms, we may be starved of CPU cycles for several reasons. + // E.g. on iOS, if we've been in the background, we will be entirely paused. + // Similarly, if we're on a desktop platform and the device has been asleep, we + // may not get any cycles. + // We detect this by checking if our max-100ms-sleep, above, ran longer than a + // full second, at which point we assume sockets may have been killed (they + // appear to be at least on some platforms, even if it has only been a second). + // Note that we have to take care to not get here just because user event + // processing was slow at the top of the loop. For example, the sample client + // may call Bitcoin Core RPCs during event handling, which very often takes + // more than a handful of seconds to complete, and shouldn't disconnect all our + // peers. + log_trace!(logger, "100ms sleep took more than a second, disconnecting peers."); + peer_manager.disconnect_all_peers(); + last_ping_call = Instant::now(); + } else if last_ping_call.elapsed().as_secs() > PING_TIMER { + log_trace!(logger, "Calling PeerManager's timer_tick_occurred"); + peer_manager.timer_tick_occurred(); + last_ping_call = Instant::now(); + } + + // Note that we want to run a graph prune once not long after startup before + // falling back to our usual hourly prunes. This avoids short-lived clients never + // pruning their network graph. We run once 60 seconds after startup before + // continuing our normal cadence. + if last_prune_call.elapsed().as_secs() > if have_pruned { NETWORK_PRUNE_TIMER } else { 60 } { + if let Some(ref handler) = net_graph_msg_handler { + log_trace!(logger, "Pruning network graph of stale entries"); + handler.network_graph().remove_stale_channels(); + last_prune_call = Instant::now(); + have_pruned = true; + } + } + } + // After we exit, ensure we persist the ChannelManager one final time - this avoids + // some races where users quit while channel updates were in-flight, with + // ChannelMonitor update(s) persisted without a corresponding ChannelManager update. + persister.persist_manager(&*channel_manager) + }); + Self { + stop_thread: stop_thread_clone, + thread_handle: Some(handle), + } + } + + /// Join `BackgroundProcessor`'s thread, returning any error that occurred while persisting + /// [`ChannelManager`]. + /// + /// # Panics + /// + /// This function panics if the background thread has panicked such as while persisting or + /// handling events. + /// + /// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager + pub fn join(mut self) -> Result<(), std::io::Error> { + assert!(self.thread_handle.is_some()); + self.join_thread() + } + + /// Stop `BackgroundProcessor`'s thread, returning any error that occurred while persisting + /// [`ChannelManager`]. + /// + /// # Panics + /// + /// This function panics if the background thread has panicked such as while persisting or + /// handling events. + /// + /// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager + pub fn stop(mut self) -> Result<(), std::io::Error> { + assert!(self.thread_handle.is_some()); + self.stop_and_join_thread() + } + + fn stop_and_join_thread(&mut self) -> Result<(), std::io::Error> { + self.stop_thread.store(true, Ordering::Release); + self.join_thread() + } + + fn join_thread(&mut self) -> Result<(), std::io::Error> { + match self.thread_handle.take() { + Some(handle) => handle.join().unwrap(), + None => Ok(()), + } + } +} + +impl Drop for BackgroundProcessor { + fn drop(&mut self) { self.stop_and_join_thread().unwrap(); } +} + +#[cfg(test)] +mod tests { + use super::{BackgroundProcessor, FRESHNESS_TIMER}; + use bitcoin::blockdata::block::BlockHeader; + use bitcoin::blockdata::constants::genesis_block; + use bitcoin::blockdata::transaction::{Transaction, TxOut}; + use bitcoin::network::constants::Network; + use db_common::sqlite::rusqlite::Connection; + use lightning::chain::channelmonitor::ANTI_REORG_DELAY; + use lightning::chain::keysinterface::{InMemorySigner, KeysInterface, KeysManager, Recipient}; + use lightning::chain::transaction::OutPoint; + use lightning::chain::{chainmonitor, BestBlock, Confirm}; + use lightning::get_event_msg; + use lightning::ln::channelmanager::{ChainParameters, ChannelManager, SimpleArcChannelManager, BREAKDOWN_TIMEOUT}; + use lightning::ln::features::InitFeatures; + use lightning::ln::msgs::{ChannelMessageHandler, Init}; + use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler, PeerManager, SocketDescriptor}; + use lightning::routing::network_graph::{NetGraphMsgHandler, NetworkGraph}; + use lightning::util::config::UserConfig; + use lightning::util::events::{Event, MessageSendEvent, MessageSendEventsProvider}; + use lightning::util::ser::Writeable; + use lightning::util::test_utils; + use lightning_invoice::payment::{InvoicePayer, RetryAttempts}; + use lightning_invoice::utils::DefaultRouter; + use lightning_persister::LightningPersister; + use std::fs; + use std::path::PathBuf; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + const EVENT_DEADLINE: u64 = 5 * FRESHNESS_TIMER; + + #[derive(Clone, Eq, Hash, PartialEq)] + struct TestDescriptor {} + impl SocketDescriptor for TestDescriptor { + fn send_data(&mut self, _data: &[u8], _resume_read: bool) -> usize { 0 } + + fn disconnect_socket(&mut self) {} + } + + type ChainMonitor = chainmonitor::ChainMonitor< + InMemorySigner, + Arc, + Arc, + Arc, + Arc, + Arc, + >; + + struct Node { + node: Arc< + SimpleArcChannelManager< + ChainMonitor, + test_utils::TestBroadcaster, + test_utils::TestFeeEstimator, + test_utils::TestLogger, + >, + >, + net_graph_msg_handler: Option< + Arc, Arc, Arc>>, + >, + peer_manager: Arc< + PeerManager< + TestDescriptor, + Arc, + Arc, + Arc, + IgnoringMessageHandler, + >, + >, + chain_monitor: Arc, + persister: Arc, + tx_broadcaster: Arc, + network_graph: Arc, + logger: Arc, + best_block: BestBlock, + } + + impl Drop for Node { + fn drop(&mut self) { + let data_dir = self.persister.main_path(); + match fs::remove_dir_all(data_dir.clone()) { + Err(e) => println!( + "Failed to remove test persister directory {}: {}", + data_dir.to_str().unwrap(), + e + ), + _ => {}, + } + } + } + + fn get_full_filepath(filepath: String, filename: String) -> String { + let mut path = PathBuf::from(filepath); + path.push(filename); + path.to_str().unwrap().to_string() + } + + fn create_nodes(num_nodes: usize, persist_dir: String) -> Vec { + let mut nodes = Vec::new(); + for i in 0..num_nodes { + let tx_broadcaster = Arc::new(test_utils::TestBroadcaster { + txn_broadcasted: Mutex::new(Vec::new()), + blocks: Arc::new(Mutex::new(Vec::new())), + }); + let fee_estimator = Arc::new(test_utils::TestFeeEstimator { + sat_per_kw: Mutex::new(253), + }); + let chain_source = Arc::new(test_utils::TestChainSource::new(Network::Testnet)); + let logger = Arc::new(test_utils::TestLogger::with_id(format!("node {}", i))); + let persister = Arc::new(LightningPersister::new( + format!("node_{}_ticker", i), + PathBuf::from(format!("{}_persister_{}", persist_dir, i)), + None, + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + )); + let seed = [i as u8; 32]; + let network = Network::Testnet; + let genesis_block = genesis_block(network); + let now = Duration::from_secs(genesis_block.header.time as u64); + let keys_manager = Arc::new(KeysManager::new(&seed, now.as_secs(), now.subsec_nanos())); + let chain_monitor = Arc::new(chainmonitor::ChainMonitor::new( + Some(chain_source.clone()), + tx_broadcaster.clone(), + logger.clone(), + fee_estimator.clone(), + persister.clone(), + )); + let best_block = BestBlock::from_genesis(network); + let params = ChainParameters { network, best_block }; + let manager = Arc::new(ChannelManager::new( + fee_estimator.clone(), + chain_monitor.clone(), + tx_broadcaster.clone(), + logger.clone(), + keys_manager.clone(), + UserConfig::default(), + params, + )); + let network_graph = Arc::new(NetworkGraph::new(genesis_block.header.block_hash())); + let net_graph_msg_handler = Some(Arc::new(NetGraphMsgHandler::new( + network_graph.clone(), + Some(chain_source.clone()), + logger.clone(), + ))); + let msg_handler = MessageHandler { + chan_handler: Arc::new(test_utils::TestChannelMessageHandler::new()), + route_handler: Arc::new(test_utils::TestRoutingMessageHandler::new()), + }; + let peer_manager = Arc::new(PeerManager::new( + msg_handler, + keys_manager.get_node_secret(Recipient::Node).unwrap(), + &seed, + logger.clone(), + IgnoringMessageHandler {}, + )); + let node = Node { + node: manager, + net_graph_msg_handler, + peer_manager, + chain_monitor, + persister, + tx_broadcaster, + network_graph, + logger, + best_block, + }; + nodes.push(node); + } + + for i in 0..num_nodes { + for j in (i + 1)..num_nodes { + nodes[i].node.peer_connected(&nodes[j].node.get_our_node_id(), &Init { + features: InitFeatures::known(), + remote_network_address: None, + }); + nodes[j].node.peer_connected(&nodes[i].node.get_our_node_id(), &Init { + features: InitFeatures::known(), + remote_network_address: None, + }); + } + } + + nodes + } + + macro_rules! open_channel { + ($node_a: expr, $node_b: expr, $channel_value: expr) => {{ + begin_open_channel!($node_a, $node_b, $channel_value); + let events = $node_a.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let (temporary_channel_id, tx) = handle_funding_generation_ready!(&events[0], $channel_value); + end_open_channel!($node_a, $node_b, temporary_channel_id, tx); + tx + }}; + } + + macro_rules! begin_open_channel { + ($node_a: expr, $node_b: expr, $channel_value: expr) => {{ + $node_a + .node + .create_channel($node_b.node.get_our_node_id(), $channel_value, 100, 42, None) + .unwrap(); + $node_b.node.handle_open_channel( + &$node_a.node.get_our_node_id(), + InitFeatures::known(), + &get_event_msg!( + $node_a, + MessageSendEvent::SendOpenChannel, + $node_b.node.get_our_node_id() + ), + ); + $node_a.node.handle_accept_channel( + &$node_b.node.get_our_node_id(), + InitFeatures::known(), + &get_event_msg!( + $node_b, + MessageSendEvent::SendAcceptChannel, + $node_a.node.get_our_node_id() + ), + ); + }}; + } + + macro_rules! handle_funding_generation_ready { + ($event: expr, $channel_value: expr) => {{ + match $event { + &Event::FundingGenerationReady { + temporary_channel_id, + channel_value_satoshis, + ref output_script, + user_channel_id, + } => { + assert_eq!(channel_value_satoshis, $channel_value); + assert_eq!(user_channel_id, 42); + + let tx = Transaction { + version: 1 as i32, + lock_time: 0, + input: Vec::new(), + output: vec![TxOut { + value: channel_value_satoshis, + script_pubkey: output_script.clone(), + }], + }; + (temporary_channel_id, tx) + }, + _ => panic!("Unexpected event"), + } + }}; + } + + macro_rules! end_open_channel { + ($node_a: expr, $node_b: expr, $temporary_channel_id: expr, $tx: expr) => {{ + $node_a + .node + .funding_transaction_generated(&$temporary_channel_id, $tx.clone()) + .unwrap(); + $node_b.node.handle_funding_created( + &$node_a.node.get_our_node_id(), + &get_event_msg!( + $node_a, + MessageSendEvent::SendFundingCreated, + $node_b.node.get_our_node_id() + ), + ); + $node_a.node.handle_funding_signed( + &$node_b.node.get_our_node_id(), + &get_event_msg!( + $node_b, + MessageSendEvent::SendFundingSigned, + $node_a.node.get_our_node_id() + ), + ); + }}; + } + + fn confirm_transaction_depth(node: &mut Node, tx: &Transaction, depth: u32) { + for i in 1..=depth { + let prev_blockhash = node.best_block.block_hash(); + let height = node.best_block.height() + 1; + let header = BlockHeader { + version: 0x20000000, + prev_blockhash, + merkle_root: Default::default(), + time: height, + bits: 42, + nonce: 42, + }; + let txdata = vec![(0, tx)]; + node.best_block = BestBlock::new(header.block_hash(), height); + match i { + 1 => { + node.node.transactions_confirmed(&header, &txdata, height); + node.chain_monitor.transactions_confirmed(&header, &txdata, height); + }, + x if x == depth => { + node.node.best_block_updated(&header, height); + node.chain_monitor.best_block_updated(&header, height); + }, + _ => {}, + } + } + } + fn confirm_transaction(node: &mut Node, tx: &Transaction) { confirm_transaction_depth(node, tx, ANTI_REORG_DELAY); } + + #[test] + fn test_background_processor() { + // Test that when a new channel is created, the ChannelManager needs to be re-persisted with + // updates. Also test that when new updates are available, the manager signals that it needs + // re-persistence and is successfully re-persisted. + let nodes = create_nodes(2, "test_background_processor".to_string()); + + // Go through the channel creation process so that each node has something to persist. Since + // open_channel consumes events, it must complete before starting BackgroundProcessor to + // avoid a race with processing events. + let tx = open_channel!(nodes[0], nodes[1], 100000); + + // Initiate the background processors to watch each node. + let node_0_persister = nodes[0].persister.clone(); + let persister = move |node: &ChannelManager< + InMemorySigner, + Arc, + Arc, + Arc, + Arc, + Arc, + >| node_0_persister.persist_manager(node); + let event_handler = |_: &_| {}; + let bg_processor = BackgroundProcessor::start( + persister, + event_handler, + nodes[0].chain_monitor.clone(), + nodes[0].node.clone(), + nodes[0].net_graph_msg_handler.clone(), + nodes[0].peer_manager.clone(), + nodes[0].logger.clone(), + ); + + macro_rules! check_persisted_data { + ($node: expr, $filepath: expr, $expected_bytes: expr) => { + loop { + $expected_bytes.clear(); + match $node.write(&mut $expected_bytes) { + Ok(()) => match std::fs::read($filepath) { + Ok(bytes) => { + if bytes == $expected_bytes { + break; + } else { + continue; + } + }, + Err(_) => continue, + }, + Err(e) => panic!("Unexpected error: {}", e), + } + } + }; + } + + // Check that the initial channel manager data is persisted as expected. + let filepath = get_full_filepath( + "test_background_processor_persister_0".to_string(), + "manager".to_string(), + ); + let mut expected_bytes = Vec::new(); + check_persisted_data!(nodes[0].node, filepath.clone(), expected_bytes); + loop { + if !nodes[0].node.get_persistence_condvar_value() { + break; + } + } + + // Force-close the channel. + nodes[0] + .node + .force_close_channel( + &OutPoint { + txid: tx.txid(), + index: 0, + } + .to_channel_id(), + ) + .unwrap(); + + // Check that the force-close updates are persisted. + let mut expected_bytes = Vec::new(); + check_persisted_data!(nodes[0].node, filepath.clone(), expected_bytes); + loop { + if !nodes[0].node.get_persistence_condvar_value() { + break; + } + } + + assert!(bg_processor.stop().is_ok()); + } + + #[test] + fn test_timer_tick_called() { + // Test that ChannelManager's and PeerManager's `timer_tick_occurred` is called every + // `FRESHNESS_TIMER`. + let nodes = create_nodes(1, "test_timer_tick_called".to_string()); + let node_0_persister = nodes[0].persister.clone(); + let persister = move |node: &ChannelManager< + InMemorySigner, + Arc, + Arc, + Arc, + Arc, + Arc, + >| node_0_persister.persist_manager(node); + let event_handler = |_: &_| {}; + let bg_processor = BackgroundProcessor::start( + persister, + event_handler, + nodes[0].chain_monitor.clone(), + nodes[0].node.clone(), + nodes[0].net_graph_msg_handler.clone(), + nodes[0].peer_manager.clone(), + nodes[0].logger.clone(), + ); + loop { + let log_entries = nodes[0].logger.lines.lock().unwrap(); + let desired_log = "Calling ChannelManager's timer_tick_occurred".to_string(); + let second_desired_log = "Calling PeerManager's timer_tick_occurred".to_string(); + if log_entries + .get(&("lightning_background_processor".to_string(), desired_log)) + .is_some() + && log_entries + .get(&("lightning_background_processor".to_string(), second_desired_log)) + .is_some() + { + break; + } + } + + assert!(bg_processor.stop().is_ok()); + } + + #[test] + fn test_persist_error() { + // Test that if we encounter an error during manager persistence, the thread panics. + let nodes = create_nodes(2, "test_persist_error".to_string()); + open_channel!(nodes[0], nodes[1], 100000); + + let persister = |_: &_| Err(std::io::Error::new(std::io::ErrorKind::Other, "test")); + let event_handler = |_: &_| {}; + let bg_processor = BackgroundProcessor::start( + persister, + event_handler, + nodes[0].chain_monitor.clone(), + nodes[0].node.clone(), + nodes[0].net_graph_msg_handler.clone(), + nodes[0].peer_manager.clone(), + nodes[0].logger.clone(), + ); + match bg_processor.join() { + Ok(_) => panic!("Expected error persisting manager"), + Err(e) => { + assert_eq!(e.kind(), std::io::ErrorKind::Other); + assert_eq!(e.get_ref().unwrap().to_string(), "test"); + }, + } + } + + #[test] + fn test_background_event_handling() { + let mut nodes = create_nodes(2, "test_background_event_handling".to_string()); + let channel_value = 100000; + let node_0_persister = nodes[0].persister.clone(); + let persister = move |node: &_| node_0_persister.persist_manager(node); + + // Set up a background event handler for FundingGenerationReady events. + let (sender, receiver) = std::sync::mpsc::sync_channel(1); + let event_handler = move |event: &Event| { + sender + .send(handle_funding_generation_ready!(event, channel_value)) + .unwrap(); + }; + let bg_processor = BackgroundProcessor::start( + persister.clone(), + event_handler, + nodes[0].chain_monitor.clone(), + nodes[0].node.clone(), + nodes[0].net_graph_msg_handler.clone(), + nodes[0].peer_manager.clone(), + nodes[0].logger.clone(), + ); + + // Open a channel and check that the FundingGenerationReady event was handled. + begin_open_channel!(nodes[0], nodes[1], channel_value); + let (temporary_channel_id, funding_tx) = receiver + .recv_timeout(Duration::from_secs(EVENT_DEADLINE)) + .expect("FundingGenerationReady not handled within deadline"); + end_open_channel!(nodes[0], nodes[1], temporary_channel_id, funding_tx); + + // Confirm the funding transaction. + confirm_transaction(&mut nodes[0], &funding_tx); + let as_funding = get_event_msg!( + nodes[0], + MessageSendEvent::SendFundingLocked, + nodes[1].node.get_our_node_id() + ); + confirm_transaction(&mut nodes[1], &funding_tx); + let bs_funding = get_event_msg!( + nodes[1], + MessageSendEvent::SendFundingLocked, + nodes[0].node.get_our_node_id() + ); + nodes[0] + .node + .handle_funding_locked(&nodes[1].node.get_our_node_id(), &bs_funding); + let _as_channel_update = get_event_msg!( + nodes[0], + MessageSendEvent::SendChannelUpdate, + nodes[1].node.get_our_node_id() + ); + nodes[1] + .node + .handle_funding_locked(&nodes[0].node.get_our_node_id(), &as_funding); + let _bs_channel_update = get_event_msg!( + nodes[1], + MessageSendEvent::SendChannelUpdate, + nodes[0].node.get_our_node_id() + ); + + assert!(bg_processor.stop().is_ok()); + + // Set up a background event handler for SpendableOutputs events. + let (sender, receiver) = std::sync::mpsc::sync_channel(1); + let event_handler = move |event: &Event| sender.send(event.clone()).unwrap(); + let bg_processor = BackgroundProcessor::start( + persister, + event_handler, + nodes[0].chain_monitor.clone(), + nodes[0].node.clone(), + nodes[0].net_graph_msg_handler.clone(), + nodes[0].peer_manager.clone(), + nodes[0].logger.clone(), + ); + + // Force close the channel and check that the SpendableOutputs event was handled. + nodes[0] + .node + .force_close_channel(&nodes[0].node.list_channels()[0].channel_id) + .unwrap(); + let commitment_tx = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().pop().unwrap(); + confirm_transaction_depth(&mut nodes[0], &commitment_tx, BREAKDOWN_TIMEOUT as u32); + let event = receiver + .recv_timeout(Duration::from_secs(EVENT_DEADLINE)) + .expect("SpendableOutputs not handled within deadline"); + match event { + Event::SpendableOutputs { .. } => {}, + Event::ChannelClosed { .. } => {}, + _ => panic!("Unexpected event: {:?}", event), + } + + assert!(bg_processor.stop().is_ok()); + } + + #[test] + fn test_invoice_payer() { + let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet); + let random_seed_bytes = keys_manager.get_secure_random_bytes(); + let nodes = create_nodes(2, "test_invoice_payer".to_string()); + + // Initiate the background processors to watch each node. + let node_0_persister = nodes[0].persister.clone(); + let persister = move |node: &ChannelManager< + InMemorySigner, + Arc, + Arc, + Arc, + Arc, + Arc, + >| node_0_persister.persist_manager(node); + let router = DefaultRouter::new( + Arc::clone(&nodes[0].network_graph), + Arc::clone(&nodes[0].logger), + random_seed_bytes, + ); + let scorer = Arc::new(Mutex::new(test_utils::TestScorer::with_penalty(0))); + let invoice_payer = Arc::new(InvoicePayer::new( + Arc::clone(&nodes[0].node), + router, + scorer, + Arc::clone(&nodes[0].logger), + |_: &_| {}, + RetryAttempts(2), + )); + let event_handler = Arc::clone(&invoice_payer); + let bg_processor = BackgroundProcessor::start( + persister, + event_handler, + nodes[0].chain_monitor.clone(), + nodes[0].node.clone(), + nodes[0].net_graph_msg_handler.clone(), + nodes[0].peer_manager.clone(), + nodes[0].logger.clone(), + ); + assert!(bg_processor.stop().is_ok()); + } +} diff --git a/mm2src/coins/lightning_persister/Cargo.toml b/mm2src/coins/lightning_persister/Cargo.toml new file mode 100644 index 0000000000..32b5d7eb1d --- /dev/null +++ b/mm2src/coins/lightning_persister/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "lightning-persister" +version = "0.0.106" +edition = "2018" +authors = ["Valentine Wallace", "Matt Corallo"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/lightningdevkit/rust-lightning/" +description = """ +Utilities to manage Rust-Lightning channel data persistence and retrieval. +""" + +[dependencies] +async-trait = "0.1" +bitcoin = "0.27.1" +common = { path = "../../common" } +mm2_io = { path = "../../mm2_io" } +db_common = { path = "../../db_common" } +derive_more = "0.99" +hex = "0.4.2" +lightning = { git = "https://github.com/shamardy/rust-lightning", branch = "0.0.106" } +libc = "0.2" +parking_lot = { version = "0.12.0", features = ["nightly"] } +secp256k1 = { version = "0.20" } +serde = "1.0" +serde_json = "1.0" + +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3", features = ["winbase"] } + +[dev-dependencies] +lightning = { git = "https://github.com/shamardy/rust-lightning", branch = "0.0.106", features = ["_test_utils"] } +rand = { version = "0.7", features = ["std", "small_rng"] } \ No newline at end of file diff --git a/mm2src/coins/lightning_persister/src/lib.rs b/mm2src/coins/lightning_persister/src/lib.rs new file mode 100644 index 0000000000..303205c26f --- /dev/null +++ b/mm2src/coins/lightning_persister/src/lib.rs @@ -0,0 +1,2097 @@ +//! Utilities that handle persisting Rust-Lightning data to disk via standard filesystem APIs. + +#![feature(io_error_more)] + +pub mod storage; +mod util; + +extern crate async_trait; +extern crate bitcoin; +extern crate common; +extern crate libc; +extern crate lightning; +extern crate secp256k1; +extern crate serde_json; + +use crate::storage::{ChannelType, ChannelVisibility, ClosedChannelsFilter, DbStorage, FileSystemStorage, + GetClosedChannelsResult, GetPaymentsResult, HTLCStatus, NodesAddressesMap, + NodesAddressesMapShared, PaymentInfo, PaymentType, PaymentsFilter, Scorer, SqlChannelDetails}; +use crate::util::DiskWriteable; +use async_trait::async_trait; +use bitcoin::blockdata::constants::genesis_block; +use bitcoin::hash_types::{BlockHash, Txid}; +use bitcoin::hashes::hex::{FromHex, ToHex}; +use bitcoin::Network; +use common::{async_blocking, PagingOptionsEnum}; +use db_common::sqlite::rusqlite::{Error as SqlError, Row, ToSql, NO_PARAMS}; +use db_common::sqlite::sql_builder::SqlBuilder; +use db_common::sqlite::{h256_option_slice_from_row, h256_slice_from_row, offset_by_id, query_single_row, + sql_text_conversion_err, string_from_row, validate_table_name, SqliteConnShared, + CHECK_TABLE_EXISTS_SQL}; +use lightning::chain; +use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; +use lightning::chain::chainmonitor; +use lightning::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate}; +use lightning::chain::keysinterface::{KeysInterface, Sign}; +use lightning::chain::transaction::OutPoint; +use lightning::ln::channelmanager::ChannelManager; +use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; +use lightning::routing::network_graph::NetworkGraph; +use lightning::routing::scoring::ProbabilisticScoringParameters; +use lightning::util::logger::Logger; +use lightning::util::ser::{Readable, ReadableArgs, Writeable}; +use mm2_io::fs::check_dir_operations; +use secp256k1::PublicKey; +use std::collections::HashMap; +use std::convert::TryInto; +use std::fs; +use std::io::{BufReader, BufWriter, Cursor, Error}; +use std::net::SocketAddr; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +/// LightningPersister persists channel data on disk, where each channel's +/// data is stored in a file named after its funding outpoint. +/// It is also used to persist payments and channels history to sqlite database. +/// +/// Warning: this module does the best it can with calls to persist data, but it +/// can only guarantee that the data is passed to the drive. It is up to the +/// drive manufacturers to do the actual persistence properly, which they often +/// don't (especially on consumer-grade hardware). Therefore, it is up to the +/// user to validate their entire storage stack, to ensure the writes are +/// persistent. +/// Corollary: especially when dealing with larger amounts of money, it is best +/// practice to have multiple channel data backups and not rely only on one +/// LightningPersister. + +pub struct LightningPersister { + storage_ticker: String, + main_path: PathBuf, + backup_path: Option, + sqlite_connection: SqliteConnShared, +} + +impl DiskWriteable for ChannelMonitor { + fn write_to_file(&self, writer: &mut fs::File) -> Result<(), Error> { self.write(writer) } +} + +impl DiskWriteable + for ChannelManager +where + M::Target: chain::Watch, + T::Target: BroadcasterInterface, + K::Target: KeysInterface, + F::Target: FeeEstimator, + L::Target: Logger, +{ + fn write_to_file(&self, writer: &mut fs::File) -> Result<(), std::io::Error> { self.write(writer) } +} + +fn channels_history_table(ticker: &str) -> String { ticker.to_owned() + "_channels_history" } + +fn payments_history_table(ticker: &str) -> String { ticker.to_owned() + "_payments_history" } + +fn create_channels_history_table_sql(for_coin: &str) -> Result { + let table_name = channels_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "CREATE TABLE IF NOT EXISTS {} ( + id INTEGER NOT NULL PRIMARY KEY, + rpc_id INTEGER NOT NULL UNIQUE, + channel_id VARCHAR(255) NOT NULL, + counterparty_node_id VARCHAR(255) NOT NULL, + funding_tx VARCHAR(255), + funding_value INTEGER, + funding_generated_in_block Integer, + closing_tx VARCHAR(255), + closure_reason TEXT, + claiming_tx VARCHAR(255), + claimed_balance REAL, + is_outbound INTEGER NOT NULL, + is_public INTEGER NOT NULL, + is_closed INTEGER NOT NULL, + created_at INTEGER NOT NULL, + closed_at INTEGER + );", + table_name + ); + + Ok(sql) +} + +fn create_payments_history_table_sql(for_coin: &str) -> Result { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "CREATE TABLE IF NOT EXISTS {} ( + id INTEGER NOT NULL PRIMARY KEY, + payment_hash VARCHAR(255) NOT NULL UNIQUE, + destination VARCHAR(255), + description VARCHAR(641) NOT NULL, + preimage VARCHAR(255), + secret VARCHAR(255), + amount_msat INTEGER, + fee_paid_msat INTEGER, + is_outbound INTEGER NOT NULL, + status VARCHAR(255) NOT NULL, + created_at INTEGER NOT NULL, + last_updated INTEGER NOT NULL + );", + table_name + ); + + Ok(sql) +} + +fn insert_channel_sql(for_coin: &str) -> Result { + let table_name = channels_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "INSERT INTO {} ( + rpc_id, + channel_id, + counterparty_node_id, + is_outbound, + is_public, + is_closed, + created_at + ) VALUES ( + ?1, ?2, ?3, ?4, ?5, ?6, ?7 + );", + table_name + ); + + Ok(sql) +} + +fn upsert_payment_sql(for_coin: &str) -> Result { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "INSERT OR REPLACE INTO {} ( + payment_hash, + destination, + description, + preimage, + secret, + amount_msat, + fee_paid_msat, + is_outbound, + status, + created_at, + last_updated + ) VALUES ( + ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11 + );", + table_name + ); + + Ok(sql) +} + +fn select_channel_by_rpc_id_sql(for_coin: &str) -> Result { + let table_name = channels_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "SELECT + rpc_id, + channel_id, + counterparty_node_id, + funding_tx, + funding_value, + funding_generated_in_block, + closing_tx, + closure_reason, + claiming_tx, + claimed_balance, + is_outbound, + is_public, + is_closed, + created_at, + closed_at + FROM + {} + WHERE + rpc_id=?1", + table_name + ); + + Ok(sql) +} + +fn select_payment_by_hash_sql(for_coin: &str) -> Result { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "SELECT + payment_hash, + destination, + description, + preimage, + secret, + amount_msat, + fee_paid_msat, + status, + is_outbound, + created_at, + last_updated + FROM + {} + WHERE + payment_hash=?1;", + table_name + ); + + Ok(sql) +} + +fn channel_details_from_row(row: &Row<'_>) -> Result { + let channel_details = SqlChannelDetails { + rpc_id: row.get::<_, u32>(0)? as u64, + channel_id: row.get(1)?, + counterparty_node_id: row.get(2)?, + funding_tx: row.get(3)?, + funding_value: row.get::<_, Option>(4)?.map(|v| v as u64), + funding_generated_in_block: row.get::<_, Option>(5)?.map(|v| v as u64), + closing_tx: row.get(6)?, + closure_reason: row.get(7)?, + claiming_tx: row.get(8)?, + claimed_balance: row.get::<_, Option>(9)?, + is_outbound: row.get(10)?, + is_public: row.get(11)?, + is_closed: row.get(12)?, + created_at: row.get::<_, u32>(13)? as u64, + closed_at: row.get::<_, Option>(14)?.map(|t| t as u64), + }; + Ok(channel_details) +} + +fn payment_info_from_row(row: &Row<'_>) -> Result { + let is_outbound = row.get::<_, bool>(8)?; + let payment_type = if is_outbound { + PaymentType::OutboundPayment { + destination: PublicKey::from_str(&row.get::<_, String>(1)?).map_err(|e| sql_text_conversion_err(1, e))?, + } + } else { + PaymentType::InboundPayment + }; + + let payment_info = PaymentInfo { + payment_hash: PaymentHash(h256_slice_from_row::(row, 0)?), + payment_type, + description: row.get(2)?, + preimage: h256_option_slice_from_row::(row, 3)?.map(PaymentPreimage), + secret: h256_option_slice_from_row::(row, 4)?.map(PaymentSecret), + amt_msat: row.get::<_, Option>(5)?.map(|v| v as u64), + fee_paid_msat: row.get::<_, Option>(6)?.map(|v| v as u64), + status: HTLCStatus::from_str(&row.get::<_, String>(7)?)?, + created_at: row.get::<_, u32>(9)? as u64, + last_updated: row.get::<_, u32>(10)? as u64, + }; + Ok(payment_info) +} + +fn get_last_channel_rpc_id_sql(for_coin: &str) -> Result { + let table_name = channels_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!("SELECT IFNULL(MAX(rpc_id), 0) FROM {};", table_name); + + Ok(sql) +} + +fn update_funding_tx_sql(for_coin: &str) -> Result { + let table_name = channels_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "UPDATE {} SET + funding_tx = ?1, + funding_value = ?2, + funding_generated_in_block = ?3 + WHERE + rpc_id = ?4;", + table_name + ); + + Ok(sql) +} + +fn update_funding_tx_block_height_sql(for_coin: &str) -> Result { + let table_name = channels_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "UPDATE {} SET funding_generated_in_block = ?1 WHERE funding_tx = ?2;", + table_name + ); + + Ok(sql) +} + +fn update_channel_to_closed_sql(for_coin: &str) -> Result { + let table_name = channels_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "UPDATE {} SET closure_reason = ?1, is_closed = ?2, closed_at = ?3 WHERE rpc_id = ?4;", + table_name + ); + + Ok(sql) +} + +fn update_closing_tx_sql(for_coin: &str) -> Result { + let table_name = channels_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!("UPDATE {} SET closing_tx = ?1 WHERE rpc_id = ?2;", table_name); + + Ok(sql) +} + +fn get_channels_builder_preimage(for_coin: &str) -> Result { + let table_name = channels_history_table(for_coin); + validate_table_name(&table_name)?; + + let mut sql_builder = SqlBuilder::select_from(table_name); + sql_builder.and_where("is_closed = 1"); + Ok(sql_builder) +} + +fn add_fields_to_get_channels_sql_builder(sql_builder: &mut SqlBuilder) { + sql_builder + .field("rpc_id") + .field("channel_id") + .field("counterparty_node_id") + .field("funding_tx") + .field("funding_value") + .field("funding_generated_in_block") + .field("closing_tx") + .field("closure_reason") + .field("claiming_tx") + .field("claimed_balance") + .field("is_outbound") + .field("is_public") + .field("is_closed") + .field("created_at") + .field("closed_at"); +} + +fn finalize_get_channels_sql_builder(sql_builder: &mut SqlBuilder, offset: usize, limit: usize) { + sql_builder.offset(offset); + sql_builder.limit(limit); + sql_builder.order_desc("closed_at"); +} + +fn apply_get_channels_filter(builder: &mut SqlBuilder, params: &mut Vec<(&str, String)>, filter: ClosedChannelsFilter) { + if let Some(channel_id) = filter.channel_id { + builder.and_where("channel_id = :channel_id"); + params.push((":channel_id", channel_id)); + } + + if let Some(counterparty_node_id) = filter.counterparty_node_id { + builder.and_where("counterparty_node_id = :counterparty_node_id"); + params.push((":counterparty_node_id", counterparty_node_id)); + } + + if let Some(funding_tx) = filter.funding_tx { + builder.and_where("funding_tx = :funding_tx"); + params.push((":funding_tx", funding_tx)); + } + + if let Some(from_funding_value) = filter.from_funding_value { + builder.and_where("funding_value >= :from_funding_value"); + params.push((":from_funding_value", from_funding_value.to_string())); + } + + if let Some(to_funding_value) = filter.to_funding_value { + builder.and_where("funding_value <= :to_funding_value"); + params.push((":to_funding_value", to_funding_value.to_string())); + } + + if let Some(closing_tx) = filter.closing_tx { + builder.and_where("closing_tx = :closing_tx"); + params.push((":closing_tx", closing_tx)); + } + + if let Some(closure_reason) = filter.closure_reason { + builder.and_where(format!("closure_reason LIKE '%{}%'", closure_reason)); + } + + if let Some(claiming_tx) = filter.claiming_tx { + builder.and_where("claiming_tx = :claiming_tx"); + params.push((":claiming_tx", claiming_tx)); + } + + if let Some(from_claimed_balance) = filter.from_claimed_balance { + builder.and_where("claimed_balance >= :from_claimed_balance"); + params.push((":from_claimed_balance", from_claimed_balance.to_string())); + } + + if let Some(to_claimed_balance) = filter.to_claimed_balance { + builder.and_where("claimed_balance <= :to_claimed_balance"); + params.push((":to_claimed_balance", to_claimed_balance.to_string())); + } + + if let Some(channel_type) = filter.channel_type { + let is_outbound = match channel_type { + ChannelType::Outbound => true as i32, + ChannelType::Inbound => false as i32, + }; + + builder.and_where("is_outbound = :is_outbound"); + params.push((":is_outbound", is_outbound.to_string())); + } + + if let Some(channel_visibility) = filter.channel_visibility { + let is_public = match channel_visibility { + ChannelVisibility::Public => true as i32, + ChannelVisibility::Private => false as i32, + }; + + builder.and_where("is_public = :is_public"); + params.push((":is_public", is_public.to_string())); + } +} + +fn get_payments_builder_preimage(for_coin: &str) -> Result { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + + Ok(SqlBuilder::select_from(table_name)) +} + +fn finalize_get_payments_sql_builder(sql_builder: &mut SqlBuilder, offset: usize, limit: usize) { + sql_builder + .field("payment_hash") + .field("destination") + .field("description") + .field("preimage") + .field("secret") + .field("amount_msat") + .field("fee_paid_msat") + .field("status") + .field("is_outbound") + .field("created_at") + .field("last_updated"); + sql_builder.offset(offset); + sql_builder.limit(limit); + sql_builder.order_desc("last_updated"); +} + +fn apply_get_payments_filter(builder: &mut SqlBuilder, params: &mut Vec<(&str, String)>, filter: PaymentsFilter) { + if let Some(payment_type) = filter.payment_type { + let (is_outbound, destination) = match payment_type { + PaymentType::OutboundPayment { destination } => (true as i32, Some(destination.to_string())), + PaymentType::InboundPayment => (false as i32, None), + }; + if let Some(dest) = destination { + builder.and_where("destination = :dest"); + params.push((":dest", dest)); + } + + builder.and_where("is_outbound = :is_outbound"); + params.push((":is_outbound", is_outbound.to_string())); + } + + if let Some(description) = filter.description { + builder.and_where(format!("description LIKE '%{}%'", description)); + } + + if let Some(status) = filter.status { + builder.and_where("status = :status"); + params.push((":status", status.to_string())); + } + + if let Some(from_amount) = filter.from_amount_msat { + builder.and_where("amount_msat >= :from_amount"); + params.push((":from_amount", from_amount.to_string())); + } + + if let Some(to_amount) = filter.to_amount_msat { + builder.and_where("amount_msat <= :to_amount"); + params.push((":to_amount", to_amount.to_string())); + } + + if let Some(from_fee) = filter.from_fee_paid_msat { + builder.and_where("fee_paid_msat >= :from_fee"); + params.push((":from_fee", from_fee.to_string())); + } + + if let Some(to_fee) = filter.to_fee_paid_msat { + builder.and_where("fee_paid_msat <= :to_fee"); + params.push((":to_fee", to_fee.to_string())); + } + + if let Some(from_time) = filter.from_timestamp { + builder.and_where("created_at >= :from_time"); + params.push((":from_time", from_time.to_string())); + } + + if let Some(to_time) = filter.to_timestamp { + builder.and_where("created_at <= :to_time"); + params.push((":to_time", to_time.to_string())); + } +} + +fn update_claiming_tx_sql(for_coin: &str) -> Result { + let table_name = channels_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "UPDATE {} SET claiming_tx = ?1, claimed_balance = ?2 WHERE closing_tx = ?3;", + table_name + ); + + Ok(sql) +} + +impl LightningPersister { + /// Initialize a new LightningPersister and set the path to the individual channels' + /// files. + pub fn new( + storage_ticker: String, + main_path: PathBuf, + backup_path: Option, + sqlite_connection: SqliteConnShared, + ) -> Self { + Self { + storage_ticker, + main_path, + backup_path, + sqlite_connection, + } + } + + /// Get the directory which was provided when this persister was initialized. + pub fn main_path(&self) -> PathBuf { self.main_path.clone() } + + /// Get the backup directory which was provided when this persister was initialized. + pub fn backup_path(&self) -> Option { self.backup_path.clone() } + + pub(crate) fn monitor_path(&self) -> PathBuf { + let mut path = self.main_path(); + path.push("monitors"); + path + } + + pub(crate) fn monitor_backup_path(&self) -> Option { + if let Some(mut backup_path) = self.backup_path() { + backup_path.push("monitors"); + return Some(backup_path); + } + None + } + + pub(crate) fn nodes_addresses_path(&self) -> PathBuf { + let mut path = self.main_path(); + path.push("channel_nodes_data"); + path + } + + pub(crate) fn nodes_addresses_backup_path(&self) -> Option { + if let Some(mut backup_path) = self.backup_path() { + backup_path.push("channel_nodes_data"); + return Some(backup_path); + } + None + } + + pub(crate) fn network_graph_path(&self) -> PathBuf { + let mut path = self.main_path(); + path.push("network_graph"); + path + } + + pub(crate) fn scorer_path(&self) -> PathBuf { + let mut path = self.main_path(); + path.push("scorer"); + path + } + + pub fn manager_path(&self) -> PathBuf { + let mut path = self.main_path(); + path.push("manager"); + path + } + + /// Writes the provided `ChannelManager` to the path provided at `LightningPersister` + /// initialization, within a file called "manager". + pub fn persist_manager( + &self, + manager: &ChannelManager, + ) -> Result<(), std::io::Error> + where + M::Target: chain::Watch, + T::Target: BroadcasterInterface, + K::Target: KeysInterface, + F::Target: FeeEstimator, + L::Target: Logger, + { + let path = self.main_path(); + util::write_to_file(path, "manager".to_string(), manager)?; + if let Some(backup_path) = self.backup_path() { + util::write_to_file(backup_path, "manager".to_string(), manager)?; + } + Ok(()) + } + + /// Read `ChannelMonitor`s from disk. + pub fn read_channelmonitors( + &self, + keys_manager: K, + ) -> Result)>, std::io::Error> + where + K::Target: KeysInterface + Sized, + { + let path = self.monitor_path(); + if !Path::new(&path).exists() { + return Ok(Vec::new()); + } + let mut res = Vec::new(); + for file_option in fs::read_dir(path).unwrap() { + let file = file_option.unwrap(); + let owned_file_name = file.file_name(); + let filename = owned_file_name.to_str(); + if filename.is_none() || !filename.unwrap().is_ascii() || filename.unwrap().len() < 65 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid ChannelMonitor file name", + )); + } + if filename.unwrap().ends_with(".tmp") { + // If we were in the middle of committing an new update and crashed, it should be + // safe to ignore the update - we should never have returned to the caller and + // irrevocably committed to the new state in any way. + continue; + } + + let txid = Txid::from_hex(filename.unwrap().split_at(64).0); + if txid.is_err() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid tx ID in filename", + )); + } + + let index = filename.unwrap().split_at(65).1.parse::(); + if index.is_err() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid tx index in filename", + )); + } + + let contents = fs::read(&file.path())?; + let mut buffer = Cursor::new(&contents); + match <(BlockHash, ChannelMonitor)>::read(&mut buffer, &*keys_manager) { + Ok((blockhash, channel_monitor)) => { + if channel_monitor.get_funding_txo().0.txid != txid.unwrap() + || channel_monitor.get_funding_txo().0.index != index.unwrap() + { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "ChannelMonitor was stored in the wrong file", + )); + } + res.push((blockhash, channel_monitor)); + }, + Err(e) => { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to deserialize ChannelMonitor: {}", e), + )) + }, + } + } + Ok(res) + } +} + +impl chainmonitor::Persist for LightningPersister { + // TODO: We really need a way for the persister to inform the user that its time to crash/shut + // down once these start returning failure. + // A PermanentFailure implies we need to shut down since we're force-closing channels without + // even broadcasting! + + fn persist_new_channel( + &self, + funding_txo: OutPoint, + monitor: &ChannelMonitor, + _update_id: chainmonitor::MonitorUpdateId, + ) -> Result<(), chain::ChannelMonitorUpdateErr> { + let filename = format!("{}_{}", funding_txo.txid.to_hex(), funding_txo.index); + util::write_to_file(self.monitor_path(), filename.clone(), monitor) + .map_err(|_| chain::ChannelMonitorUpdateErr::PermanentFailure)?; + if let Some(backup_path) = self.monitor_backup_path() { + util::write_to_file(backup_path, filename, monitor) + .map_err(|_| chain::ChannelMonitorUpdateErr::PermanentFailure)?; + } + Ok(()) + } + + fn update_persisted_channel( + &self, + funding_txo: OutPoint, + _update: &Option, + monitor: &ChannelMonitor, + _update_id: chainmonitor::MonitorUpdateId, + ) -> Result<(), chain::ChannelMonitorUpdateErr> { + let filename = format!("{}_{}", funding_txo.txid.to_hex(), funding_txo.index); + util::write_to_file(self.monitor_path(), filename.clone(), monitor) + .map_err(|_| chain::ChannelMonitorUpdateErr::PermanentFailure)?; + if let Some(backup_path) = self.monitor_backup_path() { + util::write_to_file(backup_path, filename, monitor) + .map_err(|_| chain::ChannelMonitorUpdateErr::PermanentFailure)?; + } + Ok(()) + } +} + +#[async_trait] +impl FileSystemStorage for LightningPersister { + type Error = std::io::Error; + + async fn init_fs(&self) -> Result<(), Self::Error> { + let path = self.main_path(); + let backup_path = self.backup_path(); + async_blocking(move || { + fs::create_dir_all(path.clone())?; + if let Some(path) = backup_path { + fs::create_dir_all(path.clone())?; + check_dir_operations(&path)?; + } + check_dir_operations(&path) + }) + .await + } + + async fn is_fs_initialized(&self) -> Result { + let dir_path = self.main_path(); + let backup_dir_path = self.backup_path(); + async_blocking(move || { + if !dir_path.exists() || backup_dir_path.as_ref().map(|path| !path.exists()).unwrap_or(false) { + Ok(false) + } else if !dir_path.is_dir() { + Err(std::io::Error::new( + std::io::ErrorKind::NotADirectory, + format!("{} is not a directory", dir_path.display()), + )) + } else if backup_dir_path.as_ref().map(|path| !path.is_dir()).unwrap_or(false) { + Err(std::io::Error::new( + std::io::ErrorKind::NotADirectory, + "Backup path is not a directory", + )) + } else { + let check_backup_ops = if let Some(backup_path) = backup_dir_path { + check_dir_operations(&backup_path).is_ok() + } else { + true + }; + check_dir_operations(&dir_path).map(|_| check_backup_ops) + } + }) + .await + } + + async fn get_nodes_addresses(&self) -> Result { + let path = self.nodes_addresses_path(); + if !path.exists() { + return Ok(HashMap::new()); + } + async_blocking(move || { + let file = fs::File::open(path)?; + let reader = BufReader::new(file); + let nodes_addresses: HashMap = + serde_json::from_reader(reader).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + nodes_addresses + .iter() + .map(|(pubkey_str, addr)| { + let pubkey = PublicKey::from_str(pubkey_str) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + Ok((pubkey, *addr)) + }) + .collect() + }) + .await + } + + async fn save_nodes_addresses(&self, nodes_addresses: NodesAddressesMapShared) -> Result<(), Self::Error> { + let path = self.nodes_addresses_path(); + let backup_path = self.nodes_addresses_backup_path(); + async_blocking(move || { + let nodes_addresses: HashMap = nodes_addresses + .lock() + .iter() + .map(|(pubkey, addr)| (pubkey.to_string(), *addr)) + .collect(); + + let file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + serde_json::to_writer(file, &nodes_addresses) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + if let Some(path) = backup_path { + let file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + serde_json::to_writer(file, &nodes_addresses) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + } + + Ok(()) + }) + .await + } + + async fn get_network_graph(&self, network: Network) -> Result { + let path = self.network_graph_path(); + if !path.exists() { + return Ok(NetworkGraph::new(genesis_block(network).header.block_hash())); + } + async_blocking(move || { + let file = fs::File::open(path)?; + common::log::info!("Reading the saved lightning network graph from file, this can take some time!"); + NetworkGraph::read(&mut BufReader::new(file)) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + }) + .await + } + + async fn save_network_graph(&self, network_graph: Arc) -> Result<(), Self::Error> { + let path = self.network_graph_path(); + async_blocking(move || { + let file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + network_graph.write(&mut BufWriter::new(file)) + }) + .await + } + + async fn get_scorer(&self, network_graph: Arc) -> Result { + let path = self.scorer_path(); + if !path.exists() { + return Ok(Scorer::new(ProbabilisticScoringParameters::default(), network_graph)); + } + async_blocking(move || { + let file = fs::File::open(path)?; + Scorer::read( + &mut BufReader::new(file), + (ProbabilisticScoringParameters::default(), network_graph), + ) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + }) + .await + } + + async fn save_scorer(&self, scorer: Arc>) -> Result<(), Self::Error> { + let path = self.scorer_path(); + async_blocking(move || { + let scorer = scorer.lock().unwrap(); + let file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + scorer.write(&mut BufWriter::new(file)) + }) + .await + } +} + +#[async_trait] +impl DbStorage for LightningPersister { + type Error = SqlError; + + async fn init_db(&self) -> Result<(), Self::Error> { + let sqlite_connection = self.sqlite_connection.clone(); + let sql_channels_history = create_channels_history_table_sql(self.storage_ticker.as_str())?; + let sql_payments_history = create_payments_history_table_sql(self.storage_ticker.as_str())?; + async_blocking(move || { + let conn = sqlite_connection.lock().unwrap(); + conn.execute(&sql_channels_history, NO_PARAMS).map(|_| ())?; + conn.execute(&sql_payments_history, NO_PARAMS).map(|_| ())?; + Ok(()) + }) + .await + } + + async fn is_db_initialized(&self) -> Result { + let channels_history_table = channels_history_table(self.storage_ticker.as_str()); + validate_table_name(&channels_history_table)?; + let payments_history_table = payments_history_table(self.storage_ticker.as_str()); + validate_table_name(&payments_history_table)?; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let conn = sqlite_connection.lock().unwrap(); + let channels_history_initialized = + query_single_row(&conn, CHECK_TABLE_EXISTS_SQL, [channels_history_table], string_from_row)?; + let payments_history_initialized = + query_single_row(&conn, CHECK_TABLE_EXISTS_SQL, [payments_history_table], string_from_row)?; + Ok(channels_history_initialized.is_some() && payments_history_initialized.is_some()) + }) + .await + } + + async fn get_last_channel_rpc_id(&self) -> Result { + let sql = get_last_channel_rpc_id_sql(self.storage_ticker.as_str())?; + let sqlite_connection = self.sqlite_connection.clone(); + + async_blocking(move || { + let conn = sqlite_connection.lock().unwrap(); + let count: u32 = conn.query_row(&sql, NO_PARAMS, |r| r.get(0))?; + Ok(count) + }) + .await + } + + async fn add_channel_to_db(&self, details: SqlChannelDetails) -> Result<(), Self::Error> { + let for_coin = self.storage_ticker.clone(); + let rpc_id = details.rpc_id.to_string(); + let channel_id = details.channel_id; + let counterparty_node_id = details.counterparty_node_id; + let is_outbound = (details.is_outbound as i32).to_string(); + let is_public = (details.is_public as i32).to_string(); + let is_closed = (details.is_closed as i32).to_string(); + let created_at = (details.created_at as u32).to_string(); + + let params = [ + rpc_id, + channel_id, + counterparty_node_id, + is_outbound, + is_public, + is_closed, + created_at, + ]; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + sql_transaction.execute(&insert_channel_sql(&for_coin)?, ¶ms)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn add_funding_tx_to_db( + &self, + rpc_id: u64, + funding_tx: String, + funding_value: u64, + funding_generated_in_block: u64, + ) -> Result<(), Self::Error> { + let for_coin = self.storage_ticker.clone(); + let funding_value = funding_value.to_string(); + let funding_generated_in_block = funding_generated_in_block.to_string(); + let rpc_id = rpc_id.to_string(); + + let params = [funding_tx, funding_value, funding_generated_in_block, rpc_id]; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + sql_transaction.execute(&update_funding_tx_sql(&for_coin)?, ¶ms)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn update_funding_tx_block_height(&self, funding_tx: String, block_height: u64) -> Result<(), Self::Error> { + let for_coin = self.storage_ticker.clone(); + let generated_in_block = block_height as u32; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = [&generated_in_block as &dyn ToSql, &funding_tx as &dyn ToSql]; + sql_transaction.execute(&update_funding_tx_block_height_sql(&for_coin)?, ¶ms)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn update_channel_to_closed( + &self, + rpc_id: u64, + closure_reason: String, + closed_at: u64, + ) -> Result<(), Self::Error> { + let for_coin = self.storage_ticker.clone(); + let is_closed = "1".to_string(); + let rpc_id = rpc_id.to_string(); + + let params = [closure_reason, is_closed, closed_at.to_string(), rpc_id]; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + sql_transaction.execute(&update_channel_to_closed_sql(&for_coin)?, ¶ms)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn get_closed_channels_with_no_closing_tx(&self) -> Result, Self::Error> { + let mut builder = get_channels_builder_preimage(self.storage_ticker.as_str())?; + builder.and_where("closing_tx IS NULL"); + add_fields_to_get_channels_sql_builder(&mut builder); + let sql = builder.sql().expect("valid sql"); + let sqlite_connection = self.sqlite_connection.clone(); + + async_blocking(move || { + let conn = sqlite_connection.lock().unwrap(); + + let mut stmt = conn.prepare(&sql)?; + let result = stmt + .query_map_named(&[], channel_details_from_row)? + .collect::>()?; + Ok(result) + }) + .await + } + + async fn add_closing_tx_to_db(&self, rpc_id: u64, closing_tx: String) -> Result<(), Self::Error> { + let for_coin = self.storage_ticker.clone(); + let rpc_id = rpc_id.to_string(); + + let params = [closing_tx, rpc_id]; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + sql_transaction.execute(&update_closing_tx_sql(&for_coin)?, ¶ms)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn add_claiming_tx_to_db( + &self, + closing_tx: String, + claiming_tx: String, + claimed_balance: f64, + ) -> Result<(), Self::Error> { + let for_coin = self.storage_ticker.clone(); + let claimed_balance = claimed_balance.to_string(); + + let params = [claiming_tx, claimed_balance, closing_tx]; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + sql_transaction.execute(&update_claiming_tx_sql(&for_coin)?, ¶ms)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn get_channel_from_db(&self, rpc_id: u64) -> Result, Self::Error> { + let params = [rpc_id.to_string()]; + let sql = select_channel_by_rpc_id_sql(self.storage_ticker.as_str())?; + let sqlite_connection = self.sqlite_connection.clone(); + + async_blocking(move || { + let conn = sqlite_connection.lock().unwrap(); + query_single_row(&conn, &sql, params, channel_details_from_row) + }) + .await + } + + async fn get_closed_channels_by_filter( + &self, + filter: Option, + paging: PagingOptionsEnum, + limit: usize, + ) -> Result { + let mut sql_builder = get_channels_builder_preimage(self.storage_ticker.as_str())?; + let sqlite_connection = self.sqlite_connection.clone(); + + async_blocking(move || { + let conn = sqlite_connection.lock().unwrap(); + + let mut total_builder = sql_builder.clone(); + total_builder.count("id"); + let total_sql = total_builder.sql().expect("valid sql"); + let total: isize = conn.query_row(&total_sql, NO_PARAMS, |row| row.get(0))?; + let total = total.try_into().expect("count should be always above zero"); + + let offset = match paging { + PagingOptionsEnum::PageNumber(page) => (page.get() - 1) * limit, + PagingOptionsEnum::FromId(rpc_id) => { + let params = [rpc_id as u32]; + let maybe_offset = + offset_by_id(&conn, &sql_builder, params, "rpc_id", "closed_at DESC", "rpc_id = ?1")?; + match maybe_offset { + Some(offset) => offset, + None => { + return Ok(GetClosedChannelsResult { + channels: vec![], + skipped: 0, + total, + }) + }, + } + }, + }; + + let mut params = vec![]; + if let Some(f) = filter { + apply_get_channels_filter(&mut sql_builder, &mut params, f); + } + let params_as_trait: Vec<_> = params.iter().map(|(key, value)| (*key, value as &dyn ToSql)).collect(); + add_fields_to_get_channels_sql_builder(&mut sql_builder); + finalize_get_channels_sql_builder(&mut sql_builder, offset, limit); + + let sql = sql_builder.sql().expect("valid sql"); + let mut stmt = conn.prepare(&sql)?; + let channels = stmt + .query_map_named(params_as_trait.as_slice(), channel_details_from_row)? + .collect::>()?; + let result = GetClosedChannelsResult { + channels, + skipped: offset, + total, + }; + Ok(result) + }) + .await + } + + async fn add_or_update_payment_in_db(&self, info: PaymentInfo) -> Result<(), Self::Error> { + let for_coin = self.storage_ticker.clone(); + let payment_hash = hex::encode(info.payment_hash.0); + let (is_outbound, destination) = match info.payment_type { + PaymentType::OutboundPayment { destination } => (true as i32, Some(destination.to_string())), + PaymentType::InboundPayment => (false as i32, None), + }; + let description = info.description; + let preimage = info.preimage.map(|p| hex::encode(p.0)); + let secret = info.secret.map(|s| hex::encode(s.0)); + let amount_msat = info.amt_msat.map(|a| a as u32); + let fee_paid_msat = info.fee_paid_msat.map(|f| f as u32); + let status = info.status.to_string(); + let created_at = info.created_at as u32; + let last_updated = info.last_updated as u32; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let params = [ + &payment_hash as &dyn ToSql, + &destination as &dyn ToSql, + &description as &dyn ToSql, + &preimage as &dyn ToSql, + &secret as &dyn ToSql, + &amount_msat as &dyn ToSql, + &fee_paid_msat as &dyn ToSql, + &is_outbound as &dyn ToSql, + &status as &dyn ToSql, + &created_at as &dyn ToSql, + &last_updated as &dyn ToSql, + ]; + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + sql_transaction.execute(&upsert_payment_sql(&for_coin)?, ¶ms)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn get_payment_from_db(&self, hash: PaymentHash) -> Result, Self::Error> { + let params = [hex::encode(hash.0)]; + let sql = select_payment_by_hash_sql(self.storage_ticker.as_str())?; + let sqlite_connection = self.sqlite_connection.clone(); + + async_blocking(move || { + let conn = sqlite_connection.lock().unwrap(); + query_single_row(&conn, &sql, params, payment_info_from_row) + }) + .await + } + + async fn get_payments_by_filter( + &self, + filter: Option, + paging: PagingOptionsEnum, + limit: usize, + ) -> Result { + let mut sql_builder = get_payments_builder_preimage(self.storage_ticker.as_str())?; + let sqlite_connection = self.sqlite_connection.clone(); + + async_blocking(move || { + let conn = sqlite_connection.lock().unwrap(); + + let mut total_builder = sql_builder.clone(); + total_builder.count("id"); + let total_sql = total_builder.sql().expect("valid sql"); + let total: isize = conn.query_row(&total_sql, NO_PARAMS, |row| row.get(0))?; + let total = total.try_into().expect("count should be always above zero"); + + let offset = match paging { + PagingOptionsEnum::PageNumber(page) => (page.get() - 1) * limit, + PagingOptionsEnum::FromId(hash) => { + let hash_str = hex::encode(hash.0); + let params = [&hash_str]; + let maybe_offset = offset_by_id( + &conn, + &sql_builder, + params, + "payment_hash", + "last_updated DESC", + "payment_hash = ?1", + )?; + match maybe_offset { + Some(offset) => offset, + None => { + return Ok(GetPaymentsResult { + payments: vec![], + skipped: 0, + total, + }) + }, + } + }, + }; + + let mut params = vec![]; + if let Some(f) = filter { + apply_get_payments_filter(&mut sql_builder, &mut params, f); + } + let params_as_trait: Vec<_> = params.iter().map(|(key, value)| (*key, value as &dyn ToSql)).collect(); + finalize_get_payments_sql_builder(&mut sql_builder, offset, limit); + + let sql = sql_builder.sql().expect("valid sql"); + let mut stmt = conn.prepare(&sql)?; + let payments = stmt + .query_map_named(params_as_trait.as_slice(), payment_info_from_row)? + .collect::>()?; + let result = GetPaymentsResult { + payments, + skipped: offset, + total, + }; + Ok(result) + }) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + extern crate bitcoin; + extern crate lightning; + use bitcoin::blockdata::block::{Block, BlockHeader}; + use bitcoin::hashes::hex::FromHex; + use bitcoin::Txid; + use common::{block_on, now_ms}; + use db_common::sqlite::rusqlite::Connection; + use lightning::chain::chainmonitor::Persist; + use lightning::chain::transaction::OutPoint; + use lightning::chain::ChannelMonitorUpdateErr; + use lightning::ln::features::InitFeatures; + use lightning::ln::functional_test_utils::*; + use lightning::util::events::{ClosureReason, MessageSendEventsProvider}; + use lightning::util::test_utils; + use lightning::{check_added_monitors, check_closed_broadcast, check_closed_event}; + use rand::distributions::Alphanumeric; + use rand::{Rng, RngCore}; + use secp256k1::{Secp256k1, SecretKey}; + use std::fs; + use std::num::NonZeroUsize; + use std::path::PathBuf; + use std::sync::{Arc, Mutex}; + + impl Drop for LightningPersister { + fn drop(&mut self) { + // We test for invalid directory names, so it's OK if directory removal + // fails. + match fs::remove_dir_all(&self.main_path) { + Err(e) => println!("Failed to remove test persister directory: {}", e), + _ => {}, + } + } + } + + fn generate_random_channels(num: u64) -> Vec { + let mut rng = rand::thread_rng(); + let mut channels = vec![]; + let s = Secp256k1::new(); + let mut bytes = [0; 32]; + for i in 0..num { + let details = SqlChannelDetails { + rpc_id: i + 1, + channel_id: { + rng.fill_bytes(&mut bytes); + hex::encode(bytes) + }, + counterparty_node_id: { + rng.fill_bytes(&mut bytes); + let secret = SecretKey::from_slice(&bytes).unwrap(); + let pubkey = PublicKey::from_secret_key(&s, &secret); + pubkey.to_string() + }, + funding_tx: { + rng.fill_bytes(&mut bytes); + Some(hex::encode(bytes)) + }, + funding_value: Some(rng.gen::() as u64), + closing_tx: { + rng.fill_bytes(&mut bytes); + Some(hex::encode(bytes)) + }, + closure_reason: { + Some( + rng.sample_iter(&Alphanumeric) + .take(30) + .map(char::from) + .collect::(), + ) + }, + claiming_tx: { + rng.fill_bytes(&mut bytes); + Some(hex::encode(bytes)) + }, + claimed_balance: Some(rng.gen::()), + funding_generated_in_block: Some(rng.gen::() as u64), + is_outbound: rand::random(), + is_public: rand::random(), + is_closed: rand::random(), + created_at: rng.gen::() as u64, + closed_at: Some(rng.gen::() as u64), + }; + channels.push(details); + } + channels + } + + fn generate_random_payments(num: u64) -> Vec { + let mut rng = rand::thread_rng(); + let mut payments = vec![]; + let s = Secp256k1::new(); + let mut bytes = [0; 32]; + for _ in 0..num { + let payment_type = if let 0 = rng.gen::() % 2 { + PaymentType::InboundPayment + } else { + rng.fill_bytes(&mut bytes); + let secret = SecretKey::from_slice(&bytes).unwrap(); + PaymentType::OutboundPayment { + destination: PublicKey::from_secret_key(&s, &secret), + } + }; + let status_rng: u8 = rng.gen(); + let status = if status_rng % 3 == 0 { + HTLCStatus::Succeeded + } else if status_rng % 3 == 1 { + HTLCStatus::Pending + } else { + HTLCStatus::Failed + }; + let description: String = rng.sample_iter(&Alphanumeric).take(30).map(char::from).collect(); + let info = PaymentInfo { + payment_hash: { + rng.fill_bytes(&mut bytes); + PaymentHash(bytes) + }, + payment_type, + description, + preimage: { + rng.fill_bytes(&mut bytes); + Some(PaymentPreimage(bytes)) + }, + secret: { + rng.fill_bytes(&mut bytes); + Some(PaymentSecret(bytes)) + }, + amt_msat: Some(rng.gen::() as u64), + fee_paid_msat: Some(rng.gen::() as u64), + status, + created_at: rng.gen::() as u64, + last_updated: rng.gen::() as u64, + }; + payments.push(info); + } + payments + } + + // Integration-test the LightningPersister. Test relaying a few payments + // and check that the persisted data is updated the appropriate number of + // times. + #[test] + fn test_filesystem_persister() { + // Create the nodes, giving them LightningPersisters for data persisters. + let persister_0 = LightningPersister::new( + "test_filesystem_persister_0".into(), + PathBuf::from("test_filesystem_persister_0"), + None, + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + let persister_1 = LightningPersister::new( + "test_filesystem_persister_1".into(), + PathBuf::from("test_filesystem_persister_1"), + None, + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + let chanmon_cfgs = create_chanmon_cfgs(2); + let mut node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let chain_mon_0 = test_utils::TestChainMonitor::new( + Some(&chanmon_cfgs[0].chain_source), + &chanmon_cfgs[0].tx_broadcaster, + &chanmon_cfgs[0].logger, + &chanmon_cfgs[0].fee_estimator, + &persister_0, + &node_cfgs[0].keys_manager, + ); + let chain_mon_1 = test_utils::TestChainMonitor::new( + Some(&chanmon_cfgs[1].chain_source), + &chanmon_cfgs[1].tx_broadcaster, + &chanmon_cfgs[1].logger, + &chanmon_cfgs[1].fee_estimator, + &persister_1, + &node_cfgs[1].keys_manager, + ); + node_cfgs[0].chain_monitor = chain_mon_0; + node_cfgs[1].chain_monitor = chain_mon_1; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + // Check that the persisted channel data is empty before any channels are + // open. + let mut persisted_chan_data_0 = persister_0.read_channelmonitors(nodes[0].keys_manager).unwrap(); + assert_eq!(persisted_chan_data_0.len(), 0); + let mut persisted_chan_data_1 = persister_1.read_channelmonitors(nodes[1].keys_manager).unwrap(); + assert_eq!(persisted_chan_data_1.len(), 0); + + // Helper to make sure the channel is on the expected update ID. + macro_rules! check_persisted_data { + ($expected_update_id: expr) => { + persisted_chan_data_0 = persister_0.read_channelmonitors(nodes[0].keys_manager).unwrap(); + assert_eq!(persisted_chan_data_0.len(), 1); + for (_, mon) in persisted_chan_data_0.iter() { + assert_eq!(mon.get_latest_update_id(), $expected_update_id); + } + persisted_chan_data_1 = persister_1.read_channelmonitors(nodes[1].keys_manager).unwrap(); + assert_eq!(persisted_chan_data_1.len(), 1); + for (_, mon) in persisted_chan_data_1.iter() { + assert_eq!(mon.get_latest_update_id(), $expected_update_id); + } + }; + } + + // Create some initial channel and check that a channel was persisted. + let _ = create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), InitFeatures::known()); + check_persisted_data!(0); + + // Send a few payments and make sure the monitors are updated to the latest. + send_payment(&nodes[0], &vec![&nodes[1]][..], 8000000); + check_persisted_data!(5); + send_payment(&nodes[1], &vec![&nodes[0]][..], 4000000); + check_persisted_data!(10); + + // Force close because cooperative close doesn't result in any persisted + // updates. + nodes[0] + .node + .force_close_channel(&nodes[0].node.list_channels()[0].channel_id) + .unwrap(); + check_closed_event!(nodes[0], 1, ClosureReason::HolderForceClosed); + check_closed_broadcast!(nodes[0], true); + check_added_monitors!(nodes[0], 1); + + let node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap(); + assert_eq!(node_txn.len(), 1); + + let header = BlockHeader { + version: 0x20000000, + prev_blockhash: nodes[0].best_block_hash(), + merkle_root: Default::default(), + time: 42, + bits: 42, + nonce: 42, + }; + connect_block(&nodes[1], &Block { + header, + txdata: vec![node_txn[0].clone(), node_txn[0].clone()], + }); + check_closed_broadcast!(nodes[1], true); + check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed); + check_added_monitors!(nodes[1], 1); + + // Make sure everything is persisted as expected after close. + check_persisted_data!(11); + } + + // Test that if the persister's path to channel data is read-only, writing a + // monitor to it results in the persister returning a PermanentFailure. + // Windows ignores the read-only flag for folders, so this test is Unix-only. + #[cfg(not(target_os = "windows"))] + #[test] + fn test_readonly_dir_perm_failure() { + let persister = LightningPersister::new( + "test_readonly_dir_perm_failure".into(), + PathBuf::from("test_readonly_dir_perm_failure"), + None, + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + fs::create_dir_all(&persister.main_path).unwrap(); + + // Set up a dummy channel and force close. This will produce a monitor + // that we can then use to test persistence. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let chan = create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), InitFeatures::known()); + nodes[1].node.force_close_channel(&chan.2).unwrap(); + check_closed_event!(nodes[1], 1, ClosureReason::HolderForceClosed); + let mut added_monitors = nodes[1].chain_monitor.added_monitors.lock().unwrap(); + let update_map = nodes[1].chain_monitor.latest_monitor_update_id.lock().unwrap(); + let update_id = update_map.get(&added_monitors[0].0.to_channel_id()).unwrap(); + + // Set the persister's directory to read-only, which should result in + // returning a permanent failure when we then attempt to persist a + // channel update. + let path = &persister.main_path; + let mut perms = fs::metadata(path).unwrap().permissions(); + perms.set_readonly(true); + fs::set_permissions(path, perms).unwrap(); + + let test_txo = OutPoint { + txid: Txid::from_hex("8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be").unwrap(), + index: 0, + }; + match persister.persist_new_channel(test_txo, &added_monitors[0].1, update_id.2) { + Err(ChannelMonitorUpdateErr::PermanentFailure) => {}, + _ => panic!("unexpected result from persisting new channel"), + } + + nodes[1].node.get_and_clear_pending_msg_events(); + added_monitors.clear(); + } + + // Test that if a persister's directory name is invalid, monitor persistence + // will fail. + #[cfg(target_os = "windows")] + #[test] + fn test_fail_on_open() { + // Set up a dummy channel and force close. This will produce a monitor + // that we can then use to test persistence. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let chan = create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), InitFeatures::known()); + nodes[1].node.force_close_channel(&chan.2).unwrap(); + check_closed_event!(nodes[1], 1, ClosureReason::HolderForceClosed); + let mut added_monitors = nodes[1].chain_monitor.added_monitors.lock().unwrap(); + let update_map = nodes[1].chain_monitor.latest_monitor_update_id.lock().unwrap(); + let update_id = update_map.get(&added_monitors[0].0.to_channel_id()).unwrap(); + + // Create the persister with an invalid directory name and test that the + // channel fails to open because the directories fail to be created. There + // don't seem to be invalid filename characters on Unix that Rust doesn't + // handle, hence why the test is Windows-only. + let persister = LightningPersister::new( + "test_fail_on_open".into(), + PathBuf::from(":<>/"), + None, + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + + let test_txo = OutPoint { + txid: Txid::from_hex("8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be").unwrap(), + index: 0, + }; + match persister.persist_new_channel(test_txo, &added_monitors[0].1, update_id.2) { + Err(ChannelMonitorUpdateErr::PermanentFailure) => {}, + _ => panic!("unexpected result from persisting new channel"), + } + + nodes[1].node.get_and_clear_pending_msg_events(); + added_monitors.clear(); + } + + #[test] + fn test_init_sql_collection() { + let persister = LightningPersister::new( + "init_sql_collection".into(), + PathBuf::from("test_filesystem_persister"), + None, + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + let initialized = block_on(persister.is_db_initialized()).unwrap(); + assert!(!initialized); + + block_on(persister.init_db()).unwrap(); + // repetitive init must not fail + block_on(persister.init_db()).unwrap(); + + let initialized = block_on(persister.is_db_initialized()).unwrap(); + assert!(initialized); + } + + #[test] + fn test_add_get_channel_sql() { + let persister = LightningPersister::new( + "add_get_channel".into(), + PathBuf::from("test_filesystem_persister"), + None, + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + + block_on(persister.init_db()).unwrap(); + + let last_channel_rpc_id = block_on(persister.get_last_channel_rpc_id()).unwrap(); + assert_eq!(last_channel_rpc_id, 0); + + let channel = block_on(persister.get_channel_from_db(1)).unwrap(); + assert!(channel.is_none()); + + let mut expected_channel_details = SqlChannelDetails::new( + 1, + [0; 32], + PublicKey::from_str("038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9").unwrap(), + true, + true, + ); + block_on(persister.add_channel_to_db(expected_channel_details.clone())).unwrap(); + let last_channel_rpc_id = block_on(persister.get_last_channel_rpc_id()).unwrap(); + assert_eq!(last_channel_rpc_id, 1); + + let actual_channel_details = block_on(persister.get_channel_from_db(1)).unwrap().unwrap(); + assert_eq!(expected_channel_details, actual_channel_details); + + // must fail because we are adding channel with the same rpc_id + block_on(persister.add_channel_to_db(expected_channel_details.clone())).unwrap_err(); + assert_eq!(last_channel_rpc_id, 1); + + expected_channel_details.rpc_id = 2; + block_on(persister.add_channel_to_db(expected_channel_details.clone())).unwrap(); + let last_channel_rpc_id = block_on(persister.get_last_channel_rpc_id()).unwrap(); + assert_eq!(last_channel_rpc_id, 2); + + block_on(persister.add_funding_tx_to_db( + 2, + "9cdafd6d42dcbdc06b0b5bce1866deb82630581285bbfb56870577300c0a8c6e".into(), + 3000, + 50000, + )) + .unwrap(); + expected_channel_details.funding_tx = + Some("9cdafd6d42dcbdc06b0b5bce1866deb82630581285bbfb56870577300c0a8c6e".into()); + expected_channel_details.funding_value = Some(3000); + expected_channel_details.funding_generated_in_block = Some(50000); + + let actual_channel_details = block_on(persister.get_channel_from_db(2)).unwrap().unwrap(); + assert_eq!(expected_channel_details, actual_channel_details); + + block_on(persister.update_funding_tx_block_height( + "9cdafd6d42dcbdc06b0b5bce1866deb82630581285bbfb56870577300c0a8c6e".into(), + 50001, + )) + .unwrap(); + expected_channel_details.funding_generated_in_block = Some(50001); + + let actual_channel_details = block_on(persister.get_channel_from_db(2)).unwrap().unwrap(); + assert_eq!(expected_channel_details, actual_channel_details); + + let current_time = now_ms() / 1000; + block_on(persister.update_channel_to_closed(2, "the channel was cooperatively closed".into(), current_time)) + .unwrap(); + expected_channel_details.closure_reason = Some("the channel was cooperatively closed".into()); + expected_channel_details.is_closed = true; + expected_channel_details.closed_at = Some(current_time); + + let actual_channel_details = block_on(persister.get_channel_from_db(2)).unwrap().unwrap(); + assert_eq!(expected_channel_details, actual_channel_details); + + let actual_channels = block_on(persister.get_closed_channels_with_no_closing_tx()).unwrap(); + assert_eq!(actual_channels.len(), 1); + + let closed_channels = + block_on(persister.get_closed_channels_by_filter(None, PagingOptionsEnum::default(), 10)).unwrap(); + assert_eq!(closed_channels.channels.len(), 1); + assert_eq!(expected_channel_details, closed_channels.channels[0]); + + block_on(persister.update_channel_to_closed(1, "the channel was cooperatively closed".into(), now_ms() / 1000)) + .unwrap(); + let closed_channels = + block_on(persister.get_closed_channels_by_filter(None, PagingOptionsEnum::default(), 10)).unwrap(); + assert_eq!(closed_channels.channels.len(), 2); + + let actual_channels = block_on(persister.get_closed_channels_with_no_closing_tx()).unwrap(); + assert_eq!(actual_channels.len(), 2); + + block_on(persister.add_closing_tx_to_db( + 2, + "5557df9ad2c9b3c57a4df8b4a7da0b7a6f4e923b4a01daa98bf9e5a3b33e9c8f".into(), + )) + .unwrap(); + expected_channel_details.closing_tx = + Some("5557df9ad2c9b3c57a4df8b4a7da0b7a6f4e923b4a01daa98bf9e5a3b33e9c8f".into()); + + let actual_channels = block_on(persister.get_closed_channels_with_no_closing_tx()).unwrap(); + assert_eq!(actual_channels.len(), 1); + + let actual_channel_details = block_on(persister.get_channel_from_db(2)).unwrap().unwrap(); + assert_eq!(expected_channel_details, actual_channel_details); + + block_on(persister.add_claiming_tx_to_db( + "5557df9ad2c9b3c57a4df8b4a7da0b7a6f4e923b4a01daa98bf9e5a3b33e9c8f".into(), + "97f061634a4a7b0b0c2b95648f86b1c39b95e0cf5073f07725b7143c095b612a".into(), + 2000.333333, + )) + .unwrap(); + expected_channel_details.claiming_tx = + Some("97f061634a4a7b0b0c2b95648f86b1c39b95e0cf5073f07725b7143c095b612a".into()); + expected_channel_details.claimed_balance = Some(2000.333333); + + let actual_channel_details = block_on(persister.get_channel_from_db(2)).unwrap().unwrap(); + assert_eq!(expected_channel_details, actual_channel_details); + } + + #[test] + fn test_add_get_payment_sql() { + let persister = LightningPersister::new( + "add_get_payment".into(), + PathBuf::from("test_filesystem_persister"), + None, + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + + block_on(persister.init_db()).unwrap(); + + let payment = block_on(persister.get_payment_from_db(PaymentHash([0; 32]))).unwrap(); + assert!(payment.is_none()); + + let mut expected_payment_info = PaymentInfo { + payment_hash: PaymentHash([0; 32]), + payment_type: PaymentType::InboundPayment, + description: "test payment".into(), + preimage: Some(PaymentPreimage([2; 32])), + secret: Some(PaymentSecret([3; 32])), + amt_msat: Some(2000), + fee_paid_msat: Some(100), + status: HTLCStatus::Failed, + created_at: now_ms() / 1000, + last_updated: now_ms() / 1000, + }; + block_on(persister.add_or_update_payment_in_db(expected_payment_info.clone())).unwrap(); + + let actual_payment_info = block_on(persister.get_payment_from_db(PaymentHash([0; 32]))) + .unwrap() + .unwrap(); + assert_eq!(expected_payment_info, actual_payment_info); + + expected_payment_info.payment_hash = PaymentHash([1; 32]); + expected_payment_info.payment_type = PaymentType::OutboundPayment { + destination: PublicKey::from_str("038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9") + .unwrap(), + }; + expected_payment_info.secret = None; + expected_payment_info.amt_msat = None; + expected_payment_info.status = HTLCStatus::Succeeded; + expected_payment_info.last_updated = now_ms() / 1000; + block_on(persister.add_or_update_payment_in_db(expected_payment_info.clone())).unwrap(); + + let actual_payment_info = block_on(persister.get_payment_from_db(PaymentHash([1; 32]))) + .unwrap() + .unwrap(); + assert_eq!(expected_payment_info, actual_payment_info); + } + + #[test] + fn test_get_payments_by_filter() { + let persister = LightningPersister::new( + "test_get_payments_by_filter".into(), + PathBuf::from("test_filesystem_persister"), + None, + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + + block_on(persister.init_db()).unwrap(); + + let mut payments = generate_random_payments(100); + + for payment in payments.clone() { + block_on(persister.add_or_update_payment_in_db(payment)).unwrap(); + } + + let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); + let limit = 4; + + let result = block_on(persister.get_payments_by_filter(None, paging, limit)).unwrap(); + + payments.sort_by(|a, b| b.last_updated.cmp(&a.last_updated)); + let expected_payments = &payments[..4].to_vec(); + let actual_payments = &result.payments; + + assert_eq!(0, result.skipped); + assert_eq!(100, result.total); + assert_eq!(expected_payments, actual_payments); + + let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(2).unwrap()); + let limit = 5; + + let result = block_on(persister.get_payments_by_filter(None, paging, limit)).unwrap(); + + let expected_payments = &payments[5..10].to_vec(); + let actual_payments = &result.payments; + + assert_eq!(5, result.skipped); + assert_eq!(100, result.total); + assert_eq!(expected_payments, actual_payments); + + let from_payment_hash = payments[20].payment_hash; + let paging = PagingOptionsEnum::FromId(from_payment_hash); + let limit = 3; + + let result = block_on(persister.get_payments_by_filter(None, paging, limit)).unwrap(); + + let expected_payments = &payments[21..24].to_vec(); + let actual_payments = &result.payments; + + assert_eq!(expected_payments, actual_payments); + + let mut filter = PaymentsFilter { + payment_type: Some(PaymentType::InboundPayment), + description: None, + status: None, + from_amount_msat: None, + to_amount_msat: None, + from_fee_paid_msat: None, + to_fee_paid_msat: None, + from_timestamp: None, + to_timestamp: None, + }; + let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); + let limit = 10; + + let result = block_on(persister.get_payments_by_filter(Some(filter.clone()), paging.clone(), limit)).unwrap(); + let expected_payments_vec: Vec = payments + .iter() + .map(|p| p.clone()) + .filter(|p| p.payment_type == PaymentType::InboundPayment) + .collect(); + let expected_payments = if expected_payments_vec.len() > 10 { + expected_payments_vec[..10].to_vec() + } else { + expected_payments_vec.clone() + }; + let actual_payments = result.payments; + + assert_eq!(expected_payments, actual_payments); + + filter.status = Some(HTLCStatus::Succeeded); + let result = block_on(persister.get_payments_by_filter(Some(filter.clone()), paging.clone(), limit)).unwrap(); + let expected_payments_vec: Vec = expected_payments_vec + .iter() + .map(|p| p.clone()) + .filter(|p| p.status == HTLCStatus::Succeeded) + .collect(); + let expected_payments = if expected_payments_vec.len() > 10 { + expected_payments_vec[..10].to_vec() + } else { + expected_payments_vec + }; + let actual_payments = result.payments; + + assert_eq!(expected_payments, actual_payments); + + let description = &payments[42].description; + let substr = &description[5..10]; + filter.payment_type = None; + filter.status = None; + filter.description = Some(substr.to_string()); + let result = block_on(persister.get_payments_by_filter(Some(filter), paging, limit)).unwrap(); + let expected_payments_vec: Vec = payments + .iter() + .map(|p| p.clone()) + .filter(|p| p.description.contains(&substr)) + .collect(); + let expected_payments = if expected_payments_vec.len() > 10 { + expected_payments_vec[..10].to_vec() + } else { + expected_payments_vec.clone() + }; + let actual_payments = result.payments; + + assert_eq!(expected_payments, actual_payments); + } + + #[test] + fn test_get_channels_by_filter() { + let persister = LightningPersister::new( + "test_get_channels_by_filter".into(), + PathBuf::from("test_filesystem_persister"), + None, + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + + block_on(persister.init_db()).unwrap(); + + let channels = generate_random_channels(100); + + for channel in channels { + block_on(persister.add_channel_to_db(channel.clone())).unwrap(); + block_on(persister.add_funding_tx_to_db( + channel.rpc_id, + channel.funding_tx.unwrap(), + channel.funding_value.unwrap(), + channel.funding_generated_in_block.unwrap(), + )) + .unwrap(); + block_on(persister.update_channel_to_closed(channel.rpc_id, channel.closure_reason.unwrap(), 1655806080)) + .unwrap(); + block_on(persister.add_closing_tx_to_db(channel.rpc_id, channel.closing_tx.clone().unwrap())).unwrap(); + block_on(persister.add_claiming_tx_to_db( + channel.closing_tx.unwrap(), + channel.claiming_tx.unwrap(), + channel.claimed_balance.unwrap(), + )) + .unwrap(); + } + + // get all channels from SQL since updated_at changed from channels generated by generate_random_channels + let channels = block_on(persister.get_closed_channels_by_filter(None, PagingOptionsEnum::default(), 100)) + .unwrap() + .channels; + assert_eq!(100, channels.len()); + + let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); + let limit = 4; + + let result = block_on(persister.get_closed_channels_by_filter(None, paging, limit)).unwrap(); + + let expected_channels = &channels[..4].to_vec(); + let actual_channels = &result.channels; + + assert_eq!(0, result.skipped); + assert_eq!(100, result.total); + assert_eq!(expected_channels, actual_channels); + + let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(2).unwrap()); + let limit = 5; + + let result = block_on(persister.get_closed_channels_by_filter(None, paging, limit)).unwrap(); + + let expected_channels = &channels[5..10].to_vec(); + let actual_channels = &result.channels; + + assert_eq!(5, result.skipped); + assert_eq!(100, result.total); + assert_eq!(expected_channels, actual_channels); + + let from_rpc_id = 20; + let paging = PagingOptionsEnum::FromId(from_rpc_id); + let limit = 3; + + let result = block_on(persister.get_closed_channels_by_filter(None, paging, limit)).unwrap(); + + let expected_channels = channels[20..23].to_vec(); + let actual_channels = result.channels; + + assert_eq!(expected_channels, actual_channels); + + let mut filter = ClosedChannelsFilter { + channel_id: None, + counterparty_node_id: None, + funding_tx: None, + from_funding_value: None, + to_funding_value: None, + closing_tx: None, + closure_reason: None, + claiming_tx: None, + from_claimed_balance: None, + to_claimed_balance: None, + channel_type: Some(ChannelType::Outbound), + channel_visibility: None, + }; + let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); + let limit = 10; + + let result = + block_on(persister.get_closed_channels_by_filter(Some(filter.clone()), paging.clone(), limit)).unwrap(); + let expected_channels_vec: Vec = channels + .iter() + .map(|chan| chan.clone()) + .filter(|chan| chan.is_outbound) + .collect(); + let expected_channels = if expected_channels_vec.len() > 10 { + expected_channels_vec[..10].to_vec() + } else { + expected_channels_vec.clone() + }; + let actual_channels = result.channels; + + assert_eq!(expected_channels, actual_channels); + + filter.channel_visibility = Some(ChannelVisibility::Public); + let result = + block_on(persister.get_closed_channels_by_filter(Some(filter.clone()), paging.clone(), limit)).unwrap(); + let expected_channels_vec: Vec = expected_channels_vec + .iter() + .map(|chan| chan.clone()) + .filter(|chan| chan.is_public) + .collect(); + let expected_channels = if expected_channels_vec.len() > 10 { + expected_channels_vec[..10].to_vec() + } else { + expected_channels_vec + }; + let actual_channels = result.channels; + + assert_eq!(expected_channels, actual_channels); + + let channel_id = channels[42].channel_id.clone(); + filter.channel_type = None; + filter.channel_visibility = None; + filter.channel_id = Some(channel_id.clone()); + let result = block_on(persister.get_closed_channels_by_filter(Some(filter), paging, limit)).unwrap(); + let expected_channels_vec: Vec = channels + .iter() + .map(|chan| chan.clone()) + .filter(|chan| chan.channel_id == channel_id) + .collect(); + let expected_channels = if expected_channels_vec.len() > 10 { + expected_channels_vec[..10].to_vec() + } else { + expected_channels_vec.clone() + }; + let actual_channels = result.channels; + + assert_eq!(expected_channels, actual_channels); + } +} diff --git a/mm2src/coins/lightning_persister/src/storage.rs b/mm2src/coins/lightning_persister/src/storage.rs new file mode 100644 index 0000000000..c0b9ac7e9d --- /dev/null +++ b/mm2src/coins/lightning_persister/src/storage.rs @@ -0,0 +1,274 @@ +use async_trait::async_trait; +use bitcoin::Network; +use common::{now_ms, PagingOptionsEnum}; +use db_common::sqlite::rusqlite::types::FromSqlError; +use derive_more::Display; +use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; +use lightning::routing::network_graph::NetworkGraph; +use lightning::routing::scoring::ProbabilisticScorer; +use parking_lot::Mutex as PaMutex; +use secp256k1::PublicKey; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +pub type NodesAddressesMap = HashMap; +pub type NodesAddressesMapShared = Arc>; +pub type Scorer = ProbabilisticScorer>; +#[async_trait] +pub trait FileSystemStorage { + type Error; + + /// Initializes dirs/collection/tables in storage for a specified coin + async fn init_fs(&self) -> Result<(), Self::Error>; + + async fn is_fs_initialized(&self) -> Result; + + async fn get_nodes_addresses(&self) -> Result, Self::Error>; + + async fn save_nodes_addresses(&self, nodes_addresses: NodesAddressesMapShared) -> Result<(), Self::Error>; + + async fn get_network_graph(&self, network: Network) -> Result; + + async fn save_network_graph(&self, network_graph: Arc) -> Result<(), Self::Error>; + + async fn get_scorer(&self, network_graph: Arc) -> Result; + + async fn save_scorer(&self, scorer: Arc>) -> Result<(), Self::Error>; +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct SqlChannelDetails { + pub rpc_id: u64, + pub channel_id: String, + pub counterparty_node_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub funding_tx: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub funding_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub closing_tx: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub closure_reason: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub claiming_tx: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub claimed_balance: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub funding_generated_in_block: Option, + pub is_outbound: bool, + pub is_public: bool, + pub is_closed: bool, + pub created_at: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub closed_at: Option, +} + +impl SqlChannelDetails { + #[inline] + pub fn new( + rpc_id: u64, + channel_id: [u8; 32], + counterparty_node_id: PublicKey, + is_outbound: bool, + is_public: bool, + ) -> Self { + SqlChannelDetails { + rpc_id, + channel_id: hex::encode(channel_id), + counterparty_node_id: counterparty_node_id.to_string(), + funding_tx: None, + funding_value: None, + funding_generated_in_block: None, + closing_tx: None, + closure_reason: None, + claiming_tx: None, + claimed_balance: None, + is_outbound, + is_public, + is_closed: false, + created_at: now_ms() / 1000, + closed_at: None, + } + } +} + +#[derive(Clone, Deserialize)] +pub enum ChannelType { + Outbound, + Inbound, +} + +#[derive(Clone, Deserialize)] +pub enum ChannelVisibility { + Public, + Private, +} + +#[derive(Clone, Deserialize)] +pub struct ClosedChannelsFilter { + pub channel_id: Option, + pub counterparty_node_id: Option, + pub funding_tx: Option, + pub from_funding_value: Option, + pub to_funding_value: Option, + pub closing_tx: Option, + pub closure_reason: Option, + pub claiming_tx: Option, + pub from_claimed_balance: Option, + pub to_claimed_balance: Option, + pub channel_type: Option, + pub channel_visibility: Option, +} + +pub struct GetClosedChannelsResult { + pub channels: Vec, + pub skipped: usize, + pub total: usize, +} + +#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum HTLCStatus { + Pending, + Succeeded, + Failed, +} + +impl FromStr for HTLCStatus { + type Err = FromSqlError; + + fn from_str(s: &str) -> Result { + match s { + "Pending" => Ok(HTLCStatus::Pending), + "Succeeded" => Ok(HTLCStatus::Succeeded), + "Failed" => Ok(HTLCStatus::Failed), + _ => Err(FromSqlError::InvalidType), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum PaymentType { + OutboundPayment { destination: PublicKey }, + InboundPayment, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PaymentInfo { + pub payment_hash: PaymentHash, + pub payment_type: PaymentType, + pub description: String, + pub preimage: Option, + pub secret: Option, + pub amt_msat: Option, + pub fee_paid_msat: Option, + pub status: HTLCStatus, + pub created_at: u64, + pub last_updated: u64, +} + +#[derive(Clone)] +pub struct PaymentsFilter { + pub payment_type: Option, + pub description: Option, + pub status: Option, + pub from_amount_msat: Option, + pub to_amount_msat: Option, + pub from_fee_paid_msat: Option, + pub to_fee_paid_msat: Option, + pub from_timestamp: Option, + pub to_timestamp: Option, +} + +pub struct GetPaymentsResult { + pub payments: Vec, + pub skipped: usize, + pub total: usize, +} + +#[async_trait] +pub trait DbStorage { + type Error; + + /// Initializes tables in DB. + async fn init_db(&self) -> Result<(), Self::Error>; + + /// Checks if tables have been initialized or not in DB. + async fn is_db_initialized(&self) -> Result; + + /// Gets the last added channel rpc_id. Can be used to deduce the rpc_id for a new channel to be added to DB. + async fn get_last_channel_rpc_id(&self) -> Result; + + /// Inserts a new channel record in the DB. The record's data is completed using add_funding_tx_to_db, + /// add_closing_tx_to_db, add_claiming_tx_to_db when this information is available. + async fn add_channel_to_db(&self, details: SqlChannelDetails) -> Result<(), Self::Error>; + + /// Updates a channel's DB record with the channel's funding transaction information. + async fn add_funding_tx_to_db( + &self, + rpc_id: u64, + funding_tx: String, + funding_value: u64, + funding_generated_in_block: u64, + ) -> Result<(), Self::Error>; + + /// Updates funding_tx_block_height value for a channel in the DB. Should be used to update the block height of + /// the funding tx when the transaction is confirmed on-chain. + async fn update_funding_tx_block_height(&self, funding_tx: String, block_height: u64) -> Result<(), Self::Error>; + + /// Updates the is_closed value for a channel in the DB to 1. + async fn update_channel_to_closed( + &self, + rpc_id: u64, + closure_reason: String, + close_at: u64, + ) -> Result<(), Self::Error>; + + /// Gets the list of closed channels records in the DB with no closing tx hashs saved yet. Can be used to check if + /// the closing tx hash needs to be fetched from the chain and saved to DB when initializing the persister. + async fn get_closed_channels_with_no_closing_tx(&self) -> Result, Self::Error>; + + /// Updates a channel's DB record with the channel's closing transaction hash. + async fn add_closing_tx_to_db(&self, rpc_id: u64, closing_tx: String) -> Result<(), Self::Error>; + + /// Updates a channel's DB record with information about the transaction responsible for claiming the channel's + /// closing balance back to the user's address. + async fn add_claiming_tx_to_db( + &self, + closing_tx: String, + claiming_tx: String, + claimed_balance: f64, + ) -> Result<(), Self::Error>; + + /// Gets a channel record from DB by the channel's rpc_id. + async fn get_channel_from_db(&self, rpc_id: u64) -> Result, Self::Error>; + + /// Gets the list of closed channels that match the provided filter criteria. The number of requested records is + /// specified by the limit parameter, the starting record to list from is specified by the paging parameter. The + /// total number of matched records along with the number of skipped records are also returned in the result. + async fn get_closed_channels_by_filter( + &self, + filter: Option, + paging: PagingOptionsEnum, + limit: usize, + ) -> Result; + + /// Inserts or updates a new payment record in the DB. + async fn add_or_update_payment_in_db(&self, info: PaymentInfo) -> Result<(), Self::Error>; + + /// Gets a payment's record from DB by the payment's hash. + async fn get_payment_from_db(&self, hash: PaymentHash) -> Result, Self::Error>; + + /// Gets the list of payments that match the provided filter criteria. The number of requested records is specified + /// by the limit parameter, the starting record to list from is specified by the paging parameter. The total number + /// of matched records along with the number of skipped records are also returned in the result. + async fn get_payments_by_filter( + &self, + filter: Option, + paging: PagingOptionsEnum, + limit: usize, + ) -> Result; +} diff --git a/mm2src/coins/lightning_persister/src/util.rs b/mm2src/coins/lightning_persister/src/util.rs new file mode 100644 index 0000000000..ac5bc99de5 --- /dev/null +++ b/mm2src/coins/lightning_persister/src/util.rs @@ -0,0 +1,196 @@ +#[cfg(target_os = "windows")] extern crate winapi; + +use std::fs; +use std::path::{Path, PathBuf}; + +#[cfg(not(target_os = "windows"))] +use std::os::unix::io::AsRawFd; + +#[cfg(target_os = "windows")] +use {std::ffi::OsStr, std::os::windows::ffi::OsStrExt}; + +pub(crate) trait DiskWriteable { + fn write_to_file(&self, writer: &mut fs::File) -> Result<(), std::io::Error>; +} + +pub(crate) fn get_full_filepath(mut filepath: PathBuf, filename: String) -> String { + filepath.push(filename); + filepath.to_str().unwrap().to_string() +} + +#[cfg(target_os = "windows")] +macro_rules! call { + ($e: expr) => { + if $e != 0 { + return Ok(()); + } else { + return Err(std::io::Error::last_os_error()); + } + }; +} + +#[cfg(target_os = "windows")] +fn path_to_windows_str>(path: T) -> Vec { + path.as_ref().encode_wide().chain(Some(0)).collect() +} + +#[allow(bare_trait_objects)] +pub(crate) fn write_to_file(path: PathBuf, filename: String, data: &D) -> std::io::Result<()> { + fs::create_dir_all(path.clone())?; + // Do a crazy dance with lots of fsync()s to be overly cautious here... + // We never want to end up in a state where we've lost the old data, or end up using the + // old data on power loss after we've returned. + // The way to atomically write a file on Unix platforms is: + // open(tmpname), write(tmpfile), fsync(tmpfile), close(tmpfile), rename(), fsync(dir) + let filename_with_path = get_full_filepath(path, filename); + let tmp_filename = format!("{}.tmp", filename_with_path); + + { + // Note that going by rust-lang/rust@d602a6b, on MacOS it is only safe to use + // rust stdlib 1.36 or higher. + let mut f = fs::File::create(&tmp_filename)?; + data.write_to_file(&mut f)?; + f.sync_all()?; + } + // Fsync the parent directory on Unix. + #[cfg(not(target_os = "windows"))] + { + fs::rename(&tmp_filename, &filename_with_path)?; + let path = Path::new(&filename_with_path).parent().unwrap(); + let dir_file = fs::OpenOptions::new().read(true).open(path)?; + unsafe { + libc::fsync(dir_file.as_raw_fd()); + } + } + #[cfg(target_os = "windows")] + { + let src = PathBuf::from(tmp_filename); + let dst = PathBuf::from(filename_with_path.clone()); + if Path::new(&filename_with_path).exists() { + unsafe { + winapi::um::winbase::ReplaceFileW( + path_to_windows_str(dst).as_ptr(), + path_to_windows_str(src).as_ptr(), + std::ptr::null(), + winapi::um::winbase::REPLACEFILE_IGNORE_MERGE_ERRORS, + std::ptr::null_mut() as *mut winapi::ctypes::c_void, + std::ptr::null_mut() as *mut winapi::ctypes::c_void, + ) + }; + } else { + call!(unsafe { + winapi::um::winbase::MoveFileExW( + path_to_windows_str(src).as_ptr(), + path_to_windows_str(dst).as_ptr(), + winapi::um::winbase::MOVEFILE_WRITE_THROUGH | winapi::um::winbase::MOVEFILE_REPLACE_EXISTING, + ) + }); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{get_full_filepath, write_to_file, DiskWriteable}; + use std::fs; + use std::io; + use std::io::Write; + use std::path::PathBuf; + + struct TestWriteable {} + impl DiskWriteable for TestWriteable { + fn write_to_file(&self, writer: &mut fs::File) -> Result<(), io::Error> { writer.write_all(&[42; 1]) } + } + + // Test that if the persister's path to channel data is read-only, writing + // data to it fails. Windows ignores the read-only flag for folders, so this + // test is Unix-only. + #[cfg(not(target_os = "windows"))] + #[test] + fn test_readonly_dir() { + let test_writeable = TestWriteable {}; + let filename = "test_readonly_dir_persister_filename".to_string(); + let path = "test_readonly_dir_persister_dir"; + fs::create_dir_all(path.to_string()).unwrap(); + let mut perms = fs::metadata(path.to_string()).unwrap().permissions(); + perms.set_readonly(true); + fs::set_permissions(path.to_string(), perms).unwrap(); + match write_to_file(PathBuf::from(path.to_string()), filename, &test_writeable) { + Err(e) => assert_eq!(e.kind(), io::ErrorKind::PermissionDenied), + _ => panic!("Unexpected error message"), + } + let mut perms = fs::metadata(path.to_string()).unwrap().permissions(); + perms.set_readonly(false); + fs::set_permissions(path.to_string(), perms).unwrap(); + fs::remove_dir_all(path).unwrap(); + } + + // Test failure to rename in the process of atomically creating a channel + // monitor's file. We induce this failure by making the `tmp` file a + // directory. + // Explanation: given "from" = the file being renamed, "to" = the destination + // file that already exists: Unix should fail because if "from" is a file, + // then "to" is also required to be a file. + // TODO: ideally try to make this work on Windows again + #[cfg(not(target_os = "windows"))] + #[test] + fn test_rename_failure() { + let test_writeable = TestWriteable {}; + let filename = "test_rename_failure_filename"; + let path = PathBuf::from("test_rename_failure_dir"); + // Create the channel data file and make it a directory. + fs::create_dir_all(get_full_filepath(path.clone(), filename.to_string())).unwrap(); + match write_to_file(path.clone(), filename.to_string(), &test_writeable) { + Err(e) => assert_eq!(e.raw_os_error(), Some(libc::EISDIR)), + _ => panic!("Unexpected Ok(())"), + } + fs::remove_dir_all(path).unwrap(); + } + + #[test] + fn test_diskwriteable_failure() { + struct FailingWriteable {} + impl DiskWriteable for FailingWriteable { + fn write_to_file(&self, _writer: &mut fs::File) -> Result<(), std::io::Error> { + Err(std::io::Error::new(std::io::ErrorKind::Other, "expected failure")) + } + } + + let filename = "test_diskwriteable_failure"; + let path = PathBuf::from("test_diskwriteable_failure_dir"); + let test_writeable = FailingWriteable {}; + match write_to_file(path.clone(), filename.to_string(), &test_writeable) { + Err(e) => { + assert_eq!(e.kind(), std::io::ErrorKind::Other); + assert_eq!(e.get_ref().unwrap().to_string(), "expected failure"); + }, + _ => panic!("unexpected result"), + } + fs::remove_dir_all(path).unwrap(); + } + + // Test failure to create the temporary file in the persistence process. + // We induce this failure by having the temp file already exist and be a + // directory. + #[test] + fn test_tmp_file_creation_failure() { + let test_writeable = TestWriteable {}; + let filename = "test_tmp_file_creation_failure_filename".to_string(); + let path = PathBuf::from("test_tmp_file_creation_failure_dir"); + + // Create the tmp file and make it a directory. + let tmp_path = get_full_filepath(path.clone(), format!("{}.tmp", filename.clone())); + fs::create_dir_all(tmp_path).unwrap(); + match write_to_file(path.clone(), filename, &test_writeable) { + Err(e) => { + #[cfg(not(target_os = "windows"))] + assert_eq!(e.raw_os_error(), Some(libc::EISDIR)); + #[cfg(target_os = "windows")] + assert_eq!(e.kind(), io::ErrorKind::PermissionDenied); + }, + _ => panic!("Unexpected error message"), + } + fs::remove_dir_all(path).unwrap(); + } +} diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index db9d799cc5..4fed0635b9 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -1,13 +1,15 @@ /****************************************************************************** - * Copyright © 2014-2018 The SuperNET Developers. * + * Copyright © 2022 Atomic Private Limited and its contributors * * * - * See the AUTHORS, DEVELOPER-AGREEMENT and LICENSE files at * + * See the CONTRIBUTOR-LICENSE-AGREEMENT, COPYING, LICENSE-COPYRIGHT-NOTICE * + * and DEVELOPER-CERTIFICATE-OF-ORIGIN files in the LEGAL directory in * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * - * SuperNET software, including this file may be copied, modified, propagated * - * or distributed except according to the terms contained in the LICENSE file * + * AtomicDEX software, including this file may be copied, modified, propagated* + * or distributed except according to the terms contained in the * + * LICENSE-COPYRIGHT-NOTICE file. * * * * Removal or modification of this copyright notice is prohibited. * * * @@ -21,9 +23,9 @@ #![feature(integer_atomics)] #![feature(async_closure)] #![feature(hash_raw_entry)] +#![feature(stmt_expr_attributes)] #[macro_use] extern crate common; -#[macro_use] extern crate fomat_macros; #[macro_use] extern crate gstuff; #[macro_use] extern crate lazy_static; #[macro_use] extern crate serde_derive; @@ -31,29 +33,52 @@ #[macro_use] extern crate ser_error_derive; use async_trait::async_trait; -use bigdecimal::{BigDecimal, ParseBigDecimalError}; -use common::executor::{spawn, Timer}; -use common::mm_ctx::{from_ctx, MmArc, MmWeak}; -use common::mm_error::prelude::*; +use base58::FromBase58Error; use common::mm_metrics::MetricsWeak; -use common::mm_number::MmNumber; -use common::{calc_total_pages, now_ms, HttpStatusCode}; +use common::{calc_total_pages, now_ms, ten, HttpStatusCode}; +use crypto::{Bip32Error, CryptoCtx, DerivationPath}; use derive_more::Display; use futures::compat::Future01CompatExt; -use futures::lock::{MappedMutexGuard as AsyncMappedMutexGuard, Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; +use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use http::{Response, StatusCode}; -use keys::AddressFormat as UtxoAddressFormat; -use rpc::v1::types::Bytes as BytesJson; -use serde::{Deserialize, Deserializer}; +use keys::{AddressFormat as UtxoAddressFormat, KeyPair, NetworkPrefix as CashAddrPrefix}; +use mm2_core::mm_ctx::{from_ctx, MmArc}; +use mm2_err_handle::prelude::*; +use mm2_number::bigdecimal::{BigDecimal, ParseBigDecimalError, Zero}; +use mm2_number::MmNumber; +use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{self as json, Value as Json}; +use std::cmp::Ordering; use std::collections::hash_map::{HashMap, RawEntryMut}; use std::fmt; use std::num::NonZeroUsize; -use std::ops::Deref; +use std::ops::{Add, Deref}; use std::path::PathBuf; +use std::str::FromStr; use std::sync::Arc; +use std::time::Duration; +use utxo_signer::with_key_pair::UtxoSignWithKeyPairError; + +cfg_native! { + use crate::lightning::LightningCoin; + use crate::lightning::ln_conf::PlatformCoinConfirmations; + use async_std::fs; + use futures::AsyncWriteExt; + use std::io; + use zcash_primitives::transaction::Transaction as ZTransaction; + use z_coin::ZcoinConsensusParams; +} + +cfg_wasm32! { + use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; + use hd_wallet_storage::HDWalletDb; + use tx_history_storage::wasm::{clear_tx_history, load_tx_history, save_tx_history, TxHistoryDb}; + + pub type TxHistoryDbLocked<'a> = DbLocked<'a, TxHistoryDb>; +} // using custom copy of try_fus as futures crate was renamed to futures01 macro_rules! try_fus { @@ -74,46 +99,266 @@ macro_rules! try_f { }; } +/// `TransactionErr` compatible `try_fus` macro. +macro_rules! try_tx_fus { + ($e: expr) => { + match $e { + Ok(ok) => ok, + Err(err) => return Box::new(futures01::future::err(crate::TransactionErr::Plain(ERRL!("{:?}", err)))), + } + }; + ($e: expr, $tx: expr) => { + match $e { + Ok(ok) => ok, + Err(err) => { + return Box::new(futures01::future::err(crate::TransactionErr::TxRecoverable( + TransactionEnum::from($tx), + ERRL!("{:?}", err), + ))) + }, + } + }; +} + +/// `TransactionErr` compatible `try_s` macro. +macro_rules! try_tx_s { + ($e: expr) => { + match $e { + Ok(ok) => ok, + Err(err) => { + return Err(crate::TransactionErr::Plain(format!( + "{}:{}] {:?}", + file!(), + line!(), + err + ))) + }, + } + }; + ($e: expr, $tx: expr) => { + match $e { + Ok(ok) => ok, + Err(err) => { + return Err(crate::TransactionErr::TxRecoverable( + TransactionEnum::from($tx), + format!("{}:{}] {:?}", file!(), line!(), err), + )) + }, + } + }; +} + +/// `TransactionErr:Plain` compatible `ERR` macro. +macro_rules! TX_PLAIN_ERR { + ($format: expr, $($args: tt)+) => { Err(crate::TransactionErr::Plain((ERRL!($format, $($args)+)))) }; + ($format: expr) => { Err(crate::TransactionErr::Plain(ERRL!($format))) } +} + +/// `TransactionErr:TxRecoverable` compatible `ERR` macro. +#[allow(unused_macros)] +macro_rules! TX_RECOVERABLE_ERR { + ($tx: expr, $format: expr, $($args: tt)+) => { + Err(crate::TransactionErr::TxRecoverable(TransactionEnum::from($tx), ERRL!($format, $($args)+))) + }; + ($tx: expr, $format: expr) => { + Err(crate::TransactionErr::TxRecoverable(TransactionEnum::from($tx), ERRL!($format))) + }; +} + +macro_rules! ok_or_continue_after_sleep { + ($e:expr, $delay: ident) => { + match $e { + Ok(res) => res, + Err(e) => { + error!("error {:?}", e); + Timer::sleep($delay).await; + continue; + }, + } + }; +} + +#[cfg(not(target_arch = "wasm32"))] +macro_rules! ok_or_retry_after_sleep { + ($e:expr, $delay: ident) => { + loop { + match $e { + Ok(res) => break res, + Err(e) => { + error!("error {:?}", e); + Timer::sleep($delay).await; + continue; + }, + } + } + }; +} + +#[cfg(not(target_arch = "wasm32"))] +macro_rules! ok_or_retry_after_sleep_sync { + ($e:expr, $delay: ident) => { + loop { + match $e { + Ok(res) => break res, + Err(e) => { + error!("error {:?}", e); + std::thread::sleep(core::time::Duration::from_secs($delay)); + continue; + }, + } + } + }; +} + +pub mod coin_balance; #[doc(hidden)] #[cfg(test)] pub mod coins_tests; - pub mod eth; -use eth::{eth_coin_from_conf_and_request, EthCoin, EthTxFeeDetails, SignedEthTx}; - -pub mod utxo; -use utxo::qtum::{self, qtum_coin_from_conf_and_request, QtumCoin}; -use utxo::utxo_common::big_decimal_from_sat_unsigned; -use utxo::utxo_standard::{utxo_standard_coin_from_conf_and_request, UtxoStandardCoin}; -use utxo::{GenerateTxError, UtxoFeeDetails, UtxoTx}; - +pub mod hd_pubkey; +pub mod hd_wallet; +pub mod hd_wallet_storage; +#[cfg(not(target_arch = "wasm32"))] pub mod lightning; +#[cfg_attr(target_arch = "wasm32", allow(dead_code, unused_imports))] +pub mod my_tx_history_v2; pub mod qrc20; -use qrc20::{qrc20_coin_from_conf_and_request, Qrc20Coin, Qrc20FeeDetails}; - +pub mod rpc_command; #[doc(hidden)] #[allow(unused_variables)] pub mod test_coin; +pub mod tx_history_storage; pub use test_coin::TestCoin; -pub mod tx_history_db; -use tx_history_db::{TxHistoryDb, TxHistoryError, TxHistoryOps, TxHistoryResult}; +#[doc(hidden)] +#[allow(unused_variables)] +#[cfg(not(target_arch = "wasm32"))] +pub mod solana; +#[cfg(not(target_arch = "wasm32"))] +pub use solana::spl::SplToken; +#[cfg(not(target_arch = "wasm32"))] +pub use solana::{solana_coin_from_conf_and_params, SolanaActivationParams, SolanaCoin, SolanaFeeDetails}; -#[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] -pub mod z_coin; -use crate::utxo::UnsupportedAddr; -#[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] -use z_coin::{z_coin_from_conf_and_request, ZCoin}; +pub mod utxo; +#[cfg(not(target_arch = "wasm32"))] pub mod z_coin; + +use eth::{eth_coin_from_conf_and_request, EthCoin, EthTxFeeDetails, SignedEthTx}; +use hd_wallet::{HDAddress, HDAddressId}; +use qrc20::Qrc20ActivationParams; +use qrc20::{qrc20_coin_from_conf_and_params, Qrc20Coin, Qrc20FeeDetails}; +use qtum::{Qrc20AddressError, ScriptHashTypeNotSupported}; +use rpc_command::init_create_account::{CreateAccountTaskManager, CreateAccountTaskManagerShared}; +use rpc_command::init_scan_for_new_addresses::{ScanAddressesTaskManager, ScanAddressesTaskManagerShared}; +use rpc_command::init_withdraw::{WithdrawTaskManager, WithdrawTaskManagerShared}; +use utxo::bch::{bch_coin_from_conf_and_params, BchActivationRequest, BchCoin}; +use utxo::qtum::{self, qtum_coin_with_priv_key, QtumCoin}; +use utxo::qtum::{QtumDelegationOps, QtumDelegationRequest, QtumStakingInfosDetails}; +use utxo::rpc_clients::UtxoRpcError; +use utxo::slp::SlpToken; +use utxo::slp::{slp_addr_from_pubkey_str, SlpFeeDetails}; +use utxo::utxo_common::big_decimal_from_sat_unsigned; +use utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; +use utxo::UtxoActivationParams; +use utxo::{BlockchainNetwork, GenerateTxError, UtxoFeeDetails, UtxoTx}; +#[cfg(not(target_arch = "wasm32"))] use z_coin::ZCoin; pub type BalanceResult = Result>; pub type BalanceFut = Box> + Send>; +pub type NonZeroBalanceFut = Box> + Send>; pub type NumConversResult = Result>; +pub type StakingInfosResult = Result>; +pub type StakingInfosFut = Box> + Send>; +pub type DelegationResult = Result>; +pub type DelegationFut = Box> + Send>; pub type WithdrawResult = Result>; pub type WithdrawFut = Box> + Send>; pub type TradePreimageResult = Result>; pub type TradePreimageFut = Box> + Send>; pub type CoinFindResult = Result>; pub type TxHistoryFut = Box> + Send>; -pub type TxHistoryDbLocked<'a> = AsyncMappedMutexGuard<'a, Option, TxHistoryDb>; +pub type TxHistoryResult = Result>; +pub type RawTransactionResult = Result>; +pub type RawTransactionFut<'a> = + Box> + Send + 'a>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum RawTransactionError { + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "Invalid hash: {}", _0)] + InvalidHashError(String), + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Hash does not exist: {}", _0)] + HashNotExist(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for RawTransactionError { + fn status_code(&self) -> StatusCode { + match self { + RawTransactionError::NoSuchCoin { .. } + | RawTransactionError::InvalidHashError(_) + | RawTransactionError::HashNotExist(_) => StatusCode::BAD_REQUEST, + RawTransactionError::Transport(_) | RawTransactionError::InternalError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + }, + } + } +} + +impl From for RawTransactionError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => RawTransactionError::NoSuchCoin { coin }, + } + } +} + +#[derive(Deserialize)] +pub struct RawTransactionRequest { + pub coin: String, + pub tx_hash: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct RawTransactionRes { + /// Raw bytes of signed transaction in hexadecimal string, this should be return hexadecimal encoded signed transaction for get_raw_transaction + pub tx_hex: BytesJson, +} + +pub type SignatureResult = Result>; +pub type VerificationResult = Result>; + +#[derive(Debug, Display)] +pub enum TxHistoryError { + ErrorSerializing(String), + ErrorDeserializing(String), + ErrorSaving(String), + ErrorLoading(String), + ErrorClearing(String), + #[display(fmt = "'internal_id' not found: {:?}", internal_id)] + FromIdNotFound { + internal_id: BytesJson, + }, + NotSupported(String), + InternalError(String), +} + +#[derive(Debug, Display)] +pub enum PrivKeyNotAllowed { + #[display(fmt = "Hardware Wallet is not supported")] + HardwareWalletNotSupported, +} + +#[derive(Debug, Display, PartialEq, Serialize)] +pub enum UnexpectedDerivationMethod { + #[display(fmt = "Iguana private key is unavailable")] + IguanaPrivKeyUnavailable, + #[display(fmt = "HD wallet is unavailable")] + HDWalletUnavailable, +} pub trait Transaction: fmt::Debug + 'static { /// Raw transaction bytes of the transaction @@ -126,9 +371,13 @@ pub trait Transaction: fmt::Debug + 'static { pub enum TransactionEnum { UtxoTx(UtxoTx), SignedEthTx(SignedEthTx), + #[cfg(not(target_arch = "wasm32"))] + ZTransaction(ZTransaction), } ifrom!(TransactionEnum, UtxoTx); ifrom!(TransactionEnum, SignedEthTx); +#[cfg(not(target_arch = "wasm32"))] +ifrom!(TransactionEnum, ZTransaction); // NB: When stable and groked by IDEs, `enum_dispatch` can be used instead of `Deref` to speed things up. impl Deref for TransactionEnum { @@ -137,11 +386,42 @@ impl Deref for TransactionEnum { match self { TransactionEnum::UtxoTx(ref t) => t, TransactionEnum::SignedEthTx(ref t) => t, + #[cfg(not(target_arch = "wasm32"))] + TransactionEnum::ZTransaction(ref t) => t, } } } -pub type TransactionFut = Box + Send>; +#[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] +pub enum TransactionErr { + /// Keeps transactions while throwing errors. + TxRecoverable(TransactionEnum, String), + /// Simply for plain error messages. + Plain(String), +} + +impl TransactionErr { + /// Returns transaction if the error includes it. + #[inline] + pub fn get_tx(&self) -> Option { + match self { + TransactionErr::TxRecoverable(tx, _) => Some(tx.clone()), + _ => None, + } + } + + #[inline] + /// Returns plain text part of error. + pub fn get_plain_text_format(&self) -> String { + match self { + TransactionErr::TxRecoverable(_, err) => err.to_string(), + TransactionErr::Plain(err) => err.to_string(), + } + } +} + +pub type TransactionFut = Box + Send>; #[derive(Debug, PartialEq)] pub enum FoundSwapTxSpend { @@ -164,9 +444,33 @@ pub enum NegotiateSwapContractAddrErr { NoOtherAddrAndNoFallback, } +#[derive(Clone, Debug)] +pub struct ValidatePaymentInput { + pub payment_tx: Vec, + pub time_lock: u32, + pub other_pub: Vec, + pub secret_hash: Vec, + pub amount: BigDecimal, + pub swap_contract_address: Option, + pub try_spv_proof_until: u64, + pub confirmations: u64, + pub unique_swap_data: Vec, +} + +pub struct SearchForSwapTxSpendInput<'a> { + pub time_lock: u32, + pub other_pub: &'a [u8], + pub secret_hash: &'a [u8], + pub tx: &'a [u8], + pub search_from_block: u64, + pub swap_contract_address: &'a Option, + pub swap_unique_data: &'a [u8], +} + /// Swap operations (mostly based on the Hash/Time locked transactions implemented by coin wallets). +#[async_trait] pub trait SwapOps { - fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal) -> TransactionFut; + fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, uuid: &[u8]) -> TransactionFut; fn send_maker_payment( &self, @@ -175,6 +479,7 @@ pub trait SwapOps { secret_hash: &[u8], amount: BigDecimal, swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut; fn send_taker_payment( @@ -184,6 +489,7 @@ pub trait SwapOps { secret_hash: &[u8], amount: BigDecimal, swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut; fn send_maker_spends_taker_payment( @@ -193,6 +499,7 @@ pub trait SwapOps { taker_pub: &[u8], secret: &[u8], swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut; fn send_taker_spends_maker_payment( @@ -202,6 +509,7 @@ pub trait SwapOps { maker_pub: &[u8], secret: &[u8], swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut; fn send_taker_refunds_payment( @@ -211,6 +519,7 @@ pub trait SwapOps { maker_pub: &[u8], secret_hash: &[u8], swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut; fn send_maker_refunds_payment( @@ -220,6 +529,7 @@ pub trait SwapOps { taker_pub: &[u8], secret_hash: &[u8], swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut; fn validate_fee( @@ -229,27 +539,12 @@ pub trait SwapOps { fee_addr: &[u8], amount: &BigDecimal, min_block_number: u64, + uuid: &[u8], ) -> Box + Send>; - fn validate_maker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - priv_bn_hash: &[u8], - amount: BigDecimal, - swap_contract_address: &Option, - ) -> Box + Send>; + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send>; - fn validate_taker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - priv_bn_hash: &[u8], - amount: BigDecimal, - swap_contract_address: &Option, - ) -> Box + Send>; + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send>; fn check_if_my_payment_sent( &self, @@ -258,26 +553,17 @@ pub trait SwapOps { secret_hash: &[u8], search_from_block: u64, swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> Box, Error = String> + Send>; - fn search_for_swap_tx_spend_my( + async fn search_for_swap_tx_spend_my( &self, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - swap_contract_address: &Option, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String>; - fn search_for_swap_tx_spend_other( + async fn search_for_swap_tx_spend_other( &self, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - swap_contract_address: &Option, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String>; fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String>; @@ -299,6 +585,8 @@ pub trait SwapOps { &self, other_side_address: Option<&[u8]>, ) -> Result, MmError>; + + fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair; } /// Operations that coins have independently from the MarketMaker. @@ -308,6 +596,24 @@ pub trait MarketCoinOps { fn my_address(&self) -> Result; + fn get_public_key(&self) -> Result>; + + fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]>; + + fn sign_message(&self, _message: &str) -> SignatureResult; + + fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult; + + fn get_non_zero_balance(&self) -> NonZeroBalanceFut { + let closure = |spendable: BigDecimal| { + if spendable.is_zero() { + return MmError::err(GetNonZeroBalance::BalanceIsZero); + } + Ok(MmNumber::from(spendable)) + }; + Box::new(self.my_spendable_balance().map_err(From::from).and_then(closure)) + } + fn my_balance(&self) -> BalanceFut; fn my_spendable_balance(&self) -> BalanceFut { @@ -316,10 +622,14 @@ pub trait MarketCoinOps { /// Base coin balance for tokens, e.g. ETH balance in ERC20 case fn base_coin_balance(&self) -> BalanceFut; + fn platform_ticker(&self) -> &str; /// Receives raw transaction bytes in hexadecimal format as input and returns tx hash in hexadecimal format fn send_raw_tx(&self, tx: &str) -> Box + Send>; + /// Receives raw transaction bytes as input and returns tx hash in hexadecimal format + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send>; + fn wait_for_confirmations( &self, tx: &[u8], @@ -341,16 +651,18 @@ pub trait MarketCoinOps { fn current_block(&self) -> Box + Send>; - fn display_priv_key(&self) -> String; + fn display_priv_key(&self) -> Result; /// Get the minimum amount to send. fn min_tx_amount(&self) -> BigDecimal; /// Get the minimum amount to trade. fn min_trading_vol(&self) -> MmNumber; + + fn is_privacy(&self) -> bool { false } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, PartialEq)] #[serde(tag = "type")] pub enum WithdrawFee { UtxoFixed { @@ -371,10 +683,51 @@ pub enum WithdrawFee { }, } -#[allow(dead_code)] +pub struct WithdrawSenderAddress { + address: Address, + pubkey: Pubkey, + derivation_path: Option, +} + +impl From> for WithdrawSenderAddress { + fn from(addr: HDAddress) -> Self { + WithdrawSenderAddress { + address: addr.address, + pubkey: addr.pubkey, + derivation_path: Some(addr.derivation_path), + } + } +} + +/// Rename to `GetWithdrawSenderAddresses` when withdraw supports multiple `from` addresses. +#[async_trait] +pub trait GetWithdrawSenderAddress { + type Address; + type Pubkey; + + async fn get_withdraw_sender_address( + &self, + req: &WithdrawRequest, + ) -> MmResult, WithdrawError>; +} + +#[derive(Clone, Deserialize)] +#[serde(untagged)] +pub enum WithdrawFrom { + // AccountId { account_id: u32 }, + AddressId(HDAddressId), + /// Don't use `Bip44DerivationPath` or `RpcDerivationPath` because if there is an error in the path, + /// `serde::Deserialize` returns "data did not match any variant of untagged enum WithdrawFrom". + /// It's better to show the user an informative error. + DerivationPath { + derivation_path: String, + }, +} + #[derive(Deserialize)] pub struct WithdrawRequest { coin: String, + from: Option, to: String, #[serde(default)] amount: BigDecimal, @@ -383,10 +736,67 @@ pub struct WithdrawRequest { fee: Option, } +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum StakingDetails { + Qtum(QtumDelegationRequest), +} + +#[allow(dead_code)] +#[derive(Deserialize)] +pub struct AddDelegateRequest { + pub coin: String, + pub staking_details: StakingDetails, +} + +#[allow(dead_code)] +#[derive(Deserialize)] +pub struct RemoveDelegateRequest { + pub coin: String, +} + +#[derive(Deserialize)] +pub struct GetStakingInfosRequest { + pub coin: String, +} + +#[derive(Serialize, Deserialize)] +pub struct SignatureRequest { + coin: String, + message: String, +} + +#[derive(Serialize, Deserialize)] +pub struct VerificationRequest { + coin: String, + message: String, + signature: String, + address: String, +} + impl WithdrawRequest { + pub fn new( + coin: String, + from: Option, + to: String, + amount: BigDecimal, + max: bool, + fee: Option, + ) -> WithdrawRequest { + WithdrawRequest { + coin, + from, + to, + amount, + max, + fee, + } + } + pub fn new_max(coin: String, to: String) -> WithdrawRequest { WithdrawRequest { coin, + from: None, to, amount: 0.into(), max: true, @@ -395,6 +805,31 @@ impl WithdrawRequest { } } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum StakingInfosDetails { + Qtum(QtumStakingInfosDetails), +} + +impl From for StakingInfosDetails { + fn from(qtum_staking_infos: QtumStakingInfosDetails) -> Self { StakingInfosDetails::Qtum(qtum_staking_infos) } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct StakingInfos { + pub staking_infos_details: StakingInfosDetails, +} + +#[derive(Serialize)] +pub struct SignatureResponse { + signature: String, +} + +#[derive(Serialize)] +pub struct VerificationResponse { + is_valid: bool, +} + /// Please note that no type should have the same structure as another type, /// because this enum has the `untagged` deserialization. #[derive(Clone, Debug, PartialEq, Serialize)] @@ -403,6 +838,9 @@ pub enum TxFeeDetails { Utxo(UtxoFeeDetails), Eth(EthTxFeeDetails), Qrc20(Qrc20FeeDetails), + Slp(SlpFeeDetails), + #[cfg(not(target_arch = "wasm32"))] + Solana(SolanaFeeDetails), } /// Deserialize the TxFeeDetails as an untagged enum. @@ -417,12 +855,16 @@ impl<'de> Deserialize<'de> for TxFeeDetails { Utxo(UtxoFeeDetails), Eth(EthTxFeeDetails), Qrc20(Qrc20FeeDetails), + #[cfg(not(target_arch = "wasm32"))] + Solana(SolanaFeeDetails), } match Deserialize::deserialize(deserializer)? { TxFeeDetailsUnTagged::Utxo(f) => Ok(TxFeeDetails::Utxo(f)), TxFeeDetailsUnTagged::Eth(f) => Ok(TxFeeDetails::Eth(f)), TxFeeDetailsUnTagged::Qrc20(f) => Ok(TxFeeDetails::Qrc20(f)), + #[cfg(not(target_arch = "wasm32"))] + TxFeeDetailsUnTagged::Solana(f) => Ok(TxFeeDetails::Solana(f)), } } } @@ -439,6 +881,11 @@ impl From for TxFeeDetails { fn from(qrc20_details: Qrc20FeeDetails) -> Self { TxFeeDetails::Qrc20(qrc20_details) } } +#[cfg(not(target_arch = "wasm32"))] +impl From for TxFeeDetails { + fn from(solana_details: SolanaFeeDetails) -> Self { TxFeeDetails::Solana(solana_details) } +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct KmdRewardsDetails { amount: BigDecimal, @@ -454,13 +901,25 @@ impl KmdRewardsDetails { } } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum TransactionType { + StakingDelegation, + RemoveDelegation, + StandardTransfer, + TokenTransfer(BytesJson), +} + +impl Default for TransactionType { + fn default() -> Self { TransactionType::StandardTransfer } +} + /// Transaction details #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct TransactionDetails { - /// Raw bytes of signed transaction in hexadecimal string, this should be sent as is to send_raw_transaction RPC to broadcast the transaction + /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction_bytes` RPC to broadcast the transaction pub tx_hex: BytesJson, /// Transaction hash in hexadecimal format - tx_hash: BytesJson, + tx_hash: String, /// Coins are sent from these addresses from: Vec, /// Coins are sent to these addresses @@ -488,6 +947,15 @@ pub struct TransactionDetails { /// Amount of accrued rewards. #[serde(skip_serializing_if = "Option::is_none")] kmd_rewards: Option, + /// Type of transactions, default is StandardTransfer + #[serde(default)] + transaction_type: TransactionType, +} + +#[derive(Clone, Copy, Debug)] +pub struct BlockHeightAndTime { + height: u64, + timestamp: u64, } impl TransactionDetails { @@ -529,12 +997,36 @@ pub struct TradeFee { pub paid_from_trading_vol: bool, } -#[derive(Clone, Debug, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Default, PartialEq, PartialOrd, Serialize)] pub struct CoinBalance { pub spendable: BigDecimal, pub unspendable: BigDecimal, } +impl CoinBalance { + pub fn new(spendable: BigDecimal) -> CoinBalance { + CoinBalance { + spendable, + unspendable: BigDecimal::from(0), + } + } + + pub fn into_total(self) -> BigDecimal { self.spendable + self.unspendable } + + pub fn get_total(&self) -> BigDecimal { &self.spendable + &self.unspendable } +} + +impl Add for CoinBalance { + type Output = CoinBalance; + + fn add(self, rhs: Self) -> Self::Output { + CoinBalance { + spendable: self.spendable + rhs.spendable, + unspendable: self.unspendable + rhs.unspendable, + } + } +} + /// The approximation is needed to cover the dynamic miner fee changing during a swap. #[derive(Clone, Debug)] pub enum FeeApproxStage { @@ -554,7 +1046,7 @@ pub enum TradePreimageValue { UpperBound(BigDecimal), } -#[derive(Debug, Display)] +#[derive(Debug, Display, PartialEq)] pub enum TradePreimageError { #[display( fmt = "Not enough {} to preimage the trade: available {}, required at least {}", @@ -579,6 +1071,10 @@ impl From for TradePreimageError { fn from(e: NumConversError) -> Self { TradePreimageError::InternalError(e.to_string()) } } +impl From for TradePreimageError { + fn from(e: UnexpectedDerivationMethod) -> Self { TradePreimageError::InternalError(e.to_string()) } +} + impl TradePreimageError { /// Construct [`TradePreimageError`] from [`GenerateTxError`] using additional `coin` and `decimals`. pub fn from_generate_tx_error( @@ -666,17 +1162,276 @@ pub enum BalanceError { Transport(String), #[display(fmt = "Invalid response: {}", _0)] InvalidResponse(String), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), + #[display(fmt = "Wallet storage error: {}", _0)] + WalletStorageError(String), #[display(fmt = "Internal: {}", _0)] Internal(String), } +#[derive(Debug, PartialEq, Display)] +pub enum GetNonZeroBalance { + #[display(fmt = "Internal error when retrieving balance")] + MyBalanceError(BalanceError), + #[display(fmt = "Balance is zero")] + BalanceIsZero, +} + +impl From for GetNonZeroBalance { + fn from(e: BalanceError) -> Self { GetNonZeroBalance::MyBalanceError(e) } +} + impl From for BalanceError { fn from(e: NumConversError) -> Self { BalanceError::Internal(e.to_string()) } } +impl From for BalanceError { + fn from(e: UnexpectedDerivationMethod) -> Self { BalanceError::UnexpectedDerivationMethod(e) } +} + +impl From for BalanceError { + fn from(e: Bip32Error) -> Self { BalanceError::Internal(e.to_string()) } +} + #[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] +pub enum StakingInfosError { + #[display(fmt = "Staking infos not available for: {}", coin)] + CoinDoesntSupportStakingInfos { coin: String }, + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "Derivation method is not supported: {}", _0)] + UnexpectedDerivationMethod(String), + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for StakingInfosError { + fn from(e: UtxoRpcError) -> Self { + match e { + UtxoRpcError::Transport(rpc) | UtxoRpcError::ResponseParseError(rpc) => { + StakingInfosError::Transport(rpc.to_string()) + }, + UtxoRpcError::InvalidResponse(error) => StakingInfosError::Transport(error), + UtxoRpcError::Internal(error) => StakingInfosError::Internal(error), + } + } +} + +impl From for StakingInfosError { + fn from(e: UnexpectedDerivationMethod) -> Self { StakingInfosError::UnexpectedDerivationMethod(e.to_string()) } +} + +impl From for StakingInfosError { + fn from(e: Qrc20AddressError) -> Self { + match e { + Qrc20AddressError::UnexpectedDerivationMethod(e) => StakingInfosError::UnexpectedDerivationMethod(e), + Qrc20AddressError::ScriptHashTypeNotSupported { script_hash_type } => { + StakingInfosError::Internal(format!("Script hash type '{}' is not supported", script_hash_type)) + }, + } + } +} + +impl HttpStatusCode for StakingInfosError { + fn status_code(&self) -> StatusCode { + match self { + StakingInfosError::NoSuchCoin { .. } + | StakingInfosError::CoinDoesntSupportStakingInfos { .. } + | StakingInfosError::UnexpectedDerivationMethod(_) => StatusCode::BAD_REQUEST, + StakingInfosError::Transport(_) | StakingInfosError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for StakingInfosError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => StakingInfosError::NoSuchCoin { coin }, + } + } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum DelegationError { + #[display( + fmt = "Not enough {} to delegate: available {}, required at least {}", + coin, + available, + required + )] + NotSufficientBalance { + coin: String, + available: BigDecimal, + required: BigDecimal, + }, + #[display(fmt = "The amount {} is too small, required at least {}", amount, threshold)] + AmountTooLow { amount: BigDecimal, threshold: BigDecimal }, + #[display(fmt = "Delegation not available for: {}", coin)] + CoinDoesntSupportDelegation { coin: String }, + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "{}", _0)] + CannotInteractWithSmartContract(String), + #[display(fmt = "{}", _0)] + AddressError(String), + #[display(fmt = "Already delegating to: {}", _0)] + AlreadyDelegating(String), + #[display(fmt = "Delegation is not supported, reason: {}", reason)] + DelegationOpsNotSupported { reason: String }, + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl From for DelegationError { + fn from(e: UtxoRpcError) -> Self { + match e { + UtxoRpcError::Transport(transport) | UtxoRpcError::ResponseParseError(transport) => { + DelegationError::Transport(transport.to_string()) + }, + UtxoRpcError::InvalidResponse(resp) => DelegationError::Transport(resp), + UtxoRpcError::Internal(internal) => DelegationError::InternalError(internal), + } + } +} + +impl From for DelegationError { + fn from(e: StakingInfosError) -> Self { + match e { + StakingInfosError::CoinDoesntSupportStakingInfos { coin } => { + DelegationError::CoinDoesntSupportDelegation { coin } + }, + StakingInfosError::NoSuchCoin { coin } => DelegationError::NoSuchCoin { coin }, + StakingInfosError::Transport(e) => DelegationError::Transport(e), + StakingInfosError::UnexpectedDerivationMethod(reason) => { + DelegationError::DelegationOpsNotSupported { reason } + }, + StakingInfosError::Internal(e) => DelegationError::InternalError(e), + } + } +} + +impl From for DelegationError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => DelegationError::NoSuchCoin { coin }, + } + } +} + +impl From for DelegationError { + fn from(e: BalanceError) -> Self { + match e { + BalanceError::Transport(error) | BalanceError::InvalidResponse(error) => DelegationError::Transport(error), + BalanceError::UnexpectedDerivationMethod(e) => { + DelegationError::DelegationOpsNotSupported { reason: e.to_string() } + }, + e @ BalanceError::WalletStorageError(_) => DelegationError::InternalError(e.to_string()), + BalanceError::Internal(internal) => DelegationError::InternalError(internal), + } + } +} + +impl From for DelegationError { + fn from(e: UtxoSignWithKeyPairError) -> Self { + let error = format!("Error signing: {}", e); + DelegationError::InternalError(error) + } +} + +impl From for DelegationError { + fn from(e: PrivKeyNotAllowed) -> Self { DelegationError::DelegationOpsNotSupported { reason: e.to_string() } } +} + +impl From for DelegationError { + fn from(e: UnexpectedDerivationMethod) -> Self { + DelegationError::DelegationOpsNotSupported { reason: e.to_string() } + } +} + +impl From for DelegationError { + fn from(e: ScriptHashTypeNotSupported) -> Self { DelegationError::AddressError(e.to_string()) } +} + +impl HttpStatusCode for DelegationError { + fn status_code(&self) -> StatusCode { + match self { + DelegationError::Transport(_) | DelegationError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + _ => StatusCode::BAD_REQUEST, + } + } +} + +impl DelegationError { + pub fn from_generate_tx_error(gen_tx_err: GenerateTxError, coin: String, decimals: u8) -> DelegationError { + match gen_tx_err { + GenerateTxError::EmptyUtxoSet { required } => { + let required = big_decimal_from_sat_unsigned(required, decimals); + DelegationError::NotSufficientBalance { + coin, + available: BigDecimal::from(0), + required, + } + }, + GenerateTxError::EmptyOutputs => DelegationError::InternalError(gen_tx_err.to_string()), + GenerateTxError::OutputValueLessThanDust { value, dust } => { + let amount = big_decimal_from_sat_unsigned(value, decimals); + let threshold = big_decimal_from_sat_unsigned(dust, decimals); + DelegationError::AmountTooLow { amount, threshold } + }, + GenerateTxError::DeductFeeFromOutputFailed { + output_value, required, .. + } => { + let available = big_decimal_from_sat_unsigned(output_value, decimals); + let required = big_decimal_from_sat_unsigned(required, decimals); + DelegationError::NotSufficientBalance { + coin, + available, + required, + } + }, + GenerateTxError::NotEnoughUtxos { sum_utxos, required } => { + let available = big_decimal_from_sat_unsigned(sum_utxos, decimals); + let required = big_decimal_from_sat_unsigned(required, decimals); + DelegationError::NotSufficientBalance { + coin, + available, + required, + } + }, + GenerateTxError::Transport(e) => DelegationError::Transport(e), + GenerateTxError::Internal(e) => DelegationError::InternalError(e), + } + } +} + +#[derive(Clone, Debug, Deserialize, Display, Serialize, SerializeErrorType, PartialEq)] +#[serde(tag = "error_type", content = "error_data")] pub enum WithdrawError { + /* */ + /*------------ Trezor device errors ------------*/ + /* */ + #[display(fmt = "Trezor device disconnected")] + TrezorDisconnected, + #[display(fmt = "Trezor internal error: {}", _0)] + HardwareWalletInternal(String), + #[display(fmt = "No Trezor device available")] + NoTrezorDeviceAvailable, + #[display(fmt = "Unexpected Hardware Wallet device: {}", _0)] + FoundUnexpectedDevice(String), + /* */ + /*------------- WithdrawError -------------*/ + /* */ + #[display( + fmt = "'{}' coin doesn't support 'init_withdraw' yet. Consider using 'withdraw' request instead", + coin + )] + CoinDoesntSupportInitWithdraw { coin: String }, #[display( fmt = "Not enough {} to withdraw: available {}, required at least {}", coin, @@ -698,6 +1453,16 @@ pub enum WithdrawError { InvalidFeePolicy(String), #[display(fmt = "No such coin {}", coin)] NoSuchCoin { coin: String }, + #[display(fmt = "Withdraw timed out {:?}", _0)] + Timeout(Duration), + #[display(fmt = "Unexpected user action. Expected '{}'", expected)] + UnexpectedUserAction { expected: String }, + #[display(fmt = "Request should contain a 'from' address/account")] + FromAddressNotFound, + #[display(fmt = "Unexpected 'from' address: {}", _0)] + UnexpectedFromAddress(String), + #[display(fmt = "Unknown '{}' account", account_id)] + UnknownAccount { account_id: u32 }, #[display(fmt = "Transport error: {}", _0)] Transport(String), #[display(fmt = "Internal error: {}", _0)] @@ -707,13 +1472,24 @@ pub enum WithdrawError { impl HttpStatusCode for WithdrawError { fn status_code(&self) -> StatusCode { match self { - WithdrawError::NotSufficientBalance { .. } + WithdrawError::NoSuchCoin { .. } => StatusCode::NOT_FOUND, + WithdrawError::Timeout(_) => StatusCode::REQUEST_TIMEOUT, + WithdrawError::CoinDoesntSupportInitWithdraw { .. } + | WithdrawError::UnexpectedUserAction { .. } + | WithdrawError::NotSufficientBalance { .. } | WithdrawError::ZeroBalanceToWithdrawMax | WithdrawError::AmountTooLow { .. } | WithdrawError::InvalidAddress(_) | WithdrawError::InvalidFeePolicy(_) - | WithdrawError::NoSuchCoin { .. } => StatusCode::BAD_REQUEST, - WithdrawError::Transport(_) | WithdrawError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + | WithdrawError::FromAddressNotFound + | WithdrawError::UnexpectedFromAddress(_) + | WithdrawError::UnknownAccount { .. } => StatusCode::BAD_REQUEST, + WithdrawError::NoTrezorDeviceAvailable + | WithdrawError::TrezorDisconnected + | WithdrawError::FoundUnexpectedDevice(_) => StatusCode::GONE, + WithdrawError::HardwareWalletInternal(_) + | WithdrawError::Transport(_) + | WithdrawError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } @@ -726,6 +1502,8 @@ impl From for WithdrawError { fn from(e: BalanceError) -> Self { match e { BalanceError::Transport(error) | BalanceError::InvalidResponse(error) => WithdrawError::Transport(error), + BalanceError::UnexpectedDerivationMethod(e) => WithdrawError::from(e), + e @ BalanceError::WalletStorageError(_) => WithdrawError::InternalError(e.to_string()), BalanceError::Internal(internal) => WithdrawError::InternalError(internal), } } @@ -739,8 +1517,19 @@ impl From for WithdrawError { } } -impl From for WithdrawError { - fn from(e: UnsupportedAddr) -> Self { WithdrawError::InvalidAddress(e.to_string()) } +impl From for WithdrawError { + fn from(e: UtxoSignWithKeyPairError) -> Self { + let error = format!("Error signing: {}", e); + WithdrawError::InternalError(error) + } +} + +impl From for WithdrawError { + fn from(e: UnexpectedDerivationMethod) -> Self { WithdrawError::InternalError(e.to_string()) } +} + +impl From for WithdrawError { + fn from(e: PrivKeyNotAllowed) -> Self { WithdrawError::InternalError(e.to_string()) } } impl WithdrawError { @@ -772,22 +1561,126 @@ impl WithdrawError { required, } }, - GenerateTxError::NotEnoughUtxos { sum_utxos, required } => { - let available = big_decimal_from_sat_unsigned(sum_utxos, decimals); - let required = big_decimal_from_sat_unsigned(required, decimals); - WithdrawError::NotSufficientBalance { - coin, - available, - required, - } + GenerateTxError::NotEnoughUtxos { sum_utxos, required } => { + let available = big_decimal_from_sat_unsigned(sum_utxos, decimals); + let required = big_decimal_from_sat_unsigned(required, decimals); + WithdrawError::NotSufficientBalance { + coin, + available, + required, + } + }, + GenerateTxError::Transport(e) => WithdrawError::Transport(e), + GenerateTxError::Internal(e) => WithdrawError::InternalError(e), + } + } +} + +#[derive(Serialize, Display, Debug, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum SignatureError { + #[display(fmt = "Invalid request: {}", _0)] + InvalidRequest(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), + #[display(fmt = "Coin is not found: {}", _0)] + CoinIsNotFound(String), + #[display(fmt = "sign_message_prefix is not set in coin config")] + PrefixNotFound, +} + +impl HttpStatusCode for SignatureError { + fn status_code(&self) -> StatusCode { + match self { + SignatureError::InvalidRequest(_) => StatusCode::BAD_REQUEST, + SignatureError::CoinIsNotFound(_) => StatusCode::BAD_REQUEST, + SignatureError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + SignatureError::PrefixNotFound => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for SignatureError { + fn from(e: keys::Error) -> Self { SignatureError::InternalError(e.to_string()) } +} + +impl From for SignatureError { + fn from(e: ethkey::Error) -> Self { SignatureError::InternalError(e.to_string()) } +} + +impl From for SignatureError { + fn from(e: PrivKeyNotAllowed) -> Self { SignatureError::InternalError(e.to_string()) } +} + +impl From for SignatureError { + fn from(e: CoinFindError) -> Self { SignatureError::CoinIsNotFound(e.to_string()) } +} + +#[derive(Serialize, Display, Debug, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum VerificationError { + #[display(fmt = "Invalid request: {}", _0)] + InvalidRequest(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), + #[display(fmt = "Signature decoding error: {}", _0)] + SignatureDecodingError(String), + #[display(fmt = "Address decoding error: {}", _0)] + AddressDecodingError(String), + #[display(fmt = "Coin is not found: {}", _0)] + CoinIsNotFound(String), + #[display(fmt = "sign_message_prefix is not set in coin config")] + PrefixNotFound, +} + +impl HttpStatusCode for VerificationError { + fn status_code(&self) -> StatusCode { + match self { + VerificationError::InvalidRequest(_) => StatusCode::BAD_REQUEST, + VerificationError::SignatureDecodingError(_) => StatusCode::BAD_REQUEST, + VerificationError::AddressDecodingError(_) => StatusCode::BAD_REQUEST, + VerificationError::CoinIsNotFound(_) => StatusCode::BAD_REQUEST, + VerificationError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + VerificationError::PrefixNotFound => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for VerificationError { + fn from(e: base64::DecodeError) -> Self { VerificationError::SignatureDecodingError(e.to_string()) } +} + +impl From for VerificationError { + fn from(e: hex::FromHexError) -> Self { VerificationError::AddressDecodingError(e.to_string()) } +} + +impl From for VerificationError { + fn from(e: FromBase58Error) -> Self { + match e { + FromBase58Error::InvalidBase58Character(c, _) => { + VerificationError::AddressDecodingError(format!("Invalid Base58 Character: {}", c)) + }, + FromBase58Error::InvalidBase58Length => { + VerificationError::AddressDecodingError(String::from("Invalid Base58 Length")) }, - GenerateTxError::Transport(e) => WithdrawError::Transport(e), - GenerateTxError::Internal(e) => WithdrawError::InternalError(e), } } } +impl From for VerificationError { + fn from(e: keys::Error) -> Self { VerificationError::InternalError(e.to_string()) } +} + +impl From for VerificationError { + fn from(e: ethkey::Error) -> Self { VerificationError::InternalError(e.to_string()) } +} + +impl From for VerificationError { + fn from(e: CoinFindError) -> Self { VerificationError::CoinIsNotFound(e.to_string()) } +} + /// NB: Implementations are expected to follow the pImpl idiom, providing cheap reference-counted cloning and garbage collection. +#[async_trait] pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { // `MmCoin` is an extension fulcrum for something that doesn't fit the `MarketCoinOps`. Practical examples: // name (might be required for some APIs, CoinMarketCap for instance); @@ -805,6 +1698,8 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut; + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut; + /// Maximum number of digits after decimal point used to denominate integer coin units (satoshis, wei, etc.) fn decimals(&self) -> u8; @@ -816,46 +1711,25 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { /// Loop collecting coin transaction history and saving it to local DB fn process_history_loop(&self, ctx: MmArc) -> Box + Send>; + /// Path to tx history file + fn tx_history_path(&self, ctx: &MmArc) -> PathBuf { + let my_address = self.my_address().unwrap_or_default(); + // BCH cash address format has colon after prefix, e.g. bitcoincash: + // Colon can't be used in file names on Windows so it should be escaped + let my_address = my_address.replace(':', "_"); + ctx.dbdir() + .join("TRANSACTIONS") + .join(format!("{}_{}.json", self.ticker(), my_address)) + } + /// Loads existing tx history from file, returns empty vector if file is not found /// Cleans the existing file if deserialization fails fn load_history_from_file(&self, ctx: &MmArc) -> TxHistoryFut> { - let ctx = ctx.clone(); - let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); - let ticker = self.ticker().to_owned(); - let my_address = self.my_address().unwrap_or_default(); - - let fut = async move { - let mut db = coins_ctx.tx_history_db().await?; - let err = match db.load_history(&ticker, &my_address).await { - Ok(history) => return Ok(history), - Err(e) => e, - }; - - if let TxHistoryError::ErrorDeserializing(e) = err.get_inner() { - ctx.log.log( - "🌋", - &[&"tx_history", &ticker.to_owned()], - &ERRL!("Error {} on history deserialization, resetting the cache.", e), - ); - db.clear(&ticker, &my_address).await?; - return Ok(Vec::new()); - } - - Err(err) - }; - Box::new(fut.boxed().compat()) + load_history_from_file_impl(self, ctx) } fn save_history_to_file(&self, ctx: &MmArc, history: Vec) -> TxHistoryFut<()> { - let coins_ctx = CoinsContext::from_ctx(ctx).unwrap(); - let ticker = self.ticker().to_owned(); - let my_address = self.my_address().unwrap_or_default(); - - let fut = async move { - let mut db = coins_ctx.tx_history_db().await?; - db.save_history(&ticker, &my_address, history).await - }; - Box::new(fut.boxed().compat()) + save_history_to_file_impl(self, ctx, history) } /// Transaction history background sync status @@ -865,17 +1739,21 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { fn get_trade_fee(&self) -> Box + Send>; /// Get fee to be paid by sender per whole swap using the sending value and check if the wallet has sufficient balance to pay the fee. - fn get_sender_trade_fee(&self, value: TradePreimageValue, stage: FeeApproxStage) -> TradePreimageFut; + async fn get_sender_trade_fee( + &self, + value: TradePreimageValue, + stage: FeeApproxStage, + ) -> TradePreimageResult; /// Get fee to be paid by receiver per whole swap and check if the wallet has sufficient balance to pay the fee. fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut; /// Get transaction fee the Taker has to pay to send a `TakerFee` transaction and check if the wallet has sufficient balance to pay the fee. - fn get_fee_to_send_taker_fee( + async fn get_fee_to_send_taker_fee( &self, dex_fee_amount: BigDecimal, stage: FeeApproxStage, - ) -> TradePreimageFut; + ) -> TradePreimageResult; /// required transaction confirmations number to ensure double-spend safety fn required_confirmations(&self) -> u64; @@ -903,13 +1781,22 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { } #[derive(Clone, Debug)] +#[allow(clippy::large_enum_variant)] pub enum MmCoinEnum { UtxoCoin(UtxoStandardCoin), QtumCoin(QtumCoin), Qrc20Coin(Qrc20Coin), EthCoin(EthCoin), - #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] + #[cfg(not(target_arch = "wasm32"))] ZCoin(ZCoin), + Bch(BchCoin), + SlpToken(SlpToken), + #[cfg(not(target_arch = "wasm32"))] + SolanaCoin(SolanaCoin), + #[cfg(not(target_arch = "wasm32"))] + SplToken(SplToken), + #[cfg(not(target_arch = "wasm32"))] + LightningCoin(LightningCoin), Test(TestCoin), } @@ -925,6 +1812,16 @@ impl From for MmCoinEnum { fn from(c: TestCoin) -> MmCoinEnum { MmCoinEnum::Test(c) } } +#[cfg(not(target_arch = "wasm32"))] +impl From for MmCoinEnum { + fn from(c: SolanaCoin) -> MmCoinEnum { MmCoinEnum::SolanaCoin(c) } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for MmCoinEnum { + fn from(c: SplToken) -> MmCoinEnum { MmCoinEnum::SplToken(c) } +} + impl From for MmCoinEnum { fn from(coin: QtumCoin) -> Self { MmCoinEnum::QtumCoin(coin) } } @@ -933,7 +1830,20 @@ impl From for MmCoinEnum { fn from(c: Qrc20Coin) -> MmCoinEnum { MmCoinEnum::Qrc20Coin(c) } } -#[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] +impl From for MmCoinEnum { + fn from(c: BchCoin) -> MmCoinEnum { MmCoinEnum::Bch(c) } +} + +impl From for MmCoinEnum { + fn from(c: SlpToken) -> MmCoinEnum { MmCoinEnum::SlpToken(c) } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for MmCoinEnum { + fn from(c: LightningCoin) -> MmCoinEnum { MmCoinEnum::LightningCoin(c) } +} + +#[cfg(not(target_arch = "wasm32"))] impl From for MmCoinEnum { fn from(c: ZCoin) -> MmCoinEnum { MmCoinEnum::ZCoin(c) } } @@ -947,9 +1857,32 @@ impl Deref for MmCoinEnum { MmCoinEnum::QtumCoin(ref c) => c, MmCoinEnum::Qrc20Coin(ref c) => c, MmCoinEnum::EthCoin(ref c) => c, - #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] + MmCoinEnum::Bch(ref c) => c, + MmCoinEnum::SlpToken(ref c) => c, + #[cfg(not(target_arch = "wasm32"))] + MmCoinEnum::LightningCoin(ref c) => c, + #[cfg(not(target_arch = "wasm32"))] MmCoinEnum::ZCoin(ref c) => c, MmCoinEnum::Test(ref c) => c, + #[cfg(not(target_arch = "wasm32"))] + MmCoinEnum::SolanaCoin(ref c) => c, + #[cfg(not(target_arch = "wasm32"))] + MmCoinEnum::SplToken(ref c) => c, + } + } +} + +impl MmCoinEnum { + pub fn is_utxo_in_native_mode(&self) -> bool { + match self { + MmCoinEnum::UtxoCoin(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::QtumCoin(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::Qrc20Coin(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::Bch(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::SlpToken(ref c) => c.as_ref().rpc_client.is_native(), + #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] + MmCoinEnum::ZCoin(ref c) => c.as_ref().rpc_client.is_native(), + _ => false, } } } @@ -959,55 +1892,187 @@ pub trait BalanceTradeFeeUpdatedHandler { async fn balance_updated(&self, coin: &MmCoinEnum, new_balance: &BigDecimal); } -struct CoinsContext { +pub struct CoinsContext { /// A map from a currency ticker symbol to the corresponding coin. /// Similar to `LP_coins`. coins: AsyncMutex>, balance_update_handlers: AsyncMutex>>, - tx_history_path: PathBuf, - /// The database has to be initialized only once! - /// It's better to use something like [`Constructible`], but it doesn't provide a method to get the inner value by the mutable reference. - tx_history_db: AsyncMutex>, + withdraw_task_manager: WithdrawTaskManagerShared, + create_account_manager: CreateAccountTaskManagerShared, + scan_addresses_manager: ScanAddressesTaskManagerShared, + #[cfg(target_arch = "wasm32")] + tx_history_db: SharedDb, + #[cfg(target_arch = "wasm32")] + hd_wallet_db: SharedDb, +} + +#[derive(Debug)] +pub struct CoinIsAlreadyActivatedErr { + pub ticker: String, +} + +#[derive(Debug)] +pub struct PlatformIsAlreadyActivatedErr { + pub ticker: String, } + impl CoinsContext { /// Obtains a reference to this crate context, creating it if necessary. - fn from_ctx(ctx: &MmArc) -> Result, String> { - let tx_history_path = ctx.dbdir().join("TRANSACTIONS"); + pub fn from_ctx(ctx: &MmArc) -> Result, String> { Ok(try_s!(from_ctx(&ctx.coins_ctx, move || { Ok(CoinsContext { coins: AsyncMutex::new(HashMap::new()), balance_update_handlers: AsyncMutex::new(vec![]), - tx_history_path, - tx_history_db: AsyncMutex::new(None), + withdraw_task_manager: WithdrawTaskManager::new_shared(), + create_account_manager: CreateAccountTaskManager::new_shared(), + scan_addresses_manager: ScanAddressesTaskManager::new_shared(), + #[cfg(target_arch = "wasm32")] + tx_history_db: ConstructibleDb::new_shared(ctx), + #[cfg(target_arch = "wasm32")] + hd_wallet_db: ConstructibleDb::new_shared(ctx), }) }))) } + pub async fn add_coin(&self, coin: MmCoinEnum) -> Result<(), MmError> { + let mut coins = self.coins.lock().await; + if coins.contains_key(coin.ticker()) { + return MmError::err(CoinIsAlreadyActivatedErr { + ticker: coin.ticker().into(), + }); + } + + coins.insert(coin.ticker().into(), coin); + Ok(()) + } + + pub async fn add_platform_with_tokens( + &self, + platform: MmCoinEnum, + tokens: Vec, + ) -> Result<(), MmError> { + let mut coins = self.coins.lock().await; + if coins.contains_key(platform.ticker()) { + return MmError::err(PlatformIsAlreadyActivatedErr { + ticker: platform.ticker().into(), + }); + } + + coins.insert(platform.ticker().into(), platform); + + // Tokens can't be activated without platform coin so we can safely insert them without checking prior existence + for token in tokens { + coins.insert(token.ticker().into(), token); + } + Ok(()) + } + + #[cfg(target_arch = "wasm32")] async fn tx_history_db(&self) -> TxHistoryResult> { - /// # Panics - /// - /// This function will `panic!()` if the inner value of the `guard` is `None`. - fn unwrap_tx_history_db(guard: AsyncMutexGuard<'_, Option>) -> TxHistoryDbLocked<'_> { - AsyncMutexGuard::map(guard, |wrapped_db| { - wrapped_db - .as_mut() - .expect("'CoinsContext::tx_history_db' must contain a value") - }) + Ok(self.tx_history_db.get_or_initialize().await?) + } +} + +/// This enum is used in coin activation requests. +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +pub enum PrivKeyActivationPolicy { + IguanaPrivKey, + Trezor, +} + +impl PrivKeyActivationPolicy { + /// The function can be used as a default deserialization constructor: + /// `#[serde(default = "PrivKeyActivationPolicy::iguana_priv_key")]` + pub fn iguana_priv_key() -> PrivKeyActivationPolicy { PrivKeyActivationPolicy::IguanaPrivKey } + + /// The function can be used as a default deserialization constructor: + /// `#[serde(default = "PrivKeyActivationPolicy::trezor")]` + pub fn trezor() -> PrivKeyActivationPolicy { PrivKeyActivationPolicy::Trezor } +} + +#[derive(Debug)] +pub enum PrivKeyPolicy { + KeyPair(T), + Trezor, +} + +impl PrivKeyPolicy { + pub fn key_pair(&self) -> Option<&T> { + match self { + PrivKeyPolicy::KeyPair(key_pair) => Some(key_pair), + PrivKeyPolicy::Trezor => None, + } + } + + pub fn key_pair_or_err(&self) -> Result<&T, MmError> { + self.key_pair() + .or_mm_err(|| PrivKeyNotAllowed::HardwareWalletNotSupported) + } +} + +#[derive(Clone)] +pub enum PrivKeyBuildPolicy<'a> { + IguanaPrivKey(&'a [u8]), + Trezor, +} + +impl<'a> PrivKeyBuildPolicy<'a> { + pub fn iguana_priv_key(crypto_ctx: &'a CryptoCtx) -> Self { + PrivKeyBuildPolicy::IguanaPrivKey(crypto_ctx.iguana_ctx().secp256k1_privkey_bytes()) + } +} + +#[derive(Debug)] +pub enum DerivationMethod { + Iguana(Address), + HDWallet(HDWallet), +} + +impl DerivationMethod { + pub fn iguana(&self) -> Option<&Address> { + match self { + DerivationMethod::Iguana(my_address) => Some(my_address), + DerivationMethod::HDWallet(_) => None, } + } + + pub fn iguana_or_err(&self) -> MmResult<&Address, UnexpectedDerivationMethod> { + self.iguana() + .or_mm_err(|| UnexpectedDerivationMethod::IguanaPrivKeyUnavailable) + } - let mut tx_history_db = self.tx_history_db.lock().await; - if tx_history_db.is_some() { - return Ok(unwrap_tx_history_db(tx_history_db)); + pub fn hd_wallet(&self) -> Option<&HDWallet> { + match self { + DerivationMethod::Iguana(_) => None, + DerivationMethod::HDWallet(hd_wallet) => Some(hd_wallet), } + } + + pub fn hd_wallet_or_err(&self) -> MmResult<&HDWallet, UnexpectedDerivationMethod> { + self.hd_wallet() + .or_mm_err(|| UnexpectedDerivationMethod::HDWalletUnavailable) + } + + /// # Panic + /// + /// Panic if the address mode is [`DerivationMethod::HDWallet`]. + pub fn unwrap_iguana(&self) -> &Address { self.iguana_or_err().unwrap() } +} + +#[async_trait] +pub trait CoinWithDerivationMethod { + type Address; + type HDWallet; - let db = TxHistoryDb::init_with_fs_path(self.tx_history_path.clone()).await?; - *tx_history_db = Some(db); - Ok(unwrap_tx_history_db(tx_history_db)) + fn derivation_method(&self) -> &DerivationMethod; + + fn has_hd_wallet_derivation_method(&self) -> bool { + matches!(self.derivation_method(), DerivationMethod::HDWallet(_)) } } #[allow(clippy::upper_case_acronyms)] -#[derive(Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "type", content = "protocol_data")] pub enum CoinProtocol { UTXO, @@ -1021,8 +2086,33 @@ pub enum CoinProtocol { platform: String, contract_address: String, }, - #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] - ZHTLC, + SLPTOKEN { + platform: String, + token_id: H256Json, + decimals: u8, + required_confirmations: Option, + }, + BCH { + slp_prefix: String, + }, + #[cfg(not(target_arch = "wasm32"))] + LIGHTNING { + platform: String, + network: BlockchainNetwork, + confirmations: PlatformCoinConfirmations, + }, + #[cfg(not(target_arch = "wasm32"))] + SOLANA, + #[cfg(not(target_arch = "wasm32"))] + SPLTOKEN { + platform: String, + token_contract_address: String, + decimals: u8, + }, + #[cfg(not(target_arch = "wasm32"))] + ZHTLC { + consensus_params: ZcoinConsensusParams, + }, } pub type RpcTransportEventHandlerShared = Arc; @@ -1186,11 +2276,15 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result Result Result { - try_s!(utxo_standard_coin_from_conf_and_request(ctx, ticker, &coins_en, req, secret).await).into() + let params = try_s!(UtxoActivationParams::from_legacy_req(req)); + try_s!(utxo_standard_coin_with_priv_key(ctx, ticker, &coins_en, ¶ms, &secret).await).into() + }, + CoinProtocol::QTUM => { + let params = try_s!(UtxoActivationParams::from_legacy_req(req)); + try_s!(qtum_coin_with_priv_key(ctx, ticker, &coins_en, ¶ms, &secret).await).into() }, - CoinProtocol::QTUM => try_s!(qtum_coin_from_conf_and_request(ctx, ticker, &coins_en, req, secret).await).into(), CoinProtocol::ETH | CoinProtocol::ERC20 { .. } => { - try_s!(eth_coin_from_conf_and_request(ctx, ticker, &coins_en, req, secret, protocol).await).into() + try_s!(eth_coin_from_conf_and_request(ctx, ticker, &coins_en, req, &secret, protocol).await).into() }, CoinProtocol::QRC20 { platform, contract_address, } => { + let params = try_s!(Qrc20ActivationParams::from_legacy_req(req)); let contract_address = try_s!(qtum::contract_addr_from_str(contract_address)); + try_s!( - qrc20_coin_from_conf_and_request(ctx, ticker, platform, &coins_en, req, secret, contract_address).await + qrc20_coin_from_conf_and_params(ctx, ticker, platform, &coins_en, ¶ms, &secret, contract_address) + .await ) .into() }, - #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] - CoinProtocol::ZHTLC => try_s!(z_coin_from_conf_and_request(ctx, ticker, &coins_en, req, secret).await).into(), + CoinProtocol::BCH { slp_prefix } => { + let prefix = try_s!(CashAddrPrefix::from_str(slp_prefix)); + let params = try_s!(BchActivationRequest::from_legacy_req(req)); + + let bch = try_s!(bch_coin_from_conf_and_params(ctx, ticker, &coins_en, params, prefix, &secret).await); + bch.into() + }, + CoinProtocol::SLPTOKEN { + platform, + token_id, + decimals, + required_confirmations, + } => { + let platform_coin = try_s!(lp_coinfind(ctx, platform).await); + let platform_coin = match platform_coin { + Some(MmCoinEnum::Bch(coin)) => coin, + Some(_) => return ERR!("Platform coin {} is not BCH", platform), + None => return ERR!("Platform coin {} is not activated", platform), + }; + + let confs = required_confirmations.unwrap_or(platform_coin.required_confirmations()); + let token = SlpToken::new(*decimals, ticker.into(), (*token_id).into(), platform_coin, confs); + token.into() + }, + #[cfg(not(target_arch = "wasm32"))] + CoinProtocol::ZHTLC { .. } => return ERR!("ZHTLC protocol is not supported by lp_coininit"), + #[cfg(not(target_arch = "wasm32"))] + CoinProtocol::LIGHTNING { .. } => return ERR!("Lightning protocol is not supported by lp_coininit"), + #[cfg(not(target_arch = "wasm32"))] + CoinProtocol::SOLANA => { + return ERR!("Solana protocol is not supported by lp_coininit - use enable_solana_with_tokens instead") + }, + #[cfg(not(target_arch = "wasm32"))] + CoinProtocol::SPLTOKEN { .. } => { + return ERR!("SplToken protocol is not supported by lp_coininit - use enable_spl instead") + }, }; - let block_count = try_s!(coin.current_block().compat().await); - // TODO, #156: Warn the user when we know that the wallet is under-initialized. - log! ([=ticker] if !coins_en["etomic"].is_null() {", etomic"} ", " [=block_count]); + let register_params = RegisterCoinParams { + ticker: ticker.to_owned(), + tx_history: req["tx_history"].as_bool().unwrap_or(false), + }; + try_s!(lp_register_coin(ctx, coin.clone(), register_params).await); + Ok(coin) +} + +#[derive(Debug, Display)] +pub enum RegisterCoinError { + #[display(fmt = "Coin '{}' is initialized already", coin)] + CoinIsInitializedAlready { + coin: String, + }, + Internal(String), +} + +pub struct RegisterCoinParams { + pub ticker: String, + pub tx_history: bool, +} + +pub async fn lp_register_coin( + ctx: &MmArc, + coin: MmCoinEnum, + params: RegisterCoinParams, +) -> Result<(), MmError> { + let RegisterCoinParams { ticker, tx_history } = params; + let cctx = CoinsContext::from_ctx(ctx).map_to_mm(RegisterCoinError::Internal)?; + // TODO AP: locking the coins list during the entire initialization prevents different coins from being // activated concurrently which results in long activation time: https://github.com/KomodoPlatform/atomicDEX/issues/24 // So I'm leaving the possibility of race condition intentionally in favor of faster concurrent activation. // Should consider refactoring: maybe extract the RPC client initialization part from coin init functions. let mut coins = cctx.coins.lock().await; - match coins.raw_entry_mut().from_key(ticker) { - RawEntryMut::Occupied(_oe) => return ERR!("Coin {} already initialized", ticker), - RawEntryMut::Vacant(ve) => ve.insert(ticker.to_string(), coin.clone()), + match coins.raw_entry_mut().from_key(&ticker) { + RawEntryMut::Occupied(_oe) => { + return MmError::err(RegisterCoinError::CoinIsInitializedAlready { coin: ticker.clone() }) + }, + RawEntryMut::Vacant(ve) => ve.insert(ticker.clone(), coin.clone()), }; - let history = req["tx_history"].as_bool().unwrap_or(false); - if history { - try_s!(lp_spawn_tx_history(ctx.clone(), coin.clone())); + if tx_history { + lp_spawn_tx_history(ctx.clone(), coin).map_to_mm(RegisterCoinError::Internal)?; } - let ticker = ticker.to_owned(); - let ctx_weak = ctx.weak(); - spawn(async move { check_balance_update_loop(ctx_weak, ticker).await }); - Ok(coin) + Ok(()) } #[cfg(not(target_arch = "wasm32"))] @@ -1287,7 +2450,7 @@ pub async fn find_pair(ctx: &MmArc, base: &str, rel: &str) -> Result, + pub reason: Option, } pub async fn validate_address(ctx: MmArc, req: Json) -> Result>, String> { @@ -1373,6 +2536,72 @@ pub async fn withdraw(ctx: MmArc, req: WithdrawRequest) -> WithdrawResult { coin.withdraw(req).compat().await } +pub async fn get_raw_transaction(ctx: MmArc, req: RawTransactionRequest) -> RawTransactionResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + coin.get_raw_transaction(req).compat().await +} + +pub async fn sign_message(ctx: MmArc, req: SignatureRequest) -> SignatureResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let signature = coin.sign_message(&req.message)?; + Ok(SignatureResponse { signature }) +} + +pub async fn verify_message(ctx: MmArc, req: VerificationRequest) -> VerificationResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + + let validate_address_result = coin.validate_address(&req.address); + if !validate_address_result.is_valid { + return MmError::err(VerificationError::InvalidRequest( + validate_address_result.reason.unwrap_or_else(|| "Unknown".to_string()), + )); + } + + let is_valid = coin.verify_message(&req.signature, &req.message, &req.address)?; + + Ok(VerificationResponse { is_valid }) +} + +pub async fn remove_delegation(ctx: MmArc, req: RemoveDelegateRequest) -> DelegationResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + match coin { + MmCoinEnum::QtumCoin(qtum) => qtum.remove_delegation().compat().await, + _ => { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }) + }, + } +} + +pub async fn get_staking_infos(ctx: MmArc, req: GetStakingInfosRequest) -> StakingInfosResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + match coin { + MmCoinEnum::QtumCoin(qtum) => qtum.get_delegation_infos().compat().await, + _ => { + return MmError::err(StakingInfosError::CoinDoesntSupportStakingInfos { + coin: coin.ticker().to_string(), + }) + }, + } +} + +pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + // Need to find a way to do a proper dispatch + let coin_concrete = match coin { + MmCoinEnum::QtumCoin(qtum) => qtum, + _ => { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }) + }, + }; + match req.staking_details { + StakingDetails::Qtum(qtum_staking) => coin_concrete.add_delegation(qtum_staking).compat().await, + } +} + pub async fn send_raw_transaction(ctx: MmArc, req: Json) -> Result>, String> { let ticker = try_s!(req["coin"].as_str().ok_or("No 'coin' field")).to_owned(); let coin = match lp_coinfind(&ctx, &ticker).await { @@ -1396,8 +2625,6 @@ pub enum HistorySyncState { Finished, } -fn ten() -> usize { 10 } - #[derive(Deserialize)] struct MyTxHistoryRequest { coin: String, @@ -1580,40 +2807,12 @@ pub async fn show_priv_key(ctx: MmArc, req: Json) -> Result>, S let res = try_s!(json::to_vec(&json!({ "result": { "coin": ticker, - "priv_key": coin.display_priv_key(), + "priv_key": try_s!(coin.display_priv_key()), } }))); Ok(try_s!(Response::builder().body(res))) } -// TODO: Refactor this, it's actually not required to check balance and trade fee when there no orders using the coin -pub async fn check_balance_update_loop(ctx: MmWeak, ticker: String) { - let mut current_balance = None; - loop { - Timer::sleep(10.).await; - let ctx = match MmArc::from_weak(&ctx) { - Some(ctx) => ctx, - None => return, - }; - - match lp_coinfind(&ctx, &ticker).await { - Ok(Some(coin)) => { - let balance = match coin.my_spendable_balance().compat().await { - Ok(balance) => balance, - Err(_) => continue, - }; - if Some(&balance) != current_balance.as_ref() { - let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); - coins_ctx.balance_updated(&coin, &balance).await; - current_balance = Some(balance); - } - }, - Ok(None) => break, - Err(_) => continue, - } - } -} - pub async fn register_balance_update_handler( ctx: MmArc, handler: Box, @@ -1680,9 +2879,9 @@ pub async fn convert_utxo_address(ctx: MmArc, req: Json) -> Result utxo, _ => return ERR!("Coin {} is not utxo", req.to_coin), }; - addr.prefix = coin.as_ref().my_address.prefix; - addr.t_addr_prefix = coin.as_ref().my_address.t_addr_prefix; - addr.checksum_type = coin.as_ref().my_address.checksum_type; + addr.prefix = coin.as_ref().conf.pub_addr_prefix; + addr.t_addr_prefix = coin.as_ref().conf.pub_t_addr_prefix; + addr.checksum_type = coin.as_ref().conf.checksum_type; let response = try_s!(json::to_vec(&json!({ "result": addr.to_string(), @@ -1691,6 +2890,7 @@ pub async fn convert_utxo_address(ctx: MmArc, req: Json) -> Result eth::addr_from_pubkey_str(pubkey), - CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } => { + CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format) }, - #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] - CoinProtocol::ZHTLC => utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format), + CoinProtocol::SLPTOKEN { platform, .. } => { + let platform_conf = coin_conf(ctx, &platform); + if platform_conf.is_null() { + return ERR!("platform {} conf is null", platform); + } + // TODO is there any way to make it better without duplicating the prefix in the SLP conf? + let platform_protocol: CoinProtocol = try_s!(json::from_value(platform_conf["protocol"].clone())); + match platform_protocol { + CoinProtocol::BCH { slp_prefix } => { + slp_addr_from_pubkey_str(pubkey, &slp_prefix).map_err(|e| ERRL!("{}", e)) + }, + _ => ERR!("Platform protocol {:?} is not BCH", platform_protocol), + } + }, + #[cfg(not(target_arch = "wasm32"))] + CoinProtocol::LIGHTNING { .. } => { + ERR!("address_by_coin_conf_and_pubkey_str is not implemented for lightning protocol yet!") + }, + #[cfg(not(target_arch = "wasm32"))] + CoinProtocol::SOLANA | CoinProtocol::SPLTOKEN { .. } => { + ERR!("Solana pubkey is the public address - you do not need to use this rpc call.") + }, + #[cfg(not(target_arch = "wasm32"))] + CoinProtocol::ZHTLC { .. } => ERR!("address_by_coin_conf_and_pubkey_str is not supported for ZHTLC protocol!"), + } +} + +#[cfg(target_arch = "wasm32")] +fn load_history_from_file_impl(coin: &T, ctx: &MmArc) -> TxHistoryFut> +where + T: MmCoin + ?Sized, +{ + let ctx = ctx.clone(); + let ticker = coin.ticker().to_owned(); + let my_address = try_f!(coin.my_address().map_to_mm(TxHistoryError::InternalError)); + + let fut = async move { + let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); + let db = coins_ctx.tx_history_db().await?; + let err = match load_tx_history(&db, &ticker, &my_address).await { + Ok(history) => return Ok(history), + Err(e) => e, + }; + + if let TxHistoryError::ErrorDeserializing(e) = err.get_inner() { + ctx.log.log( + "🌋", + &[&"tx_history", &ticker.to_owned()], + &ERRL!("Error {} on history deserialization, resetting the cache.", e), + ); + clear_tx_history(&db, &ticker, &my_address).await?; + return Ok(Vec::new()); + } + + Err(err) + }; + Box::new(fut.boxed().compat()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn load_history_from_file_impl(coin: &T, ctx: &MmArc) -> TxHistoryFut> +where + T: MmCoin + ?Sized, +{ + let ticker = coin.ticker().to_owned(); + let history_path = coin.tx_history_path(ctx); + let ctx = ctx.clone(); + + let fut = async move { + let content = match fs::read(&history_path).await { + Ok(content) => content, + Err(err) if err.kind() == io::ErrorKind::NotFound => { + return Ok(Vec::new()); + }, + Err(err) => { + let error = format!( + "Error '{}' reading from the history file {}", + err, + history_path.display() + ); + return MmError::err(TxHistoryError::ErrorLoading(error)); + }, + }; + let serde_err = match json::from_slice(&content) { + Ok(txs) => return Ok(txs), + Err(e) => e, + }; + + ctx.log.log( + "🌋", + &[&"tx_history", &ticker], + &ERRL!("Error {} on history deserialization, resetting the cache.", serde_err), + ); + fs::remove_file(&history_path) + .await + .map_to_mm(|e| TxHistoryError::ErrorClearing(e.to_string()))?; + Ok(Vec::new()) + }; + Box::new(fut.boxed().compat()) +} + +#[cfg(target_arch = "wasm32")] +fn save_history_to_file_impl(coin: &T, ctx: &MmArc, mut history: Vec) -> TxHistoryFut<()> +where + T: MmCoin + MarketCoinOps + ?Sized, +{ + let ctx = ctx.clone(); + let ticker = coin.ticker().to_owned(); + let my_address = try_f!(coin.my_address().map_to_mm(TxHistoryError::InternalError)); + + history.sort_unstable_by(compare_transactions); + + let fut = async move { + let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); + let db = coins_ctx.tx_history_db().await?; + save_tx_history(&db, &ticker, &my_address, history).await?; + Ok(()) + }; + Box::new(fut.boxed().compat()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn save_history_to_file_impl(coin: &T, ctx: &MmArc, mut history: Vec) -> TxHistoryFut<()> +where + T: MmCoin + MarketCoinOps + ?Sized, +{ + let history_path = coin.tx_history_path(ctx); + let tmp_file = format!("{}.tmp", history_path.display()); + + history.sort_unstable_by(compare_transactions); + + let fut = async move { + let content = json::to_vec(&history).map_to_mm(|e| TxHistoryError::ErrorSerializing(e.to_string()))?; + + let fs_fut = async { + let mut file = fs::File::create(&tmp_file).await?; + file.write_all(&content).await?; + file.flush().await?; + fs::rename(&tmp_file, &history_path).await?; + Ok(()) + }; + + let res: io::Result<_> = fs_fut.await; + if let Err(e) = res { + let error = format!("Error '{}' creating/writing/renaming the tmp file {}", e, tmp_file); + return MmError::err(TxHistoryError::ErrorSaving(error)); + } + Ok(()) + }; + Box::new(fut.boxed().compat()) +} + +fn compare_transactions(a: &TransactionDetails, b: &TransactionDetails) -> Ordering { + // the transactions with block_height == 0 are the most recent so we need to separately handle them while sorting + if a.block_height == b.block_height { + a.internal_id.cmp(&b.internal_id) + } else if a.block_height == 0 { + Ordering::Less + } else if b.block_height == 0 { + Ordering::Greater + } else { + b.block_height.cmp(&a.block_height) } } diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs new file mode 100644 index 0000000000..c297264ab2 --- /dev/null +++ b/mm2src/coins/my_tx_history_v2.rs @@ -0,0 +1,392 @@ +use crate::tx_history_storage::{CreateTxHistoryStorageError, GetTxHistoryFilters, TxHistoryStorageBuilder, WalletId}; +use crate::{lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HistorySyncState, MmCoin, MmCoinEnum, Transaction, + TransactionDetails, TransactionType, TxFeeDetails, UtxoRpcError}; +use async_trait::async_trait; +use bitcrypto::sha256; +use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum, StatusCode}; +use derive_more::Display; +use futures::compat::Future01CompatExt; +use keys::{Address, CashAddress}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::BigDecimal; +use rpc::v1::types::{Bytes as BytesJson, ToTxHash}; +use std::collections::HashSet; + +#[derive(Debug)] +pub enum RemoveTxResult { + TxRemoved, + TxDidNotExist, +} + +impl RemoveTxResult { + pub fn tx_existed(&self) -> bool { matches!(self, RemoveTxResult::TxRemoved) } +} + +pub struct GetHistoryResult { + pub transactions: Vec, + pub skipped: usize, + pub total: usize, +} + +pub trait TxHistoryStorageError: std::fmt::Debug + NotMmError + NotEqual + Send {} + +#[async_trait] +pub trait TxHistoryStorage: Send + Sync + 'static { + type Error: TxHistoryStorageError; + + /// Initializes collection/tables in storage for the specified wallet. + async fn init(&self, wallet_id: &WalletId) -> Result<(), MmError>; + + /// Whether collections/tables are initialized for the specified wallet. + async fn is_initialized_for(&self, wallet_id: &WalletId) -> Result>; + + /// Adds multiple transactions to the selected wallet's history. + /// Also consider adding tx_hex to the cache during this operation. + async fn add_transactions_to_history( + &self, + wallet_id: &WalletId, + transactions: I, + ) -> Result<(), MmError> + where + I: IntoIterator + Send + 'static, + I::IntoIter: Send; + + /// Removes the transaction by internal_id from the selected wallet's history. + async fn remove_tx_from_history( + &self, + wallet_id: &WalletId, + internal_id: &BytesJson, + ) -> Result>; + + /// Gets the transaction by internal_id from the selected wallet's history + async fn get_tx_from_history( + &self, + wallet_id: &WalletId, + internal_id: &BytesJson, + ) -> Result, MmError>; + + /// Returns whether the history contains unconfirmed transactions. + async fn history_contains_unconfirmed_txes(&self, wallet_id: &WalletId) -> Result>; + + /// Gets the unconfirmed transactions from the wallet's history. + async fn get_unconfirmed_txes_from_history( + &self, + wallet_id: &WalletId, + ) -> Result, MmError>; + + /// Updates transaction in the selected wallet's history + async fn update_tx_in_history( + &self, + wallet_id: &WalletId, + tx: &TransactionDetails, + ) -> Result<(), MmError>; + + /// Whether the selected wallet's history contains a transaction with the given `tx_hash`. + async fn history_has_tx_hash(&self, wallet_id: &WalletId, tx_hash: &str) -> Result>; + + /// Returns the number of unique transaction hashes. + async fn unique_tx_hashes_num_in_history(&self, wallet_id: &WalletId) -> Result>; + + /// Adds the given `tx_hex` transaction to the selected wallet's cache. + async fn add_tx_to_cache( + &self, + wallet_id: &WalletId, + tx_hash: &str, + tx_hex: &BytesJson, + ) -> Result<(), MmError>; + + /// Gets transaction hexes from the wallet's cache. + async fn tx_bytes_from_cache( + &self, + wallet_id: &WalletId, + tx_hash: &str, + ) -> Result, MmError>; + + /// Gets transaction history for the selected wallet according to the specified `filters`. + async fn get_history( + &self, + wallet_id: &WalletId, + filters: GetTxHistoryFilters, + paging: PagingOptionsEnum, + limit: usize, + ) -> Result>; +} + +pub trait DisplayAddress { + fn display_address(&self) -> String; +} + +impl DisplayAddress for Address { + fn display_address(&self) -> String { self.to_string() } +} + +impl DisplayAddress for CashAddress { + fn display_address(&self) -> String { self.encode().expect("A valid cash address") } +} + +pub struct TxDetailsBuilder<'a, Addr: DisplayAddress, Tx: Transaction> { + coin: String, + tx: &'a Tx, + my_addresses: HashSet, + total_amount: BigDecimal, + received_by_me: BigDecimal, + spent_by_me: BigDecimal, + from_addresses: HashSet, + to_addresses: HashSet, + transaction_type: TransactionType, + block_height_and_time: Option, + tx_fee: Option, +} + +impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> TxDetailsBuilder<'a, Addr, Tx> { + pub fn new( + coin: String, + tx: &'a Tx, + block_height_and_time: Option, + my_addresses: impl IntoIterator, + ) -> Self { + TxDetailsBuilder { + coin, + tx, + my_addresses: my_addresses.into_iter().collect(), + total_amount: Default::default(), + received_by_me: Default::default(), + spent_by_me: Default::default(), + from_addresses: Default::default(), + to_addresses: Default::default(), + block_height_and_time, + transaction_type: TransactionType::StandardTransfer, + tx_fee: None, + } + } + + pub fn set_tx_fee(&mut self, tx_fee: Option) { self.tx_fee = tx_fee; } + + pub fn set_transaction_type(&mut self, tx_type: TransactionType) { self.transaction_type = tx_type; } + + pub fn transferred_to(&mut self, address: Addr, amount: &BigDecimal) { + if self.my_addresses.contains(&address) { + self.received_by_me += amount; + } + self.to_addresses.insert(address); + } + + pub fn transferred_from(&mut self, address: Addr, amount: &BigDecimal) { + if self.my_addresses.contains(&address) { + self.spent_by_me += amount; + } + self.total_amount += amount; + self.from_addresses.insert(address); + } + + pub fn build(self) -> TransactionDetails { + let (block_height, timestamp) = match self.block_height_and_time { + Some(height_with_time) => (height_with_time.height, height_with_time.timestamp), + None => (0, 0), + }; + + let mut from: Vec<_> = self + .from_addresses + .iter() + .map(DisplayAddress::display_address) + .collect(); + from.sort(); + + let mut to: Vec<_> = self.to_addresses.iter().map(DisplayAddress::display_address).collect(); + to.sort(); + + let tx_hash = self.tx.tx_hash(); + let internal_id = match &self.transaction_type { + TransactionType::TokenTransfer(token_id) => { + let mut bytes_for_hash = tx_hash.0.clone(); + bytes_for_hash.extend_from_slice(&token_id.0); + sha256(&bytes_for_hash).to_vec().into() + }, + TransactionType::StakingDelegation + | TransactionType::RemoveDelegation + | TransactionType::StandardTransfer => tx_hash.clone(), + }; + + TransactionDetails { + coin: self.coin, + tx_hex: self.tx.tx_hex().into(), + tx_hash: tx_hash.to_tx_hash(), + from, + to, + total_amount: self.total_amount, + my_balance_change: &self.received_by_me - &self.spent_by_me, + spent_by_me: self.spent_by_me, + received_by_me: self.received_by_me, + block_height, + timestamp, + fee_details: self.tx_fee, + internal_id, + kmd_rewards: None, + transaction_type: self.transaction_type, + } + } +} + +#[derive(Deserialize)] +pub struct MyTxHistoryRequestV2 { + coin: String, + #[serde(default = "ten")] + pub(crate) limit: usize, + #[serde(default)] + pub(crate) paging_options: PagingOptionsEnum, +} + +#[derive(Serialize)] +pub struct MyTxHistoryDetails { + #[serde(flatten)] + pub(crate) details: TransactionDetails, + pub(crate) confirmations: u64, +} + +#[derive(Serialize)] +pub struct MyTxHistoryResponseV2 { + pub(crate) coin: String, + pub(crate) current_block: u64, + pub(crate) transactions: Vec, + pub(crate) sync_status: HistorySyncState, + pub(crate) limit: usize, + pub(crate) skipped: usize, + pub(crate) total: usize, + pub(crate) total_pages: usize, + pub(crate) paging_options: PagingOptionsEnum, +} + +#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum MyTxHistoryErrorV2 { + CoinIsNotActive(String), + StorageIsNotInitialized(String), + StorageError(String), + RpcError(String), + NotSupportedFor(String), + Internal(String), +} + +impl HttpStatusCode for MyTxHistoryErrorV2 { + fn status_code(&self) -> StatusCode { + match self { + MyTxHistoryErrorV2::CoinIsNotActive(_) => StatusCode::PRECONDITION_REQUIRED, + MyTxHistoryErrorV2::StorageIsNotInitialized(_) + | MyTxHistoryErrorV2::StorageError(_) + | MyTxHistoryErrorV2::RpcError(_) + | MyTxHistoryErrorV2::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + MyTxHistoryErrorV2::NotSupportedFor(_) => StatusCode::BAD_REQUEST, + } + } +} + +impl From for MyTxHistoryErrorV2 { + fn from(err: CoinFindError) -> Self { + match err { + CoinFindError::NoSuchCoin { coin } => MyTxHistoryErrorV2::CoinIsNotActive(coin), + } + } +} + +impl From for MyTxHistoryErrorV2 { + fn from(err: T) -> Self { + let msg = format!("{:?}", err); + MyTxHistoryErrorV2::StorageError(msg) + } +} + +impl From for MyTxHistoryErrorV2 { + fn from(e: CreateTxHistoryStorageError) -> Self { MyTxHistoryErrorV2::StorageError(e.to_string()) } +} + +impl From for MyTxHistoryErrorV2 { + fn from(err: UtxoRpcError) -> Self { MyTxHistoryErrorV2::RpcError(err.to_string()) } +} + +pub trait CoinWithTxHistoryV2 { + fn history_wallet_id(&self) -> WalletId; + + fn get_tx_history_filters(&self) -> GetTxHistoryFilters; +} + +/// According to the [comment](https://github.com/KomodoPlatform/atomicDEX-API/pull/1285#discussion_r888410390), +/// it's worth to add [`MmCoin::my_tx_history_v2`] when most coins support transaction history V2. +pub async fn my_tx_history_v2_rpc( + ctx: MmArc, + request: MyTxHistoryRequestV2, +) -> Result, MmError> { + match lp_coinfind_or_err(&ctx, &request.coin).await? { + MmCoinEnum::Bch(bch) => my_tx_history_v2_impl(ctx, &bch, request).await, + MmCoinEnum::SlpToken(slp_token) => my_tx_history_v2_impl(ctx, &slp_token, request).await, + other => MmError::err(MyTxHistoryErrorV2::NotSupportedFor(other.ticker().to_owned())), + } +} + +pub(crate) async fn my_tx_history_v2_impl( + ctx: MmArc, + coin: &Coin, + request: MyTxHistoryRequestV2, +) -> Result, MmError> +where + Coin: CoinWithTxHistoryV2 + MmCoin, +{ + let tx_history_storage = TxHistoryStorageBuilder::new(&ctx).build()?; + + let wallet_id = coin.history_wallet_id(); + let is_storage_init = tx_history_storage.is_initialized_for(&wallet_id).await?; + if !is_storage_init { + let msg = format!("Storage is not initialized for {:?}", wallet_id); + return MmError::err(MyTxHistoryErrorV2::StorageIsNotInitialized(msg)); + } + let current_block = coin + .current_block() + .compat() + .await + .map_to_mm(MyTxHistoryErrorV2::RpcError)?; + + let filters = coin.get_tx_history_filters(); + let history = tx_history_storage + .get_history(&wallet_id, filters, request.paging_options.clone(), request.limit) + .await?; + + let transactions = history + .transactions + .into_iter() + .map(|mut details| { + // it can be the platform ticker instead of the token ticker for a pre-saved record + if details.coin != request.coin { + details.coin = request.coin.clone(); + } + let confirmations = if details.block_height == 0 || details.block_height > current_block { + 0 + } else { + current_block + 1 - details.block_height + }; + MyTxHistoryDetails { confirmations, details } + }) + .collect(); + + Ok(MyTxHistoryResponseV2 { + coin: request.coin, + current_block, + transactions, + sync_status: coin.history_sync_status(), + limit: request.limit, + skipped: history.skipped, + total: history.total, + total_pages: calc_total_pages(history.total, request.limit), + paging_options: request.paging_options, + }) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn z_coin_tx_history_rpc( + ctx: MmArc, + request: MyTxHistoryRequestV2, +) -> Result, MmError> { + match lp_coinfind_or_err(&ctx, &request.coin).await? { + MmCoinEnum::ZCoin(z_coin) => z_coin.tx_history(request).await, + other => MmError::err(MyTxHistoryErrorV2::NotSupportedFor(other.ticker().to_owned())), + } +} diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 49ec025d4c..4378cf44f4 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -3,60 +3,66 @@ use crate::qrc20::rpc_clients::{LogEntry, Qrc20ElectrumOps, Qrc20NativeOps, Qrc2 ViewContractCallType}; use crate::utxo::qtum::QtumBasedCoin; use crate::utxo::rpc_clients::{ElectrumClient, NativeClient, UnspentInfo, UtxoRpcClientEnum, UtxoRpcClientOps, - UtxoRpcError, UtxoRpcResult}; -use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_inputs_signed_by_pub}; -use crate::utxo::{qtum, sign_tx, ActualTxFee, AdditionalTxData, FeePolicy, GenerateTxError, GenerateTxResult, - HistoryUtxoTx, HistoryUtxoTxMap, RecentlySpentOutPoints, UtxoAddressFormat, UtxoCoinBuilder, - UtxoCoinFields, UtxoCommonOps, UtxoTx, VerboseTransactionFrom, UTXO_LOCK}; + UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; +#[cfg(not(target_arch = "wasm32"))] +use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; +use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilderCommonOps, + UtxoCoinWithIguanaPrivKeyBuilder, UtxoFieldsWithIguanaPrivKeyBuilder}; +use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_inputs_signed_by_pub, UtxoTxBuilder}; +use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, BroadcastTxErr, FeePolicy, GenerateTxError, GetUtxoListOps, + HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, + UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFromLegacyReqErr, + UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; use crate::{BalanceError, BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, - MmCoin, NegotiateSwapContractAddrErr, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionFut, - ValidateAddressResult, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; + MmCoin, NegotiateSwapContractAddrErr, PrivKeyNotAllowed, RawTransactionFut, RawTransactionRequest, + SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, + TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, + TransactionFut, TransactionType, UnexpectedDerivationMethod, ValidateAddressResult, ValidatePaymentInput, + VerificationResult, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; -use bigdecimal::BigDecimal; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; -use common::block_on; use common::executor::Timer; -use common::jsonrpc_client::{JsonRpcClient, JsonRpcError, JsonRpcRequest, RpcRes}; +use common::jsonrpc_client::{JsonRpcClient, JsonRpcRequest, RpcRes}; use common::log::{error, warn}; -use common::mm_ctx::MmArc; -use common::mm_error::prelude::*; -use common::mm_number::MmNumber; use common::now_ms; use derive_more::Display; use ethabi::{Function, Token}; use ethereum_types::{H160, U256}; use futures::compat::Future01CompatExt; -use futures::lock::MutexGuard as AsyncMutexGuard; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use keys::bytes::Bytes as ScriptBytes; -use keys::{Address as UtxoAddress, Address, Public}; +use keys::{Address as UtxoAddress, Address, KeyPair, Public}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; -use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H160 as H160Json, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, ToTxHash, Transaction as RpcTransaction, H160 as H160Json, H256 as H256Json}; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use script_pubkey::generate_contract_call_script_pubkey; use serde_json::{self as json, Value as Json}; use serialization::{deserialize, serialize, CoinVariant}; +use std::collections::{HashMap, HashSet}; use std::ops::{Deref, Neg}; #[cfg(not(target_arch = "wasm32"))] use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; +use utxo_signer::with_key_pair::{sign_tx, UtxoSignWithKeyPairError}; mod history; #[cfg(test)] mod qrc20_tests; pub mod rpc_clients; -mod script_pubkey; +pub mod script_pubkey; mod swap; /// Qtum amount is always 0 for the QRC20 UTXO outputs, /// because we should pay only a fee in Qtum to send the QRC20 transaction. -const OUTPUT_QTUM_AMOUNT: u64 = 0; -const QRC20_GAS_LIMIT_DEFAULT: u64 = 100_000; +pub const OUTPUT_QTUM_AMOUNT: u64 = 0; +pub const QRC20_GAS_LIMIT_DEFAULT: u64 = 100_000; const QRC20_PAYMENT_GAS_LIMIT: u64 = 200_000; -const QRC20_GAS_PRICE_DEFAULT: u64 = 40; -const QRC20_DUST: u64 = 0; +pub const QRC20_GAS_PRICE_DEFAULT: u64 = 40; +pub const QRC20_DUST: u64 = 0; // Keccak-256 hash of `Transfer` event const QRC20_TRANSFER_TOPIC: &str = "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; const QRC20_PAYMENT_SENT_TOPIC: &str = "ccc9c05183599bd3135da606eaaf535daffe256e9de33c048014cffcccd4ad57"; @@ -65,14 +71,89 @@ const QRC20_SENDER_REFUNDED_TOPIC: &str = "1797d500133f8e427eb9da9523aa4a25cb40f pub type Qrc20AbiResult = Result>; +#[derive(Display)] +pub enum Qrc20GenTxError { + ErrorGeneratingUtxoTx(GenerateTxError), + ErrorSigningTx(UtxoSignWithKeyPairError), + PrivKeyNotAllowed(PrivKeyNotAllowed), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), +} + +impl From for Qrc20GenTxError { + fn from(e: GenerateTxError) -> Self { Qrc20GenTxError::ErrorGeneratingUtxoTx(e) } +} + +impl From for Qrc20GenTxError { + fn from(e: UtxoSignWithKeyPairError) -> Self { Qrc20GenTxError::ErrorSigningTx(e) } +} + +impl From for Qrc20GenTxError { + fn from(e: PrivKeyNotAllowed) -> Self { Qrc20GenTxError::PrivKeyNotAllowed(e) } +} + +impl From for Qrc20GenTxError { + fn from(e: UnexpectedDerivationMethod) -> Self { Qrc20GenTxError::UnexpectedDerivationMethod(e) } +} + +impl From for Qrc20GenTxError { + fn from(e: UtxoRpcError) -> Self { Qrc20GenTxError::ErrorGeneratingUtxoTx(GenerateTxError::from(e)) } +} + +impl Qrc20GenTxError { + fn into_withdraw_error(self, coin: String, decimals: u8) -> WithdrawError { + match self { + Qrc20GenTxError::ErrorGeneratingUtxoTx(gen_err) => { + WithdrawError::from_generate_tx_error(gen_err, coin, decimals) + }, + Qrc20GenTxError::ErrorSigningTx(sign_err) => WithdrawError::InternalError(sign_err.to_string()), + Qrc20GenTxError::PrivKeyNotAllowed(priv_err) => WithdrawError::InternalError(priv_err.to_string()), + Qrc20GenTxError::UnexpectedDerivationMethod(addr_err) => WithdrawError::InternalError(addr_err.to_string()), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Qrc20ActivationParams { + swap_contract_address: H160, + fallback_swap_contract: Option, + #[serde(flatten)] + utxo_params: UtxoActivationParams, +} + +#[derive(Debug, Display)] +pub enum Qrc20FromLegacyReqErr { + InvalidSwapContractAddr(json::Error), + InvalidFallbackSwapContract(json::Error), + InvalidUtxoParams(UtxoFromLegacyReqErr), +} + +impl From for Qrc20FromLegacyReqErr { + fn from(err: UtxoFromLegacyReqErr) -> Self { Qrc20FromLegacyReqErr::InvalidUtxoParams(err) } +} + +impl Qrc20ActivationParams { + pub fn from_legacy_req(req: &Json) -> Result> { + let swap_contract_address = json::from_value(req["swap_contract_address"].clone()) + .map_to_mm(Qrc20FromLegacyReqErr::InvalidSwapContractAddr)?; + let fallback_swap_contract = json::from_value(req["fallback_swap_contract"].clone()) + .map_to_mm(Qrc20FromLegacyReqErr::InvalidFallbackSwapContract)?; + let utxo_params = UtxoActivationParams::from_legacy_req(req)?; + Ok(Qrc20ActivationParams { + swap_contract_address, + fallback_swap_contract, + utxo_params, + }) + } +} + struct Qrc20CoinBuilder<'a> { ctx: &'a MmArc, ticker: &'a str, conf: &'a Json, - req: &'a Json, + activation_params: &'a Qrc20ActivationParams, priv_key: &'a [u8], platform: String, - contract_address: H160, + token_contract_address: H160, } impl<'a> Qrc20CoinBuilder<'a> { @@ -80,85 +161,49 @@ impl<'a> Qrc20CoinBuilder<'a> { ctx: &'a MmArc, ticker: &'a str, conf: &'a Json, - req: &'a Json, + activation_params: &'a Qrc20ActivationParams, priv_key: &'a [u8], platform: String, - contract_address: H160, + token_contract_address: H160, ) -> Qrc20CoinBuilder<'a> { Qrc20CoinBuilder { ctx, ticker, conf, - req, + activation_params, priv_key, platform, - contract_address, - } - } -} - -impl Qrc20CoinBuilder<'_> { - fn swap_contract_address(&self) -> Result { - match self.req()["swap_contract_address"].as_str() { - Some(address) => qtum::contract_addr_from_str(address).map_err(|e| ERRL!("{}", e)), - None => return ERR!("\"swap_contract_address\" field is expected"), - } - } - - fn fallback_swap_contract(&self) -> Result, String> { - match self.req()["fallback_swap_contract"].as_str() { - Some(address) => qtum::contract_addr_from_str(address) - .map_err(|e| ERRL!("{}", e)) - .map(Some), - None => Ok(None), + token_contract_address, } } } #[async_trait] -impl UtxoCoinBuilder for Qrc20CoinBuilder<'_> { - type ResultCoin = Qrc20Coin; - - async fn build(self) -> Result { - let swap_contract_address = try_s!(self.swap_contract_address()); - let fallback_swap_contract = try_s!(self.fallback_swap_contract()); - let utxo = try_s!(self.build_utxo_fields().await); - let inner = Qrc20CoinFields { - utxo, - platform: self.platform, - contract_address: self.contract_address, - swap_contract_address, - fallback_swap_contract, - }; - Ok(Qrc20Coin(Arc::new(inner))) - } - +impl<'a> UtxoCoinBuilderCommonOps for Qrc20CoinBuilder<'a> { fn ctx(&self) -> &MmArc { self.ctx } fn conf(&self) -> &Json { self.conf } - fn req(&self) -> &Json { self.req } + fn activation_params(&self) -> &UtxoActivationParams { &self.activation_params.utxo_params } fn ticker(&self) -> &str { self.ticker } - fn priv_key(&self) -> &[u8] { self.priv_key } - - async fn decimals(&self, rpc_client: &UtxoRpcClientEnum) -> Result { + async fn decimals(&self, rpc_client: &UtxoRpcClientEnum) -> UtxoCoinBuildResult { if let Some(d) = self.conf()["decimals"].as_u64() { return Ok(d as u8); } rpc_client - .token_decimals(&self.contract_address) + .token_decimals(&self.token_contract_address) .compat() .await - .map_err(|e| ERRL!("{}", e)) + .map_to_mm(UtxoCoinBuildError::ErrorDetectingDecimals) } fn dust_amount(&self) -> u64 { QRC20_DUST } #[cfg(not(target_arch = "wasm32"))] - fn confpath(&self) -> Result { + fn confpath(&self) -> UtxoCoinBuildResult { use crate::utxo::coin_daemon_data_dir; // Documented at https://github.com/jl777/coins#bitcoin-protocol-specific-json @@ -184,25 +229,72 @@ impl UtxoCoinBuilder for Qrc20CoinBuilder<'_> { }; if rel_to_home { - let home = try_s!(dirs::home_dir().ok_or("Can not detect the user home directory")); + let home = dirs::home_dir().or_mm_err(|| UtxoCoinBuildError::CantDetectUserHome)?; Ok(home.join(confpath)) } else { Ok(confpath.into()) } } + + fn check_utxo_maturity(&self) -> bool { + if let Some(false) = self.activation_params.utxo_params.check_utxo_maturity { + warn!("'check_utxo_maturity' is ignored because QRC20 gas refund is returned as a coinbase transaction"); + } + true + } + + /// Override [`UtxoCoinBuilderCommonOps::tx_cache`] to initialize TX cache with the platform ticker. + /// Please note the method is overridden for Native mode only. + #[inline] + #[cfg(not(target_arch = "wasm32"))] + fn tx_cache(&self) -> UtxoVerboseCacheShared { + crate::utxo::tx_cache::fs_tx_cache::FsVerboseCache::new(self.platform.clone(), self.tx_cache_path()) + .into_shared() + } } -pub async fn qrc20_coin_from_conf_and_request( +#[async_trait] +impl<'a> UtxoFieldsWithIguanaPrivKeyBuilder for Qrc20CoinBuilder<'a> {} + +#[async_trait] +impl<'a> UtxoCoinWithIguanaPrivKeyBuilder for Qrc20CoinBuilder<'a> { + type ResultCoin = Qrc20Coin; + type Error = UtxoCoinBuildError; + + fn priv_key(&self) -> &[u8] { self.priv_key } + + async fn build(self) -> MmResult { + let utxo = self.build_utxo_fields_with_iguana_priv_key(self.priv_key()).await?; + let inner = Qrc20CoinFields { + utxo, + platform: self.platform, + contract_address: self.token_contract_address, + swap_contract_address: self.activation_params.swap_contract_address, + fallback_swap_contract: self.activation_params.fallback_swap_contract, + }; + Ok(Qrc20Coin(Arc::new(inner))) + } +} + +pub async fn qrc20_coin_from_conf_and_params( ctx: &MmArc, ticker: &str, platform: &str, conf: &Json, - req: &Json, + params: &Qrc20ActivationParams, priv_key: &[u8], contract_address: H160, ) -> Result { - let builder = Qrc20CoinBuilder::new(ctx, ticker, conf, req, priv_key, platform.to_owned(), contract_address); - builder.build().await + let builder = Qrc20CoinBuilder::new( + ctx, + ticker, + conf, + params, + priv_key, + platform.to_owned(), + contract_address, + ); + Ok(try_s!(builder.build().await)) } #[derive(Debug)] @@ -308,10 +400,10 @@ impl MutContractCallType { fn short_signature(&self) -> [u8; 4] { self.as_function().short_signature() } } -struct GenerateQrc20TxResult { - signed: UtxoTx, - miner_fee: u64, - gas_fee: u64, +pub struct GenerateQrc20TxResult { + pub signed: UtxoTx, + pub miner_fee: u64, + pub gas_fee: u64, } #[derive(Debug, Display)] @@ -326,6 +418,10 @@ impl From for Qrc20AbiError { fn from(e: ethabi::Error) -> Qrc20AbiError { Qrc20AbiError::AbiError(e.to_string()) } } +impl From for GenerateTxError { + fn from(e: Qrc20AbiError) -> Self { GenerateTxError::Internal(e.to_string()) } +} + impl From for TradePreimageError { fn from(e: Qrc20AbiError) -> Self { // `Qrc20ABIError` is always an internal error @@ -358,19 +454,19 @@ impl Qrc20Coin { /// Generate and send a transaction with the specified UTXO outputs. /// Note this function locks the `UTXO_LOCK`. - pub async fn send_contract_calls(&self, outputs: Vec) -> Result { + pub async fn send_contract_calls( + &self, + outputs: Vec, + ) -> Result { // TODO: we need to somehow refactor it using RecentlySpentOutpoints cache // Move over all QRC20 tokens should share the same cache with each other and base QTUM coin let _utxo_lock = UTXO_LOCK.lock().await; - let platform = self.platform.clone(); - let decimals = self.utxo.decimals; let GenerateQrc20TxResult { signed, .. } = self .generate_qrc20_transaction(outputs) .await - .mm_err(|e| WithdrawError::from_generate_tx_error(e, platform, decimals)) - .map_err(|e| ERRL!("{}", e))?; - let _tx = try_s!(self.utxo.rpc_client.send_transaction(&signed).compat().await); + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + try_tx_s!(self.utxo.rpc_client.send_transaction(&signed).compat().await, signed); Ok(signed.into()) } @@ -379,8 +475,9 @@ impl Qrc20Coin { async fn generate_qrc20_transaction( &self, contract_outputs: Vec, - ) -> Result> { - let (unspents, _) = self.ordered_mature_unspents(&self.utxo.my_address).await?; + ) -> Result> { + let my_address = self.utxo.derivation_method.iguana_or_err()?; + let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; let mut gas_fee = 0; let mut outputs = Vec::with_capacity(contract_outputs.len()); @@ -388,21 +485,25 @@ impl Qrc20Coin { gas_fee += output.gas_limit * output.gas_price; outputs.push(TransactionOutput::from(output)); } - let fee_policy = FeePolicy::SendExact; - let tx_fee = None; - let (unsigned, data) = self - .generate_transaction(unspents, outputs, fee_policy, tx_fee, Some(gas_fee)) + let (unsigned, data) = UtxoTxBuilder::new(self) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_gas_fee(gas_fee) + .build() .await?; - let prev_script = ScriptBuilder::build_p2pkh(&self.utxo.my_address.hash); + + let my_address = self.utxo.derivation_method.iguana_or_err()?; + let key_pair = self.utxo.priv_key_policy.key_pair_or_err()?; + + let prev_script = ScriptBuilder::build_p2pkh(&my_address.hash); let signed = sign_tx( unsigned, - &self.utxo.key_pair, + key_pair, prev_script, self.utxo.conf.signature_version, self.utxo.conf.fork_id, - ) - .map_to_mm(GenerateTxError::Internal)?; + )?; let miner_fee = data.fee_amount + data.unused_change.unwrap_or_default(); Ok(GenerateQrc20TxResult { @@ -454,13 +555,62 @@ impl Qrc20Coin { } } +// if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt #[async_trait] #[cfg_attr(test, mockable)] -impl UtxoCommonOps for Qrc20Coin { +impl UtxoTxBroadcastOps for Qrc20Coin { + async fn broadcast_tx(&self, tx: &UtxoTx) -> Result> { + utxo_common::broadcast_tx(self, tx).await + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl UtxoTxGenerationOps for Qrc20Coin { /// Get only QTUM transaction fee. - async fn get_tx_fee(&self) -> Result { utxo_common::get_tx_fee(&self.utxo).await } + async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo).await } + + async fn calc_interest_if_required( + &self, + unsigned: TransactionInputSigner, + data: AdditionalTxData, + my_script_pub: ScriptBytes, + ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { + utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub).await + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoListOps for Qrc20Coin { + async fn get_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_unspent_ordered_list(self, address).await + } - async fn get_htlc_spend_fee(&self) -> UtxoRpcResult { utxo_common::get_htlc_spend_fee(self).await } + async fn get_all_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_list(self, address).await + } + + async fn get_mature_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_mature_unspent_ordered_list(self, address).await + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl UtxoCommonOps for Qrc20Coin { + async fn get_htlc_spend_fee(&self, tx_size: u64) -> UtxoRpcResult { + utxo_common::get_htlc_spend_fee(self, tx_size).await + } fn addresses_from_script(&self, script: &Script) -> Result, String> { utxo_common::addresses_from_script(self, script) @@ -468,10 +618,12 @@ impl UtxoCommonOps for Qrc20Coin { fn denominate_satoshis(&self, satoshi: i64) -> f64 { utxo_common::denominate_satoshis(&self.utxo, satoshi) } - fn my_public_key(&self) -> &Public { self.utxo.key_pair.public() } + fn my_public_key(&self) -> Result<&Public, MmError> { + utxo_common::my_public_key(self.as_ref()) + } fn address_from_str(&self, address: &str) -> Result { - utxo_common::checked_address_from_str(&self.utxo, address) + utxo_common::checked_address_from_str(self, address) } async fn get_current_mtp(&self) -> UtxoRpcResult { @@ -480,27 +632,6 @@ impl UtxoCommonOps for Qrc20Coin { fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { self.is_qtum_unspent_mature(output) } - /// Generate UTXO transaction with specified unspent inputs and specified outputs. - async fn generate_transaction( - &self, - utxos: Vec, - outputs: Vec, - fee_policy: FeePolicy, - fee: Option, - gas_fee: Option, - ) -> GenerateTxResult { - utxo_common::generate_transaction(self, utxos, outputs, fee_policy, fee, gas_fee).await - } - - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: ScriptBytes, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub).await - } - async fn calc_interest_of_tx( &self, _tx: &UtxoTx, @@ -519,54 +650,19 @@ impl UtxoCommonOps for Qrc20Coin { utxo_common::get_mut_verbose_transaction_from_map_or_rpc(self, tx_hash, utxo_tx_map).await } - async fn p2sh_spending_tx( - &self, - prev_transaction: UtxoTx, - redeem_script: ScriptBytes, - outputs: Vec, - script_data: Script, - sequence: u32, - lock_time: u32, - ) -> Result { - utxo_common::p2sh_spending_tx( - self, - prev_transaction, - redeem_script, - outputs, - script_data, - sequence, - lock_time, - ) - .await + async fn p2sh_spending_tx(&self, input: utxo_common::P2SHSpendingTxInput<'_>) -> Result { + utxo_common::p2sh_spending_tx(self, input).await } - async fn ordered_mature_unspents<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::ordered_mature_unspents(self, address).await - } - - fn get_verbose_transaction_from_cache_or_rpc( + fn get_verbose_transactions_from_cache_or_rpc( &self, - txid: H256Json, - ) -> Box + Send> { + tx_ids: HashSet, + ) -> UtxoRpcFut> { let selfi = self.clone(); - let fut = async move { utxo_common::get_verbose_transaction_from_cache_or_rpc(&selfi.utxo, txid).await }; + let fut = async move { utxo_common::get_verbose_transactions_from_cache_or_rpc(&selfi.utxo, tx_ids).await }; Box::new(fut.boxed().compat()) } - async fn cache_transaction_if_possible(&self, tx: &RpcTransaction) -> Result<(), String> { - utxo_common::cache_transaction_if_possible(&self.utxo, tx).await - } - - async fn list_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::ordered_mature_unspents(self, address).await - } - async fn preimage_trade_fee_required_to_send_outputs( &self, outputs: Vec, @@ -574,7 +670,15 @@ impl UtxoCommonOps for Qrc20Coin { gas_fee: Option, stage: &FeeApproxStage, ) -> TradePreimageResult { - utxo_common::preimage_trade_fee_required_to_send_outputs(self, outputs, fee_policy, gas_fee, stage).await + utxo_common::preimage_trade_fee_required_to_send_outputs( + self, + self.platform_ticker(), + outputs, + fee_policy, + gas_fee, + stage, + ) + .await } fn increase_dynamic_fee_by_stage(&self, dynamic_fee: u64, stage: &FeeApproxStage) -> u64 { @@ -585,17 +689,32 @@ impl UtxoCommonOps for Qrc20Coin { utxo_common::p2sh_tx_locktime(self, &self.utxo.conf.ticker, htlc_locktime).await } + fn addr_format(&self) -> &UtxoAddressFormat { utxo_common::addr_format(self) } + fn addr_format_for_standard_scripts(&self) -> UtxoAddressFormat { utxo_common::addr_format_for_standard_scripts(self) } + + fn address_from_pubkey(&self, pubkey: &Public) -> Address { + let conf = &self.utxo.conf; + utxo_common::address_from_pubkey( + pubkey, + conf.pub_addr_prefix, + conf.pub_t_addr_prefix, + conf.checksum_type, + conf.bech32_hrp.clone(), + self.addr_format().clone(), + ) + } } +#[async_trait] impl SwapOps for Qrc20Coin { - fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal) -> TransactionFut { - let to_address = try_fus!(self.contract_address_from_raw_pubkey(fee_addr)); - let amount = try_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); + fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { + let to_address = try_tx_fus!(self.contract_address_from_raw_pubkey(fee_addr)); + let amount = try_tx_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); let transfer_output = - try_fus!(self.transfer_output(to_address, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)); + try_tx_fus!(self.transfer_output(to_address, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)); let outputs = vec![transfer_output]; let selfi = self.clone(); @@ -611,12 +730,13 @@ impl SwapOps for Qrc20Coin { secret_hash: &[u8], amount: BigDecimal, swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let taker_addr = try_fus!(self.contract_address_from_raw_pubkey(taker_pub)); + let taker_addr = try_tx_fus!(self.contract_address_from_raw_pubkey(taker_pub)); let id = qrc20_swap_id(time_lock, secret_hash); - let value = try_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); + let value = try_tx_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); let secret_hash = Vec::from(secret_hash); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -634,12 +754,13 @@ impl SwapOps for Qrc20Coin { secret_hash: &[u8], amount: BigDecimal, swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let maker_addr = try_fus!(self.contract_address_from_raw_pubkey(maker_pub)); + let maker_addr = try_tx_fus!(self.contract_address_from_raw_pubkey(maker_pub)); let id = qrc20_swap_id(time_lock, secret_hash); - let value = try_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); + let value = try_tx_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); let secret_hash = Vec::from(secret_hash); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -657,9 +778,10 @@ impl SwapOps for Qrc20Coin { _taker_pub: &[u8], secret: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let payment_tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let payment_tx: UtxoTx = try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let secret = secret.to_vec(); let selfi = self.clone(); @@ -678,10 +800,11 @@ impl SwapOps for Qrc20Coin { _maker_pub: &[u8], secret: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let payment_tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let payment_tx: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); let secret = secret.to_vec(); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -699,9 +822,10 @@ impl SwapOps for Qrc20Coin { _maker_pub: &[u8], _secret_hash: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let payment_tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let payment_tx: UtxoTx = try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -719,9 +843,10 @@ impl SwapOps for Qrc20Coin { _taker_pub: &[u8], _secret_hash: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { - let payment_tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let payment_tx: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -739,6 +864,7 @@ impl SwapOps for Qrc20Coin { fee_addr: &[u8], amount: &BigDecimal, min_block_number: u64, + _uuid: &[u8], ) -> Box + Send> { let fee_tx = match fee_tx { TransactionEnum::UtxoTx(tx) => tx, @@ -760,29 +886,20 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - fn validate_maker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - secret_hash: &[u8], - amount: BigDecimal, - swap_contract_address: &Option, - ) -> Box + Send> { - let payment_tx: UtxoTx = try_fus!(deserialize(payment_tx).map_err(|e| ERRL!("{:?}", e))); - let sender = try_fus!(self.contract_address_from_raw_pubkey(maker_pub)); - let secret_hash = secret_hash.to_vec(); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + let payment_tx: UtxoTx = try_fus!(deserialize(input.payment_tx.as_slice()).map_err(|e| ERRL!("{:?}", e))); + let sender = try_fus!(self.contract_address_from_raw_pubkey(&input.other_pub)); + let swap_contract_address = try_fus!(input.swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { selfi .validate_payment( payment_tx, - time_lock, + input.time_lock, sender, - secret_hash, - amount, + input.secret_hash, + input.amount, swap_contract_address, ) .await @@ -790,29 +907,20 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - fn validate_taker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - secret_hash: &[u8], - amount: BigDecimal, - swap_contract_address: &Option, - ) -> Box + Send> { - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); - let payment_tx: UtxoTx = try_fus!(deserialize(payment_tx).map_err(|e| ERRL!("{:?}", e))); - let sender = try_fus!(self.contract_address_from_raw_pubkey(taker_pub)); - let secret_hash = secret_hash.to_vec(); + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + let swap_contract_address = try_fus!(input.swap_contract_address.try_to_address()); + let payment_tx: UtxoTx = try_fus!(deserialize(input.payment_tx.as_slice()).map_err(|e| ERRL!("{:?}", e))); + let sender = try_fus!(self.contract_address_from_raw_pubkey(&input.other_pub)); let selfi = self.clone(); let fut = async move { selfi .validate_payment( payment_tx, - time_lock, + input.time_lock, sender, - secret_hash, - amount, + input.secret_hash, + input.amount, swap_contract_address, ) .await @@ -827,6 +935,7 @@ impl SwapOps for Qrc20Coin { secret_hash: &[u8], search_from_block: u64, swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> Box, Error = String> + Send> { let swap_id = qrc20_swap_id(time_lock, secret_hash); let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); @@ -840,36 +949,24 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - fn search_for_swap_tx_spend_my( + async fn search_for_swap_tx_spend_my( &self, - time_lock: u32, - _other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - _swap_contract_address: &Option, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { - let tx: UtxoTx = try_s!(deserialize(tx).map_err(|e| ERRL!("{:?}", e))); + let tx: UtxoTx = try_s!(deserialize(input.tx).map_err(|e| ERRL!("{:?}", e))); - let selfi = self.clone(); - let fut = selfi.search_for_swap_tx_spend(time_lock, secret_hash.to_vec(), tx, search_from_block); - block_on(fut) + self.search_for_swap_tx_spend(input.time_lock, input.secret_hash, tx, input.search_from_block) + .await } - fn search_for_swap_tx_spend_other( + async fn search_for_swap_tx_spend_other( &self, - time_lock: u32, - _other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - _swap_contract_address: &Option, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { - let tx: UtxoTx = try_s!(deserialize(tx).map_err(|e| ERRL!("{:?}", e))); + let tx: UtxoTx = try_s!(deserialize(input.tx).map_err(|e| ERRL!("{:?}", e))); - let selfi = self.clone(); - let fut = selfi.search_for_swap_tx_spend(time_lock, secret_hash.to_vec(), tx, search_from_block); - block_on(fut) + self.search_for_swap_tx_spend(input.time_lock, input.secret_hash, tx, input.search_from_block) + .await } fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { @@ -901,6 +998,10 @@ impl SwapOps for Qrc20Coin { .ok_or_else(|| MmError::new(NegotiateSwapContractAddrErr::NoOtherAddrAndNoFallback)), } } + + fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { + utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) + } } impl MarketCoinOps for Qrc20Coin { @@ -908,14 +1009,33 @@ impl MarketCoinOps for Qrc20Coin { fn my_address(&self) -> Result { utxo_common::my_address(self) } + fn get_public_key(&self) -> Result> { + let pubkey = utxo_common::my_public_key(self.as_ref())?; + Ok(pubkey.to_string()) + } + + fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { + utxo_common::sign_message_hash(self.as_ref(), message) + } + + fn sign_message(&self, message: &str) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message) + } + + fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { + utxo_common::verify_message(self, signature_base64, message, address) + } + fn my_balance(&self) -> BalanceFut { - let my_address = self.my_addr_as_contract_addr(); - let params = [Token::Address(my_address)]; - let contract_address = self.contract_address; let decimals = self.utxo.decimals; let coin = self.clone(); let fut = async move { + let my_address = coin + .my_addr_as_contract_addr() + .mm_err(|e| BalanceError::Internal(e.to_string()))?; + let params = [Token::Address(my_address)]; + let contract_address = coin.contract_address; let tokens = coin .utxo .rpc_client @@ -938,18 +1058,22 @@ impl MarketCoinOps for Qrc20Coin { } fn base_coin_balance(&self) -> BalanceFut { - let selfi = self.clone(); - let fut = async move { - let CoinBalance { spendable, .. } = selfi.qtum_balance().await?; - Ok(spendable) - }; - Box::new(fut.boxed().compat()) + // use standard UTXO my_balance implementation that returns Qtum balance instead of QRC20 + Box::new(utxo_common::my_balance(self.clone()).map(|CoinBalance { spendable, .. }| spendable)) } + fn platform_ticker(&self) -> &str { &self.0.platform } + + #[inline(always)] fn send_raw_tx(&self, tx: &str) -> Box + Send> { utxo_common::send_raw_tx(&self.utxo, tx) } + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + utxo_common::send_raw_tx_bytes(&self.utxo, tx) + } + fn wait_for_confirmations( &self, tx: &[u8], @@ -975,10 +1099,15 @@ impl MarketCoinOps for Qrc20Coin { from_block: u64, _swap_contract_address: &Option, ) -> TransactionFut { - let tx: UtxoTx = try_fus!(deserialize(transaction).map_err(|e| ERRL!("{:?}", e))); + let tx: UtxoTx = try_tx_fus!(deserialize(transaction).map_err(|e| ERRL!("{:?}", e))); let selfi = self.clone(); - let fut = async move { selfi.wait_for_tx_spend_impl(tx, wait_until, from_block).await }; + let fut = async move { + selfi + .wait_for_tx_spend_impl(tx, wait_until, from_block) + .map_err(TransactionErr::Plain) + .await + }; Box::new(fut.boxed().compat()) } @@ -990,7 +1119,7 @@ impl MarketCoinOps for Qrc20Coin { utxo_common::current_block(&self.utxo) } - fn display_priv_key(&self) -> String { utxo_common::display_priv_key(&self.utxo) } + fn display_priv_key(&self) -> Result { utxo_common::display_priv_key(&self.utxo) } fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } @@ -1000,6 +1129,7 @@ impl MarketCoinOps for Qrc20Coin { } } +#[async_trait] impl MmCoin for Qrc20Coin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo) } @@ -1007,6 +1137,10 @@ impl MmCoin for Qrc20Coin { Box::new(qrc20_withdraw(self.clone(), req).boxed().compat()) } + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { + Box::new(utxo_common::get_raw_transaction(&self.utxo, req).boxed().compat()) + } + fn decimals(&self) -> u8 { utxo_common::decimals(&self.utxo) } fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { @@ -1038,61 +1172,54 @@ impl MmCoin for Qrc20Coin { Box::new(fut.boxed().compat()) } - fn get_sender_trade_fee(&self, value: TradePreimageValue, stage: FeeApproxStage) -> TradePreimageFut { - let selfi = self.clone(); + async fn get_sender_trade_fee( + &self, + value: TradePreimageValue, + stage: FeeApproxStage, + ) -> TradePreimageResult { let decimals = self.utxo.decimals; - let fut = async move { - // pass the dummy params - let timelock = (now_ms() / 1000) as u32; - let secret_hash = vec![0; 20]; - let swap_id = qrc20_swap_id(timelock, &secret_hash); - let receiver_addr = H160::default(); - // we can avoid the requesting balance, because it doesn't affect the total fee - let my_balance = U256::max_value(); - let value = match value { - TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => { - wei_from_big_decimal(&value, decimals)? - }, - }; - - let erc20_payment_fee = { - let erc20_payment_outputs = selfi - .generate_swap_payment_outputs( - my_balance, - swap_id.clone(), - value, - timelock, - secret_hash.clone(), - receiver_addr, - selfi.swap_contract_address, - ) - .await?; - selfi - .preimage_trade_fee_required_to_send_outputs(erc20_payment_outputs, &stage) - .await? - }; + // pass the dummy params + let timelock = (now_ms() / 1000) as u32; + let secret_hash = vec![0; 20]; + let swap_id = qrc20_swap_id(timelock, &secret_hash); + let receiver_addr = H160::default(); + // we can avoid the requesting balance, because it doesn't affect the total fee + let my_balance = U256::max_value(); + let value = match value { + TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => { + wei_from_big_decimal(&value, decimals)? + }, + }; - let sender_refund_fee = { - let sender_refund_output = selfi.sender_refund_output( - &selfi.swap_contract_address, - swap_id, + let erc20_payment_fee = { + let erc20_payment_outputs = self + .generate_swap_payment_outputs( + my_balance, + swap_id.clone(), value, - secret_hash, + timelock, + secret_hash.clone(), receiver_addr, - )?; - selfi - .preimage_trade_fee_required_to_send_outputs(vec![sender_refund_output], &stage) - .await? - }; + self.swap_contract_address, + ) + .await?; + self.preimage_trade_fee_required_to_send_outputs(erc20_payment_outputs, &stage) + .await? + }; - let total_fee = erc20_payment_fee + sender_refund_fee; - Ok(TradeFee { - coin: selfi.platform.clone(), - amount: total_fee.into(), - paid_from_trading_vol: false, - }) + let sender_refund_fee = { + let sender_refund_output = + self.sender_refund_output(&self.swap_contract_address, swap_id, value, secret_hash, receiver_addr)?; + self.preimage_trade_fee_required_to_send_outputs(vec![sender_refund_output], &stage) + .await? }; - Box::new(fut.boxed().compat()) + + let total_fee = erc20_payment_fee + sender_refund_fee; + Ok(TradeFee { + coin: self.platform.clone(), + amount: total_fee.into(), + paid_from_trading_vol: false, + }) } fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { @@ -1105,7 +1232,7 @@ impl MmCoin for Qrc20Coin { let sender_addr = H160::default(); // get the max available value that we can pass into the contract call params // see `generate_contract_call_script_pubkey` - let value = u64::max_value().into(); + let value = u64::MAX.into(); let output = selfi.receiver_spend_output(&selfi.swap_contract_address, swap_id, value, secret, sender_addr)?; @@ -1121,30 +1248,27 @@ impl MmCoin for Qrc20Coin { Box::new(fut.boxed().compat()) } - fn get_fee_to_send_taker_fee( + async fn get_fee_to_send_taker_fee( &self, dex_fee_amount: BigDecimal, stage: FeeApproxStage, - ) -> TradePreimageFut { - let selfi = self.clone(); - let fut = async move { - let amount = wei_from_big_decimal(&dex_fee_amount, selfi.utxo.decimals)?; + ) -> TradePreimageResult { + let amount = wei_from_big_decimal(&dex_fee_amount, self.utxo.decimals)?; - // pass the dummy params - let to_addr = H160::default(); - let transfer_output = - selfi.transfer_output(to_addr, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)?; + // pass the dummy params + let to_addr = H160::default(); + let transfer_output = + self.transfer_output(to_addr, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)?; - let total_fee = selfi - .preimage_trade_fee_required_to_send_outputs(vec![transfer_output], &stage) - .await?; - Ok(TradeFee { - coin: selfi.platform.clone(), - amount: total_fee.into(), - paid_from_trading_vol: false, - }) - }; - Box::new(fut.boxed().compat()) + let total_fee = self + .preimage_trade_fee_required_to_send_outputs(vec![transfer_output], &stage) + .await?; + + Ok(TradeFee { + coin: self.platform.clone(), + amount: total_fee.into(), + paid_from_trading_vol: false, + }) } fn required_confirmations(&self) -> u64 { utxo_common::required_confirmations(&self.utxo) } @@ -1165,10 +1289,10 @@ impl MmCoin for Qrc20Coin { fn mature_confirmations(&self) -> Option { Some(self.utxo.conf.mature_confirmations) } - fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(&self.utxo) } + fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } fn is_coin_protocol_supported(&self, info: &Option>) -> bool { - utxo_common::is_coin_protocol_supported(&self.utxo, info) + utxo_common::is_coin_protocol_supported(self, info) } } @@ -1179,20 +1303,20 @@ pub fn qrc20_swap_id(time_lock: u32, secret_hash: &[u8]) -> Vec { sha256(&input).to_vec() } -fn contract_addr_into_rpc_format(address: &H160) -> H160Json { H160Json::from(address.0) } +pub fn contract_addr_into_rpc_format(address: &H160) -> H160Json { H160Json::from(address.0) } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct Qrc20FeeDetails { /// Coin name - coin: String, + pub coin: String, /// Standard UTXO miner fee based on transaction size - miner_fee: BigDecimal, + pub miner_fee: BigDecimal, /// Gas limit in satoshi. - gas_limit: u64, + pub gas_limit: u64, /// Gas price in satoshi. - gas_price: u64, + pub gas_price: u64, /// Total used gas. - total_gas_fee: BigDecimal, + pub total_gas_fee: BigDecimal, } async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult { @@ -1201,10 +1325,8 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult .map_to_mm(WithdrawError::InvalidAddress)?; let conf = &coin.utxo.conf; let is_p2pkh = to_addr.prefix == conf.pub_addr_prefix && to_addr.t_addr_prefix == conf.pub_t_addr_prefix; - let is_p2sh = - to_addr.prefix == conf.p2sh_addr_prefix && to_addr.t_addr_prefix == conf.p2sh_t_addr_prefix && conf.segwit; - if !is_p2pkh && !is_p2sh { - let error = "Expected either P2PKH or P2SH".to_owned(); + if !is_p2pkh { + let error = "QRC20 can be sent to P2PKH addresses only".to_owned(); return MmError::err(WithdrawError::InvalidAddress(error)); } @@ -1241,23 +1363,21 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult }; // [`Qrc20Coin::transfer_output`] shouldn't fail if the arguments are correct - let transfer_output = coin.transfer_output( - qtum::contract_addr_from_utxo_addr(to_addr.clone()), - qrc20_amount_sat, - gas_limit, - gas_price, - )?; + let contract_addr = qtum::contract_addr_from_utxo_addr(to_addr.clone())?; + let transfer_output = coin.transfer_output(contract_addr, qrc20_amount_sat, gas_limit, gas_price)?; let outputs = vec![transfer_output]; let GenerateQrc20TxResult { signed, miner_fee, gas_fee, - } = coin.generate_qrc20_transaction(outputs).await.mm_err(|gen_tx_error| { - WithdrawError::from_generate_tx_error(gen_tx_error, coin.platform.clone(), coin.utxo.decimals) - })?; + } = coin + .generate_qrc20_transaction(outputs) + .await + .mm_err(|gen_tx_error| gen_tx_error.into_withdraw_error(coin.platform.clone(), coin.utxo.decimals))?; - let received_by_me = if to_addr == coin.utxo.my_address { + let my_address = coin.utxo.derivation_method.iguana_or_err()?; + let received_by_me = if to_addr == *my_address { qrc20_amount.clone() } else { 0.into() @@ -1265,7 +1385,7 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult let my_balance_change = &received_by_me - &qrc20_amount; // [`MarketCoinOps::my_address`] and [`UtxoCommonOps::display_address`] shouldn't fail - let my_address = coin.my_address().map_to_mm(WithdrawError::InternalError)?; + let my_address_string = coin.my_address().map_to_mm(WithdrawError::InternalError)?; let to_address = to_addr.display_address().map_to_mm(WithdrawError::InternalError)?; let fee_details = Qrc20FeeDetails { @@ -1277,13 +1397,13 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult total_gas_fee: utxo_common::big_decimal_from_sat(gas_fee as i64, coin.utxo.decimals), }; Ok(TransactionDetails { - from: vec![my_address], + from: vec![my_address_string], to: vec![to_address], total_amount: qrc20_amount.clone(), spent_by_me: qrc20_amount, received_by_me, my_balance_change, - tx_hash: signed.hash().reversed().to_vec().into(), + tx_hash: signed.hash().reversed().to_vec().to_tx_hash(), tx_hex: serialize(&signed).into(), fee_details: Some(fee_details.into()), block_height: 0, @@ -1291,6 +1411,7 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult internal_id: vec![].into(), timestamp: now_ms() / 1000, kmd_rewards: None, + transaction_type: TransactionType::StandardTransfer, }) } diff --git a/mm2src/coins/qrc20/history.rs b/mm2src/coins/qrc20/history.rs index 091d61ada9..967ba82ba4 100644 --- a/mm2src/coins/qrc20/history.rs +++ b/mm2src/coins/qrc20/history.rs @@ -1,14 +1,11 @@ use super::*; -use crate::tx_history_db::TxHistoryResult; use crate::utxo::{RequestTxHistoryResult, UtxoFeeDetails}; -use crate::CoinsContext; -use crate::TxFeeDetails; +use crate::{CoinsContext, TxFeeDetails, TxHistoryResult}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use common::jsonrpc_client::JsonRpcErrorType; use common::mm_metrics::MetricsArc; use itertools::Itertools; use script_pubkey::{extract_contract_call_from_script, extract_gas_from_script, ExtractGasEnum}; -use std::cmp::Ordering; use std::collections::HashMap; use std::io::Cursor; use utxo_common::{HISTORY_TOO_LARGE_ERROR, HISTORY_TOO_LARGE_ERR_CODE}; @@ -148,7 +145,7 @@ impl Qrc20Coin { })); break; }, - RequestTxHistoryResult::UnknownError(e) => { + RequestTxHistoryResult::CriticalError(e) => { ctx.log.log( "😟", &[&"tx_history", &self.utxo.conf.ticker], @@ -175,19 +172,11 @@ impl Qrc20Coin { } // `history_map` has been updated. - let mut to_write: Vec = history_map + let to_write: Vec = history_map .iter() - .map(|(_, value)| value) - .flatten() + .flat_map(|(_, value)| value) .map(|(_tx_id, tx)| tx.clone()) .collect(); - to_write.sort_unstable_by(|a, b| { - match sort_newest_to_oldest(a.block_height, b.block_height) { - // do not reverse `transfer` events in one transaction - Ordering::Equal => a.internal_id.cmp(&b.internal_id), - ord => ord, - } - }); if let Err(e) = self.save_history_to_file(&ctx, to_write).compat().await { ctx.log.log( "", @@ -209,7 +198,7 @@ impl Qrc20Coin { let miner_fee = { let total_qtum_fee = match qtum_details.fee_details { - Some(TxFeeDetails::Utxo(UtxoFeeDetails { ref amount })) => amount.clone(), + Some(TxFeeDetails::Utxo(UtxoFeeDetails { ref amount, .. })) => amount.clone(), Some(ref fee) => return ERR!("Unexpected fee details {:?}", fee), None => return ERR!("No Qtum fee details"), }; @@ -235,7 +224,8 @@ impl Qrc20Coin { receipt: TxReceipt, miner_fee: BigDecimal, ) -> Result { - let tx_hash: H256Json = qtum_details.tx_hash.as_slice().into(); + let my_address = try_s!(self.utxo.derivation_method.iguana_or_err()); + let tx_hash: H256Json = try_s!(H256Json::from_str(&qtum_details.tx_hash)); if qtum_tx.outputs.len() <= (receipt.output_index as usize) { return ERR!( "Length of the transaction {:?} outputs less than output_index {}", @@ -290,17 +280,17 @@ impl Qrc20Coin { }; // https://github.com/qtumproject/qtum-electrum/blob/v4.0.2/electrum/wallet.py#L2102 - if from != self.utxo.my_address && to != self.utxo.my_address { + if from != *my_address && to != *my_address { // address mismatch continue; } - let spent_by_me = if from == self.utxo.my_address { + let spent_by_me = if from == *my_address { total_amount.clone() } else { 0.into() }; - let received_by_me = if to == self.utxo.my_address { + let received_by_me = if to == *my_address { total_amount.clone() } else { 0.into() @@ -309,16 +299,16 @@ impl Qrc20Coin { // do not inherit the block_height from qtum_tx (usually it is None) let block_height = receipt.block_number; let my_balance_change = &received_by_me - &spent_by_me; - let internal_id = TxInternalId::new(tx_hash.clone(), receipt.output_index, log_index as u64); + let internal_id = TxInternalId::new(tx_hash, receipt.output_index, log_index as u64); let from = if is_transferred_from_contract(&script_pubkey) { - qtum::display_as_contract_address(from) + try_s!(qtum::display_as_contract_address(from)) } else { try_s!(from.display_address()) }; let to = if is_transferred_to_contract(&script_pubkey) { - qtum::display_as_contract_address(to) + try_s!(qtum::display_as_contract_address(to)) } else { try_s!(to.display_address()) }; @@ -360,7 +350,9 @@ impl Qrc20Coin { } } }, - JsonRpcErrorType::Transport(err) | JsonRpcErrorType::Parse(_, err) => { + JsonRpcErrorType::InvalidRequest(err) + | JsonRpcErrorType::Transport(err) + | JsonRpcErrorType::Parse(_, err) => { return RequestTxHistoryResult::Retry { error: ERRL!("Error {} on blockchain_contract_event_get_history", err), }; @@ -391,8 +383,7 @@ impl Qrc20Coin { ) -> bool { let need_update = history .iter() - .map(|(_, txs)| txs) - .flatten() + .flat_map(|(_, txs)| txs) .any(|(_, tx)| tx.should_update_timestamp() || tx.should_update_block_height()); match last_balance { Some(last_balance) if last_balance == actual_balance && !need_update => { @@ -413,13 +404,7 @@ impl Qrc20Coin { ) -> ProcessCachedTransferMapResult { async fn get_verbose_transaction(coin: &Qrc20Coin, ctx: &MmArc, tx_hash: H256Json) -> Option { mm_counter!(ctx.metrics, "tx.history.request.count", 1, "coin" => coin.utxo.conf.ticker.clone(), "method" => "get_verbose_transaction"); - match coin - .utxo - .rpc_client - .get_verbose_transaction(tx_hash.clone()) - .compat() - .await - { + match coin.utxo.rpc_client.get_verbose_transaction(&tx_hash).compat().await { Ok(d) => { mm_counter!(ctx.metrics, "tx.history.response.count", 1, "coin" => coin.utxo.conf.ticker.clone(), "method" => "get_verbose_transaction"); Some(d) @@ -460,7 +445,7 @@ impl Qrc20Coin { } if tx.should_update_timestamp() { if qtum_verbose.is_none() { - qtum_verbose = get_verbose_transaction(self, ctx, tx_hash.clone()).await; + qtum_verbose = get_verbose_transaction(self, ctx, *tx_hash).await; } if let Some(ref qtum_verbose) = qtum_verbose { tx.timestamp = qtum_verbose.time as u64; @@ -478,6 +463,10 @@ impl Qrc20Coin { /// Returns true if the `history_map` has been updated. async fn process_tx_ids(&self, ctx: &MmArc, history_map: &mut HistoryMapByHash, tx_ids: TxIds) -> bool { + // Remove transactions in the history_map that are not in the requested transaction list anymore + let requested_ids: HashSet = tx_ids.iter().map(|x| x.0).collect(); + history_map.retain(|hash, _| requested_ids.contains(hash)); + let mut transactions_left = if history_map.len() < tx_ids.len() { tx_ids.len() - history_map.len() } else { @@ -507,7 +496,7 @@ impl Qrc20Coin { // `transfer` details are not initialized for the `tx_hash` // or there is an error in cached `tx_hash_history` mm_counter!(ctx.metrics, "tx.history.request.count", 1, "coin" => self.utxo.conf.ticker.clone(), "method" => "transfer_details_by_hash"); - let tx_hash_history = match self.transfer_details_by_hash(tx_hash.clone()).await { + let tx_hash_history = match self.transfer_details_by_hash(tx_hash).await { Ok(d) => d, Err(e) => { ctx.log.log( @@ -519,7 +508,7 @@ impl Qrc20Coin { }, }; - if history_map.insert(tx_hash.clone(), tx_hash_history).is_some() { + if history_map.insert(tx_hash, tx_hash_history).is_some() { ctx.log.log( "😟", &[&"tx_history", &self.utxo.conf.ticker], @@ -557,7 +546,7 @@ impl Qrc20Coin { return Ok(HistoryMapByHash::default()); }, }; - let tx_hash_history = history_map.entry(id.tx_hash.clone()).or_insert_with(HashMap::default); + let tx_hash_history = history_map.entry(id.tx_hash).or_insert_with(HashMap::default); if tx_hash_history.insert(id, tx).is_some() { ctx.log.log( "😟", @@ -574,7 +563,9 @@ impl Qrc20Coin { pub struct TransferHistoryBuilder { coin: Qrc20Coin, - params: TransferHistoryParams, + from_block: u64, + address: Option, + token_address: H160, } struct TransferHistoryParams { @@ -585,39 +576,62 @@ struct TransferHistoryParams { impl TransferHistoryBuilder { pub fn new(coin: Qrc20Coin) -> TransferHistoryBuilder { - let address = qtum::contract_addr_from_utxo_addr(coin.utxo.my_address.clone()); let token_address = coin.contract_address; - let params = TransferHistoryParams { + TransferHistoryBuilder { + coin, from_block: 0, - address, + address: None, token_address, - }; - TransferHistoryBuilder { coin, params } + } } #[allow(clippy::wrong_self_convention)] pub fn from_block(mut self, from_block: u64) -> TransferHistoryBuilder { - self.params.from_block = from_block; + self.from_block = from_block; self } pub fn address(mut self, address: H160) -> TransferHistoryBuilder { - self.params.address = address; + self.address = Some(address); self } #[allow(dead_code)] pub fn token_address(mut self, token_address: H160) -> TransferHistoryBuilder { - self.params.token_address = token_address; + self.token_address = token_address; self } pub async fn build(self) -> Result, MmError> { - self.coin.utxo.rpc_client.build(self.params).await + let params = self.build_params()?; + self.coin.utxo.rpc_client.build(params).await } pub async fn build_tx_idents(self) -> Result, MmError> { - self.coin.utxo.rpc_client.build_tx_idents(self.params).await + let params = self.build_params()?; + self.coin.utxo.rpc_client.build_tx_idents(params).await + } + + fn build_params(&self) -> Result> { + let address = match self.address { + Some(addr) => addr, + None => { + let my_address = self + .coin + .utxo + .derivation_method + .iguana_or_err() + .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; + qtum::contract_addr_from_utxo_addr(my_address.clone()) + .mm_err(|e| UtxoRpcError::Internal(e.to_string()))? + }, + }; + + Ok(TransferHistoryParams { + from_block: self.from_block, + address, + token_address: self.token_address, + }) } } @@ -658,7 +672,7 @@ impl BuildTransferHistory for ElectrumClient { let mut receipts = Vec::new(); for (tx_hash, _height) in tx_idents { - let mut tx_receipts = self.blochchain_transaction_get_receipt(&tx_hash).compat().await?; + let mut tx_receipts = self.blockchain_transaction_get_receipt(&tx_hash).compat().await?; // remove receipts of contract calls didn't emit at least one `Transfer` event tx_receipts.retain(|receipt| receipt.log.iter().any(is_transfer_event_log)); receipts.extend(tx_receipts.into_iter()); @@ -714,7 +728,7 @@ impl BuildTransferHistory for NativeClient { while from_block <= block_count { let to_block = from_block + SEARCH_LOGS_STEP - 1; let mut receipts = self - .search_logs(from_block, Some(to_block), vec![token_address.clone()], topics.clone()) + .search_logs(from_block, Some(to_block), vec![token_address], topics.clone()) .compat() .await?; @@ -788,17 +802,6 @@ fn is_transferred_to_contract(script_pubkey: &Script) -> bool { } } -fn sort_newest_to_oldest(x_height: u64, y_height: u64) -> Ordering { - // the transactions with block_height == 0 are the most recent - if x_height == 0 { - Ordering::Less - } else if y_height == 0 { - Ordering::Greater - } else { - y_height.cmp(&x_height) - } -} - fn is_transfer_event_log(log: &LogEntry) -> bool { match log.topics.first() { Some(first_topic) => first_topic == QRC20_TRANSFER_TOPIC, @@ -809,8 +812,9 @@ fn is_transfer_event_log(log: &LogEntry) -> bool { #[cfg(test)] mod tests { use super::*; - use common::for_tests::find_metrics_in_json; + use common::block_on; use common::mm_metrics::{MetricType, MetricsJson, MetricsOps}; + use mm2_test_helpers::for_tests::find_metrics_in_json; use qrc20_tests::qrc20_coin_for_test; #[test] diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index e53bb98b6f..f5848056da 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -1,11 +1,15 @@ use super::*; +use crate::utxo::rpc_clients::UnspentInfo; use crate::TxFeeDetails; -use bigdecimal::Zero; use chain::OutPoint; -use common::mm_ctx::MmCtxBuilder; -use common::DEX_FEE_ADDR_RAW_PUBKEY; +use common::{block_on, DEX_FEE_ADDR_RAW_PUBKEY}; use itertools::Itertools; +use mm2_core::mm_ctx::MmCtxBuilder; +use mm2_number::bigdecimal::Zero; use mocktopus::mocking::{MockResult, Mockable}; +use rpc::v1::types::ToTxHash; +use std::convert::TryFrom; +use std::mem::discriminant; const EXPECTED_TX_FEE: i64 = 1000; const CONTRACT_CALL_GAS_FEE: i64 = (QRC20_GAS_LIMIT_DEFAULT * QRC20_GAS_PRICE_DEFAULT) as i64; @@ -19,25 +23,26 @@ pub fn qrc20_coin_for_test(priv_key: &[u8], fallback_swap: Option<&str>) -> (MmA "pubtype":120, "p2shtype":110, "wiftype":128, - "segwit":true, "mm2":1, "mature_confirmations":2000, "dust":72800, }); let req = json!({ "method": "electrum", - "servers": [{"url":"95.217.83.126:10001"}], + "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], "swap_contract_address": "0xba8b71f3544b93e2f681f996da519a98ace0107a", "fallback_swap_contract": fallback_swap, }); let contract_address = "0xd362e096e873eb7907e205fadc6175c6fec7bc44".into(); let ctx = MmCtxBuilder::new().into_mm_arc(); - let coin = block_on(qrc20_coin_from_conf_and_request( + let params = Qrc20ActivationParams::from_legacy_req(&req).unwrap(); + + let coin = block_on(qrc20_coin_from_conf_and_params( &ctx, "QRC20", "QTUM", &conf, - &req, + ¶ms, priv_key, contract_address, )) @@ -50,9 +55,39 @@ fn check_tx_fee(coin: &Qrc20Coin, expected_tx_fee: ActualTxFee) { assert_eq!(actual_tx_fee, expected_tx_fee); } +#[test] +fn test_withdraw_to_p2sh_address_should_fail() { + let priv_key = [ + 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, + 172, 110, 180, 13, 123, 179, 10, 49, + ]; + let (_, coin) = qrc20_coin_for_test(&priv_key, None); + + let p2sh_address = Address { + prefix: coin.as_ref().conf.p2sh_addr_prefix, + hash: coin.as_ref().derivation_method.unwrap_iguana().hash.clone(), + t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, + checksum_type: coin.as_ref().derivation_method.unwrap_iguana().checksum_type, + hrp: coin.as_ref().conf.bech32_hrp.clone(), + addr_format: UtxoAddressFormat::Standard, + }; + + let req = WithdrawRequest { + amount: 10.into(), + from: None, + to: p2sh_address.to_string(), + coin: "QRC20".into(), + max: false, + fee: None, + }; + let err = coin.withdraw(req).wait().unwrap_err().into_inner(); + let expect = WithdrawError::InvalidAddress("QRC20 can be sent to P2PKH addresses only".to_owned()); + assert_eq!(err, expect); +} + #[test] fn test_withdraw_impl_fee_details() { - Qrc20Coin::ordered_mature_unspents.mock_safe(|coin, _| { + Qrc20Coin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -74,6 +109,7 @@ fn test_withdraw_impl_fee_details() { let withdraw_req = WithdrawRequest { amount: 10.into(), + from: None, to: "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(), coin: "QRC20".into(), max: false, @@ -108,87 +144,54 @@ fn test_validate_maker_payment() { ]; let (_ctx, coin) = qrc20_coin_for_test(&priv_key, None); - assert_eq!(coin.utxo.my_address, "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf".into()); + assert_eq!( + *coin.utxo.derivation_method.unwrap_iguana(), + "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf".into() + ); // tx_hash: 016a59dd2b181b3906b0f0333d5c7561dacb332dc99ac39679a591e523f2c49a let payment_tx = hex::decode("010000000194448324c14fc6b78c7a52c59debe3240fc392019dbd6f1457422e3308ce1e75010000006b483045022100800a4956a30a36708536d98e8ea55a3d0983b963af6c924f60241616e2ff056d0220239e622f8ec8f1a0f5ef0fc93ff094a8e6b5aab964a62bed680b17bf6a848aac012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000e35403a0860101284cc49b415b2a0c692f2ec8ebab181a79e31b7baab30fef0902e57f901c47a342643eeafa6b510000000000000000000000000000000000000000000000000000000001312d00000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000783cf0be521101942da509846ea476e683aad8320101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000005f72ec7514ba8b71f3544b93e2f681f996da519a98ace0107ac201319302000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88ac40ed725f").unwrap(); - let time_lock = 1601367157; // pubkey of "cMhHM3PMpMrChygR4bLF7QsTdenhWpFrrmf2UezBG3eeFsz41rtL" passphrase - let maker_pub = hex::decode("03693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9").unwrap(); - let secret_hash = &[1; 20]; - let amount = BigDecimal::from_str("0.2").unwrap(); + let correct_maker_pub = hex::decode("03693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9").unwrap(); + let correct_amount = BigDecimal::from_str("0.2").unwrap(); + + let mut input = ValidatePaymentInput { + payment_tx, + time_lock: 1601367157, + other_pub: correct_maker_pub.clone(), + secret_hash: vec![1; 20], + amount: correct_amount.clone(), + swap_contract_address: coin.swap_contract_address(), + try_spv_proof_until: now_ms() / 1000 + 30, + confirmations: 1, + unique_swap_data: Vec::new(), + }; - coin.validate_maker_payment( - &payment_tx, - time_lock, - &maker_pub, - secret_hash, - amount.clone(), - &coin.swap_contract_address(), - ) - .wait() - .unwrap(); + coin.validate_maker_payment(input.clone()).wait().unwrap(); - let maker_pub_dif = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); - let error = coin - .validate_maker_payment( - &payment_tx, - time_lock, - &maker_pub_dif, - secret_hash, - amount.clone(), - &coin.swap_contract_address(), - ) - .wait() - .unwrap_err(); - log!("error: "[error]); + input.other_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); + let error = coin.validate_maker_payment(input.clone()).wait().unwrap_err(); + log!("error: {:?}", error); assert!( error.contains("Payment tx was sent from wrong address, expected 0x783cf0be521101942da509846ea476e683aad832") ); + input.other_pub = correct_maker_pub; - let amount_dif = BigDecimal::from_str("0.3").unwrap(); - let error = coin - .validate_maker_payment( - &payment_tx, - time_lock, - &maker_pub, - secret_hash, - amount_dif, - &coin.swap_contract_address(), - ) - .wait() - .unwrap_err(); - log!("error: "[error]); + input.amount = BigDecimal::from_str("0.3").unwrap(); + let error = coin.validate_maker_payment(input.clone()).wait().unwrap_err(); + log!("error: {:?}", error); assert!(error.contains("Unexpected 'erc20Payment' contract call bytes")); + input.amount = correct_amount; - let secret_hash_dif = &[2; 20]; - let error = coin - .validate_maker_payment( - &payment_tx, - time_lock, - &maker_pub, - secret_hash_dif, - amount.clone(), - &coin.swap_contract_address(), - ) - .wait() - .unwrap_err(); - log!("error: "[error]); + input.secret_hash = vec![2; 20]; + let error = coin.validate_maker_payment(input.clone()).wait().unwrap_err(); + log!("error: {:?}", error); assert!(error.contains("Payment state is not PAYMENT_STATE_SENT, got 0")); + input.secret_hash = vec![1; 20]; - let time_lock_dif = 123; - let error = coin - .validate_maker_payment( - &payment_tx, - time_lock_dif, - &maker_pub, - secret_hash, - amount, - &coin.swap_contract_address(), - ) - .wait() - .unwrap_err(); - log!("error: "[error]); + input.time_lock = 123; + let error = coin.validate_maker_payment(input).wait().unwrap_err(); + log!("error: {:?}", error); assert!(error.contains("Payment state is not PAYMENT_STATE_SENT, got 0")); } @@ -201,7 +204,10 @@ fn test_wait_for_confirmations_excepted() { ]; let (_ctx, coin) = qrc20_coin_for_test(&priv_key, None); - assert_eq!(coin.utxo.my_address, "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf".into()); + assert_eq!( + *coin.utxo.derivation_method.unwrap_iguana(), + "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf".into() + ); // tx_hash: 35e03bc529528a853ee75dde28f27eec8ed7b152b6af7ab6dfa5d55ea46f25ac // `approve` contract call excepted only, and `erc20Payment` completed @@ -222,7 +228,7 @@ fn test_wait_for_confirmations_excepted() { .wait_for_confirmations(&payment_tx, confirmations, requires_nota, wait_until, check_every) .wait() .unwrap_err(); - log!("error: "[error]); + log!("error: {:?}", error); assert!(error.contains("Contract call failed with an error: Revert")); // tx_hash: aa992c028c07e239dbd2ff32bf67251f026929c644b4d02a469e351cb44abab7 @@ -232,7 +238,7 @@ fn test_wait_for_confirmations_excepted() { .wait_for_confirmations(&payment_tx, confirmations, requires_nota, wait_until, check_every) .wait() .unwrap_err(); - log!("error: "[error]); + log!("error: {:?}", error); assert!(error.contains("Contract call failed with an error: Revert")); } @@ -247,17 +253,24 @@ fn test_send_taker_fee() { let amount = BigDecimal::from_str("0.01").unwrap(); let tx = coin - .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, amount.clone()) + .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, amount.clone(), &[]) .wait() .unwrap(); let tx_hash: H256Json = match tx { TransactionEnum::UtxoTx(ref tx) => tx.hash().reversed().into(), _ => panic!("Expected UtxoTx"), }; - log!("Fee tx "[tx_hash]); + log!("Fee tx {:?}", tx_hash); let result = coin - .validate_fee(&tx, &*coin.utxo.key_pair.public(), &DEX_FEE_ADDR_RAW_PUBKEY, &amount, 0) + .validate_fee( + &tx, + coin.my_public_key().unwrap(), + &DEX_FEE_ADDR_RAW_PUBKEY, + &amount, + 0, + &[], + ) .wait(); assert_eq!(result, Ok(())); } @@ -278,53 +291,53 @@ fn test_validate_fee() { let amount = BigDecimal::from_str("0.01").unwrap(); let result = coin - .validate_fee(&tx, &sender_pub, &DEX_FEE_ADDR_RAW_PUBKEY, &amount, 0) + .validate_fee(&tx, &sender_pub, &DEX_FEE_ADDR_RAW_PUBKEY, &amount, 0, &[]) .wait(); assert_eq!(result, Ok(())); let fee_addr_dif = hex::decode("03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc05").unwrap(); let err = coin - .validate_fee(&tx, &sender_pub, &fee_addr_dif, &amount, 0) + .validate_fee(&tx, &sender_pub, &fee_addr_dif, &amount, 0, &[]) .wait() .err() .expect("Expected an error"); - log!("error: "[err]); + log!("error: {:?}", err); assert!(err.contains("QRC20 Fee tx was sent to wrong address")); let err = coin - .validate_fee(&tx, &DEX_FEE_ADDR_RAW_PUBKEY, &DEX_FEE_ADDR_RAW_PUBKEY, &amount, 0) + .validate_fee(&tx, &DEX_FEE_ADDR_RAW_PUBKEY, &DEX_FEE_ADDR_RAW_PUBKEY, &amount, 0, &[]) .wait() .err() .expect("Expected an error"); - log!("error: "[err]); + log!("error: {:?}", err); assert!(err.contains("was sent from wrong address")); let err = coin - .validate_fee(&tx, &sender_pub, &DEX_FEE_ADDR_RAW_PUBKEY, &amount, 2000000) + .validate_fee(&tx, &sender_pub, &DEX_FEE_ADDR_RAW_PUBKEY, &amount, 2000000, &[]) .wait() .err() .expect("Expected an error"); - log!("error: "[err]); + log!("error: {:?}", err); assert!(err.contains("confirmed before min_block")); let amount_dif = BigDecimal::from_str("0.02").unwrap(); let err = coin - .validate_fee(&tx, &sender_pub, &DEX_FEE_ADDR_RAW_PUBKEY, &amount_dif, 0) + .validate_fee(&tx, &sender_pub, &DEX_FEE_ADDR_RAW_PUBKEY, &amount_dif, 0, &[]) .wait() .err() .expect("Expected an error"); - log!("error: "[err]); + log!("error: {:?}", err); assert!(err.contains("QRC20 Fee tx value 1000000 is less than expected 2000000")); // QTUM tx "8a51f0ffd45f34974de50f07c5bf2f0949da4e88433f8f75191953a442cf9310" let tx = TransactionEnum::UtxoTx("020000000113640281c9332caeddd02a8dd0d784809e1ad87bda3c972d89d5ae41f5494b85010000006a47304402207c5c904a93310b8672f4ecdbab356b65dd869a426e92f1064a567be7ccfc61ff02203e4173b9467127f7de4682513a21efb5980e66dbed4da91dff46534b8e77c7ef012102baefe72b3591de2070c0da3853226b00f082d72daa417688b61cb18c1d543d1afeffffff020001b2c4000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acbc4dd20c2f0000001976a9144208fa7be80dcf972f767194ad365950495064a488ac76e70800".into()); let sender_pub = hex::decode("02baefe72b3591de2070c0da3853226b00f082d72daa417688b61cb18c1d543d1a").unwrap(); let err = coin - .validate_fee(&tx, &sender_pub, &DEX_FEE_ADDR_RAW_PUBKEY, &amount, 0) + .validate_fee(&tx, &sender_pub, &DEX_FEE_ADDR_RAW_PUBKEY, &amount, 0, &[]) .wait() .err() .expect("Expected an error"); - log!("error: "[err]); + log!("error: {:?}", err); assert!(err.contains("Expected 'transfer' contract call")); } @@ -422,7 +435,7 @@ fn test_generate_token_transfer_script_pubkey() { }; let to_addr: UtxoAddress = "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(); - let to_addr = qtum::contract_addr_from_utxo_addr(to_addr); + let to_addr = qtum::contract_addr_from_utxo_addr(to_addr).unwrap(); let amount: U256 = 1000000000.into(); let actual = coin .transfer_output(to_addr.clone(), amount, gas_limit, gas_price) @@ -475,11 +488,10 @@ fn test_transfer_details_by_hash() { }; // qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8 is UTXO representation of 1549128bbfb33b997949b4105b6a6371c998e212 contract address - let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.clone().into(), + tx_hash: tx_hash_bytes.to_tx_hash(), from: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], to: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], total_amount: BigDecimal::from_str("0.003").unwrap(), @@ -496,13 +508,14 @@ fn test_transfer_details_by_hash() { .unwrap() .into(), kmd_rewards: None, + transaction_type: Default::default(), }; assert_eq!(actual, expected); let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.clone().into(), + tx_hash: tx_hash_bytes.to_tx_hash(), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00295").unwrap(), @@ -519,13 +532,14 @@ fn test_transfer_details_by_hash() { .unwrap() .into(), kmd_rewards: None, + transaction_type: Default::default(), }; assert_eq!(actual, expected); let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.clone().into(), + tx_hash: tx_hash_bytes.to_tx_hash(), from: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], to: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], total_amount: BigDecimal::from_str("0.003").unwrap(), @@ -542,13 +556,14 @@ fn test_transfer_details_by_hash() { .unwrap() .into(), kmd_rewards: None, + transaction_type: Default::default(), }; assert_eq!(actual, expected); let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.clone().into(), + tx_hash: tx_hash_bytes.to_tx_hash(), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00295").unwrap(), @@ -565,13 +580,14 @@ fn test_transfer_details_by_hash() { .unwrap() .into(), kmd_rewards: None, + transaction_type: Default::default(), }; assert_eq!(actual, expected); let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.clone().into(), + tx_hash: tx_hash_bytes.to_tx_hash(), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00005000").unwrap(), @@ -588,6 +604,7 @@ fn test_transfer_details_by_hash() { .unwrap() .into(), kmd_rewards: None, + transaction_type: Default::default(), }; assert_eq!(actual, expected); assert!(it.next().is_none()); @@ -640,10 +657,9 @@ fn test_sender_trade_preimage_zero_allowance() { ); let sender_refund_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + EXPECTED_TX_FEE, coin.utxo.decimals); - let actual = coin - .get_sender_trade_fee(TradePreimageValue::Exact(1.into()), FeeApproxStage::WithoutApprox) - .wait() - .expect("!get_sender_trade_fee"); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(1.into()), FeeApproxStage::WithoutApprox)) + .expect("!get_sender_trade_fee"); // one `approve` contract call should be included into the expected trade fee let expected = TradeFee { coin: "QTUM".to_owned(), @@ -679,10 +695,11 @@ fn test_sender_trade_preimage_with_allowance() { ); let sender_refund_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + EXPECTED_TX_FEE, coin.utxo.decimals); - let actual = coin - .get_sender_trade_fee(TradePreimageValue::Exact(2.5.into()), FeeApproxStage::WithoutApprox) - .wait() - .expect("!get_sender_trade_fee"); + let actual = block_on(coin.get_sender_trade_fee( + TradePreimageValue::Exact(BigDecimal::try_from(2.5).unwrap()), + FeeApproxStage::WithoutApprox, + )) + .expect("!get_sender_trade_fee"); // the expected fee should not include any `approve` contract call let expected = TradeFee { coin: "QTUM".to_owned(), @@ -691,10 +708,11 @@ fn test_sender_trade_preimage_with_allowance() { }; assert_eq!(actual, expected); - let actual = coin - .get_sender_trade_fee(TradePreimageValue::Exact(3.5.into()), FeeApproxStage::WithoutApprox) - .wait() - .expect("!get_sender_trade_fee"); + let actual = block_on(coin.get_sender_trade_fee( + TradePreimageValue::Exact(BigDecimal::try_from(3.5).unwrap()), + FeeApproxStage::WithoutApprox, + )) + .expect("!get_sender_trade_fee"); // two `approve` contract calls should be included into the expected trade fee let expected = TradeFee { coin: "QTUM".to_owned(), @@ -704,6 +722,63 @@ fn test_sender_trade_preimage_with_allowance() { assert_eq!(actual, expected); } +#[test] +fn test_get_sender_trade_fee_preimage_for_correct_ticker() { + // where balance for tQTUM is at 0 for the priv_key below (using 0 bal just to make sure we get required(TradePreimageError::NotSufficientBalance) result for get_sender_trade_fee) + let priv_key = [ + 48, 88, 65, 23, 20, 154, 63, 74, 243, 8, 108, 134, 154, 199, 60, 197, 51, 238, 7, 68, 199, 14, 127, 221, 89, + 80, 37, 174, 221, 178, 233, 65, + ]; + let conf = json!({ + "coin":"QRC20", + "required_confirmations":0, + "pubtype":120, + "p2shtype":110, + "wiftype":128, + "mm2":1, + "mature_confirmations":2000, + "protocol": { + "type": "QRC20", + "protocol_data": { + "platform": "tQTUM", + "contract_address": "0xd362e096e873eb7907e205fadc6175c6fec7bc44" + } + } + }); + let req = json!({ + "method": "electrum", + "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], + "swap_contract_address": "0xba8b71f3544b93e2f681f996da519a98ace0107a", + }); + + let contract_address = "0xd362e096e873eb7907e205fadc6175c6fec7bc44".into(); + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = Qrc20ActivationParams::from_legacy_req(&req).unwrap(); + + let coin = block_on(qrc20_coin_from_conf_and_params( + &ctx, + "QRC20", + "tQTUM", + &conf, + ¶ms, + &priv_key, + contract_address, + )) + .unwrap(); + + let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(0.into()), FeeApproxStage::OrderIssue)) + .err() + .unwrap() + .into_inner(); + // expecting TradePreimageError::NotSufficientBalance + let expected = TradePreimageError::NotSufficientBalance { + coin: "tQTUM".to_string(), + available: BigDecimal::from_str("0").unwrap(), + required: BigDecimal::from_str("0.08").unwrap(), + }; + assert_eq!(expected, actual); +} + /// `receiverSpend` should be included in the estimated trade fee. #[test] fn test_receiver_trade_preimage() { @@ -744,15 +819,13 @@ fn test_taker_fee_tx_fee() { // check if the coin's tx fee is expected check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); let expected_balance = CoinBalance { - spendable: BigDecimal::from(5), - unspendable: BigDecimal::from(0), + spendable: BigDecimal::from(5u32), + unspendable: BigDecimal::from(0u32), }; assert_eq!(coin.my_balance().wait().expect("!my_balance"), expected_balance); - let dex_fee_amount = BigDecimal::from(5); - let actual = coin - .get_fee_to_send_taker_fee(dex_fee_amount, FeeApproxStage::WithoutApprox) - .wait() + let dex_fee_amount = BigDecimal::from(5u32); + let actual = block_on(coin.get_fee_to_send_taker_fee(dex_fee_amount, FeeApproxStage::WithoutApprox)) .expect("!get_fee_to_send_taker_fee"); // only one contract call should be included into the expected trade fee let expected_receiver_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + EXPECTED_TX_FEE, coin.utxo.decimals); @@ -777,24 +850,25 @@ fn test_coin_from_conf_without_decimals() { "pubtype":120, "p2shtype":110, "wiftype":128, - "segwit":true, "mm2":1, "mature_confirmations":2000, }); let req = json!({ "method": "electrum", - "servers": [{"url":"95.217.83.126:10001"}], + "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], "swap_contract_address": "0xba8b71f3544b93e2f681f996da519a98ace0107a", }); // 0459c999c3edf05e73c83f3fbae9f0f020919f91 has 12 decimals instead of standard 8 let contract_address = "0x0459c999c3edf05e73c83f3fbae9f0f020919f91".into(); let ctx = MmCtxBuilder::new().into_mm_arc(); - let coin = block_on(qrc20_coin_from_conf_and_request( + let params = Qrc20ActivationParams::from_legacy_req(&req).unwrap(); + + let coin = block_on(qrc20_coin_from_conf_and_params( &ctx, "QRC20", "QTUM", &conf, - &req, + ¶ms, &priv_key, contract_address, )) @@ -857,24 +931,28 @@ fn test_validate_maker_payment_malicious() { // Malicious tx 81540dc6abe59cf1e301a97a7e1c9b66d5f475da916faa3f0ef7ea896c0b3e5a let payment_tx = hex::decode("01000000010144e2b8b5e6da0666faf1db95075653ef49e2acaa8924e1ec595f6b89a6f715050000006a4730440220415adec5e24148db8e9654a6beda4b1af8aded596ab1cd8667af32187853e8f5022007a91d44ee13046194aafc07ca46ec44f770e75b41187acaa4e38e17d4eccb5d012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff030000000000000000625403a08601012844095ea7b300000000000000000000000085a4df739bbb2d247746bea611d5d365204725830000000000000000000000000000000000000000000000000000000005f5e10014d362e096e873eb7907e205fadc6175c6fec7bc44c20000000000000000e35403a0860101284cc49b415b2a0a1a8b4af2762154115ced87e2424b3cb940c0181cc3c850523702f1ec298fef0000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000783cf0be521101942da509846ea476e683aad8324b6b2e5444c2639cc0fb7bcea5afba3f3cdce239000000000000000000000000000000000000000000000000000000000000000000000000000000005fa0fffb1485a4df739bbb2d247746bea611d5d36520472583c208535c01000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acc700a15f").unwrap(); - let time_lock = 1604386811; let maker_pub = hex::decode("03693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9").unwrap(); let secret = &[1; 32]; - let secret_hash = &*dhash160(secret); + let secret_hash = dhash160(secret).to_vec(); let amount = BigDecimal::from_str("1").unwrap(); + + let input = ValidatePaymentInput { + payment_tx, + time_lock: 1604386811, + secret_hash, + amount, + swap_contract_address: coin.swap_contract_address(), + try_spv_proof_until: now_ms() / 1000 + 30, + confirmations: 1, + other_pub: maker_pub, + unique_swap_data: Vec::new(), + }; let error = coin - .validate_maker_payment( - &payment_tx, - time_lock, - &maker_pub, - secret_hash, - amount, - &coin.swap_contract_address(), - ) + .validate_maker_payment(input) .wait() .err() .expect("'erc20Payment' was called from another swap contract, expected an error"); - log!("error: "(error)); + log!("error: {}", error); assert!(error.contains("Unexpected amount 1000 in 'Transfer' event, expected 100000000")); } @@ -938,3 +1016,35 @@ fn test_negotiate_swap_contract_addr_has_fallback() { let result = coin.negotiate_swap_contract_addr(Some(slice)).unwrap(); assert_eq!(Some(fallback_addr.to_vec().into()), result); } + +#[test] +fn test_send_contract_calls_recoverable_tx() { + let priv_key = [ + 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, + 172, 110, 180, 13, 123, 179, 10, 49, + ]; + let (_ctx, coin) = qrc20_coin_for_test(&priv_key, None); + + let tx = TransactionEnum::UtxoTx("010000000160fd74b5714172f285db2b36f0b391cd6883e7291441631c8b18f165b0a4635d020000006a47304402205d409e141111adbc4f185ae856997730de935ac30a0d2b1ccb5a6c4903db8171022024fc59bbcfdbba283556d7eeee4832167301dc8e8ad9739b7865f67b9676b226012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000625403a08601012844a9059cbb000000000000000000000000ca1e04745e8ca0c60d8c5881531d51bec470743f00000000000000000000000000000000000000000000000000000000000f424014d362e096e873eb7907e205fadc6175c6fec7bc44c200ada205000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acfe967d5f".into()); + + let fee_addr = hex::decode("03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc05").unwrap(); + let to_address = coin.contract_address_from_raw_pubkey(&fee_addr).unwrap(); + let amount = BigDecimal::try_from(0.2).unwrap(); + let amount = wei_from_big_decimal(&amount, coin.utxo.decimals).unwrap(); + let mut transfer_output = coin + .transfer_output(to_address, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT) + .unwrap(); + + // break the transfer output + transfer_output.value = 777; + transfer_output.gas_limit = 777; + transfer_output.gas_price = 777; + + let tx_err = block_on(coin.send_contract_calls(vec![transfer_output])).unwrap_err(); + + // The error variant should equal to `TxRecoverable` + assert_eq!( + discriminant(&tx_err), + discriminant(&TransactionErr::TxRecoverable(TransactionEnum::from(tx), String::new())) + ); +} diff --git a/mm2src/coins/qrc20/rpc_clients.rs b/mm2src/coins/qrc20/rpc_clients.rs index 61a2da8c45..4feb1848a1 100644 --- a/mm2src/coins/qrc20/rpc_clients.rs +++ b/mm2src/coins/qrc20/rpc_clients.rs @@ -139,6 +139,7 @@ pub struct ExecutionResult { #[derive(Debug, Deserialize)] pub struct ContractCallResult { + #[allow(dead_code)] address: H160Json, #[serde(rename = "executionResult")] pub execution_result: ExecutionResult, @@ -185,7 +186,7 @@ impl ViewContractCallType { /// The structure is the same as Qtum Core RPC gettransactionreceipt returned data. /// https://docs.qtum.site/en/Qtum-RPC-API/#gettransactionreceipt -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct TxReceipt { /// Hash of the block this transaction was included within. #[serde(rename = "blockHash")] @@ -225,7 +226,7 @@ pub struct TxReceipt { pub excepted_message: Option, } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct LogEntry { /// Contract address. pub address: String, @@ -277,7 +278,7 @@ pub trait Qrc20ElectrumOps { /// This can be used to get eventlogs in the transaction, the returned data is the same as Qtum Core RPC gettransactionreceipt. /// from the eventlogs, we can get QRC20 Token transafer informations(from, to, amount). /// https://github.com/qtumproject/qtum-electrumx-server/blob/master/docs/qrc20-integration.md#blochchaintransactionget_receipttxid - fn blochchain_transaction_get_receipt(&self, hash: &H256Json) -> RpcRes>; + fn blockchain_transaction_get_receipt(&self, hash: &H256Json) -> RpcRes>; } pub trait Qrc20NativeOps { @@ -359,7 +360,7 @@ impl Qrc20ElectrumOps for ElectrumClient { ) } - fn blochchain_transaction_get_receipt(&self, hash: &H256Json) -> RpcRes> { + fn blockchain_transaction_get_receipt(&self, hash: &H256Json) -> RpcRes> { rpc_func!(self, "blochchain.transaction.get_receipt", hash) } } @@ -380,7 +381,7 @@ pub trait Qrc20RpcOps { impl Qrc20RpcOps for UtxoRpcClientEnum { fn get_transaction_receipts(&self, tx_hash: &H256) -> RpcRes> { match self { - UtxoRpcClientEnum::Electrum(electrum) => electrum.blochchain_transaction_get_receipt(tx_hash), + UtxoRpcClientEnum::Electrum(electrum) => electrum.blockchain_transaction_get_receipt(tx_hash), UtxoRpcClientEnum::Native(native) => native.get_transaction_receipt(tx_hash), } } diff --git a/mm2src/coins/qrc20/script_pubkey.rs b/mm2src/coins/qrc20/script_pubkey.rs index caf3793184..2cbb4bba9e 100644 --- a/mm2src/coins/qrc20/script_pubkey.rs +++ b/mm2src/coins/qrc20/script_pubkey.rs @@ -214,8 +214,8 @@ mod tests { (-256, vec![0, 129]), (2500000, vec![160, 37, 38]), (-2500000, vec![160, 37, 166]), - (i64::max_value(), vec![255, 255, 255, 255, 255, 255, 255, 127]), - (i64::min_value(), vec![0, 0, 0, 0, 0, 0, 0, 128, 128]), + (i64::MAX, vec![255, 255, 255, 255, 255, 255, 255, 127]), + (i64::MIN, vec![0, 0, 0, 0, 0, 0, 0, 128, 128]), (Opcode::OP_4 as i64, vec![84]), (Opcode::OP_CALL as i64, vec![194, 0]), ]; @@ -247,7 +247,7 @@ mod tests { let script: Script = "5403a02526012844a9059cbb0000000000000000000000000240b898276ad2cc0d2fe6f527e8e31104e7fde3000000000000000000000000000000000000000000000000000000003b9aca0014d362e096e873eb7907e205fadc6175c6fec7bc44c2".into(); let to_addr: UtxoAddress = "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(); - let to_addr = qtum::contract_addr_from_utxo_addr(to_addr); + let to_addr = qtum::contract_addr_from_utxo_addr(to_addr).unwrap(); let amount: U256 = 1000000000.into(); let function = eth::ERC20_CONTRACT.function("transfer").unwrap(); let expected = function diff --git a/mm2src/coins/qrc20/swap.rs b/mm2src/coins/qrc20/swap.rs index 3f790a97dc..098ddd8ab9 100644 --- a/mm2src/coins/qrc20/swap.rs +++ b/mm2src/coins/qrc20/swap.rs @@ -37,16 +37,16 @@ impl Qrc20Coin { secret_hash: Vec, receiver_addr: H160, swap_contract_address: H160, - ) -> Result { - let balance = try_s!(self.my_spendable_balance().compat().await); - let balance = try_s!(wei_from_big_decimal(&balance, self.utxo.decimals)); + ) -> Result { + let balance = try_tx_s!(self.my_spendable_balance().compat().await); + let balance = try_tx_s!(wei_from_big_decimal(&balance, self.utxo.decimals)); // Check the balance to avoid unnecessary burning of gas if balance < value { - return ERR!("Balance {} is less than value {}", balance, value); + return TX_PLAIN_ERR!("Balance {} is less than value {}", balance, value); } - let outputs = try_s!( + let outputs = try_tx_s!( self.generate_swap_payment_outputs( balance, id, @@ -67,17 +67,18 @@ impl Qrc20Coin { payment_tx: UtxoTx, swap_contract_address: H160, secret: Vec, - ) -> Result { + ) -> Result { let Erc20PaymentDetails { swap_id, value, sender, .. - } = try_s!(self.erc20_payment_details_from_tx(&payment_tx).await); + } = try_tx_s!(self.erc20_payment_details_from_tx(&payment_tx).await); - let status = try_s!(self.payment_status(&swap_contract_address, swap_id.clone()).await); + let status = try_tx_s!(self.payment_status(&swap_contract_address, swap_id.clone()).await); if status != eth::PAYMENT_STATE_SENT.into() { - return ERR!("Payment state is not PAYMENT_STATE_SENT, got {}", status); + return TX_PLAIN_ERR!("Payment state is not PAYMENT_STATE_SENT, got {}", status); } - let spend_output = try_s!(self.receiver_spend_output(&swap_contract_address, swap_id, value, secret, sender)); + let spend_output = + try_tx_s!(self.receiver_spend_output(&swap_contract_address, swap_id, value, secret, sender)); self.send_contract_calls(vec![spend_output]).await } @@ -85,22 +86,22 @@ impl Qrc20Coin { &self, swap_contract_address: H160, payment_tx: UtxoTx, - ) -> Result { + ) -> Result { let Erc20PaymentDetails { swap_id, value, receiver, secret_hash, .. - } = try_s!(self.erc20_payment_details_from_tx(&payment_tx).await); + } = try_tx_s!(self.erc20_payment_details_from_tx(&payment_tx).await); - let status = try_s!(self.payment_status(&swap_contract_address, swap_id.clone()).await); + let status = try_tx_s!(self.payment_status(&swap_contract_address, swap_id.clone()).await); if status != eth::PAYMENT_STATE_SENT.into() { - return ERR!("Payment state is not PAYMENT_STATE_SENT, got {}", status); + return TX_PLAIN_ERR!("Payment state is not PAYMENT_STATE_SENT, got {}", status); } let refund_output = - try_s!(self.sender_refund_output(&swap_contract_address, swap_id, value, secret_hash, receiver)); + try_tx_s!(self.sender_refund_output(&swap_contract_address, swap_id, value, secret_hash, receiver)); self.send_contract_calls(vec![refund_output]).await } @@ -124,7 +125,8 @@ impl Qrc20Coin { let expected_call_bytes = { let expected_value = try_s!(wei_from_big_decimal(&amount, self.utxo.decimals)); - let expected_receiver = qtum::contract_addr_from_utxo_addr(self.utxo.my_address.clone()); + let my_address = try_s!(self.utxo.derivation_method.iguana_or_err()).clone(); + let expected_receiver = try_s!(qtum::contract_addr_from_utxo_addr(my_address)); try_s!(self.erc20_payment_call_bytes( expected_swap_id, expected_value, @@ -163,7 +165,13 @@ impl Qrc20Coin { expected_value: U256, min_block_number: u64, ) -> Result<(), String> { - let verbose_tx = try_s!(self.utxo.rpc_client.get_verbose_transaction(fee_tx_hash).compat().await); + let verbose_tx = try_s!( + self.utxo + .rpc_client + .get_verbose_transaction(&fee_tx_hash) + .compat() + .await + ); let conf_before_block = utxo_common::is_tx_confirmed_before_block(self, &verbose_tx, min_block_number); if try_s!(conf_before_block.await) { return ERR!( @@ -213,18 +221,18 @@ impl Qrc20Coin { pub async fn search_for_swap_tx_spend( &self, time_lock: u32, - secret_hash: Vec, + secret_hash: &[u8], tx: UtxoTx, search_from_block: u64, ) -> Result, String> { let tx_hash = tx.hash().reversed().into(); - let verbose_tx = try_s!(self.utxo.rpc_client.get_verbose_transaction(tx_hash).compat().await); + let verbose_tx = try_s!(self.utxo.rpc_client.get_verbose_transaction(&tx_hash).compat().await); if verbose_tx.confirmations < 1 { return ERR!("'erc20Payment' was not confirmed yet. Please wait for at least one confirmation"); } let Erc20PaymentDetails { swap_id, receiver, .. } = try_s!(self.erc20_payment_details_from_tx(&tx).await); - let expected_swap_id = qrc20_swap_id(time_lock, &secret_hash); + let expected_swap_id = qrc20_swap_id(time_lock, secret_hash); if expected_swap_id != swap_id { return ERR!("Unexpected swap_id {}", hex::encode(swap_id)); } @@ -233,13 +241,14 @@ impl Qrc20Coin { let spend_txs = try_s!(self.receiver_spend_transactions(receiver, search_from_block).await); let found = spend_txs .into_iter() - .find(|tx| find_receiver_spend_with_swap_id_and_secret_hash(tx, &expected_swap_id, &secret_hash).is_some()); + .find(|tx| find_receiver_spend_with_swap_id_and_secret_hash(tx, &expected_swap_id, secret_hash).is_some()); if let Some(spent_tx) = found { return Ok(Some(FoundSwapTxSpend::Spent(TransactionEnum::UtxoTx(spent_tx)))); } // Else try to find a 'senderRefund' contract call. - let sender = qtum::contract_addr_from_utxo_addr(self.utxo.my_address.clone()); + let my_address = try_s!(self.utxo.derivation_method.iguana_or_err()).clone(); + let sender = try_s!(qtum::contract_addr_from_utxo_addr(my_address)); let refund_txs = try_s!(self.sender_refund_transactions(sender, search_from_block).await); let found = refund_txs.into_iter().find(|tx| { find_swap_contract_call_with_swap_id(MutContractCallType::SenderRefund, tx, &expected_swap_id).is_some() @@ -262,7 +271,8 @@ impl Qrc20Coin { return Ok(None); }; - let sender = qtum::contract_addr_from_utxo_addr(self.utxo.my_address.clone()); + let my_address = try_s!(self.utxo.derivation_method.iguana_or_err()).clone(); + let sender = try_s!(qtum::contract_addr_from_utxo_addr(my_address)); let erc20_payment_txs = try_s!(self.erc20_payment_transactions(sender, search_from_block).await); let found = erc20_payment_txs .into_iter() @@ -341,14 +351,21 @@ impl Qrc20Coin { wait_until: u64, check_every: u64, ) -> Result<(), String> { + let tx_hash = H256Json::from(qtum_tx.hash().reversed()); try_s!( self.utxo .rpc_client - .wait_for_confirmations(&qtum_tx, confirmations as u32, requires_nota, wait_until, check_every) + .wait_for_confirmations( + tx_hash, + qtum_tx.expiry_height, + confirmations as u32, + requires_nota, + wait_until, + check_every + ) .compat() .await ); - let tx_hash = qtum_tx.hash().reversed().into(); let receipts = try_s!(self.utxo.rpc_client.get_transaction_receipts(&tx_hash).compat().await); for receipt in receipts { @@ -416,11 +433,19 @@ impl Qrc20Coin { } pub async fn allowance(&self, spender: H160) -> UtxoRpcResult { + let my_address = self + .utxo + .derivation_method + .iguana_or_err() + .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; let tokens = self .utxo .rpc_client .rpc_contract_call(ViewContractCallType::Allowance, &self.contract_address, &[ - Token::Address(qtum::contract_addr_from_utxo_addr(self.utxo.my_address.clone())), + Token::Address( + qtum::contract_addr_from_utxo_addr(my_address.clone()) + .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?, + ), Token::Address(spender), ]) .compat() @@ -771,7 +796,7 @@ impl Qrc20Coin { let verbose_tx = try_s!( self.utxo .rpc_client - .get_verbose_transaction(receipt.transaction_hash) + .get_verbose_transaction(&receipt.transaction_hash) .compat() .await ); diff --git a/mm2src/coins/rpc_command/account_balance.rs b/mm2src/coins/rpc_command/account_balance.rs new file mode 100644 index 0000000000..4c29383d1e --- /dev/null +++ b/mm2src/coins/rpc_command/account_balance.rs @@ -0,0 +1,111 @@ +use crate::coin_balance::HDAddressBalance; +use crate::hd_wallet::HDWalletCoinOps; +use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; +use crate::{lp_coinfind_or_err, CoinBalance, CoinWithDerivationMethod, MmCoinEnum}; +use async_trait::async_trait; +use common::PagingOptionsEnum; +use crypto::{Bip44Chain, RpcDerivationPath}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use std::fmt; + +#[derive(Deserialize)] +pub struct HDAccountBalanceRequest { + coin: String, + #[serde(flatten)] + params: AccountBalanceParams, +} + +#[derive(Deserialize)] +pub struct AccountBalanceParams { + pub account_index: u32, + pub chain: Bip44Chain, + #[serde(default = "common::ten")] + pub limit: usize, + #[serde(default)] + pub paging_options: PagingOptionsEnum, +} + +#[derive(Debug, PartialEq, Serialize)] +pub struct HDAccountBalanceResponse { + pub account_index: u32, + pub derivation_path: RpcDerivationPath, + pub addresses: Vec, + pub page_balance: CoinBalance, + pub limit: usize, + pub skipped: u32, + pub total: u32, + pub total_pages: usize, + pub paging_options: PagingOptionsEnum, +} + +#[async_trait] +pub trait AccountBalanceRpcOps { + async fn account_balance_rpc( + &self, + params: AccountBalanceParams, + ) -> MmResult; +} + +pub async fn account_balance( + ctx: MmArc, + req: HDAccountBalanceRequest, +) -> MmResult { + match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::UtxoCoin(utxo) => utxo.account_balance_rpc(req.params).await, + MmCoinEnum::QtumCoin(qtum) => qtum.account_balance_rpc(req.params).await, + _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), + } +} + +pub mod common_impl { + use super::*; + use crate::coin_balance::HDWalletBalanceOps; + use crate::hd_wallet::{HDAccountOps, HDWalletOps}; + use common::calc_total_pages; + + pub async fn account_balance_rpc( + coin: &Coin, + params: AccountBalanceParams, + ) -> MmResult + where + Coin: HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync, + ::Address: fmt::Display + Clone, + { + let account_id = params.account_index; + let hd_account = coin + .derivation_method() + .hd_wallet_or_err()? + .get_account(account_id) + .await + .or_mm_err(|| HDAccountBalanceRpcError::UnknownAccount { account_id })?; + let total_addresses_number = hd_account.known_addresses_number(params.chain)?; + + let from_address_id = match params.paging_options { + PagingOptionsEnum::FromId(from_address_id) => from_address_id + 1, + PagingOptionsEnum::PageNumber(page_number) => ((page_number.get() - 1) * params.limit) as u32, + }; + let to_address_id = std::cmp::min(from_address_id + params.limit as u32, total_addresses_number); + + let addresses = coin + .known_addresses_balances_with_ids(&hd_account, params.chain, from_address_id..to_address_id) + .await?; + let page_balance = addresses.iter().fold(CoinBalance::default(), |total, addr_balance| { + total + addr_balance.balance.clone() + }); + + let result = HDAccountBalanceResponse { + account_index: account_id, + derivation_path: RpcDerivationPath(hd_account.account_derivation_path()), + addresses, + page_balance, + limit: params.limit, + skipped: std::cmp::min(from_address_id, total_addresses_number), + total: total_addresses_number, + total_pages: calc_total_pages(total_addresses_number as usize, params.limit), + paging_options: params.paging_options, + }; + + Ok(result) + } +} diff --git a/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs new file mode 100644 index 0000000000..851d2b42d8 --- /dev/null +++ b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs @@ -0,0 +1,106 @@ +use crate::hd_wallet::{AddressDerivingError, InvalidBip44ChainError}; +use crate::{BalanceError, CoinFindError, UnexpectedDerivationMethod}; +use common::HttpStatusCode; +use crypto::Bip44Chain; +use derive_more::Display; +use http::StatusCode; +use rpc_task::RpcTaskError; +use std::time::Duration; + +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum HDAccountBalanceRpcError { + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "RPC timed out {:?}", _0)] + Timeout(Duration), + #[display(fmt = "Coin is expected to be activated with the HD wallet derivation method")] + CoinIsActivatedNotWithHDWallet, + #[display(fmt = "HD account '{}' is not activated", account_id)] + UnknownAccount { account_id: u32 }, + #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] + InvalidBip44Chain { chain: Bip44Chain }, + #[display(fmt = "Error deriving an address: {}", _0)] + ErrorDerivingAddress(String), + #[display(fmt = "Wallet storage error: {}", _0)] + WalletStorageError(String), + #[display(fmt = "Electrum/Native RPC invalid response: {}", _0)] + RpcInvalidResponse(String), + #[display(fmt = "Transport: {}", _0)] + Transport(String), + #[display(fmt = "Internal: {}", _0)] + Internal(String), +} + +impl HttpStatusCode for HDAccountBalanceRpcError { + fn status_code(&self) -> StatusCode { + match self { + HDAccountBalanceRpcError::NoSuchCoin { .. } + | HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet + | HDAccountBalanceRpcError::UnknownAccount { .. } + | HDAccountBalanceRpcError::InvalidBip44Chain { .. } + | HDAccountBalanceRpcError::ErrorDerivingAddress(_) => StatusCode::BAD_REQUEST, + HDAccountBalanceRpcError::Timeout(_) => StatusCode::REQUEST_TIMEOUT, + HDAccountBalanceRpcError::Transport(_) + | HDAccountBalanceRpcError::WalletStorageError(_) + | HDAccountBalanceRpcError::RpcInvalidResponse(_) + | HDAccountBalanceRpcError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => HDAccountBalanceRpcError::NoSuchCoin { coin }, + } + } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: UnexpectedDerivationMethod) -> Self { + match e { + UnexpectedDerivationMethod::HDWalletUnavailable => HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet, + unexpected_error => HDAccountBalanceRpcError::Internal(unexpected_error.to_string()), + } + } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: BalanceError) -> Self { + match e { + BalanceError::Transport(transport) => HDAccountBalanceRpcError::Transport(transport), + BalanceError::InvalidResponse(rpc) => HDAccountBalanceRpcError::RpcInvalidResponse(rpc), + BalanceError::UnexpectedDerivationMethod(der_method) => HDAccountBalanceRpcError::from(der_method), + BalanceError::WalletStorageError(e) => HDAccountBalanceRpcError::Internal(e), + BalanceError::Internal(internal) => HDAccountBalanceRpcError::Internal(internal), + } + } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: InvalidBip44ChainError) -> Self { HDAccountBalanceRpcError::InvalidBip44Chain { chain: e.chain } } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::Bip32Error(bip32) => { + HDAccountBalanceRpcError::ErrorDerivingAddress(bip32.to_string()) + }, + } + } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: RpcTaskError) -> Self { + match e { + RpcTaskError::Canceled => HDAccountBalanceRpcError::Internal("Canceled".to_owned()), + RpcTaskError::Timeout(timeout) => HDAccountBalanceRpcError::Timeout(timeout), + RpcTaskError::NoSuchTask(_) | RpcTaskError::UnexpectedTaskStatus { .. } => { + HDAccountBalanceRpcError::Internal(e.to_string()) + }, + RpcTaskError::Internal(internal) => HDAccountBalanceRpcError::Internal(internal), + } + } +} diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs new file mode 100644 index 0000000000..89667aa649 --- /dev/null +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -0,0 +1,197 @@ +use crate::coin_balance::HDAccountBalance; +use crate::hd_pubkey::{HDXPubExtractor, RpcTaskXPubExtractor}; +use crate::hd_wallet::HDWalletRpcError; +use crate::{lp_coinfind_or_err, CoinBalance, CoinWithDerivationMethod, CoinsContext, MmCoinEnum}; +use async_trait::async_trait; +use common::{true_f, SuccessResponse}; +use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction, HwRpcTaskUserActionRequest}; +use crypto::RpcDerivationPath; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc_task::rpc_common::{InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; +use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; + +pub type CreateAccountUserAction = HwRpcTaskUserAction; +pub type CreateAccountAwaitingStatus = HwRpcTaskAwaitingStatus; +pub type CreateAccountTaskManager = RpcTaskManager; +pub type CreateAccountTaskManagerShared = RpcTaskManagerShared; +pub type CreateAccountTaskHandle = RpcTaskHandle; +pub type CreateAccountRpcTaskStatus = + RpcTaskStatus; + +type CreateAccountXPubExtractor<'task> = RpcTaskXPubExtractor<'task, InitCreateAccountTask>; + +#[derive(Deserialize)] +pub struct CreateNewAccountRequest { + coin: String, + #[serde(flatten)] + params: CreateNewAccountParams, +} + +#[derive(Deserialize)] +pub struct CreateNewAccountParams { + #[serde(default = "true_f")] + scan: bool, + gap_limit: Option, +} + +#[derive(Clone, Serialize)] +pub enum CreateAccountInProgressStatus { + Preparing, + RequestingAccountBalance, + Finishing, + /// The following statuses don't require the user to send `UserAction`, + /// but they tell the user that he should confirm/decline the operation on his device. + WaitingForTrezorToConnect, + WaitingForUserToConfirmPubkey, +} + +#[async_trait] +pub trait InitCreateHDAccountRpcOps { + async fn init_create_account_rpc( + &self, + params: CreateNewAccountParams, + xpub_extractor: &XPubExtractor, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Sync; +} + +pub struct InitCreateAccountTask { + ctx: MmArc, + coin: MmCoinEnum, + req: CreateNewAccountRequest, +} + +impl RpcTaskTypes for InitCreateAccountTask { + type Item = HDAccountBalance; + type Error = HDWalletRpcError; + type InProgressStatus = CreateAccountInProgressStatus; + type AwaitingStatus = CreateAccountAwaitingStatus; + type UserAction = CreateAccountUserAction; +} + +#[async_trait] +impl RpcTask for InitCreateAccountTask { + fn initial_status(&self) -> Self::InProgressStatus { CreateAccountInProgressStatus::Preparing } + + async fn run(self, task_handle: &CreateAccountTaskHandle) -> Result> { + async fn create_new_account_helper( + ctx: &MmArc, + coin: Coin, + params: CreateNewAccountParams, + task_handle: &CreateAccountTaskHandle, + ) -> MmResult + where + Coin: InitCreateHDAccountRpcOps + Send + Sync, + { + let hw_statuses = HwConnectStatuses { + on_connect: CreateAccountInProgressStatus::WaitingForTrezorToConnect, + on_connected: CreateAccountInProgressStatus::Preparing, + on_connection_failed: CreateAccountInProgressStatus::Finishing, + on_button_request: CreateAccountInProgressStatus::WaitingForUserToConfirmPubkey, + on_pin_request: CreateAccountAwaitingStatus::WaitForTrezorPin, + on_ready: CreateAccountInProgressStatus::RequestingAccountBalance, + }; + let xpub_extractor = CreateAccountXPubExtractor::new(ctx, task_handle, hw_statuses)?; + coin.init_create_account_rpc(params, &xpub_extractor).await + } + + match self.coin { + MmCoinEnum::UtxoCoin(utxo) => { + create_new_account_helper(&self.ctx, utxo, self.req.params, task_handle).await + }, + MmCoinEnum::QtumCoin(qtum) => { + create_new_account_helper(&self.ctx, qtum, self.req.params, task_handle).await + }, + _ => MmError::err(HDWalletRpcError::CoinIsActivatedNotWithHDWallet), + } + } +} + +pub async fn init_create_new_account( + ctx: MmArc, + req: CreateNewAccountRequest, +) -> MmResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(HDWalletRpcError::Internal)?; + let task = InitCreateAccountTask { ctx, coin, req }; + let task_id = CreateAccountTaskManager::spawn_rpc_task(&coins_ctx.create_account_manager, task)?; + Ok(InitRpcTaskResponse { task_id }) +} + +pub async fn init_create_new_account_status( + ctx: MmArc, + req: RpcTaskStatusRequest, +) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(RpcTaskStatusError::Internal)?; + let mut task_manager = coins_ctx + .create_account_manager + .lock() + .map_to_mm(|e| RpcTaskStatusError::Internal(e.to_string()))?; + task_manager + .task_status(req.task_id, req.forget_if_finished) + .or_mm_err(|| RpcTaskStatusError::NoSuchTask(req.task_id)) +} + +pub async fn init_create_new_account_user_action( + ctx: MmArc, + req: HwRpcTaskUserActionRequest, +) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(RpcTaskUserActionError::Internal)?; + let mut task_manager = coins_ctx + .create_account_manager + .lock() + .map_to_mm(|e| RpcTaskUserActionError::Internal(e.to_string()))?; + task_manager.on_user_action(req.task_id, req.user_action)?; + Ok(SuccessResponse::new()) +} + +pub(crate) mod common_impl { + use super::*; + use crate::coin_balance::HDWalletBalanceOps; + use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; + use crate::MarketCoinOps; + + pub async fn init_create_new_account_rpc<'a, Coin, XPubExtractor>( + coin: &Coin, + params: CreateNewAccountParams, + xpub_extractor: &XPubExtractor, + ) -> MmResult + where + Coin: HDWalletBalanceOps + + CoinWithDerivationMethod::HDWallet> + + Send + + Sync + + MarketCoinOps, + XPubExtractor: HDXPubExtractor + Sync, + { + let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; + + let mut new_account = coin.create_new_account(hd_wallet, xpub_extractor).await?; + let address_scanner = coin.produce_hd_address_scanner().await?; + let account_index = new_account.account_id(); + let account_derivation_path = new_account.account_derivation_path(); + + let addresses = if params.scan { + let gap_limit = params.gap_limit.unwrap_or_else(|| hd_wallet.gap_limit()); + coin.scan_for_new_addresses(hd_wallet, &mut new_account, &address_scanner, gap_limit) + .await? + } else { + Vec::new() + }; + + let total_balance = addresses + .iter() + .fold(CoinBalance::default(), |total_balance, address_balance| { + total_balance + address_balance.balance.clone() + }); + + Ok(HDAccountBalance { + account_index, + derivation_path: RpcDerivationPath(account_derivation_path), + total_balance, + addresses, + }) + } +} diff --git a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs new file mode 100644 index 0000000000..e2400e8b4a --- /dev/null +++ b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs @@ -0,0 +1,153 @@ +use crate::coin_balance::HDAddressBalance; +use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; +use crate::{lp_coinfind_or_err, CoinsContext, MmCoinEnum}; +use async_trait::async_trait; +use crypto::RpcDerivationPath; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc_task::rpc_common::{InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest}; +use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; + +pub type ScanAddressesTaskManager = RpcTaskManager; +pub type ScanAddressesTaskManagerShared = RpcTaskManagerShared; +pub type ScanAddressesTaskHandle = RpcTaskHandle; +pub type ScanAddressesRpcTaskStatus = RpcTaskStatus< + ScanAddressesResponse, + HDAccountBalanceRpcError, + ScanAddressesInProgressStatus, + ScanAddressesAwaitingStatus, +>; + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct ScanAddressesResponse { + pub account_index: u32, + pub derivation_path: RpcDerivationPath, + pub new_addresses: Vec, +} + +#[derive(Deserialize)] +pub struct ScanAddressesRequest { + coin: String, + #[serde(flatten)] + params: ScanAddressesParams, +} + +#[derive(Deserialize)] +pub struct ScanAddressesParams { + pub account_index: u32, + pub gap_limit: Option, +} + +#[derive(Clone, Serialize)] +pub enum ScanAddressesInProgressStatus { + InProgress, +} + +/// We can't use `std::convert::Infallible` as [`RpcTaskTypes::UserAction`] because it doesn't implement `Serialize`. +/// Use `!` when it's stable. +#[derive(Clone, Serialize)] +pub enum ScanAddressesUserAction {} + +/// We can't use `std::convert::Infallible` as [`RpcTaskTypes::AwaitingStatus`] because it doesn't implement `Serialize`. +/// Use `!` when it's stable. +#[derive(Clone, Serialize)] +pub enum ScanAddressesAwaitingStatus {} + +#[async_trait] +pub trait InitScanAddressesRpcOps { + async fn init_scan_for_new_addresses_rpc( + &self, + params: ScanAddressesParams, + ) -> MmResult; +} + +pub struct InitScanAddressesTask { + req: ScanAddressesRequest, + coin: MmCoinEnum, +} + +impl RpcTaskTypes for InitScanAddressesTask { + type Item = ScanAddressesResponse; + type Error = HDAccountBalanceRpcError; + type InProgressStatus = ScanAddressesInProgressStatus; + type AwaitingStatus = ScanAddressesAwaitingStatus; + type UserAction = ScanAddressesUserAction; +} + +#[async_trait] +impl RpcTask for InitScanAddressesTask { + #[inline] + fn initial_status(&self) -> Self::InProgressStatus { ScanAddressesInProgressStatus::InProgress } + + async fn run(self, _task_handle: &ScanAddressesTaskHandle) -> Result> { + match self.coin { + MmCoinEnum::UtxoCoin(utxo) => utxo.init_scan_for_new_addresses_rpc(self.req.params).await, + MmCoinEnum::QtumCoin(qtum) => qtum.init_scan_for_new_addresses_rpc(self.req.params).await, + _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), + } + } +} + +pub async fn init_scan_for_new_addresses( + ctx: MmArc, + req: ScanAddressesRequest, +) -> MmResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(HDAccountBalanceRpcError::Internal)?; + let task = InitScanAddressesTask { req, coin }; + let task_id = ScanAddressesTaskManager::spawn_rpc_task(&coins_ctx.scan_addresses_manager, task)?; + Ok(InitRpcTaskResponse { task_id }) +} + +pub async fn init_scan_for_new_addresses_status( + ctx: MmArc, + req: RpcTaskStatusRequest, +) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(RpcTaskStatusError::Internal)?; + let mut task_manager = coins_ctx + .scan_addresses_manager + .lock() + .map_to_mm(|e| RpcTaskStatusError::Internal(e.to_string()))?; + task_manager + .task_status(req.task_id, req.forget_if_finished) + .or_mm_err(|| RpcTaskStatusError::NoSuchTask(req.task_id)) +} + +pub mod common_impl { + use super::*; + use crate::coin_balance::HDWalletBalanceOps; + use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; + use crate::CoinWithDerivationMethod; + use std::fmt; + use std::ops::DerefMut; + + pub async fn scan_for_new_addresses_rpc( + coin: &Coin, + params: ScanAddressesParams, + ) -> MmResult + where + Coin: CoinWithDerivationMethod::HDWallet> + HDWalletBalanceOps + Sync, + ::Address: fmt::Display, + { + let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; + + let account_id = params.account_index; + let mut hd_account = hd_wallet + .get_account_mut(account_id) + .await + .or_mm_err(|| HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet)?; + let account_derivation_path = hd_account.account_derivation_path(); + let address_scanner = coin.produce_hd_address_scanner().await?; + let gap_limit = params.gap_limit.unwrap_or_else(|| hd_wallet.gap_limit()); + + let new_addresses = coin + .scan_for_new_addresses(hd_wallet, hd_account.deref_mut(), &address_scanner, gap_limit) + .await?; + + Ok(ScanAddressesResponse { + account_index: account_id, + derivation_path: RpcDerivationPath(account_derivation_path), + new_addresses, + }) + } +} diff --git a/mm2src/coins/rpc_command/init_withdraw.rs b/mm2src/coins/rpc_command/init_withdraw.rs new file mode 100644 index 0000000000..dd3d22ca63 --- /dev/null +++ b/mm2src/coins/rpc_command/init_withdraw.rs @@ -0,0 +1,126 @@ +use crate::{lp_coinfind_or_err, CoinsContext, MmCoinEnum, WithdrawError}; +use crate::{TransactionDetails, WithdrawRequest}; +use async_trait::async_trait; +use common::SuccessResponse; +use crypto::hw_rpc_task::{HwRpcTaskAwaitingStatus, HwRpcTaskUserAction, HwRpcTaskUserActionRequest}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc_task::rpc_common::{InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; +use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatusAlias, RpcTaskTypes}; + +pub type WithdrawAwaitingStatus = HwRpcTaskAwaitingStatus; +pub type WithdrawUserAction = HwRpcTaskUserAction; +pub type WithdrawStatusError = RpcTaskStatusError; +pub type WithdrawUserActionError = RpcTaskUserActionError; +pub type InitWithdrawResponse = InitRpcTaskResponse; +pub type WithdrawStatusRequest = RpcTaskStatusRequest; +pub type WithdrawUserActionRequest = HwRpcTaskUserActionRequest; +pub type WithdrawTaskManager = RpcTaskManager; +pub type WithdrawTaskManagerShared = RpcTaskManagerShared; +pub type WithdrawTaskHandle = RpcTaskHandle; +pub type WithdrawRpcStatus = RpcTaskStatusAlias; +pub type WithdrawInitResult = Result>; + +#[async_trait] +pub trait CoinWithdrawInit { + fn init_withdraw( + ctx: MmArc, + req: WithdrawRequest, + rpc_task_handle: &WithdrawTaskHandle, + ) -> WithdrawInitResult; +} + +pub async fn init_withdraw(ctx: MmArc, request: WithdrawRequest) -> WithdrawInitResult { + let coin = lp_coinfind_or_err(&ctx, &request.coin).await?; + let task = WithdrawTask { + ctx: ctx.clone(), + coin, + request, + }; + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(WithdrawError::InternalError)?; + let task_id = WithdrawTaskManager::spawn_rpc_task(&coins_ctx.withdraw_task_manager, task)?; + Ok(InitWithdrawResponse { task_id }) +} + +pub async fn withdraw_status( + ctx: MmArc, + req: WithdrawStatusRequest, +) -> Result> { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(WithdrawStatusError::Internal)?; + let mut task_manager = coins_ctx + .withdraw_task_manager + .lock() + .map_to_mm(|e| WithdrawStatusError::Internal(e.to_string()))?; + task_manager + .task_status(req.task_id, req.forget_if_finished) + .or_mm_err(|| WithdrawStatusError::NoSuchTask(req.task_id)) +} + +#[derive(Clone, Serialize)] +pub enum WithdrawInProgressStatus { + Preparing, + GeneratingTransaction, + SigningTransaction, + Finishing, + /// The following statuses don't require the user to send `UserAction`, + /// but they tell the user that he should confirm/decline the operation on his device. + WaitingForTrezorToConnect, + WaitingForUserToConfirmPubkey, + WaitingForUserToConfirmSigning, +} + +pub async fn withdraw_user_action( + ctx: MmArc, + req: WithdrawUserActionRequest, +) -> Result> { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(WithdrawUserActionError::Internal)?; + let mut task_manager = coins_ctx + .withdraw_task_manager + .lock() + .map_to_mm(|e| WithdrawUserActionError::Internal(e.to_string()))?; + task_manager.on_user_action(req.task_id, req.user_action)?; + Ok(SuccessResponse::new()) +} + +#[async_trait] +pub trait InitWithdrawCoin { + async fn init_withdraw( + &self, + ctx: MmArc, + req: WithdrawRequest, + task_handle: &WithdrawTaskHandle, + ) -> Result>; +} + +pub struct WithdrawTask { + ctx: MmArc, + coin: MmCoinEnum, + request: WithdrawRequest, +} + +impl RpcTaskTypes for WithdrawTask { + type Item = TransactionDetails; + type Error = WithdrawError; + type InProgressStatus = WithdrawInProgressStatus; + type AwaitingStatus = WithdrawAwaitingStatus; + type UserAction = WithdrawUserAction; +} + +#[async_trait] +impl RpcTask for WithdrawTask { + fn initial_status(&self) -> Self::InProgressStatus { WithdrawInProgressStatus::Preparing } + + async fn run(self, task_handle: &WithdrawTaskHandle) -> Result> { + match self.coin { + MmCoinEnum::UtxoCoin(ref standard_utxo) => { + standard_utxo.init_withdraw(self.ctx, self.request, task_handle).await + }, + MmCoinEnum::QtumCoin(ref qtum) => qtum.init_withdraw(self.ctx, self.request, task_handle).await, + #[cfg(not(target_arch = "wasm32"))] + MmCoinEnum::ZCoin(ref z) => z.init_withdraw(self.ctx, self.request, task_handle).await, + _ => MmError::err(WithdrawError::CoinDoesntSupportInitWithdraw { + coin: self.coin.ticker().to_owned(), + }), + } + } +} diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs new file mode 100644 index 0000000000..7a98d3fc2e --- /dev/null +++ b/mm2src/coins/rpc_command/mod.rs @@ -0,0 +1,5 @@ +pub mod account_balance; +pub mod hd_account_balance_rpc_error; +pub mod init_create_account; +pub mod init_scan_for_new_addresses; +pub mod init_withdraw; diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs new file mode 100644 index 0000000000..6bf3c7049c --- /dev/null +++ b/mm2src/coins/solana.rs @@ -0,0 +1,670 @@ +use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum}; +use crate::solana::solana_common::{lamports_to_sol, PrepareTransferData, SufficientBalanceError}; +use crate::solana::spl::SplTokenInfo; +use crate::{BalanceError, BalanceFut, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, + RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, TradePreimageFut, + TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionFut, TransactionType, + UnexpectedDerivationMethod, ValidateAddressResult, ValidatePaymentInput, VerificationResult, + WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; +use async_trait::async_trait; +use base58::ToBase58; +use bincode::{deserialize, serialize}; +use common::{async_blocking, now_ms}; +use derive_more::Display; +use futures::{FutureExt, TryFutureExt}; +use futures01::Future; +use keys::KeyPair; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::{BigDecimal, MmNumber}; +use rpc::v1::types::Bytes as BytesJson; +use serde_json::{self as json, Value as Json}; +use solana_client::rpc_request::TokenAccountsFilter; +use solana_client::{client_error::{ClientError, ClientErrorKind}, + rpc_client::RpcClient}; +use solana_sdk::commitment_config::{CommitmentConfig, CommitmentLevel}; +use solana_sdk::program_error::ProgramError; +use solana_sdk::pubkey::ParsePubkeyError; +use solana_sdk::transaction::Transaction; +use solana_sdk::{pubkey::Pubkey, + signature::{Keypair, Signer}}; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Mutex; +use std::{convert::TryFrom, + fmt::{Debug, Formatter, Result as FmtResult}, + ops::Deref, + sync::Arc}; + +pub mod solana_common; +#[cfg(test)] mod solana_common_tests; +mod solana_decode_tx_helpers; +#[cfg(test)] mod solana_tests; +pub mod spl; +#[cfg(test)] mod spl_tests; + +pub const SOLANA_DEFAULT_DECIMALS: u64 = 9; +pub const LAMPORTS_DUMMY_AMOUNT: u64 = 10; + +#[async_trait] +pub trait SolanaCommonOps { + fn rpc(&self) -> &RpcClient; + + fn is_token(&self) -> bool; + + async fn check_balance_and_prepare_transfer( + &self, + max: bool, + amount: BigDecimal, + fees: u64, + ) -> Result>; +} + +impl From for BalanceError { + fn from(e: ClientError) -> Self { + match e.kind { + ClientErrorKind::Io(e) => BalanceError::Transport(e.to_string()), + ClientErrorKind::Reqwest(e) => BalanceError::Transport(e.to_string()), + ClientErrorKind::RpcError(e) => BalanceError::Transport(format!("{:?}", e)), + ClientErrorKind::SerdeJson(e) => BalanceError::InvalidResponse(e.to_string()), + ClientErrorKind::Custom(e) => BalanceError::Internal(e), + ClientErrorKind::SigningError(_) + | ClientErrorKind::TransactionError(_) + | ClientErrorKind::FaucetError(_) => BalanceError::Internal("not_reacheable".to_string()), + } + } +} + +impl From for BalanceError { + fn from(e: ParsePubkeyError) -> Self { BalanceError::Internal(format!("{:?}", e)) } +} + +impl From for WithdrawError { + fn from(e: ClientError) -> Self { + match e.kind { + ClientErrorKind::Io(e) => WithdrawError::Transport(e.to_string()), + ClientErrorKind::Reqwest(e) => WithdrawError::Transport(e.to_string()), + ClientErrorKind::RpcError(e) => WithdrawError::Transport(format!("{:?}", e)), + ClientErrorKind::SerdeJson(e) => WithdrawError::InternalError(e.to_string()), + ClientErrorKind::Custom(e) => WithdrawError::InternalError(e), + ClientErrorKind::SigningError(_) + | ClientErrorKind::TransactionError(_) + | ClientErrorKind::FaucetError(_) => WithdrawError::InternalError("not_reacheable".to_string()), + } + } +} + +impl From for WithdrawError { + fn from(e: ParsePubkeyError) -> Self { WithdrawError::InvalidAddress(format!("{:?}", e)) } +} + +impl From for WithdrawError { + fn from(e: ProgramError) -> Self { WithdrawError::InternalError(format!("{:?}", e)) } +} + +#[derive(Debug)] +pub enum AccountError { + NotFundedError(String), + ParsePubKeyError(String), + ClientError(ClientErrorKind), +} + +impl From for AccountError { + fn from(e: ClientError) -> Self { AccountError::ClientError(e.kind) } +} + +impl From for AccountError { + fn from(e: ParsePubkeyError) -> Self { AccountError::ParsePubKeyError(format!("{:?}", e)) } +} + +impl From for WithdrawError { + fn from(e: AccountError) -> Self { + match e { + AccountError::NotFundedError(_) => WithdrawError::ZeroBalanceToWithdrawMax, + AccountError::ParsePubKeyError(err) => WithdrawError::InternalError(err), + AccountError::ClientError(e) => WithdrawError::Transport(format!("{:?}", e)), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SolanaActivationParams { + confirmation_commitment: CommitmentLevel, + client_url: String, +} + +#[derive(Debug, Display)] +pub enum SolanaFromLegacyReqErr { + InvalidCommitmentLevel(String), + InvalidClientParsing(json::Error), + ClientNoAvailableNodes(String), +} + +#[derive(Debug, Display)] +pub enum KeyPairCreationError { + #[display(fmt = "Signature error: {}", _0)] + SignatureError(ed25519_dalek::SignatureError), + #[display(fmt = "KeyPairFromSeed error: {}", _0)] + KeyPairFromSeed(String), +} + +impl From for KeyPairCreationError { + fn from(e: ed25519_dalek::SignatureError) -> Self { KeyPairCreationError::SignatureError(e) } +} + +fn generate_keypair_from_slice(priv_key: &[u8]) -> Result> { + let secret_key = ed25519_dalek::SecretKey::from_bytes(priv_key)?; + let public_key = ed25519_dalek::PublicKey::from(&secret_key); + let key_pair = ed25519_dalek::Keypair { + secret: secret_key, + public: public_key, + }; + solana_sdk::signature::keypair_from_seed(key_pair.to_bytes().as_ref()) + .map_to_mm(|e| KeyPairCreationError::KeyPairFromSeed(e.to_string())) +} + +pub async fn solana_coin_from_conf_and_params( + ticker: &str, + conf: &Json, + params: SolanaActivationParams, + priv_key: &[u8], +) -> Result { + let client = RpcClient::new_with_commitment(params.client_url.clone(), CommitmentConfig { + commitment: params.confirmation_commitment, + }); + let decimals = conf["decimals"].as_u64().unwrap_or(SOLANA_DEFAULT_DECIMALS) as u8; + let key_pair = try_s!(generate_keypair_from_slice(priv_key)); + let my_address = key_pair.pubkey().to_string(); + let spl_tokens_infos = Arc::new(Mutex::new(HashMap::new())); + let solana_coin = SolanaCoin(Arc::new(SolanaCoinImpl { + my_address, + key_pair, + ticker: ticker.to_string(), + client, + decimals, + spl_tokens_infos, + })); + Ok(solana_coin) +} + +/// pImpl idiom. +pub struct SolanaCoinImpl { + ticker: String, + key_pair: Keypair, + client: RpcClient, + decimals: u8, + my_address: String, + spl_tokens_infos: Arc>>, +} + +impl Debug for SolanaCoinImpl { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.write_str(&*self.ticker) } +} + +#[derive(Clone, Debug)] +pub struct SolanaCoin(Arc); +impl Deref for SolanaCoin { + type Target = SolanaCoinImpl; + fn deref(&self) -> &SolanaCoinImpl { &*self.0 } +} + +#[async_trait] +impl SolanaCommonOps for SolanaCoin { + fn rpc(&self) -> &RpcClient { &self.client } + + fn is_token(&self) -> bool { false } + + async fn check_balance_and_prepare_transfer( + &self, + max: bool, + amount: BigDecimal, + fees: u64, + ) -> Result> { + solana_common::check_balance_and_prepare_transfer(self, max, amount, fees).await + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct SolanaFeeDetails { + pub amount: BigDecimal, +} + +async fn withdraw_base_coin_impl(coin: SolanaCoin, req: WithdrawRequest) -> WithdrawResult { + let (hash, fees) = coin.estimate_withdraw_fees().await?; + let res = coin + .check_balance_and_prepare_transfer(req.max, req.amount.clone(), fees) + .await?; + let to = solana_sdk::pubkey::Pubkey::try_from(&*req.to)?; + let tx = solana_sdk::system_transaction::transfer(&coin.key_pair, &to, res.lamports_to_send, hash); + let serialized_tx = serialize(&tx).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + let total_amount = lamports_to_sol(res.lamports_to_send); + let received_by_me = if req.to == coin.my_address { + total_amount.clone() + } else { + 0.into() + }; + let spent_by_me = &total_amount + &res.sol_required; + Ok(TransactionDetails { + tx_hex: serialized_tx.into(), + tx_hash: tx.signatures[0].to_string(), + from: vec![coin.my_address.clone()], + to: vec![req.to], + total_amount: spent_by_me.clone(), + my_balance_change: &received_by_me - &spent_by_me, + spent_by_me, + received_by_me, + block_height: 0, + timestamp: now_ms() / 1000, + fee_details: Some( + SolanaFeeDetails { + amount: res.sol_required, + } + .into(), + ), + coin: coin.ticker.clone(), + internal_id: vec![].into(), + kmd_rewards: None, + transaction_type: TransactionType::StandardTransfer, + }) +} + +async fn withdraw_impl(coin: SolanaCoin, req: WithdrawRequest) -> WithdrawResult { + let validate_address_result = coin.validate_address(&*req.to); + if !validate_address_result.is_valid { + return MmError::err(WithdrawError::InvalidAddress( + validate_address_result.reason.unwrap_or_else(|| "Unknown".to_string()), + )); + } + withdraw_base_coin_impl(coin, req).await +} + +impl SolanaCoin { + pub async fn estimate_withdraw_fees(&self) -> Result<(solana_sdk::hash::Hash, u64), MmError> { + let hash = async_blocking({ + let coin = self.clone(); + move || coin.rpc().get_latest_blockhash() + }) + .await?; + let to = self.key_pair.pubkey(); + + let tx = solana_sdk::system_transaction::transfer(&self.key_pair, &to, LAMPORTS_DUMMY_AMOUNT, hash); + let fees = async_blocking({ + let coin = self.clone(); + move || coin.rpc().get_fee_for_message(tx.message()) + }) + .await?; + Ok((hash, fees)) + } + + pub async fn my_balance_spl(&self, infos: &SplTokenInfo) -> Result> { + let token_accounts = async_blocking({ + let coin = self.clone(); + let infos = infos.clone(); + move || { + coin.rpc().get_token_accounts_by_owner( + &coin.key_pair.pubkey(), + TokenAccountsFilter::Mint(infos.token_contract_address), + ) + } + }) + .await?; + if token_accounts.is_empty() { + return Ok(CoinBalance { + spendable: Default::default(), + unspendable: Default::default(), + }); + } + let actual_token_pubkey = + Pubkey::from_str(&*token_accounts[0].pubkey).map_err(|e| BalanceError::Internal(format!("{:?}", e)))?; + let amount = async_blocking({ + let coin = self.clone(); + move || coin.rpc().get_token_account_balance(&actual_token_pubkey) + }) + .await?; + let balance = + BigDecimal::from_str(&*amount.ui_amount_string).map_to_mm(|e| BalanceError::Internal(e.to_string()))?; + Ok(CoinBalance { + spendable: balance, + unspendable: Default::default(), + }) + } + + fn my_balance_impl(&self) -> BalanceFut { + let coin = self.clone(); + let fut = async_blocking(move || { + // this is blocking IO + let res = coin.rpc().get_balance(&coin.key_pair.pubkey())?; + Ok(lamports_to_sol(res)) + }); + Box::new(fut.boxed().compat()) + } + + pub fn add_spl_token_info(&self, ticker: String, info: SplTokenInfo) { + self.spl_tokens_infos.lock().unwrap().insert(ticker, info); + } + + pub fn get_spl_tokens_infos(&self) -> HashMap { + let guard = self.spl_tokens_infos.lock().unwrap(); + (*guard).clone() + } +} + +impl MarketCoinOps for SolanaCoin { + fn ticker(&self) -> &str { &self.ticker } + + fn my_address(&self) -> Result { Ok(self.my_address.clone()) } + + fn get_public_key(&self) -> Result> { unimplemented!() } + + fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } + + fn sign_message(&self, message: &str) -> SignatureResult { solana_common::sign_message(self, message) } + + fn verify_message(&self, signature: &str, message: &str, pubkey_bs58: &str) -> VerificationResult { + solana_common::verify_message(self, signature, message, pubkey_bs58) + } + + fn my_balance(&self) -> BalanceFut { + let decimals = self.decimals as u64; + let fut = self.my_balance_impl().and_then(move |result| { + Ok(CoinBalance { + spendable: result.with_prec(decimals), + unspendable: 0.into(), + }) + }); + Box::new(fut) + } + + fn base_coin_balance(&self) -> BalanceFut { + let decimals = self.decimals as u64; + let fut = self + .my_balance_impl() + .and_then(move |result| Ok(result.with_prec(decimals))); + Box::new(fut) + } + + fn platform_ticker(&self) -> &str { self.ticker() } + + fn send_raw_tx(&self, tx: &str) -> Box + Send> { + let coin = self.clone(); + let tx = tx.to_owned(); + let fut = async_blocking(move || { + let bytes = hex::decode(tx).map_to_mm(|e| e).map_err(|e| format!("{:?}", e))?; + let tx: Transaction = deserialize(bytes.as_slice()) + .map_to_mm(|e| e) + .map_err(|e| format!("{:?}", e))?; + // this is blocking IO + let signature = coin.rpc().send_transaction(&tx).map_err(|e| format!("{:?}", e))?; + Ok(signature.to_string()) + }); + Box::new(fut.boxed().compat()) + } + + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + let coin = self.clone(); + let tx = tx.to_owned(); + let fut = async_blocking(move || { + let tx = try_s!(deserialize(tx.as_slice())); + // this is blocking IO + let signature = coin.rpc().send_transaction(&tx).map_err(|e| format!("{:?}", e))?; + Ok(signature.to_string()) + }); + Box::new(fut.boxed().compat()) + } + + fn wait_for_confirmations( + &self, + _tx: &[u8], + _confirmations: u64, + _requires_nota: bool, + _wait_until: u64, + _check_every: u64, + ) -> Box + Send> { + unimplemented!() + } + + fn wait_for_tx_spend( + &self, + _transaction: &[u8], + _wait_until: u64, + _from_block: u64, + _swap_contract_address: &Option, + ) -> TransactionFut { + unimplemented!() + } + + fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result { unimplemented!() } + + fn current_block(&self) -> Box + Send> { + let coin = self.clone(); + let fut = async_blocking(move || coin.rpc().get_block_height().map_err(|e| format!("{:?}", e))); + Box::new(fut.boxed().compat()) + } + + fn display_priv_key(&self) -> Result { Ok(self.key_pair.secret().to_bytes()[..].to_base58()) } + + fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } + + fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } +} + +#[allow(clippy::forget_ref, clippy::forget_copy, clippy::cast_ref_to_mut)] +#[async_trait] +impl SwapOps for SolanaCoin { + fn send_taker_fee(&self, _fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { unimplemented!() } + + fn send_maker_payment( + &self, + time_lock: u32, + taker_pub: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_taker_payment( + &self, + time_lock: u32, + taker_pub: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_maker_spends_taker_payment( + &self, + taker_payment_tx: &[u8], + time_lock: u32, + taker_pub: &[u8], + secret: &[u8], + swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_taker_spends_maker_payment( + &self, + maker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret: &[u8], + swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_taker_refunds_payment( + &self, + taker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_maker_refunds_payment( + &self, + maker_payment_tx: &[u8], + time_lock: u32, + taker_pub: &[u8], + secret_hash: &[u8], + swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn validate_fee( + &self, + _fee_tx: &TransactionEnum, + _expected_sender: &[u8], + _fee_addr: &[u8], + _amount: &BigDecimal, + _min_block_number: u64, + _uuid: &[u8], + ) -> Box + Send> { + unimplemented!() + } + + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + unimplemented!() + } + + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + unimplemented!() + } + + fn check_if_my_payment_sent( + &self, + time_lock: u32, + my_pub: &[u8], + other_pub: &[u8], + search_from_block: u64, + swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> Box, Error = String> + Send> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_my( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_other( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + + fn negotiate_swap_contract_addr( + &self, + _other_side_address: Option<&[u8]>, + ) -> Result, MmError> { + unimplemented!() + } + + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { todo!() } +} + +#[allow(clippy::forget_ref, clippy::forget_copy, clippy::cast_ref_to_mut)] +#[async_trait] +impl MmCoin for SolanaCoin { + fn is_asset_chain(&self) -> bool { false } + + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { + Box::new(Box::pin(withdraw_impl(self.clone(), req)).compat()) + } + + fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } + + fn decimals(&self) -> u8 { self.decimals } + + fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { unimplemented!() } + + fn validate_address(&self, address: &str) -> ValidateAddressResult { + if address.len() != 44 { + return ValidateAddressResult { + is_valid: false, + reason: Some("Invalid address length".to_string()), + }; + } + let result = Pubkey::try_from(address); + match result { + Ok(pubkey) => { + if pubkey.is_on_curve() { + ValidateAddressResult { + is_valid: true, + reason: None, + } + } else { + ValidateAddressResult { + is_valid: false, + reason: Some("not_on_curve".to_string()), + } + } + }, + Err(err) => ValidateAddressResult { + is_valid: false, + reason: Some(format!("{:?}", err)), + }, + } + } + + fn process_history_loop(&self, _ctx: MmArc) -> Box + Send> { unimplemented!() } + + fn history_sync_status(&self) -> HistorySyncState { unimplemented!() } + + /// Get fee to be paid per 1 swap transaction + fn get_trade_fee(&self) -> Box + Send> { unimplemented!() } + + async fn get_sender_trade_fee( + &self, + value: TradePreimageValue, + stage: FeeApproxStage, + ) -> TradePreimageResult { + unimplemented!() + } + + fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + + async fn get_fee_to_send_taker_fee( + &self, + dex_fee_amount: BigDecimal, + stage: FeeApproxStage, + ) -> TradePreimageResult { + unimplemented!() + } + + fn required_confirmations(&self) -> u64 { 1 } + + fn requires_notarization(&self) -> bool { false } + + fn set_required_confirmations(&self, _confirmations: u64) { unimplemented!() } + + fn set_requires_notarization(&self, _requires_nota: bool) { unimplemented!() } + + fn swap_contract_address(&self) -> Option { unimplemented!() } + + fn mature_confirmations(&self) -> Option { None } + + fn coin_protocol_info(&self) -> Vec { Vec::new() } + + fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } +} diff --git a/mm2src/coins/solana/solana_common.rs b/mm2src/coins/solana/solana_common.rs new file mode 100644 index 0000000000..3f74c1caff --- /dev/null +++ b/mm2src/coins/solana/solana_common.rs @@ -0,0 +1,181 @@ +use crate::solana::SolanaCommonOps; +use crate::{BalanceError, MarketCoinOps, NumConversError, SignatureError, SignatureResult, SolanaCoin, + UnexpectedDerivationMethod, VerificationError, VerificationResult, WithdrawError}; +use base58::FromBase58; +use derive_more::Display; +use futures::compat::Future01CompatExt; +use mm2_err_handle::prelude::*; +use mm2_number::bigdecimal::{BigDecimal, ToPrimitive}; +use solana_sdk::native_token::LAMPORTS_PER_SOL; +use solana_sdk::signature::{Signature, Signer}; +use std::str::FromStr; + +#[derive(Debug, Display)] +pub enum SufficientBalanceError { + #[display( + fmt = "Not enough {} to withdraw: available {}, required at least {}", + coin, + available, + required + )] + NotSufficientBalance { + coin: String, + available: BigDecimal, + required: BigDecimal, + }, + #[display(fmt = "The amount {} is too small, required at least {}", amount, threshold)] + AmountTooLow { amount: BigDecimal, threshold: BigDecimal }, + #[display(fmt = "{}", _0)] + UnexpectedDerivationMethod(UnexpectedDerivationMethod), + #[display(fmt = "Wallet storage error: {}", _0)] + WalletStorageError(String), + #[display(fmt = "Invalid response: {}", _0)] + InvalidResponse(String), + #[display(fmt = "Transport: {}", _0)] + Transport(String), + #[display(fmt = "Internal: {}", _0)] + Internal(String), +} + +impl From for SufficientBalanceError { + fn from(e: NumConversError) -> Self { SufficientBalanceError::Internal(e.to_string()) } +} + +impl From for SufficientBalanceError { + fn from(e: BalanceError) -> Self { + match e { + BalanceError::Transport(e) => SufficientBalanceError::Transport(e), + BalanceError::InvalidResponse(e) => SufficientBalanceError::InvalidResponse(e), + BalanceError::UnexpectedDerivationMethod(e) => SufficientBalanceError::UnexpectedDerivationMethod(e), + BalanceError::Internal(e) => SufficientBalanceError::Internal(e), + BalanceError::WalletStorageError(e) => SufficientBalanceError::WalletStorageError(e), + } + } +} +impl From for WithdrawError { + fn from(e: SufficientBalanceError) -> Self { + match e { + SufficientBalanceError::NotSufficientBalance { + coin, + available, + required, + } => WithdrawError::NotSufficientBalance { + coin, + available, + required, + }, + SufficientBalanceError::UnexpectedDerivationMethod(e) => WithdrawError::from(e), + SufficientBalanceError::InvalidResponse(e) | SufficientBalanceError::Transport(e) => { + WithdrawError::Transport(e) + }, + SufficientBalanceError::Internal(e) | SufficientBalanceError::WalletStorageError(e) => { + WithdrawError::InternalError(e) + }, + SufficientBalanceError::AmountTooLow { amount, threshold } => { + WithdrawError::AmountTooLow { amount, threshold } + }, + } + } +} + +pub struct PrepareTransferData { + pub to_send: BigDecimal, + pub my_balance: BigDecimal, + pub sol_required: BigDecimal, + pub lamports_to_send: u64, +} + +pub fn lamports_to_sol(lamports: u64) -> BigDecimal { BigDecimal::from(lamports) / BigDecimal::from(LAMPORTS_PER_SOL) } + +pub fn sol_to_lamports(sol: &BigDecimal) -> Result> { + let maybe_lamports = (sol * BigDecimal::from(LAMPORTS_PER_SOL)).to_u64(); + match maybe_lamports { + None => MmError::err(NumConversError("Error when converting sol to lamports".to_string())), + Some(lamports) => Ok(lamports), + } +} + +pub fn ui_amount_to_amount(ui_amount: BigDecimal, decimals: u8) -> Result> { + let maybe_amount = (ui_amount * BigDecimal::from(10_u64.pow(decimals as u32))).to_u64(); + match maybe_amount { + None => MmError::err(NumConversError("Error when converting ui amount to amount".to_string())), + Some(amount) => Ok(amount), + } +} + +pub fn amount_to_ui_amount(amount: u64, decimals: u8) -> BigDecimal { + BigDecimal::from(amount) / BigDecimal::from(10_u64.pow(decimals as u32)) +} + +pub fn sign_message(coin: &SolanaCoin, message: &str) -> SignatureResult { + let signature = coin + .key_pair + .try_sign_message(message.as_bytes()) + .map_err(|e| SignatureError::InternalError(e.to_string()))?; + Ok(signature.to_string()) +} + +pub fn verify_message( + coin: &SolanaCoin, + signature: &str, + message: &str, + pubkey_bs58: &str, +) -> VerificationResult { + let pubkey = pubkey_bs58.from_base58()?; + let signature = + Signature::from_str(signature).map_err(|e| VerificationError::SignatureDecodingError(e.to_string()))?; + let is_valid = signature.verify(&pubkey, message.as_bytes()); + Ok(is_valid) +} + +pub async fn check_balance_and_prepare_transfer( + coin: &T, + max: bool, + amount: BigDecimal, + fees: u64, +) -> Result> +where + T: SolanaCommonOps + MarketCoinOps, +{ + let base_balance = coin.base_coin_balance().compat().await?; + let sol_required = lamports_to_sol(fees); + if base_balance < sol_required { + return MmError::err(SufficientBalanceError::NotSufficientBalance { + coin: coin.platform_ticker().to_string(), + available: base_balance.clone(), + required: sol_required.clone(), + }); + } + + let my_balance = coin.my_balance().compat().await?.spendable; + let to_send = if max { my_balance.clone() } else { amount.clone() }; + let to_check = if max || coin.is_token() { + to_send.clone() + } else { + &to_send + &sol_required + }; + if to_check > my_balance { + return MmError::err(SufficientBalanceError::NotSufficientBalance { + coin: coin.ticker().to_string(), + available: my_balance, + required: to_check, + }); + } + + let lamports_to_send = if !coin.is_token() { + if max { + sol_to_lamports(&my_balance)? - sol_to_lamports(&sol_required)? + } else { + sol_to_lamports(&amount)? + } + } else { + 0_u64 + }; + + Ok(PrepareTransferData { + to_send, + my_balance, + sol_required, + lamports_to_send, + }) +} diff --git a/mm2src/coins/solana/solana_common_tests.rs b/mm2src/coins/solana/solana_common_tests.rs new file mode 100644 index 0000000000..9f5eebddf7 --- /dev/null +++ b/mm2src/coins/solana/solana_common_tests.rs @@ -0,0 +1,98 @@ +use super::*; +use crate::solana::spl::{SplToken, SplTokenConf}; +use bip39::Language; +use crypto::privkey::key_pair_from_seed; +use ed25519_dalek_bip32::{DerivationPath, ExtendedSecretKey}; +use mm2_core::mm_ctx::MmCtxBuilder; +use solana_client::rpc_client::RpcClient; +use solana_sdk::commitment_config::{CommitmentConfig, CommitmentLevel}; +use std::str::FromStr; + +pub enum SolanaNet { + //Mainnet, + Testnet, + Devnet, +} + +pub fn solana_net_to_url(net_type: SolanaNet) -> String { + match net_type { + //SolanaNet::Mainnet => "https://api.mainnet-beta.solana.com".to_string(), + SolanaNet::Testnet => "https://api.testnet.solana.com/".to_string(), + SolanaNet::Devnet => "https://api.devnet.solana.com".to_string(), + } +} + +pub fn generate_key_pair_from_seed(seed: String) -> Keypair { + let derivation_path = DerivationPath::from_str("m/44'/501'/0'").unwrap(); + let mnemonic = bip39::Mnemonic::from_phrase(seed.as_str(), Language::English).unwrap(); + let seed = bip39::Seed::new(&mnemonic, ""); + let seed_bytes: &[u8] = seed.as_bytes(); + + let ext = ExtendedSecretKey::from_seed(seed_bytes) + .unwrap() + .derive(&derivation_path) + .unwrap(); + let ref priv_key = ext.secret_key; + let pub_key = ext.public_key(); + let pair = ed25519_dalek::Keypair { + secret: ext.secret_key, + public: pub_key, + }; + + solana_sdk::signature::keypair_from_seed(pair.to_bytes().as_ref()).unwrap() +} + +pub fn generate_key_pair_from_iguana_seed(seed: String) -> Keypair { + let key_pair = key_pair_from_seed(seed.as_str()).unwrap(); + let secret_key = ed25519_dalek::SecretKey::from_bytes(key_pair.private().secret.as_slice()).unwrap(); + let public_key = ed25519_dalek::PublicKey::from(&secret_key); + let other_key_pair = ed25519_dalek::Keypair { + secret: secret_key, + public: public_key, + }; + solana_sdk::signature::keypair_from_seed(other_key_pair.to_bytes().as_ref()).unwrap() +} + +pub fn spl_coin_for_test( + solana_coin: SolanaCoin, + ticker: String, + decimals: u8, + token_contract_address: Pubkey, +) -> SplToken { + let spl_coin = SplToken { + conf: Arc::new(SplTokenConf { + decimals, + ticker, + token_contract_address, + }), + platform_coin: solana_coin, + }; + spl_coin +} + +pub fn solana_coin_for_test(seed: String, net_type: SolanaNet) -> (MmArc, SolanaCoin) { + let url = solana_net_to_url(net_type); + let client = RpcClient::new_with_commitment(url, CommitmentConfig { + commitment: CommitmentLevel::Finalized, + }); + let conf = json!({ + "coins":[ + {"coin":"SOL","name":"solana","protocol":{"type":"SOL"},"rpcport":80,"mm2":1} + ] + }); + let ctx = MmCtxBuilder::new().with_conf(conf.clone()).into_mm_arc(); + let (ticker, decimals) = ("SOL".to_string(), 8); + let key_pair = generate_key_pair_from_iguana_seed(seed); + let my_address = key_pair.pubkey().to_string(); + let spl_tokens_infos = Arc::new(Mutex::new(HashMap::new())); + + let solana_coin = SolanaCoin(Arc::new(SolanaCoinImpl { + decimals, + my_address, + key_pair, + ticker, + client, + spl_tokens_infos, + })); + (ctx, solana_coin) +} diff --git a/mm2src/coins/solana/solana_decode_tx_helpers.rs b/mm2src/coins/solana/solana_decode_tx_helpers.rs new file mode 100644 index 0000000000..1f089de2df --- /dev/null +++ b/mm2src/coins/solana/solana_decode_tx_helpers.rs @@ -0,0 +1,222 @@ +extern crate serde_derive; + +use crate::{NumConversResult, SolanaCoin, SolanaFeeDetails, TransactionDetails, TransactionType}; +use mm2_number::BigDecimal; +use solana_sdk::native_token::lamports_to_sol; +use std::convert::TryFrom; + +#[derive(Debug, Serialize, Deserialize)] +pub struct SolanaConfirmedTransaction { + slot: u64, + transaction: Transaction, + meta: Meta, + #[serde(rename = "blockTime")] + block_time: u64, +} + +#[allow(dead_code)] +impl SolanaConfirmedTransaction { + pub fn extract_account_index(&self, address: String) -> usize { + // find the equivalent of index_of(needle) in rust, and return result later + let mut idx = 0_usize; + for account in self.transaction.message.account_keys.iter() { + if account.pubkey == address { + return idx; + } + idx += 1; + } + idx + } + + pub fn extract_solana_transactions(&self, solana_coin: &SolanaCoin) -> NumConversResult> { + let mut transactions = Vec::new(); + let account_idx = self.extract_account_index(solana_coin.my_address.clone()); + for instruction in self.transaction.message.instructions.iter() { + if instruction.is_solana_transfer() { + let lamports = instruction.parsed.info.lamports.unwrap_or_default(); + let amount = BigDecimal::try_from(lamports_to_sol(lamports))?; + let is_self_transfer = instruction.parsed.info.source == instruction.parsed.info.destination; + let am_i_sender = instruction.parsed.info.source == solana_coin.my_address; + let spent_by_me = if am_i_sender && !is_self_transfer { + amount.clone() + } else { + 0.into() + }; + let received_by_me = if is_self_transfer { amount.clone() } else { 0.into() }; + let my_balance_change = if am_i_sender { + BigDecimal::try_from(lamports_to_sol( + self.meta.pre_balances[account_idx] - self.meta.post_balances[account_idx], + ))? + } else { + BigDecimal::try_from(lamports_to_sol( + self.meta.post_balances[account_idx] - self.meta.pre_balances[account_idx], + ))? + }; + let fee = BigDecimal::try_from(lamports_to_sol(self.meta.fee))?; + let tx = TransactionDetails { + tx_hex: Default::default(), + tx_hash: self.transaction.signatures[0].to_string(), + from: vec![instruction.parsed.info.source.clone()], + to: vec![instruction.parsed.info.destination.clone()], + total_amount: amount, + spent_by_me, + received_by_me, + my_balance_change, + block_height: self.slot, + timestamp: self.block_time, + fee_details: Some(SolanaFeeDetails { amount: fee }.into()), + coin: solana_coin.ticker.clone(), + internal_id: Default::default(), + kmd_rewards: None, + transaction_type: TransactionType::StandardTransfer, + }; + transactions.push(tx); + } + } + Ok(transactions) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Meta { + err: Option, + status: Status, + fee: u64, + #[serde(rename = "preBalances")] + pre_balances: Vec, + #[serde(rename = "postBalances")] + post_balances: Vec, + #[serde(rename = "innerInstructions")] + inner_instructions: Vec>, + #[serde(rename = "logMessages")] + log_messages: Vec, + #[serde(rename = "preTokenBalances")] + pre_token_balances: Vec, + #[serde(rename = "postTokenBalances")] + post_token_balances: Vec, + rewards: Vec>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenBalance { + #[serde(rename = "accountIndex")] + account_index: u64, + mint: String, + #[serde(rename = "uiTokenAmount")] + ui_token_amount: TokenAmount, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenAmount { + #[serde(rename = "uiAmount")] + ui_amount: f64, + decimals: u64, + amount: String, + #[serde(rename = "uiAmountString")] + ui_amount_string: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Status { + #[serde(rename = "Ok")] + ok: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Transaction { + signatures: Vec, + message: Message, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Message { + #[serde(rename = "accountKeys")] + account_keys: Vec, + #[serde(rename = "recentBlockhash")] + recent_blockhash: String, + instructions: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AccountKey { + pubkey: String, + writable: bool, + signer: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Instruction { + program: Program, + #[serde(rename = "programId")] + program_id: String, + parsed: Parsed, +} + +#[allow(dead_code)] +impl Instruction { + pub fn is_solana_transfer(&self) -> bool { + let is_system = match self.program { + Program::SplToken => return false, + Program::System => true, + }; + let is_transfer = match self.parsed.parsed_type { + Type::Transfer => true, + Type::TransferChecked => true, + Type::Unknown => false, + }; + is_system && is_transfer && self.parsed.info.lamports.is_some() + } + + // Will be used later + pub fn is_spl_transfer(&self) -> bool { + let is_spl_token = match self.program { + Program::SplToken => true, + Program::System => return false, + }; + let is_transfer = match self.parsed.parsed_type { + Type::Transfer => true, + Type::TransferChecked => true, + Type::Unknown => false, + }; + is_spl_token && is_transfer + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Parsed { + info: Info, + #[serde(rename = "type")] + parsed_type: Type, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Info { + destination: String, + lamports: Option, + source: String, + mint: Option, + #[serde(rename = "multisigAuthority")] + multisig_authority: Option, + signers: Option>, + #[serde(rename = "tokenAmount")] + token_amount: Option, + authority: Option, + amount: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Type { + #[serde(rename = "transfer")] + Transfer, + #[serde(rename = "transferChecked")] + TransferChecked, + Unknown, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Program { + #[serde(rename = "spl-token")] + SplToken, + #[serde(rename = "system")] + System, +} diff --git a/mm2src/coins/solana/solana_tests.rs b/mm2src/coins/solana/solana_tests.rs new file mode 100644 index 0000000000..300d9014cf --- /dev/null +++ b/mm2src/coins/solana/solana_tests.rs @@ -0,0 +1,322 @@ +use super::*; +use crate::solana::solana_common_tests::{generate_key_pair_from_iguana_seed, generate_key_pair_from_seed, + solana_coin_for_test, SolanaNet}; +use crate::solana::solana_decode_tx_helpers::SolanaConfirmedTransaction; +use crate::MarketCoinOps; +use base58::ToBase58; +use common::{block_on, Future01CompatExt}; +use solana_client::rpc_request::TokenAccountsFilter; +use solana_sdk::signature::{Signature, Signer}; +use solana_transaction_status::UiTransactionEncoding; +use std::ops::Neg; +use std::str::FromStr; + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn solana_keypair_from_secp() { + let solana_key_pair = generate_key_pair_from_iguana_seed("federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string()); + assert_eq!( + "FJktmyjV9aBHEShT4hfnLpr9ELywdwVtEL1w1rSWgbVf", + solana_key_pair.pubkey().to_string() + ); + + let other_solana_keypair = generate_key_pair_from_iguana_seed("bob passphrase".to_string()); + assert_eq!( + "B7KMMHyc3eYguUMneXRznY1NWh91HoVA2muVJetstYKE", + other_solana_keypair.pubkey().to_string() + ); +} + +// Research tests +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn solana_prerequisites() { + // same test as trustwallet + { + let fin = generate_key_pair_from_seed( + "shoot island position soft burden budget tooth cruel issue economy destroy above".to_string(), + ); + let public_address = fin.pubkey().to_string(); + let priv_key = &fin.secret().to_bytes()[..].to_base58(); + assert_eq!(public_address.len(), 44); + assert_eq!(public_address, "2bUBiBNZyD29gP1oV6de7nxowMLoDBtopMMTGgMvjG5m"); + assert_eq!(priv_key, "F6czu7fdefbsCDH52JesQrBSJS5Sz25AkPLWFf8zUWhm"); + let client = solana_client::rpc_client::RpcClient::new("https://api.testnet.solana.com/".to_string()); + let balance = client.get_balance(&fin.pubkey()).expect("Expect to retrieve balance"); + assert_eq!(balance, 0); + } + + { + let key_pair = generate_key_pair_from_iguana_seed("passphrase not really secure".to_string()); + let public_address = key_pair.pubkey().to_string(); + assert_eq!(public_address.len(), 44); + assert_eq!(public_address, "2jTgfhf98GosnKSCXjL5YSiEa3MLrmR42yy9kZZq1i2c"); + let client = solana_client::rpc_client::RpcClient::new("https://api.testnet.solana.com/".to_string()); + let balance = client + .get_balance(&key_pair.pubkey()) + .expect("Expect to retrieve balance"); + assert_eq!(lamports_to_sol(balance), BigDecimal::from(0)); + assert_eq!(balance, 0); + + // This will fetch all the balance from all tokens + let token_accounts = client + .get_token_accounts_by_owner(&key_pair.pubkey(), TokenAccountsFilter::ProgramId(spl_token::id())) + .expect(""); + println!("{:?}", token_accounts); + let actual_token_pubkey = solana_sdk::pubkey::Pubkey::from_str(token_accounts[0].pubkey.as_str()).unwrap(); + let amount = client.get_token_account_balance(&actual_token_pubkey).unwrap(); + assert_ne!(amount.ui_amount_string.as_str(), "0"); + } +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn solana_coin_creation() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + assert_eq!( + sol_coin.my_address().unwrap(), + "FJktmyjV9aBHEShT4hfnLpr9ELywdwVtEL1w1rSWgbVf" + ); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn solana_my_balance() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + let res = block_on(sol_coin.my_balance().compat()).unwrap(); + assert_ne!(res.spendable, BigDecimal::from(0)); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn solana_block_height() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + let res = block_on(sol_coin.current_block().compat()).unwrap(); + println!("block is : {}", res); + assert!(res > 0); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn solana_validate_address() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + + // invalid len + let res = sol_coin.validate_address("invalidaddressobviously"); + assert_eq!(res.is_valid, false); + + let res = sol_coin.validate_address("GMtMFbuVgjDnzsBd3LLBfM4X8RyYcDGCM92tPq2PG6B2"); + assert_eq!(res.is_valid, true); + + // Typo + let res = sol_coin.validate_address("Fr8fraJXAe1cFU81mF7NhHTrUzXjZAJkQE1gUQ11riH"); + assert_eq!(res.is_valid, false); + + // invalid len + let res = sol_coin.validate_address("r8fraJXAe1cFU81mF7NhHTrUzXjZAJkQE1gUQ11riHn"); + assert_eq!(res.is_valid, false); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_sign_message() { + let passphrase = "spice describe gravity federal blast come thank unfair canal monkey style afraid".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + let signature = sol_coin.sign_message("test").unwrap(); + assert_eq!( + signature, + "4dzKwEteN8nch76zPMEjPX19RsaQwGTxsbtfg2bwGTkGenLfrdm31zvn9GH5rvaJBwivp6ESXx1KYR672ngs3UfF" + ); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_verify_message() { + let passphrase = "spice describe gravity federal blast come thank unfair canal monkey style afraid".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + let is_valid = sol_coin + .verify_message( + "4dzKwEteN8nch76zPMEjPX19RsaQwGTxsbtfg2bwGTkGenLfrdm31zvn9GH5rvaJBwivp6ESXx1KYR672ngs3UfF", + "test", + "8UF6jSVE1jW8mSiGqt8Hft1rLwPjdKLaTfhkNozFwoAG", + ) + .unwrap(); + assert!(is_valid); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn solana_transaction_simulations() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Devnet); + let request_amount = BigDecimal::try_from(0.0001).unwrap(); + let valid_tx_details = block_on( + sol_coin + .withdraw(WithdrawRequest { + coin: "SOL".to_string(), + from: None, + to: sol_coin.my_address.clone(), + amount: request_amount.clone(), + max: false, + fee: None, + }) + .compat(), + ) + .unwrap(); + let (_, fees) = block_on(sol_coin.estimate_withdraw_fees()).unwrap(); + let sol_required = lamports_to_sol(fees); + let expected_spent_by_me = &request_amount + &sol_required; + assert_eq!(valid_tx_details.spent_by_me, expected_spent_by_me); + assert_eq!(valid_tx_details.received_by_me, request_amount); + assert_eq!(valid_tx_details.total_amount, expected_spent_by_me); + assert_eq!(valid_tx_details.my_balance_change, sol_required.neg()); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn solana_transaction_zero_balance() { + let passphrase = "fake passphrase".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Devnet); + let invalid_tx_details = block_on( + sol_coin + .withdraw(WithdrawRequest { + coin: "SOL".to_string(), + from: None, + to: sol_coin.my_address.clone(), + amount: BigDecimal::from_str("0.000001").unwrap(), + max: false, + fee: None, + }) + .compat(), + ); + let error = invalid_tx_details.unwrap_err(); + let (_, fees) = block_on(sol_coin.estimate_withdraw_fees()).unwrap(); + let sol_required = lamports_to_sol(fees); + match error.into_inner() { + WithdrawError::NotSufficientBalance { required, .. } => { + assert_eq!(required, sol_required); + }, + e @ _ => panic!("Unexpected err {:?}", e), + }; +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn solana_transaction_simulations_not_enough_for_fees() { + let passphrase = "non existent passphrase".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Devnet); + let invalid_tx_details = block_on( + sol_coin + .withdraw(WithdrawRequest { + coin: "SOL".to_string(), + from: None, + to: sol_coin.my_address.clone(), + amount: BigDecimal::from(1), + max: false, + fee: None, + }) + .compat(), + ); + let error = invalid_tx_details.unwrap_err(); + let (_, fees) = block_on(sol_coin.estimate_withdraw_fees()).unwrap(); + let sol_required = lamports_to_sol(fees); + match error.into_inner() { + WithdrawError::NotSufficientBalance { + coin: _, + available, + required, + } => { + assert_eq!(available, 0.into()); + assert_eq!(required, sol_required); + }, + e @ _ => panic!("Unexpected err {:?}", e), + }; +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn solana_transaction_simulations_max() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Devnet); + let valid_tx_details = block_on( + sol_coin + .withdraw(WithdrawRequest { + coin: "SOL".to_string(), + from: None, + to: sol_coin.my_address.clone(), + amount: BigDecimal::from(0), + max: true, + fee: None, + }) + .compat(), + ) + .unwrap(); + let balance = block_on(sol_coin.my_balance().compat()).unwrap().spendable; + let (_, fees) = block_on(sol_coin.estimate_withdraw_fees()).unwrap(); + let sol_required = lamports_to_sol(fees); + assert_eq!(valid_tx_details.my_balance_change, sol_required.clone().neg()); + assert_eq!(valid_tx_details.total_amount, balance); + assert_eq!(valid_tx_details.spent_by_me, balance); + assert_eq!(valid_tx_details.received_by_me, &balance - &sol_required); + println!("{:?}", valid_tx_details); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn solana_test_transactions() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Devnet); + let valid_tx_details = block_on( + sol_coin + .withdraw(WithdrawRequest { + coin: "SOL".to_string(), + from: None, + to: sol_coin.my_address.clone(), + amount: BigDecimal::try_from(0.0001).unwrap(), + max: false, + fee: None, + }) + .compat(), + ) + .unwrap(); + println!("{:?}", valid_tx_details); + + let tx_str = hex::encode(&*valid_tx_details.tx_hex.0); + let res = block_on(sol_coin.send_raw_tx(&tx_str).compat()).unwrap(); + + let res2 = block_on(sol_coin.send_raw_tx_bytes(&*valid_tx_details.tx_hex.0).compat()).unwrap(); + assert_eq!(res, res2); + + //println!("{:?}", res); +} + +// This test is just a unit test for brainstorming around tx_history for base_coin. +#[test] +#[ignore] +#[cfg(not(target_arch = "wasm32"))] +fn solana_test_tx_history() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + let res = sol_coin + .client + .get_signatures_for_address(&sol_coin.key_pair.pubkey()) + .unwrap(); + let mut history = Vec::new(); + for cur in res.iter() { + let signature = Signature::from_str(cur.signature.clone().as_str()).unwrap(); + let res = sol_coin + .client + .get_transaction(&signature, UiTransactionEncoding::JsonParsed) + .unwrap(); + println!("{}", serde_json::to_string(&res).unwrap()); + let parsed = serde_json::to_value(&res).unwrap(); + let tx_infos: SolanaConfirmedTransaction = serde_json::from_value(parsed).unwrap(); + let mut txs = tx_infos.extract_solana_transactions(&sol_coin).unwrap(); + history.append(&mut txs); + } + println!("{}", serde_json::to_string(&history).unwrap()); +} diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs new file mode 100644 index 0000000000..8447d10b28 --- /dev/null +++ b/mm2src/coins/solana/spl.rs @@ -0,0 +1,473 @@ +use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum}; +use crate::solana::solana_common::{ui_amount_to_amount, PrepareTransferData, SufficientBalanceError}; +use crate::solana::{solana_common, AccountError, SolanaCommonOps, SolanaFeeDetails}; +use crate::{BalanceFut, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, RawTransactionFut, + RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, SolanaCoin, TradePreimageFut, + TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionFut, TransactionType, + UnexpectedDerivationMethod, ValidateAddressResult, ValidatePaymentInput, VerificationResult, + WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; +use async_trait::async_trait; +use bincode::serialize; +use common::{async_blocking, now_ms}; +use futures::{FutureExt, TryFutureExt}; +use futures01::Future; +use keys::KeyPair; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::{BigDecimal, MmNumber}; +use rpc::v1::types::Bytes as BytesJson; +use serde_json::Value as Json; +use solana_client::{rpc_client::RpcClient, rpc_request::TokenAccountsFilter}; +use solana_sdk::message::Message; +use solana_sdk::transaction::Transaction; +use solana_sdk::{pubkey::Pubkey, signature::Signer}; +use spl_associated_token_account::{create_associated_token_account, get_associated_token_address}; +use std::{convert::TryFrom, + fmt::{Debug, Formatter, Result as FmtResult}, + str::FromStr, + sync::Arc}; + +#[derive(Debug)] +pub enum SplTokenCreationError { + InvalidPubkey(String), +} + +#[derive(Debug)] +pub struct SplTokenConf { + pub decimals: u8, + pub ticker: String, + pub token_contract_address: Pubkey, +} + +#[derive(Clone, Debug)] +pub struct SplTokenInfo { + pub token_contract_address: Pubkey, + pub decimals: u8, +} + +#[derive(Debug)] +pub struct SplProtocolConf { + pub platform_coin_ticker: String, + pub decimals: u8, + pub token_contract_address: String, +} + +#[derive(Clone)] +pub struct SplToken { + pub conf: Arc, + pub platform_coin: SolanaCoin, +} + +impl Debug for SplToken { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.write_str(&*self.conf.ticker) } +} + +impl SplToken { + pub fn new( + decimals: u8, + ticker: String, + token_address: String, + platform_coin: SolanaCoin, + ) -> Result> { + let token_contract_address = solana_sdk::pubkey::Pubkey::from_str(&token_address) + .map_err(|e| MmError::new(SplTokenCreationError::InvalidPubkey(format!("{:?}", e))))?; + let conf = Arc::new(SplTokenConf { + decimals, + ticker, + token_contract_address, + }); + Ok(SplToken { conf, platform_coin }) + } + + pub fn get_info(&self) -> SplTokenInfo { + SplTokenInfo { + token_contract_address: self.conf.token_contract_address, + decimals: self.decimals(), + } + } +} + +async fn withdraw_spl_token_impl(coin: SplToken, req: WithdrawRequest) -> WithdrawResult { + let (hash, fees) = coin.platform_coin.estimate_withdraw_fees().await?; + let res = coin + .check_balance_and_prepare_transfer(req.max, req.amount.clone(), fees) + .await?; + let system_destination_pubkey = solana_sdk::pubkey::Pubkey::try_from(&*req.to)?; + let contract_key = coin.get_underlying_contract_pubkey(); + let auth_key = coin.platform_coin.key_pair.pubkey(); + let funding_address = coin.get_pubkey().await?; + let dest_token_address = get_associated_token_address(&system_destination_pubkey, &contract_key); + let mut instructions = Vec::with_capacity(1); + let account_info = async_blocking({ + let coin = coin.clone(); + move || coin.rpc().get_account(&dest_token_address) + }) + .await; + if account_info.is_err() { + let instruction_creation = create_associated_token_account(&auth_key, &dest_token_address, &contract_key); + instructions.push(instruction_creation); + } + let amount = ui_amount_to_amount(req.amount, coin.conf.decimals)?; + let instruction_transfer_checked = spl_token::instruction::transfer_checked( + &spl_token::id(), + &funding_address, + &contract_key, + &dest_token_address, + &auth_key, + &[&auth_key], + amount, + coin.conf.decimals, + )?; + instructions.push(instruction_transfer_checked); + let msg = Message::new(&instructions, Some(&auth_key)); + let signers = vec![&coin.platform_coin.key_pair]; + let tx = Transaction::new(&signers, msg, hash); + let serialized_tx = serialize(&tx).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + let received_by_me = if req.to == coin.platform_coin.my_address { + res.to_send.clone() + } else { + 0.into() + }; + Ok(TransactionDetails { + tx_hex: serialized_tx.into(), + tx_hash: tx.signatures[0].to_string(), + from: vec![coin.platform_coin.my_address.clone()], + to: vec![req.to], + total_amount: res.to_send.clone(), + spent_by_me: res.to_send.clone(), + my_balance_change: &received_by_me - &res.to_send, + received_by_me, + block_height: 0, + timestamp: now_ms() / 1000, + fee_details: Some( + SolanaFeeDetails { + amount: res.sol_required, + } + .into(), + ), + coin: coin.conf.ticker.clone(), + internal_id: vec![].into(), + kmd_rewards: None, + transaction_type: TransactionType::StandardTransfer, + }) +} + +async fn withdraw_impl(coin: SplToken, req: WithdrawRequest) -> WithdrawResult { + let validate_address_result = coin.validate_address(&*req.to); + if !validate_address_result.is_valid { + return MmError::err(WithdrawError::InvalidAddress( + validate_address_result.reason.unwrap_or_else(|| "Unknown".to_string()), + )); + } + withdraw_spl_token_impl(coin, req).await +} + +#[async_trait] +impl SolanaCommonOps for SplToken { + fn rpc(&self) -> &RpcClient { &self.platform_coin.client } + + fn is_token(&self) -> bool { true } + + async fn check_balance_and_prepare_transfer( + &self, + max: bool, + amount: BigDecimal, + fees: u64, + ) -> Result> { + solana_common::check_balance_and_prepare_transfer(self, max, amount, fees).await + } +} + +impl SplToken { + fn get_underlying_contract_pubkey(&self) -> Pubkey { self.conf.token_contract_address } + + async fn get_pubkey(&self) -> Result> { + let coin = self.clone(); + let token_accounts = async_blocking(move || { + coin.rpc().get_token_accounts_by_owner( + &coin.platform_coin.key_pair.pubkey(), + TokenAccountsFilter::Mint(coin.get_underlying_contract_pubkey()), + ) + }) + .await?; + if token_accounts.is_empty() { + return MmError::err(AccountError::NotFundedError("account_not_funded".to_string())); + } + Ok(Pubkey::from_str(&*token_accounts[0].pubkey)?) + } + + fn my_balance_impl(&self) -> BalanceFut { + let coin = self.clone(); + let fut = async move { + coin.platform_coin + .my_balance_spl(&SplTokenInfo { + token_contract_address: coin.conf.token_contract_address, + decimals: coin.conf.decimals, + }) + .await + }; + Box::new(fut.boxed().compat()) + } +} + +impl MarketCoinOps for SplToken { + fn ticker(&self) -> &str { &self.conf.ticker } + + fn my_address(&self) -> Result { Ok(self.platform_coin.my_address.clone()) } + + fn get_public_key(&self) -> Result> { unimplemented!() } + + fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } + + fn sign_message(&self, message: &str) -> SignatureResult { + solana_common::sign_message(&self.platform_coin, message) + } + + fn verify_message(&self, signature: &str, message: &str, pubkey_bs58: &str) -> VerificationResult { + solana_common::verify_message(&self.platform_coin, signature, message, pubkey_bs58) + } + + fn my_balance(&self) -> BalanceFut { + let fut = self.my_balance_impl().and_then(Ok); + Box::new(fut) + } + + fn base_coin_balance(&self) -> BalanceFut { self.platform_coin.base_coin_balance() } + + fn platform_ticker(&self) -> &str { self.platform_coin.ticker() } + + #[inline(always)] + fn send_raw_tx(&self, tx: &str) -> Box + Send> { + self.platform_coin.send_raw_tx(tx) + } + + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + self.platform_coin.send_raw_tx_bytes(tx) + } + + fn wait_for_confirmations( + &self, + _tx: &[u8], + _confirmations: u64, + _requires_nota: bool, + _wait_until: u64, + _check_every: u64, + ) -> Box + Send> { + unimplemented!() + } + + fn wait_for_tx_spend( + &self, + _transaction: &[u8], + _wait_until: u64, + _from_block: u64, + _swap_contract_address: &Option, + ) -> TransactionFut { + unimplemented!() + } + + fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result { unimplemented!() } + + fn current_block(&self) -> Box + Send> { self.platform_coin.current_block() } + + fn display_priv_key(&self) -> Result { self.platform_coin.display_priv_key() } + + fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } + + fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } +} + +#[allow(clippy::forget_ref, clippy::forget_copy, clippy::cast_ref_to_mut)] +#[async_trait] +impl SwapOps for SplToken { + fn send_taker_fee(&self, _fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { unimplemented!() } + + fn send_maker_payment( + &self, + time_lock: u32, + taker_pub: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_taker_payment( + &self, + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_maker_spends_taker_payment( + &self, + taker_payment_tx: &[u8], + time_lock: u32, + taker_pub: &[u8], + secret: &[u8], + swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_taker_spends_maker_payment( + &self, + maker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret: &[u8], + swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_taker_refunds_payment( + &self, + taker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!() + } + + fn send_maker_refunds_payment( + &self, + maker_payment_tx: &[u8], + time_lock: u32, + taker_pub: &[u8], + secret_hash: &[u8], + swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + todo!() + } + + fn validate_fee( + &self, + _fee_tx: &TransactionEnum, + _expected_sender: &[u8], + _fee_addr: &[u8], + _amount: &BigDecimal, + _min_block_number: u64, + _uuid: &[u8], + ) -> Box + Send> { + unimplemented!() + } + + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + unimplemented!() + } + + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + unimplemented!() + } + + fn check_if_my_payment_sent( + &self, + time_lock: u32, + other_pub: &[u8], + secret_hash: &[u8], + search_from_block: u64, + swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> Box, Error = String> + Send> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_my( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_other( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + + fn negotiate_swap_contract_addr( + &self, + _other_side_address: Option<&[u8]>, + ) -> Result, MmError> { + unimplemented!() + } + + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { todo!() } +} + +#[allow(clippy::forget_ref, clippy::forget_copy, clippy::cast_ref_to_mut)] +#[async_trait] +impl MmCoin for SplToken { + fn is_asset_chain(&self) -> bool { false } + + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { + Box::new(Box::pin(withdraw_impl(self.clone(), req)).compat()) + } + + fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } + + fn decimals(&self) -> u8 { self.conf.decimals } + + fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { unimplemented!() } + + fn validate_address(&self, address: &str) -> ValidateAddressResult { self.platform_coin.validate_address(address) } + + fn process_history_loop(&self, _ctx: MmArc) -> Box + Send> { unimplemented!() } + + fn history_sync_status(&self) -> HistorySyncState { unimplemented!() } + + /// Get fee to be paid per 1 swap transaction + fn get_trade_fee(&self) -> Box + Send> { unimplemented!() } + + async fn get_sender_trade_fee( + &self, + _value: TradePreimageValue, + _stage: FeeApproxStage, + ) -> TradePreimageResult { + unimplemented!() + } + + fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + + async fn get_fee_to_send_taker_fee( + &self, + _dex_fee_amount: BigDecimal, + _stage: FeeApproxStage, + ) -> TradePreimageResult { + unimplemented!() + } + + fn required_confirmations(&self) -> u64 { 1 } + + fn requires_notarization(&self) -> bool { false } + + fn set_required_confirmations(&self, _confirmations: u64) { unimplemented!() } + + fn set_requires_notarization(&self, _requires_nota: bool) { unimplemented!() } + + fn swap_contract_address(&self) -> Option { unimplemented!() } + + fn mature_confirmations(&self) -> Option { Some(1) } + + fn coin_protocol_info(&self) -> Vec { Vec::new() } + + fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } +} diff --git a/mm2src/coins/solana/spl_tests.rs b/mm2src/coins/solana/spl_tests.rs new file mode 100644 index 0000000000..aa0c58b717 --- /dev/null +++ b/mm2src/coins/solana/spl_tests.rs @@ -0,0 +1,129 @@ +use super::*; +use crate::{solana::solana_common_tests::solana_coin_for_test, + solana::solana_common_tests::{spl_coin_for_test, SolanaNet}}; +use common::{block_on, Future01CompatExt}; +use std::ops::Neg; +use std::str::FromStr; + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn spl_coin_creation() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + let sol_spl_usdc_coin = spl_coin_for_test( + sol_coin.clone(), + "USDC".to_string(), + 6, + solana_sdk::pubkey::Pubkey::from_str("CpMah17kQEL2wqyMKt3mZBdTnZbkbfx4nqmQMFDP5vwp").unwrap(), + ); + + println!("address: {}", sol_spl_usdc_coin.my_address().unwrap()); + assert_eq!( + sol_spl_usdc_coin.my_address().unwrap(), + "FJktmyjV9aBHEShT4hfnLpr9ELywdwVtEL1w1rSWgbVf" + ); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_sign_message() { + let passphrase = "spice describe gravity federal blast come thank unfair canal monkey style afraid".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + let sol_spl_usdc_coin = spl_coin_for_test( + sol_coin.clone(), + "USDC".to_string(), + 6, + solana_sdk::pubkey::Pubkey::from_str("CpMah17kQEL2wqyMKt3mZBdTnZbkbfx4nqmQMFDP5vwp").unwrap(), + ); + let signature = sol_spl_usdc_coin.sign_message("test").unwrap(); + assert_eq!( + signature, + "4dzKwEteN8nch76zPMEjPX19RsaQwGTxsbtfg2bwGTkGenLfrdm31zvn9GH5rvaJBwivp6ESXx1KYR672ngs3UfF" + ); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_verify_message() { + let passphrase = "spice describe gravity federal blast come thank unfair canal monkey style afraid".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + let sol_spl_usdc_coin = spl_coin_for_test( + sol_coin.clone(), + "USDC".to_string(), + 6, + solana_sdk::pubkey::Pubkey::from_str("CpMah17kQEL2wqyMKt3mZBdTnZbkbfx4nqmQMFDP5vwp").unwrap(), + ); + let is_valid = sol_spl_usdc_coin + .verify_message( + "4dzKwEteN8nch76zPMEjPX19RsaQwGTxsbtfg2bwGTkGenLfrdm31zvn9GH5rvaJBwivp6ESXx1KYR672ngs3UfF", + "test", + "8UF6jSVE1jW8mSiGqt8Hft1rLwPjdKLaTfhkNozFwoAG", + ) + .unwrap(); + assert!(is_valid); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn spl_my_balance() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + let sol_spl_usdc_coin = spl_coin_for_test( + sol_coin.clone(), + "USDC".to_string(), + 6, + solana_sdk::pubkey::Pubkey::from_str("CpMah17kQEL2wqyMKt3mZBdTnZbkbfx4nqmQMFDP5vwp").unwrap(), + ); + + let res = block_on(sol_spl_usdc_coin.my_balance().compat()).unwrap(); + assert_ne!(res.spendable, BigDecimal::from(0)); + assert!(res.spendable < BigDecimal::from(10)); + + let sol_spl_wsol_coin = spl_coin_for_test( + sol_coin.clone(), + "WSOL".to_string(), + 8, + solana_sdk::pubkey::Pubkey::from_str("So11111111111111111111111111111111111111112").unwrap(), + ); + let res = block_on(sol_spl_wsol_coin.my_balance().compat()).unwrap(); + assert_eq!(res.spendable, BigDecimal::from(0)); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_spl_transactions() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, sol_coin) = solana_coin_for_test(passphrase.clone(), SolanaNet::Testnet); + let usdc_sol_coin = spl_coin_for_test( + sol_coin.clone(), + "USDC".to_string(), + 6, + solana_sdk::pubkey::Pubkey::from_str("CpMah17kQEL2wqyMKt3mZBdTnZbkbfx4nqmQMFDP5vwp").unwrap(), + ); + let withdraw_amount = BigDecimal::from_str("0.0001").unwrap(); + let valid_tx_details = block_on( + usdc_sol_coin + .withdraw(WithdrawRequest { + coin: "USDC".to_string(), + from: None, + to: "AYJmtzc9D4KU6xsDzhKShFyYKUNXY622j9QoQEo4LfpX".to_string(), + amount: withdraw_amount.clone(), + max: false, + fee: None, + }) + .compat(), + ) + .unwrap(); + println!("{:?}", valid_tx_details); + assert_eq!(valid_tx_details.total_amount, withdraw_amount); + assert_eq!(valid_tx_details.my_balance_change, withdraw_amount.neg()); + assert_eq!(valid_tx_details.coin, "USDC".to_string()); + assert_ne!(valid_tx_details.timestamp, 0); + + let tx_str = hex::encode(&*valid_tx_details.tx_hex.0); + let res = block_on(usdc_sol_coin.send_raw_tx(&tx_str).compat()).unwrap(); + println!("{:?}", res); + + let res2 = block_on(usdc_sol_coin.send_raw_tx_bytes(&*valid_tx_details.tx_hex.0).compat()).unwrap(); + assert_eq!(res, res2); +} diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 7562b00a13..fa8d4d1662 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -1,11 +1,14 @@ -use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum, TransactionFut}; -use crate::{BalanceFut, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, TradePreimageFut, - TradePreimageValue, ValidateAddressResult, WithdrawFut, WithdrawRequest}; -use bigdecimal::BigDecimal; -use common::mm_ctx::MmArc; -use common::mm_error::MmError; -use common::mm_number::MmNumber; +use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, SwapOps, + TradeFee, TransactionEnum, TransactionFut}; +use crate::{BalanceFut, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, SearchForSwapTxSpendInput, + SignatureResult, TradePreimageFut, TradePreimageResult, TradePreimageValue, UnexpectedDerivationMethod, + ValidateAddressResult, ValidatePaymentInput, VerificationResult, WithdrawFut, WithdrawRequest}; +use async_trait::async_trait; use futures01::Future; +use keys::KeyPair; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::{BigDecimal, MmNumber}; use mocktopus::macros::*; use rpc::v1::types::Bytes as BytesJson; use serde_json::Value as Json; @@ -32,13 +35,27 @@ impl MarketCoinOps for TestCoin { fn my_address(&self) -> Result { unimplemented!() } + fn get_public_key(&self) -> Result> { unimplemented!() } + + fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } + + fn sign_message(&self, _message: &str) -> SignatureResult { unimplemented!() } + + fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult { + unimplemented!() + } + fn my_balance(&self) -> BalanceFut { unimplemented!() } fn base_coin_balance(&self) -> BalanceFut { unimplemented!() } + fn platform_ticker(&self) -> &str { unimplemented!() } + /// Receives raw transaction bytes in hexadecimal format as input and returns tx hash in hexadecimal format fn send_raw_tx(&self, tx: &str) -> Box + Send> { unimplemented!() } + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { unimplemented!() } + fn wait_for_confirmations( &self, tx: &[u8], @@ -64,17 +81,18 @@ impl MarketCoinOps for TestCoin { fn current_block(&self) -> Box + Send> { unimplemented!() } - fn display_priv_key(&self) -> String { unimplemented!() } + fn display_priv_key(&self) -> Result { unimplemented!() } fn min_tx_amount(&self) -> BigDecimal { unimplemented!() } fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } } +#[async_trait] #[mockable] #[allow(clippy::forget_ref, clippy::forget_copy, clippy::cast_ref_to_mut)] impl SwapOps for TestCoin { - fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal) -> TransactionFut { unimplemented!() } + fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, uuid: &[u8]) -> TransactionFut { unimplemented!() } fn send_maker_payment( &self, @@ -83,6 +101,7 @@ impl SwapOps for TestCoin { secret_hash: &[u8], amount: BigDecimal, swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() } @@ -94,6 +113,7 @@ impl SwapOps for TestCoin { secret_hash: &[u8], amount: BigDecimal, swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() } @@ -105,6 +125,7 @@ impl SwapOps for TestCoin { taker_pub: &[u8], secret: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() } @@ -116,6 +137,7 @@ impl SwapOps for TestCoin { maker_pub: &[u8], secret: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() } @@ -127,6 +149,7 @@ impl SwapOps for TestCoin { maker_pub: &[u8], secret_hash: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() } @@ -138,6 +161,7 @@ impl SwapOps for TestCoin { taker_pub: &[u8], secret_hash: &[u8], swap_contract_address: &Option, + _swap_unique_data: &[u8], ) -> TransactionFut { unimplemented!() } @@ -149,30 +173,21 @@ impl SwapOps for TestCoin { fee_addr: &[u8], amount: &BigDecimal, min_block_number: u64, + _uuid: &[u8], ) -> Box + Send> { unimplemented!() } fn validate_maker_payment( &self, - payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - priv_bn_hash: &[u8], - amount: BigDecimal, - swap_contract_address: &Option, + _input: ValidatePaymentInput, ) -> Box + Send> { unimplemented!() } fn validate_taker_payment( &self, - payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - priv_bn_hash: &[u8], - amount: BigDecimal, - swap_contract_address: &Option, + _input: ValidatePaymentInput, ) -> Box + Send> { unimplemented!() } @@ -184,30 +199,21 @@ impl SwapOps for TestCoin { secret_hash: &[u8], search_from_block: u64, swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> Box, Error = String> + Send> { unimplemented!() } - fn search_for_swap_tx_spend_my( + async fn search_for_swap_tx_spend_my( &self, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - swap_contract_address: &Option, + _: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { unimplemented!() } - fn search_for_swap_tx_spend_other( + async fn search_for_swap_tx_spend_other( &self, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - swap_contract_address: &Option, + _: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { unimplemented!() } @@ -220,13 +226,18 @@ impl SwapOps for TestCoin { ) -> Result, MmError> { unimplemented!() } + + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { unimplemented!() } } +#[async_trait] #[mockable] #[allow(clippy::forget_ref, clippy::forget_copy, clippy::cast_ref_to_mut)] impl MmCoin for TestCoin { fn is_asset_chain(&self) -> bool { unimplemented!() } + fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { unimplemented!() } fn decimals(&self) -> u8 { unimplemented!() } @@ -242,17 +253,21 @@ impl MmCoin for TestCoin { /// Get fee to be paid per 1 swap transaction fn get_trade_fee(&self) -> Box + Send> { unimplemented!() } - fn get_sender_trade_fee(&self, value: TradePreimageValue, stage: FeeApproxStage) -> TradePreimageFut { + async fn get_sender_trade_fee( + &self, + value: TradePreimageValue, + stage: FeeApproxStage, + ) -> TradePreimageResult { unimplemented!() } fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } - fn get_fee_to_send_taker_fee( + async fn get_fee_to_send_taker_fee( &self, dex_fee_amount: BigDecimal, stage: FeeApproxStage, - ) -> TradePreimageFut { + ) -> TradePreimageResult { unimplemented!() } diff --git a/mm2src/coins/tx_history_db.rs b/mm2src/coins/tx_history_db.rs deleted file mode 100644 index 27a0f7b2ec..0000000000 --- a/mm2src/coins/tx_history_db.rs +++ /dev/null @@ -1,467 +0,0 @@ -use crate::TransactionDetails; -use async_trait::async_trait; -use common::mm_error::prelude::*; -use derive_more::Display; -use std::path::PathBuf; - -#[cfg(not(target_arch = "wasm32"))] -pub use native_db::TxHistoryDb; -#[cfg(target_arch = "wasm32")] pub use wasm_db::TxHistoryDb; - -pub type TxHistoryResult = Result>; - -#[derive(Debug, Display)] -pub enum TxHistoryError { - ErrorSerializing(String), - ErrorDeserializing(String), - ErrorSaving(String), - ErrorLoading(String), - ErrorClearing(String), - NotSupported(String), - InternalError(String), -} - -#[async_trait] -pub trait TxHistoryOps { - async fn init_with_fs_path(db_dir: PathBuf) -> TxHistoryResult; - - async fn load_history(&mut self, ticker: &str, wallet_address: &str) -> TxHistoryResult>; - - async fn save_history( - &mut self, - ticker: &str, - wallet_address: &str, - txs: Vec, - ) -> TxHistoryResult<()>; - - async fn clear(&mut self, ticker: &str, wallet_address: &str) -> TxHistoryResult<()>; -} - -#[cfg(not(target_arch = "wasm32"))] -mod native_db { - use super::*; - use async_std::fs; - use futures::AsyncWriteExt; - use serde_json as json; - use std::io; - - pub struct TxHistoryDb { - tx_history_path: PathBuf, - } - - #[async_trait] - impl TxHistoryOps for TxHistoryDb { - async fn init_with_fs_path(db_dir: PathBuf) -> TxHistoryResult { - Ok(TxHistoryDb { - tx_history_path: db_dir, - }) - } - - async fn load_history( - &mut self, - ticker: &str, - wallet_address: &str, - ) -> TxHistoryResult> { - let path = self.ticker_history_path(ticker, wallet_address); - let content = match fs::read(&path).await { - Ok(content) => content, - Err(err) if err.kind() == io::ErrorKind::NotFound => { - return Ok(Vec::new()); - }, - Err(err) => { - let error = format!("Error '{}' reading from the history file {}", err, path.display()); - return MmError::err(TxHistoryError::ErrorLoading(error)); - }, - }; - json::from_slice(&content).map_to_mm(|e| TxHistoryError::ErrorDeserializing(e.to_string())) - } - - async fn save_history( - &mut self, - ticker: &str, - wallet_address: &str, - txs: Vec, - ) -> TxHistoryResult<()> { - let content = json::to_vec(&txs).map_to_mm(|e| TxHistoryError::ErrorSerializing(e.to_string()))?; - let path = self.ticker_history_path(ticker, wallet_address); - - let tmp_file = format!("{}.tmp", path.display()); - let fut = async { - let mut file = fs::File::create(&tmp_file).await?; - file.write_all(&content).await?; - file.flush().await?; - fs::rename(&tmp_file, &path).await?; - Ok(()) - }; - let res: io::Result<_> = fut.await; - if let Err(e) = res { - let error = format!("Error '{}' creating/writing/renaming the tmp file {}", e, tmp_file); - return MmError::err(TxHistoryError::ErrorSaving(error)); - } - Ok(()) - } - - async fn clear(&mut self, ticker: &str, wallet_address: &str) -> TxHistoryResult<()> { - fs::remove_file(&self.ticker_history_path(ticker, wallet_address)) - .await - .map_to_mm(|e| TxHistoryError::ErrorClearing(e.to_string())) - } - } - - impl TxHistoryDb { - fn ticker_history_path(&self, ticker: &str, wallet_address: &str) -> PathBuf { - // BCH cash address format has colon after prefix, e.g. bitcoincash: - // Colon can't be used in file names on Windows so it should be escaped - let wallet_address = wallet_address.replace(":", "_"); - self.tx_history_path.join(format!("{}_{}.json", ticker, wallet_address)) - } - } -} - -/// Since `IndexedDb`, `DbTransaction`, `DbTable` are not `Send`, -/// so we have to spawn locally the database and communicate with it through the `mpsc` channel. -#[cfg(target_arch = "wasm32")] -mod wasm_db { - use super::*; - use common::executor::spawn_local; - use common::panic_w; - use common::wasm_indexed_db::{DbTransactionError, DbUpgrader, IndexedDb, IndexedDbBuilder, InitDbError, - InitDbResult, OnUpgradeResult, TableSignature}; - use common::WasmUnwrapExt; - use futures::channel::{mpsc, oneshot}; - use futures::StreamExt; - use std::path::PathBuf; - - const DB_NAME: &str = "tx_history"; - const DB_VERSION: u32 = 1; - - impl From for TxHistoryError { - fn from(e: InitDbError) -> Self { - match &e { - InitDbError::NotSupported(_) => TxHistoryError::NotSupported(e.to_string()), - InitDbError::EmptyTableList - | InitDbError::DbIsOpenAlready { .. } - | InitDbError::InvalidVersion(_) - | InitDbError::OpeningError(_) - | InitDbError::TypeMismatch { .. } - | InitDbError::UnexpectedState(_) - | InitDbError::UpgradingError { .. } => TxHistoryError::InternalError(e.to_string()), - } - } - } - - impl From for TxHistoryError { - fn from(e: DbTransactionError) -> Self { - match e { - DbTransactionError::ErrorSerializingItem(_) => TxHistoryError::ErrorSerializing(e.to_string()), - DbTransactionError::ErrorDeserializingItem(_) => TxHistoryError::ErrorDeserializing(e.to_string()), - DbTransactionError::ErrorUploadingItem(_) => TxHistoryError::ErrorSaving(e.to_string()), - DbTransactionError::ErrorGettingItems(_) => TxHistoryError::ErrorLoading(e.to_string()), - DbTransactionError::ErrorDeletingItems(_) => TxHistoryError::ErrorClearing(e.to_string()), - DbTransactionError::NoSuchTable { .. } - | DbTransactionError::ErrorCreatingTransaction(_) - | DbTransactionError::ErrorOpeningTable { .. } - | DbTransactionError::UnexpectedState(_) - | DbTransactionError::TransactionAborted - | DbTransactionError::NoSuchIndex { .. } - | DbTransactionError::InvalidIndex { .. } => TxHistoryError::InternalError(e.to_string()), - } - } - } - - type LoadHistoryResult = TxHistoryResult>; - type SaveHistoryResult = TxHistoryResult<()>; - type ClearHistoryResult = TxHistoryResult<()>; - - #[derive(Debug)] - enum TxHistoryEvent { - LoadHistory { - history_id: HistoryId, - result_tx: oneshot::Sender, - }, - SaveHistory { - history_id: HistoryId, - txs: Vec, - result_tx: oneshot::Sender, - }, - Clear { - history_id: HistoryId, - result_tx: oneshot::Sender, - }, - } - - pub struct TxHistoryDb { - event_tx: mpsc::Sender, - } - - #[async_trait] - impl TxHistoryOps for TxHistoryDb { - async fn init_with_fs_path(_path: PathBuf) -> TxHistoryResult { - let (init_tx, init_rx) = oneshot::channel(); - let (event_tx, event_rx) = mpsc::channel(1024); - - Self::init_and_spawn(init_tx, event_rx); - init_rx.await.expect_w("The init channel must not be closed")?; - Ok(TxHistoryDb { event_tx }) - } - - async fn load_history(&mut self, ticker: &str, wallet_address: &str) -> LoadHistoryResult { - let (result_tx, result_rx) = oneshot::channel(); - let load_event = TxHistoryEvent::LoadHistory { - history_id: HistoryId::new(ticker, wallet_address), - result_tx, - }; - if let Err(e) = self.event_tx.try_send(load_event) { - let error = format!("Couldn't send the 'TxHistoryEvent::LoadHistory' event: {}", e); - return MmError::err(TxHistoryError::InternalError(error)); - } - result_rx.await.expect_w("The result channel must not be closed") - } - - async fn save_history( - &mut self, - ticker: &str, - wallet_address: &str, - txs: Vec, - ) -> SaveHistoryResult { - let (result_tx, result_rx) = oneshot::channel(); - let save_event = TxHistoryEvent::SaveHistory { - history_id: HistoryId::new(ticker, wallet_address), - txs, - result_tx, - }; - if let Err(e) = self.event_tx.try_send(save_event) { - let error = format!("Couldn't send the 'TxHistoryEvent::SaveHistory' event: {}", e); - return MmError::err(TxHistoryError::InternalError(error)); - } - result_rx.await.expect_w("The result channel must not be closed") - } - - async fn clear(&mut self, ticker: &str, wallet_address: &str) -> TxHistoryResult<()> { - let (result_tx, result_rx) = oneshot::channel(); - let clear_event = TxHistoryEvent::Clear { - history_id: HistoryId::new(ticker, wallet_address), - result_tx, - }; - if let Err(e) = self.event_tx.try_send(clear_event) { - let error = format!("Couldn't send the 'TxHistoryEvent::Clear' event: {}", e); - return MmError::err(TxHistoryError::InternalError(error)); - } - result_rx.await.expect_w("The result channel must not be closed") - } - } - - impl TxHistoryDb { - fn init_and_spawn(init_tx: oneshot::Sender>, event_rx: mpsc::Receiver) { - let fut = async move { - let db = match IndexedDbBuilder::new(DB_NAME) - .with_version(DB_VERSION) - .with_table::() - .init() - .await - { - Ok(db) => db, - Err(e) => { - // ignore if the receiver is closed - let _res = init_tx.send(Err(e)); - return; - }, - }; - - // ignore if the receiver is closed - let _res = init_tx.send(Ok(())); - // run the event loop - Self::event_loop(event_rx, db).await; - }; - spawn_local(fut); - } - - async fn event_loop(mut rx: mpsc::Receiver, db: IndexedDb) { - while let Some(event) = rx.next().await { - match event { - TxHistoryEvent::LoadHistory { history_id, result_tx } => { - let result = Self::load_history(&db, history_id).await; - // ignore if the receiver is closed - let _res = result_tx.send(result); - }, - TxHistoryEvent::SaveHistory { - history_id, - txs, - result_tx, - } => { - let result = Self::save_history(&db, history_id, txs).await; - // ignore if the receiver is closed - let _res = result_tx.send(result); - }, - TxHistoryEvent::Clear { history_id, result_tx } => { - let result = Self::clear_history(&db, history_id).await; - // ignore if the receiver is closed - let _res = result_tx.send(result); - }, - } - } - } - - async fn load_history(db: &IndexedDb, history_id: HistoryId) -> LoadHistoryResult { - let transaction = db.transaction()?; - let table = transaction.open_table::()?; - let items = table.get_items("history_id", &history_id.0).await?; - if items.len() > 1 { - let error = format!( - "Expected only one item by the 'history_id' index, found {}", - items.len() - ); - return MmError::err(TxHistoryError::InternalError(error)); - } - - let mut item_iter = items.into_iter(); - match item_iter.next() { - Some((_item_id, TxHistoryTable { txs, .. })) => Ok(txs), - None => Ok(Vec::new()), - } - } - - async fn save_history( - db: &IndexedDb, - history_id: HistoryId, - txs: Vec, - ) -> SaveHistoryResult { - let history_id_value = history_id.0.clone(); - let tx_history_item = TxHistoryTable { history_id, txs }; - - let transaction = db.transaction()?; - let table = transaction.open_table::()?; - - // First, check if the coin's tx history exists already. - let ids = table.get_item_ids("history_id", &history_id_value).await?; - match ids.len() { - // The history doesn't exist, add the new `tx_history_item`. - 0 => { - table.add_item(&tx_history_item).await?; - }, - // The history exists already, replace it with the actual `tx_history_item`. - 1 => { - let item_id = ids[0]; - table.replace_item(item_id, tx_history_item).await?; - }, - unexpected_len => { - let error = format!( - "Expected only one item by the 'history_id' index, found {}", - unexpected_len - ); - return MmError::err(TxHistoryError::InternalError(error)); - }, - } - - transaction.wait_for_complete().await?; - Ok(()) - } - - async fn clear_history(db: &IndexedDb, history_id: HistoryId) -> ClearHistoryResult { - let transaction = db.transaction()?; - let table = transaction.open_table::()?; - - // First, check if the coin's tx history exists. - let ids = table.get_item_ids("history_id", &history_id.0).await?; - match ids.len() { - // The history doesn't exist, we don't need to do anything. - 0 => (), - 1 => { - let item_id = ids[0]; - table.delete_item(item_id).await?; - }, - unexpected_len => { - let error = format!( - "Expected only one item by the 'history_id' index, found {}", - unexpected_len - ); - return MmError::err(TxHistoryError::InternalError(error)); - }, - } - - transaction.wait_for_complete().await?; - Ok(()) - } - } - - #[derive(Debug, Deserialize, Serialize)] - struct HistoryId(String); - - impl HistoryId { - fn new(ticker: &str, wallet_address: &str) -> HistoryId { HistoryId(format!("{}_{}", ticker, wallet_address)) } - } - - #[derive(Debug, Deserialize, Serialize)] - struct TxHistoryTable { - history_id: HistoryId, - txs: Vec, - } - - impl TableSignature for TxHistoryTable { - fn table_name() -> &'static str { "tx_history" } - - fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let table = upgrader.create_table(Self::table_name())?; - table.create_index("history_id", true)?; - }, - (1, 1) => (), - v => panic_w(&format!("Unexpected (old, new) versions: {:?}", v)), - } - Ok(()) - } - } -} - -#[cfg(target_arch = "wasm32")] -mod tests { - use super::wasm_db::*; - use super::*; - use common::WasmUnwrapExt; - use serde_json as json; - use wasm_bindgen_test::*; - - wasm_bindgen_test_configure!(run_in_browser); - - #[wasm_bindgen_test] - async fn test_tx_history() { - let mut db = TxHistoryDb::init_with_fs_path(PathBuf::default()) - .await - .expect_w("!TxHistoryDb::init_with_fs_path"); - - let history = db - .load_history("RICK", "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD") - .await - .expect_w("!TxHistoryDb::load_history"); - assert!(history.is_empty()); - - let history_str = r#"[{"tx_hex":"0400008085202f89018ec8f6f02e008ebd57bbf94c0d8297c1825f3af204490c43f5652b002a2c8b17010000006b483045022100e625a8b77beac5ec891e7d44b18fc4d780ef8456847eb2c6fa2f765e3379a9c102200f323612189fa44ee16f5deb39809b71c4811545b36ee4d6cc622d01aab10ef3012102043663e9c5af8275771809b3889d437f559e49e8df79b6ba19ade4cc5d8eb3e0ffffffff03809698000000000017a9142ffaf6694c6b441790546eefd277e430e08e47a6870000000000000000166a14094ab490fffa9939544545a656b345bf21920a90f6b35714000000001976a9145ed376ce9faa63cb2fef5862e1a5cc811c17316588acf9d29260000000000000000000000000000000","tx_hash":"f05d786bd4b647a5720094bf0a2c6f23b5e131c451d750a96102898f7b5458e8","from":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3"],"to":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3","bH6y6RtvbLToqSUNtLA5rQRjSwyNNzUSNc"],"total_amount":"3.51293022","spent_by_me":"3.51293022","received_by_me":"3.41292022","my_balance_change":"-0.10001","block_height":916940,"timestamp":1620235027,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"RICK","internal_id":"f05d786bd4b647a5720094bf0a2c6f23b5e131c451d750a96102898f7b5458e8"}]"#; - let history: Vec = json::from_str(history_str).unwrap_w(); - db.save_history("RICK", "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD", history) - .await - .expect_w("!TxHistoryDb::save_history"); - - let updated_history_str = r#"[{"tx_hex":"0400008085202f89018ec8f6f02e008ebd57bbf94c0d8297c1825f3af204490c43f5652b002a2c8b17010000006b483045022100e625a8b77beac5ec891e7d44b18fc4d780ef8456847eb2c6fa2f765e3379a9c102200f323612189fa44ee16f5deb39809b71c4811545b36ee4d6cc622d01aab10ef3012102043663e9c5af8275771809b3889d437f559e49e8df79b6ba19ade4cc5d8eb3e0ffffffff03809698000000000017a9142ffaf6694c6b441790546eefd277e430e08e47a6870000000000000000166a14094ab490fffa9939544545a656b345bf21920a90f6b35714000000001976a9145ed376ce9faa63cb2fef5862e1a5cc811c17316588acf9d29260000000000000000000000000000000","tx_hash":"f05d786bd4b647a5720094bf0a2c6f23b5e131c451d750a96102898f7b5458e8","from":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3"],"to":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3","bH6y6RtvbLToqSUNtLA5rQRjSwyNNzUSNc"],"total_amount":"3.51293022","spent_by_me":"3.51293022","received_by_me":"3.41292022","my_balance_change":"-0.10001","block_height":916940,"timestamp":1620235027,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"RICK","internal_id":"f05d786bd4b647a5720094bf0a2c6f23b5e131c451d750a96102898f7b5458e8"},{"tx_hex":"0400008085202f8901a5620f30001e5e31bcbffacee7687fd84490fa1f8625ddd1e098f0bc530d673e020000006b483045022100a9864707855307681b81d94ae17328f6feccb2a9439d27378dfeae2df0220cc102207476f8304af14794a9cd2fe6287e07a1c802c05ec134b789ddb22ab51f7d2238012102043663e9c5af8275771809b3889d437f559e49e8df79b6ba19ade4cc5d8eb3e0ffffffff0246320000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ac5e4ef014000000001976a9145ed376ce9faa63cb2fef5862e1a5cc811c17316588acacd29260000000000000000000000000000000","tx_hash":"178b2c2a002b65f5430c4904f23a5f82c197820d4cf9bb57bd8e002ef0f6c88e","from":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3"],"to":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3","RThtXup6Zo7LZAi8kRWgjAyi1s4u6U9Cpf"],"total_amount":"3.51306892","spent_by_me":"3.51306892","received_by_me":"3.51293022","my_balance_change":"-0.0001387","block_height":916939,"timestamp":1620234997,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"RICK","internal_id":"178b2c2a002b65f5430c4904f23a5f82c197820d4cf9bb57bd8e002ef0f6c88e"}]"#; - let updated_history: Vec = json::from_str(updated_history_str).unwrap_w(); - db.save_history("RICK", "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD", updated_history.clone()) - .await - .expect_w("!TxHistoryDb::save_history"); - - let actual_history = db - .load_history("RICK", "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD") - .await - .expect_w("!TxHistoryDb::load_history"); - assert_eq!(actual_history, updated_history); - - db.clear("RICK", "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD") - .await - .expect_w("!TxHistoryDb::clear"); - - let history = db - .load_history("RICK", "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD") - .await - .expect_w("!TxHistoryDb::load_history"); - assert!(history.is_empty()); - } -} diff --git a/mm2src/coins/tx_history_storage/mod.rs b/mm2src/coins/tx_history_storage/mod.rs new file mode 100644 index 0000000000..9fca3dcc3d --- /dev/null +++ b/mm2src/coins/tx_history_storage/mod.rs @@ -0,0 +1,183 @@ +use crate::my_tx_history_v2::TxHistoryStorage; +use crate::TransactionType; +use derive_more::Display; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use num_traits::Zero; +use primitives::hash::H160; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::HashSet; +use std::iter::FromIterator; + +#[cfg(target_arch = "wasm32")] pub mod wasm; + +#[cfg(not(target_arch = "wasm32"))] +pub mod sql_tx_history_storage_v2; + +#[cfg(any(test, target_arch = "wasm32"))] +mod tx_history_v2_tests; + +/// Get `token_id` from the transaction type. +/// Returns an empty `token_id` if the transaction is [`TransactionType::StandardTransfer`]. +#[inline] +pub fn token_id_from_tx_type(tx_type: &TransactionType) -> String { + match tx_type { + TransactionType::TokenTransfer(token_id) => format!("{:02x}", token_id), + _ => String::new(), + } +} + +#[derive(Debug, Display)] +pub enum CreateTxHistoryStorageError { + Internal(String), +} + +/// `TxHistoryStorageBuilder` is used to create an instance that implements the `TxHistoryStorage` trait. +pub struct TxHistoryStorageBuilder<'a> { + ctx: &'a MmArc, +} + +impl<'a> TxHistoryStorageBuilder<'a> { + #[inline] + pub fn new(ctx: &MmArc) -> TxHistoryStorageBuilder<'_> { TxHistoryStorageBuilder { ctx } } + + #[inline] + pub fn build(self) -> MmResult { + #[cfg(target_arch = "wasm32")] + return wasm::IndexedDbTxHistoryStorage::new(self.ctx); + #[cfg(not(target_arch = "wasm32"))] + sql_tx_history_storage_v2::SqliteTxHistoryStorage::new(self.ctx) + } +} + +/// Whether transaction is unconfirmed or confirmed. +/// Serializes to either `0u8` or `1u8` correspondingly. +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +pub enum ConfirmationStatus { + Unconfirmed = 0, + Confirmed = 1, +} + +impl ConfirmationStatus { + #[inline] + pub fn from_block_height(height: Height) -> ConfirmationStatus { + if height.is_zero() { + ConfirmationStatus::Unconfirmed + } else { + ConfirmationStatus::Confirmed + } + } +} + +impl Serialize for ConfirmationStatus { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (*self as u8).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for ConfirmationStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let code = u8::deserialize(deserializer)?; + match code { + 0 => Ok(ConfirmationStatus::Unconfirmed), + 1 => Ok(ConfirmationStatus::Confirmed), + unknown => Err(D::Error::custom(format!( + "Expected either '0' or '1' confirmation status, found '{}'", + unknown + ))), + } + } +} + +#[derive(Clone, Debug)] +pub struct WalletId { + ticker: String, + hd_wallet_rmd160: Option, +} + +impl WalletId { + #[inline] + pub fn new(ticker: String) -> WalletId { + WalletId { + ticker, + hd_wallet_rmd160: None, + } + } + + #[inline] + pub fn set_hd_wallet_rmd160(&mut self, hd_wallet_rmd160: H160) { self.hd_wallet_rmd160 = Some(hd_wallet_rmd160); } + + #[inline] + pub fn with_hd_wallet_rmd160(mut self, hd_wallet_rmd160: H160) -> WalletId { + self.set_hd_wallet_rmd160(hd_wallet_rmd160); + self + } +} + +#[derive(Debug, Default)] +pub struct GetTxHistoryFilters { + token_id: Option, + for_addresses: Option, +} + +impl GetTxHistoryFilters { + #[inline] + pub fn new() -> GetTxHistoryFilters { GetTxHistoryFilters::default() } + + #[inline] + pub fn with_token_id(mut self, token_id: String) -> GetTxHistoryFilters { + self.token_id = Some(token_id); + self + } + + #[inline] + pub fn set_for_addresses>(&mut self, addresses: I) { + self.for_addresses = Some(addresses.into_iter().collect()); + } + + #[inline] + pub fn with_for_addresses>(mut self, addresses: I) -> GetTxHistoryFilters { + self.set_for_addresses(addresses); + self + } + + /// If [`GetTxHistoryFilters::token_id`] is not specified, + /// we should exclude token's transactions by applying an empty `token_id` filter. + fn token_id_or_exclude(&self) -> String { self.token_id.clone().unwrap_or_default() } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct FilteringAddresses(HashSet); + +impl FilteringAddresses { + #[inline] + pub fn is_empty(&self) -> bool { self.0.is_empty() } + + #[inline] + pub fn len(&self) -> usize { self.0.len() } + + /// Whether the containers have the same addresses. + #[inline] + pub fn has_intersection(&self, other: &FilteringAddresses) -> bool { + self.0.intersection(&other.0).next().is_some() + } +} + +impl IntoIterator for FilteringAddresses { + type Item = String; + type IntoIter = std::collections::hash_set::IntoIter; + + fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } +} + +impl FromIterator for FilteringAddresses { + fn from_iter>(iter: T) -> Self { FilteringAddresses(iter.into_iter().collect()) } +} diff --git a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs new file mode 100644 index 0000000000..a0bcf1c915 --- /dev/null +++ b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs @@ -0,0 +1,642 @@ +use crate::my_tx_history_v2::{GetHistoryResult, RemoveTxResult, TxHistoryStorage, TxHistoryStorageError}; +use crate::tx_history_storage::{token_id_from_tx_type, ConfirmationStatus, CreateTxHistoryStorageError, + FilteringAddresses, GetTxHistoryFilters, WalletId}; +use crate::TransactionDetails; +use async_trait::async_trait; +use common::{async_blocking, PagingOptionsEnum}; +use db_common::sql_query::SqlQuery; +use db_common::sqlite::rusqlite::types::Type; +use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Row, NO_PARAMS}; +use db_common::sqlite::{query_single_row, string_from_row, validate_table_name, CHECK_TABLE_EXISTS_SQL}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc::v1::types::Bytes as BytesJson; +use serde_json::{self as json}; +use std::convert::TryInto; +use std::sync::{Arc, Mutex}; + +fn tx_history_table(wallet_id: &WalletId) -> String { wallet_id.to_sql_table_name() + "_tx_history" } + +fn tx_address_table(wallet_id: &WalletId) -> String { wallet_id.to_sql_table_name() + "_tx_address" } + +/// Please note TX cache table name doesn't depend on [`WalletId::hd_wallet_rmd160`]. +fn tx_cache_table(wallet_id: &WalletId) -> String { format!("{}_tx_cache", wallet_id.ticker) } + +fn create_tx_history_table_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_history_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!( + "CREATE TABLE IF NOT EXISTS {} ( + id INTEGER NOT NULL PRIMARY KEY, + tx_hash VARCHAR(255) NOT NULL, + internal_id VARCHAR(255) NOT NULL UNIQUE, + block_height INTEGER NOT NULL, + confirmation_status INTEGER NOT NULL, + token_id VARCHAR(255) NOT NULL, + details_json TEXT + );", + table_name + ); + + Ok(sql) +} + +fn create_tx_address_table_sql(wallet_id: &WalletId) -> Result> { + let tx_address_table = tx_address_table(wallet_id); + validate_table_name(&tx_address_table)?; + + let sql = format!( + "CREATE TABLE IF NOT EXISTS {} ( + id INTEGER NOT NULL PRIMARY KEY, + internal_id VARCHAR(255) NOT NULL, + address TEXT NOT NULL + );", + tx_address_table + ); + + Ok(sql) +} + +fn create_tx_cache_table_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_cache_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!( + "CREATE TABLE IF NOT EXISTS {} ( + tx_hash VARCHAR(255) NOT NULL UNIQUE, + tx_hex TEXT NOT NULL + );", + table_name + ); + + Ok(sql) +} + +fn create_internal_id_index_sql(wallet_id: &WalletId, table_name_creator: F) -> Result> +where + F: FnOnce(&WalletId) -> String, +{ + let table_name = table_name_creator(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!( + "CREATE INDEX IF NOT EXISTS internal_id_idx ON {} (internal_id);", + table_name + ); + Ok(sql) +} + +fn insert_tx_in_history_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_history_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!( + "INSERT INTO {} ( + tx_hash, + internal_id, + block_height, + confirmation_status, + token_id, + details_json + ) VALUES ( + ?1, ?2, ?3, ?4, ?5, ?6 + );", + table_name + ); + + Ok(sql) +} + +fn insert_tx_address_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_address_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!( + "INSERT INTO {} ( + internal_id, + address + ) VALUES (?1, ?2);", + table_name + ); + + Ok(sql) +} + +fn insert_tx_in_cache_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_cache_table(wallet_id); + validate_table_name(&table_name)?; + + // We can simply ignore the repetitive attempt to insert the same tx_hash + let sql = format!( + "INSERT OR IGNORE INTO {} (tx_hash, tx_hex) VALUES (?1, ?2);", + table_name + ); + + Ok(sql) +} + +fn remove_tx_by_internal_id_sql(wallet_id: &WalletId, table_name_creator: F) -> Result> +where + F: FnOnce(&WalletId) -> String, +{ + let table_name = table_name_creator(wallet_id); + validate_table_name(&table_name)?; + let sql = format!("DELETE FROM {} WHERE internal_id=?1;", table_name); + Ok(sql) +} + +fn select_tx_by_internal_id_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_history_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!("SELECT details_json FROM {} WHERE internal_id=?1;", table_name); + + Ok(sql) +} + +fn update_tx_in_table_by_internal_id_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_history_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!( + "UPDATE {} SET + block_height = ?1, + confirmation_status = ?2, + details_json = ?3 + WHERE + internal_id=?4;", + table_name + ); + + Ok(sql) +} + +fn contains_unconfirmed_transactions_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_history_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!( + "SELECT COUNT(id) FROM {} WHERE confirmation_status = {};", + table_name, + ConfirmationStatus::Unconfirmed.to_sql_param() + ); + + Ok(sql) +} + +fn get_unconfirmed_transactions_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_history_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!( + "SELECT details_json FROM {} WHERE confirmation_status = {};", + table_name, + ConfirmationStatus::Unconfirmed.to_sql_param() + ); + + Ok(sql) +} + +fn has_transactions_with_hash_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_history_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!("SELECT COUNT(id) FROM {} WHERE tx_hash = ?1;", table_name); + + Ok(sql) +} + +fn unique_tx_hashes_num_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_history_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!("SELECT COUNT(DISTINCT tx_hash) FROM {};", table_name); + + Ok(sql) +} + +fn get_tx_hex_from_cache_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_cache_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!("SELECT tx_hex FROM {} WHERE tx_hash = ?1 LIMIT 1;", table_name); + + Ok(sql) +} + +/// Creates an `SqlQuery` instance with the required `WHERE`, `ORDER`, `GROUP_BY` constraints. +/// Please note you can refer to the [`tx_history_table(wallet_id)`] table by the `tx_history` alias. +fn get_history_builder_preimage<'a>( + connection: &'a Connection, + wallet_id: &WalletId, + token_id: String, + for_addresses: Option, +) -> Result, MmError> { + let mut sql_builder = SqlQuery::select_from_alias(connection, &tx_history_table(wallet_id), "tx_history")?; + + // Check if we need to join the [`tx_address_table(wallet_id)`] table + // to query transactions that were sent from/to `for_addresses` addresses. + if let Some(for_addresses) = for_addresses { + let tx_address_table_name = tx_address_table(wallet_id); + + sql_builder + .join_alias(&tx_address_table_name, "tx_address")? + .on_join_eq("tx_history.internal_id", "tx_address.internal_id")?; + + sql_builder + .and_where_in_params("tx_address.address", for_addresses)? + .group_by("tx_history.internal_id")?; + } + + sql_builder + .and_where_eq_param("tx_history.token_id", token_id)? + .order_asc("tx_history.confirmation_status")? + .order_desc("tx_history.block_height")? + .order_asc("tx_history.id")?; + Ok(sql_builder) +} + +fn finalize_get_total_count_sql_builder(mut subquery: SqlQuery<'_>) -> Result, MmError> { + /// The alias is needed so that the external query can access the results of the subquery. + /// Example: + /// SUBQUERY: `SELECT h.internal_id AS __INTERNAL_ID_ALIAS FROM tx_history h JOIN tx_address a ON h.internal_id = a.internal_id WHERE a.address IN ('address_2', 'address_4') GROUP BY h.internal_id` + /// EXTERNAL_QUERY: `SELECT COUNT(__INTERNAL_ID_ALIAS) FROM ();` + /// Here we can't use `h.internal_id` in the external query because it doesn't know about the `tx_history h` table. + /// So we need to give the `h.internal_id` an alias like `__INTERNAL_ID_ALIAS`. + const INTERNAL_ID_ALIAS: &str = "__INTERNAL_ID_ALIAS"; + + // Query `id_field` and give it the `__ID_FIELD` alias. + subquery.field_alias("tx_history.internal_id", INTERNAL_ID_ALIAS)?; + + let mut external_query = SqlQuery::select_from_subquery(subquery.subquery())?; + external_query.count(INTERNAL_ID_ALIAS)?; + Ok(external_query) +} + +fn finalize_get_history_sql_builder(sql_builder: &mut SqlQuery, offset: usize, limit: usize) -> Result<(), SqlError> { + sql_builder + .field("tx_history.details_json")? + .offset(offset) + .limit(limit); + Ok(()) +} + +fn tx_details_from_row(row: &Row<'_>) -> Result { + let json_string: String = row.get(0)?; + json::from_str(&json_string).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e))) +} + +impl TxHistoryStorageError for SqlError {} + +impl ConfirmationStatus { + fn to_sql_param(self) -> String { (self as u8).to_string() } +} + +impl WalletId { + fn to_sql_table_name(&self) -> String { + match self.hd_wallet_rmd160 { + Some(hd_wallet_rmd160) => format!("{}_{}", self.ticker, hd_wallet_rmd160), + None => self.ticker.clone(), + } + } +} + +#[derive(Clone)] +pub struct SqliteTxHistoryStorage(Arc>); + +impl SqliteTxHistoryStorage { + pub fn new(ctx: &MmArc) -> Result> { + let sqlite_connection = ctx + .sqlite_connection + .ok_or(MmError::new(CreateTxHistoryStorageError::Internal( + "sqlite_connection is not initialized".to_owned(), + )))?; + Ok(SqliteTxHistoryStorage(sqlite_connection.clone())) + } +} + +#[async_trait] +impl TxHistoryStorage for SqliteTxHistoryStorage { + type Error = SqlError; + + async fn init(&self, wallet_id: &WalletId) -> Result<(), MmError> { + let selfi = self.clone(); + + let sql_history = create_tx_history_table_sql(wallet_id)?; + let sql_cache = create_tx_cache_table_sql(wallet_id)?; + let sql_addr = create_tx_address_table_sql(wallet_id)?; + + let sql_history_index = create_internal_id_index_sql(wallet_id, tx_history_table)?; + let sql_addr_index = create_internal_id_index_sql(wallet_id, tx_address_table)?; + + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + + conn.execute(&sql_history, NO_PARAMS).map(|_| ())?; + conn.execute(&sql_addr, NO_PARAMS).map(|_| ())?; + conn.execute(&sql_cache, NO_PARAMS).map(|_| ())?; + + conn.execute(&sql_history_index, NO_PARAMS).map(|_| ())?; + conn.execute(&sql_addr_index, NO_PARAMS).map(|_| ())?; + Ok(()) + }) + .await + } + + async fn is_initialized_for(&self, wallet_id: &WalletId) -> Result> { + let tx_history_table = tx_history_table(wallet_id); + validate_table_name(&tx_history_table)?; + + let tx_cache_table = tx_cache_table(wallet_id); + validate_table_name(&tx_cache_table)?; + + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let history_initialized = + query_single_row(&conn, CHECK_TABLE_EXISTS_SQL, [tx_history_table], string_from_row)?; + let cache_initialized = query_single_row(&conn, CHECK_TABLE_EXISTS_SQL, [tx_cache_table], string_from_row)?; + Ok(history_initialized.is_some() && cache_initialized.is_some()) + }) + .await + } + + async fn add_transactions_to_history( + &self, + wallet_id: &WalletId, + transactions: I, + ) -> Result<(), MmError> + where + I: IntoIterator + Send + 'static, + I::IntoIter: Send, + { + let selfi = self.clone(); + let wallet_id = wallet_id.clone(); + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + + for tx in transactions { + let tx_hash = tx.tx_hash.clone(); + let internal_id = format!("{:02x}", tx.internal_id); + let confirmation_status = ConfirmationStatus::from_block_height(tx.block_height); + let token_id = token_id_from_tx_type(&tx.transaction_type); + let tx_json = json::to_string(&tx).expect("serialization should not fail"); + + let tx_hex = format!("{:02x}", tx.tx_hex); + let tx_cache_params = [&tx_hash, &tx_hex]; + + sql_transaction.execute(&insert_tx_in_cache_sql(&wallet_id)?, tx_cache_params)?; + + let params = [ + tx_hash, + internal_id.clone(), + tx.block_height.to_string(), + confirmation_status.to_sql_param(), + token_id, + tx_json, + ]; + sql_transaction.execute(&insert_tx_in_history_sql(&wallet_id)?, ¶ms)?; + + let addresses: FilteringAddresses = tx.from.into_iter().chain(tx.to.into_iter()).collect(); + for address in addresses { + let params = [internal_id.clone(), address]; + sql_transaction.execute(&insert_tx_address_sql(&wallet_id)?, ¶ms)?; + } + } + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn remove_tx_from_history( + &self, + wallet_id: &WalletId, + internal_id: &BytesJson, + ) -> Result> { + let remove_tx_history_sql = remove_tx_by_internal_id_sql(wallet_id, tx_history_table)?; + let remove_tx_addr_sql = remove_tx_by_internal_id_sql(wallet_id, tx_address_table)?; + + let params = [format!("{:02x}", internal_id)]; + let selfi = self.clone(); + + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + + sql_transaction.execute(&remove_tx_addr_sql, ¶ms)?; + + let rows_num = sql_transaction.execute(&remove_tx_history_sql, ¶ms)?; + let remove_tx_result = if rows_num > 0 { + RemoveTxResult::TxRemoved + } else { + RemoveTxResult::TxDidNotExist + }; + + sql_transaction.commit()?; + Ok(remove_tx_result) + }) + .await + } + + async fn get_tx_from_history( + &self, + wallet_id: &WalletId, + internal_id: &BytesJson, + ) -> Result, MmError> { + let params = [format!("{:02x}", internal_id)]; + let sql = select_tx_by_internal_id_sql(wallet_id)?; + let selfi = self.clone(); + + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + query_single_row(&conn, &sql, params, tx_details_from_row).map_to_mm(SqlError::from) + }) + .await + } + + async fn history_contains_unconfirmed_txes(&self, wallet_id: &WalletId) -> Result> { + let sql = contains_unconfirmed_transactions_sql(wallet_id)?; + let selfi = self.clone(); + + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let count_unconfirmed = conn.query_row::(&sql, NO_PARAMS, |row| row.get(0))?; + Ok(count_unconfirmed > 0) + }) + .await + } + + async fn get_unconfirmed_txes_from_history( + &self, + wallet_id: &WalletId, + ) -> Result, MmError> { + let sql = get_unconfirmed_transactions_sql(wallet_id)?; + let selfi = self.clone(); + + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let mut stmt = conn.prepare(&sql)?; + let rows = stmt.query(NO_PARAMS)?; + let result = rows.mapped(tx_details_from_row).collect::>()?; + Ok(result) + }) + .await + } + + async fn update_tx_in_history( + &self, + wallet_id: &WalletId, + tx: &TransactionDetails, + ) -> Result<(), MmError> { + let sql = update_tx_in_table_by_internal_id_sql(wallet_id)?; + + let block_height = tx.block_height.to_string(); + let confirmation_status = ConfirmationStatus::from_block_height(tx.block_height); + let json_details = json::to_string(tx).unwrap(); + let internal_id = format!("{:02x}", tx.internal_id); + + let params = [ + block_height, + confirmation_status.to_sql_param(), + json_details, + internal_id, + ]; + + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + conn.execute(&sql, params).map(|_| ()).map_err(MmError::new) + }) + .await + } + + async fn history_has_tx_hash(&self, wallet_id: &WalletId, tx_hash: &str) -> Result> { + let sql = has_transactions_with_hash_sql(wallet_id)?; + let params = [tx_hash.to_owned()]; + + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let count: u32 = conn.query_row(&sql, params, |row| row.get(0))?; + Ok(count > 0) + }) + .await + } + + async fn unique_tx_hashes_num_in_history(&self, wallet_id: &WalletId) -> Result> { + let sql = unique_tx_hashes_num_sql(wallet_id)?; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let count: u32 = conn.query_row(&sql, NO_PARAMS, |row| row.get(0))?; + Ok(count as usize) + }) + .await + } + + async fn add_tx_to_cache( + &self, + wallet_id: &WalletId, + tx_hash: &str, + tx_hex: &BytesJson, + ) -> Result<(), MmError> { + let sql = insert_tx_in_cache_sql(wallet_id)?; + let params = [tx_hash.to_owned(), format!("{:02x}", tx_hex)]; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + conn.execute(&sql, params)?; + Ok(()) + }) + .await + } + + async fn tx_bytes_from_cache( + &self, + wallet_id: &WalletId, + tx_hash: &str, + ) -> Result, MmError> { + let sql = get_tx_hex_from_cache_sql(wallet_id)?; + let params = [tx_hash.to_owned()]; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let maybe_tx_hex: Result = conn.query_row(&sql, params, |row| row.get(0)); + if let Err(SqlError::QueryReturnedNoRows) = maybe_tx_hex { + return Ok(None); + } + let tx_hex = maybe_tx_hex?; + let tx_bytes = + hex::decode(&tx_hex).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e)))?; + Ok(Some(tx_bytes.into())) + }) + .await + } + + async fn get_history( + &self, + wallet_id: &WalletId, + filters: GetTxHistoryFilters, + paging: PagingOptionsEnum, + limit: usize, + ) -> Result> { + // Check if [`GetTxHistoryFilters::for_addresses`] is specified and empty. + // If it is, it's much more efficient to return an empty result before we do any query. + if matches!(filters.for_addresses, Some(ref for_addresses) if for_addresses.is_empty()) { + return Ok(GetHistoryResult { + transactions: Vec::new(), + skipped: 0, + total: 0, + }); + } + + let wallet_id = wallet_id.clone(); + let selfi = self.clone(); + + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let token_id = filters.token_id_or_exclude(); + let mut sql_builder = get_history_builder_preimage(&conn, &wallet_id, token_id, filters.for_addresses)?; + + let total_count_builder = finalize_get_total_count_sql_builder(sql_builder.clone())?; + let total: isize = total_count_builder + .query_single_row(|row| row.get(0))? + .or_mm_err(|| SqlError::QueryReturnedNoRows)?; + let total = total.try_into().expect("count should be always above zero"); + + let offset = match paging { + PagingOptionsEnum::PageNumber(page) => (page.get() - 1) * limit, + PagingOptionsEnum::FromId(from_internal_id) => { + let maybe_offset = sql_builder + .clone() + .query_offset_by_id("tx_history.internal_id", format!("{:02x}", from_internal_id))?; + match maybe_offset { + Some(offset) => offset, + None => { + // TODO do we need to return `SqlError::QueryReturnedNoRows` error instead? + return Ok(GetHistoryResult { + transactions: vec![], + skipped: 0, + total, + }); + }, + } + }, + }; + + finalize_get_history_sql_builder(&mut sql_builder, offset, limit)?; + let transactions = sql_builder.query(tx_details_from_row)?; + + let result = GetHistoryResult { + transactions, + skipped: offset, + total, + }; + Ok(result) + }) + .await + } +} diff --git a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs new file mode 100644 index 0000000000..8adefb6a74 --- /dev/null +++ b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs @@ -0,0 +1,646 @@ +use crate::my_tx_history_v2::{GetHistoryResult, TxHistoryStorage}; +use crate::tx_history_storage::{GetTxHistoryFilters, TxHistoryStorageBuilder, WalletId}; +use crate::{BytesJson, TransactionDetails}; +use common::PagingOptionsEnum; +use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; +use serde_json as json; +use std::collections::HashMap; +use std::num::NonZeroUsize; + +const BCH_TX_HISTORY_STR: &str = include_str!("../for_tests/tBCH_tx_history_fixtures.json"); + +lazy_static! { + static ref BCH_TX_HISTORY: Vec = parse_tx_history(); + static ref BCH_TX_HISTORY_MAP: HashMap = parse_tx_history_map(); +} + +fn parse_tx_history() -> Vec { json::from_str(BCH_TX_HISTORY_STR).unwrap() } + +fn parse_tx_history_map() -> HashMap { + parse_tx_history() + .into_iter() + .map(|tx| (format!("{:02x}", tx.internal_id), tx)) + .collect() +} + +fn get_bch_tx_details(internal_id: &str) -> TransactionDetails { BCH_TX_HISTORY_MAP.get(internal_id).unwrap().clone() } + +fn wallet_id_for_test(test_name: &str) -> WalletId { WalletId::new(test_name.to_owned()) } + +#[track_caller] +fn assert_get_history_result(actual: GetHistoryResult, expected_ids: Vec, skipped: usize, total: usize) { + let actual_ids: Vec<_> = actual.transactions.into_iter().map(|tx| tx.internal_id).collect(); + assert_eq!(actual_ids, expected_ids); + assert_eq!(actual.skipped, skipped, "!skipped"); + assert_eq!(actual.total, total, "!total"); +} + +async fn get_coin_history( + storage: &Storage, + wallet_id: &WalletId, +) -> Vec { + let filters = GetTxHistoryFilters::new(); + let paging_options = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); + let limit = u32::MAX as usize; + storage + .get_history(wallet_id, filters, paging_options, limit) + .await + .unwrap() + .transactions +} + +async fn test_add_transactions_impl() { + let wallet_id = wallet_id_for_test("TEST_ADD_TRANSACTIONS"); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id).await.unwrap(); + + let tx1 = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); + let transactions = [tx1.clone(), tx1.clone()]; + + // must fail because we are adding transactions with the same internal_id + storage + .add_transactions_to_history(&wallet_id, transactions) + .await + .unwrap_err(); + let actual_txs = get_coin_history(&storage, &wallet_id).await; + assert!(actual_txs.is_empty()); + + let tx2 = get_bch_tx_details("c07836722bbdfa2404d8fe0ea56700d02e2012cb9dc100ccaf1138f334a759ce"); + let transactions = vec![tx1, tx2]; + storage + .add_transactions_to_history(&wallet_id, transactions.clone()) + .await + .unwrap(); + let actual_txs = get_coin_history(&storage, &wallet_id).await; + assert_eq!(actual_txs, transactions); +} + +async fn test_remove_transaction_impl() { + let wallet_id = wallet_id_for_test("TEST_REMOVE_TRANSACTION"); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id).await.unwrap(); + let tx_details = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); + storage + .add_transactions_to_history(&wallet_id, [tx_details]) + .await + .unwrap(); + + let remove_res = storage + .remove_tx_from_history( + &wallet_id, + &"6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into(), + ) + .await + .unwrap(); + assert!(remove_res.tx_existed()); + + let remove_res = storage + .remove_tx_from_history( + &wallet_id, + &"6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into(), + ) + .await + .unwrap(); + assert!(!remove_res.tx_existed()); +} + +async fn test_get_transaction_impl() { + let wallet_id = wallet_id_for_test("TEST_GET_TRANSACTION"); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id).await.unwrap(); + + let tx_details = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); + storage + .add_transactions_to_history(&wallet_id, [tx_details]) + .await + .unwrap(); + + let tx = storage + .get_tx_from_history( + &wallet_id, + &"6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into(), + ) + .await + .unwrap() + .unwrap(); + println!("{:?}", tx); + + storage + .remove_tx_from_history( + &wallet_id, + &"6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into(), + ) + .await + .unwrap(); + + let tx = storage + .get_tx_from_history( + &wallet_id, + &"6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into(), + ) + .await + .unwrap(); + assert!(tx.is_none()); +} + +async fn test_update_transaction_impl() { + let wallet_id = wallet_id_for_test("TEST_UPDATE_TRANSACTION"); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id).await.unwrap(); + + let mut tx_details = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); + storage + .add_transactions_to_history(&wallet_id, [tx_details.clone()]) + .await + .unwrap(); + + tx_details.block_height = 12345; + + storage.update_tx_in_history(&wallet_id, &tx_details).await.unwrap(); + + let updated = storage + .get_tx_from_history(&wallet_id, &tx_details.internal_id) + .await + .unwrap() + .unwrap(); + + assert_eq!(12345, updated.block_height); +} + +async fn test_contains_and_get_unconfirmed_transaction_impl() { + let wallet_id = wallet_id_for_test("TEST_CONTAINS_AND_GET_UNCONFIRMED_TRANSACTION"); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id).await.unwrap(); + + let mut tx_details = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); + tx_details.block_height = 0; + storage + .add_transactions_to_history(&wallet_id, [tx_details.clone()]) + .await + .unwrap(); + + let contains_unconfirmed = storage.history_contains_unconfirmed_txes(&wallet_id).await.unwrap(); + assert!(contains_unconfirmed); + + let unconfirmed_transactions = storage.get_unconfirmed_txes_from_history(&wallet_id).await.unwrap(); + assert_eq!(unconfirmed_transactions.len(), 1); + + tx_details.block_height = 12345; + storage.update_tx_in_history(&wallet_id, &tx_details).await.unwrap(); + + let contains_unconfirmed = storage.history_contains_unconfirmed_txes(&wallet_id).await.unwrap(); + assert!(!contains_unconfirmed); + + let unconfirmed_transactions = storage.get_unconfirmed_txes_from_history(&wallet_id).await.unwrap(); + assert!(unconfirmed_transactions.is_empty()); +} + +async fn test_has_transactions_with_hash_impl() { + let wallet_id = wallet_id_for_test("TEST_HAS_TRANSACTIONS_WITH_HASH"); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id).await.unwrap(); + + let has_tx_hash = storage + .history_has_tx_hash( + &wallet_id, + "6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69", + ) + .await + .unwrap(); + assert!(!has_tx_hash); + + let tx_details = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); + storage + .add_transactions_to_history(&wallet_id, [tx_details]) + .await + .unwrap(); + + let has_tx_hash = storage + .history_has_tx_hash( + &wallet_id, + "6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69", + ) + .await + .unwrap(); + assert!(has_tx_hash); +} + +async fn test_unique_tx_hashes_num_impl() { + let wallet_id = wallet_id_for_test("TEST_UNIQUE_TX_HASHES_NUM"); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id).await.unwrap(); + + let tx1 = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); + + let mut tx2 = tx1.clone(); + tx2.internal_id = BytesJson(vec![1; 32]); + + let tx3 = get_bch_tx_details("c07836722bbdfa2404d8fe0ea56700d02e2012cb9dc100ccaf1138f334a759ce"); + + let transactions = [tx1, tx2, tx3]; + storage + .add_transactions_to_history(&wallet_id, transactions) + .await + .unwrap(); + + let tx_hashes_num = storage.unique_tx_hashes_num_in_history(&wallet_id).await.unwrap(); + assert_eq!(2, tx_hashes_num); +} + +async fn test_add_and_get_tx_from_cache_impl() { + let wallet_id_1 = WalletId::new("TEST_ADD_AND_GET_TX_FROM_CACHE".to_owned()); + // `wallet_id_2` has the same `ticker` and a non-empty `hd_wallet_rmd160`. + let wallet_id_2 = WalletId::new("TEST_ADD_AND_GET_TX_FROM_CACHE".to_owned()) + .with_hd_wallet_rmd160("108f07b8382412612c048d07d13f814118445acd".into()); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id_1).await.unwrap(); + storage.init(&wallet_id_2).await.unwrap(); + + let tx = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); + + storage + .add_tx_to_cache(&wallet_id_1, &tx.tx_hash, &tx.tx_hex) + .await + .unwrap(); + + let tx_hex_from_1 = storage + .tx_bytes_from_cache(&wallet_id_1, &tx.tx_hash) + .await + .unwrap() + .unwrap(); + assert_eq!(tx_hex_from_1, tx.tx_hex); + + // Since `wallet_id_1` and `wallet_id_2` wallets have the same `ticker`, the wallets must have one transaction cache. + let tx_hex_from_2 = storage + .tx_bytes_from_cache(&wallet_id_2, &tx.tx_hash) + .await + .unwrap() + .unwrap(); + assert_eq!(tx_hex_from_2, tx.tx_hex); +} + +async fn test_get_raw_tx_bytes_on_add_transactions_impl() { + let wallet_id = wallet_id_for_test("TEST_GET_RAW_TX_BYTES_ON_ADD_TRANSACTIONS"); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id).await.unwrap(); + + let tx_hash = "6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"; + + let maybe_tx_hex = storage.tx_bytes_from_cache(&wallet_id, &tx_hash).await.unwrap(); + assert!(maybe_tx_hex.is_none()); + + let tx1 = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); + + let mut tx2 = tx1.clone(); + tx2.internal_id = BytesJson(vec![1; 32]); + + let expected_tx_hex = tx1.tx_hex.clone(); + + let transactions = [tx1, tx2]; + storage + .add_transactions_to_history(&wallet_id, transactions) + .await + .unwrap(); + + let tx_hex = storage + .tx_bytes_from_cache(&wallet_id, &tx_hash) + .await + .unwrap() + .unwrap(); + + assert_eq!(tx_hex, expected_tx_hex); +} + +async fn test_get_history_page_number_impl() { + let wallet_id = wallet_id_for_test("TEST_GET_HISTORY_PAGE_NUMBER"); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id).await.unwrap(); + + storage + .add_transactions_to_history(&wallet_id, BCH_TX_HISTORY.clone()) + .await + .unwrap(); + + let filters = GetTxHistoryFilters::new(); + let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); + let limit = 4; + + let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); + + let expected_internal_ids: Vec = vec![ + "6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into(), + "c07836722bbdfa2404d8fe0ea56700d02e2012cb9dc100ccaf1138f334a759ce".into(), + "091877294268b2b1734255067146f15c3ac5e6199e72cd4f68a8d9dec32bb0c0".into(), + "d76723c092b64bc598d5d2ceafd6f0db37dce4032db569d6f26afb35491789a7".into(), + ]; + assert_get_history_result(result, expected_internal_ids, 0, 123); + + let filters = GetTxHistoryFilters::new() + .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); + let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(2).unwrap()); + let limit = 5; + + let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); + + let expected_internal_ids: Vec = vec![ + "433b641bc89e1b59c22717918583c60ec98421805c8e85b064691705d9aeb970".into(), + "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989".into(), + "1c1e68357cf5a6dacb53881f13aa5d2048fe0d0fab24b76c9ec48f53884bed97".into(), + "c4304b5ef4f1b88ed4939534a8ca9eca79f592939233174ae08002e8454e3f06".into(), + "b0035434a1e7be5af2ed991ee2a21a90b271c5852a684a0b7d315c5a770d1b1c".into(), + ]; + assert_get_history_result(result, expected_internal_ids, 5, 121); +} + +async fn test_get_history_from_id_impl() { + let wallet_id = wallet_id_for_test("TEST_GET_HISTORY_FROM_ID"); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id).await.unwrap(); + + storage + .add_transactions_to_history(&wallet_id, BCH_TX_HISTORY.clone()) + .await + .unwrap(); + + let filters = GetTxHistoryFilters::new(); + let paging = PagingOptionsEnum::FromId("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into()); + let limit = 3; + + let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); + + let expected_internal_ids: Vec = vec![ + "c07836722bbdfa2404d8fe0ea56700d02e2012cb9dc100ccaf1138f334a759ce".into(), + "091877294268b2b1734255067146f15c3ac5e6199e72cd4f68a8d9dec32bb0c0".into(), + "d76723c092b64bc598d5d2ceafd6f0db37dce4032db569d6f26afb35491789a7".into(), + ]; + assert_get_history_result(result, expected_internal_ids, 1, 123); + + let filters = GetTxHistoryFilters::new() + .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); + let paging = PagingOptionsEnum::FromId("433b641bc89e1b59c22717918583c60ec98421805c8e85b064691705d9aeb970".into()); + let limit = 4; + + let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); + + let expected_internal_ids: Vec = vec![ + "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989".into(), + "1c1e68357cf5a6dacb53881f13aa5d2048fe0d0fab24b76c9ec48f53884bed97".into(), + "c4304b5ef4f1b88ed4939534a8ca9eca79f592939233174ae08002e8454e3f06".into(), + "b0035434a1e7be5af2ed991ee2a21a90b271c5852a684a0b7d315c5a770d1b1c".into(), + ]; + assert_get_history_result(result, expected_internal_ids, 6, 121); +} + +async fn test_get_history_for_addresses_impl() { + let wallet_id = wallet_id_for_test("TEST_GET_HISTORY_FROM_ID"); + + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + + storage.init(&wallet_id).await.unwrap(); + + storage + .add_transactions_to_history(&wallet_id, BCH_TX_HISTORY.clone()) + .await + .unwrap(); + + let for_addresses = vec![ + "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), + "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), + ]; + let filters = GetTxHistoryFilters::new() + .with_for_addresses(for_addresses) + .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); + let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); + let limit = 5; + + let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); + let expected_ids = vec![ + "660d57aad6e7807ee99459a77ed6b526771db8567fff99ca055d652913555d08".into(), + "e46fa0836be0534f7799b2ef5b538551ea25b6f430b7e015a95731efb7a0cd4f".into(), + "fc666307cafcbf29e4b95ccc261a24603c8168535283c6ed8243d4cd8c2543c8".into(), + "fe78e04399219ef75271019f6d5db5d77179e9f310f8364604a6e4e05c4d7563".into(), + ]; + assert_get_history_result(result, expected_ids, 0, 4); + + // Try to request by the specified internal ID. + + let for_addresses = vec![ + "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), + "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), + ]; + let filters = GetTxHistoryFilters::new() + .with_for_addresses(for_addresses) + .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); + let paging = PagingOptionsEnum::FromId("e46fa0836be0534f7799b2ef5b538551ea25b6f430b7e015a95731efb7a0cd4f".into()); + let limit = 4; + + let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); + let expected_ids = vec![ + "fc666307cafcbf29e4b95ccc261a24603c8168535283c6ed8243d4cd8c2543c8".into(), + "fe78e04399219ef75271019f6d5db5d77179e9f310f8364604a6e4e05c4d7563".into(), + ]; + assert_get_history_result(result, expected_ids, 2, 4); + + // If there are no transactions by the specified filters and paging options, + // we need to get an empty history. + + let for_addresses = vec![ + "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), + "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), + ]; + let filters = GetTxHistoryFilters::new() + .with_for_addresses(for_addresses) + .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); + let paging = PagingOptionsEnum::FromId("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into()); + let limit = 2; + + let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); + assert_get_history_result(result, Vec::new(), 0, 4); + + // If there are no transactions by the specified filters and paging option, + // we need to get an empty history. + + let for_addresses = vec![ + "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), + "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), + ]; + let filters = GetTxHistoryFilters::new() + .with_for_addresses(for_addresses) + .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); + let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(2).unwrap()); + let limit = 4; + + let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); + assert_get_history_result(result, Vec::new(), 4, 4); +} + +#[cfg(test)] +mod native_tests { + use super::wallet_id_for_test; + use crate::my_tx_history_v2::TxHistoryStorage; + use crate::tx_history_storage::sql_tx_history_storage_v2::SqliteTxHistoryStorage; + use common::block_on; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + + #[test] + fn test_init_collection() { + let wallet_id = wallet_id_for_test("TEST_INIT_COLLECTION"); + + let ctx = mm_ctx_with_custom_db(); + let storage = SqliteTxHistoryStorage::new(&ctx).unwrap(); + + let initialized = block_on(storage.is_initialized_for(&wallet_id)).unwrap(); + assert!(!initialized); + + block_on(storage.init(&wallet_id)).unwrap(); + // repetitive init must not fail + block_on(storage.init(&wallet_id)).unwrap(); + + let initialized = block_on(storage.is_initialized_for(&wallet_id)).unwrap(); + assert!(initialized); + } + + #[test] + fn test_add_transactions() { block_on(super::test_add_transactions_impl()); } + + #[test] + fn test_remove_transaction() { block_on(super::test_remove_transaction_impl()); } + + #[test] + fn test_get_transaction() { block_on(super::test_get_transaction_impl()); } + + #[test] + fn test_update_transaction() { block_on(super::test_update_transaction_impl()); } + + #[test] + fn test_contains_and_get_unconfirmed_transaction() { + block_on(super::test_contains_and_get_unconfirmed_transaction_impl()); + } + + #[test] + fn test_has_transactions_with_hash() { block_on(super::test_has_transactions_with_hash_impl()); } + + #[test] + fn test_unique_tx_hashes_num() { block_on(super::test_unique_tx_hashes_num_impl()); } + + #[test] + fn test_add_and_get_tx_from_cache() { block_on(super::test_add_and_get_tx_from_cache_impl()); } + + #[test] + fn test_get_raw_tx_bytes_on_add_transactions() { + block_on(super::test_get_raw_tx_bytes_on_add_transactions_impl()); + } + + #[test] + fn test_get_history_page_number() { block_on(super::test_get_history_page_number_impl()); } + + #[test] + fn test_get_history_from_id() { block_on(super::test_get_history_from_id_impl()); } + + #[test] + fn test_get_history_for_addresses() { block_on(super::test_get_history_for_addresses_impl()); } +} + +#[cfg(target_arch = "wasm32")] +mod wasm_tests { + use super::wallet_id_for_test; + use crate::my_tx_history_v2::TxHistoryStorage; + use crate::tx_history_storage::wasm::tx_history_storage_v2::IndexedDbTxHistoryStorage; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test] + async fn test_init_collection() { + let wallet_id = wallet_id_for_test("TEST_INIT_COLLECTION"); + + let ctx = mm_ctx_with_custom_db(); + let storage = IndexedDbTxHistoryStorage::new(&ctx).unwrap(); + + // Please note this is the `IndexedDbTxHistoryStorage` specific: + // [`IndexedDbTxHistoryStorage::is_initialized_for`] always returns `true`. + let initialized = storage.is_initialized_for(&wallet_id).await.unwrap(); + assert!(initialized); + + // repetitive init must not fail + storage.init(&wallet_id).await.unwrap(); + + let initialized = storage.is_initialized_for(&wallet_id).await.unwrap(); + assert!(initialized); + } + + #[wasm_bindgen_test] + async fn test_add_transactions() { super::test_add_transactions_impl().await; } + + #[wasm_bindgen_test] + async fn test_remove_transaction() { super::test_remove_transaction_impl().await; } + + #[wasm_bindgen_test] + async fn test_get_transaction() { super::test_get_transaction_impl().await; } + + #[wasm_bindgen_test] + async fn test_update_transaction() { super::test_update_transaction_impl().await; } + + #[wasm_bindgen_test] + async fn test_contains_and_get_unconfirmed_transaction() { + super::test_contains_and_get_unconfirmed_transaction_impl().await; + } + + #[wasm_bindgen_test] + async fn test_has_transactions_with_hash() { super::test_has_transactions_with_hash_impl().await; } + + #[wasm_bindgen_test] + async fn test_unique_tx_hashes_num() { super::test_unique_tx_hashes_num_impl().await; } + + #[wasm_bindgen_test] + async fn test_add_and_get_tx_from_cache() { super::test_add_and_get_tx_from_cache_impl().await; } + + #[wasm_bindgen_test] + async fn test_get_raw_tx_bytes_on_add_transactions() { + super::test_get_raw_tx_bytes_on_add_transactions_impl().await; + } + + #[wasm_bindgen_test] + async fn test_get_history_page_number() { super::test_get_history_page_number_impl().await; } + + #[wasm_bindgen_test] + async fn test_get_history_from_id() { super::test_get_history_from_id_impl().await; } + + #[wasm_bindgen_test] + async fn test_get_history_for_addresses() { super::test_get_history_for_addresses_impl().await; } +} diff --git a/mm2src/coins/tx_history_storage/wasm/mod.rs b/mm2src/coins/tx_history_storage/wasm/mod.rs new file mode 100644 index 0000000000..a3ba6cb193 --- /dev/null +++ b/mm2src/coins/tx_history_storage/wasm/mod.rs @@ -0,0 +1,54 @@ +use crate::my_tx_history_v2::TxHistoryStorageError; +use mm2_db::indexed_db::{DbTransactionError, InitDbError}; +use mm2_err_handle::prelude::*; + +pub mod tx_history_db; +pub mod tx_history_storage_v1; +pub mod tx_history_storage_v2; + +pub use tx_history_db::{TxHistoryDb, TxHistoryDbLocked}; +pub use tx_history_storage_v1::{clear_tx_history, load_tx_history, save_tx_history}; +pub use tx_history_storage_v2::IndexedDbTxHistoryStorage; + +pub type WasmTxHistoryResult = MmResult; +pub type WasmTxHistoryError = crate::TxHistoryError; + +impl TxHistoryStorageError for WasmTxHistoryError {} + +impl From for WasmTxHistoryError { + fn from(e: InitDbError) -> Self { + match &e { + InitDbError::NotSupported(_) => WasmTxHistoryError::NotSupported(e.to_string()), + InitDbError::EmptyTableList + | InitDbError::DbIsOpenAlready { .. } + | InitDbError::InvalidVersion(_) + | InitDbError::OpeningError(_) + | InitDbError::TypeMismatch { .. } + | InitDbError::UnexpectedState(_) + | InitDbError::UpgradingError { .. } => WasmTxHistoryError::InternalError(e.to_string()), + } + } +} + +impl From for WasmTxHistoryError { + fn from(e: DbTransactionError) -> Self { + match e { + DbTransactionError::ErrorSerializingItem(_) => WasmTxHistoryError::ErrorSerializing(e.to_string()), + DbTransactionError::ErrorDeserializingItem(_) => WasmTxHistoryError::ErrorDeserializing(e.to_string()), + DbTransactionError::ErrorUploadingItem(_) => WasmTxHistoryError::ErrorSaving(e.to_string()), + DbTransactionError::ErrorGettingItems(_) | DbTransactionError::ErrorCountingItems(_) => { + WasmTxHistoryError::ErrorLoading(e.to_string()) + }, + DbTransactionError::ErrorDeletingItems(_) => WasmTxHistoryError::ErrorClearing(e.to_string()), + DbTransactionError::NoSuchTable { .. } + | DbTransactionError::ErrorCreatingTransaction(_) + | DbTransactionError::ErrorOpeningTable { .. } + | DbTransactionError::ErrorSerializingIndex { .. } + | DbTransactionError::UnexpectedState(_) + | DbTransactionError::TransactionAborted + | DbTransactionError::MultipleItemsByUniqueIndex { .. } + | DbTransactionError::NoSuchIndex { .. } + | DbTransactionError::InvalidIndex { .. } => WasmTxHistoryError::InternalError(e.to_string()), + } + } +} diff --git a/mm2src/coins/tx_history_storage/wasm/tx_history_db.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_db.rs new file mode 100644 index 0000000000..c88fd3defc --- /dev/null +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_db.rs @@ -0,0 +1,33 @@ +use crate::tx_history_storage::wasm::tx_history_storage_v1::TxHistoryTableV1; +use crate::tx_history_storage::wasm::tx_history_storage_v2::{TxCacheTableV2, TxHistoryTableV2}; +use async_trait::async_trait; +use mm2_db::indexed_db::{DbIdentifier, DbInstance, DbLocked, IndexedDb, IndexedDbBuilder, InitDbResult}; + +const DB_NAME: &str = "tx_history"; +const DB_VERSION: u32 = 1; + +pub type TxHistoryDbLocked<'a> = DbLocked<'a, TxHistoryDb>; + +pub struct TxHistoryDb { + inner: IndexedDb, +} + +#[async_trait] +impl DbInstance for TxHistoryDb { + fn db_name() -> &'static str { DB_NAME } + + async fn init(db_id: DbIdentifier) -> InitDbResult { + let inner = IndexedDbBuilder::new(db_id) + .with_version(DB_VERSION) + .with_table::() + .with_table::() + .with_table::() + .build() + .await?; + Ok(TxHistoryDb { inner }) + } +} + +impl TxHistoryDb { + pub fn get_inner(&self) -> &IndexedDb { &self.inner } +} diff --git a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs new file mode 100644 index 0000000000..80d573de23 --- /dev/null +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs @@ -0,0 +1,138 @@ +use crate::tx_history_storage::wasm::tx_history_db::TxHistoryDb; +use crate::tx_history_storage::wasm::WasmTxHistoryResult; +use crate::TransactionDetails; +use mm2_db::indexed_db::{DbIdentifier, DbInstance, DbUpgrader, OnUpgradeResult, TableSignature}; + +pub async fn load_tx_history( + db: &TxHistoryDb, + ticker: &str, + wallet_address: &str, +) -> WasmTxHistoryResult> { + let history_id = HistoryId::new(ticker, wallet_address); + + let transaction = db.get_inner().transaction().await?; + let table = transaction.table::().await?; + + let item_opt = table + .get_item_by_unique_index("history_id", history_id.as_str()) + .await?; + match item_opt { + Some((_item_id, TxHistoryTableV1 { txs, .. })) => Ok(txs), + None => Ok(Vec::new()), + } +} + +pub async fn save_tx_history( + db: &TxHistoryDb, + ticker: &str, + wallet_address: &str, + txs: Vec, +) -> WasmTxHistoryResult<()> { + let history_id = HistoryId::new(ticker, wallet_address); + let history_id_value = history_id.to_string(); + let tx_history_item = TxHistoryTableV1 { history_id, txs }; + + let transaction = db.get_inner().transaction().await?; + let table = transaction.table::().await?; + + table + .replace_item_by_unique_index("history_id", &history_id_value, &tx_history_item) + .await?; + Ok(()) +} + +pub async fn clear_tx_history(db: &TxHistoryDb, ticker: &str, wallet_address: &str) -> WasmTxHistoryResult<()> { + let history_id = HistoryId::new(ticker, wallet_address); + + let transaction = db.get_inner().transaction().await?; + let table = transaction.table::().await?; + + table + .delete_item_by_unique_index("history_id", history_id.as_str()) + .await?; + Ok(()) +} + +#[derive(Debug, Deserialize, Serialize)] +struct HistoryId(String); + +impl HistoryId { + fn new(ticker: &str, wallet_address: &str) -> HistoryId { HistoryId(format!("{}_{}", ticker, wallet_address)) } + + fn as_str(&self) -> &str { &self.0 } + + fn to_string(&self) -> String { self.0.clone() } +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct TxHistoryTableV1 { + history_id: HistoryId, + txs: Vec, +} + +impl TableSignature for TxHistoryTableV1 { + fn table_name() -> &'static str { "tx_history" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + match (old_version, new_version) { + (0, 1) => { + let table = upgrader.create_table(Self::table_name())?; + table.create_index("history_id", true)?; + }, + _ => (), + } + Ok(()) + } +} + +mod tests { + use super::*; + use serde_json as json; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test] + async fn test_tx_history() { + const DB_NAME: &'static str = "TEST_TX_HISTORY"; + let db = TxHistoryDb::init(DbIdentifier::for_test(DB_NAME)) + .await + .expect("!TxHistoryDb::init_with_fs_path"); + + let history = load_tx_history(&db, "RICK", "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD") + .await + .expect("!TxHistoryDb::load_history"); + assert!(history.is_empty()); + + let history_str = r#"[{"tx_hex":"0400008085202f89018ec8f6f02e008ebd57bbf94c0d8297c1825f3af204490c43f5652b002a2c8b17010000006b483045022100e625a8b77beac5ec891e7d44b18fc4d780ef8456847eb2c6fa2f765e3379a9c102200f323612189fa44ee16f5deb39809b71c4811545b36ee4d6cc622d01aab10ef3012102043663e9c5af8275771809b3889d437f559e49e8df79b6ba19ade4cc5d8eb3e0ffffffff03809698000000000017a9142ffaf6694c6b441790546eefd277e430e08e47a6870000000000000000166a14094ab490fffa9939544545a656b345bf21920a90f6b35714000000001976a9145ed376ce9faa63cb2fef5862e1a5cc811c17316588acf9d29260000000000000000000000000000000","tx_hash":"f05d786bd4b647a5720094bf0a2c6f23b5e131c451d750a96102898f7b5458e8","from":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3"],"to":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3","bH6y6RtvbLToqSUNtLA5rQRjSwyNNzUSNc"],"total_amount":"3.51293022","spent_by_me":"3.51293022","received_by_me":"3.41292022","my_balance_change":"-0.10001","block_height":916940,"timestamp":1620235027,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"RICK","internal_id":"f05d786bd4b647a5720094bf0a2c6f23b5e131c451d750a96102898f7b5458e8"}]"#; + let history: Vec = json::from_str(history_str).unwrap(); + save_tx_history(&db, "RICK", "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD", history) + .await + .expect("!TxHistoryDb::save_history"); + + let updated_history_str = r#"[{"tx_hex":"0400008085202f89018ec8f6f02e008ebd57bbf94c0d8297c1825f3af204490c43f5652b002a2c8b17010000006b483045022100e625a8b77beac5ec891e7d44b18fc4d780ef8456847eb2c6fa2f765e3379a9c102200f323612189fa44ee16f5deb39809b71c4811545b36ee4d6cc622d01aab10ef3012102043663e9c5af8275771809b3889d437f559e49e8df79b6ba19ade4cc5d8eb3e0ffffffff03809698000000000017a9142ffaf6694c6b441790546eefd277e430e08e47a6870000000000000000166a14094ab490fffa9939544545a656b345bf21920a90f6b35714000000001976a9145ed376ce9faa63cb2fef5862e1a5cc811c17316588acf9d29260000000000000000000000000000000","tx_hash":"f05d786bd4b647a5720094bf0a2c6f23b5e131c451d750a96102898f7b5458e8","from":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3"],"to":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3","bH6y6RtvbLToqSUNtLA5rQRjSwyNNzUSNc"],"total_amount":"3.51293022","spent_by_me":"3.51293022","received_by_me":"3.41292022","my_balance_change":"-0.10001","block_height":916940,"timestamp":1620235027,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"RICK","internal_id":"f05d786bd4b647a5720094bf0a2c6f23b5e131c451d750a96102898f7b5458e8"},{"tx_hex":"0400008085202f8901a5620f30001e5e31bcbffacee7687fd84490fa1f8625ddd1e098f0bc530d673e020000006b483045022100a9864707855307681b81d94ae17328f6feccb2a9439d27378dfeae2df0220cc102207476f8304af14794a9cd2fe6287e07a1c802c05ec134b789ddb22ab51f7d2238012102043663e9c5af8275771809b3889d437f559e49e8df79b6ba19ade4cc5d8eb3e0ffffffff0246320000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ac5e4ef014000000001976a9145ed376ce9faa63cb2fef5862e1a5cc811c17316588acacd29260000000000000000000000000000000","tx_hash":"178b2c2a002b65f5430c4904f23a5f82c197820d4cf9bb57bd8e002ef0f6c88e","from":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3"],"to":["RHvavL8j683JwrN2ygk9Bg495DvPu5QVN3","RThtXup6Zo7LZAi8kRWgjAyi1s4u6U9Cpf"],"total_amount":"3.51306892","spent_by_me":"3.51306892","received_by_me":"3.51293022","my_balance_change":"-0.0001387","block_height":916939,"timestamp":1620234997,"fee_details":{"type":"Utxo","amount":"0.00001"},"coin":"RICK","internal_id":"178b2c2a002b65f5430c4904f23a5f82c197820d4cf9bb57bd8e002ef0f6c88e"}]"#; + let updated_history: Vec = json::from_str(updated_history_str).unwrap(); + save_tx_history( + &db, + "RICK", + "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD", + updated_history.clone(), + ) + .await + .expect("!TxHistoryDb::save_history"); + + let actual_history = load_tx_history(&db, "RICK", "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD") + .await + .expect("!TxHistoryDb::load_history"); + assert_eq!(actual_history, updated_history); + + clear_tx_history(&db, "RICK", "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD") + .await + .expect("!TxHistoryDb::clear"); + + let history = load_tx_history(&db, "RICK", "RRnMcSeKiLrNdbp91qNVQwwXx5azD4S4CD") + .await + .expect("!TxHistoryDb::load_history"); + assert!(history.is_empty()); + } +} diff --git a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs new file mode 100644 index 0000000000..a07bdaf6fc --- /dev/null +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs @@ -0,0 +1,484 @@ +use crate::my_tx_history_v2::{GetHistoryResult, RemoveTxResult, TxHistoryStorage}; +use crate::tx_history_storage::wasm::tx_history_db::{TxHistoryDb, TxHistoryDbLocked}; +use crate::tx_history_storage::wasm::{WasmTxHistoryError, WasmTxHistoryResult}; +use crate::tx_history_storage::{token_id_from_tx_type, ConfirmationStatus, CreateTxHistoryStorageError, + FilteringAddresses, GetTxHistoryFilters, WalletId}; +use crate::{CoinsContext, TransactionDetails}; +use async_trait::async_trait; +use common::PagingOptionsEnum; +use itertools::Itertools; +use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::{BeBigUint, DbUpgrader, MultiIndex, OnUpgradeResult, SharedDb, TableSignature}; +use mm2_err_handle::prelude::*; +use rpc::v1::types::Bytes as BytesJson; +use serde_json::{self as json, Value as Json}; + +impl WalletId { + /// If [`WalletId::hd_wallet_rmd160`] is not specified, + /// we need to exclude transactions of each HD wallet by specifying an empty `hd_wallet_rmd160`. + fn hd_wallet_rmd160_or_exclude(&self) -> String { + self.hd_wallet_rmd160.map(|hash| hash.to_string()).unwrap_or_default() + } +} + +#[derive(Clone)] +pub struct IndexedDbTxHistoryStorage { + db: SharedDb, +} + +impl IndexedDbTxHistoryStorage { + pub fn new(ctx: &MmArc) -> MmResult + where + Self: Sized, + { + let coins_ctx = CoinsContext::from_ctx(ctx).map_to_mm(CreateTxHistoryStorageError::Internal)?; + Ok(IndexedDbTxHistoryStorage { + db: coins_ctx.tx_history_db.clone(), + }) + } +} + +#[async_trait] +impl TxHistoryStorage for IndexedDbTxHistoryStorage { + type Error = WasmTxHistoryError; + + async fn init(&self, _wallet_id: &WalletId) -> MmResult<(), Self::Error> { Ok(()) } + + async fn is_initialized_for(&self, _wallet_id: &WalletId) -> MmResult { Ok(true) } + + /// Adds multiple transactions to the selected coin's history + /// Also consider adding tx_hex to the cache during this operation + async fn add_transactions_to_history(&self, wallet_id: &WalletId, transactions: I) -> MmResult<(), Self::Error> + where + I: IntoIterator + Send + 'static, + I::IntoIter: Send, + { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let history_table = db_transaction.table::().await?; + let cache_table = db_transaction.table::().await?; + + for tx in transactions { + let history_item = TxHistoryTableV2::from_tx_details(wallet_id.clone(), &tx)?; + history_table.add_item(&history_item).await?; + + let cache_item = TxCacheTableV2::from_tx_details(wallet_id.clone(), &tx); + let index_keys = MultiIndex::new(TxCacheTableV2::COIN_TX_HASH_INDEX) + .with_value(&wallet_id.ticker)? + .with_value(&tx.tx_hash)?; + // `TxHistoryTableV2::tx_hash` is not a unique field, but `TxCacheTableV2::tx_hash` is unique. + // So we use `DbTable::add_item_or_ignore_by_unique_multi_index` instead of `DbTable::add_item` + // since `transactions` may contain txs with same `tx_hash` but different `internal_id`. + cache_table + .add_item_or_ignore_by_unique_multi_index(index_keys, &cache_item) + .await?; + } + Ok(()) + } + + async fn remove_tx_from_history( + &self, + wallet_id: &WalletId, + internal_id: &BytesJson, + ) -> MmResult { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(TxHistoryTableV2::WALLET_ID_INTERNAL_ID_INDEX) + .with_value(&wallet_id.ticker)? + .with_value(wallet_id.hd_wallet_rmd160_or_exclude())? + .with_value(internal_id)?; + + if table.delete_item_by_unique_multi_index(index_keys).await?.is_some() { + Ok(RemoveTxResult::TxRemoved) + } else { + Ok(RemoveTxResult::TxDidNotExist) + } + } + + async fn get_tx_from_history( + &self, + wallet_id: &WalletId, + internal_id: &BytesJson, + ) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(TxHistoryTableV2::WALLET_ID_INTERNAL_ID_INDEX) + .with_value(&wallet_id.ticker)? + .with_value(wallet_id.hd_wallet_rmd160_or_exclude())? + .with_value(internal_id)?; + + let details_json = match table.get_item_by_unique_multi_index(index_keys).await? { + Some((_item_id, item)) => item.details_json, + None => return Ok(None), + }; + json::from_value(details_json).map_to_mm(|e| WasmTxHistoryError::ErrorDeserializing(e.to_string())) + } + + async fn history_contains_unconfirmed_txes(&self, wallet_id: &WalletId) -> Result> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(TxHistoryTableV2::WALLET_ID_CONFIRMATION_STATUS_INDEX) + .with_value(&wallet_id.ticker)? + .with_value(wallet_id.hd_wallet_rmd160_or_exclude())? + .with_value(ConfirmationStatus::Unconfirmed)?; + + let count_unconfirmed = table.count_by_multi_index(index_keys).await?; + Ok(count_unconfirmed > 0) + } + + /// Gets the unconfirmed transactions from the history + async fn get_unconfirmed_txes_from_history( + &self, + wallet_id: &WalletId, + ) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(TxHistoryTableV2::WALLET_ID_CONFIRMATION_STATUS_INDEX) + .with_value(&wallet_id.ticker)? + .with_value(wallet_id.hd_wallet_rmd160_or_exclude())? + .with_value(ConfirmationStatus::Unconfirmed)?; + + table + .get_items_by_multi_index(index_keys) + .await? + .into_iter() + .map(|(_item_id, item)| tx_details_from_item(item)) + // Collect `WasmTxHistoryResult>`. + .collect() + } + + /// Updates transaction in the selected coin's history + async fn update_tx_in_history(&self, wallet_id: &WalletId, tx: &TransactionDetails) -> MmResult<(), Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(TxHistoryTableV2::WALLET_ID_INTERNAL_ID_INDEX) + .with_value(&wallet_id.ticker)? + .with_value(wallet_id.hd_wallet_rmd160_or_exclude())? + .with_value(&tx.internal_id)?; + let item = TxHistoryTableV2::from_tx_details(wallet_id.clone(), tx)?; + table.replace_item_by_unique_multi_index(index_keys, &item).await?; + Ok(()) + } + + async fn history_has_tx_hash(&self, wallet_id: &WalletId, tx_hash: &str) -> Result> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(TxHistoryTableV2::WALLET_ID_TX_HASH_INDEX) + .with_value(&wallet_id.ticker)? + .with_value(wallet_id.hd_wallet_rmd160_or_exclude())? + .with_value(tx_hash)?; + let count_txs = table.count_by_multi_index(index_keys).await?; + Ok(count_txs > 0) + } + + /// TODO consider refactoring this method to return unique internal_id's instead of tx_hash, + /// since the method requests the whole TX history of the specified wallet. + async fn unique_tx_hashes_num_in_history(&self, wallet_id: &WalletId) -> Result> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(TxHistoryTableV2::WALLET_ID_INDEX) + .with_value(&wallet_id.ticker)? + .with_value(wallet_id.hd_wallet_rmd160_or_exclude())?; + + // `IndexedDb` doesn't provide an elegant way to count records applying custom filters to index properties like `tx_hash`, + // so currently fetch all records with `coin,hd_wallet_rmd160=wallet_id` and apply the `unique_by(|tx| tx.tx_hash)` to them. + Ok(table + .get_items_by_multi_index(index_keys) + .await? + .into_iter() + .unique_by(|(_item_id, tx)| tx.tx_hash.clone()) + .count()) + } + + async fn add_tx_to_cache( + &self, + wallet_id: &WalletId, + tx_hash: &str, + tx_hex: &BytesJson, + ) -> Result<(), MmError> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + table + .add_item(&TxCacheTableV2 { + coin: wallet_id.ticker.clone(), + tx_hash: tx_hash.to_owned(), + tx_hex: tx_hex.clone(), + }) + .await?; + Ok(()) + } + + async fn tx_bytes_from_cache( + &self, + wallet_id: &WalletId, + tx_hash: &str, + ) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(TxCacheTableV2::COIN_TX_HASH_INDEX) + .with_value(&wallet_id.ticker)? + .with_value(tx_hash)?; + match table.get_item_by_unique_multi_index(index_keys).await? { + Some((_item_id, item)) => Ok(Some(item.tx_hex)), + None => Ok(None), + } + } + + /// This is totally inefficient due to we query all items from the storage + /// and then checks whether it were sent from/to one of the specified `for_addresses`. + /// + /// TODO One of the possible solutions is to do the following: + /// 1) Add `TxFromAddressTable` and `TxToAddressTable` tables; + /// 2) Add [`CursorBoundValue::BTreeSet`] that iterates items over its index values; + /// 3) Query transaction internal IDs from the `TxFromAddressTable` and `TxToAddressTable` tables + /// by using a cursor with the specified `ticker`, `hd_wallet_rmd160`, `token_id` constant indexes + /// and the iterable [`CursorBoundValue::BTreeMap = for_addresses`] index; + /// 4) Query transaction details from the `TxHistoryTableV2` table by using a cursor with the specified `ticker`, `hd_wallet_rmd160`, `token_id` constant indexes + /// and the iterable [`CursorBoundValue::BTreeMap = expected_internal_ids`]. + async fn get_history( + &self, + wallet_id: &WalletId, + filters: GetTxHistoryFilters, + paging: PagingOptionsEnum, + limit: usize, + ) -> MmResult { + // Check if [`GetTxHistoryFilters::for_addresses`] is specified and empty. + // If it is, it's much more efficient to return an empty result before we do any query. + if matches!(filters.for_addresses, Some(ref for_addresses) if for_addresses.is_empty()) { + return Ok(GetHistoryResult { + transactions: Vec::new(), + skipped: 0, + total: 0, + }); + } + + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(TxHistoryTableV2::WALLET_ID_TOKEN_ID_INDEX) + .with_value(&wallet_id.ticker)? + .with_value(wallet_id.hd_wallet_rmd160_or_exclude())? + .with_value(filters.token_id_or_exclude())?; + + let transactions = table + .get_items_by_multi_index(index_keys) + .await? + .into_iter() + .map(|(_item_id, tx)| tx); + + let transactions = Self::take_according_to_filtering_addresses(transactions, &filters.for_addresses); + Self::take_according_to_paging_opts(transactions, paging, limit) + } +} + +impl IndexedDbTxHistoryStorage { + fn take_according_to_filtering_addresses( + txs: I, + for_addresses: &Option, + ) -> Vec + where + I: Iterator, + { + match for_addresses { + Some(for_addresses) => txs + .filter(|tx| { + tx.from_addresses.has_intersection(for_addresses) || tx.to_addresses.has_intersection(for_addresses) + }) + .collect(), + None => txs.collect(), + } + } + + pub(super) fn take_according_to_paging_opts( + txs: Vec, + paging: PagingOptionsEnum, + limit: usize, + ) -> WasmTxHistoryResult { + let total_count = txs.len(); + + let skip = match paging { + // `page_number` is ignored if from_uuid is set + PagingOptionsEnum::FromId(from_internal_id) => { + let maybe_skip = txs + .iter() + .position(|tx| tx.internal_id == from_internal_id) + .map(|pos| pos + 1); + match maybe_skip { + Some(skip) => skip, + None => { + return Ok(GetHistoryResult { + transactions: Vec::new(), + skipped: 0, + total: total_count, + }) + }, + } + }, + PagingOptionsEnum::PageNumber(page_number) => (page_number.get() - 1) * limit, + }; + + let transactions = txs + .into_iter() + .skip(skip) + .take(limit) + .map(tx_details_from_item) + // Collect `WasmTxHistoryResult` items into `WasmTxHistoryResult>` + .collect::>>()?; + Ok(GetHistoryResult { + transactions, + skipped: skip, + total: total_count, + }) + } + + async fn lock_db(&self) -> WasmTxHistoryResult> { + self.db.get_or_initialize().await.mm_err(WasmTxHistoryError::from) + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct TxHistoryTableV2 { + coin: String, + hd_wallet_rmd160: String, + tx_hash: String, + internal_id: BytesJson, + block_height: BeBigUint, + confirmation_status: ConfirmationStatus, + token_id: String, + from_addresses: FilteringAddresses, + to_addresses: FilteringAddresses, + details_json: Json, +} + +impl TxHistoryTableV2 { + /// An index that consists of the only one `coin` property. + const WALLET_ID_INDEX: &'static str = "wallet_id"; + /// A **unique** index that consists of the following properties: + /// * coin - coin ticker + /// * internal_id - transaction internal ID + const WALLET_ID_INTERNAL_ID_INDEX: &'static str = "wallet_id_internal_id"; + /// An index that consists of the following properties: + /// * coin - coin ticker + /// * tx_hash - transaction hash + const WALLET_ID_TX_HASH_INDEX: &'static str = "wallet_id_tx_hash"; + /// An index that consists of the following properties: + /// * coin - coin ticker + /// * confirmation_status - whether transaction is confirmed or unconfirmed + const WALLET_ID_CONFIRMATION_STATUS_INDEX: &'static str = "wallet_id_confirmation_status"; + /// An index that consists of the following properties: + /// * coin - coin ticker + /// * token_id - token ID (can be an empty string) + const WALLET_ID_TOKEN_ID_INDEX: &'static str = "wallet_id_token_id"; + + fn from_tx_details(wallet_id: WalletId, tx: &TransactionDetails) -> WasmTxHistoryResult { + let details_json = json::to_value(tx).map_to_mm(|e| WasmTxHistoryError::ErrorSerializing(e.to_string()))?; + let hd_wallet_rmd160 = wallet_id.hd_wallet_rmd160_or_exclude(); + Ok(TxHistoryTableV2 { + coin: wallet_id.ticker, + hd_wallet_rmd160, + tx_hash: tx.tx_hash.clone(), + internal_id: tx.internal_id.clone(), + block_height: BeBigUint::from(tx.block_height), + confirmation_status: ConfirmationStatus::from_block_height(tx.block_height), + token_id: token_id_from_tx_type(&tx.transaction_type), + from_addresses: tx.from.clone().into_iter().collect(), + to_addresses: tx.to.clone().into_iter().collect(), + details_json, + }) + } +} + +impl TableSignature for TxHistoryTableV2 { + fn table_name() -> &'static str { "tx_history_v2" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + match (old_version, new_version) { + (0, 1) => { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(TxHistoryTableV2::WALLET_ID_INDEX, &["coin", "hd_wallet_rmd160"], false)?; + table.create_multi_index( + TxHistoryTableV2::WALLET_ID_INTERNAL_ID_INDEX, + &["coin", "hd_wallet_rmd160", "internal_id"], + true, + )?; + table.create_multi_index( + TxHistoryTableV2::WALLET_ID_TX_HASH_INDEX, + &["coin", "hd_wallet_rmd160", "tx_hash"], + false, + )?; + table.create_multi_index( + TxHistoryTableV2::WALLET_ID_CONFIRMATION_STATUS_INDEX, + &["coin", "hd_wallet_rmd160", "confirmation_status"], + false, + )?; + table.create_multi_index( + TxHistoryTableV2::WALLET_ID_TOKEN_ID_INDEX, + &["coin", "hd_wallet_rmd160", "token_id"], + false, + )?; + }, + _ => (), + } + Ok(()) + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct TxCacheTableV2 { + coin: String, + tx_hash: String, + tx_hex: BytesJson, +} + +impl TxCacheTableV2 { + /// A **unique** index that consists of the following properties: + /// * coin - coin ticker + /// * tx_hash - transaction hash + const COIN_TX_HASH_INDEX: &'static str = "coin_tx_hash"; + + fn from_tx_details(wallet_id: WalletId, tx: &TransactionDetails) -> TxCacheTableV2 { + TxCacheTableV2 { + coin: wallet_id.ticker, + tx_hash: tx.tx_hash.clone(), + tx_hex: tx.tx_hex.clone(), + } + } +} + +impl TableSignature for TxCacheTableV2 { + fn table_name() -> &'static str { "tx_cache_v2" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + match (old_version, new_version) { + (0, 1) => { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(TxCacheTableV2::COIN_TX_HASH_INDEX, &["coin", "tx_hash"], true)?; + }, + _ => (), + } + Ok(()) + } +} + +fn tx_details_from_item(item: TxHistoryTableV2) -> WasmTxHistoryResult { + json::from_value(item.details_json).map_to_mm(|e| WasmTxHistoryError::ErrorDeserializing(e.to_string())) +} diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 6a3ad7b8ac..6e30d5468f 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -1,13 +1,15 @@ /****************************************************************************** - * Copyright © 2014-2019 The SuperNET Developers. * + * Copyright © 2022 Atomic Private Limited and its contributors * * * - * See the AUTHORS, DEVELOPER-AGREEMENT and LICENSE files at * + * See the CONTRIBUTOR-LICENSE-AGREEMENT, COPYING, LICENSE-COPYRIGHT-NOTICE * + * and DEVELOPER-CERTIFICATE-OF-ORIGIN files in the LEGAL directory in * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * - * SuperNET software, including this file may be copied, modified, propagated * - * or distributed except according to the terms contained in the LICENSE file * + * AtomicDEX software, including this file may be copied, modified, propagated* + * or distributed except according to the terms contained in the * + * LICENSE-COPYRIGHT-NOTICE file. * * * * Removal or modification of this copyright notice is prohibited. * * * @@ -16,73 +18,104 @@ // utxo.rs // marketmaker // -// Copyright © 2017-2019 SuperNET. All rights reserved. +// Copyright © 2022 AtomicDEX. All rights reserved. // +pub mod bch; +pub mod bch_and_slp_tx_history; +mod bchd_grpc; +#[allow(clippy::all)] +#[rustfmt::skip] +#[path = "utxo/pb.rs"] +mod bchd_pb; pub mod qtum; pub mod rpc_clients; pub mod slp; +pub mod utxo_block_header_storage; +pub mod utxo_builder; pub mod utxo_common; pub mod utxo_standard; - -#[cfg(not(target_arch = "wasm32"))] pub mod tx_cache; +pub mod utxo_withdraw; use async_trait::async_trait; -use bigdecimal::BigDecimal; +use bitcoin::network::constants::Network as BitcoinNetwork; pub use bitcrypto::{dhash160, sha256, ChecksumType}; -use chain::{OutPoint, TransactionInput, TransactionOutput, TxHashAlgo}; -use common::executor::{spawn, Timer}; +pub use chain::Transaction as UtxoTx; +use chain::{OutPoint, TransactionOutput, TxHashAlgo}; #[cfg(not(target_arch = "wasm32"))] use common::first_char_to_upper; use common::jsonrpc_client::JsonRpcError; -use common::mm_ctx::MmArc; -use common::mm_error::prelude::*; use common::mm_metrics::MetricsArc; -use common::{now_ms, small_rng}; +use common::now_ms; +use crypto::trezor::utxo::TrezorUtxoCoin; +use crypto::{Bip32DerPathOps, Bip32Error, Bip44Chain, Bip44DerPathError, Bip44PathToAccount, Bip44PathToCoin, + ChildNumber, DerivationPath, Secp256k1ExtendedPublicKey}; use derive_more::Display; #[cfg(not(target_arch = "wasm32"))] use dirs::home_dir; use futures::channel::mpsc; use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; -use futures::stream::StreamExt; use futures01::Future; use keys::bytes::Bytes; -pub use keys::{Address, AddressFormat as UtxoAddressFormat, KeyPair, Private, Public, Secret, Type as ScriptType}; +pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, KeyPair, Private, Public, Secret, + Type as ScriptType}; +use lightning_invoice::Currency as LightningCurrency; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::BigDecimal; #[cfg(test)] use mocktopus::macros::*; use num_traits::ToPrimitive; -use primitives::hash::{H256, H264, H512}; -use rand::seq::SliceRandom; +use primitives::hash::{H256, H264}; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; use script::{Builder, Script, SignatureVersion, TransactionInputSigner}; use serde_json::{self as json, Value as Json}; use serialization::{serialize, serialize_with_flags, SERIALIZE_TRANSACTION_WITNESS}; +use spv_validation::helpers_validation::SPVError; +use std::array::TryFromSliceError; use std::collections::{HashMap, HashSet}; use std::convert::TryInto; +use std::hash::Hash; use std::num::NonZeroU64; use std::ops::Deref; -#[cfg(not(target_arch = "wasm32"))] use std::path::Path; -use std::path::PathBuf; +#[cfg(not(target_arch = "wasm32"))] +use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::atomic::{AtomicBool, AtomicU64}; use std::sync::{Arc, Mutex, Weak}; -use utxo_common::big_decimal_from_sat; - -pub use chain::Transaction as UtxoTx; - +use utxo_builder::UtxoConfBuilder; +use utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; +use utxo_signer::with_key_pair::sign_tx; +use utxo_signer::{TxProvider, TxProviderError, UtxoSignTxError, UtxoSignTxResult}; + +use self::rpc_clients::{electrum_script_hash, ElectrumClient, ElectrumRpcRequest, EstimateFeeMethod, EstimateFeeMode, + NativeClient, UnspentInfo, UnspentMap, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, + UtxoRpcResult}; +use super::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BalanceResult, CoinBalance, CoinsContext, + DerivationMethod, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, KmdRewardsDetails, MarketCoinOps, + MmCoin, NumConversError, NumConversResult, PrivKeyActivationPolicy, PrivKeyNotAllowed, PrivKeyPolicy, + RawTransactionFut, RawTransactionRequest, RawTransactionResult, RpcTransportEventHandler, + RpcTransportEventHandlerShared, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, + Transaction, TransactionDetails, TransactionEnum, UnexpectedDerivationMethod, WithdrawError, + WithdrawRequest}; +use crate::coin_balance::{EnableCoinScanPolicy, HDAddressBalanceScanner}; +use crate::hd_wallet::{HDAccountOps, HDAccountsMutex, HDAddress, HDWalletCoinOps, HDWalletOps, InvalidBip44ChainError}; +use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletCoinStorage, HDWalletStorageError, HDWalletStorageResult}; +use crate::utxo::tx_cache::UtxoVerboseCacheShared; +use crate::utxo::utxo_block_header_storage::BlockHeaderStorageError; +use crate::TransactionErr; +use utxo_block_header_storage::BlockHeaderStorage; + +pub mod tx_cache; +#[cfg(target_arch = "wasm32")] +pub mod utxo_indexedb_block_header_storage; #[cfg(not(target_arch = "wasm32"))] -use self::rpc_clients::{ConcurrentRequestMap, NativeClient, NativeClientImpl}; -use self::rpc_clients::{ElectrumClient, ElectrumClientImpl, ElectrumRpcRequest, EstimateFeeMethod, EstimateFeeMode, - UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcResult}; -use super::{BalanceError, BalanceFut, BalanceResult, CoinTransportMetrics, CoinsContext, FeeApproxStage, - FoundSwapTxSpend, HistorySyncState, KmdRewardsDetails, MarketCoinOps, MmCoin, NumConversError, - NumConversResult, RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, TradeFee, - TradePreimageError, TradePreimageFut, TradePreimageResult, Transaction, TransactionDetails, - TransactionEnum, TransactionFut, WithdrawError, WithdrawFee, WithdrawRequest}; +pub mod utxo_sql_block_header_storage; +#[cfg(any(test, target_arch = "wasm32"))] +pub mod utxo_common_tests; #[cfg(test)] pub mod utxo_tests; #[cfg(target_arch = "wasm32")] pub mod utxo_wasm_tests; -const SWAP_TX_SPEND_SIZE: u64 = 305; const KILO_BYTE: u64 = 1000; /// https://bitcoin.stackexchange.com/a/77192 const MAX_DER_SIGNATURE_LEN: usize = 72; @@ -96,9 +129,12 @@ const UTXO_DUST_AMOUNT: u64 = 1000; /// 11 > 0 const KMD_MTP_BLOCK_COUNT: NonZeroU64 = unsafe { NonZeroU64::new_unchecked(11u64) }; const DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT: f64 = 0.5; +const DEFAULT_GAP_LIMIT: u32 = 20; pub type GenerateTxResult = Result<(TransactionInputSigner, AdditionalTxData), MmError>; pub type HistoryUtxoTxMap = HashMap; +pub type MatureUnspentMap = HashMap; +pub type RecentlySpentOutPointsGuard<'a> = AsyncMutexGuard<'a, RecentlySpentOutPoints>; #[cfg(windows)] #[cfg(not(target_arch = "wasm32"))] @@ -177,6 +213,33 @@ impl From for TradePreimageError { } } +impl From for TxProviderError { + fn from(rpc: UtxoRpcError) -> Self { + match rpc { + resp @ UtxoRpcError::ResponseParseError(_) | resp @ UtxoRpcError::InvalidResponse(_) => { + TxProviderError::InvalidResponse(resp.to_string()) + }, + UtxoRpcError::Transport(transport) => TxProviderError::Transport(transport.to_string()), + UtxoRpcError::Internal(internal) => TxProviderError::Internal(internal), + } + } +} + +impl From for HDWalletStorageError { + fn from(e: Bip44DerPathError) -> Self { HDWalletStorageError::ErrorDeserializing(e.to_string()) } +} + +impl From for HDWalletStorageError { + fn from(e: Bip32Error) -> Self { HDWalletStorageError::ErrorDeserializing(e.to_string()) } +} + +#[async_trait] +impl TxProvider for UtxoRpcClientEnum { + async fn get_rpc_transaction(&self, tx_hash: &H256Json) -> Result> { + Ok(self.get_verbose_transaction(tx_hash).compat().await?) + } +} + /// The `UtxoTx` with the block height transaction mined in. pub struct HistoryUtxoTx { pub height: Option, @@ -205,7 +268,7 @@ pub enum TxFee { } /// The actual "runtime" fee that is received from RPC in case of dynamic calculation -#[derive(Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum ActualTxFee { /// fee amount per Kbyte received from coin RPC Dynamic(u64), @@ -277,7 +340,7 @@ impl RecentlySpentOutPoints { if output.script_pubkey == self.for_script_pubkey { Some(CachedUnspentInfo { outpoint: OutPoint { - hash: spend_tx_hash.clone(), + hash: spend_tx_hash, index: index as u32, }, value: output.value, @@ -343,7 +406,7 @@ impl RecentlySpentOutPoints { } } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub enum BlockchainNetwork { #[serde(rename = "mainnet")] Mainnet, @@ -353,6 +416,26 @@ pub enum BlockchainNetwork { Regtest, } +impl From for BitcoinNetwork { + fn from(network: BlockchainNetwork) -> Self { + match network { + BlockchainNetwork::Mainnet => BitcoinNetwork::Bitcoin, + BlockchainNetwork::Testnet => BitcoinNetwork::Testnet, + BlockchainNetwork::Regtest => BitcoinNetwork::Regtest, + } + } +} + +impl From for LightningCurrency { + fn from(network: BlockchainNetwork) -> Self { + match network { + BlockchainNetwork::Mainnet => LightningCurrency::Bitcoin, + BlockchainNetwork::Testnet => LightningCurrency::BitcoinTestnet, + BlockchainNetwork::Regtest => LightningCurrency::Regtest, + } + } +} + #[derive(Debug)] pub struct UtxoCoinConf { pub ticker: String, @@ -363,6 +446,7 @@ pub struct UtxoCoinConf { pub wif_prefix: u8, pub pub_t_addr_prefix: u8, pub p2sh_t_addr_prefix: u8, + pub sign_message_prefix: Option, // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Segwit_address_format pub bech32_hrp: Option, /// True if coins uses Proof of Stake consensus algo @@ -380,9 +464,7 @@ pub struct UtxoCoinConf { /// Overwinter and then Sapling upgrades /// https://github.com/zcash/zips/blob/master/zip-0243.rst pub tx_version: i32, - /// If true - allow coins withdraw to P2SH addresses (Segwit). - /// the flag will also affect the address that MM2 generates by default in the future - /// will be the Segwit (starting from 3 for BTC case) instead of legacy + /// Defines if Segwit is enabled for this coin. /// https://en.bitcoin.it/wiki/Segregated_Witness pub segwit: bool, /// Does coin require transactions to be notarized to be considered as confirmed? @@ -420,6 +502,10 @@ pub struct UtxoCoinConf { pub mature_confirmations: u32, /// The number of blocks used for estimate_fee/estimate_smart_fee RPC calls pub estimate_fee_blocks: u32, + /// The name of the coin with which Trezor wallet associates this asset. + pub trezor_coin: Option, + /// Used in condition where the coin will validate spv proof or not + pub enable_spv_proof: bool, } #[derive(Debug)] @@ -437,18 +523,22 @@ pub struct UtxoCoinFields { pub dust_amount: u64, /// RPC client pub rpc_client: UtxoRpcClientEnum, - /// ECDSA key pair - pub key_pair: KeyPair, - /// Lock the mutex when we deal with address utxos - pub my_address: Address, + /// Either ECDSA key pair or a Hardware Wallet info. + pub priv_key_policy: PrivKeyPolicy, + /// Either an Iguana address or an info about last derived account/address. + pub derivation_method: DerivationMethod, pub history_sync_state: Mutex, - /// Path to the TX cache directory - pub tx_cache_directory: Option, + /// The cache of verbose transactions. + pub tx_cache: UtxoVerboseCacheShared, + pub block_headers_storage: Option, /// The cache of recently send transactions used to track the spent UTXOs and replace them with new outputs /// The daemon needs some time to update the listunspent list for address which makes it return already spent UTXOs /// This cache helps to prevent UTXO reuse in such cases pub recently_spent_outpoints: AsyncMutex, pub tx_hash_algo: TxHashAlgo, + /// The flag determines whether to use mature unspent outputs *only* to generate transactions. + /// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 + pub check_utxo_maturity: bool, } #[derive(Debug, Display)] @@ -472,6 +562,60 @@ pub enum UnsupportedAddr { SegwitNotActivated(String), } +impl From for WithdrawError { + fn from(e: UnsupportedAddr) -> Self { WithdrawError::InvalidAddress(e.to_string()) } +} + +#[derive(Debug)] +pub enum GetTxHeightError { + HeightNotFound, +} + +impl From for SPVError { + fn from(e: GetTxHeightError) -> Self { + match e { + GetTxHeightError::HeightNotFound => SPVError::InvalidHeight, + } + } +} + +#[derive(Debug)] +pub enum GetBlockHeaderError { + StorageError(BlockHeaderStorageError), + RpcError(JsonRpcError), + SerializationError(serialization::Error), + InvalidResponse(String), + SPVError(SPVError), + NativeNotSupported(String), + Internal(String), +} + +impl From for GetBlockHeaderError { + fn from(err: JsonRpcError) -> Self { GetBlockHeaderError::RpcError(err) } +} + +impl From for GetBlockHeaderError { + fn from(e: UtxoRpcError) -> Self { + match e { + UtxoRpcError::Transport(e) | UtxoRpcError::ResponseParseError(e) => GetBlockHeaderError::RpcError(e), + UtxoRpcError::InvalidResponse(e) => GetBlockHeaderError::InvalidResponse(e), + UtxoRpcError::Internal(e) => GetBlockHeaderError::Internal(e), + } + } +} + +impl From for GetBlockHeaderError { + fn from(e: SPVError) -> Self { GetBlockHeaderError::SPVError(e) } +} + +impl From for GetBlockHeaderError { + fn from(err: serialization::Error) -> Self { GetBlockHeaderError::SerializationError(err) } +} + +impl From for GetBlockHeaderError { + fn from(err: BlockHeaderStorageError) -> Self { GetBlockHeaderError::StorageError(err) } +} + impl UtxoCoinFields { pub fn transaction_preimage(&self) -> TransactionInputSigner { let lock_time = if self.conf.ticker == "KMD" { @@ -480,9 +624,21 @@ impl UtxoCoinFields { (now_ms() / 1000) as u32 }; + let str_d_zeel = if self.conf.ticker == "NAV" { + Some("".into()) + } else { + None + }; + + let n_time = if self.conf.is_pos { + Some((now_ms() / 1000) as u32) + } else { + None + }; + TransactionInputSigner { version: self.conf.tx_version, - n_time: None, + n_time, overwintered: self.conf.overwintered, version_group_id: self.conf.version_group_id, consensus_branch_id: self.conf.consensus_branch_id, @@ -495,69 +651,157 @@ impl UtxoCoinFields { shielded_spends: vec![], shielded_outputs: vec![], zcash: self.conf.zcash, - str_d_zeel: None, + str_d_zeel, hash_algo: self.tx_hash_algo.into(), } } +} - pub fn check_withdraw_address_supported(&self, addr: &Address) -> Result<(), MmError> { - let conf = &self.conf; - - match addr.addr_format { - // Considering that legacy is supported with any configured formats - // This can be changed depending on the coins implementation - UtxoAddressFormat::Standard => { - let is_p2pkh = addr.prefix == conf.pub_addr_prefix && addr.t_addr_prefix == conf.pub_t_addr_prefix; - let is_p2sh = addr.prefix == conf.p2sh_addr_prefix - && addr.t_addr_prefix == conf.p2sh_t_addr_prefix - && conf.segwit; - if !is_p2pkh && !is_p2sh { - MmError::err(UnsupportedAddr::PrefixError(conf.ticker.clone())) - } else { - Ok(()) - } - }, - UtxoAddressFormat::Segwit => { - if !conf.segwit { - return MmError::err(UnsupportedAddr::SegwitNotActivated(conf.ticker.clone())); - } +#[derive(Debug, Display)] +#[allow(clippy::large_enum_variant)] +pub enum BroadcastTxErr { + /// RPC client error + Rpc(UtxoRpcError), + /// Other specific error + Other(String), +} - if addr.hrp != conf.bech32_hrp { - MmError::err(UnsupportedAddr::HrpError { - ticker: conf.ticker.clone(), - hrp: addr.hrp.clone().unwrap_or_default(), - }) - } else { - Ok(()) - } - }, - UtxoAddressFormat::CashAddress { .. } => { - if addr.addr_format == conf.default_address_format || addr.addr_format == self.my_address.addr_format { - Ok(()) - } else { - MmError::err(UnsupportedAddr::FormatMismatch { - ticker: conf.ticker.clone(), - activated_format: self.my_address.addr_format.to_string(), - used_format: addr.addr_format.to_string(), - }) - } +impl From for BroadcastTxErr { + fn from(err: UtxoRpcError) -> Self { BroadcastTxErr::Rpc(err) } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +pub trait UtxoTxBroadcastOps { + async fn broadcast_tx(&self, tx: &UtxoTx) -> Result>; +} + +#[async_trait] +#[cfg_attr(test, mockable)] +pub trait UtxoTxGenerationOps { + async fn get_tx_fee(&self) -> UtxoRpcResult; + + /// Calculates interest if the coin is KMD + /// Adds the value to existing output to my_script_pub or creates additional interest output + /// returns transaction and data as is if the coin is not KMD + async fn calc_interest_if_required( + &self, + mut unsigned: TransactionInputSigner, + mut data: AdditionalTxData, + my_script_pub: Bytes, + ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)>; +} + +/// The UTXO address balance scanner. +/// If the coin is initialized with a native RPC client, it's better to request the list of used addresses +/// right on `UtxoAddressBalanceScanner` initialization. +/// See [`NativeClientImpl::list_transactions`]. +pub enum UtxoAddressScanner { + Native { non_empty_addresses: HashSet }, + Electrum(ElectrumClient), +} + +#[async_trait] +impl HDAddressBalanceScanner for UtxoAddressScanner { + type Address = Address; + + async fn is_address_used(&self, address: &Self::Address) -> BalanceResult { + let is_used = match self { + UtxoAddressScanner::Native { non_empty_addresses } => non_empty_addresses.contains(&address.to_string()), + UtxoAddressScanner::Electrum(electrum_client) => { + let script = output_script(address, ScriptType::P2PKH); + let script_hash = electrum_script_hash(&script); + + let electrum_history = electrum_client + .scripthash_get_history(&hex::encode(script_hash)) + .compat() + .await?; + + !electrum_history.is_empty() }, + }; + Ok(is_used) + } +} + +impl UtxoAddressScanner { + pub async fn init(rpc_client: UtxoRpcClientEnum) -> UtxoRpcResult { + match rpc_client { + UtxoRpcClientEnum::Native(native) => UtxoAddressScanner::init_with_native_client(&native).await, + UtxoRpcClientEnum::Electrum(electrum) => Ok(UtxoAddressScanner::Electrum(electrum)), + } + } + + pub async fn init_with_native_client(native: &NativeClient) -> UtxoRpcResult { + const STEP: u64 = 100; + + let non_empty_addresses = native + .list_all_transactions(STEP) + .compat() + .await? + .into_iter() + .map(|tx_item| tx_item.address) + .collect(); + Ok(UtxoAddressScanner::Native { non_empty_addresses }) + } +} + +/// Contains lists of mature and immature UTXOs. +#[derive(Debug, Default)] +pub struct MatureUnspentList { + mature: Vec, + immature: Vec, +} + +impl MatureUnspentList { + #[inline] + pub fn with_capacity(capacity: usize) -> MatureUnspentList { + MatureUnspentList { + mature: Vec::with_capacity(capacity), + immature: Vec::with_capacity(capacity), + } + } + + #[inline] + pub fn new_mature(mature: Vec) -> MatureUnspentList { + MatureUnspentList { + mature, + immature: Vec::new(), + } + } + + #[inline] + pub fn only_mature(self) -> Vec { self.mature } + + #[inline] + pub fn to_coin_balance(&self, decimals: u8) -> CoinBalance { + let fold = |acc: BigDecimal, x: &UnspentInfo| acc + big_decimal_from_sat_unsigned(x.value, decimals); + CoinBalance { + spendable: self.mature.iter().fold(BigDecimal::default(), fold), + unspendable: self.immature.iter().fold(BigDecimal::default(), fold), } } } #[async_trait] #[cfg_attr(test, mockable)] -pub trait UtxoCommonOps { - async fn get_tx_fee(&self) -> Result; - - async fn get_htlc_spend_fee(&self) -> UtxoRpcResult; +pub trait UtxoCommonOps: + AsRef + UtxoTxGenerationOps + UtxoTxBroadcastOps + Clone + Send + Sync + 'static +{ + async fn get_htlc_spend_fee(&self, tx_size: u64) -> UtxoRpcResult; fn addresses_from_script(&self, script: &Script) -> Result, String>; fn denominate_satoshis(&self, satoshi: i64) -> f64; - fn my_public_key(&self) -> &Public; + /// Get a public key that matches [`PrivKeyPolicy::KeyPair`]. + /// + /// # Fail + /// + /// The method is expected to fail if [`UtxoCoinFields::priv_key_policy`] is [`PrivKeyPolicy::HardwareWallet`]. + /// It's worth adding a method like `my_public_key_der_path` + /// that takes a derivation path from which we derive the corresponding public key. + fn my_public_key(&self) -> Result<&Public, MmError>; /// Try to parse address from string using specified on asset enable format, /// and if it failed inform user that he used a wrong format. @@ -568,30 +812,6 @@ pub trait UtxoCommonOps { /// Check if the output is spendable (is not coinbase or it has enough confirmations). fn is_unspent_mature(&self, output: &RpcTransaction) -> bool; - /// Generates unsigned transaction (TransactionInputSigner) from specified utxos and outputs. - /// This function expects that utxos are sorted by amounts in ascending order - /// Consider sorting before calling this function - /// Sends the change (inputs amount - outputs amount) to "my_address" - /// Also returns additional transaction data - async fn generate_transaction( - &self, - utxos: Vec, - outputs: Vec, - fee_policy: FeePolicy, - fee: Option, - gas_fee: Option, - ) -> GenerateTxResult; - - /// Calculates interest if the coin is KMD - /// Adds the value to existing output to my_script_pub or creates additional interest output - /// returns transaction and data as is if the coin is not KMD - async fn calc_interest_if_required( - &self, - mut unsigned: TransactionInputSigner, - mut data: AdditionalTxData, - my_script_pub: Bytes, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)>; - /// Calculates interest of the specified transaction. /// Please note, this method has to be used for KMD transactions only. async fn calc_interest_of_tx(&self, tx: &UtxoTx, input_transactions: &mut HistoryUtxoTxMap) -> UtxoRpcResult; @@ -603,37 +823,13 @@ pub trait UtxoCommonOps { utxo_tx_map: &'b mut HistoryUtxoTxMap, ) -> UtxoRpcResult<&'b mut HistoryUtxoTx>; - async fn p2sh_spending_tx( - &self, - prev_transaction: UtxoTx, - redeem_script: Bytes, - outputs: Vec, - script_data: Script, - sequence: u32, - lock_time: u32, - ) -> Result; + async fn p2sh_spending_tx(&self, input: utxo_common::P2SHSpendingTxInput<'_>) -> Result; - /// Get transaction outputs available to spend. - async fn ordered_mature_unspents<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)>; - - /// Try to load verbose transaction from cache or try to request it from Rpc client. - fn get_verbose_transaction_from_cache_or_rpc( - &self, - txid: H256Json, - ) -> Box + Send>; - - /// Cache transaction if the coin supports `TX_CACHE` and tx height is set and not zero. - async fn cache_transaction_if_possible(&self, tx: &RpcTransaction) -> Result<(), String>; - - /// Returns available unspents in ascending order + RecentlySpentOutPoints MutexGuard for further interaction - /// (e.g. to add new transaction to it). - async fn list_unspent_ordered( + /// Loads verbose transactions from cache or requests it using RPC client. + fn get_verbose_transactions_from_cache_or_rpc( &self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'_, RecentlySpentOutPoints>)>; + tx_ids: HashSet, + ) -> UtxoRpcFut>; async fn preimage_trade_fee_required_to_send_outputs( &self, @@ -649,7 +845,90 @@ pub trait UtxoCommonOps { async fn p2sh_tx_locktime(&self, htlc_locktime: u32) -> Result>; + fn addr_format(&self) -> &UtxoAddressFormat; + fn addr_format_for_standard_scripts(&self) -> UtxoAddressFormat; + + fn address_from_pubkey(&self, pubkey: &Public) -> Address; + + fn address_from_extended_pubkey(&self, extended_pubkey: &Secp256k1ExtendedPublicKey) -> Address { + let pubkey = Public::Compressed(H264::from(extended_pubkey.public_key().serialize())); + self.address_from_pubkey(&pubkey) + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +pub trait GetUtxoListOps { + /// Returns available unspents in ascending order + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// The function uses either [`GetUtxoListOps::get_all_unspent_ordered_list`] or [`GetUtxoListOps::get_mature_unspent_ordered_list`] + /// depending on the coin configuration. + async fn get_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)>; + + /// Returns available unspents in ascending order + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// + /// # Important + /// + /// The function doesn't check if the unspents are mature or immature. + /// Consider using [`GetUtxoListOps::get_unspent_ordered_list`] instead. + async fn get_all_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)>; + + /// Returns available mature and immature unspents in ascending order + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// + /// # Important + /// + /// The function may request extra data using RPC to check each unspent output whether it's mature or not. + /// It may be overhead in some cases, so consider using [`GetUtxoListOps::get_unspent_ordered_list`] instead. + async fn get_mature_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)>; +} + +#[async_trait] +#[cfg_attr(test, mockable)] +pub trait GetUtxoMapOps { + /// Returns available unspents in ascending order for every given `addresses` + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// The function uses either [`GetUtxoMapOps::get_all_unspent_ordered_map`] or [`GetUtxoMapOps::get_mature_unspent_ordered_map`] + /// depending on the coin configuration. + async fn get_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)>; + + /// Returns available unspents in ascending order for every given `addresses` + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// + /// # Important + /// + /// The function doesn't check if the unspents are mature or immature. + /// Consider using [`GetUtxoMapOps::get_unspent_ordered_map`] instead. + async fn get_all_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)>; + + /// Returns available mature and immature unspents in ascending order for every given `addresses` + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// + /// # Important + /// + /// The function may request extra data using RPC to check each unspent output whether it's mature or not. + /// It may be overhead in some cases, so consider using [`GetUtxoMapOps::get_unspent_ordered_map`] instead. + async fn get_mature_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(MatureUnspentMap, RecentlySpentOutPointsGuard<'_>)>; } #[async_trait] @@ -682,7 +961,7 @@ impl Deref for UtxoArc { } impl From for UtxoArc { - fn from(coin: UtxoCoinFields) -> UtxoArc { UtxoArc(Arc::new(coin)) } + fn from(coin: UtxoCoinFields) -> UtxoArc { UtxoArc::new(coin) } } impl From> for UtxoArc { @@ -690,8 +969,12 @@ impl From> for UtxoArc { } impl UtxoArc { + pub fn new(fields: UtxoCoinFields) -> UtxoArc { UtxoArc(Arc::new(fields)) } + + pub fn with_arc(inner: Arc) -> UtxoArc { UtxoArc(inner) } + /// Returns weak reference to the inner UtxoCoinFields - fn downgrade(&self) -> UtxoWeak { + pub fn downgrade(&self) -> UtxoWeak { let weak = Arc::downgrade(&self.0); UtxoWeak(weak) } @@ -705,7 +988,7 @@ impl From> for UtxoWeak { } impl UtxoWeak { - fn upgrade(&self) -> Option { self.0.upgrade().map(UtxoArc::from) } + pub fn upgrade(&self) -> Option { self.0.upgrade().map(UtxoArc::from) } } // We can use a shared UTXO lock for all UTXO coins at 1 time. @@ -772,14 +1055,31 @@ pub enum RequestTxHistoryResult { Ok(Vec<(H256Json, u64)>), Retry { error: String }, HistoryTooLarge, - UnknownError(String), + CriticalError(String), } +#[derive(Clone)] pub enum VerboseTransactionFrom { Cache(RpcTransaction), Rpc(RpcTransaction), } +impl VerboseTransactionFrom { + #[inline] + fn to_inner(&self) -> &RpcTransaction { + match self { + VerboseTransactionFrom::Rpc(tx) | VerboseTransactionFrom::Cache(tx) => tx, + } + } + + #[inline] + pub fn into_inner(self) -> RpcTransaction { + match self { + VerboseTransactionFrom::Rpc(tx) | VerboseTransactionFrom::Cache(tx) => tx, + } + } +} + pub fn compressed_key_pair_from_bytes(raw: &[u8], prefix: u8, checksum_type: ChecksumType) -> Result { if raw.len() != 32 { return ERR!("Invalid raw priv key len {}", raw.len()); @@ -801,6 +1101,7 @@ pub fn compressed_pub_key_from_priv_raw(raw_priv: &[u8], sum_type: ChecksumType) #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct UtxoFeeDetails { + pub coin: Option, pub amount: BigDecimal, } @@ -858,50 +1159,6 @@ pub fn coin_daemon_data_dir(name: &str, is_asset_chain: bool) -> PathBuf { data_dir } -/// Attempts to parse native daemon conf file and return rpcport, rpcuser and rpcpassword -#[cfg(not(target_arch = "wasm32"))] -fn read_native_mode_conf( - filename: &dyn AsRef, - network: &BlockchainNetwork, -) -> Result<(Option, String, String), String> { - use ini::Ini; - - fn read_property<'a>(conf: &'a ini::Ini, network: &BlockchainNetwork, property: &str) -> Option<&'a String> { - let subsection = match network { - BlockchainNetwork::Mainnet => None, - BlockchainNetwork::Testnet => conf.section(Some("test")), - BlockchainNetwork::Regtest => conf.section(Some("regtest")), - }; - subsection - .and_then(|props| props.get(property)) - .or_else(|| conf.general_section().get(property)) - } - - let conf: Ini = match Ini::load_from_file(&filename) { - Ok(ini) => ini, - Err(err) => { - return ERR!( - "Error parsing the native wallet configuration '{}': {}", - filename.as_ref().display(), - err - ) - }, - }; - let rpc_port = match read_property(&conf, network, "rpcport") { - Some(port) => port.parse::().ok(), - None => None, - }; - let rpc_user = try_s!(read_property(&conf, network, "rpcuser").ok_or(ERRL!( - "Conf file {} doesn't have the rpcuser key", - filename.as_ref().display() - ))); - let rpc_password = try_s!(read_property(&conf, network, "rpcpassword").ok_or(ERRL!( - "Conf file {} doesn't have the rpcpassword key", - filename.as_ref().display() - ))); - Ok((rpc_port, rpc_user.clone(), rpc_password.clone())) -} - /// Electrum protocol version verifier. /// The structure is used to handle the `on_connected` event and notify `electrum_version_loop`. struct ElectrumProtoVerifier { @@ -925,241 +1182,106 @@ impl RpcTransportEventHandler for ElectrumProtoVerifier { } } -pub struct UtxoConfBuilder<'a> { - conf: &'a Json, - req: &'a Json, - ticker: &'a str, +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UtxoMergeParams { + pub merge_at: usize, + #[serde(default = "common::ten_f64")] + pub check_every: f64, + #[serde(default = "common::one_hundred")] + pub max_merge_at_once: usize, } -impl<'a> UtxoConfBuilder<'a> { - pub fn new(conf: &'a Json, req: &'a Json, ticker: &'a str) -> Self { UtxoConfBuilder { conf, req, ticker } } - - pub fn build(&self) -> Result { - let checksum_type = self.checksum_type(); - let pub_addr_prefix = self.pub_addr_prefix(); - let p2sh_addr_prefix = self.p2sh_address_prefix(); - let pub_t_addr_prefix = self.pub_t_address_prefix(); - let p2sh_t_addr_prefix = self.p2sh_t_address_prefix(); - - let wif_prefix = self.wif_prefix(); - - let bech32_hrp = self.bech32_hrp(); - - let default_address_format = self.default_address_format(); - - let asset_chain = self.asset_chain(); - let tx_version = self.tx_version(); - let overwintered = self.overwintered(); - - let tx_fee_volatility_percent = self.tx_fee_volatility_percent(); - let version_group_id = try_s!(self.version_group_id(tx_version, overwintered)); - let consensus_branch_id = try_s!(self.consensus_branch_id(tx_version)); - let signature_version = self.signature_version(); - let fork_id = self.fork_id(); - - // should be sufficient to detect zcash by overwintered flag - let zcash = overwintered; - - let required_confirmations = self.required_confirmations(); - let requires_notarization = self.requires_notarization(); - - let mature_confirmations = self.mature_confirmations(); - - let is_pos = self.is_pos(); - let segwit = self.segwit(); - let force_min_relay_fee = self.conf["force_min_relay_fee"].as_bool().unwrap_or(false); - let mtp_block_count = self.mtp_block_count(); - let estimate_fee_mode = self.estimate_fee_mode(); - let estimate_fee_blocks = self.estimate_fee_blocks(); - - Ok(UtxoCoinConf { - ticker: self.ticker.to_owned(), - is_pos, - requires_notarization, - overwintered, - pub_addr_prefix, - p2sh_addr_prefix, - pub_t_addr_prefix, - p2sh_t_addr_prefix, - bech32_hrp, - segwit, - wif_prefix, - tx_version, - default_address_format, - asset_chain, - tx_fee_volatility_percent, - version_group_id, - consensus_branch_id, - zcash, - checksum_type, - signature_version, - fork_id, - required_confirmations: required_confirmations.into(), - force_min_relay_fee, - mtp_block_count, - estimate_fee_mode, - mature_confirmations, - estimate_fee_blocks, - }) - } - - fn checksum_type(&self) -> ChecksumType { - match self.ticker { - "GRS" => ChecksumType::DGROESTL512, - "SMART" => ChecksumType::KECCAK256, - _ => ChecksumType::DSHA256, - } - } - - fn pub_addr_prefix(&self) -> u8 { - let pubtype = self.conf["pubtype"] - .as_u64() - .unwrap_or(if self.ticker == "BTC" { 0 } else { 60 }); - pubtype as u8 - } - - fn p2sh_address_prefix(&self) -> u8 { - self.conf["p2shtype"] - .as_u64() - .unwrap_or(if self.ticker == "BTC" { 5 } else { 85 }) as u8 - } - - fn pub_t_address_prefix(&self) -> u8 { self.conf["taddr"].as_u64().unwrap_or(0) as u8 } - - fn p2sh_t_address_prefix(&self) -> u8 { self.conf["taddr"].as_u64().unwrap_or(0) as u8 } - - fn wif_prefix(&self) -> u8 { - let wiftype = self.conf["wiftype"] - .as_u64() - .unwrap_or(if self.ticker == "BTC" { 128 } else { 188 }); - wiftype as u8 - } - - fn bech32_hrp(&self) -> Option { json::from_value(self.conf["bech32_hrp"].clone()).unwrap_or(None) } - - fn default_address_format(&self) -> UtxoAddressFormat { - let mut address_format: UtxoAddressFormat = - json::from_value(self.conf["address_format"].clone()).unwrap_or(UtxoAddressFormat::Standard); - - if let UtxoAddressFormat::CashAddress { - network: _, - ref mut pub_addr_prefix, - ref mut p2sh_addr_prefix, - } = address_format - { - *pub_addr_prefix = self.pub_addr_prefix(); - *p2sh_addr_prefix = self.p2sh_address_prefix(); - } - - address_format - } - - fn asset_chain(&self) -> bool { self.conf["asset"].as_str().is_some() } - - fn tx_version(&self) -> i32 { self.conf["txversion"].as_i64().unwrap_or(1) as i32 } - - fn overwintered(&self) -> bool { self.conf["overwintered"].as_u64().unwrap_or(0) == 1 } - - fn tx_fee_volatility_percent(&self) -> f64 { - match self.conf["txfee_volatility_percent"].as_f64() { - Some(volatility) => volatility, - None => DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT, - } - } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UtxoBlockHeaderVerificationParams { + pub difficulty_check: bool, + pub constant_difficulty: bool, + pub blocks_limit_to_check: NonZeroU64, + pub check_every: f64, +} - fn version_group_id(&self, tx_version: i32, overwintered: bool) -> Result { - let version_group_id = match self.conf["version_group_id"].as_str() { - Some(mut s) => { - if s.starts_with("0x") { - s = &s[2..]; - } - let bytes = try_s!(hex::decode(s)); - u32::from_be_bytes(try_s!(bytes.as_slice().try_into())) - }, - None => { - if tx_version == 3 && overwintered { - 0x03c4_8270 - } else if tx_version == 4 && overwintered { - 0x892f_2085 - } else { - 0 - } - }, - }; - Ok(version_group_id) - } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UtxoActivationParams { + pub mode: UtxoRpcMode, + pub utxo_merge_params: Option, + #[serde(default)] + pub tx_history: bool, + pub required_confirmations: Option, + pub requires_notarization: Option, + pub address_format: Option, + pub gap_limit: Option, + #[serde(default)] + pub scan_policy: EnableCoinScanPolicy, + #[serde(default = "PrivKeyActivationPolicy::iguana_priv_key")] + pub priv_key_policy: PrivKeyActivationPolicy, + /// The flag determines whether to use mature unspent outputs *only* to generate transactions. + /// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 + pub check_utxo_maturity: Option, +} - fn consensus_branch_id(&self, tx_version: i32) -> Result { - let consensus_branch_id = match self.conf["consensus_branch_id"].as_str() { - Some(mut s) => { - if s.starts_with("0x") { - s = &s[2..]; - } - let bytes = try_s!(hex::decode(s)); - u32::from_be_bytes(try_s!(bytes.as_slice().try_into())) - }, - None => match tx_version { - 3 => 0x5ba8_1b19, - 4 => 0x76b8_09bb, - _ => 0, +#[derive(Debug, Display)] +pub enum UtxoFromLegacyReqErr { + UnexpectedMethod, + InvalidElectrumServers(json::Error), + InvalidMergeParams(json::Error), + InvalidBlockHeaderVerificationParams(json::Error), + InvalidRequiredConfs(json::Error), + InvalidRequiresNota(json::Error), + InvalidAddressFormat(json::Error), + InvalidCheckUtxoMaturity(json::Error), + InvalidScanPolicy(json::Error), + InvalidPrivKeyPolicy(json::Error), +} + +impl UtxoActivationParams { + pub fn from_legacy_req(req: &Json) -> Result> { + let mode = match req["method"].as_str() { + Some("enable") => UtxoRpcMode::Native, + Some("electrum") => { + let servers = + json::from_value(req["servers"].clone()).map_to_mm(UtxoFromLegacyReqErr::InvalidElectrumServers)?; + UtxoRpcMode::Electrum { servers } }, + _ => return MmError::err(UtxoFromLegacyReqErr::UnexpectedMethod), }; - Ok(consensus_branch_id) - } - - fn signature_version(&self) -> SignatureVersion { - let default_signature_version = if self.ticker == "BCH" || self.fork_id() != 0 { - SignatureVersion::ForkId - } else { - SignatureVersion::Base - }; - json::from_value(self.conf["signature_version"].clone()).unwrap_or(default_signature_version) - } - - fn fork_id(&self) -> u32 { - let default_fork_id = match self.ticker { - "BCH" => "0x40", - _ => "0x0", - }; - let hex_string = self.conf["fork_id"].as_str().unwrap_or(default_fork_id); - let fork_id = u32::from_str_radix(hex_string.trim_start_matches("0x"), 16).unwrap(); - fork_id - } - - fn required_confirmations(&self) -> u64 { - // param from request should override the config - self.req["required_confirmations"] - .as_u64() - .unwrap_or_else(|| self.conf["required_confirmations"].as_u64().unwrap_or(1)) - } - - fn requires_notarization(&self) -> AtomicBool { - self.req["requires_notarization"] - .as_bool() - .unwrap_or_else(|| self.conf["requires_notarization"].as_bool().unwrap_or(false)) - .into() - } - - fn mature_confirmations(&self) -> u32 { - self.conf["mature_confirmations"] - .as_u64() - .map(|x| x as u32) - .unwrap_or(MATURE_CONFIRMATIONS_DEFAULT) - } - - fn is_pos(&self) -> bool { self.conf["isPoS"].as_u64() == Some(1) } - - fn segwit(&self) -> bool { self.conf["segwit"].as_bool().unwrap_or(false) } - - fn mtp_block_count(&self) -> NonZeroU64 { - json::from_value(self.conf["mtp_block_count"].clone()).unwrap_or(KMD_MTP_BLOCK_COUNT) - } - - fn estimate_fee_mode(&self) -> Option { - json::from_value(self.conf["estimate_fee_mode"].clone()).unwrap_or(None) + let utxo_merge_params = + json::from_value(req["utxo_merge_params"].clone()).map_to_mm(UtxoFromLegacyReqErr::InvalidMergeParams)?; + + let tx_history = req["tx_history"].as_bool().unwrap_or_default(); + let required_confirmations = json::from_value(req["required_confirmations"].clone()) + .map_to_mm(UtxoFromLegacyReqErr::InvalidRequiredConfs)?; + let requires_notarization = json::from_value(req["requires_notarization"].clone()) + .map_to_mm(UtxoFromLegacyReqErr::InvalidRequiresNota)?; + let address_format = + json::from_value(req["address_format"].clone()).map_to_mm(UtxoFromLegacyReqErr::InvalidAddressFormat)?; + let check_utxo_maturity = json::from_value(req["check_utxo_maturity"].clone()) + .map_to_mm(UtxoFromLegacyReqErr::InvalidCheckUtxoMaturity)?; + let scan_policy = json::from_value::>(req["scan_policy"].clone()) + .map_to_mm(UtxoFromLegacyReqErr::InvalidScanPolicy)? + .unwrap_or_default(); + let priv_key_policy = json::from_value::>(req["priv_key_policy"].clone()) + .map_to_mm(UtxoFromLegacyReqErr::InvalidPrivKeyPolicy)? + .unwrap_or(PrivKeyActivationPolicy::IguanaPrivKey); + + Ok(UtxoActivationParams { + mode, + utxo_merge_params, + tx_history, + required_confirmations, + requires_notarization, + address_format, + gap_limit: None, + scan_policy, + priv_key_policy, + check_utxo_maturity, + }) } +} - fn estimate_fee_blocks(&self) -> u32 { json::from_value(self.conf["estimate_fee_blocks"].clone()).unwrap_or(1) } +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "rpc", content = "rpc_data")] +pub enum UtxoRpcMode { + Native, + Electrum { servers: Vec }, } #[derive(Debug)] @@ -1179,438 +1301,87 @@ impl Default for ElectrumBuilderArgs { } } -#[async_trait] -pub trait UtxoCoinBuilder { - type ResultCoin; - - async fn build(self) -> Result; - - fn ctx(&self) -> &MmArc; - - fn conf(&self) -> &Json; - - fn req(&self) -> &Json; - - fn ticker(&self) -> &str; - - fn priv_key(&self) -> &[u8]; - - async fn build_utxo_fields(&self) -> Result { - let conf = try_s!(UtxoConfBuilder::new(self.conf(), self.req(), self.ticker()).build()); - - let private = Private { - prefix: conf.wif_prefix, - secret: H256::from(self.priv_key()), - compressed: true, - checksum_type: conf.checksum_type, - }; - let key_pair = try_s!(KeyPair::from_private(private)); - let addr_format = try_s!(self.address_format()); - let my_address = Address { - prefix: conf.pub_addr_prefix, - t_addr_prefix: conf.pub_t_addr_prefix, - hash: key_pair.public().address_hash(), - checksum_type: conf.checksum_type, - hrp: conf.bech32_hrp.clone(), - addr_format, - }; - let my_script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); - let rpc_client = try_s!(self.rpc_client().await); - let tx_fee = try_s!(self.tx_fee(&rpc_client).await); - let decimals = try_s!(self.decimals(&rpc_client).await); - let dust_amount = self.dust_amount(); - - let initial_history_state = self.initial_history_state(); - let tx_cache_directory = Some(self.ctx().dbdir().join("TX_CACHE")); - let tx_hash_algo = self.tx_hash_algo(); - - let coin = UtxoCoinFields { - conf, - decimals, - dust_amount, - rpc_client, - key_pair, - my_address, - history_sync_state: Mutex::new(initial_history_state), - tx_cache_directory, - recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), - tx_fee, - tx_hash_algo, - }; - Ok(coin) - } - - fn address_format(&self) -> Result { - let format_from_req: Option = try_s!(json::from_value(self.req()["address_format"].clone())); - let format_from_conf = try_s!(json::from_value::>( - self.conf()["address_format"].clone() - )) - .unwrap_or(UtxoAddressFormat::Standard); - - let mut address_format = match format_from_req { - Some(from_req) => { - if from_req.is_segwit() != format_from_conf.is_segwit() { - return ERR!( - "Both conf {:?} and request {:?} must be either Segwit or Standard/CashAddress", - format_from_conf, - from_req - ); - } else { - from_req - } - }, - None => format_from_conf, - }; - - if let UtxoAddressFormat::CashAddress { - network: _, - ref mut pub_addr_prefix, - ref mut p2sh_addr_prefix, - } = address_format - { - *pub_addr_prefix = self.pub_addr_prefix(); - *p2sh_addr_prefix = self.p2sh_address_prefix(); - } - - if address_format.is_segwit() - && (!self.conf()["segwit"].as_bool().unwrap_or(false) || self.conf()["bech32_hrp"].is_null()) - { - ERR!("Cannot use Segwit address format for coin without segwit support or bech32_hrp in config") - } else { - Ok(address_format) - } - } - - fn pub_addr_prefix(&self) -> u8 { - let pubtype = self.conf()["pubtype"] - .as_u64() - .unwrap_or(if self.ticker() == "BTC" { 0 } else { 60 }); - pubtype as u8 - } - - fn p2sh_address_prefix(&self) -> u8 { - self.conf()["p2shtype"] - .as_u64() - .unwrap_or(if self.ticker() == "BTC" { 5 } else { 85 }) as u8 - } - - fn dust_amount(&self) -> u64 { json::from_value(self.conf()["dust"].clone()).unwrap_or(UTXO_DUST_AMOUNT) } - - fn network(&self) -> Result { - let conf = self.conf(); - if !conf["network"].is_null() { - return json::from_value(conf["network"].clone()).map_err(|e| ERRL!("{}", e)); - } - Ok(BlockchainNetwork::Mainnet) - } - - async fn decimals(&self, _rpc_client: &UtxoRpcClientEnum) -> Result { - Ok(self.conf()["decimals"].as_u64().unwrap_or(8) as u8) - } - - async fn tx_fee(&self, rpc_client: &UtxoRpcClientEnum) -> Result { - let tx_fee = match self.conf()["txfee"].as_u64() { - None => TxFee::FixedPerKb(1000), - Some(0) => { - let fee_method = match &rpc_client { - UtxoRpcClientEnum::Electrum(_) => EstimateFeeMethod::Standard, - UtxoRpcClientEnum::Native(client) => try_s!(client.detect_fee_method().compat().await), - }; - TxFee::Dynamic(fee_method) - }, - Some(fee) => TxFee::FixedPerKb(fee), - }; - Ok(tx_fee) - } - - fn initial_history_state(&self) -> HistorySyncState { - if self.req()["tx_history"].as_bool().unwrap_or(false) { - HistorySyncState::NotStarted - } else { - HistorySyncState::NotEnabled - } - } - - async fn rpc_client(&self) -> Result { - match self.req()["method"].as_str() { - Some("enable") => { - #[cfg(target_arch = "wasm32")] - { - ERR!("Native UTXO mode is only supported in native mode") - } - #[cfg(not(target_arch = "wasm32"))] - { - let native = try_s!(self.native_client()); - Ok(UtxoRpcClientEnum::Native(native)) - } - }, - Some("electrum") => { - let electrum = try_s!(self.electrum_client(ElectrumBuilderArgs::default()).await); - Ok(UtxoRpcClientEnum::Electrum(electrum)) - }, - _ => ERR!("Expected enable or electrum request"), - } - } - - async fn electrum_client(&self, args: ElectrumBuilderArgs) -> Result { - let (on_connect_tx, on_connect_rx) = mpsc::unbounded(); - let ticker = self.ticker().to_owned(); - let ctx = self.ctx(); - let mut event_handlers = vec![]; - if args.collect_metrics { - event_handlers.push( - CoinTransportMetrics::new(ctx.metrics.weak(), ticker.clone(), RpcClientType::Electrum).into_shared(), - ); - } - - if args.negotiate_version { - event_handlers.push(ElectrumProtoVerifier { on_connect_tx }.into_shared()); - } - - let mut servers: Vec = try_s!(json::from_value(self.req()["servers"].clone())); - let mut rng = small_rng(); - servers.as_mut_slice().shuffle(&mut rng); - let client = ElectrumClientImpl::new(ticker, event_handlers); - for server in servers.iter() { - match client.add_server(server).await { - Ok(_) => (), - Err(e) => log!("Error " (e) " connecting to " [server] ". Address won't be used"), - }; - } - - let mut attempts = 0i32; - while !client.is_connected().await { - if attempts >= 10 { - return ERR!("Failed to connect to at least 1 of {:?} in 5 seconds.", servers); - } - - Timer::sleep(0.5).await; - attempts += 1; - } - - let client = Arc::new(client); - - if args.negotiate_version { - let weak_client = Arc::downgrade(&client); - let client_name = format!("{} GUI/MM2 {}", ctx.gui().unwrap_or("UNKNOWN"), ctx.mm_version()); - spawn_electrum_version_loop(weak_client, on_connect_rx, client_name); - - try_s!(wait_for_protocol_version_checked(&client).await); - } - - if args.spawn_ping { - let weak_client = Arc::downgrade(&client); - spawn_electrum_ping_loop(weak_client, servers); +#[derive(Debug)] +pub struct UtxoHDWallet { + pub hd_wallet_storage: HDWalletCoinStorage, + pub address_format: UtxoAddressFormat, + /// Derivation path of the coin. + /// This derivation path consists of `purpose` and `coin_type` only + /// where the full `BIP44` address has the following structure: + /// `m/purpose'/coin_type'/account'/change/address_index`. + pub derivation_path: Bip44PathToCoin, + /// User accounts. + pub accounts: HDAccountsMutex, + pub gap_limit: u32, +} + +impl HDWalletOps for UtxoHDWallet { + type HDAccount = UtxoHDAccount; + + fn coin_type(&self) -> u32 { self.derivation_path.coin_type() } + + fn gap_limit(&self) -> u32 { self.gap_limit } + + fn get_accounts_mutex(&self) -> &HDAccountsMutex { &self.accounts } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct UtxoHDAccount { + pub account_id: u32, + /// [Extended public key](https://learnmeabitcoin.com/technical/extended-keys) that corresponds to the derivation path: + /// `m/purpose'/coin_type'/account'`. + pub extended_pubkey: Secp256k1ExtendedPublicKey, + /// [`UtxoHDWallet::derivation_path`] derived by [`UtxoHDAccount::account_id`]. + pub account_derivation_path: Bip44PathToAccount, + /// The number of addresses that we know have been used by the user. + /// This is used in order not to check the transaction history for each address, + /// but to request the balance of addresses whose index is less than `address_number`. + pub external_addresses_number: u32, + pub internal_addresses_number: u32, +} + +impl HDAccountOps for UtxoHDAccount { + fn known_addresses_number(&self, chain: Bip44Chain) -> MmResult { + match chain { + Bip44Chain::External => Ok(self.external_addresses_number), + Bip44Chain::Internal => Ok(self.internal_addresses_number), } - - Ok(ElectrumClient(client)) } - #[cfg(not(target_arch = "wasm32"))] - fn native_client(&self) -> Result { - use base64::{encode_config as base64_encode, URL_SAFE}; - - let native_conf_path = try_s!(self.confpath()); - let network = try_s!(self.network()); - let (rpc_port, rpc_user, rpc_password) = try_s!(read_native_mode_conf(&native_conf_path, &network)); - let auth_str = fomat!((rpc_user)":"(rpc_password)); - let rpc_port = match rpc_port { - Some(p) => p, - None => try_s!(self.conf()["rpcport"].as_u64().ok_or(ERRL!( - "Rpc port is not set neither in `coins` file nor in native daemon config" - ))) as u16, - }; - - let ctx = self.ctx(); - let coin_ticker = self.ticker().to_owned(); - let event_handlers = - vec![ - CoinTransportMetrics::new(ctx.metrics.weak(), coin_ticker.clone(), RpcClientType::Native).into_shared(), - ]; - let client = Arc::new(NativeClientImpl { - coin_ticker, - uri: fomat!("http://127.0.0.1:"(rpc_port)), - auth: format!("Basic {}", base64_encode(&auth_str, URL_SAFE)), - event_handlers, - request_id: 0u64.into(), - list_unspent_concurrent_map: ConcurrentRequestMap::new(), - }); + fn account_derivation_path(&self) -> DerivationPath { self.account_derivation_path.to_derivation_path() } - Ok(NativeClient(client)) - } - - #[cfg(not(target_arch = "wasm32"))] - fn confpath(&self) -> Result { - let conf = self.conf(); - // Documented at https://github.com/jl777/coins#bitcoin-protocol-specific-json - // "USERHOME/" prefix should be replaced with the user's home folder. - let declared_confpath = match self.conf()["confpath"].as_str() { - Some(path) if !path.is_empty() => path.trim(), - _ => { - let (name, is_asset_chain) = { - match conf["asset"].as_str() { - Some(a) => (a, true), - None => ( - try_s!(conf["name"].as_str().ok_or("'name' field is not found in config")), - false, - ), - } - }; - let data_dir = coin_daemon_data_dir(name, is_asset_chain); - let confname = format!("{}.conf", name); - - return Ok(data_dir.join(&confname[..])); - }, - }; - - let (confpath, rel_to_home) = match declared_confpath.strip_prefix("~/") { - Some(stripped) => (stripped, true), - None => match declared_confpath.strip_prefix("USERHOME/") { - Some(stripped) => (stripped, true), - None => (declared_confpath, false), - }, - }; - - if rel_to_home { - let home = try_s!(home_dir().ok_or("Can not detect the user home directory")); - Ok(home.join(confpath)) - } else { - Ok(confpath.into()) - } - } - - fn tx_hash_algo(&self) -> TxHashAlgo { - if self.ticker() == "GRS" { - TxHashAlgo::SHA256 - } else { - TxHashAlgo::DSHA256 - } - } + fn account_id(&self) -> u32 { self.account_id } } -/// Ping the electrum servers every 30 seconds to prevent them from disconnecting us. -/// According to docs server can do it if there are no messages in ~10 minutes. -/// https://electrumx.readthedocs.io/en/latest/protocol-methods.html?highlight=keep#server-ping -/// Weak reference will allow to stop the thread if client is dropped. -fn spawn_electrum_ping_loop(weak_client: Weak, servers: Vec) { - spawn(async move { - loop { - if let Some(client) = weak_client.upgrade() { - if let Err(e) = ElectrumClient(client).server_ping().compat().await { - log!("Electrum servers " [servers] " ping error " [e]); - } - } else { - log!("Electrum servers " [servers] " ping loop stopped"); - break; - } - Timer::sleep(30.).await - } - }); -} - -async fn check_electrum_server_version( - weak_client: Weak, - client_name: String, - electrum_addr: String, -) { - // client.remove_server() is called too often - async fn remove_server(client: ElectrumClient, electrum_addr: &str) { - if let Err(e) = client.remove_server(electrum_addr).await { - log!("Error on remove server "[e]); - } - } - - if let Some(c) = weak_client.upgrade() { - let client = ElectrumClient(c); - let available_protocols = client.protocol_version(); - let version = match client - .server_version(&electrum_addr, &client_name, available_protocols) - .compat() - .await - { - Ok(version) => version, - Err(e) => { - log!("Electrum " (electrum_addr) " server.version error \"" [e] "\"."); - if !e.error.is_transport() { - remove_server(client, &electrum_addr).await; - }; - return; - }, - }; - - // check if the version is allowed - let actual_version = match version.protocol_version.parse::() { - Ok(v) => v, - Err(e) => { - log!("Error on parse protocol_version "[e]); - remove_server(client, &electrum_addr).await; - return; - }, - }; - - if !available_protocols.contains(&actual_version) { - log!("Received unsupported protocol version " [actual_version] " from " [electrum_addr] ". Remove the connection"); - remove_server(client, &electrum_addr).await; - return; - } +impl UtxoHDAccount { + pub fn try_from_storage_item( + wallet_der_path: &Bip44PathToCoin, + account_info: &HDAccountStorageItem, + ) -> HDWalletStorageResult { + const ACCOUNT_CHILD_HARDENED: bool = true; - match client.set_protocol_version(&electrum_addr, actual_version).await { - Ok(()) => { - log!("Use protocol version " [actual_version] " for Electrum " [electrum_addr]); - }, - Err(e) => { - log!("Error on set protocol_version "[e]); - }, - }; + let account_child = ChildNumber::new(account_info.account_id, ACCOUNT_CHILD_HARDENED)?; + let account_derivation_path = wallet_der_path + .derive(account_child) + .map_to_mm(Bip44DerPathError::from)?; + let extended_pubkey = Secp256k1ExtendedPublicKey::from_str(&account_info.account_xpub)?; + Ok(UtxoHDAccount { + account_id: account_info.account_id, + extended_pubkey, + account_derivation_path, + external_addresses_number: account_info.external_addresses_number, + internal_addresses_number: account_info.internal_addresses_number, + }) } -} - -/// Follow the `on_connect_rx` stream and verify the protocol version of each connected electrum server. -/// https://electrumx.readthedocs.io/en/latest/protocol-methods.html?highlight=keep#server-version -/// Weak reference will allow to stop the thread if client is dropped. -fn spawn_electrum_version_loop( - weak_client: Weak, - mut on_connect_rx: mpsc::UnboundedReceiver, - client_name: String, -) { - spawn(async move { - while let Some(electrum_addr) = on_connect_rx.next().await { - spawn(check_electrum_server_version( - weak_client.clone(), - client_name.clone(), - electrum_addr, - )); - } - - log!("Electrum server.version loop stopped"); - }); -} - -/// Wait until the protocol version of at least one client's Electrum is checked. -async fn wait_for_protocol_version_checked(client: &ElectrumClientImpl) -> Result<(), String> { - let mut attempts = 0; - loop { - if attempts >= 10 { - return ERR!("Failed protocol version verifying of at least 1 of Electrums in 5 seconds."); - } - - if client.count_connections().await == 0 { - // All of the connections were removed because of server.version checking - return ERR!( - "There are no Electrums with the required protocol version {:?}", - client.protocol_version() - ); - } - if client.is_protocol_version_checked().await { - break; + pub fn to_storage_item(&self) -> HDAccountStorageItem { + HDAccountStorageItem { + account_id: self.account_id, + account_xpub: self.extended_pubkey.to_string(bip32::Prefix::XPUB), + external_addresses_number: self.external_addresses_number, + internal_addresses_number: self.internal_addresses_number, } - - Timer::sleep(0.5).await; - attempts += 1; } - - Ok(()) } /// Function calculating KMD interest @@ -1728,25 +1499,22 @@ pub struct KmdRewardsInfoElement { /// Get rewards info of unspent outputs. /// The list is ordered by the output value. -pub async fn kmd_rewards_info(coin: &T) -> Result, String> -where - T: AsRef + UtxoCommonOps, -{ +pub async fn kmd_rewards_info(coin: &T) -> Result, String> { if coin.as_ref().conf.ticker != "KMD" { return ERR!("rewards info can be obtained for KMD only"); } let utxo = coin.as_ref(); + let my_address = try_s!(utxo.derivation_method.iguana_or_err()); let rpc_client = &utxo.rpc_client; - let mut unspents = try_s!(rpc_client.list_unspent(&utxo.my_address, utxo.decimals).compat().await); - // list_unspent_ordered() returns ordered from lowest to highest by value unspent outputs. - // reverse it to reorder from highest to lowest outputs. - unspents.reverse(); + let mut unspents = try_s!(rpc_client.list_unspent(my_address, utxo.decimals).compat().await); + // Reorder from highest to lowest unspent outputs. + unspents.sort_unstable_by(|x, y| y.value.cmp(&x.value)); let mut result = Vec::with_capacity(unspents.len()); for unspent in unspents { let tx_hash: H256Json = unspent.outpoint.hash.reversed().into(); - let tx_info = try_s!(rpc_client.get_verbose_transaction(tx_hash.clone()).compat().await); + let tx_info = try_s!(rpc_client.get_verbose_transaction(&tx_hash).compat().await); let value = unspent.value; let locktime = tx_info.locktime as u64; @@ -1797,255 +1565,81 @@ pub fn sat_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResu }) } -pub(crate) fn sign_tx( - unsigned: TransactionInputSigner, - key_pair: &KeyPair, - prev_script: Script, - signature_version: SignatureVersion, - fork_id: u32, -) -> Result { - let mut signed_inputs = vec![]; - match signature_version { - SignatureVersion::WitnessV0 => { - for (i, _) in unsigned.inputs.iter().enumerate() { - signed_inputs.push(try_s!(p2wpkh_spend( - &unsigned, - i, - key_pair, - &prev_script, - signature_version, - fork_id - ))); - } - }, - _ => { - for (i, _) in unsigned.inputs.iter().enumerate() { - signed_inputs.push(try_s!(p2pkh_spend( - &unsigned, - i, - key_pair, - &prev_script, - signature_version, - fork_id - ))); - } - }, - } - - Ok(UtxoTx { - inputs: signed_inputs, - n_time: unsigned.n_time, - outputs: unsigned.outputs.clone(), - version: unsigned.version, - overwintered: unsigned.overwintered, - lock_time: unsigned.lock_time, - expiry_height: unsigned.expiry_height, - join_splits: vec![], - shielded_spends: vec![], - shielded_outputs: vec![], - value_balance: 0, - version_group_id: unsigned.version_group_id, - binding_sig: H512::default(), - join_split_sig: H512::default(), - join_split_pubkey: H256::default(), - zcash: unsigned.zcash, - str_d_zeel: unsigned.str_d_zeel, - tx_hash_algo: unsigned.hash_algo.into(), - }) -} - -async fn send_outputs_from_my_address_impl(coin: T, outputs: Vec) -> Result +async fn send_outputs_from_my_address_impl( + coin: T, + outputs: Vec, +) -> Result where - T: AsRef + UtxoCommonOps, + T: UtxoCommonOps + GetUtxoListOps, { - let (unspents, recently_sent_txs) = try_s!(coin.list_unspent_ordered(&coin.as_ref().my_address).await); - generate_and_send_tx(&coin, unspents, outputs, FeePolicy::SendExact, recently_sent_txs).await + let my_address = try_tx_s!(coin.as_ref().derivation_method.iguana_or_err()); + let (unspents, recently_sent_txs) = try_tx_s!(coin.get_unspent_ordered_list(my_address).await); + generate_and_send_tx(&coin, unspents, None, FeePolicy::SendExact, recently_sent_txs, outputs).await } /// Generates and sends tx using unspents and outputs adding new record to the recently_spent in case of success async fn generate_and_send_tx( coin: &T, unspents: Vec, - outputs: Vec, + required_inputs: Option>, fee_policy: FeePolicy, - mut recently_spent: AsyncMutexGuard<'_, RecentlySpentOutPoints>, -) -> Result + mut recently_spent: RecentlySpentOutPointsGuard<'_>, + outputs: Vec, +) -> Result where - T: AsRef + UtxoCommonOps, + T: AsRef + UtxoTxGenerationOps + UtxoTxBroadcastOps, { - let (unsigned, _) = try_s!( - coin.generate_transaction(unspents, outputs, fee_policy, None, None) - .await - ); + let my_address = try_tx_s!(coin.as_ref().derivation_method.iguana_or_err()); + let key_pair = try_tx_s!(coin.as_ref().priv_key_policy.key_pair_or_err()); + + let mut builder = UtxoTxBuilder::new(coin) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_fee_policy(fee_policy); + if let Some(required) = required_inputs { + builder = builder.add_required_inputs(required); + } + let (unsigned, _) = try_tx_s!(builder.build().await); let spent_unspents = unsigned .inputs .iter() .map(|input| UnspentInfo { - outpoint: input.previous_output.clone(), + outpoint: input.previous_output, value: input.amount, height: None, }) .collect(); - let signature_version = match &coin.as_ref().my_address.addr_format { + let signature_version = match &my_address.addr_format { UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, _ => coin.as_ref().conf.signature_version, }; - let prev_script = Builder::build_p2pkh(&coin.as_ref().my_address.hash); - let signed = try_s!(sign_tx( + let prev_script = Builder::build_p2pkh(&my_address.hash); + let signed = try_tx_s!(sign_tx( unsigned, - &coin.as_ref().key_pair, + key_pair, prev_script, signature_version, coin.as_ref().conf.fork_id )); - try_s!( - coin.as_ref() - .rpc_client - .send_transaction(&signed) - .map_err(|e| ERRL!("{}", e)) - .compat() - .await - ); + try_tx_s!(coin.broadcast_tx(&signed).await, signed); recently_spent.add_spent(spent_unspents, signed.hash(), signed.outputs.clone()); Ok(signed) } -/// Creates signed input spending p2pkh output -pub fn p2pkh_spend( - signer: &TransactionInputSigner, - input_index: usize, - key_pair: &KeyPair, - prev_script: &Script, - signature_version: SignatureVersion, - fork_id: u32, -) -> Result { - let script = Builder::build_p2pkh(&key_pair.public().address_hash()); - if script != *prev_script { - return ERR!( - "p2pkh script {} built from input key pair doesn't match expected prev script {}", - script, - prev_script - ); - } - let sighash_type = 1 | fork_id; - let sighash = signer.signature_hash( - input_index, - signer.inputs[input_index].amount, - &script, - signature_version, - sighash_type, - ); - - let script_sig = try_s!(script_sig_with_pub(&sighash, key_pair, fork_id)); - - Ok(TransactionInput { - script_sig, - sequence: signer.inputs[input_index].sequence, - script_witness: vec![], - previous_output: signer.inputs[input_index].previous_output.clone(), - }) -} - -/// Creates signed input spending p2pkh output -pub fn p2pk_spend( - signer: &TransactionInputSigner, - input_index: usize, - key_pair: &KeyPair, - signature_version: SignatureVersion, - fork_id: u32, -) -> Result { - let script = Builder::build_p2pk(key_pair.public()); - let sighash_type = 1 | fork_id; - let sighash = signer.signature_hash( - input_index, - signer.inputs[input_index].amount, - &script, - signature_version, - sighash_type, - ); - - let script_sig = try_s!(script_sig(&sighash, key_pair, fork_id)); - - Ok(TransactionInput { - script_sig: Builder::default().push_bytes(&script_sig).into_bytes(), - sequence: signer.inputs[input_index].sequence, - script_witness: vec![], - previous_output: signer.inputs[input_index].previous_output.clone(), - }) -} - -/// Creates signed input spending p2wpkh output -fn p2wpkh_spend( - signer: &TransactionInputSigner, - input_index: usize, - key_pair: &KeyPair, - prev_script: &Script, - signature_version: SignatureVersion, - fork_id: u32, -) -> Result { - let script = Builder::build_p2pkh(&key_pair.public().address_hash()); - - if script != *prev_script { - return ERR!( - "p2pkh script {} built from input key pair doesn't match expected prev script {}", - script, - prev_script - ); - } - let sighash_type = 1 | fork_id; - let sighash = signer.signature_hash( - input_index, - signer.inputs[input_index].amount, - &script, - signature_version, - sighash_type, - ); - - let sig_script = try_s!(script_sig(&sighash, key_pair, fork_id)); - - Ok(TransactionInput { - previous_output: signer.inputs[input_index].previous_output.clone(), - script_sig: Bytes::from(Vec::new()), - sequence: signer.inputs[input_index].sequence, - script_witness: vec![sig_script, Bytes::from(key_pair.public().deref())], - }) -} - -fn script_sig_with_pub(message: &H256, key_pair: &KeyPair, fork_id: u32) -> Result { - let sig_script = try_s!(script_sig(message, key_pair, fork_id)); - - let builder = Builder::default(); - - Ok(builder - .push_data(&sig_script) - .push_data(&key_pair.public().to_vec()) - .into_bytes()) -} - -fn script_sig(message: &H256, key_pair: &KeyPair, fork_id: u32) -> Result { - let signature = try_s!(key_pair.private().sign(message)); - - let mut sig_script = Bytes::default(); - sig_script.append(&mut Bytes::from((*signature).to_vec())); - // Using SIGHASH_ALL only for now - sig_script.append(&mut Bytes::from(vec![1 | fork_id as u8])); - - Ok(sig_script) -} - pub fn output_script(address: &Address, script_type: ScriptType) -> Script { match address.addr_format { - UtxoAddressFormat::Segwit => Builder::build_p2wpkh(&address.hash), + UtxoAddressFormat::Segwit => Builder::build_witness_script(&address.hash), _ => match script_type { ScriptType::P2PKH => Builder::build_p2pkh(&address.hash), ScriptType::P2SH => Builder::build_p2sh(&address.hash), - ScriptType::P2WPKH => Builder::build_p2wpkh(&address.hash), + ScriptType::P2WPKH => Builder::build_witness_script(&address.hash), + ScriptType::P2WSH => Builder::build_witness_script(&address.hash), }, } } @@ -2056,8 +1650,20 @@ pub fn address_by_conf_and_pubkey_str( pubkey: &str, addr_format: UtxoAddressFormat, ) -> Result { - let null = Json::Null; - let conf_builder = UtxoConfBuilder::new(conf, &null, coin); + // using a reasonable default here + let params = UtxoActivationParams { + mode: UtxoRpcMode::Native, + utxo_merge_params: None, + tx_history: false, + required_confirmations: None, + requires_notarization: None, + address_format: None, + gap_limit: None, + scan_policy: EnableCoinScanPolicy::default(), + priv_key_policy: PrivKeyActivationPolicy::IguanaPrivKey, + check_utxo_maturity: None, + }; + let conf_builder = UtxoConfBuilder::new(conf, ¶ms, coin); let utxo_conf = try_s!(conf_builder.build()); let pubkey_bytes = try_s!(hex::decode(pubkey)); let hash = dhash160(&pubkey_bytes); @@ -2065,10 +1671,27 @@ pub fn address_by_conf_and_pubkey_str( let address = Address { prefix: utxo_conf.pub_addr_prefix, t_addr_prefix: utxo_conf.pub_t_addr_prefix, - hash, + hash: hash.into(), checksum_type: utxo_conf.checksum_type, hrp: utxo_conf.bech32_hrp, addr_format, }; address.display_address() } + +fn parse_hex_encoded_u32(hex_encoded: &str) -> Result> { + let hex_encoded = hex_encoded.strip_prefix("0x").unwrap_or(hex_encoded); + let bytes = hex::decode(hex_encoded).map_to_mm(|e| e.to_string())?; + let be_bytes: [u8; 4] = bytes + .as_slice() + .try_into() + .map_to_mm(|e: TryFromSliceError| e.to_string())?; + Ok(u32::from_be_bytes(be_bytes)) +} + +#[test] +fn test_parse_hex_encoded_u32() { + assert_eq!(parse_hex_encoded_u32("0x892f2085"), Ok(2301567109)); + assert_eq!(parse_hex_encoded_u32("892f2085"), Ok(2301567109)); + assert_eq!(parse_hex_encoded_u32("0x7361707a"), Ok(1935765626)); +} diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs new file mode 100644 index 0000000000..1cbaea6a5e --- /dev/null +++ b/mm2src/coins/utxo/bch.rs @@ -0,0 +1,1451 @@ +use super::*; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, TxDetailsBuilder, TxHistoryStorage, TxHistoryStorageError}; +use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; +use crate::utxo::rpc_clients::UtxoRpcFut; +use crate::utxo::slp::{parse_slp_script, ParseSlpScriptError, SlpGenesisParams, SlpTokenInfo, SlpTransaction, + SlpUnspent}; +use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; +use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; +use crate::{BlockHeightAndTime, CanRefundHtlc, CoinBalance, CoinProtocol, NegotiateSwapContractAddrErr, + PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, + SwapOps, TradePreimageValue, TransactionFut, TransactionType, TxFeeDetails, UnexpectedDerivationMethod, + ValidateAddressResult, ValidatePaymentInput, VerificationResult, WithdrawFut}; +use common::log::warn; +use common::mm_metrics::MetricsArc; +use derive_more::Display; +use futures::{FutureExt, TryFutureExt}; +use itertools::Either as EitherIter; +use keys::hash::H256; +use keys::CashAddress; +pub use keys::NetworkPrefix as CashAddrPrefix; +use mm2_number::MmNumber; +use serde_json::{self as json, Value as Json}; +use serialization::{deserialize, CoinVariant}; +use std::sync::MutexGuard; + +pub type BchUnspentMap = HashMap; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BchActivationRequest { + #[serde(default)] + allow_slp_unsafe_conf: bool, + bchd_urls: Vec, + #[serde(flatten)] + pub utxo_params: UtxoActivationParams, +} + +#[derive(Debug, Display)] +pub enum BchFromLegacyReqErr { + InvalidUtxoParams(UtxoFromLegacyReqErr), + InvalidBchdUrls(json::Error), +} + +impl From for BchFromLegacyReqErr { + fn from(err: UtxoFromLegacyReqErr) -> Self { BchFromLegacyReqErr::InvalidUtxoParams(err) } +} + +impl BchActivationRequest { + pub fn from_legacy_req(req: &Json) -> Result> { + let bchd_urls = json::from_value(req["bchd_urls"].clone()).map_to_mm(BchFromLegacyReqErr::InvalidBchdUrls)?; + let allow_slp_unsafe_conf = req["allow_slp_unsafe_conf"].as_bool().unwrap_or_default(); + let utxo_params = UtxoActivationParams::from_legacy_req(req)?; + + Ok(BchActivationRequest { + allow_slp_unsafe_conf, + bchd_urls, + utxo_params, + }) + } +} + +#[derive(Clone, Debug)] +pub struct BchCoin { + utxo_arc: UtxoArc, + slp_addr_prefix: CashAddrPrefix, + bchd_urls: Vec, + slp_tokens_infos: Arc>>, +} + +#[allow(clippy::large_enum_variant)] +pub enum IsSlpUtxoError { + Rpc(UtxoRpcError), + TxDeserialization(serialization::Error), +} + +#[derive(Debug, Default)] +pub struct BchUnspents { + /// Standard BCH UTXOs + standard: Vec, + /// SLP related UTXOs + slp: HashMap>, + /// SLP minting batons outputs, DO NOT use them as MM2 doesn't support SLP minting by default + slp_batons: Vec, + /// The unspents of transaction with an undetermined protocol (OP_RETURN in 0 output but not SLP) + /// DO NOT ever use them to avoid burning users funds + undetermined: Vec, +} + +impl BchUnspents { + fn add_standard(&mut self, utxo: UnspentInfo) { self.standard.push(utxo) } + + fn add_slp(&mut self, token_id: H256, bch_unspent: UnspentInfo, slp_amount: u64) { + let slp_unspent = SlpUnspent { + bch_unspent, + slp_amount, + }; + self.slp.entry(token_id).or_insert_with(Vec::new).push(slp_unspent); + } + + fn add_slp_baton(&mut self, utxo: UnspentInfo) { self.slp_batons.push(utxo) } + + fn add_undetermined(&mut self, utxo: UnspentInfo) { self.undetermined.push(utxo) } + + pub fn platform_balance(&self, decimals: u8) -> CoinBalance { + let spendable_sat = total_unspent_value(&self.standard); + + let unspendable_slp = self.slp.iter().fold(0, |cur, (_, slp_unspents)| { + let bch_value = total_unspent_value(slp_unspents.iter().map(|slp| &slp.bch_unspent)); + cur + bch_value + }); + + let unspendable_slp_batons = total_unspent_value(&self.slp_batons); + let unspendable_undetermined = total_unspent_value(&self.undetermined); + + let total_unspendable = unspendable_slp + unspendable_slp_batons + unspendable_undetermined; + CoinBalance { + spendable: big_decimal_from_sat_unsigned(spendable_sat, decimals), + unspendable: big_decimal_from_sat_unsigned(total_unspendable, decimals), + } + } + + pub fn slp_token_balance(&self, token_id: &H256, decimals: u8) -> CoinBalance { + self.slp + .get(token_id) + .map(|unspents| { + let total_sat = unspents.iter().fold(0, |cur, unspent| cur + unspent.slp_amount); + CoinBalance { + spendable: big_decimal_from_sat_unsigned(total_sat, decimals), + unspendable: 0.into(), + } + }) + .unwrap_or_default() + } +} + +impl From for IsSlpUtxoError { + fn from(err: UtxoRpcError) -> IsSlpUtxoError { IsSlpUtxoError::Rpc(err) } +} + +impl From for IsSlpUtxoError { + fn from(err: serialization::Error) -> IsSlpUtxoError { IsSlpUtxoError::TxDeserialization(err) } +} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum GetTxDetailsError { + StorageError(E), + AddressesFromScriptError(String), + SlpTokenIdIsNotGenesisTx(H256), + TxDeserializationError(serialization::Error), + RpcError(UtxoRpcError), + ParseSlpScriptError(ParseSlpScriptError), + ToSlpAddressError(String), + InvalidSlpTransaction(H256), + AddressDerivationError(UnexpectedDerivationMethod), +} + +impl From for GetTxDetailsError { + fn from(err: UtxoRpcError) -> Self { GetTxDetailsError::RpcError(err) } +} + +impl From for GetTxDetailsError { + fn from(err: E) -> Self { GetTxDetailsError::StorageError(err) } +} + +impl From for GetTxDetailsError { + fn from(err: serialization::Error) -> Self { GetTxDetailsError::TxDeserializationError(err) } +} + +impl From for GetTxDetailsError { + fn from(err: ParseSlpScriptError) -> Self { GetTxDetailsError::ParseSlpScriptError(err) } +} + +impl From for GetTxDetailsError { + fn from(err: UnexpectedDerivationMethod) -> Self { GetTxDetailsError::AddressDerivationError(err) } +} + +impl BchCoin { + pub fn slp_prefix(&self) -> &CashAddrPrefix { &self.slp_addr_prefix } + + pub fn slp_address(&self, address: &Address) -> Result { + let conf = &self.as_ref().conf; + address.to_cashaddress( + &self.slp_prefix().to_string(), + conf.pub_addr_prefix, + conf.p2sh_addr_prefix, + ) + } + + pub fn bchd_urls(&self) -> &[String] { &self.bchd_urls } + + async fn utxos_into_bch_unspents(&self, utxos: Vec) -> UtxoRpcResult { + let mut result = BchUnspents::default(); + let mut temporary_undetermined = Vec::new(); + + let to_verbose: HashSet = utxos + .into_iter() + .filter_map(|unspent| { + if unspent.outpoint.index == 0 { + // Zero output is reserved for OP_RETURN of specific protocols + // so if we get it we can safely consider this as standard BCH UTXO. + // There is no need to request verbose transaction for such UTXO. + result.add_standard(unspent); + None + } else { + let hash = unspent.outpoint.hash.reversed().into(); + temporary_undetermined.push(unspent); + Some(hash) + } + }) + .collect(); + + let verbose_txs = self + .get_verbose_transactions_from_cache_or_rpc(to_verbose) + .compat() + .await?; + + for unspent in temporary_undetermined { + let prev_tx_hash = unspent.outpoint.hash.reversed().into(); + let prev_tx_bytes = verbose_txs + .get(&prev_tx_hash) + .or_mm_err(|| { + UtxoRpcError::Internal(format!( + "'get_verbose_transactions_from_cache_or_rpc' should have returned '{:?}'", + prev_tx_hash + )) + })? + .to_inner(); + let prev_tx: UtxoTx = match deserialize(prev_tx_bytes.hex.as_slice()) { + Ok(b) => b, + Err(e) => { + warn!( + "Failed to deserialize prev_tx {:?} with error {:?}, considering {:?} as undetermined", + prev_tx_bytes, e, unspent + ); + result.add_undetermined(unspent); + continue; + }, + }; + + if prev_tx.outputs.is_empty() { + warn!( + "Prev_tx {:?} outputs are empty, considering {:?} as undetermined", + prev_tx_bytes, unspent + ); + result.add_undetermined(unspent); + continue; + } + + let zero_out_script: Script = prev_tx.outputs[0].script_pubkey.clone().into(); + if zero_out_script.is_pay_to_public_key() + || zero_out_script.is_pay_to_public_key_hash() + || zero_out_script.is_pay_to_script_hash() + { + result.add_standard(unspent); + } else { + match parse_slp_script(&prev_tx.outputs[0].script_pubkey) { + Ok(slp_data) => match slp_data.transaction { + SlpTransaction::Send { token_id, amounts } => { + match amounts.get(unspent.outpoint.index as usize - 1) { + Some(slp_amount) => result.add_slp(token_id, unspent, *slp_amount), + None => result.add_standard(unspent), + } + }, + SlpTransaction::Genesis(genesis) => { + if unspent.outpoint.index == 1 { + let token_id = prev_tx.hash().reversed(); + result.add_slp(token_id, unspent, genesis.initial_token_mint_quantity); + } else if Some(unspent.outpoint.index) == genesis.mint_baton_vout.map(|u| u as u32) { + result.add_slp_baton(unspent); + } else { + result.add_standard(unspent); + } + }, + SlpTransaction::Mint { + token_id, + additional_token_quantity, + mint_baton_vout, + } => { + if unspent.outpoint.index == 1 { + result.add_slp(token_id, unspent, additional_token_quantity); + } else if Some(unspent.outpoint.index) == mint_baton_vout.map(|u| u as u32) { + result.add_slp_baton(unspent); + } else { + result.add_standard(unspent); + } + }, + }, + Err(e) => { + warn!( + "Error {} parsing script {:?} as SLP, considering {:?} as undetermined", + e, prev_tx.outputs[0].script_pubkey, unspent + ); + result.undetermined.push(unspent); + }, + }; + } + } + Ok(result) + } + + /// Returns unspents to calculate balance, use for displaying purposes only! + /// DO NOT USE to build transactions, it can lead to double spending attempt and also have other unpleasant consequences + pub async fn bch_unspents_for_display(&self, address: &Address) -> UtxoRpcResult { + // ordering is not required to display balance to we can simply call "normal" list_unspent + let all_unspents = self + .utxo_arc + .rpc_client + .list_unspent(address, self.utxo_arc.decimals) + .compat() + .await?; + self.utxos_into_bch_unspents(all_unspents).await + } + + /// Locks recently spent cache to safely return UTXOs for spending + pub async fn bch_unspents_for_spend( + &self, + address: &Address, + ) -> UtxoRpcResult<(BchUnspents, RecentlySpentOutPointsGuard<'_>)> { + let (all_unspents, recently_spent) = utxo_common::get_unspent_ordered_list(self, address).await?; + let result = self.utxos_into_bch_unspents(all_unspents).await?; + + Ok((result, recently_spent)) + } + + pub async fn get_token_utxos_for_spend( + &self, + token_id: &H256, + ) -> UtxoRpcResult<(Vec, Vec, RecentlySpentOutPointsGuard<'_>)> { + let my_address = self + .as_ref() + .derivation_method + .iguana_or_err() + .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; + let (mut bch_unspents, recently_spent) = self.bch_unspents_for_spend(my_address).await?; + let (mut slp_unspents, standard_utxos) = ( + bch_unspents.slp.remove(token_id).unwrap_or_default(), + bch_unspents.standard, + ); + + slp_unspents.sort_by(|a, b| a.slp_amount.cmp(&b.slp_amount)); + Ok((slp_unspents, standard_utxos, recently_spent)) + } + + pub async fn get_token_utxos_for_display( + &self, + token_id: &H256, + ) -> UtxoRpcResult<(Vec, Vec)> { + let my_address = self + .as_ref() + .derivation_method + .iguana_or_err() + .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; + let mut bch_unspents = self.bch_unspents_for_display(my_address).await?; + let (mut slp_unspents, standard_utxos) = ( + bch_unspents.slp.remove(token_id).unwrap_or_default(), + bch_unspents.standard, + ); + + slp_unspents.sort_by(|a, b| a.slp_amount.cmp(&b.slp_amount)); + Ok((slp_unspents, standard_utxos)) + } + + pub fn add_slp_token_info(&self, ticker: String, info: SlpTokenInfo) { + self.slp_tokens_infos.lock().unwrap().insert(ticker, info); + } + + pub fn get_slp_tokens_infos(&self) -> MutexGuard<'_, HashMap> { + self.slp_tokens_infos.lock().unwrap() + } + + pub fn get_my_slp_address(&self) -> Result { + let my_address = try_s!(self.as_ref().derivation_method.iguana_or_err()); + let slp_address = my_address.to_cashaddress( + &self.slp_prefix().to_string(), + self.as_ref().conf.pub_addr_prefix, + self.as_ref().conf.p2sh_addr_prefix, + )?; + Ok(slp_address) + } + + async fn tx_from_storage_or_rpc( + &self, + tx_hash: &H256Json, + storage: &T, + ) -> Result>> { + let tx_hash_str = format!("{:02x}", tx_hash); + let wallet_id = self.history_wallet_id(); + let tx_bytes = match storage.tx_bytes_from_cache(&wallet_id, &tx_hash_str).await? { + Some(tx_bytes) => tx_bytes, + None => { + let tx_bytes = self.as_ref().rpc_client.get_transaction_bytes(tx_hash).compat().await?; + storage.add_tx_to_cache(&wallet_id, &tx_hash_str, &tx_bytes).await?; + tx_bytes + }, + }; + let tx = deserialize(tx_bytes.0.as_slice())?; + Ok(tx) + } + + /// Returns multiple details by tx hash if token transfers also occurred in the transaction + pub async fn transaction_details_with_token_transfers( + &self, + tx_hash: &H256Json, + block_height_and_time: Option, + storage: &T, + ) -> Result, MmError>> { + let tx = self.tx_from_storage_or_rpc(tx_hash, storage).await?; + + let bch_tx_details = self + .bch_tx_details(tx_hash, &tx, block_height_and_time, storage) + .await?; + let maybe_op_return: Script = tx.outputs[0].script_pubkey.clone().into(); + if !(maybe_op_return.is_pay_to_public_key_hash() + || maybe_op_return.is_pay_to_public_key() + || maybe_op_return.is_pay_to_script_hash()) + { + if let Ok(slp_details) = parse_slp_script(&maybe_op_return) { + let slp_tx_details = self + .slp_tx_details( + &tx, + slp_details.transaction, + block_height_and_time, + bch_tx_details.fee_details.clone(), + storage, + ) + .await?; + return Ok(vec![bch_tx_details, slp_tx_details]); + } + } + + Ok(vec![bch_tx_details]) + } + + async fn bch_tx_details( + &self, + tx_hash: &H256Json, + tx: &UtxoTx, + height_and_time: Option, + storage: &T, + ) -> Result>> { + let my_address = self.as_ref().derivation_method.iguana_or_err()?; + let my_addresses = [my_address.clone()]; + let mut tx_builder = TxDetailsBuilder::new(self.ticker().to_owned(), tx, height_and_time, my_addresses); + for output in &tx.outputs { + let addresses = match self.addresses_from_script(&output.script_pubkey.clone().into()) { + Ok(a) => a, + Err(_) => continue, + }; + + if addresses.is_empty() { + continue; + } + + if addresses.len() != 1 { + let msg = format!( + "{} tx {:02x} output script resulted into unexpected number of addresses", + self.ticker(), + tx_hash, + ); + return MmError::err(GetTxDetailsError::AddressesFromScriptError(msg)); + } + + let amount = big_decimal_from_sat_unsigned(output.value, self.decimals()); + for address in addresses { + tx_builder.transferred_to(address, &amount); + } + } + + let mut total_input = 0; + for input in &tx.inputs { + let index = input.previous_output.index; + let prev_tx = self + .tx_from_storage_or_rpc(&input.previous_output.hash.reversed().into(), storage) + .await?; + let prev_script = prev_tx.outputs[index as usize].script_pubkey.clone().into(); + let addresses = self + .addresses_from_script(&prev_script) + .map_to_mm(GetTxDetailsError::AddressesFromScriptError)?; + if addresses.len() != 1 { + let msg = format!( + "{} tx {:02x} output script resulted into unexpected number of addresses", + self.ticker(), + tx_hash, + ); + return MmError::err(GetTxDetailsError::AddressesFromScriptError(msg)); + } + + let prev_value = prev_tx.outputs[index as usize].value; + total_input += prev_value; + let amount = big_decimal_from_sat_unsigned(prev_value, self.decimals()); + for address in addresses { + tx_builder.transferred_from(address, &amount); + } + } + + let total_output = tx.outputs.iter().fold(0, |total, output| total + output.value); + let fee = Some(TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some(self.ticker().into()), + amount: big_decimal_from_sat_unsigned(total_input - total_output, self.decimals()), + })); + tx_builder.set_tx_fee(fee); + Ok(tx_builder.build()) + } + + async fn get_slp_genesis_params( + &self, + token_id: H256, + storage: &T, + ) -> Result>> { + let token_genesis_tx = self.tx_from_storage_or_rpc(&token_id.into(), storage).await?; + let maybe_genesis_script: Script = token_genesis_tx.outputs[0].script_pubkey.clone().into(); + let slp_details = parse_slp_script(&maybe_genesis_script)?; + match slp_details.transaction { + SlpTransaction::Genesis(params) => Ok(params), + _ => MmError::err(GetTxDetailsError::SlpTokenIdIsNotGenesisTx(token_id)), + } + } + + async fn slp_transferred_amounts( + &self, + utxo_tx: &UtxoTx, + slp_tx: SlpTransaction, + storage: &T, + ) -> Result, MmError>> { + let slp_amounts = match slp_tx { + SlpTransaction::Send { token_id, amounts } => { + let genesis_params = self.get_slp_genesis_params(token_id, storage).await?; + EitherIter::Left( + amounts + .into_iter() + .map(move |amount| big_decimal_from_sat_unsigned(amount, genesis_params.decimals[0])), + ) + }, + SlpTransaction::Mint { + token_id, + additional_token_quantity, + .. + } => { + let slp_genesis_params = self.get_slp_genesis_params(token_id, storage).await?; + EitherIter::Right(std::iter::once(big_decimal_from_sat_unsigned( + additional_token_quantity, + slp_genesis_params.decimals[0], + ))) + }, + SlpTransaction::Genesis(genesis_params) => EitherIter::Right(std::iter::once( + big_decimal_from_sat_unsigned(genesis_params.initial_token_mint_quantity, genesis_params.decimals[0]), + )), + }; + + let mut result = HashMap::new(); + for (i, amount) in slp_amounts.into_iter().enumerate() { + let output_index = i + 1; + match utxo_tx.outputs.get(output_index) { + Some(output) => { + let addresses = self + .addresses_from_script(&output.script_pubkey.clone().into()) + .map_to_mm(GetTxDetailsError::AddressesFromScriptError)?; + if addresses.len() != 1 { + let msg = format!( + "{} tx {:?} output script resulted into unexpected number of addresses", + self.ticker(), + utxo_tx.hash().reversed(), + ); + return MmError::err(GetTxDetailsError::AddressesFromScriptError(msg)); + } + + let slp_address = self + .slp_address(&addresses[0]) + .map_to_mm(GetTxDetailsError::ToSlpAddressError)?; + result.insert(output_index, (slp_address, amount)); + }, + None => return MmError::err(GetTxDetailsError::InvalidSlpTransaction(utxo_tx.hash().reversed())), + } + } + Ok(result) + } + + async fn slp_tx_details( + &self, + tx: &UtxoTx, + slp_tx: SlpTransaction, + height_and_time: Option, + tx_fee: Option, + storage: &Storage, + ) -> Result>> { + let token_id = match slp_tx.token_id() { + Some(id) => id, + None => tx.hash().reversed(), + }; + + let my_address = self.as_ref().derivation_method.iguana_or_err()?; + let slp_address = self + .slp_address(my_address) + .map_to_mm(GetTxDetailsError::ToSlpAddressError)?; + let addresses = [slp_address]; + + let mut slp_tx_details_builder = + TxDetailsBuilder::new(self.ticker().to_owned(), tx, height_and_time, addresses); + let slp_transferred_amounts = self.slp_transferred_amounts(tx, slp_tx, storage).await?; + for (_, (address, amount)) in slp_transferred_amounts { + slp_tx_details_builder.transferred_to(address, &amount); + } + + for input in &tx.inputs { + let prev_tx = self + .tx_from_storage_or_rpc(&input.previous_output.hash.reversed().into(), storage) + .await?; + if let Ok(slp_tx_details) = parse_slp_script(&prev_tx.outputs[0].script_pubkey) { + let mut prev_slp_transferred = self + .slp_transferred_amounts(&prev_tx, slp_tx_details.transaction, storage) + .await?; + let i = input.previous_output.index as usize; + if let Some((address, amount)) = prev_slp_transferred.remove(&i) { + slp_tx_details_builder.transferred_from(address, &amount); + } + } + } + + slp_tx_details_builder.set_transaction_type(TransactionType::TokenTransfer(token_id.take().to_vec().into())); + slp_tx_details_builder.set_tx_fee(tx_fee); + + Ok(slp_tx_details_builder.build()) + } + + pub async fn get_block_timestamp(&self, height: u64) -> Result> { + self.as_ref().rpc_client.get_block_timestamp(height).await + } +} + +impl AsRef for BchCoin { + fn as_ref(&self) -> &UtxoCoinFields { &self.utxo_arc } +} + +pub async fn bch_coin_from_conf_and_params( + ctx: &MmArc, + ticker: &str, + conf: &Json, + params: BchActivationRequest, + slp_addr_prefix: CashAddrPrefix, + priv_key: &[u8], +) -> Result { + if params.bchd_urls.is_empty() && !params.allow_slp_unsafe_conf { + return Err("Using empty bchd_urls is unsafe for SLP users!".into()); + } + + let bchd_urls = params.bchd_urls; + let slp_tokens_infos = Arc::new(Mutex::new(HashMap::new())); + let constructor = { + move |utxo_arc| BchCoin { + utxo_arc, + slp_addr_prefix: slp_addr_prefix.clone(), + bchd_urls: bchd_urls.clone(), + slp_tokens_infos: slp_tokens_infos.clone(), + } + }; + + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(priv_key); + let coin = try_s!( + UtxoArcBuilder::new(ctx, ticker, conf, ¶ms.utxo_params, priv_key_policy, constructor) + .build() + .await + ); + Ok(coin) +} + +#[derive(Debug)] +pub enum BchActivationError { + CoinInitError(String), + TokenConfIsNotFound { + token: String, + }, + TokenCoinProtocolParseError { + token: String, + error: json::Error, + }, + TokenCoinProtocolIsNotSlp { + token: String, + protocol: CoinProtocol, + }, + TokenPlatformCoinIsInvalidInConf { + token: String, + expected_platform: String, + actual_platform: String, + }, + RpcError(UtxoRpcError), + SlpPrefixParseError(String), +} + +impl From for BchActivationError { + fn from(e: UtxoRpcError) -> Self { BchActivationError::RpcError(e) } +} + +// if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt +#[async_trait] +#[cfg_attr(test, mockable)] +impl UtxoTxBroadcastOps for BchCoin { + async fn broadcast_tx(&self, tx: &UtxoTx) -> Result> { + utxo_common::broadcast_tx(self, tx).await + } +} + +// if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt +#[async_trait] +#[cfg_attr(test, mockable)] +impl UtxoTxGenerationOps for BchCoin { + async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + + async fn calc_interest_if_required( + &self, + unsigned: TransactionInputSigner, + data: AdditionalTxData, + my_script_pub: Bytes, + ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { + utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub).await + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoListOps for BchCoin { + async fn get_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + let (bch_unspents, recently_spent) = self.bch_unspents_for_spend(address).await?; + Ok((bch_unspents.standard, recently_spent)) + } + + async fn get_all_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_list(self, address).await + } + + async fn get_mature_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)> { + let (unspents, recently_spent) = utxo_common::get_all_unspent_ordered_list(self, address).await?; + Ok((MatureUnspentList::new_mature(unspents), recently_spent)) + } +} + +// if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt +#[async_trait] +#[cfg_attr(test, mockable)] +impl UtxoCommonOps for BchCoin { + async fn get_htlc_spend_fee(&self, tx_size: u64) -> UtxoRpcResult { + utxo_common::get_htlc_spend_fee(self, tx_size).await + } + + fn addresses_from_script(&self, script: &Script) -> Result, String> { + utxo_common::addresses_from_script(self, script) + } + + fn denominate_satoshis(&self, satoshi: i64) -> f64 { utxo_common::denominate_satoshis(&self.utxo_arc, satoshi) } + + fn my_public_key(&self) -> Result<&Public, MmError> { + utxo_common::my_public_key(self.as_ref()) + } + + fn address_from_str(&self, address: &str) -> Result { + utxo_common::checked_address_from_str(self, address) + } + + async fn get_current_mtp(&self) -> UtxoRpcResult { + utxo_common::get_current_mtp(&self.utxo_arc, CoinVariant::Standard).await + } + + fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { + utxo_common::is_unspent_mature(self.utxo_arc.conf.mature_confirmations, output) + } + + async fn calc_interest_of_tx(&self, tx: &UtxoTx, input_transactions: &mut HistoryUtxoTxMap) -> UtxoRpcResult { + utxo_common::calc_interest_of_tx(self, tx, input_transactions).await + } + + async fn get_mut_verbose_transaction_from_map_or_rpc<'a, 'b>( + &'a self, + tx_hash: H256Json, + utxo_tx_map: &'b mut HistoryUtxoTxMap, + ) -> UtxoRpcResult<&'b mut HistoryUtxoTx> { + utxo_common::get_mut_verbose_transaction_from_map_or_rpc(self, tx_hash, utxo_tx_map).await + } + + async fn p2sh_spending_tx(&self, input: utxo_common::P2SHSpendingTxInput<'_>) -> Result { + utxo_common::p2sh_spending_tx(self, input).await + } + + fn get_verbose_transactions_from_cache_or_rpc( + &self, + tx_ids: HashSet, + ) -> UtxoRpcFut> { + let selfi = self.clone(); + let fut = async move { utxo_common::get_verbose_transactions_from_cache_or_rpc(&selfi.utxo_arc, tx_ids).await }; + Box::new(fut.boxed().compat()) + } + + async fn preimage_trade_fee_required_to_send_outputs( + &self, + outputs: Vec, + fee_policy: FeePolicy, + gas_fee: Option, + stage: &FeeApproxStage, + ) -> TradePreimageResult { + utxo_common::preimage_trade_fee_required_to_send_outputs( + self, + self.ticker(), + outputs, + fee_policy, + gas_fee, + stage, + ) + .await + } + + fn increase_dynamic_fee_by_stage(&self, dynamic_fee: u64, stage: &FeeApproxStage) -> u64 { + utxo_common::increase_dynamic_fee_by_stage(self, dynamic_fee, stage) + } + + async fn p2sh_tx_locktime(&self, htlc_locktime: u32) -> Result> { + utxo_common::p2sh_tx_locktime(self, &self.utxo_arc.conf.ticker, htlc_locktime).await + } + + fn addr_format(&self) -> &UtxoAddressFormat { utxo_common::addr_format(self) } + + fn addr_format_for_standard_scripts(&self) -> UtxoAddressFormat { + utxo_common::addr_format_for_standard_scripts(self) + } + + fn address_from_pubkey(&self, pubkey: &Public) -> Address { + let conf = &self.utxo_arc.conf; + let addr_format = self.addr_format().clone(); + utxo_common::address_from_pubkey( + pubkey, + conf.pub_addr_prefix, + conf.pub_t_addr_prefix, + conf.checksum_type, + conf.bech32_hrp.clone(), + addr_format, + ) + } +} + +#[async_trait] +impl SwapOps for BchCoin { + fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { + utxo_common::send_taker_fee(self.clone(), fee_addr, amount) + } + + fn send_maker_payment( + &self, + time_lock: u32, + taker_pub: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + _swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::send_maker_payment( + self.clone(), + time_lock, + taker_pub, + secret_hash, + amount, + swap_unique_data, + ) + } + + fn send_taker_payment( + &self, + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + _swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::send_taker_payment( + self.clone(), + time_lock, + maker_pub, + secret_hash, + amount, + swap_unique_data, + ) + } + + fn send_maker_spends_taker_payment( + &self, + taker_payment_tx: &[u8], + time_lock: u32, + taker_pub: &[u8], + secret: &[u8], + _swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::send_maker_spends_taker_payment( + self.clone(), + taker_payment_tx, + time_lock, + taker_pub, + secret, + swap_unique_data, + ) + } + + fn send_taker_spends_maker_payment( + &self, + maker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret: &[u8], + _swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::send_taker_spends_maker_payment( + self.clone(), + maker_payment_tx, + time_lock, + maker_pub, + secret, + swap_unique_data, + ) + } + + fn send_taker_refunds_payment( + &self, + taker_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + _swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::send_taker_refunds_payment( + self.clone(), + taker_tx, + time_lock, + maker_pub, + secret_hash, + swap_unique_data, + ) + } + + fn send_maker_refunds_payment( + &self, + maker_tx: &[u8], + time_lock: u32, + taker_pub: &[u8], + secret_hash: &[u8], + _swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::send_maker_refunds_payment( + self.clone(), + maker_tx, + time_lock, + taker_pub, + secret_hash, + swap_unique_data, + ) + } + + fn validate_fee( + &self, + fee_tx: &TransactionEnum, + expected_sender: &[u8], + fee_addr: &[u8], + amount: &BigDecimal, + min_block_number: u64, + _uuid: &[u8], + ) -> Box + Send> { + let tx = match fee_tx { + TransactionEnum::UtxoTx(tx) => tx.clone(), + _ => panic!(), + }; + utxo_common::validate_fee( + self.clone(), + tx, + utxo_common::DEFAULT_FEE_VOUT, + expected_sender, + amount, + min_block_number, + fee_addr, + ) + } + + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + utxo_common::validate_maker_payment(self, input) + } + + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + utxo_common::validate_taker_payment(self, input) + } + + fn check_if_my_payment_sent( + &self, + time_lock: u32, + other_pub: &[u8], + secret_hash: &[u8], + _search_from_block: u64, + _swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> Box, Error = String> + Send> { + utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash, swap_unique_data) + } + + async fn search_for_swap_tx_spend_my( + &self, + input: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + utxo_common::search_for_swap_tx_spend_my(self, input, utxo_common::DEFAULT_SWAP_VOUT).await + } + + async fn search_for_swap_tx_spend_other( + &self, + input: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + utxo_common::search_for_swap_tx_spend_other(self, input, utxo_common::DEFAULT_SWAP_VOUT).await + } + + fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + utxo_common::extract_secret(secret_hash, spend_tx) + } + + fn can_refund_htlc(&self, locktime: u64) -> Box + Send + '_> { + Box::new( + utxo_common::can_refund_htlc(self, locktime) + .boxed() + .map_err(|e| ERRL!("{}", e)) + .compat(), + ) + } + + fn negotiate_swap_contract_addr( + &self, + _other_side_address: Option<&[u8]>, + ) -> Result, MmError> { + Ok(None) + } + + fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { + utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) + } +} + +fn total_unspent_value<'a>(unspents: impl IntoIterator) -> u64 { + unspents.into_iter().fold(0, |cur, unspent| cur + unspent.value) +} + +impl MarketCoinOps for BchCoin { + fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } + + fn my_address(&self) -> Result { utxo_common::my_address(self) } + + fn get_public_key(&self) -> Result> { + let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; + Ok(pubkey.to_string()) + } + + fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { + utxo_common::sign_message_hash(self.as_ref(), message) + } + + fn sign_message(&self, message: &str) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message) + } + + fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { + utxo_common::verify_message(self, signature_base64, message, address) + } + + fn my_balance(&self) -> BalanceFut { + let coin = self.clone(); + let fut = async move { + let my_address = coin.as_ref().derivation_method.iguana_or_err()?; + let bch_unspents = coin.bch_unspents_for_display(my_address).await?; + Ok(bch_unspents.platform_balance(coin.as_ref().decimals)) + }; + Box::new(fut.boxed().compat()) + } + + fn base_coin_balance(&self) -> BalanceFut { utxo_common::base_coin_balance(self) } + + fn platform_ticker(&self) -> &str { self.ticker() } + + #[inline(always)] + fn send_raw_tx(&self, tx: &str) -> Box + Send> { + utxo_common::send_raw_tx(&self.utxo_arc, tx) + } + + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) + } + + fn wait_for_confirmations( + &self, + tx: &[u8], + confirmations: u64, + requires_nota: bool, + wait_until: u64, + check_every: u64, + ) -> Box + Send> { + utxo_common::wait_for_confirmations( + &self.utxo_arc, + tx, + confirmations, + requires_nota, + wait_until, + check_every, + ) + } + + fn wait_for_tx_spend( + &self, + transaction: &[u8], + wait_until: u64, + from_block: u64, + _swap_contract_address: &Option, + ) -> TransactionFut { + utxo_common::wait_for_output_spend( + &self.utxo_arc, + transaction, + utxo_common::DEFAULT_SWAP_VOUT, + from_block, + wait_until, + ) + } + + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result { + utxo_common::tx_enum_from_bytes(self.as_ref(), bytes) + } + + fn current_block(&self) -> Box + Send> { + utxo_common::current_block(&self.utxo_arc) + } + + fn display_priv_key(&self) -> Result { utxo_common::display_priv_key(&self.utxo_arc) } + + fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } + + fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } +} + +#[async_trait] +impl UtxoStandardOps for BchCoin { + async fn tx_details_by_hash( + &self, + hash: &[u8], + input_transactions: &mut HistoryUtxoTxMap, + ) -> Result { + utxo_common::tx_details_by_hash(self, hash, input_transactions).await + } + + async fn request_tx_history(&self, metrics: MetricsArc) -> RequestTxHistoryResult { + utxo_common::request_tx_history(self, metrics).await + } + + async fn update_kmd_rewards( + &self, + tx_details: &mut TransactionDetails, + input_transactions: &mut HistoryUtxoTxMap, + ) -> UtxoRpcResult<()> { + utxo_common::update_kmd_rewards(self, tx_details, input_transactions).await + } +} + +#[async_trait] +impl MmCoin for BchCoin { + fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } + + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { + Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) + } + + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { + Box::new(utxo_common::withdraw(self.clone(), req).boxed().compat()) + } + + fn decimals(&self) -> u8 { utxo_common::decimals(&self.utxo_arc) } + + fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { + utxo_common::convert_to_address(self, from, to_address_format) + } + + fn validate_address(&self, address: &str) -> ValidateAddressResult { utxo_common::validate_address(self, address) } + + fn process_history_loop(&self, ctx: MmArc) -> Box + Send> { + Box::new( + utxo_common::process_history_loop(self.clone(), ctx) + .map(|_| Ok(())) + .boxed() + .compat(), + ) + } + + fn history_sync_status(&self) -> HistorySyncState { utxo_common::history_sync_status(&self.utxo_arc) } + + fn get_trade_fee(&self) -> Box + Send> { + utxo_common::get_trade_fee(self.clone()) + } + + async fn get_sender_trade_fee( + &self, + value: TradePreimageValue, + stage: FeeApproxStage, + ) -> TradePreimageResult { + utxo_common::get_sender_trade_fee(self, value, stage).await + } + + fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + utxo_common::get_receiver_trade_fee(self.clone()) + } + + async fn get_fee_to_send_taker_fee( + &self, + dex_fee_amount: BigDecimal, + stage: FeeApproxStage, + ) -> TradePreimageResult { + utxo_common::get_fee_to_send_taker_fee(self, dex_fee_amount, stage).await + } + + fn required_confirmations(&self) -> u64 { utxo_common::required_confirmations(&self.utxo_arc) } + + fn requires_notarization(&self) -> bool { utxo_common::requires_notarization(&self.utxo_arc) } + + fn set_required_confirmations(&self, confirmations: u64) { + utxo_common::set_required_confirmations(&self.utxo_arc, confirmations) + } + + fn set_requires_notarization(&self, requires_nota: bool) { + utxo_common::set_requires_notarization(&self.utxo_arc, requires_nota) + } + + fn swap_contract_address(&self) -> Option { utxo_common::swap_contract_address() } + + fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } + + fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } + + fn is_coin_protocol_supported(&self, info: &Option>) -> bool { + utxo_common::is_coin_protocol_supported(self, info) + } +} + +impl CoinWithTxHistoryV2 for BchCoin { + fn history_wallet_id(&self) -> WalletId { WalletId::new(self.ticker().to_owned()) } + + /// There are not specific filters for `BchCoin`. + fn get_tx_history_filters(&self) -> GetTxHistoryFilters { GetTxHistoryFilters::new() } +} + +// testnet +#[cfg(test)] +pub fn tbch_coin_for_test() -> BchCoin { + use common::block_on; + use crypto::privkey::key_pair_from_seed; + use mm2_core::mm_ctx::MmCtxBuilder; + + let ctx = MmCtxBuilder::default().into_mm_arc(); + let keypair = key_pair_from_seed("BCH SLP test").unwrap(); + + let conf = json!({"coin":"BCH","pubtype":0,"p2shtype":5,"mm2":1,"fork_id":"0x40","protocol":{"type":"UTXO"}, "sign_message_prefix": "Bitcoin Signed Message:\n", + "address_format":{"format":"cashaddress","network":"bchtest"}}); + let req = json!({ + "method": "electrum", + "coin": "BCH", + "servers": [{"url":"blackie.c3-soft.com:60001"},{"url":"testnet.imaginary.cash:50001"},{"url":"tbch.loping.net:60001"},{"url":"electroncash.de:50003"}], + "bchd_urls": ["https://bchd-testnet.electroncash.de:18335"], + "allow_slp_unsafe_conf": false, + }); + + let params = BchActivationRequest::from_legacy_req(&req).unwrap(); + block_on(bch_coin_from_conf_and_params( + &ctx, + "BCH", + &conf, + params, + CashAddrPrefix::SlpTest, + &*keypair.private().secret, + )) + .unwrap() +} + +// mainnet +#[cfg(test)] +pub fn bch_coin_for_test() -> BchCoin { + use common::block_on; + use crypto::privkey::key_pair_from_seed; + use mm2_core::mm_ctx::MmCtxBuilder; + + let ctx = MmCtxBuilder::default().into_mm_arc(); + let keypair = key_pair_from_seed("BCH SLP test").unwrap(); + + let conf = json!({"coin":"BCH","pubtype":0,"p2shtype":5,"mm2":1,"fork_id":"0x40","protocol":{"type":"UTXO"}, + "address_format":{"format":"cashaddress","network":"bitcoincash"}}); + let req = json!({ + "method": "electrum", + "coin": "BCH", + "servers": [{"url":"electrum1.cipig.net:10055"},{"url":"electrum2.cipig.net:10055"},{"url":"electrum3.cipig.net:10055"}], + "bchd_urls": [], + "allow_slp_unsafe_conf": true, + }); + + let params = BchActivationRequest::from_legacy_req(&req).unwrap(); + block_on(bch_coin_from_conf_and_params( + &ctx, + "BCH", + &conf, + params, + CashAddrPrefix::SimpleLedger, + &*keypair.private().secret, + )) + .unwrap() +} + +#[cfg(test)] +mod bch_tests { + use super::*; + use crate::tx_history_storage::TxHistoryStorageBuilder; + use crate::{TransactionType, TxFeeDetails}; + use common::block_on; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + + fn init_storage_for(coin: &Coin) -> (MmArc, impl TxHistoryStorage) { + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + block_on(storage.init(&coin.history_wallet_id())).unwrap(); + (ctx, storage) + } + + #[test] + fn test_get_slp_genesis_params() { + let coin = tbch_coin_for_test(); + let token_id = "bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".into(); + let (_ctx, storage) = init_storage_for(&coin); + + let slp_params = block_on(coin.get_slp_genesis_params(token_id, &storage)).unwrap(); + assert_eq!("USDF", slp_params.token_ticker); + assert_eq!(4, slp_params.decimals[0]); + } + + #[test] + fn test_plain_bch_tx_details() { + let coin = tbch_coin_for_test(); + let (_ctx, storage) = init_storage_for(&coin); + + let hash = "a8dcc3c6776e93e7bd21fb81551e853447c55e2d8ac141b418583bc8095ce390".into(); + let tx = block_on(coin.tx_from_storage_or_rpc(&hash, &storage)).unwrap(); + + let details = block_on(coin.bch_tx_details(&hash, &tx, None, &storage)).unwrap(); + let expected_total: BigDecimal = "0.11407782".parse().unwrap(); + assert_eq!(expected_total, details.total_amount); + + let expected_received: BigDecimal = "0.11405301".parse().unwrap(); + assert_eq!(expected_received, details.received_by_me); + + let expected_spent: BigDecimal = "0.11407782".parse().unwrap(); + assert_eq!(expected_spent, details.spent_by_me); + + let expected_balance_change: BigDecimal = "-0.00002481".parse().unwrap(); + assert_eq!(expected_balance_change, details.my_balance_change); + + let expected_from = vec!["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66".to_owned()]; + assert_eq!(expected_from, details.from); + + let expected_to = vec![ + "bchtest:qrhdt5adye8lc68upfj9fctfdgcd3aq9hctf8ft6md".to_owned(), + "bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66".to_owned(), + ]; + assert_eq!(expected_to, details.to); + + let expected_internal_id = BytesJson::from("a8dcc3c6776e93e7bd21fb81551e853447c55e2d8ac141b418583bc8095ce390"); + assert_eq!(expected_internal_id, details.internal_id); + + let expected_fee = Some(TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some("BCH".into()), + amount: "0.00001481".parse().unwrap(), + })); + assert_eq!(expected_fee, details.fee_details); + + assert_eq!(coin.ticker(), details.coin); + } + + #[test] + fn test_slp_tx_details() { + let coin = tbch_coin_for_test(); + let (_ctx, storage) = init_storage_for(&coin); + + let hash = "a8dcc3c6776e93e7bd21fb81551e853447c55e2d8ac141b418583bc8095ce390".into(); + let tx = block_on(coin.tx_from_storage_or_rpc(&hash, &storage)).unwrap(); + + let slp_details = parse_slp_script(&tx.outputs[0].script_pubkey).unwrap(); + + let slp_tx_details = block_on(coin.slp_tx_details(&tx, slp_details.transaction, None, None, &storage)).unwrap(); + + let expected_total: BigDecimal = "6.2974".parse().unwrap(); + assert_eq!(expected_total, slp_tx_details.total_amount); + + let expected_spent: BigDecimal = "6.2974".parse().unwrap(); + assert_eq!(expected_spent, slp_tx_details.spent_by_me); + + let expected_received: BigDecimal = "5.2974".parse().unwrap(); + assert_eq!(expected_received, slp_tx_details.received_by_me); + + let expected_balance_change = BigDecimal::from(-1i32); + assert_eq!(expected_balance_change, slp_tx_details.my_balance_change); + + let expected_from = vec!["slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8".to_owned()]; + assert_eq!(expected_from, slp_tx_details.from); + + let expected_to = vec![ + "slptest:qrhdt5adye8lc68upfj9fctfdgcd3aq9hcsaqj3dfs".to_owned(), + "slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8".to_owned(), + ]; + assert_eq!(expected_to, slp_tx_details.to); + + let expected_tx_type = + TransactionType::TokenTransfer("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".into()); + assert_eq!(expected_tx_type, slp_tx_details.transaction_type); + + assert_eq!(coin.ticker(), slp_tx_details.coin); + } + + #[test] + fn test_sign_message() { + let coin = tbch_coin_for_test(); + let signature = coin.sign_message("test").unwrap(); + assert_eq!( + signature, + "ILuePKMsycXwJiNDOT7Zb7TfIlUW7Iq+5ylKd15AK72vGVYXbnf7Gj9Lk9MFV+6Ub955j7MiAkp0wQjvuIoRPPA=" + ); + } + + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn test_verify_message() { + let coin = tbch_coin_for_test(); + let is_valid = coin + .verify_message( + "ILuePKMsycXwJiNDOT7Zb7TfIlUW7Iq+5ylKd15AK72vGVYXbnf7Gj9Lk9MFV+6Ub955j7MiAkp0wQjvuIoRPPA=", + "test", + "bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66", + ) + .unwrap(); + assert!(is_valid); + } +} diff --git a/mm2src/coins/utxo/bch_and_slp_tx_history.rs b/mm2src/coins/utxo/bch_and_slp_tx_history.rs new file mode 100644 index 0000000000..5da1212e9d --- /dev/null +++ b/mm2src/coins/utxo/bch_and_slp_tx_history.rs @@ -0,0 +1,406 @@ +/// This module is named bch_and_slp_tx_history temporary. We will most likely use the same approach for every +/// supported UTXO coin. +use super::RequestTxHistoryResult; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, TxHistoryStorage}; +use crate::utxo::bch::BchCoin; +use crate::utxo::utxo_common; +use crate::utxo::UtxoStandardOps; +use crate::{BlockHeightAndTime, HistorySyncState, MarketCoinOps}; +use async_trait::async_trait; +use common::executor::Timer; +use common::log::{error, info}; +use common::mm_metrics::MetricsArc; +use common::state_machine::prelude::*; +use futures::compat::Future01CompatExt; +use mm2_number::BigDecimal; +use rpc::v1::types::H256 as H256Json; +use std::collections::HashMap; +use std::str::FromStr; + +struct BchAndSlpHistoryCtx { + coin: BchCoin, + storage: Storage, + metrics: MetricsArc, + current_balance: BigDecimal, +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct Init { + phantom: std::marker::PhantomData, +} + +impl Init { + fn new() -> Self { + Init { + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for Stopped {} + +#[async_trait] +impl State for Init { + type Ctx = BchAndSlpHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut BchAndSlpHistoryCtx) -> StateResult, ()> { + *ctx.coin.as_ref().history_sync_state.lock().unwrap() = HistorySyncState::NotStarted; + + if let Err(e) = ctx.storage.init(&ctx.coin.history_wallet_id()).await { + return Self::change_state(Stopped::storage_error(e)); + } + + Self::change_state(FetchingTxHashes::new()) + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct FetchingTxHashes { + phantom: std::marker::PhantomData, +} + +impl FetchingTxHashes { + fn new() -> Self { + FetchingTxHashes { + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for FetchingTxHashes {} +impl TransitionFrom> for FetchingTxHashes {} +impl TransitionFrom> for FetchingTxHashes {} + +#[async_trait] +impl State for FetchingTxHashes { + type Ctx = BchAndSlpHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut BchAndSlpHistoryCtx) -> StateResult, ()> { + let wallet_id = ctx.coin.history_wallet_id(); + if let Err(e) = ctx.storage.init(&wallet_id).await { + return Self::change_state(Stopped::storage_error(e)); + } + + let maybe_tx_ids = ctx.coin.request_tx_history(ctx.metrics.clone()).await; + match maybe_tx_ids { + RequestTxHistoryResult::Ok(all_tx_ids_with_height) => { + let in_storage = match ctx.storage.unique_tx_hashes_num_in_history(&wallet_id).await { + Ok(num) => num, + Err(e) => return Self::change_state(Stopped::storage_error(e)), + }; + if all_tx_ids_with_height.len() > in_storage { + let txes_left = all_tx_ids_with_height.len() - in_storage; + *ctx.coin.as_ref().history_sync_state.lock().unwrap() = + HistorySyncState::InProgress(json!({ "transactions_left": txes_left })); + } + + Self::change_state(UpdatingUnconfirmedTxes::new(all_tx_ids_with_height)) + }, + RequestTxHistoryResult::HistoryTooLarge => Self::change_state(Stopped::::history_too_large()), + RequestTxHistoryResult::Retry { error } => { + error!("Error {} on requesting tx history for {}", error, ctx.coin.ticker()); + Self::change_state(OnIoErrorCooldown::new()) + }, + RequestTxHistoryResult::CriticalError(e) => { + error!( + "Critical error {} on requesting tx history for {}", + e, + ctx.coin.ticker() + ); + Self::change_state(Stopped::::unknown(e)) + }, + } + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct OnIoErrorCooldown { + phantom: std::marker::PhantomData, +} + +impl OnIoErrorCooldown { + fn new() -> Self { + OnIoErrorCooldown { + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for OnIoErrorCooldown {} +impl TransitionFrom> for OnIoErrorCooldown {} +impl TransitionFrom> for OnIoErrorCooldown {} + +#[async_trait] +impl State for OnIoErrorCooldown { + type Ctx = BchAndSlpHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, _ctx: &mut BchAndSlpHistoryCtx) -> StateResult, ()> { + Timer::sleep(30.).await; + Self::change_state(FetchingTxHashes::new()) + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct WaitForHistoryUpdateTrigger { + phantom: std::marker::PhantomData, +} + +impl WaitForHistoryUpdateTrigger { + fn new() -> Self { + WaitForHistoryUpdateTrigger { + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for WaitForHistoryUpdateTrigger {} + +#[async_trait] +impl State for WaitForHistoryUpdateTrigger { + type Ctx = BchAndSlpHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut BchAndSlpHistoryCtx) -> StateResult { + let wallet_id = ctx.coin.history_wallet_id(); + loop { + Timer::sleep(30.).await; + match ctx.storage.history_contains_unconfirmed_txes(&wallet_id).await { + Ok(contains) => { + if contains { + return Self::change_state(FetchingTxHashes::new()); + } + }, + Err(e) => return Self::change_state(Stopped::storage_error(e)), + } + + match ctx.coin.my_balance().compat().await { + Ok(balance) => { + let total_balance = balance.into_total(); + if ctx.current_balance != total_balance { + ctx.current_balance = total_balance; + return Self::change_state(FetchingTxHashes::new()); + } + }, + Err(e) => { + error!("Error {} on balance fetching for the coin {}", e, ctx.coin.ticker()); + }, + } + } + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct UpdatingUnconfirmedTxes { + phantom: std::marker::PhantomData, + all_tx_ids_with_height: Vec<(H256Json, u64)>, +} + +impl UpdatingUnconfirmedTxes { + fn new(all_tx_ids_with_height: Vec<(H256Json, u64)>) -> Self { + UpdatingUnconfirmedTxes { + phantom: Default::default(), + all_tx_ids_with_height, + } + } +} + +impl TransitionFrom> for UpdatingUnconfirmedTxes {} + +#[async_trait] +impl State for UpdatingUnconfirmedTxes { + type Ctx = BchAndSlpHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut BchAndSlpHistoryCtx) -> StateResult, ()> { + let wallet_id = ctx.coin.history_wallet_id(); + match ctx.storage.get_unconfirmed_txes_from_history(&wallet_id).await { + Ok(unconfirmed) => { + let txs_with_height: HashMap = self.all_tx_ids_with_height.clone().into_iter().collect(); + for mut tx in unconfirmed { + let found = match H256Json::from_str(&tx.tx_hash) { + Ok(unconfirmed_tx_hash) => txs_with_height.get(&unconfirmed_tx_hash), + Err(_) => None, + }; + + match found { + Some(height) => { + if *height > 0 { + match ctx.coin.get_block_timestamp(*height).await { + Ok(time) => tx.timestamp = time, + Err(_) => return Self::change_state(OnIoErrorCooldown::new()), + }; + tx.block_height = *height; + if let Err(e) = ctx.storage.update_tx_in_history(&wallet_id, &tx).await { + return Self::change_state(Stopped::storage_error(e)); + } + } + }, + None => { + // This can potentially happen when unconfirmed tx is removed from mempool for some reason. + // Or if the hash is undecodable. We should remove it from storage too. + if let Err(e) = ctx.storage.remove_tx_from_history(&wallet_id, &tx.internal_id).await { + return Self::change_state(Stopped::storage_error(e)); + } + }, + } + } + Self::change_state(FetchingTransactionsData::new(self.all_tx_ids_with_height)) + }, + Err(e) => Self::change_state(Stopped::storage_error(e)), + } + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct FetchingTransactionsData { + phantom: std::marker::PhantomData, + all_tx_ids_with_height: Vec<(H256Json, u64)>, +} + +impl TransitionFrom> for FetchingTransactionsData {} + +impl FetchingTransactionsData { + fn new(all_tx_ids_with_height: Vec<(H256Json, u64)>) -> Self { + FetchingTransactionsData { + phantom: Default::default(), + all_tx_ids_with_height, + } + } +} + +#[async_trait] +impl State for FetchingTransactionsData { + type Ctx = BchAndSlpHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut BchAndSlpHistoryCtx) -> StateResult, ()> { + let wallet_id = ctx.coin.history_wallet_id(); + for (tx_hash, height) in self.all_tx_ids_with_height { + let tx_hash_string = format!("{:02x}", tx_hash); + match ctx.storage.history_has_tx_hash(&wallet_id, &tx_hash_string).await { + Ok(true) => continue, + Ok(false) => (), + Err(e) => return Self::change_state(Stopped::storage_error(e)), + } + + let block_height_and_time = if height > 0 { + let timestamp = match ctx.coin.get_block_timestamp(height).await { + Ok(time) => time, + Err(_) => return Self::change_state(OnIoErrorCooldown::new()), + }; + Some(BlockHeightAndTime { height, timestamp }) + } else { + None + }; + let tx_details = match ctx + .coin + .transaction_details_with_token_transfers(&tx_hash, block_height_and_time, &ctx.storage) + .await + { + Ok(tx) => tx, + Err(e) => { + error!( + "Error {:?} on getting {} tx details for hash {:02x}", + e, + ctx.coin.ticker(), + tx_hash + ); + return Self::change_state(OnIoErrorCooldown::new()); + }, + }; + + if let Err(e) = ctx.storage.add_transactions_to_history(&wallet_id, tx_details).await { + return Self::change_state(Stopped::storage_error(e)); + } + + // wait for for one second to reduce the number of requests to electrum servers + Timer::sleep(1.).await; + } + info!("Tx history fetching finished for {}", ctx.coin.ticker()); + *ctx.coin.as_ref().history_sync_state.lock().unwrap() = HistorySyncState::Finished; + Self::change_state(WaitForHistoryUpdateTrigger::new()) + } +} + +#[derive(Debug)] +enum StopReason { + HistoryTooLarge, + StorageError(E), + UnknownError(String), +} + +struct Stopped { + phantom: std::marker::PhantomData, + stop_reason: StopReason, +} + +impl Stopped { + fn history_too_large() -> Self { + Stopped { + phantom: Default::default(), + stop_reason: StopReason::HistoryTooLarge, + } + } + + fn storage_error(e: E) -> Self { + Stopped { + phantom: Default::default(), + stop_reason: StopReason::StorageError(e), + } + } + + fn unknown(e: String) -> Self { + Stopped { + phantom: Default::default(), + stop_reason: StopReason::UnknownError(e), + } + } +} + +impl TransitionFrom> for Stopped {} +impl TransitionFrom> for Stopped {} +impl TransitionFrom> for Stopped {} +impl TransitionFrom> for Stopped {} + +#[async_trait] +impl LastState for Stopped { + type Ctx = BchAndSlpHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> Self::Result { + info!( + "Stopping tx history fetching for {}. Reason: {:?}", + ctx.coin.ticker(), + self.stop_reason + ); + let new_state_json = match self.stop_reason { + StopReason::HistoryTooLarge => json!({ + "code": utxo_common::HISTORY_TOO_LARGE_ERR_CODE, + "message": "Got `history too large` error from Electrum server. History is not available", + }), + reason => json!({ + "message": format!("{:?}", reason), + }), + }; + *ctx.coin.as_ref().history_sync_state.lock().unwrap() = HistorySyncState::Error(new_state_json); + } +} + +pub async fn bch_and_slp_history_loop( + coin: BchCoin, + storage: impl TxHistoryStorage, + metrics: MetricsArc, + current_balance: BigDecimal, +) { + let ctx = BchAndSlpHistoryCtx { + coin, + storage, + metrics, + current_balance, + }; + let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ctx); + state_machine.run(Init::new()).await; +} diff --git a/mm2src/coins/utxo/bchd_grpc.rs b/mm2src/coins/utxo/bchd_grpc.rs new file mode 100644 index 0000000000..f0a632cef6 --- /dev/null +++ b/mm2src/coins/utxo/bchd_grpc.rs @@ -0,0 +1,434 @@ +/// https://bchd.cash/ +/// https://bchd.fountainhead.cash/ +use super::bchd_pb::*; +use crate::utxo::slp::SlpUnspent; +use chain::OutPoint; +use derive_more::Display; +use futures::future::join_all; +use futures::FutureExt; +use get_slp_trusted_validation_response::validity_result::ValidityResultType; +use keys::hash::H256; +use mm2_err_handle::prelude::*; +use mm2_net::grpc_web::{post_grpc_web, PostGrpcWebErr}; + +#[derive(Debug, Display)] +#[display(fmt = "Error {:?} on request to the url {}", err, to_url)] +pub struct GrpcWebMultiUrlReqErr { + to_url: String, + err: PostGrpcWebErr, +} + +/// This fn will simply return Ok() if urls are empty. +/// It is intended behaviour to make "unsafe" mode possible for BCH. +#[allow(clippy::needless_lifetimes)] +async fn grpc_web_multi_url_request<'a, Req, Res, Url>( + urls: &'a [Url], + req: &'a Req, +) -> Result, MmError> +where + Req: prost::Message + Send + 'static, + Res: prost::Message + Default + Send + 'static, + Url: AsRef, +{ + let futures = urls + .iter() + .map(|url| post_grpc_web::<_, Res>(url.as_ref(), req).map(move |res| (url, res))); + + join_all(futures) + .await + .into_iter() + .map(|(url, response)| { + Ok(( + url, + response.mm_err(|err| GrpcWebMultiUrlReqErr { + to_url: url.as_ref().to_string(), + err, + })?, + )) + }) + .collect() +} + +#[derive(Debug, Display)] +pub enum ValidateSlpUtxosErrKind { + MultiReqErr(GrpcWebMultiUrlReqErr), + #[display(fmt = "Expected {} token id, but got {}", expected, actual)] + UnexpectedTokenId { + expected: H256, + actual: H256, + }, + #[display( + fmt = "Unexpected validity_result {:?} for unspent {:?}", + validity_result, + for_unspent + )] + UnexpectedValidityResultType { + for_unspent: SlpUnspent, + validity_result: Option, + }, + #[display(fmt = "Unexpected utxo {:?} in response", outpoint)] + UnexpectedUtxoInResponse { + outpoint: OutPoint, + }, +} + +#[derive(Debug, Display)] +#[display(fmt = "Error {} on request to the url {}", kind, to_url)] +pub struct ValidateSlpUtxosErr { + to_url: String, + kind: ValidateSlpUtxosErrKind, +} + +impl From for ValidateSlpUtxosErr { + fn from(err: GrpcWebMultiUrlReqErr) -> Self { + ValidateSlpUtxosErr { + to_url: err.to_url.clone(), + kind: ValidateSlpUtxosErrKind::MultiReqErr(err), + } + } +} + +pub async fn validate_slp_utxos( + bchd_urls: &[impl AsRef], + utxos: &[SlpUnspent], + token_id: &H256, +) -> Result<(), MmError> { + let queries = utxos + .iter() + .map(|utxo| get_slp_trusted_validation_request::Query { + prev_out_hash: utxo.bch_unspent.outpoint.hash.take().into(), + prev_out_vout: utxo.bch_unspent.outpoint.index, + graphsearch_valid_hashes: Vec::new(), + }) + .collect(); + let request = GetSlpTrustedValidationRequest { + queries, + include_graphsearch_count: false, + }; + + let urls: Vec<_> = bchd_urls + .iter() + .map(|url| url.as_ref().to_owned() + "/pb.bchrpc/GetSlpTrustedValidation") + .collect(); + let responses: Vec<(_, GetSlpTrustedValidationResponse)> = grpc_web_multi_url_request(&urls, &request).await?; + for (url, response) in responses { + for validation_result in response.results { + let actual_token_id = validation_result.token_id.as_slice().into(); + if actual_token_id != *token_id { + return MmError::err(ValidateSlpUtxosErr { + to_url: url.clone(), + kind: ValidateSlpUtxosErrKind::UnexpectedTokenId { + expected: *token_id, + actual: actual_token_id, + }, + }); + } + + let outpoint = OutPoint { + hash: validation_result.prev_out_hash.as_slice().into(), + index: validation_result.prev_out_vout, + }; + + let initial_unspent = utxos + .iter() + .find(|unspent| unspent.bch_unspent.outpoint == outpoint) + .or_mm_err(|| ValidateSlpUtxosErr { + to_url: url.clone(), + kind: ValidateSlpUtxosErrKind::UnexpectedUtxoInResponse { outpoint }, + })?; + + match validation_result.validity_result_type { + Some(ValidityResultType::V1TokenAmount(slp_amount)) => { + if slp_amount != initial_unspent.slp_amount { + return MmError::err(ValidateSlpUtxosErr { + to_url: url.clone(), + kind: ValidateSlpUtxosErrKind::UnexpectedValidityResultType { + for_unspent: initial_unspent.clone(), + validity_result: validation_result.validity_result_type, + }, + }); + } + }, + _ => { + return MmError::err(ValidateSlpUtxosErr { + to_url: url.clone(), + kind: ValidateSlpUtxosErrKind::UnexpectedValidityResultType { + for_unspent: initial_unspent.clone(), + validity_result: validation_result.validity_result_type, + }, + }) + }, + } + } + } + Ok(()) +} + +#[derive(Debug, Display)] +pub enum CheckSlpTransactionErrKind { + MultiReqErr(GrpcWebMultiUrlReqErr), + #[display(fmt = "Transaction {:?} is not valid with reason {}", transaction, reason)] + InvalidTransaction { + transaction: Vec, + reason: String, + }, +} + +#[derive(Debug, Display)] +#[display(fmt = "Error {} on request to the url {}", kind, to_url)] +pub struct CheckSlpTransactionErr { + to_url: String, + kind: CheckSlpTransactionErrKind, +} + +impl From for CheckSlpTransactionErr { + fn from(err: GrpcWebMultiUrlReqErr) -> Self { + CheckSlpTransactionErr { + to_url: err.to_url.clone(), + kind: CheckSlpTransactionErrKind::MultiReqErr(err), + } + } +} + +pub async fn check_slp_transaction( + bchd_urls: &[impl AsRef], + transaction: Vec, +) -> Result<(), MmError> { + let request = CheckSlpTransactionRequest { + transaction, + required_slp_burns: Vec::new(), + use_spec_validity_judgement: false, + }; + + let urls: Vec<_> = bchd_urls + .iter() + .map(|url| url.as_ref().to_owned() + "/pb.bchrpc/CheckSlpTransaction") + .collect(); + + let responses: Vec<(_, CheckSlpTransactionResponse)> = grpc_web_multi_url_request(&urls, &request).await?; + for (url, response) in responses { + if !response.is_valid { + return MmError::err(CheckSlpTransactionErr { + to_url: url.clone(), + kind: CheckSlpTransactionErrKind::InvalidTransaction { + transaction: request.transaction, + reason: response.invalid_reason, + }, + }); + } + } + Ok(()) +} + +#[cfg(test)] +mod bchd_grpc_tests { + use super::*; + use crate::utxo::rpc_clients::UnspentInfo; + use common::block_on; + + #[test] + fn test_validate_slp_utxos_valid() { + let tx_hash = H256::from_reversed_str("0ba1b91abbfceaa0777424165edb2928dace87d59669c913989950da31968032"); + + let slp_utxos = [ + SlpUnspent { + bch_unspent: UnspentInfo { + outpoint: OutPoint { + hash: tx_hash, + index: 1, + }, + value: 0, + height: None, + }, + slp_amount: 1000, + }, + SlpUnspent { + bch_unspent: UnspentInfo { + outpoint: OutPoint { + hash: tx_hash, + index: 2, + }, + value: 0, + height: None, + }, + slp_amount: 8999, + }, + ]; + + let url = "https://bchd-testnet.electroncash.de:18335"; + let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); + block_on(validate_slp_utxos(&[url], &slp_utxos, &token_id)).unwrap(); + } + + #[test] + fn test_validate_slp_utxos_non_slp_input() { + let tx_hash = H256::from_reversed_str("0ba1b91abbfceaa0777424165edb2928dace87d59669c913989950da31968032"); + + let slp_utxos = [ + SlpUnspent { + bch_unspent: UnspentInfo { + outpoint: OutPoint { + hash: tx_hash, + index: 1, + }, + value: 0, + height: None, + }, + slp_amount: 1000, + }, + SlpUnspent { + bch_unspent: UnspentInfo { + outpoint: OutPoint { + hash: tx_hash, + index: 2, + }, + value: 0, + height: None, + }, + slp_amount: 8999, + }, + SlpUnspent { + bch_unspent: UnspentInfo { + outpoint: OutPoint { + hash: tx_hash, + index: 3, + }, + value: 0, + height: None, + }, + slp_amount: 8999, + }, + ]; + + let url = "https://bchd-testnet.electroncash.de:18335"; + let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); + let err = block_on(validate_slp_utxos(&[url], &slp_utxos, &token_id)).unwrap_err(); + match err.into_inner().kind { + ValidateSlpUtxosErrKind::MultiReqErr { .. } => (), + err @ _ => panic!("Unexpected error {:?}", err), + } + } + + #[test] + fn test_validate_slp_utxos_invalid_amount() { + let tx_hash = H256::from_reversed_str("0ba1b91abbfceaa0777424165edb2928dace87d59669c913989950da31968032"); + let invalid_utxo = SlpUnspent { + bch_unspent: UnspentInfo { + outpoint: OutPoint { + hash: tx_hash, + index: 1, + }, + value: 0, + height: None, + }, + slp_amount: 999, + }; + + let slp_utxos = [invalid_utxo.clone(), SlpUnspent { + bch_unspent: UnspentInfo { + outpoint: OutPoint { + hash: tx_hash, + index: 2, + }, + value: 0, + height: None, + }, + slp_amount: 8999, + }]; + + let url = "https://bchd-testnet.electroncash.de:18335"; + let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); + let err = block_on(validate_slp_utxos(&[url], &slp_utxos, &token_id)).unwrap_err(); + match err.into_inner().kind { + ValidateSlpUtxosErrKind::UnexpectedValidityResultType { + for_unspent, + validity_result, + } => { + let expected_validity = Some(ValidityResultType::V1TokenAmount(1000)); + assert_eq!(invalid_utxo, for_unspent); + assert_eq!(expected_validity, validity_result); + }, + err @ _ => panic!("Unexpected error {:?}", err), + } + } + + #[test] + fn test_validate_slp_utxos_unexpected_token_id() { + let tx_hash = H256::from_reversed_str("0ba1b91abbfceaa0777424165edb2928dace87d59669c913989950da31968032"); + + let slp_utxos = [ + SlpUnspent { + bch_unspent: UnspentInfo { + outpoint: OutPoint { + hash: tx_hash, + index: 1, + }, + value: 0, + height: None, + }, + slp_amount: 1000, + }, + SlpUnspent { + bch_unspent: UnspentInfo { + outpoint: OutPoint { + hash: tx_hash, + index: 2, + }, + value: 0, + height: None, + }, + slp_amount: 8999, + }, + ]; + + let url = "https://bchd-testnet.electroncash.de:18335"; + let valid_token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); + let invalid_token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb8"); + let err = block_on(validate_slp_utxos(&[url], &slp_utxos, &invalid_token_id)).unwrap_err(); + match err.into_inner().kind { + ValidateSlpUtxosErrKind::UnexpectedTokenId { expected, actual } => { + assert_eq!(invalid_token_id, expected); + assert_eq!(valid_token_id, actual); + }, + err @ _ => panic!("Unexpected error {:?}", err), + } + } + + #[test] + fn test_check_slp_transaction_valid() { + let url = "https://bchd-testnet.electroncash.de:18335"; + // https://testnet.simpleledger.info/tx/c5f46ccc5431687154335d5b6526f1b9cfa961c44b97956b7bec77f884f56c73 + let tx = hex::decode("010000000232809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b020000006a473044022057c88d815fa563eda8ef7d0dd5c522f4501ffa6110df455b151b31609f149c22022048fecfc9b16e983fbfd05b0d2b7c011c3dbec542577fa00cd9bd192b81961f8e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff32809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b030000006a4730440220539e1204d2805c0474111a1f233ff82c0ab06e6e2bfc0cbe4975eacae64a0b1f02200ec83d32c2180f5567d0f760e85f1efc99d9341cfebd86c9a334310f6d4381494121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000002326e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f694801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8983d460").unwrap(); + block_on(check_slp_transaction(&[url], tx)).unwrap(); + } + + #[test] + fn test_check_slp_transaction_invalid() { + let url = "https://bchd-testnet.electroncash.de:18335"; + // https://www.blockchain.com/bch-testnet/tx/d76723c092b64bc598d5d2ceafd6f0db37dce4032db569d6f26afb35491789a7 + let tx = hex::decode("010000000190e35c09c83b5818b441c18a2d5ec54734851e5581fb21bde7936e77c6c3dca8030000006b483045022100e6b1415cbd81f2d04360597fba65965bc77ab5a972f5b8f8d5c0f1b1912923c402206a63f305f03e9c49ffba6c71c7a76ef60631f67dce7631f673a0e8485b86898d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff020000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e82500ae00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac62715161").unwrap(); + let err = block_on(check_slp_transaction(&[url], tx)).unwrap_err(); + match err.into_inner().kind { + CheckSlpTransactionErrKind::InvalidTransaction { reason, .. } => { + println!("{}", reason); + }, + err @ _ => panic!("Unexpected error {:?}", err), + } + } +} + +#[cfg(target_arch = "wasm32")] +mod wasm_tests { + use super::*; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test] + async fn test_check_slp_transaction_valid() { + let url = "https://bchd-testnet.electroncash.de:18335"; + // https://testnet.simpleledger.info/tx/c5f46ccc5431687154335d5b6526f1b9cfa961c44b97956b7bec77f884f56c73 + let tx = hex::decode("010000000232809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b020000006a473044022057c88d815fa563eda8ef7d0dd5c522f4501ffa6110df455b151b31609f149c22022048fecfc9b16e983fbfd05b0d2b7c011c3dbec542577fa00cd9bd192b81961f8e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff32809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b030000006a4730440220539e1204d2805c0474111a1f233ff82c0ab06e6e2bfc0cbe4975eacae64a0b1f02200ec83d32c2180f5567d0f760e85f1efc99d9341cfebd86c9a334310f6d4381494121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000002326e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f694801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8983d460").unwrap(); + check_slp_transaction(&[url], tx).await.unwrap(); + } +} diff --git a/mm2src/coins/utxo/bchrpc.proto b/mm2src/coins/utxo/bchrpc.proto new file mode 100644 index 0000000000..aa082f5f00 --- /dev/null +++ b/mm2src/coins/utxo/bchrpc.proto @@ -0,0 +1,997 @@ +syntax = "proto3"; +option go_package="github.com/gcash/bchd/bchrpc/pb"; + +package pb; +option java_package = "cash.bchd.rpc"; + +// bchrpc contains a set of RPCs that can be exposed publicly via +// the command line options. This service could be authenticated or +// unauthenticated. +service bchrpc { + + // GetMempoolInfo returns the state of the current mempool. + rpc GetMempoolInfo(GetMempoolInfoRequest) returns (GetMempoolInfoResponse) {} + + // GetMempool returns information about all transactions currently in the memory pool. + // Offers an option to return full transactions or just transactions hashes. + rpc GetMempool(GetMempoolRequest) returns (GetMempoolResponse) {} + + // GetBlockchainInfo returns data about the blockchain including the most recent + // block hash and height. + rpc GetBlockchainInfo(GetBlockchainInfoRequest) returns (GetBlockchainInfoResponse) {} + + // GetBlockInfo returns metadata and info for a specified block. + rpc GetBlockInfo(GetBlockInfoRequest)returns (GetBlockInfoResponse) {} + + // GetBlock returns detailed data for a block. + rpc GetBlock(GetBlockRequest) returns (GetBlockResponse) {} + + // GetRawBlock returns a block in a serialized format. + rpc GetRawBlock(GetRawBlockRequest) returns (GetRawBlockResponse) {} + + // GetBlockFilter returns the compact filter (cf) of a block as a Golomb-Rice encoded set. + // + // **Requires CfIndex** + rpc GetBlockFilter(GetBlockFilterRequest) returns (GetBlockFilterResponse) {} + + // GetHeaders takes a block locator object and returns a batch of no more than 2000 + // headers. Upon parsing the block locator, if the server concludes there has been a + // fork, it will send headers starting at the fork point, or genesis if no blocks in + // the locator are in the best chain. If the locator is already at the tip no headers + // will be returned. + // see: bchd/bchrpc/documentation/wallet_operation.md + rpc GetHeaders(GetHeadersRequest) returns (GetHeadersResponse) {} + + // GetTransaction returns a transaction given a transaction hash. + // + // **Requires TxIndex** + // **Requires SlpIndex for slp related information ** + rpc GetTransaction(GetTransactionRequest) returns (GetTransactionResponse) {} + + // GetRawTransaction returns a serialized transaction given a transaction hash. + // + // **Requires TxIndex** + rpc GetRawTransaction(GetRawTransactionRequest) returns (GetRawTransactionResponse) {} + + // GetAddressTransactions returns the transactions for the given address. Offers offset, + // limit, and from block options. + // + // **Requires AddressIndex** + // **Requires SlpIndex for slp related information ** + rpc GetAddressTransactions(GetAddressTransactionsRequest) returns (GetAddressTransactionsResponse) {} + + // GetRawAddressTransactions returns the serialized raw transactions for + // the given address. Offers offset, limit, and from block options. + // + // **Requires AddressIndex** + rpc GetRawAddressTransactions(GetRawAddressTransactionsRequest) returns (GetRawAddressTransactionsResponse) {} + + // GetAddressUnspentOutputs returns all the unspent transaction outputs + // for the given address. + // + // **Requires AddressIndex** + // **Requires SlpIndex for slp related information ** + rpc GetAddressUnspentOutputs(GetAddressUnspentOutputsRequest) returns (GetAddressUnspentOutputsResponse) {} + + // GetUnspentOutput takes an unspent output in the utxo set and returns + // the utxo metadata or not found. + // + // **Requires SlpIndex for slp related information ** + rpc GetUnspentOutput(GetUnspentOutputRequest) returns (GetUnspentOutputResponse) {} + + // GetMerkleProof returns a Merkle (SPV) proof for a specific transaction + // in the provided block. + // + // **Requires TxIndex** + rpc GetMerkleProof(GetMerkleProofRequest) returns (GetMerkleProofResponse) {} + + // GetSlpTokenMetadata return slp token metadata for one or more tokens. + // + // **Requires SlpIndex** + rpc GetSlpTokenMetadata(GetSlpTokenMetadataRequest) returns (GetSlpTokenMetadataResponse) {} + + // GetSlpParsedScript returns marshalled object from parsing an slp pubKeyScript + // using goslp package. This endpoint does not require SlpIndex. + rpc GetSlpParsedScript(GetSlpParsedScriptRequest) returns (GetSlpParsedScriptResponse) {} + + // GetSlpTrustedValidation returns slp validity related information for one or more transactions. + // + // **Requires SlpIndex** + rpc GetSlpTrustedValidation(GetSlpTrustedValidationRequest) returns (GetSlpTrustedValidationResponse) {} + + // GraphSearch returns all the transactions needed for a client to validate an SLP graph + // + // **Requires SlpIndex and SlpGraphSearch** + rpc GetSlpGraphSearch (GetSlpGraphSearchRequest) returns (GetSlpGraphSearchResponse) {} + + // CheckSlpTransaction checks the validity of a supposed slp transaction before it is broadcasted. + rpc CheckSlpTransaction(CheckSlpTransactionRequest) returns (CheckSlpTransactionResponse) {} + + // Submit a transaction to all connected peers. + rpc SubmitTransaction(SubmitTransactionRequest) returns (SubmitTransactionResponse) {} + + // SubscribeTransactions creates subscription to all relevant transactions based on + // the subscription filter. + // + // This RPC does not use bidirectional streams and therefore can be used + // with grpc-web. You will need to close and reopen the stream whenever + // you want to update the subscription filter. If you are not using grpc-web + // then SubscribeTransactionStream is more appropriate. + // + // **Requires TxIndex to receive input metadata** + // **Requires SlpIndex to receive slp input/output metadata, or SlpTokenMetadata** + rpc SubscribeTransactions(SubscribeTransactionsRequest) returns (stream TransactionNotification) {} + + // SubscribeTransactionStream subscribes to relevant transactions based on + // the subscription requests. The parameters to filter transactions on can + // be updated by sending new SubscribeTransactionsRequest objects on the stream. + // + // NOTE: Because this RPC is using bi-directional streaming it cannot be used with + // grpc-web. + // + // **Requires TxIndex to receive input metadata** + rpc SubscribeTransactionStream(stream SubscribeTransactionsRequest) returns (stream TransactionNotification) {} + + // SubscribeBlocks creates a subscription for notifications of new blocks being + // connected to the blockchain or blocks being disconnected. + rpc SubscribeBlocks(SubscribeBlocksRequest) returns (stream BlockNotification) {} +} + + +// RPC MESSAGES + +message GetMempoolInfoRequest {} +message GetMempoolInfoResponse { + // The count of transactions in the mempool + uint32 size = 1; + // The size in bytes of all transactions in the mempool + uint32 bytes = 2; +} + +message GetMempoolRequest { + // When `full_transactions` is true, full transaction data is provided + // instead of just transaction hashes. Default is false. + bool full_transactions = 1; +} + +message GetMempoolResponse { + message TransactionData { + // Either one of the two following is provided, depending on the request. + oneof txids_or_txs { + // The transaction hash, little-endian. + bytes transaction_hash = 1; + // The transaction data. + Transaction transaction = 2; + } + } + + // List of unconfirmed transactions. + repeated TransactionData transaction_data = 1; +} + +message GetBlockchainInfoRequest {} +message GetBlockchainInfoResponse { + + // Bitcoin network types + enum BitcoinNet { + + // Live public network with monetary value. + MAINNET = 0; + // An isolated environment for automated testing. + REGTEST = 1; + // A public environment where monetary value is agreed to be zero, + // and some checks for transaction conformity are disabled. + TESTNET3 = 2; + // Private testnets for large scale simulations (or stress testing), + // where a specified list of nodes is used, rather than node discovery. + SIMNET = 3; + } + + // Which network the node is operating on. + BitcoinNet bitcoin_net = 1; + + // The current number of blocks on the longest chain. + int32 best_height = 2; + // The hash of the best (tip) block in the most-work fully-validated chain, little-endian. + bytes best_block_hash = 3; + // Threshold for adding new blocks. + double difficulty = 4; + // Median time of the last 11 blocks. + int64 median_time = 5; + // When `tx_index` is true, the node has full transaction index enabled. + bool tx_index = 6; + // When `addr_index` is true, the node has address index enabled and may + // be used with call related by address. + bool addr_index =7; + // When `slp_index` is true, the node has the slp index enabled and may + // be used with slp related rpc methods and also causes slp metadata to be added + // in some of the existing rpc methods. + bool slp_index = 8; + // When `slp_graphsearch` is true, the node is able to handle calls to slp graph search + bool slp_graphsearch = 9; +} + +message GetBlockInfoRequest { + oneof hash_or_height { + // The block hash as a byte array or base64 encoded string, little-endian. + bytes hash = 1; + // The block number. + int32 height = 2; + } +} +message GetBlockInfoResponse { + // Marshaled block header data, as well as metadata. + BlockInfo info = 1; +} + +message GetBlockRequest { + oneof hash_or_height { + // The block hash as a byte array or base64 encoded string, little-endian. + bytes hash = 1; + // The block number. + int32 height = 2; + } + // When `full_transactions` is true, full transactions are returned + // instead of just hashes. Default is false. + bool full_transactions = 3; +} +message GetBlockResponse { + // A marshaled block. + Block block = 1; +} + +message GetRawBlockRequest { + oneof hash_or_height { + // The block hash as a byte array or base64 encoded string, little-endian. + bytes hash = 1; + // The block number. + int32 height = 2; + } +} +message GetRawBlockResponse { + // Raw block data (with header) serialized according the the bitcoin block protocol. + bytes block = 1; +} + +message GetBlockFilterRequest { + oneof hash_or_height { + // The block hash as a byte array or base64 encoded string, little-endian. + bytes hash = 1; + // The block number. + int32 height = 2; + } +} + +message GetBlockFilterResponse { + // A compact filter matching input outpoints and public key scripts contained + // in a block (encoded according to BIP158). + bytes filter = 1; +} + +// Request headers using a list of known block hashes. +message GetHeadersRequest { + // A list of block hashes known to the client (most recent first) which + // is exponentially sparser toward the genesis block (0), little-endian. + // Common practice is to include all of the last 10 blocks, and then + // 9 blocks for each order of ten thereafter. + repeated bytes block_locator_hashes = 1; + // hash of the latest desired block header, little-endian; only blocks + // occurring before the stop will be returned. + bytes stop_hash = 2; +} +message GetHeadersResponse { + // List of block headers. + repeated BlockInfo headers = 1; +} + +// Get a transaction from a transaction hash. +message GetTransactionRequest { + // A transaction hash, little-endian. + bytes hash = 1; + + bool include_token_metadata = 2; +} +message GetTransactionResponse { + // A marshaled transaction. + Transaction transaction = 1; + + SlpTokenMetadata token_metadata = 2; +} + +// Get an encoded transaction from a transaction hash. +message GetRawTransactionRequest { + // A transaction hash, little-endian. + bytes hash = 1; +} +message GetRawTransactionResponse { + // Raw transaction in bytes. + bytes transaction = 1; +} + +// Get marshaled transactions related to a specific address. +// +// RECOMMENDED: +// Parameters have been provided to query without creating +// performance issues on the node or client. +// +// - The number of transactions to skip and fetch allow for iterating +// over a large set of transactions, if necessary. +// +// - A starting block parameter (either `hash` or `height`) +// may then be used to filter results to those occurring +// after a certain time. +// +// This approach will reduce network traffic and response processing +// for the client, as well as reduce workload on the node. +message GetAddressTransactionsRequest { + // The address to query transactions, in lowercase cashaddr format. + // The network prefix is optional (i.e. "cashaddress:"). + string address = 1; + + // The number of confirmed transactions to skip, starting with the oldest first. + // Does not affect results of unconfirmed transactions. + uint32 nb_skip = 2; + // Specify the number of transactions to fetch. + uint32 nb_fetch = 3; + + + oneof start_block { + // Recommended. Only get transactions after (or within) a + // starting block identified by hash, little-endian. + bytes hash = 4; + // Recommended. Only get transactions after (or within) a + // starting block identified by block number. + int32 height = 5; + } +} +message GetAddressTransactionsResponse { + // Transactions that have been included in a block. + repeated Transaction confirmed_transactions = 1; + // Transactions in mempool which have not been included in a block. + repeated MempoolTransaction unconfirmed_transactions = 2; +} + +// Get encoded transactions related to a specific address. +// +// RECOMMENDED: +// Parameters have been provided to query without creating +// performance issues on the node or client. +// +// - The number of transactions to skip and fetch allow for iterating +// over a large set of transactions, if necessary. +// +// - A starting block parameter (either `hash` or `height`) +// may then be used to filter results to those occurring +// after a certain time. +// +// This approach will reduce network traffic and response processing +// for the client, as well as reduce workload on the node. +message GetRawAddressTransactionsRequest { + // The address to query transactions, in lowercase cashaddr format. + // The network prefix is optional (i.e. "cashaddress:"). + string address = 1; + + // The number of confirmed transactions to skip, starting with the oldest first. + // Does not affect results of unconfirmed transactions. + uint32 nb_skip = 2; + // Specify the number of transactions to fetch. + uint32 nb_fetch = 3; + + oneof start_block { + // Recommended. Only return transactions after some starting block + // identified by hash, little-endian. + bytes hash = 4; + // Recommended. Only return transactions after some starting block + // identified by block number. + int32 height = 5; + } +} +message GetRawAddressTransactionsResponse { + // Transactions that have been included in a block. + repeated bytes confirmed_transactions = 1; + // Transactions in mempool which have not been included in a block. + repeated bytes unconfirmed_transactions = 2; +} + +message GetAddressUnspentOutputsRequest { + // The address to query transactions, in lowercase cashaddr format. + // The network identifier is optional (i.e. "cashaddress:"). + string address = 1; + // When `include_mempool` is true, unconfirmed transactions from mempool + // are returned. Default is false. + bool include_mempool = 2; + bool include_token_metadata = 3; +} +message GetAddressUnspentOutputsResponse { + // List of unspent outputs. + repeated UnspentOutput outputs = 1; + repeated SlpTokenMetadata token_metadata = 2; +} + +message GetUnspentOutputRequest { + // The hash of the transaction, little-endian. + bytes hash = 1; + // The number of the output, starting from zero. + uint32 index = 2; + // When include_mempool is true, unconfirmed transactions from mempool + // are returned. Default is false. + bool include_mempool = 3; + bool include_token_metadata = 4; +} +message GetUnspentOutputResponse { + // A reference to the related input. + Transaction.Input.Outpoint outpoint = 1; + // Locking script dictating how funds can be spent in the future + bytes pubkey_script = 2; + // Amount in satoshi. + int64 value = 3; + // When is_coinbase is true, the transaction was the first in a block, + // created by a miner, and used to pay the block reward + bool is_coinbase = 4; + // The index number of the block containing the transaction creating the output. + int32 block_height = 5; + + SlpToken slp_token = 6; + SlpTokenMetadata token_metadata = 7; +} + +message GetMerkleProofRequest { + // A transaction hash, little-endian. + bytes transaction_hash = 1; +} +message GetMerkleProofResponse { + // Block header information for the corresponding transaction + BlockInfo block = 1; + // A list containing the transaction hash, the adjacent leaf transaction hash + // and the hashes of the highest nodes in the merkle tree not built with the transaction. + // Proof hashes are ordered following transaction order, or left to right on the merkle tree + repeated bytes hashes = 2; + // Binary representing the location of the matching transaction in the full merkle tree, + // starting with the root (`1`) at position/level 0, where `1` corresponds + // to a left branch and `01` is a right branch. + bytes flags = 3; +} + +message SubmitTransactionRequest { + // The encoded transaction. + bytes transaction = 1; + bool skip_slp_validity_check = 2; + repeated SlpRequiredBurn required_slp_burns = 3; +} +message SubmitTransactionResponse { + // Transaction hash, little-endian. + bytes hash = 1; +} + +message CheckSlpTransactionRequest { + bytes transaction = 1; + repeated SlpRequiredBurn required_slp_burns = 2; + + // Using the slp specification as a basis for validity judgement can lead to confusion for new users and + // result in accidental token burns. use_spec_validity_judgement will cause the response's is_valid property + // to be returned according to the slp specification. Therefore, use_spec_validity_judgement is false by + // default in order to avoid accidental token burns. When use_spec_validity_judgement is false we return + // invalid in any case which would result in a burned token, unless the burn is explicitly included as an + // item in required_slp_burns property. + // + // When use_spec_validity_judgement is true, there are three cases where the is_valid response property + // will be returned as valid, instead of invalid, as per the slp specification. + // 1) inputs > outputs + // 2) missing transaction outputs + // 3) burned inputs from other tokens + // + // required_slp_burns is not used when use_spec_validity_judgement is set to true. + // + bool use_spec_validity_judgement = 3; +} + +message CheckSlpTransactionResponse { + bool is_valid = 1; + string invalid_reason = 2; + int32 best_height = 3; +} + +// Request to subscribe or unsubscribe from a stream of transactions. +message SubscribeTransactionsRequest { + // Subscribe to a filter. add items to a filter + TransactionFilter subscribe = 1; + // Unsubscribe to a filter, remove items from a filter + TransactionFilter unsubscribe = 2; + + // When include_mempool is true, new unconfirmed transactions from mempool are + // included apart from the ones confirmed in a block. + bool include_mempool = 3; + + // When include_in_block is true, transactions are included when they are confirmed. + // This notification is sent in addition to any requested mempool notifications. + bool include_in_block = 4; + + // When serialize_tx is true, transactions are serialized using + // bitcoin protocol encoding. Default is false, transaction will be Marshaled + // (see `Transaction`, `MempoolTransaction` and `TransactionNotification`) + bool serialize_tx = 5; +} + +// Options to define data structure to be sent by SubscribeBlock stream: +// +// - BlockInfo (block metadata): `BlockInfo` +// - SubscribeBlocksRequest {} +// +// - Marshaled Block (with transaction hashes): `Block` +// - SubscribeBlocksRequest { +// full_block = true +// } +// - Marshaled Block (with full transaction data): `Block` +// - SubscribeBlocksRequest { +// full_block = true +// full_transactions = true +// } +// - Serialized Block acccording to bitcoin protocol encoding: `bytes` +// - SubscribeBlocksRequest { +// serialize_block = true +// } +message SubscribeBlocksRequest { + // When full_block is true, a complete marshaled block is sent. See `Block`. + // Default is false, block metadata is sent. See `BlockInfo`. + bool full_block = 1; + + // When full_transactions is true, provide full transaction info + // for a marshaled block. + // Default is false, only the transaction hashes are included for + // a marshaled block. See `TransactionData`. + bool full_transactions = 2; + + // When serialize_block is true, blocks are serialized using bitcoin protocol encoding. + // Default is false, block will be Marshaled (see `BlockInfo` and `BlockNotification`) + bool serialize_block = 3; +} + +message GetSlpTokenMetadataRequest { + repeated bytes token_ids = 1; +} + +message GetSlpTokenMetadataResponse { + repeated SlpTokenMetadata token_metadata = 1; +} + +message GetSlpParsedScriptRequest { + bytes slp_opreturn_script = 1; +} + +message GetSlpParsedScriptResponse { + string parsing_error = 1; + bytes token_id = 2; + SlpAction slp_action = 3; + SlpTokenType token_type = 4; + oneof slp_metadata { + SlpV1GenesisMetadata v1_genesis = 5; // NFT1 Group also uses this + SlpV1MintMetadata v1_mint = 6; // NFT1 Group also uses this + SlpV1SendMetadata v1_send = 7; // NFT1 Group also uses this + SlpV1Nft1ChildGenesisMetadata v1_nft1_child_genesis = 8; + SlpV1Nft1ChildSendMetadata v1_nft1_child_send = 9; + } +} + +message GetSlpTrustedValidationRequest { + message Query { + bytes prev_out_hash = 1; + uint32 prev_out_vout = 2; + repeated bytes graphsearch_valid_hashes = 3; + } + repeated Query queries = 1; + bool include_graphsearch_count = 2; +} + +message GetSlpTrustedValidationResponse { + message ValidityResult { + bytes prev_out_hash = 1; + uint32 prev_out_vout = 2; + bytes token_id = 3; + SlpAction slp_action = 4; + SlpTokenType token_type = 5; + oneof validity_result_type { + uint64 v1_token_amount = 6 [jstype = JS_STRING]; + bool v1_mint_baton = 7; + } + bytes slp_txn_opreturn = 8; + uint32 graphsearch_txn_count = 9; + } + + repeated ValidityResult results = 1; +} + +message GetSlpGraphSearchRequest { + bytes hash = 1; + repeated bytes valid_hashes = 2; +} + +message GetSlpGraphSearchResponse { + repeated bytes txdata = 1; +} + +// NOTIFICATIONS + +message BlockNotification { + // State of the block in relation to the chain. + enum Type { + CONNECTED = 0; + DISCONNECTED = 1; + } + + // Whether the block is connected to the chain. + Type type = 1; + oneof block { + // Marshaled block header data, as well as metadata stored by the node. + BlockInfo block_info = 2; + // A Block. + Block marshaled_block = 3; + // Binary block, serialized using bitcoin protocol encoding. + bytes serialized_block = 4; + } +} + +message TransactionNotification { + // State of the transaction acceptance. + enum Type { + // A transaction in mempool. + UNCONFIRMED = 0; + // A transaction in a block. + CONFIRMED = 1; + } + + // Whether or not the transaction has been included in a block. + Type type = 1; + oneof transaction { + // A transaction included in a block. + Transaction confirmed_transaction = 2; + // A transaction in mempool. + MempoolTransaction unconfirmed_transaction = 3; + // Binary transaction, serialized using bitcoin protocol encoding. + bytes serialized_transaction = 4; + } +} + + +// DATA MESSAGES + +// Metadata for identifying and validating a block +message BlockInfo { + // Identification. + + // The double sha256 hash of the six header fields in the first 80 bytes + // of the block, when encoded according the bitcoin protocol, little-endian. + // sha256(sha256(encoded_header)) + bytes hash = 1; + // The block number, an incremental index for each block mined. + int32 height = 2; + + // Block header data. + + // A version number to track software/protocol upgrades. + int32 version = 3; + // Hash of the previous block, little-endian. + bytes previous_block = 4; + // The root of the Merkle Tree built from all transactions in the block, little-endian. + bytes merkle_root = 5; + // When mining of the block started, expressed in seconds since 1970-01-01. + int64 timestamp = 6; + // Difficulty in Compressed Target Format. + uint32 bits = 7; + // A random value that was generated during block mining which happened to + // result in a computed block hash below the difficulty target at the time. + uint32 nonce = 8; + + // Metadata. + + // Number of blocks in a chain, including the block itself upon creation. + int32 confirmations = 9; + // Difficulty target at time of creation. + double difficulty = 10; + // Hash of the next block in this chain, little-endian. + bytes next_block_hash = 11; + // Size of the block in bytes. + int32 size = 12; + // The median block time of the latest 11 block timestamps. + int64 median_time = 13; +} + +message Block { + message TransactionData { + oneof txids_or_txs { + // Just the transaction hash, little-endian. + bytes transaction_hash = 1; + // A marshaled transaction. + Transaction transaction = 2; + } + } + // Block header data, as well as metadata stored by the node. + BlockInfo info = 1; + // List of transactions or transaction hashes. + repeated TransactionData transaction_data = 2; +} + +message Transaction { + message Input { + message Outpoint { + // The hash of the transaction containing the output to be spent, little-endian + bytes hash = 1; + // The index of specific output on the transaction. + uint32 index = 2; + } + // The number of the input, starting from zero. + uint32 index = 1; + // The related outpoint. + Outpoint outpoint = 2; + // An unlocking script asserting a transaction is permitted to spend + // the Outpoint (UTXO) + bytes signature_script = 3; + // As of BIP-68, the sequence number is interpreted as a relative + // lock-time for the input. + uint32 sequence = 4; + // Amount in satoshi. + int64 value = 5; + // The pubkey_script of the previous output that is being spent. + bytes previous_script = 6; + // The bitcoin addresses associated with this input. + string address = 7; + SlpToken slp_token = 8; + } + + message Output { + // The number of the output, starting from zero. + uint32 index = 1; + // The number of satoshis to be transferred. + int64 value = 2; + // The public key script used to pay coins. + bytes pubkey_script = 3; + // The bitcoin addresses associated with this output. + string address = 4; + // The type of script. + string script_class = 5; + // The script expressed in Bitcoin Cash Script. + string disassembled_script = 6; + SlpToken slp_token = 7; + } + + // The double sha256 hash of the encoded transaction, little-endian. + // sha256(sha256(encoded_transaction)) + bytes hash = 1; + // The version of the transaction format. + int32 version = 2; + // List of inputs. + repeated Input inputs = 3; + // List of outputs. + repeated Output outputs = 4; + // The block height or timestamp after which this transaction is allowed. + // If value is greater than 500 million, it is assumed to be an epoch timestamp, + // otherwise it is treated as a block-height. Default is zero, or lock. + uint32 lock_time = 5; + + // Metadata + + // The size of the transaction in bytes. + int32 size = 8; + // When the transaction was included in a block, in epoch time. + int64 timestamp = 9; + // Number of blocks including proof of the transaction, including + // the block it appeared. + int32 confirmations = 10; + // Number of the block containing the transaction. + int32 block_height = 11; + // Hash of the block the transaction was recorded in, little-endian. + bytes block_hash = 12; + + SlpTransactionInfo slp_transaction_info = 13; +} + +message MempoolTransaction { + Transaction transaction = 1; + // The time when the transaction was added too the pool. + int64 added_time = 2; + // The block height when the transaction was added to the pool. + int32 added_height = 3; + // The total fee in satoshi the transaction pays. + int64 fee = 4; + // The fee in satoshi per kilobyte the transaction pays. + int64 fee_per_kb = 5; + // The priority of the transaction when it was added to the pool. + double starting_priority = 6; +} + +message UnspentOutput { + // A reference to the output given by transaction hash and index. + Transaction.Input.Outpoint outpoint = 1; + // The public key script used to pay coins. + bytes pubkey_script = 2; + // The amount in satoshis + int64 value = 3; + // When is_coinbase is true, the output is the first in the block, + // a generation transaction, the result of mining. + bool is_coinbase = 4; + // The block number containing the UXTO. + int32 block_height = 5; + + SlpToken slp_token = 6; +} + +message TransactionFilter { + // Filter by address(es) + repeated string addresses = 1; + + // Filter by output hash and index. + repeated Transaction.Input.Outpoint outpoints = 2; + + // Filter by data elements contained in pubkey scripts. + repeated bytes data_elements = 3; + + // Subscribed/Unsubscribe to everything. Other filters + // will be ignored. + bool all_transactions = 4; + + // Subscribed/Unsubscribe to everything slp. Other filters + // will be ignored, except this filter will be overriden by all_transactions=true + bool all_slp_transactions = 5; + + // only transactions associated with the included tokenIds + repeated bytes slp_token_ids = 6; +} + +// SlpToken info used in transaction inputs / outputs +// +// WARNING: Some languages (e.g., JavaScript) may not properly handle the 'uint64' +// for large amounts. For this reason, an annotation has been added for JS to +// return a string for the amount field instead of casting uint64 to the JS 'number' +// type. Other languages may require similar treatment. +// +message SlpToken { + bytes token_id = 1; + uint64 amount = 2 [jstype = JS_STRING]; + bool is_mint_baton = 3; + string address = 4; + uint32 decimals = 5; + SlpAction slp_action = 6; + SlpTokenType token_type = 7; +} + +enum SlpTokenType { + VERSION_NOT_SET = 0; + V1_FUNGIBLE = 1; + V1_NFT1_CHILD = 65; + V1_NFT1_GROUP = 129; +} + +// SlpTransactionInfo is used inside the Transaction message type. +message SlpTransactionInfo { + SlpAction slp_action = 1; + enum ValidityJudgement { + UNKNOWN_OR_INVALID = 0; + VALID = 1; + } + ValidityJudgement validity_judgement = 2; + string parse_error = 3; + bytes token_id = 4; + enum BurnFlags { + BURNED_INPUTS_OUTPUTS_TOO_HIGH = 0; + BURNED_INPUTS_BAD_OPRETURN = 1; + BURNED_INPUTS_OTHER_TOKEN = 2; + BURNED_OUTPUTS_MISSING_BCH_VOUT = 3; + BURNED_INPUTS_GREATER_THAN_OUTPUTS = 4; + } + repeated BurnFlags burn_flags = 5; + oneof tx_metadata { + SlpV1GenesisMetadata v1_genesis = 6; // NFT1 Group also uses this + SlpV1MintMetadata v1_mint = 7; // NFT1 Group also uses this + SlpV1SendMetadata v1_send = 8; // NFT1 Group also uses this + SlpV1Nft1ChildGenesisMetadata v1_nft1_child_genesis = 9; + SlpV1Nft1ChildSendMetadata v1_nft1_child_send = 10; + } +} + +// SlpV1GenesisMetadata is used to marshal type 1 and NFT1 Group GENESIS OP_RETURN scriptPubKey +message SlpV1GenesisMetadata { + bytes name = 1; + bytes ticker = 2; + bytes document_url = 3; + bytes document_hash = 4; + uint32 decimals = 5; + uint32 mint_baton_vout = 6; + uint64 mint_amount = 7 [jstype = JS_STRING]; +} + +// SlpV1MintMetadata is used to marshal type 1 MINT OP_RETURN scriptPubKey +message SlpV1MintMetadata { + uint32 mint_baton_vout = 1; + uint64 mint_amount = 2 [jstype = JS_STRING]; +} + +// SlpV1SendMetadata is used to marshal type 1 and NFT1 Group SEND OP_RETURN scriptPubKey +message SlpV1SendMetadata { + repeated uint64 amounts = 1 [jstype = JS_STRING]; +} + +// SlpV1Nft1ChildGenesisMetadata is used to marshal NFT1 Child GENESIS OP_RETURN scriptPubKey +message SlpV1Nft1ChildGenesisMetadata { + bytes name = 1; + bytes ticker = 2; + bytes document_url = 3; + bytes document_hash = 4; + uint32 decimals = 5; + bytes group_token_id = 6; +} + +// SlpV1Nft1ChildSendMetadata is used to marshal NFT1 Child SEND OP_RETURN scriptPubKey +message SlpV1Nft1ChildSendMetadata { + bytes group_token_id = 1; +} + +// SlpAction is used to allow clients to identify the type of slp transaction from this single field. +// +// NOTE: All enum types except for "NON_SLP" may be annotated with one or more BurnFlags. +// +enum SlpAction { + NON_SLP = 0; + NON_SLP_BURN = 1; + SLP_PARSE_ERROR = 2; + SLP_UNSUPPORTED_VERSION = 3; + SLP_V1_GENESIS = 4; + SLP_V1_MINT = 5; + SLP_V1_SEND = 6; + SLP_V1_NFT1_GROUP_GENESIS = 7; + SLP_V1_NFT1_GROUP_MINT = 8; + SLP_V1_NFT1_GROUP_SEND = 9; + SLP_V1_NFT1_UNIQUE_CHILD_GENESIS = 10; + SLP_V1_NFT1_UNIQUE_CHILD_SEND = 11; +} + +// SlpTokenMetadata is used to marshal metadata about a specific TokenID +message SlpTokenMetadata { + bytes token_id = 1; + SlpTokenType token_type = 2; + oneof type_metadata { + V1Fungible v1_fungible = 3; + V1NFT1Group v1_nft1_group = 4; + V1NFT1Child v1_nft1_child = 5; + } + + // V1Fungible is used to marshal metadata specific to Type 1 token IDs + message V1Fungible { + string token_ticker = 1; + string token_name = 2; + string token_document_url = 3; + bytes token_document_hash = 4; + uint32 decimals = 5; + bytes mint_baton_hash = 6; + uint32 mint_baton_vout = 7; + } + + // V1NFT1Group is used to marshal metadata specific to NFT1 Group token IDs + message V1NFT1Group { + string token_ticker = 1; + string token_name = 2; + string token_document_url = 3; + bytes token_document_hash = 4; + uint32 decimals = 5; + bytes mint_baton_hash = 6; + uint32 mint_baton_vout = 7; + } + + // V1NFT1Child is used to marshal metadata specific to NFT1 Child token IDs + message V1NFT1Child { + string token_ticker = 1; + string token_name = 2; + string token_document_url = 3; + bytes token_document_hash = 4; + bytes group_id = 5; + } +} + +// SlpRequiredBurn is used by clients to allow token burning +message SlpRequiredBurn { + Transaction.Input.Outpoint outpoint = 1; + bytes token_id = 2; + SlpTokenType token_type = 3; + oneof burn_intention { + uint64 amount = 4 [jstype = JS_STRING]; + uint32 mint_baton_vout = 5; + } +} diff --git a/mm2src/coins/utxo/pb.rs b/mm2src/coins/utxo/pb.rs new file mode 100644 index 0000000000..61fb9a7e42 --- /dev/null +++ b/mm2src/coins/utxo/pb.rs @@ -0,0 +1,1229 @@ +// RPC MESSAGES + +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetMempoolInfoRequest { +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetMempoolInfoResponse { + /// The count of transactions in the mempool + #[prost(uint32, tag="1")] + pub size: u32, + /// The size in bytes of all transactions in the mempool + #[prost(uint32, tag="2")] + pub bytes: u32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetMempoolRequest { + /// When `full_transactions` is true, full transaction data is provided + /// instead of just transaction hashes. Default is false. + #[prost(bool, tag="1")] + pub full_transactions: bool, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetMempoolResponse { + /// List of unconfirmed transactions. + #[prost(message, repeated, tag="1")] + pub transaction_data: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `GetMempoolResponse`. +pub mod get_mempool_response { + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct TransactionData { + /// Either one of the two following is provided, depending on the request. + #[prost(oneof="transaction_data::TxidsOrTxs", tags="1, 2")] + pub txids_or_txs: ::core::option::Option, + } + /// Nested message and enum types in `TransactionData`. + pub mod transaction_data { + /// Either one of the two following is provided, depending on the request. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum TxidsOrTxs { + /// The transaction hash, little-endian. + #[prost(bytes, tag="1")] + TransactionHash(::prost::alloc::vec::Vec), + /// The transaction data. + #[prost(message, tag="2")] + Transaction(super::super::Transaction), + } + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetBlockchainInfoRequest { +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetBlockchainInfoResponse { + /// Which network the node is operating on. + #[prost(enumeration="get_blockchain_info_response::BitcoinNet", tag="1")] + pub bitcoin_net: i32, + /// The current number of blocks on the longest chain. + #[prost(int32, tag="2")] + pub best_height: i32, + /// The hash of the best (tip) block in the most-work fully-validated chain, little-endian. + #[prost(bytes="vec", tag="3")] + pub best_block_hash: ::prost::alloc::vec::Vec, + /// Threshold for adding new blocks. + #[prost(double, tag="4")] + pub difficulty: f64, + /// Median time of the last 11 blocks. + #[prost(int64, tag="5")] + pub median_time: i64, + /// When `tx_index` is true, the node has full transaction index enabled. + #[prost(bool, tag="6")] + pub tx_index: bool, + /// When `addr_index` is true, the node has address index enabled and may + /// be used with call related by address. + #[prost(bool, tag="7")] + pub addr_index: bool, + /// When `slp_index` is true, the node has the slp index enabled and may + /// be used with slp related rpc methods and also causes slp metadata to be added + /// in some of the existing rpc methods. + #[prost(bool, tag="8")] + pub slp_index: bool, + /// When `slp_graphsearch` is true, the node is able to handle calls to slp graph search + #[prost(bool, tag="9")] + pub slp_graphsearch: bool, +} +/// Nested message and enum types in `GetBlockchainInfoResponse`. +pub mod get_blockchain_info_response { + /// Bitcoin network types + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum BitcoinNet { + /// Live public network with monetary value. + Mainnet = 0, + /// An isolated environment for automated testing. + Regtest = 1, + /// A public environment where monetary value is agreed to be zero, + /// and some checks for transaction conformity are disabled. + Testnet3 = 2, + /// Private testnets for large scale simulations (or stress testing), + /// where a specified list of nodes is used, rather than node discovery. + Simnet = 3, + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetBlockInfoRequest { + #[prost(oneof="get_block_info_request::HashOrHeight", tags="1, 2")] + pub hash_or_height: ::core::option::Option, +} +/// Nested message and enum types in `GetBlockInfoRequest`. +pub mod get_block_info_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum HashOrHeight { + /// The block hash as a byte array or base64 encoded string, little-endian. + #[prost(bytes, tag="1")] + Hash(::prost::alloc::vec::Vec), + /// The block number. + #[prost(int32, tag="2")] + Height(i32), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetBlockInfoResponse { + /// Marshaled block header data, as well as metadata. + #[prost(message, optional, tag="1")] + pub info: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetBlockRequest { + /// When `full_transactions` is true, full transactions are returned + /// instead of just hashes. Default is false. + #[prost(bool, tag="3")] + pub full_transactions: bool, + #[prost(oneof="get_block_request::HashOrHeight", tags="1, 2")] + pub hash_or_height: ::core::option::Option, +} +/// Nested message and enum types in `GetBlockRequest`. +pub mod get_block_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum HashOrHeight { + /// The block hash as a byte array or base64 encoded string, little-endian. + #[prost(bytes, tag="1")] + Hash(::prost::alloc::vec::Vec), + /// The block number. + #[prost(int32, tag="2")] + Height(i32), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetBlockResponse { + /// A marshaled block. + #[prost(message, optional, tag="1")] + pub block: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetRawBlockRequest { + #[prost(oneof="get_raw_block_request::HashOrHeight", tags="1, 2")] + pub hash_or_height: ::core::option::Option, +} +/// Nested message and enum types in `GetRawBlockRequest`. +pub mod get_raw_block_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum HashOrHeight { + /// The block hash as a byte array or base64 encoded string, little-endian. + #[prost(bytes, tag="1")] + Hash(::prost::alloc::vec::Vec), + /// The block number. + #[prost(int32, tag="2")] + Height(i32), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetRawBlockResponse { + /// Raw block data (with header) serialized according the the bitcoin block protocol. + #[prost(bytes="vec", tag="1")] + pub block: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetBlockFilterRequest { + #[prost(oneof="get_block_filter_request::HashOrHeight", tags="1, 2")] + pub hash_or_height: ::core::option::Option, +} +/// Nested message and enum types in `GetBlockFilterRequest`. +pub mod get_block_filter_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum HashOrHeight { + /// The block hash as a byte array or base64 encoded string, little-endian. + #[prost(bytes, tag="1")] + Hash(::prost::alloc::vec::Vec), + /// The block number. + #[prost(int32, tag="2")] + Height(i32), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetBlockFilterResponse { + /// A compact filter matching input outpoints and public key scripts contained + /// in a block (encoded according to BIP158). + #[prost(bytes="vec", tag="1")] + pub filter: ::prost::alloc::vec::Vec, +} +/// Request headers using a list of known block hashes. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetHeadersRequest { + /// A list of block hashes known to the client (most recent first) which + /// is exponentially sparser toward the genesis block (0), little-endian. + /// Common practice is to include all of the last 10 blocks, and then + /// 9 blocks for each order of ten thereafter. + #[prost(bytes="vec", repeated, tag="1")] + pub block_locator_hashes: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + /// hash of the latest desired block header, little-endian; only blocks + /// occurring before the stop will be returned. + #[prost(bytes="vec", tag="2")] + pub stop_hash: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetHeadersResponse { + /// List of block headers. + #[prost(message, repeated, tag="1")] + pub headers: ::prost::alloc::vec::Vec, +} +/// Get a transaction from a transaction hash. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetTransactionRequest { + /// A transaction hash, little-endian. + #[prost(bytes="vec", tag="1")] + pub hash: ::prost::alloc::vec::Vec, + #[prost(bool, tag="2")] + pub include_token_metadata: bool, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetTransactionResponse { + /// A marshaled transaction. + #[prost(message, optional, tag="1")] + pub transaction: ::core::option::Option, + #[prost(message, optional, tag="2")] + pub token_metadata: ::core::option::Option, +} +/// Get an encoded transaction from a transaction hash. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetRawTransactionRequest { + /// A transaction hash, little-endian. + #[prost(bytes="vec", tag="1")] + pub hash: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetRawTransactionResponse { + /// Raw transaction in bytes. + #[prost(bytes="vec", tag="1")] + pub transaction: ::prost::alloc::vec::Vec, +} +/// Get marshaled transactions related to a specific address. +/// +/// RECOMMENDED: +/// Parameters have been provided to query without creating +/// performance issues on the node or client. +/// +/// - The number of transactions to skip and fetch allow for iterating +/// over a large set of transactions, if necessary. +/// +/// - A starting block parameter (either `hash` or `height`) +/// may then be used to filter results to those occurring +/// after a certain time. +/// +/// This approach will reduce network traffic and response processing +/// for the client, as well as reduce workload on the node. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAddressTransactionsRequest { + /// The address to query transactions, in lowercase cashaddr format. + /// The network prefix is optional (i.e. "cashaddress:"). + #[prost(string, tag="1")] + pub address: ::prost::alloc::string::String, + /// The number of confirmed transactions to skip, starting with the oldest first. + /// Does not affect results of unconfirmed transactions. + #[prost(uint32, tag="2")] + pub nb_skip: u32, + /// Specify the number of transactions to fetch. + #[prost(uint32, tag="3")] + pub nb_fetch: u32, + #[prost(oneof="get_address_transactions_request::StartBlock", tags="4, 5")] + pub start_block: ::core::option::Option, +} +/// Nested message and enum types in `GetAddressTransactionsRequest`. +pub mod get_address_transactions_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum StartBlock { + /// Recommended. Only get transactions after (or within) a + /// starting block identified by hash, little-endian. + #[prost(bytes, tag="4")] + Hash(::prost::alloc::vec::Vec), + /// Recommended. Only get transactions after (or within) a + /// starting block identified by block number. + #[prost(int32, tag="5")] + Height(i32), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAddressTransactionsResponse { + /// Transactions that have been included in a block. + #[prost(message, repeated, tag="1")] + pub confirmed_transactions: ::prost::alloc::vec::Vec, + /// Transactions in mempool which have not been included in a block. + #[prost(message, repeated, tag="2")] + pub unconfirmed_transactions: ::prost::alloc::vec::Vec, +} +/// Get encoded transactions related to a specific address. +/// +/// RECOMMENDED: +/// Parameters have been provided to query without creating +/// performance issues on the node or client. +/// +/// - The number of transactions to skip and fetch allow for iterating +/// over a large set of transactions, if necessary. +/// +/// - A starting block parameter (either `hash` or `height`) +/// may then be used to filter results to those occurring +/// after a certain time. +/// +/// This approach will reduce network traffic and response processing +/// for the client, as well as reduce workload on the node. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetRawAddressTransactionsRequest { + /// The address to query transactions, in lowercase cashaddr format. + /// The network prefix is optional (i.e. "cashaddress:"). + #[prost(string, tag="1")] + pub address: ::prost::alloc::string::String, + /// The number of confirmed transactions to skip, starting with the oldest first. + /// Does not affect results of unconfirmed transactions. + #[prost(uint32, tag="2")] + pub nb_skip: u32, + /// Specify the number of transactions to fetch. + #[prost(uint32, tag="3")] + pub nb_fetch: u32, + #[prost(oneof="get_raw_address_transactions_request::StartBlock", tags="4, 5")] + pub start_block: ::core::option::Option, +} +/// Nested message and enum types in `GetRawAddressTransactionsRequest`. +pub mod get_raw_address_transactions_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum StartBlock { + /// Recommended. Only return transactions after some starting block + /// identified by hash, little-endian. + #[prost(bytes, tag="4")] + Hash(::prost::alloc::vec::Vec), + /// Recommended. Only return transactions after some starting block + /// identified by block number. + #[prost(int32, tag="5")] + Height(i32), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetRawAddressTransactionsResponse { + /// Transactions that have been included in a block. + #[prost(bytes="vec", repeated, tag="1")] + pub confirmed_transactions: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + /// Transactions in mempool which have not been included in a block. + #[prost(bytes="vec", repeated, tag="2")] + pub unconfirmed_transactions: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAddressUnspentOutputsRequest { + /// The address to query transactions, in lowercase cashaddr format. + /// The network identifier is optional (i.e. "cashaddress:"). + #[prost(string, tag="1")] + pub address: ::prost::alloc::string::String, + /// When `include_mempool` is true, unconfirmed transactions from mempool + /// are returned. Default is false. + #[prost(bool, tag="2")] + pub include_mempool: bool, + #[prost(bool, tag="3")] + pub include_token_metadata: bool, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAddressUnspentOutputsResponse { + /// List of unspent outputs. + #[prost(message, repeated, tag="1")] + pub outputs: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="2")] + pub token_metadata: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetUnspentOutputRequest { + /// The hash of the transaction, little-endian. + #[prost(bytes="vec", tag="1")] + pub hash: ::prost::alloc::vec::Vec, + /// The number of the output, starting from zero. + #[prost(uint32, tag="2")] + pub index: u32, + /// When include_mempool is true, unconfirmed transactions from mempool + /// are returned. Default is false. + #[prost(bool, tag="3")] + pub include_mempool: bool, + #[prost(bool, tag="4")] + pub include_token_metadata: bool, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetUnspentOutputResponse { + /// A reference to the related input. + #[prost(message, optional, tag="1")] + pub outpoint: ::core::option::Option, + /// Locking script dictating how funds can be spent in the future + #[prost(bytes="vec", tag="2")] + pub pubkey_script: ::prost::alloc::vec::Vec, + /// Amount in satoshi. + #[prost(int64, tag="3")] + pub value: i64, + /// When is_coinbase is true, the transaction was the first in a block, + /// created by a miner, and used to pay the block reward + #[prost(bool, tag="4")] + pub is_coinbase: bool, + /// The index number of the block containing the transaction creating the output. + #[prost(int32, tag="5")] + pub block_height: i32, + #[prost(message, optional, tag="6")] + pub slp_token: ::core::option::Option, + #[prost(message, optional, tag="7")] + pub token_metadata: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetMerkleProofRequest { + /// A transaction hash, little-endian. + #[prost(bytes="vec", tag="1")] + pub transaction_hash: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetMerkleProofResponse { + /// Block header information for the corresponding transaction + #[prost(message, optional, tag="1")] + pub block: ::core::option::Option, + /// A list containing the transaction hash, the adjacent leaf transaction hash + /// and the hashes of the highest nodes in the merkle tree not built with the transaction. + /// Proof hashes are ordered following transaction order, or left to right on the merkle tree + #[prost(bytes="vec", repeated, tag="2")] + pub hashes: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + /// Binary representing the location of the matching transaction in the full merkle tree, + /// starting with the root (`1`) at position/level 0, where `1` corresponds + /// to a left branch and `01` is a right branch. + #[prost(bytes="vec", tag="3")] + pub flags: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubmitTransactionRequest { + /// The encoded transaction. + #[prost(bytes="vec", tag="1")] + pub transaction: ::prost::alloc::vec::Vec, + #[prost(bool, tag="2")] + pub skip_slp_validity_check: bool, + #[prost(message, repeated, tag="3")] + pub required_slp_burns: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubmitTransactionResponse { + /// Transaction hash, little-endian. + #[prost(bytes="vec", tag="1")] + pub hash: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CheckSlpTransactionRequest { + #[prost(bytes="vec", tag="1")] + pub transaction: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="2")] + pub required_slp_burns: ::prost::alloc::vec::Vec, + /// Using the slp specification as a basis for validity judgement can lead to confusion for new users and + /// result in accidental token burns. use_spec_validity_judgement will cause the response's is_valid property + /// to be returned according to the slp specification. Therefore, use_spec_validity_judgement is false by + /// default in order to avoid accidental token burns. When use_spec_validity_judgement is false we return + /// invalid in any case which would result in a burned token, unless the burn is explicitly included as an + /// item in required_slp_burns property. + /// + /// When use_spec_validity_judgement is true, there are three cases where the is_valid response property + /// will be returned as valid, instead of invalid, as per the slp specification. + /// 1) inputs > outputs + /// 2) missing transaction outputs + /// 3) burned inputs from other tokens + /// + /// required_slp_burns is not used when use_spec_validity_judgement is set to true. + /// + #[prost(bool, tag="3")] + pub use_spec_validity_judgement: bool, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CheckSlpTransactionResponse { + #[prost(bool, tag="1")] + pub is_valid: bool, + #[prost(string, tag="2")] + pub invalid_reason: ::prost::alloc::string::String, + #[prost(int32, tag="3")] + pub best_height: i32, +} +/// Request to subscribe or unsubscribe from a stream of transactions. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribeTransactionsRequest { + /// Subscribe to a filter. add items to a filter + #[prost(message, optional, tag="1")] + pub subscribe: ::core::option::Option, + /// Unsubscribe to a filter, remove items from a filter + #[prost(message, optional, tag="2")] + pub unsubscribe: ::core::option::Option, + /// When include_mempool is true, new unconfirmed transactions from mempool are + /// included apart from the ones confirmed in a block. + #[prost(bool, tag="3")] + pub include_mempool: bool, + /// When include_in_block is true, transactions are included when they are confirmed. + /// This notification is sent in addition to any requested mempool notifications. + #[prost(bool, tag="4")] + pub include_in_block: bool, + /// When serialize_tx is true, transactions are serialized using + /// bitcoin protocol encoding. Default is false, transaction will be Marshaled + /// (see `Transaction`, `MempoolTransaction` and `TransactionNotification`) + #[prost(bool, tag="5")] + pub serialize_tx: bool, +} +/// Options to define data structure to be sent by SubscribeBlock stream: +/// +/// - BlockInfo (block metadata): `BlockInfo` +/// - SubscribeBlocksRequest {} +/// +/// - Marshaled Block (with transaction hashes): `Block` +/// - SubscribeBlocksRequest { +/// full_block = true +/// } +/// - Marshaled Block (with full transaction data): `Block` +/// - SubscribeBlocksRequest { +/// full_block = true +/// full_transactions = true +/// } +/// - Serialized Block acccording to bitcoin protocol encoding: `bytes` +/// - SubscribeBlocksRequest { +/// serialize_block = true +/// } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribeBlocksRequest { + /// When full_block is true, a complete marshaled block is sent. See `Block`. + /// Default is false, block metadata is sent. See `BlockInfo`. + #[prost(bool, tag="1")] + pub full_block: bool, + /// When full_transactions is true, provide full transaction info + /// for a marshaled block. + /// Default is false, only the transaction hashes are included for + /// a marshaled block. See `TransactionData`. + #[prost(bool, tag="2")] + pub full_transactions: bool, + /// When serialize_block is true, blocks are serialized using bitcoin protocol encoding. + /// Default is false, block will be Marshaled (see `BlockInfo` and `BlockNotification`) + #[prost(bool, tag="3")] + pub serialize_block: bool, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetSlpTokenMetadataRequest { + #[prost(bytes="vec", repeated, tag="1")] + pub token_ids: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetSlpTokenMetadataResponse { + #[prost(message, repeated, tag="1")] + pub token_metadata: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetSlpParsedScriptRequest { + #[prost(bytes="vec", tag="1")] + pub slp_opreturn_script: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetSlpParsedScriptResponse { + #[prost(string, tag="1")] + pub parsing_error: ::prost::alloc::string::String, + #[prost(bytes="vec", tag="2")] + pub token_id: ::prost::alloc::vec::Vec, + #[prost(enumeration="SlpAction", tag="3")] + pub slp_action: i32, + #[prost(enumeration="SlpTokenType", tag="4")] + pub token_type: i32, + #[prost(oneof="get_slp_parsed_script_response::SlpMetadata", tags="5, 6, 7, 8, 9")] + pub slp_metadata: ::core::option::Option, +} +/// Nested message and enum types in `GetSlpParsedScriptResponse`. +pub mod get_slp_parsed_script_response { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum SlpMetadata { + /// NFT1 Group also uses this + #[prost(message, tag="5")] + V1Genesis(super::SlpV1GenesisMetadata), + /// NFT1 Group also uses this + #[prost(message, tag="6")] + V1Mint(super::SlpV1MintMetadata), + /// NFT1 Group also uses this + #[prost(message, tag="7")] + V1Send(super::SlpV1SendMetadata), + #[prost(message, tag="8")] + V1Nft1ChildGenesis(super::SlpV1Nft1ChildGenesisMetadata), + #[prost(message, tag="9")] + V1Nft1ChildSend(super::SlpV1Nft1ChildSendMetadata), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetSlpTrustedValidationRequest { + #[prost(message, repeated, tag="1")] + pub queries: ::prost::alloc::vec::Vec, + #[prost(bool, tag="2")] + pub include_graphsearch_count: bool, +} +/// Nested message and enum types in `GetSlpTrustedValidationRequest`. +pub mod get_slp_trusted_validation_request { + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Query { + #[prost(bytes="vec", tag="1")] + pub prev_out_hash: ::prost::alloc::vec::Vec, + #[prost(uint32, tag="2")] + pub prev_out_vout: u32, + #[prost(bytes="vec", repeated, tag="3")] + pub graphsearch_valid_hashes: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetSlpTrustedValidationResponse { + #[prost(message, repeated, tag="1")] + pub results: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `GetSlpTrustedValidationResponse`. +pub mod get_slp_trusted_validation_response { + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct ValidityResult { + #[prost(bytes="vec", tag="1")] + pub prev_out_hash: ::prost::alloc::vec::Vec, + #[prost(uint32, tag="2")] + pub prev_out_vout: u32, + #[prost(bytes="vec", tag="3")] + pub token_id: ::prost::alloc::vec::Vec, + #[prost(enumeration="super::SlpAction", tag="4")] + pub slp_action: i32, + #[prost(enumeration="super::SlpTokenType", tag="5")] + pub token_type: i32, + #[prost(bytes="vec", tag="8")] + pub slp_txn_opreturn: ::prost::alloc::vec::Vec, + #[prost(uint32, tag="9")] + pub graphsearch_txn_count: u32, + #[prost(oneof="validity_result::ValidityResultType", tags="6, 7")] + pub validity_result_type: ::core::option::Option, + } + /// Nested message and enum types in `ValidityResult`. + pub mod validity_result { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum ValidityResultType { + #[prost(uint64, tag="6")] + V1TokenAmount(u64), + #[prost(bool, tag="7")] + V1MintBaton(bool), + } + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetSlpGraphSearchRequest { + #[prost(bytes="vec", tag="1")] + pub hash: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", repeated, tag="2")] + pub valid_hashes: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetSlpGraphSearchResponse { + #[prost(bytes="vec", repeated, tag="1")] + pub txdata: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +// NOTIFICATIONS + +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BlockNotification { + /// Whether the block is connected to the chain. + #[prost(enumeration="block_notification::Type", tag="1")] + pub r#type: i32, + #[prost(oneof="block_notification::Block", tags="2, 3, 4")] + pub block: ::core::option::Option, +} +/// Nested message and enum types in `BlockNotification`. +pub mod block_notification { + /// State of the block in relation to the chain. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum Type { + Connected = 0, + Disconnected = 1, + } + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Block { + /// Marshaled block header data, as well as metadata stored by the node. + #[prost(message, tag="2")] + BlockInfo(super::BlockInfo), + /// A Block. + #[prost(message, tag="3")] + MarshaledBlock(super::Block), + /// Binary block, serialized using bitcoin protocol encoding. + #[prost(bytes, tag="4")] + SerializedBlock(::prost::alloc::vec::Vec), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TransactionNotification { + /// Whether or not the transaction has been included in a block. + #[prost(enumeration="transaction_notification::Type", tag="1")] + pub r#type: i32, + #[prost(oneof="transaction_notification::Transaction", tags="2, 3, 4")] + pub transaction: ::core::option::Option, +} +/// Nested message and enum types in `TransactionNotification`. +pub mod transaction_notification { + /// State of the transaction acceptance. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum Type { + /// A transaction in mempool. + Unconfirmed = 0, + /// A transaction in a block. + Confirmed = 1, + } + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Transaction { + /// A transaction included in a block. + #[prost(message, tag="2")] + ConfirmedTransaction(super::Transaction), + /// A transaction in mempool. + #[prost(message, tag="3")] + UnconfirmedTransaction(super::MempoolTransaction), + /// Binary transaction, serialized using bitcoin protocol encoding. + #[prost(bytes, tag="4")] + SerializedTransaction(::prost::alloc::vec::Vec), + } +} +// DATA MESSAGES + +/// Metadata for identifying and validating a block +/// +/// Identification. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BlockInfo { + /// The double sha256 hash of the six header fields in the first 80 bytes + /// of the block, when encoded according the bitcoin protocol, little-endian. + /// sha256(sha256(encoded_header)) + #[prost(bytes="vec", tag="1")] + pub hash: ::prost::alloc::vec::Vec, + /// The block number, an incremental index for each block mined. + #[prost(int32, tag="2")] + pub height: i32, + // Block header data. + + /// A version number to track software/protocol upgrades. + #[prost(int32, tag="3")] + pub version: i32, + /// Hash of the previous block, little-endian. + #[prost(bytes="vec", tag="4")] + pub previous_block: ::prost::alloc::vec::Vec, + /// The root of the Merkle Tree built from all transactions in the block, little-endian. + #[prost(bytes="vec", tag="5")] + pub merkle_root: ::prost::alloc::vec::Vec, + /// When mining of the block started, expressed in seconds since 1970-01-01. + #[prost(int64, tag="6")] + pub timestamp: i64, + /// Difficulty in Compressed Target Format. + #[prost(uint32, tag="7")] + pub bits: u32, + /// A random value that was generated during block mining which happened to + /// result in a computed block hash below the difficulty target at the time. + #[prost(uint32, tag="8")] + pub nonce: u32, + // Metadata. + + /// Number of blocks in a chain, including the block itself upon creation. + #[prost(int32, tag="9")] + pub confirmations: i32, + /// Difficulty target at time of creation. + #[prost(double, tag="10")] + pub difficulty: f64, + /// Hash of the next block in this chain, little-endian. + #[prost(bytes="vec", tag="11")] + pub next_block_hash: ::prost::alloc::vec::Vec, + /// Size of the block in bytes. + #[prost(int32, tag="12")] + pub size: i32, + /// The median block time of the latest 11 block timestamps. + #[prost(int64, tag="13")] + pub median_time: i64, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Block { + /// Block header data, as well as metadata stored by the node. + #[prost(message, optional, tag="1")] + pub info: ::core::option::Option, + /// List of transactions or transaction hashes. + #[prost(message, repeated, tag="2")] + pub transaction_data: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `Block`. +pub mod block { + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct TransactionData { + #[prost(oneof="transaction_data::TxidsOrTxs", tags="1, 2")] + pub txids_or_txs: ::core::option::Option, + } + /// Nested message and enum types in `TransactionData`. + pub mod transaction_data { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum TxidsOrTxs { + /// Just the transaction hash, little-endian. + #[prost(bytes, tag="1")] + TransactionHash(::prost::alloc::vec::Vec), + /// A marshaled transaction. + #[prost(message, tag="2")] + Transaction(super::super::Transaction), + } + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Transaction { + /// The double sha256 hash of the encoded transaction, little-endian. + /// sha256(sha256(encoded_transaction)) + #[prost(bytes="vec", tag="1")] + pub hash: ::prost::alloc::vec::Vec, + /// The version of the transaction format. + #[prost(int32, tag="2")] + pub version: i32, + /// List of inputs. + #[prost(message, repeated, tag="3")] + pub inputs: ::prost::alloc::vec::Vec, + /// List of outputs. + #[prost(message, repeated, tag="4")] + pub outputs: ::prost::alloc::vec::Vec, + /// The block height or timestamp after which this transaction is allowed. + /// If value is greater than 500 million, it is assumed to be an epoch timestamp, + /// otherwise it is treated as a block-height. Default is zero, or lock. + #[prost(uint32, tag="5")] + pub lock_time: u32, + // Metadata + + /// The size of the transaction in bytes. + #[prost(int32, tag="8")] + pub size: i32, + /// When the transaction was included in a block, in epoch time. + #[prost(int64, tag="9")] + pub timestamp: i64, + /// Number of blocks including proof of the transaction, including + /// the block it appeared. + #[prost(int32, tag="10")] + pub confirmations: i32, + /// Number of the block containing the transaction. + #[prost(int32, tag="11")] + pub block_height: i32, + /// Hash of the block the transaction was recorded in, little-endian. + #[prost(bytes="vec", tag="12")] + pub block_hash: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag="13")] + pub slp_transaction_info: ::core::option::Option, +} +/// Nested message and enum types in `Transaction`. +pub mod transaction { + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Input { + /// The number of the input, starting from zero. + #[prost(uint32, tag="1")] + pub index: u32, + /// The related outpoint. + #[prost(message, optional, tag="2")] + pub outpoint: ::core::option::Option, + /// An unlocking script asserting a transaction is permitted to spend + /// the Outpoint (UTXO) + #[prost(bytes="vec", tag="3")] + pub signature_script: ::prost::alloc::vec::Vec, + /// As of BIP-68, the sequence number is interpreted as a relative + /// lock-time for the input. + #[prost(uint32, tag="4")] + pub sequence: u32, + /// Amount in satoshi. + #[prost(int64, tag="5")] + pub value: i64, + /// The pubkey_script of the previous output that is being spent. + #[prost(bytes="vec", tag="6")] + pub previous_script: ::prost::alloc::vec::Vec, + /// The bitcoin addresses associated with this input. + #[prost(string, tag="7")] + pub address: ::prost::alloc::string::String, + #[prost(message, optional, tag="8")] + pub slp_token: ::core::option::Option, + } + /// Nested message and enum types in `Input`. + pub mod input { + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Outpoint { + /// The hash of the transaction containing the output to be spent, little-endian + #[prost(bytes="vec", tag="1")] + pub hash: ::prost::alloc::vec::Vec, + /// The index of specific output on the transaction. + #[prost(uint32, tag="2")] + pub index: u32, + } + } + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Output { + /// The number of the output, starting from zero. + #[prost(uint32, tag="1")] + pub index: u32, + /// The number of satoshis to be transferred. + #[prost(int64, tag="2")] + pub value: i64, + /// The public key script used to pay coins. + #[prost(bytes="vec", tag="3")] + pub pubkey_script: ::prost::alloc::vec::Vec, + /// The bitcoin addresses associated with this output. + #[prost(string, tag="4")] + pub address: ::prost::alloc::string::String, + /// The type of script. + #[prost(string, tag="5")] + pub script_class: ::prost::alloc::string::String, + /// The script expressed in Bitcoin Cash Script. + #[prost(string, tag="6")] + pub disassembled_script: ::prost::alloc::string::String, + #[prost(message, optional, tag="7")] + pub slp_token: ::core::option::Option, + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MempoolTransaction { + #[prost(message, optional, tag="1")] + pub transaction: ::core::option::Option, + /// The time when the transaction was added too the pool. + #[prost(int64, tag="2")] + pub added_time: i64, + /// The block height when the transaction was added to the pool. + #[prost(int32, tag="3")] + pub added_height: i32, + /// The total fee in satoshi the transaction pays. + #[prost(int64, tag="4")] + pub fee: i64, + /// The fee in satoshi per kilobyte the transaction pays. + #[prost(int64, tag="5")] + pub fee_per_kb: i64, + /// The priority of the transaction when it was added to the pool. + #[prost(double, tag="6")] + pub starting_priority: f64, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnspentOutput { + /// A reference to the output given by transaction hash and index. + #[prost(message, optional, tag="1")] + pub outpoint: ::core::option::Option, + /// The public key script used to pay coins. + #[prost(bytes="vec", tag="2")] + pub pubkey_script: ::prost::alloc::vec::Vec, + /// The amount in satoshis + #[prost(int64, tag="3")] + pub value: i64, + /// When is_coinbase is true, the output is the first in the block, + /// a generation transaction, the result of mining. + #[prost(bool, tag="4")] + pub is_coinbase: bool, + /// The block number containing the UXTO. + #[prost(int32, tag="5")] + pub block_height: i32, + #[prost(message, optional, tag="6")] + pub slp_token: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TransactionFilter { + /// Filter by address(es) + #[prost(string, repeated, tag="1")] + pub addresses: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + /// Filter by output hash and index. + #[prost(message, repeated, tag="2")] + pub outpoints: ::prost::alloc::vec::Vec, + /// Filter by data elements contained in pubkey scripts. + #[prost(bytes="vec", repeated, tag="3")] + pub data_elements: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + /// Subscribed/Unsubscribe to everything. Other filters + /// will be ignored. + #[prost(bool, tag="4")] + pub all_transactions: bool, + /// Subscribed/Unsubscribe to everything slp. Other filters + /// will be ignored, except this filter will be overriden by all_transactions=true + #[prost(bool, tag="5")] + pub all_slp_transactions: bool, + /// only transactions associated with the included tokenIds + #[prost(bytes="vec", repeated, tag="6")] + pub slp_token_ids: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +/// SlpToken info used in transaction inputs / outputs +/// +/// WARNING: Some languages (e.g., JavaScript) may not properly handle the 'uint64' +/// for large amounts. For this reason, an annotation has been added for JS to +/// return a string for the amount field instead of casting uint64 to the JS 'number' +/// type. Other languages may require similar treatment. +/// +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SlpToken { + #[prost(bytes="vec", tag="1")] + pub token_id: ::prost::alloc::vec::Vec, + #[prost(uint64, tag="2")] + pub amount: u64, + #[prost(bool, tag="3")] + pub is_mint_baton: bool, + #[prost(string, tag="4")] + pub address: ::prost::alloc::string::String, + #[prost(uint32, tag="5")] + pub decimals: u32, + #[prost(enumeration="SlpAction", tag="6")] + pub slp_action: i32, + #[prost(enumeration="SlpTokenType", tag="7")] + pub token_type: i32, +} +/// SlpTransactionInfo is used inside the Transaction message type. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SlpTransactionInfo { + #[prost(enumeration="SlpAction", tag="1")] + pub slp_action: i32, + #[prost(enumeration="slp_transaction_info::ValidityJudgement", tag="2")] + pub validity_judgement: i32, + #[prost(string, tag="3")] + pub parse_error: ::prost::alloc::string::String, + #[prost(bytes="vec", tag="4")] + pub token_id: ::prost::alloc::vec::Vec, + #[prost(enumeration="slp_transaction_info::BurnFlags", repeated, tag="5")] + pub burn_flags: ::prost::alloc::vec::Vec, + #[prost(oneof="slp_transaction_info::TxMetadata", tags="6, 7, 8, 9, 10")] + pub tx_metadata: ::core::option::Option, +} +/// Nested message and enum types in `SlpTransactionInfo`. +pub mod slp_transaction_info { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum ValidityJudgement { + UnknownOrInvalid = 0, + Valid = 1, + } + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum BurnFlags { + BurnedInputsOutputsTooHigh = 0, + BurnedInputsBadOpreturn = 1, + BurnedInputsOtherToken = 2, + BurnedOutputsMissingBchVout = 3, + BurnedInputsGreaterThanOutputs = 4, + } + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum TxMetadata { + /// NFT1 Group also uses this + #[prost(message, tag="6")] + V1Genesis(super::SlpV1GenesisMetadata), + /// NFT1 Group also uses this + #[prost(message, tag="7")] + V1Mint(super::SlpV1MintMetadata), + /// NFT1 Group also uses this + #[prost(message, tag="8")] + V1Send(super::SlpV1SendMetadata), + #[prost(message, tag="9")] + V1Nft1ChildGenesis(super::SlpV1Nft1ChildGenesisMetadata), + #[prost(message, tag="10")] + V1Nft1ChildSend(super::SlpV1Nft1ChildSendMetadata), + } +} +/// SlpV1GenesisMetadata is used to marshal type 1 and NFT1 Group GENESIS OP_RETURN scriptPubKey +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SlpV1GenesisMetadata { + #[prost(bytes="vec", tag="1")] + pub name: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", tag="2")] + pub ticker: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", tag="3")] + pub document_url: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", tag="4")] + pub document_hash: ::prost::alloc::vec::Vec, + #[prost(uint32, tag="5")] + pub decimals: u32, + #[prost(uint32, tag="6")] + pub mint_baton_vout: u32, + #[prost(uint64, tag="7")] + pub mint_amount: u64, +} +/// SlpV1MintMetadata is used to marshal type 1 MINT OP_RETURN scriptPubKey +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SlpV1MintMetadata { + #[prost(uint32, tag="1")] + pub mint_baton_vout: u32, + #[prost(uint64, tag="2")] + pub mint_amount: u64, +} +/// SlpV1SendMetadata is used to marshal type 1 and NFT1 Group SEND OP_RETURN scriptPubKey +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SlpV1SendMetadata { + #[prost(uint64, repeated, packed="false", tag="1")] + pub amounts: ::prost::alloc::vec::Vec, +} +/// SlpV1Nft1ChildGenesisMetadata is used to marshal NFT1 Child GENESIS OP_RETURN scriptPubKey +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SlpV1Nft1ChildGenesisMetadata { + #[prost(bytes="vec", tag="1")] + pub name: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", tag="2")] + pub ticker: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", tag="3")] + pub document_url: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", tag="4")] + pub document_hash: ::prost::alloc::vec::Vec, + #[prost(uint32, tag="5")] + pub decimals: u32, + #[prost(bytes="vec", tag="6")] + pub group_token_id: ::prost::alloc::vec::Vec, +} +/// SlpV1Nft1ChildSendMetadata is used to marshal NFT1 Child SEND OP_RETURN scriptPubKey +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SlpV1Nft1ChildSendMetadata { + #[prost(bytes="vec", tag="1")] + pub group_token_id: ::prost::alloc::vec::Vec, +} +/// SlpTokenMetadata is used to marshal metadata about a specific TokenID +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SlpTokenMetadata { + #[prost(bytes="vec", tag="1")] + pub token_id: ::prost::alloc::vec::Vec, + #[prost(enumeration="SlpTokenType", tag="2")] + pub token_type: i32, + #[prost(oneof="slp_token_metadata::TypeMetadata", tags="3, 4, 5")] + pub type_metadata: ::core::option::Option, +} +/// Nested message and enum types in `SlpTokenMetadata`. +pub mod slp_token_metadata { + /// V1Fungible is used to marshal metadata specific to Type 1 token IDs + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct V1Fungible { + #[prost(string, tag="1")] + pub token_ticker: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub token_name: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub token_document_url: ::prost::alloc::string::String, + #[prost(bytes="vec", tag="4")] + pub token_document_hash: ::prost::alloc::vec::Vec, + #[prost(uint32, tag="5")] + pub decimals: u32, + #[prost(bytes="vec", tag="6")] + pub mint_baton_hash: ::prost::alloc::vec::Vec, + #[prost(uint32, tag="7")] + pub mint_baton_vout: u32, + } + /// V1NFT1Group is used to marshal metadata specific to NFT1 Group token IDs + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct V1nft1Group { + #[prost(string, tag="1")] + pub token_ticker: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub token_name: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub token_document_url: ::prost::alloc::string::String, + #[prost(bytes="vec", tag="4")] + pub token_document_hash: ::prost::alloc::vec::Vec, + #[prost(uint32, tag="5")] + pub decimals: u32, + #[prost(bytes="vec", tag="6")] + pub mint_baton_hash: ::prost::alloc::vec::Vec, + #[prost(uint32, tag="7")] + pub mint_baton_vout: u32, + } + /// V1NFT1Child is used to marshal metadata specific to NFT1 Child token IDs + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct V1nft1Child { + #[prost(string, tag="1")] + pub token_ticker: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub token_name: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub token_document_url: ::prost::alloc::string::String, + #[prost(bytes="vec", tag="4")] + pub token_document_hash: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", tag="5")] + pub group_id: ::prost::alloc::vec::Vec, + } + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum TypeMetadata { + #[prost(message, tag="3")] + V1Fungible(V1Fungible), + #[prost(message, tag="4")] + V1Nft1Group(V1nft1Group), + #[prost(message, tag="5")] + V1Nft1Child(V1nft1Child), + } +} +/// SlpRequiredBurn is used by clients to allow token burning +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SlpRequiredBurn { + #[prost(message, optional, tag="1")] + pub outpoint: ::core::option::Option, + #[prost(bytes="vec", tag="2")] + pub token_id: ::prost::alloc::vec::Vec, + #[prost(enumeration="SlpTokenType", tag="3")] + pub token_type: i32, + #[prost(oneof="slp_required_burn::BurnIntention", tags="4, 5")] + pub burn_intention: ::core::option::Option, +} +/// Nested message and enum types in `SlpRequiredBurn`. +pub mod slp_required_burn { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum BurnIntention { + #[prost(uint64, tag="4")] + Amount(u64), + #[prost(uint32, tag="5")] + MintBatonVout(u32), + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum SlpTokenType { + VersionNotSet = 0, + V1Fungible = 1, + V1Nft1Child = 65, + V1Nft1Group = 129, +} +/// SlpAction is used to allow clients to identify the type of slp transaction from this single field. +/// +/// NOTE: All enum types except for "NON_SLP" may be annotated with one or more BurnFlags. +/// +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum SlpAction { + NonSlp = 0, + NonSlpBurn = 1, + SlpParseError = 2, + SlpUnsupportedVersion = 3, + SlpV1Genesis = 4, + SlpV1Mint = 5, + SlpV1Send = 6, + SlpV1Nft1GroupGenesis = 7, + SlpV1Nft1GroupMint = 8, + SlpV1Nft1GroupSend = 9, + SlpV1Nft1UniqueChildGenesis = 10, + SlpV1Nft1UniqueChildSend = 11, +} diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 6aebfa629d..cd9ba229af 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -1,14 +1,62 @@ use super::*; -use crate::{eth, CanRefundHtlc, CoinBalance, NegotiateSwapContractAddrErr, SwapOps, TradePreimageValue, - ValidateAddressResult, WithdrawFut}; +use crate::coin_balance::{self, EnableCoinBalanceError, HDAccountBalance, HDAddressBalance, HDWalletBalance, + HDWalletBalanceOps}; +use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; +use crate::hd_wallet::{self, AccountUpdatingError, AddressDerivingError, GetNewHDAddressParams, + GetNewHDAddressResponse, HDAccountMut, HDWalletRpcError, HDWalletRpcOps, + NewAccountCreatingError}; +use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; +use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; +use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; +use crate::rpc_command::init_create_account::{self, CreateNewAccountParams, InitCreateHDAccountRpcOps}; +use crate::rpc_command::init_scan_for_new_addresses::{self, InitScanAddressesRpcOps, ScanAddressesParams, + ScanAddressesResponse}; +use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; +use crate::utxo::utxo_builder::{MergeUtxoArcOps, UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, + UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaPrivKeyBuilder}; +use crate::{eth, CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, DelegationError, DelegationFut, + GetWithdrawSenderAddress, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, + SignatureResult, StakingInfosFut, SwapOps, TradePreimageValue, TransactionFut, UnexpectedDerivationMethod, + ValidateAddressResult, ValidatePaymentInput, VerificationResult, WithdrawFut, WithdrawSenderAddress}; use common::mm_metrics::MetricsArc; -use common::mm_number::MmNumber; +use crypto::trezor::utxo::TrezorUtxoCoin; +use crypto::Bip44Chain; use ethereum_types::H160; use futures::{FutureExt, TryFutureExt}; +use keys::AddressHashEnum; +use mm2_number::MmNumber; +use serde::Serialize; use serialization::CoinVariant; +use utxo_signer::UtxoSignerOps; -pub const QTUM_STANDARD_DUST: u64 = 1000; +#[derive(Debug, Display)] +pub enum Qrc20AddressError { + UnexpectedDerivationMethod(String), + ScriptHashTypeNotSupported { script_hash_type: String }, +} + +impl From for Qrc20AddressError { + fn from(e: UnexpectedDerivationMethod) -> Self { Qrc20AddressError::UnexpectedDerivationMethod(e.to_string()) } +} + +impl From for Qrc20AddressError { + fn from(e: ScriptHashTypeNotSupported) -> Self { + Qrc20AddressError::ScriptHashTypeNotSupported { + script_hash_type: e.script_hash_type, + } + } +} +#[derive(Debug, Display)] +pub struct ScriptHashTypeNotSupported { + pub script_hash_type: String, +} + +impl From for WithdrawError { + fn from(e: ScriptHashTypeNotSupported) -> Self { WithdrawError::InvalidAddress(e.to_string()) } +} + +#[path = "qtum_delegation.rs"] mod qtum_delegation; #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "format")] pub enum QtumAddressFormat { @@ -21,28 +69,25 @@ pub enum QtumAddressFormat { Contract, } -#[async_trait] -pub trait QtumBasedCoin: AsRef + UtxoCommonOps + MarketCoinOps { - async fn qtum_balance(&self) -> BalanceResult { - let balance = self - .as_ref() - .rpc_client - .display_balance(self.as_ref().my_address.clone(), self.as_ref().decimals) - .compat() - .await?; +pub trait QtumDelegationOps { + fn add_delegation(&self, request: QtumDelegationRequest) -> DelegationFut; - let unspendable = utxo_common::my_unspendable_balance(self, &balance).await?; - let spendable = &balance - &unspendable; - Ok(CoinBalance { spendable, unspendable }) - } + fn get_delegation_infos(&self) -> StakingInfosFut; + fn remove_delegation(&self) -> DelegationFut; + + fn generate_pod(&self, addr_hash: AddressHashEnum) -> Result>; +} + +#[async_trait] +pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { let to_address_format: QtumAddressFormat = json::from_value(to_address_format).map_err(|e| ERRL!("Error on parse Qtum address format {:?}", e))?; let from_address = try_s!(self.utxo_address_from_any_format(from)); match to_address_format { QtumAddressFormat::Wallet => Ok(from_address.to_string()), - QtumAddressFormat::Contract => Ok(display_as_contract_address(from_address)), + QtumAddressFormat::Contract => Ok(try_s!(display_as_contract_address(from_address))), } } @@ -62,8 +107,8 @@ pub trait QtumBasedCoin: AsRef + UtxoCommonOps + MarketCoinOps { let utxo_segwit_err = match Address::from_segwitaddress( from, self.as_ref().conf.checksum_type, - self.as_ref().my_address.prefix, - self.as_ref().my_address.t_addr_prefix, + self.as_ref().conf.pub_addr_prefix, + self.as_ref().conf.pub_t_addr_prefix, ) { Ok(addr) => { let is_segwit = @@ -92,24 +137,27 @@ pub trait QtumBasedCoin: AsRef + UtxoCommonOps + MarketCoinOps { Address { prefix: utxo.conf.pub_addr_prefix, t_addr_prefix: utxo.conf.pub_t_addr_prefix, - hash: address.0.into(), + hash: AddressHashEnum::AddressHash(address.0.into()), checksum_type: utxo.conf.checksum_type, hrp: utxo.conf.bech32_hrp.clone(), - addr_format: utxo.my_address.addr_format.clone(), + addr_format: self.addr_format().clone(), } } - fn my_addr_as_contract_addr(&self) -> H160 { contract_addr_from_utxo_addr(self.as_ref().my_address.clone()) } + fn my_addr_as_contract_addr(&self) -> MmResult { + let my_address = self.as_ref().derivation_method.iguana_or_err()?.clone(); + contract_addr_from_utxo_addr(my_address).mm_err(Qrc20AddressError::from) + } fn utxo_address_from_contract_addr(&self, address: H160) -> Address { let utxo = self.as_ref(); Address { prefix: utxo.conf.pub_addr_prefix, t_addr_prefix: utxo.conf.pub_t_addr_prefix, - hash: address.0.into(), + hash: AddressHashEnum::AddressHash(address.0.into()), checksum_type: utxo.conf.checksum_type, hrp: utxo.conf.bech32_hrp.clone(), - addr_format: utxo.my_address.addr_format.clone(), + addr_format: self.addr_format().clone(), } } @@ -121,9 +169,10 @@ pub trait QtumBasedCoin: AsRef + UtxoCommonOps + MarketCoinOps { utxo.conf.pub_t_addr_prefix, utxo.conf.checksum_type, utxo.conf.bech32_hrp.clone(), - utxo.my_address.addr_format.clone() + self.addr_format().clone() )); - Ok(qtum::contract_addr_from_utxo_addr(qtum_address)) + let contract_addr = try_s!(contract_addr_from_utxo_addr(qtum_address)); + Ok(contract_addr) } fn is_qtum_unspent_mature(&self, output: &RpcTransaction) -> bool { @@ -133,6 +182,69 @@ pub trait QtumBasedCoin: AsRef + UtxoCommonOps + MarketCoinOps { } } +pub struct QtumCoinBuilder<'a> { + ctx: &'a MmArc, + ticker: &'a str, + conf: &'a Json, + activation_params: &'a UtxoActivationParams, + priv_key_policy: PrivKeyBuildPolicy<'a>, +} + +#[async_trait] +impl<'a> UtxoCoinBuilderCommonOps for QtumCoinBuilder<'a> { + fn ctx(&self) -> &MmArc { self.ctx } + + fn conf(&self) -> &Json { self.conf } + + fn activation_params(&self) -> &UtxoActivationParams { self.activation_params } + + fn ticker(&self) -> &str { self.ticker } + + fn check_utxo_maturity(&self) -> bool { self.activation_params().check_utxo_maturity.unwrap_or(true) } +} + +impl<'a> UtxoFieldsWithIguanaPrivKeyBuilder for QtumCoinBuilder<'a> {} + +impl<'a> UtxoFieldsWithHardwareWalletBuilder for QtumCoinBuilder<'a> {} + +#[async_trait] +impl<'a> UtxoCoinBuilder for QtumCoinBuilder<'a> { + type ResultCoin = QtumCoin; + type Error = UtxoCoinBuildError; + + fn priv_key_policy(&self) -> PrivKeyBuildPolicy<'_> { self.priv_key_policy.clone() } + + async fn build(self) -> MmResult { + let utxo = self.build_utxo_fields().await?; + let utxo_arc = UtxoArc::new(utxo); + let utxo_weak = utxo_arc.downgrade(); + let result_coin = QtumCoin::from(utxo_arc); + + self.spawn_merge_utxo_loop_if_required(utxo_weak, QtumCoin::from); + Ok(result_coin) + } +} + +impl<'a> MergeUtxoArcOps for QtumCoinBuilder<'a> {} + +impl<'a> QtumCoinBuilder<'a> { + pub fn new( + ctx: &'a MmArc, + ticker: &'a str, + conf: &'a Json, + activation_params: &'a UtxoActivationParams, + priv_key_policy: PrivKeyBuildPolicy<'a>, + ) -> Self { + QtumCoinBuilder { + ctx, + ticker, + conf, + activation_params, + priv_key_policy, + } + } +} + #[derive(Clone, Debug)] pub struct QtumCoin { utxo_arc: UtxoArc, @@ -150,25 +262,118 @@ impl From for UtxoArc { fn from(coin: QtumCoin) -> Self { coin.utxo_arc } } -pub async fn qtum_coin_from_conf_and_request( +pub async fn qtum_coin_with_priv_key( ctx: &MmArc, ticker: &str, conf: &Json, - req: &Json, + activation_params: &UtxoActivationParams, priv_key: &[u8], ) -> Result { - let coin: QtumCoin = try_s!(utxo_common::utxo_arc_from_conf_and_request(ctx, ticker, conf, req, priv_key).await); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(priv_key); + let coin = try_s!( + QtumCoinBuilder::new(ctx, ticker, conf, activation_params, priv_key_policy) + .build() + .await + ); Ok(coin) } impl QtumBasedCoin for QtumCoin {} +#[derive(Clone, Debug, Deserialize)] +pub struct QtumDelegationRequest { + pub address: String, + pub fee: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct QtumStakingInfosDetails { + pub amount: BigDecimal, + pub staker: Option, + pub am_i_staking: bool, + pub is_staking_supported: bool, +} + +// if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt #[async_trait] #[cfg_attr(test, mockable)] -impl UtxoCommonOps for QtumCoin { - async fn get_tx_fee(&self) -> Result { utxo_common::get_tx_fee(&self.utxo_arc).await } +impl UtxoTxBroadcastOps for QtumCoin { + async fn broadcast_tx(&self, tx: &UtxoTx) -> Result> { + utxo_common::broadcast_tx(self, tx).await + } +} - async fn get_htlc_spend_fee(&self) -> UtxoRpcResult { utxo_common::get_htlc_spend_fee(self).await } +#[async_trait] +#[cfg_attr(test, mockable)] +impl UtxoTxGenerationOps for QtumCoin { + async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + + async fn calc_interest_if_required( + &self, + unsigned: TransactionInputSigner, + data: AdditionalTxData, + my_script_pub: Bytes, + ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { + utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub).await + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoListOps for QtumCoin { + async fn get_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_unspent_ordered_list(self, address).await + } + + async fn get_all_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_list(self, address).await + } + + async fn get_mature_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_mature_unspent_ordered_list(self, address).await + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoMapOps for QtumCoin { + async fn get_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_unspent_ordered_map(self, addresses).await + } + + async fn get_all_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_map(self, addresses).await + } + + async fn get_mature_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(MatureUnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_mature_unspent_ordered_map(self, addresses).await + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl UtxoCommonOps for QtumCoin { + async fn get_htlc_spend_fee(&self, tx_size: u64) -> UtxoRpcResult { + utxo_common::get_htlc_spend_fee(self, tx_size).await + } fn addresses_from_script(&self, script: &Script) -> Result, String> { utxo_common::addresses_from_script(self, script) @@ -176,10 +381,12 @@ impl UtxoCommonOps for QtumCoin { fn denominate_satoshis(&self, satoshi: i64) -> f64 { utxo_common::denominate_satoshis(&self.utxo_arc, satoshi) } - fn my_public_key(&self) -> &Public { self.utxo_arc.key_pair.public() } + fn my_public_key(&self) -> Result<&Public, MmError> { + utxo_common::my_public_key(self.as_ref()) + } fn address_from_str(&self, address: &str) -> Result { - utxo_common::checked_address_from_str(&self.utxo_arc, address) + utxo_common::checked_address_from_str(self, address) } async fn get_current_mtp(&self) -> UtxoRpcResult { @@ -188,26 +395,6 @@ impl UtxoCommonOps for QtumCoin { fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { self.is_qtum_unspent_mature(output) } - async fn generate_transaction( - &self, - utxos: Vec, - outputs: Vec, - fee_policy: FeePolicy, - fee: Option, - gas_fee: Option, - ) -> GenerateTxResult { - utxo_common::generate_transaction(self, utxos, outputs, fee_policy, fee, gas_fee).await - } - - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub).await - } - async fn calc_interest_of_tx( &self, _tx: &UtxoTx, @@ -226,54 +413,19 @@ impl UtxoCommonOps for QtumCoin { utxo_common::get_mut_verbose_transaction_from_map_or_rpc(self, tx_hash, utxo_tx_map).await } - async fn p2sh_spending_tx( - &self, - prev_transaction: UtxoTx, - redeem_script: Bytes, - outputs: Vec, - script_data: Script, - sequence: u32, - lock_time: u32, - ) -> Result { - utxo_common::p2sh_spending_tx( - self, - prev_transaction, - redeem_script, - outputs, - script_data, - sequence, - lock_time, - ) - .await + async fn p2sh_spending_tx(&self, input: utxo_common::P2SHSpendingTxInput<'_>) -> Result { + utxo_common::p2sh_spending_tx(self, input).await } - async fn ordered_mature_unspents<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::ordered_mature_unspents(self, address).await - } - - fn get_verbose_transaction_from_cache_or_rpc( + fn get_verbose_transactions_from_cache_or_rpc( &self, - txid: H256Json, - ) -> Box + Send> { + tx_ids: HashSet, + ) -> UtxoRpcFut> { let selfi = self.clone(); - let fut = async move { utxo_common::get_verbose_transaction_from_cache_or_rpc(&selfi.utxo_arc, txid).await }; + let fut = async move { utxo_common::get_verbose_transactions_from_cache_or_rpc(&selfi.utxo_arc, tx_ids).await }; Box::new(fut.boxed().compat()) } - async fn cache_transaction_if_possible(&self, tx: &RpcTransaction) -> Result<(), String> { - utxo_common::cache_transaction_if_possible(&self.utxo_arc, tx).await - } - - async fn list_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::ordered_mature_unspents(self, address).await - } - async fn preimage_trade_fee_required_to_send_outputs( &self, outputs: Vec, @@ -281,7 +433,15 @@ impl UtxoCommonOps for QtumCoin { gas_fee: Option, stage: &FeeApproxStage, ) -> TradePreimageResult { - utxo_common::preimage_trade_fee_required_to_send_outputs(self, outputs, fee_policy, gas_fee, stage).await + utxo_common::preimage_trade_fee_required_to_send_outputs( + self, + self.ticker(), + outputs, + fee_policy, + gas_fee, + stage, + ) + .await } fn increase_dynamic_fee_by_stage(&self, dynamic_fee: u64, stage: &FeeApproxStage) -> u64 { @@ -292,9 +452,23 @@ impl UtxoCommonOps for QtumCoin { utxo_common::p2sh_tx_locktime(self, &self.utxo_arc.conf.ticker, htlc_locktime).await } + fn addr_format(&self) -> &UtxoAddressFormat { utxo_common::addr_format(self) } + fn addr_format_for_standard_scripts(&self) -> UtxoAddressFormat { utxo_common::addr_format_for_standard_scripts(self) } + + fn address_from_pubkey(&self, pubkey: &Public) -> Address { + let conf = &self.utxo_arc.conf; + utxo_common::address_from_pubkey( + pubkey, + conf.pub_addr_prefix, + conf.pub_t_addr_prefix, + conf.checksum_type, + conf.bech32_hrp.clone(), + self.addr_format().clone(), + ) + } } #[async_trait] @@ -320,8 +494,9 @@ impl UtxoStandardOps for QtumCoin { } } +#[async_trait] impl SwapOps for QtumCoin { - fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { utxo_common::send_taker_fee(self.clone(), fee_addr, amount) } @@ -332,8 +507,16 @@ impl SwapOps for QtumCoin { secret_hash: &[u8], amount: BigDecimal, _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_maker_payment(self.clone(), time_lock, taker_pub, secret_hash, amount) + utxo_common::send_maker_payment( + self.clone(), + time_lock, + taker_pub, + secret_hash, + amount, + swap_unique_data, + ) } fn send_taker_payment( @@ -343,52 +526,92 @@ impl SwapOps for QtumCoin { secret_hash: &[u8], amount: BigDecimal, _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_taker_payment(self.clone(), time_lock, maker_pub, secret_hash, amount) + utxo_common::send_taker_payment( + self.clone(), + time_lock, + maker_pub, + secret_hash, + amount, + swap_unique_data, + ) } fn send_maker_spends_taker_payment( &self, - taker_payment_tx: &[u8], + taker_tx: &[u8], time_lock: u32, taker_pub: &[u8], secret: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_maker_spends_taker_payment(self.clone(), taker_payment_tx, time_lock, taker_pub, secret) + utxo_common::send_maker_spends_taker_payment( + self.clone(), + taker_tx, + time_lock, + taker_pub, + secret, + swap_unique_data, + ) } fn send_taker_spends_maker_payment( &self, - maker_payment_tx: &[u8], + maker_tx: &[u8], time_lock: u32, maker_pub: &[u8], secret: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_taker_spends_maker_payment(self.clone(), maker_payment_tx, time_lock, maker_pub, secret) + utxo_common::send_taker_spends_maker_payment( + self.clone(), + maker_tx, + time_lock, + maker_pub, + secret, + swap_unique_data, + ) } fn send_taker_refunds_payment( &self, - taker_payment_tx: &[u8], + taker_tx: &[u8], time_lock: u32, maker_pub: &[u8], secret_hash: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_taker_refunds_payment(self.clone(), taker_payment_tx, time_lock, maker_pub, secret_hash) + utxo_common::send_taker_refunds_payment( + self.clone(), + taker_tx, + time_lock, + maker_pub, + secret_hash, + swap_unique_data, + ) } fn send_maker_refunds_payment( &self, - maker_payment_tx: &[u8], + maker_tx: &[u8], time_lock: u32, taker_pub: &[u8], secret_hash: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_maker_refunds_payment(self.clone(), maker_payment_tx, time_lock, taker_pub, secret_hash) + utxo_common::send_maker_refunds_payment( + self.clone(), + maker_tx, + time_lock, + taker_pub, + secret_hash, + swap_unique_data, + ) } fn validate_fee( @@ -398,6 +621,7 @@ impl SwapOps for QtumCoin { fee_addr: &[u8], amount: &BigDecimal, min_block_number: u64, + _uuid: &[u8], ) -> Box + Send> { let tx = match fee_tx { TransactionEnum::UtxoTx(tx) => tx.clone(), @@ -414,28 +638,12 @@ impl SwapOps for QtumCoin { ) } - fn validate_maker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - priv_bn_hash: &[u8], - amount: BigDecimal, - _swap_contract_address: &Option, - ) -> Box + Send> { - utxo_common::validate_maker_payment(self, payment_tx, time_lock, maker_pub, priv_bn_hash, amount) + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + utxo_common::validate_maker_payment(self, input) } - fn validate_taker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - priv_bn_hash: &[u8], - amount: BigDecimal, - _swap_contract_address: &Option, - ) -> Box + Send> { - utxo_common::validate_taker_payment(self, payment_tx, time_lock, taker_pub, priv_bn_hash, amount) + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + utxo_common::validate_taker_payment(self, input) } fn check_if_my_payment_sent( @@ -445,48 +653,23 @@ impl SwapOps for QtumCoin { secret_hash: &[u8], _search_from_block: u64, _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> Box, Error = String> + Send> { - utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash) + utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash, swap_unique_data) } - fn search_for_swap_tx_spend_my( + async fn search_for_swap_tx_spend_my( &self, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - _swap_contract_address: &Option, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { - utxo_common::search_for_swap_tx_spend_my( - &self.utxo_arc, - time_lock, - other_pub, - secret_hash, - tx, - utxo_common::DEFAULT_SWAP_VOUT, - search_from_block, - ) + utxo_common::search_for_swap_tx_spend_my(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } - fn search_for_swap_tx_spend_other( + async fn search_for_swap_tx_spend_other( &self, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - _swap_contract_address: &Option, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { - utxo_common::search_for_swap_tx_spend_other( - &self.utxo_arc, - time_lock, - other_pub, - secret_hash, - tx, - utxo_common::DEFAULT_SWAP_VOUT, - search_from_block, - ) + utxo_common::search_for_swap_tx_spend_other(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { @@ -508,6 +691,10 @@ impl SwapOps for QtumCoin { ) -> Result, MmError> { Ok(None) } + + fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { + utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) + } } impl MarketCoinOps for QtumCoin { @@ -515,18 +702,39 @@ impl MarketCoinOps for QtumCoin { fn my_address(&self) -> Result { utxo_common::my_address(self) } - fn my_balance(&self) -> BalanceFut { - let selfi = self.clone(); - let fut = async move { selfi.qtum_balance().await }; - Box::new(fut.boxed().compat()) + fn get_public_key(&self) -> Result> { + let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; + Ok(pubkey.to_string()) } + fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { + utxo_common::sign_message_hash(self.as_ref(), message) + } + + fn sign_message(&self, message: &str) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message) + } + + fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { + utxo_common::verify_message(self, signature_base64, message, address) + } + + fn my_balance(&self) -> BalanceFut { utxo_common::my_balance(self.clone()) } + fn base_coin_balance(&self) -> BalanceFut { utxo_common::base_coin_balance(self) } + fn platform_ticker(&self) -> &str { self.ticker() } + + #[inline(always)] fn send_raw_tx(&self, tx: &str) -> Box + Send> { utxo_common::send_raw_tx(&self.utxo_arc, tx) } + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) + } + fn wait_for_confirmations( &self, tx: &[u8], @@ -569,16 +777,21 @@ impl MarketCoinOps for QtumCoin { utxo_common::current_block(&self.utxo_arc) } - fn display_priv_key(&self) -> String { utxo_common::display_priv_key(&self.utxo_arc) } + fn display_priv_key(&self) -> Result { utxo_common::display_priv_key(&self.utxo_arc) } fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } } +#[async_trait] impl MmCoin for QtumCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { + Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) + } + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { Box::new(utxo_common::withdraw(self.clone(), req).boxed().compat()) } @@ -607,20 +820,24 @@ impl MmCoin for QtumCoin { utxo_common::get_trade_fee(self.clone()) } - fn get_sender_trade_fee(&self, value: TradePreimageValue, stage: FeeApproxStage) -> TradePreimageFut { - utxo_common::get_sender_trade_fee(self.clone(), value, stage) + async fn get_sender_trade_fee( + &self, + value: TradePreimageValue, + stage: FeeApproxStage, + ) -> TradePreimageResult { + utxo_common::get_sender_trade_fee(self, value, stage).await } fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { utxo_common::get_receiver_trade_fee(self.clone()) } - fn get_fee_to_send_taker_fee( + async fn get_fee_to_send_taker_fee( &self, dex_fee_amount: BigDecimal, stage: FeeApproxStage, - ) -> TradePreimageFut { - utxo_common::get_fee_to_send_taker_fee(self.clone(), dex_fee_amount, stage) + ) -> TradePreimageResult { + utxo_common::get_fee_to_send_taker_fee(self, dex_fee_amount, stage).await } fn required_confirmations(&self) -> u64 { utxo_common::required_confirmations(&self.utxo_arc) } @@ -639,10 +856,213 @@ impl MmCoin for QtumCoin { fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } - fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(&self.utxo_arc) } + fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } fn is_coin_protocol_supported(&self, info: &Option>) -> bool { - utxo_common::is_coin_protocol_supported(&self.utxo_arc, info) + utxo_common::is_coin_protocol_supported(self, info) + } +} + +#[async_trait] +impl GetWithdrawSenderAddress for QtumCoin { + type Address = Address; + type Pubkey = Public; + + async fn get_withdraw_sender_address( + &self, + req: &WithdrawRequest, + ) -> MmResult, WithdrawError> { + utxo_common::get_withdraw_from_address(self, req).await + } +} + +#[async_trait] +impl InitWithdrawCoin for QtumCoin { + async fn init_withdraw( + &self, + ctx: MmArc, + req: WithdrawRequest, + task_handle: &WithdrawTaskHandle, + ) -> Result> { + utxo_common::init_withdraw(ctx, self.clone(), req, task_handle).await + } +} + +impl UtxoSignerOps for QtumCoin { + type TxGetter = UtxoRpcClientEnum; + + fn trezor_coin(&self) -> UtxoSignTxResult { + self.utxo_arc + .conf + .trezor_coin + .or_mm_err(|| UtxoSignTxError::CoinNotSupportedWithTrezor { + coin: self.utxo_arc.conf.ticker.clone(), + }) + } + + fn fork_id(&self) -> u32 { self.utxo_arc.conf.fork_id } + + fn branch_id(&self) -> u32 { self.utxo_arc.conf.consensus_branch_id } + + fn tx_provider(&self) -> Self::TxGetter { self.utxo_arc.rpc_client.clone() } +} + +impl CoinWithDerivationMethod for QtumCoin { + type Address = Address; + type HDWallet = UtxoHDWallet; + + fn derivation_method(&self) -> &DerivationMethod { + utxo_common::derivation_method(self.as_ref()) + } +} + +#[async_trait] +impl ExtractExtendedPubkey for QtumCoin { + type ExtendedPublicKey = Secp256k1ExtendedPublicKey; + + async fn extract_extended_pubkey( + &self, + xpub_extractor: &XPubExtractor, + derivation_path: DerivationPath, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Sync, + { + utxo_common::extract_extended_pubkey(&self.utxo_arc.conf, xpub_extractor, derivation_path).await + } +} + +#[async_trait] +impl HDWalletCoinOps for QtumCoin { + type Address = Address; + type Pubkey = Public; + type HDWallet = UtxoHDWallet; + type HDAccount = UtxoHDAccount; + + fn derive_address( + &self, + hd_account: &Self::HDAccount, + chain: Bip44Chain, + address_id: u32, + ) -> MmResult, AddressDerivingError> { + utxo_common::derive_address(self, hd_account, chain, address_id) + } + + async fn create_new_account<'a, XPubExtractor>( + &self, + hd_wallet: &'a Self::HDWallet, + xpub_extractor: &XPubExtractor, + ) -> MmResult, NewAccountCreatingError> + where + XPubExtractor: HDXPubExtractor + Sync, + { + utxo_common::create_new_account(self, hd_wallet, xpub_extractor).await + } + + async fn set_known_addresses_number( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut Self::HDAccount, + chain: Bip44Chain, + new_known_addresses_number: u32, + ) -> MmResult<(), AccountUpdatingError> { + utxo_common::set_known_addresses_number(self, hd_wallet, hd_account, chain, new_known_addresses_number).await + } +} + +#[async_trait] +impl HDWalletBalanceOps for QtumCoin { + type HDAddressScanner = UtxoAddressScanner; + + async fn produce_hd_address_scanner(&self) -> BalanceResult { + utxo_common::produce_hd_address_scanner(self).await + } + + async fn enable_hd_wallet( + &self, + hd_wallet: &Self::HDWallet, + xpub_extractor: &XPubExtractor, + scan_policy: EnableCoinScanPolicy, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Sync, + { + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, scan_policy).await + } + + async fn scan_for_new_addresses( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut Self::HDAccount, + address_scanner: &Self::HDAddressScanner, + gap_limit: u32, + ) -> BalanceResult> { + utxo_common::scan_for_new_addresses(self, hd_wallet, hd_account, address_scanner, gap_limit).await + } + + async fn all_known_addresses_balances(&self, hd_account: &Self::HDAccount) -> BalanceResult> { + utxo_common::all_known_addresses_balances(self, hd_account).await + } + + async fn known_address_balance(&self, address: &Self::Address) -> BalanceResult { + utxo_common::address_balance(self, address).await + } + + async fn known_addresses_balances( + &self, + addresses: Vec, + ) -> BalanceResult> { + utxo_common::addresses_balances(self, addresses).await + } +} + +impl HDWalletCoinWithStorageOps for QtumCoin { + fn hd_wallet_storage<'a>(&self, hd_wallet: &'a Self::HDWallet) -> &'a HDWalletCoinStorage { + &hd_wallet.hd_wallet_storage + } +} + +#[async_trait] +impl HDWalletRpcOps for QtumCoin { + async fn get_new_address_rpc( + &self, + params: GetNewHDAddressParams, + ) -> MmResult { + hd_wallet::common_impl::get_new_address_rpc(self, params).await + } +} + +#[async_trait] +impl AccountBalanceRpcOps for QtumCoin { + async fn account_balance_rpc( + &self, + params: AccountBalanceParams, + ) -> MmResult { + account_balance::common_impl::account_balance_rpc(self, params).await + } +} + +#[async_trait] +impl InitScanAddressesRpcOps for QtumCoin { + async fn init_scan_for_new_addresses_rpc( + &self, + params: ScanAddressesParams, + ) -> MmResult { + init_scan_for_new_addresses::common_impl::scan_for_new_addresses_rpc(self, params).await + } +} + +#[async_trait] +impl InitCreateHDAccountRpcOps for QtumCoin { + async fn init_create_account_rpc( + &self, + params: CreateNewAccountParams, + xpub_extractor: &XPubExtractor, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Sync, + { + init_create_account::common_impl::init_create_new_account_rpc(self, params, xpub_extractor).await } } @@ -650,9 +1070,16 @@ impl MmCoin for QtumCoin { /// Qtum Contract addresses have another checksum verification algorithm, because of this do not use [`eth::valid_addr_from_str`]. pub fn contract_addr_from_str(addr: &str) -> Result { eth::addr_from_str(addr) } -pub fn contract_addr_from_utxo_addr(address: Address) -> H160 { address.hash.take().into() } +pub fn contract_addr_from_utxo_addr(address: Address) -> MmResult { + match address.hash { + AddressHashEnum::AddressHash(h) => Ok(h.take().into()), + AddressHashEnum::WitnessScriptHash(_) => MmError::err(ScriptHashTypeNotSupported { + script_hash_type: "Witness".to_owned(), + }), + } +} -pub fn display_as_contract_address(address: Address) -> String { - let address = qtum::contract_addr_from_utxo_addr(address); - format!("{:#02x}", address) +pub fn display_as_contract_address(address: Address) -> MmResult { + let address = qtum::contract_addr_from_utxo_addr(address)?; + Ok(format!("{:#02x}", address)) } diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs new file mode 100644 index 0000000000..4a532a3716 --- /dev/null +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -0,0 +1,386 @@ +use crate::qrc20::rpc_clients::Qrc20ElectrumOps; +use crate::qrc20::script_pubkey::generate_contract_call_script_pubkey; +use crate::qrc20::{contract_addr_into_rpc_format, ContractCallOutput, GenerateQrc20TxResult, Qrc20AbiError, + Qrc20FeeDetails, OUTPUT_QTUM_AMOUNT, QRC20_DUST, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT}; +use crate::utxo::qtum::{QtumBasedCoin, QtumCoin, QtumDelegationOps, QtumDelegationRequest, QtumStakingInfosDetails}; +use crate::utxo::rpc_clients::UtxoRpcClientEnum; +use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, UtxoTxBuilder}; +use crate::utxo::{qtum, utxo_common, Address, GetUtxoListOps, UtxoCommonOps}; +use crate::utxo::{PrivKeyNotAllowed, UTXO_LOCK}; +use crate::{DelegationError, DelegationFut, DelegationResult, MarketCoinOps, StakingInfos, StakingInfosError, + StakingInfosFut, StakingInfosResult, TransactionDetails, TransactionType}; +use bitcrypto::dhash256; +use common::now_ms; +use derive_more::Display; +use ethabi::{Contract, Token}; +use ethereum_types::H160; +use futures::compat::Future01CompatExt; +use futures::{FutureExt, TryFutureExt}; +use keys::{AddressHashEnum, Signature}; +use mm2_err_handle::prelude::*; +use mm2_number::bigdecimal::{BigDecimal, Zero}; +use rpc::v1::types::ToTxHash; +use script::Builder as ScriptBuilder; +use serialization::serialize; +use std::convert::TryInto; +use std::str::FromStr; +use utxo_signer::with_key_pair::sign_tx; + +pub const QTUM_DELEGATION_STANDARD_FEE: u64 = 10; +pub const QTUM_LOWER_BOUND_DELEGATION_AMOUNT: f64 = 100.0; +pub const QRC20_GAS_LIMIT_DELEGATION: u64 = 2_250_000; +pub const QTUM_ADD_DELEGATION_TOPIC: &str = "a23803f3b2b56e71f2921c22b23c32ef596a439dbe03f7250e6b58a30eb910b5"; +pub const QTUM_REMOVE_DELEGATION_TOPIC: &str = "7fe28d2d0b16cf95b5ea93f4305f89133b3892543e616381a1336fc1e7a01fa0"; +const QTUM_DELEGATE_CONTRACT_ABI: &str = r#"[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_staker","type":"address"},{"indexed":true,"internalType":"address","name":"_delegate","type":"address"},{"indexed":false,"internalType":"uint8","name":"fee","type":"uint8"},{"indexed":false,"internalType":"uint256","name":"blockHeight","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"PoD","type":"bytes"}],"name":"AddDelegation","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_staker","type":"address"},{"indexed":true,"internalType":"address","name":"_delegate","type":"address"}],"name":"RemoveDelegation","type":"event"},{"constant":false,"inputs":[{"internalType":"address","name":"_staker","type":"address"},{"internalType":"uint8","name":"_fee","type":"uint8"},{"internalType":"bytes","name":"_PoD","type":"bytes"}],"name":"addDelegation","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"delegations","outputs":[{"internalType":"address","name":"staker","type":"address"},{"internalType":"uint8","name":"fee","type":"uint8"},{"internalType":"uint256","name":"blockHeight","type":"uint256"},{"internalType":"bytes","name":"PoD","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"removeDelegation","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]"#; + +lazy_static! { + pub static ref QTUM_DELEGATE_CONTRACT: Contract = Contract::load(QTUM_DELEGATE_CONTRACT_ABI.as_bytes()).unwrap(); + pub static ref QTUM_DELEGATE_CONTRACT_ADDRESS: H160 = + H160::from_str("0000000000000000000000000000000000000086").unwrap(); +} + +pub type QtumStakingAbiResult = Result>; + +#[derive(Debug, Display)] +pub enum QtumStakingAbiError { + #[display(fmt = "Invalid QRC20 ABI params: {}", _0)] + InvalidParams(String), + #[display(fmt = "QRC20 ABI error: {}", _0)] + AbiError(String), + #[display(fmt = "Qtum POD error: {}", _0)] + PodSigningError(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for QtumStakingAbiError { + fn from(e: Qrc20AbiError) -> Self { + match e { + Qrc20AbiError::InvalidParams(e) => QtumStakingAbiError::InvalidParams(e), + Qrc20AbiError::AbiError(e) => QtumStakingAbiError::AbiError(e), + } + } +} + +impl From for DelegationError { + fn from(e: QtumStakingAbiError) -> Self { DelegationError::CannotInteractWithSmartContract(e.to_string()) } +} + +impl From for QtumStakingAbiError { + fn from(e: ethabi::Error) -> QtumStakingAbiError { QtumStakingAbiError::AbiError(e.to_string()) } +} + +impl From for DelegationError { + fn from(e: ethabi::Error) -> Self { DelegationError::from(QtumStakingAbiError::from(e)) } +} + +impl From for DelegationError { + fn from(e: Qrc20AbiError) -> Self { DelegationError::from(QtumStakingAbiError::from(e)) } +} + +impl From for QtumStakingAbiError { + fn from(e: PrivKeyNotAllowed) -> Self { QtumStakingAbiError::Internal(e.to_string()) } +} + +impl QtumDelegationOps for QtumCoin { + fn add_delegation(&self, request: QtumDelegationRequest) -> DelegationFut { + let coin = self.clone(); + let fut = async move { coin.add_delegation_impl(request).await }; + Box::new(fut.boxed().compat()) + } + + fn get_delegation_infos(&self) -> StakingInfosFut { + let coin = self.clone(); + let fut = async move { coin.get_delegation_infos_impl().await }; + Box::new(fut.boxed().compat()) + } + + fn remove_delegation(&self) -> DelegationFut { + let coin = self.clone(); + let fut = async move { coin.remove_delegation_impl().await }; + Box::new(fut.boxed().compat()) + } + + fn generate_pod(&self, addr_hash: AddressHashEnum) -> Result> { + let mut buffer = b"\x15Qtum Signed Message:\n\x28".to_vec(); + buffer.append(&mut addr_hash.to_string().into_bytes()); + let hashed = dhash256(&buffer); + let key_pair = self.as_ref().priv_key_policy.key_pair_or_err()?; + let signature = key_pair + .private() + .sign_compact(&hashed) + .map_to_mm(|e| QtumStakingAbiError::PodSigningError(e.to_string()))?; + Ok(signature) + } +} + +impl QtumCoin { + async fn remove_delegation_impl(&self) -> DelegationResult { + if self.addr_format().is_segwit() { + return MmError::err(DelegationError::DelegationOpsNotSupported { + reason: "Qtum doesn't support delegation for segwit".to_string(), + }); + } + let delegation_output = self.remove_delegation_output(QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)?; + let outputs = vec![delegation_output]; + let my_address = self.my_address().map_to_mm(DelegationError::InternalError)?; + self.generate_delegation_transaction( + outputs, + my_address, + QRC20_GAS_LIMIT_DEFAULT, + TransactionType::RemoveDelegation, + ) + .await + } + + async fn am_i_currently_staking(&self) -> Result, MmError> { + let utxo = self.as_ref(); + let contract_address = contract_addr_into_rpc_format(&QTUM_DELEGATE_CONTRACT_ADDRESS); + let client = match &utxo.rpc_client { + UtxoRpcClientEnum::Native(_) => { + return MmError::err(StakingInfosError::Internal("Native not supported".to_string())) + }, + UtxoRpcClientEnum::Electrum(electrum) => electrum, + }; + let address = self.my_addr_as_contract_addr()?; + let address_rpc = contract_addr_into_rpc_format(&address); + let add_delegation_history = client + .blockchain_contract_event_get_history(&address_rpc, &contract_address, QTUM_ADD_DELEGATION_TOPIC) + .compat() + .await + .map_to_mm(|e| StakingInfosError::Transport(e.to_string()))?; + let remove_delegation_history = client + .blockchain_contract_event_get_history(&address_rpc, &contract_address, QTUM_REMOVE_DELEGATION_TOPIC) + .compat() + .await + .map_to_mm(|e| StakingInfosError::Transport(e.to_string()))?; + let am_i_staking = add_delegation_history.len() > remove_delegation_history.len(); + if am_i_staking { + let last_tx_add = match add_delegation_history.last() { + Some(last_tx_add) => last_tx_add, + None => return Ok(None), + }; + let res = &client + .blockchain_transaction_get_receipt(&last_tx_add.tx_hash) + .compat() + .await + .map_to_mm(|e| StakingInfosError::Transport(e.to_string()))?; + // there is only 3 topics for an add_delegation + // the first entry is the operation (add_delegation / remove_delegation), + // the second entry is always the staker as hexadecimal 32 byte padded + // by trimming the start we retrieve the standard hex hash format + // https://testnet.qtum.info/tx/c62d707b67267a13a53b5910ffbf393c47f00734cff1c73aae6e05d24258372f + // topic[0] -> a23803f3b2b56e71f2921c22b23c32ef596a439dbe03f7250e6b58a30eb910b5 -> add_delegation_topic + // topic[1] -> 000000000000000000000000d4ea77298fdac12c657a18b222adc8b307e18127 -> staker_address + // topic[2] -> 0000000000000000000000006d9d2b554d768232320587df75c4338ecc8bf37d + + return if let Some(raw) = res + .iter() + .find(|receipt| { + receipt + .log + .iter() + .any(|e| !e.topics.is_empty() && e.topics[0] == QTUM_ADD_DELEGATION_TOPIC) + }) + .and_then(|receipt| { + receipt + .log + .get(0) + .and_then(|log_entry| log_entry.topics.get(1)) + .map(|padded_staker_address_hex| padded_staker_address_hex.trim_start_matches('0')) + }) { + let hash = H160::from_str(raw).map_to_mm(|e| StakingInfosError::Internal(e.to_string()))?; + let address = self.utxo_address_from_contract_addr(hash); + Ok(Some(address.to_string())) + } else { + Ok(None) + }; + } + Ok(None) + } + + async fn get_delegation_infos_impl(&self) -> StakingInfosResult { + let coin = self.as_ref(); + let my_address = coin.derivation_method.iguana_or_err()?; + + let staker = self.am_i_currently_staking().await?; + let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; + let lower_bound = QTUM_LOWER_BOUND_DELEGATION_AMOUNT + .try_into() + .expect("Conversion should succeed"); + let mut amount = BigDecimal::zero(); + if staker.is_some() { + amount = unspents + .iter() + .map(|unspent| big_decimal_from_sat_unsigned(unspent.value, coin.decimals)) + .filter(|unspent_value| unspent_value >= &lower_bound) + .fold(BigDecimal::zero(), |total, unspent_value| total + unspent_value); + } + let am_i_staking = staker.is_some(); + let infos = StakingInfos { + staking_infos_details: QtumStakingInfosDetails { + amount, + staker, + am_i_staking, + is_staking_supported: !my_address.addr_format.is_segwit(), + } + .into(), + }; + Ok(infos) + } + + async fn add_delegation_impl(&self, request: QtumDelegationRequest) -> DelegationResult { + if self.addr_format().is_segwit() { + return MmError::err(DelegationError::DelegationOpsNotSupported { + reason: "Qtum doesn't support delegation for segwit".to_string(), + }); + } + if let Some(staking_addr) = self.am_i_currently_staking().await? { + return MmError::err(DelegationError::AlreadyDelegating(staking_addr)); + } + let to_addr = + Address::from_str(request.address.as_str()).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; + let fee = request.fee.unwrap_or(QTUM_DELEGATION_STANDARD_FEE); + let _utxo_lock = UTXO_LOCK.lock(); + let staker_address_hex = qtum::contract_addr_from_utxo_addr(to_addr.clone())?; + let delegation_output = self.add_delegation_output( + staker_address_hex, + to_addr.hash, + fee, + QRC20_GAS_LIMIT_DELEGATION, + QRC20_GAS_PRICE_DEFAULT, + )?; + + let outputs = vec![delegation_output]; + let my_address = self.my_address().map_to_mm(DelegationError::InternalError)?; + self.generate_delegation_transaction( + outputs, + my_address, + QRC20_GAS_LIMIT_DELEGATION, + TransactionType::StakingDelegation, + ) + .await + } + + async fn generate_delegation_transaction( + &self, + contract_outputs: Vec, + to_address: String, + gas_limit: u64, + transaction_type: TransactionType, + ) -> DelegationResult { + let utxo = self.as_ref(); + + let key_pair = utxo.priv_key_policy.key_pair_or_err()?; + let my_address = utxo.derivation_method.iguana_or_err()?; + + let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; + let mut gas_fee = 0; + let mut outputs = Vec::with_capacity(contract_outputs.len()); + for output in contract_outputs { + gas_fee += output.gas_limit * output.gas_price; + outputs.push(output.into()); + } + + let (unsigned, data) = UtxoTxBuilder::new(self) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_gas_fee(gas_fee) + .with_dust(QRC20_DUST) + .build() + .await + .mm_err(|gen_tx_error| { + DelegationError::from_generate_tx_error(gen_tx_error, self.ticker().to_string(), utxo.decimals) + })?; + + let prev_script = ScriptBuilder::build_p2pkh(&my_address.hash); + let signed = sign_tx( + unsigned, + key_pair, + prev_script, + utxo.conf.signature_version, + utxo.conf.fork_id, + )?; + + let miner_fee = data.fee_amount + data.unused_change.unwrap_or_default(); + let generated_tx = GenerateQrc20TxResult { + signed, + miner_fee, + gas_fee, + }; + + let fee_details = Qrc20FeeDetails { + // QRC20 fees are paid in base platform currency (in particular Qtum) + coin: self.ticker().to_string(), + miner_fee: utxo_common::big_decimal_from_sat(generated_tx.miner_fee as i64, utxo.decimals), + gas_limit, + gas_price: QRC20_GAS_PRICE_DEFAULT, + total_gas_fee: utxo_common::big_decimal_from_sat(generated_tx.gas_fee as i64, utxo.decimals), + }; + let my_address_string = self.my_address().map_to_mm(DelegationError::InternalError)?; + + let spent_by_me = utxo_common::big_decimal_from_sat(data.spent_by_me as i64, utxo.decimals); + let qtum_amount = spent_by_me.clone(); + let received_by_me = utxo_common::big_decimal_from_sat(data.received_by_me as i64, utxo.decimals); + let my_balance_change = &received_by_me - &spent_by_me; + + Ok(TransactionDetails { + tx_hex: serialize(&generated_tx.signed).into(), + tx_hash: generated_tx.signed.hash().reversed().to_vec().to_tx_hash(), + from: vec![my_address_string], + to: vec![to_address], + total_amount: qtum_amount, + spent_by_me, + received_by_me, + my_balance_change, + block_height: 0, + timestamp: now_ms() / 1000, + fee_details: Some(fee_details.into()), + coin: self.ticker().to_string(), + internal_id: vec![].into(), + kmd_rewards: None, + transaction_type, + }) + } + + fn remove_delegation_output(&self, gas_limit: u64, gas_price: u64) -> QtumStakingAbiResult { + let function: ðabi::Function = QTUM_DELEGATE_CONTRACT.function("removeDelegation")?; + let params = function.encode_input(&[])?; + let script_pubkey = + generate_contract_call_script_pubkey(¶ms, gas_limit, gas_price, &QTUM_DELEGATE_CONTRACT_ADDRESS)? + .to_bytes(); + Ok(ContractCallOutput { + value: OUTPUT_QTUM_AMOUNT, + script_pubkey, + gas_limit, + gas_price, + }) + } + + fn add_delegation_output( + &self, + to_addr: H160, + addr_hash: AddressHashEnum, + fee: u64, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let function: ðabi::Function = QTUM_DELEGATE_CONTRACT.function("addDelegation")?; + let pod = self.generate_pod(addr_hash)?; + let params = function.encode_input(&[ + Token::Address(to_addr), + Token::Uint(fee.into()), + Token::Bytes(pod.into()), + ])?; + + let script_pubkey = + generate_contract_call_script_pubkey(¶ms, gas_limit, gas_price, &QTUM_DELEGATE_CONTRACT_ADDRESS)? + .to_bytes(); + Ok(ContractCallOutput { + value: OUTPUT_QTUM_AMOUNT, + script_pubkey, + gas_limit, + gas_price, + }) + } +} diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index 6482d61189..dac1d3712f 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -2,17 +2,16 @@ #![cfg_attr(target_arch = "wasm32", allow(dead_code))] use crate::utxo::{output_script, sat_from_big_decimal}; -use crate::{NumConversError, RpcTransportEventHandler, RpcTransportEventHandlerShared}; -use bigdecimal::BigDecimal; -use chain::{BlockHeader, OutPoint, Transaction as UtxoTx}; +use crate::{big_decimal_from_sat_unsigned, NumConversError, RpcTransportEventHandler, RpcTransportEventHandlerShared}; +use async_trait::async_trait; +use chain::{BlockHeader, BlockHeaderBits, BlockHeaderNonce, OutPoint, Transaction as UtxoTx}; use common::custom_futures::{select_ok_sequential, FutureTimerExt}; +use common::custom_iter::{CollectInto, TryIntoGroupMap}; use common::executor::{spawn, Timer}; -use common::jsonrpc_client::{JsonRpcClient, JsonRpcError, JsonRpcErrorType, JsonRpcMultiClient, JsonRpcRemoteAddr, - JsonRpcRequest, JsonRpcResponse, JsonRpcResponseFut, RpcRes}; +use common::jsonrpc_client::{JsonRpcBatchClient, JsonRpcBatchResponse, JsonRpcClient, JsonRpcError, JsonRpcErrorType, + JsonRpcId, JsonRpcMultiClient, JsonRpcRemoteAddr, JsonRpcRequest, JsonRpcRequestEnum, + JsonRpcResponse, JsonRpcResponseEnum, JsonRpcResponseFut, RpcRes}; use common::log::{error, info, warn}; -use common::mm_error::prelude::*; -use common::mm_number::MmNumber; -use common::wio::slurp_req; use common::{median, now_float, now_ms, OrdRange}; use derive_more::Display; use futures::channel::oneshot as async_oneshot; @@ -23,17 +22,20 @@ use futures::{select, StreamExt}; use futures01::future::select_ok; use futures01::sync::{mpsc, oneshot}; use futures01::{Future, Sink, Stream}; -use http::header::AUTHORIZATION; use http::Uri; -use http::{Request, StatusCode}; +use itertools::Itertools; +use keys::hash::H256; use keys::{Address, Type as ScriptType}; +use mm2_err_handle::prelude::*; +use mm2_number::{BigDecimal, BigInt, MmNumber}; #[cfg(test)] use mocktopus::macros::*; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; use serde_json::{self as json, Value as Json}; -use serialization::{deserialize, serialize, serialize_with_flags, CoinVariant, CompactInteger, Reader, - SERIALIZE_TRANSACTION_WITNESS}; +use serialization::{coin_variant_by_ticker, deserialize, serialize, serialize_with_flags, CoinVariant, CompactInteger, + Reader, SERIALIZE_TRANSACTION_WITNESS}; use sha2::{Digest, Sha256}; -use std::collections::hash_map::{Entry, HashMap}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; use std::fmt; use std::io; use std::net::{SocketAddr, ToSocketAddrs}; @@ -46,18 +48,31 @@ use std::time::Duration; cfg_native! { use futures::future::Either; use futures::io::Error; + use http::header::AUTHORIZATION; + use http::{Request, StatusCode}; + use rustls::client::ServerCertVerified; + use rustls::{Certificate, ClientConfig, ServerName, OwnedTrustAnchor, RootCertStore}; + use std::convert::TryFrom; use std::pin::Pin; use std::task::{Context, Poll}; + use std::time::SystemTime; use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadBuf}; use tokio::net::TcpStream; use tokio_rustls::{client::TlsStream, TlsConnector}; - use tokio_rustls::webpki::DNSNameRef; + use tokio_rustls::webpki::DnsNameRef; use webpki_roots::TLS_SERVER_ROOTS; } pub type AddressesByLabelResult = HashMap; +pub type JsonRpcPendingRequestsShared = Arc>; +pub type JsonRpcPendingRequests = HashMap>; +pub type UnspentMap = HashMap>; + +type ElectrumScriptHash = String; +type ScriptHashUnspents = Vec; #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct AddressPurpose { purpose: String, } @@ -66,15 +81,17 @@ pub struct AddressPurpose { pub struct NoCertificateVerification {} #[cfg(not(target_arch = "wasm32"))] -impl rustls::ServerCertVerifier for NoCertificateVerification { +impl rustls::client::ServerCertVerifier for NoCertificateVerification { fn verify_server_cert( &self, - _roots: &rustls::RootCertStore, - _presented_certs: &[rustls::Certificate], - _dns_name: DNSNameRef<'_>, - _ocsp: &[u8], - ) -> Result { - Ok(rustls::ServerCertVerified::assertion()) + _: &Certificate, + _: &[Certificate], + _: &ServerName, + _: &mut dyn Iterator, + _: &[u8], + _: SystemTime, + ) -> Result { + Ok(rustls::client::ServerCertVerified::assertion()) } } @@ -114,13 +131,13 @@ impl Clone for UtxoRpcClientEnum { impl UtxoRpcClientEnum { pub fn wait_for_confirmations( &self, - tx: &UtxoTx, + tx_hash: H256Json, + expiry_height: u32, confirmations: u32, requires_notarization: bool, wait_until: u64, check_every: u64, ) -> Box + Send> { - let tx = tx.clone(); let selfi = self.clone(); let fut = async move { loop { @@ -128,16 +145,12 @@ impl UtxoRpcClientEnum { return ERR!( "Waited too long until {} for transaction {:?} to be confirmed {} times", wait_until, - tx, + tx_hash, confirmations ); } - match selfi - .get_verbose_transaction(tx.hash().reversed().into()) - .compat() - .await - { + match selfi.get_verbose_transaction(&tx_hash).compat().await { Ok(t) => { let tx_confirmations = if requires_notarization { t.confirmations @@ -149,18 +162,30 @@ impl UtxoRpcClientEnum { } else { info!( "Waiting for tx {:?} confirmations, now {}, required {}, requires_notarization {}", - tx.hash().reversed(), - tx_confirmations, - confirmations, - requires_notarization + tx_hash, tx_confirmations, confirmations, requires_notarization ) } }, - Err(e) => error!( - "Error {:?} getting the transaction {:?}, retrying in 10 seconds", - e, - tx.hash().reversed() - ), + Err(e) => { + if expiry_height > 0 { + let block = match selfi.get_block_count().compat().await { + Ok(b) => b, + Err(e) => { + error!("Error {} getting block number, retrying in 10 seconds", e); + Timer::sleep(check_every as f64).await; + continue; + }, + }; + + if block > expiry_height as u64 { + return ERR!("The transaction {:?} has expired, current block {}", tx_hash, block); + } + } + error!( + "Error {:?} getting the transaction {:?}, retrying in 10 seconds", + e, tx_hash + ) + }, } Timer::sleep(check_every as f64).await; @@ -168,6 +193,14 @@ impl UtxoRpcClientEnum { }; Box::new(fut.boxed().compat()) } + + #[inline] + pub fn is_native(&self) -> bool { + match self { + UtxoRpcClientEnum::Native(_) => true, + UtxoRpcClientEnum::Electrum(_) => false, + } + } } /// Generic unspent info required to build transactions, we need this separate type because native @@ -194,6 +227,23 @@ impl From for UnspentInfo { } } +#[derive(Debug, PartialEq)] +pub enum BlockHashOrHeight { + Height(i64), + Hash(H256Json), +} + +#[derive(Debug, PartialEq)] +pub struct SpentOutputInfo { + // The transaction spending the output + pub spending_tx: UtxoTx, + // The input index that spends the output + pub input_index: usize, + // The block hash or height the includes the spending transaction + // For electrum clients the block height will be returned, for native clients the block hash will be returned + pub spent_in_block: BlockHashOrHeight, +} + pub type UtxoRpcResult = Result>; pub type UtxoRpcFut = Box> + Send + 'static>; @@ -208,6 +258,7 @@ pub enum UtxoRpcError { impl From for UtxoRpcError { fn from(e: JsonRpcError) -> Self { match e.error { + JsonRpcErrorType::InvalidRequest(_) => UtxoRpcError::Internal(e.to_string()), JsonRpcErrorType::Transport(_) => UtxoRpcError::Transport(e), JsonRpcErrorType::Parse(_, _) | JsonRpcErrorType::Response(_, _) => UtxoRpcError::ResponseParseError(e), } @@ -223,38 +274,59 @@ impl From for UtxoRpcError { } /// Common operations that both types of UTXO clients have but implement them differently +#[async_trait] pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { + /// Returns available unspents for the given `address`. fn list_unspent(&self, address: &Address, decimals: u8) -> UtxoRpcFut>; - fn send_transaction(&self, tx: &UtxoTx) -> Box + Send + 'static>; + /// Returns available unspents for every given `addresses`. + fn list_unspent_group(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut; + + /// Submits the given `tx` transaction to blockchain network. + fn send_transaction(&self, tx: &UtxoTx) -> UtxoRpcFut; + /// Submits the raw `tx` transaction (serialized, hex-encoded) to blockchain network. fn send_raw_transaction(&self, tx: BytesJson) -> UtxoRpcFut; - fn get_transaction_bytes(&self, txid: H256Json) -> UtxoRpcFut; + /// Returns raw transaction (serialized, hex-encoded) by the given `txid`. + fn get_transaction_bytes(&self, txid: &H256Json) -> UtxoRpcFut; + + /// Returns verbose transaction by the given `txid`. + fn get_verbose_transaction(&self, txid: &H256Json) -> UtxoRpcFut; - fn get_verbose_transaction(&self, txid: H256Json) -> RpcRes; + /// Returns verbose transactions in the same order they were requested. + fn get_verbose_transactions(&self, tx_ids: &[H256Json]) -> UtxoRpcFut>; + /// Returns the height of the most-work fully-validated chain. fn get_block_count(&self) -> UtxoRpcFut; + /// Requests balance of the given `address`. fn display_balance(&self, address: Address, decimals: u8) -> RpcRes; - /// returns fee estimation per KByte in satoshis + /// Requests balances of the given `addresses`. + /// The pairs `(Address, BigDecimal)` are guaranteed to be in the same order in which they were requested. + fn display_balances(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut>; + + /// Returns fee estimation per KByte in satoshis. fn estimate_fee_sat( &self, decimals: u8, fee_method: &EstimateFeeMethod, mode: &Option, n_blocks: u32, - ) -> RpcRes; + ) -> UtxoRpcFut; + /// Returns the minimum fee a low-priority transaction must pay in order to be accepted to the daemon’s memory pool. fn get_relay_fee(&self) -> RpcRes; + /// Tries to find a transaction that spends the specified `vout` output of the `tx_hash` transaction. fn find_output_spend( &self, - tx: &UtxoTx, + tx_hash: H256, + script_pubkey: &[u8], vout: usize, - from_block: u64, - ) -> Box, Error = String> + Send>; + from_block: BlockHashOrHeight, + ) -> Box, Error = String> + Send>; /// Get median time past for `count` blocks in the past including `starting_block` fn get_median_time_past( @@ -263,9 +335,13 @@ pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { count: NonZeroU64, coin_variant: CoinVariant, ) -> UtxoRpcFut; + + /// Returns block time in seconds since epoch (Jan 1 1970 GMT). + async fn get_block_timestamp(&self, height: u64) -> Result>; } #[derive(Clone, Deserialize, Debug)] +#[cfg_attr(test, derive(Default))] pub struct NativeUnspent { pub txid: H256Json, pub vout: u32, @@ -297,6 +373,7 @@ pub struct ValidateAddressRes { } #[derive(Clone, Debug, Deserialize)] +#[cfg_attr(test, derive(Default))] pub struct ListTransactionsItem { pub account: Option, #[serde(default)] @@ -347,10 +424,12 @@ pub struct EstimateSmartFeeRes { pub struct ListSinceBlockRes { transactions: Vec, #[serde(rename = "lastblock")] + #[allow(dead_code)] last_block: H256Json, } #[derive(Clone, Debug, Deserialize)] +#[allow(dead_code)] pub struct NetworkInfoLocalAddress { address: String, port: u16, @@ -358,6 +437,7 @@ pub struct NetworkInfoLocalAddress { } #[derive(Clone, Debug, Deserialize)] +#[allow(dead_code)] pub struct NetworkInfoNetwork { name: String, limited: bool, @@ -367,6 +447,7 @@ pub struct NetworkInfoNetwork { } #[derive(Clone, Debug, Deserialize)] +#[allow(dead_code)] pub struct NetworkInfo { connections: u64, #[serde(rename = "localaddresses")] @@ -448,6 +529,8 @@ pub struct VerboseBlock { pub previousblockhash: Option, /// Hash of next block pub nextblockhash: Option, + #[serde(rename = "finalsaplingroot")] + pub final_sapling_root: Option, } pub type RpcReqSub = async_oneshot::Sender>; @@ -518,7 +601,17 @@ impl JsonRpcClient for NativeClientImpl { fn client_info(&self) -> String { UtxoJsonRpcClientInfo::client_info(self) } - fn transport(&self, request: JsonRpcRequest) -> JsonRpcResponseFut { + #[cfg(target_arch = "wasm32")] + fn transport(&self, _request: JsonRpcRequestEnum) -> JsonRpcResponseFut { + Box::new(futures01::future::err(ERRL!( + "'NativeClientImpl' must be used in native mode only" + ))) + } + + #[cfg(not(target_arch = "wasm32"))] + fn transport(&self, request: JsonRpcRequestEnum) -> JsonRpcResponseFut { + use mm2_net::transport::slurp_req; + let request_body = try_fus!(json::to_string(&request)); // measure now only body length, because the `hyper` crate doesn't allow to get total HTTP packet length self.event_handlers.on_outgoing_request(request_body.as_bytes()); @@ -533,7 +626,7 @@ impl JsonRpcClient for NativeClientImpl { let event_handles = self.event_handlers.clone(); Box::new(slurp_req(http_request).boxed().compat().then( - move |result| -> Result<(JsonRpcRemoteAddr, JsonRpcResponse), String> { + move |result| -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String> { let res = try_s!(result); // measure now only body length, because the `hyper` crate doesn't allow to get total HTTP packet length event_handles.on_incoming_response(&res.2); @@ -556,6 +649,10 @@ impl JsonRpcClient for NativeClientImpl { } } +impl JsonRpcBatchClient for NativeClientImpl {} + +// if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt +#[async_trait] #[cfg_attr(test, mockable)] impl UtxoRpcClientOps for NativeClient { fn list_unspent(&self, address: &Address, decimals: u8) -> UtxoRpcFut> { @@ -581,13 +678,52 @@ impl UtxoRpcClientOps for NativeClient { Box::new(fut) } - fn send_transaction(&self, tx: &UtxoTx) -> Box + Send + 'static> { + fn list_unspent_group(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut { + let mut addresses_str = Vec::with_capacity(addresses.len()); + let mut addresses_map = HashMap::with_capacity(addresses.len()); + for addr in addresses { + let addr_str = addr.to_string(); + addresses_str.push(addr_str.clone()); + addresses_map.insert(addr_str, addr); + } + + let fut = self + .list_unspent_impl(0, std::i32::MAX, addresses_str) + .map_to_mm_fut(UtxoRpcError::from) + .and_then(move |unspents| { + unspents + .into_iter() + // Convert `Vec` into `UnspentMap`. + .map(|unspent| { + let orig_address = addresses_map + .get(&unspent.address) + .or_mm_err(|| { + UtxoRpcError::InvalidResponse(format!("Unexpected address '{}'", unspent.address)) + })? + .clone(); + let unspent_info = UnspentInfo { + outpoint: OutPoint { + hash: unspent.txid.reversed().into(), + index: unspent.vout, + }, + value: sat_from_big_decimal(&unspent.amount.to_decimal(), decimals)?, + height: None, + }; + Ok((orig_address, unspent_info)) + }) + // Collect `(Address, UnspentInfo)` items into `HashMap>` grouped by the addresses. + .try_into_group_map() + }); + Box::new(fut) + } + + fn send_transaction(&self, tx: &UtxoTx) -> UtxoRpcFut { let tx_bytes = if tx.has_witness() { BytesJson::from(serialize_with_flags(tx, SERIALIZE_TRANSACTION_WITNESS)) } else { BytesJson::from(serialize(tx)) }; - Box::new(self.send_raw_transaction(tx_bytes).map_err(|e| ERRL!("{}", e))) + Box::new(self.send_raw_transaction(tx_bytes)) } /// https://developer.bitcoin.org/reference/rpc/sendrawtransaction @@ -595,12 +731,19 @@ impl UtxoRpcClientOps for NativeClient { Box::new(rpc_func!(self, "sendrawtransaction", tx).map_to_mm_fut(UtxoRpcError::from)) } - fn get_transaction_bytes(&self, txid: H256Json) -> UtxoRpcFut { + fn get_transaction_bytes(&self, txid: &H256Json) -> UtxoRpcFut { Box::new(self.get_raw_transaction_bytes(txid).map_to_mm_fut(UtxoRpcError::from)) } - fn get_verbose_transaction(&self, txid: H256Json) -> RpcRes { - self.get_raw_transaction_verbose(txid) + fn get_verbose_transaction(&self, txid: &H256Json) -> UtxoRpcFut { + Box::new(self.get_raw_transaction_verbose(txid).map_to_mm_fut(UtxoRpcError::from)) + } + + fn get_verbose_transactions(&self, tx_ids: &[H256Json]) -> UtxoRpcFut> { + Box::new( + self.get_raw_transaction_verbose_batch(tx_ids) + .map_to_mm_fut(UtxoRpcError::from), + ) } fn get_block_count(&self) -> UtxoRpcFut { @@ -618,13 +761,29 @@ impl UtxoRpcClientOps for NativeClient { ) } + fn display_balances(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut> { + let this = self.clone(); + let fut = async move { + let unspent_map = this.list_unspent_group(addresses.clone(), decimals).compat().await?; + let balances = addresses + .into_iter() + .map(|address| { + let balance = address_balance_from_unspent_map(&address, &unspent_map, decimals); + (address, balance) + }) + .collect(); + Ok(balances) + }; + Box::new(fut.boxed().compat()) + } + fn estimate_fee_sat( &self, decimals: u8, fee_method: &EstimateFeeMethod, mode: &Option, n_blocks: u32, - ) -> RpcRes { + ) -> UtxoRpcFut { match fee_method { EstimateFeeMethod::Standard => Box::new(self.estimate_fee(n_blocks).map(move |fee| { if fee > 0.00001 { @@ -647,27 +806,34 @@ impl UtxoRpcClientOps for NativeClient { fn find_output_spend( &self, - tx: &UtxoTx, + tx_hash: H256, + _script_pubkey: &[u8], vout: usize, - from_block: u64, - ) -> Box, Error = String> + Send> { + from_block: BlockHashOrHeight, + ) -> Box, Error = String> + Send> { let selfi = self.clone(); - let tx = tx.clone(); let fut = async move { - let from_block_hash = try_s!(selfi.get_block_hash(from_block).compat().await); + let from_block_hash = match from_block { + BlockHashOrHeight::Height(h) => try_s!(selfi.get_block_hash(h as u64).compat().await), + BlockHashOrHeight::Hash(h) => h, + }; let list_since_block: ListSinceBlockRes = try_s!(selfi.list_since_block(from_block_hash).compat().await); for transaction in list_since_block .transactions .into_iter() .filter(|tx| !tx.is_conflicting()) { - let maybe_spend_tx_bytes = try_s!(selfi.get_raw_transaction_bytes(transaction.txid).compat().await); + let maybe_spend_tx_bytes = try_s!(selfi.get_raw_transaction_bytes(&transaction.txid).compat().await); let maybe_spend_tx: UtxoTx = try_s!(deserialize(maybe_spend_tx_bytes.as_slice()).map_err(|e| ERRL!("{:?}", e))); - for input in maybe_spend_tx.inputs.iter() { - if input.previous_output.hash == tx.hash() && input.previous_output.index == vout as u32 { - return Ok(Some(maybe_spend_tx)); + for (index, input) in maybe_spend_tx.inputs.iter().enumerate() { + if input.previous_output.hash == tx_hash && input.previous_output.index == vout as u32 { + return Ok(Some(SpentOutputInfo { + spending_tx: maybe_spend_tx, + input_index: index, + spent_in_block: BlockHashOrHeight::Hash(transaction.blockhash), + })); } } } @@ -706,6 +872,11 @@ impl UtxoRpcClientOps for NativeClient { }; Box::new(fut.boxed().compat()) } + + async fn get_block_timestamp(&self, height: u64) -> Result> { + let block = self.get_block_by_height(height).await?; + Ok(block.time as u64) + } } #[cfg_attr(test, mockable)] @@ -727,6 +898,32 @@ impl NativeClient { let fut = async move { arc.list_unspent_concurrent_map.wrap_request(args, request_fut).await }; Box::new(fut.boxed().compat()) } + + pub fn list_all_transactions(&self, step: u64) -> RpcRes> { + let selfi = self.clone(); + let fut = async move { + let mut from = 0; + let mut transaction_list = Vec::new(); + + loop { + let transactions = selfi.list_transactions(step, from).compat().await?; + if transactions.is_empty() { + return Ok(transaction_list); + } + + transaction_list.extend(transactions.into_iter()); + from += step; + } + }; + Box::new(fut.boxed().compat()) + } +} + +impl NativeClient { + pub async fn get_block_by_height(&self, height: u64) -> UtxoRpcResult { + let block_hash = self.get_block_hash(height).compat().await?; + self.get_block(block_hash).compat().await + } } #[cfg_attr(test, mockable)] @@ -746,7 +943,7 @@ impl NativeClientImpl { txid: H256Json, index: usize, ) -> Box + Send + 'static> { - let fut = self.get_raw_transaction_bytes(txid).map_err(|e| ERRL!("{}", e)); + let fut = self.get_raw_transaction_bytes(&txid).map_err(|e| ERRL!("{}", e)); Box::new(fut.and_then(move |bytes| { let tx: UtxoTx = try_s!(deserialize(bytes.as_slice()).map_err(|e| ERRL!( "Error {:?} trying to deserialize the transaction {:?}", @@ -759,27 +956,39 @@ impl NativeClientImpl { /// https://developer.bitcoin.org/reference/rpc/getblock.html /// Always returns verbose block - pub fn get_block(&self, hash: H256Json) -> RpcRes { + pub fn get_block(&self, hash: H256Json) -> UtxoRpcFut { let verbose = true; - rpc_func!(self, "getblock", hash, verbose) + Box::new(rpc_func!(self, "getblock", hash, verbose).map_to_mm_fut(UtxoRpcError::from)) } /// https://developer.bitcoin.org/reference/rpc/getblockhash.html - pub fn get_block_hash(&self, height: u64) -> RpcRes { rpc_func!(self, "getblockhash", height) } + pub fn get_block_hash(&self, height: u64) -> UtxoRpcFut { + Box::new(rpc_func!(self, "getblockhash", height).map_to_mm_fut(UtxoRpcError::from)) + } /// https://developer.bitcoin.org/reference/rpc/getblockcount.html pub fn get_block_count(&self) -> RpcRes { rpc_func!(self, "getblockcount") } /// https://developer.bitcoin.org/reference/rpc/getrawtransaction.html /// Always returns verbose transaction - fn get_raw_transaction_verbose(&self, txid: H256Json) -> RpcRes { + fn get_raw_transaction_verbose(&self, txid: &H256Json) -> RpcRes { let verbose = 1; rpc_func!(self, "getrawtransaction", txid, verbose) } + /// https://developer.bitcoin.org/reference/rpc/getrawtransaction.html + /// Always returns verbose transactions in the same order they were requested. + fn get_raw_transaction_verbose_batch(&self, tx_ids: &[H256Json]) -> RpcRes> { + let verbose = 1; + let requests = tx_ids + .iter() + .map(|txid| rpc_req!(self, "getrawtransaction", txid, verbose)); + self.batch_rpc(requests) + } + /// https://developer.bitcoin.org/reference/rpc/getrawtransaction.html /// Always returns transaction bytes - pub fn get_raw_transaction_bytes(&self, txid: H256Json) -> RpcRes { + pub fn get_raw_transaction_bytes(&self, txid: &H256Json) -> RpcRes { let verbose = 0; rpc_func!(self, "getrawtransaction", txid, verbose) } @@ -788,16 +997,18 @@ impl NativeClientImpl { /// It is recommended to set n_blocks as low as possible. /// However, in some cases, n_blocks = 1 leads to an unreasonably high fee estimation. /// https://github.com/KomodoPlatform/atomicDEX-API/issues/656#issuecomment-743759659 - fn estimate_fee(&self, n_blocks: u32) -> RpcRes { rpc_func!(self, "estimatefee", n_blocks) } + pub fn estimate_fee(&self, n_blocks: u32) -> UtxoRpcFut { + Box::new(rpc_func!(self, "estimatefee", n_blocks).map_to_mm_fut(UtxoRpcError::from)) + } /// https://developer.bitcoin.org/reference/rpc/estimatesmartfee.html /// It is recommended to set n_blocks as low as possible. /// However, in some cases, n_blocks = 1 leads to an unreasonably high fee estimation. /// https://github.com/KomodoPlatform/atomicDEX-API/issues/656#issuecomment-743759659 - pub fn estimate_smart_fee(&self, mode: &Option, n_blocks: u32) -> RpcRes { + pub fn estimate_smart_fee(&self, mode: &Option, n_blocks: u32) -> UtxoRpcFut { match mode { - Some(m) => rpc_func!(self, "estimatesmartfee", n_blocks, m), - None => rpc_func!(self, "estimatesmartfee", n_blocks), + Some(m) => Box::new(rpc_func!(self, "estimatesmartfee", n_blocks, m).map_to_mm_fut(UtxoRpcError::from)), + None => Box::new(rpc_func!(self, "estimatesmartfee", n_blocks).map_to_mm_fut(UtxoRpcError::from)), } } @@ -882,6 +1093,12 @@ impl NativeClientImpl { pub fn get_address_info(&self, address: &str) -> RpcRes { rpc_func!(self, "getaddressinfo", address) } + + /// https://developer.bitcoin.org/reference/rpc/getblockheader.html + pub fn get_block_header_bytes(&self, block_hash: H256Json) -> RpcRes { + let verbose = 0; + rpc_func!(self, "getblockheader", block_hash, verbose) + } } impl NativeClientImpl { @@ -908,46 +1125,124 @@ pub struct ElectrumUnspent { pub value: u64, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(untagged)] pub enum ElectrumNonce { Number(u64), Hash(H256Json), } +#[allow(clippy::from_over_into)] +impl Into for ElectrumNonce { + fn into(self) -> BlockHeaderNonce { + match self { + ElectrumNonce::Number(n) => BlockHeaderNonce::U32(n as u32), + ElectrumNonce::Hash(h) => BlockHeaderNonce::H256(h.into()), + } + } +} + #[derive(Debug, Deserialize)] pub struct ElectrumBlockHeadersRes { - count: u64, + pub count: u64, pub hex: BytesJson, + #[allow(dead_code)] max: u64, } /// The block header compatible with Electrum 1.2 -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct ElectrumBlockHeaderV12 { - bits: u64, - block_height: u64, - merkle_root: H256Json, - nonce: ElectrumNonce, - prev_block_hash: H256Json, - timestamp: u64, - version: u64, + pub bits: u64, + pub block_height: u64, + pub merkle_root: H256Json, + pub nonce: ElectrumNonce, + pub prev_block_hash: H256Json, + pub timestamp: u64, + pub version: u64, +} + +impl ElectrumBlockHeaderV12 { + fn as_block_header(&self) -> BlockHeader { + BlockHeader { + version: self.version as u32, + previous_header_hash: self.prev_block_hash.into(), + merkle_root_hash: self.merkle_root.into(), + claim_trie_root: None, + hash_final_sapling_root: None, + time: self.timestamp as u32, + bits: BlockHeaderBits::U32(self.bits as u32), + nonce: self.nonce.clone().into(), + solution: None, + aux_pow: None, + prog_pow: None, + mtp_pow: None, + is_verus: false, + hash_state_root: None, + hash_utxo_root: None, + prevout_stake: None, + vch_block_sig_dlgt: None, + n_height: None, + n_nonce_u64: None, + mix_hash: None, + } + } + + #[inline] + pub fn as_hex(&self) -> String { + let block_header = self.as_block_header(); + let serialized = serialize(&block_header); + hex::encode(serialized) + } + + #[inline] + pub fn hash(&self) -> H256Json { + let block_header = self.as_block_header(); + BlockHeader::hash(&block_header).into() + } } /// The block header compatible with Electrum 1.4 -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct ElectrumBlockHeaderV14 { - height: u64, - hex: BytesJson, + pub height: u64, + pub hex: BytesJson, } -#[derive(Debug, Deserialize)] +impl ElectrumBlockHeaderV14 { + pub fn hash(&self) -> H256Json { self.hex.clone().into_vec()[..].into() } +} + +#[derive(Clone, Debug, Deserialize)] #[serde(untagged)] pub enum ElectrumBlockHeader { V12(ElectrumBlockHeaderV12), V14(ElectrumBlockHeaderV14), } +/// The merkle branch of a confirmed transaction +#[derive(Clone, Debug, Deserialize)] +pub struct TxMerkleBranch { + pub merkle: Vec, + pub block_height: u64, + pub pos: usize, +} + +#[derive(Debug, PartialEq)] +pub struct BestBlock { + pub height: u64, + pub hash: H256Json, +} + +impl From for BestBlock { + fn from(block_header: ElectrumBlockHeader) -> Self { + BestBlock { + height: block_header.block_height(), + hash: block_header.block_hash(), + } + } +} + #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Deserialize, Serialize)] pub enum EstimateFeeMode { @@ -957,12 +1252,19 @@ pub enum EstimateFeeMode { } impl ElectrumBlockHeader { - fn block_height(&self) -> u64 { + pub fn block_height(&self) -> u64 { match self { ElectrumBlockHeader::V12(h) => h.block_height, ElectrumBlockHeader::V14(h) => h.height, } } + + fn block_hash(&self) -> H256Json { + match self { + ElectrumBlockHeader::V12(h) => h.hash(), + ElectrumBlockHeader::V14(h) => h.hash(), + } + } } #[derive(Debug, Deserialize)] @@ -974,16 +1276,26 @@ pub struct ElectrumTxHistoryItem { #[derive(Clone, Debug, Deserialize)] pub struct ElectrumBalance { - confirmed: i64, - unconfirmed: i64, + pub(crate) confirmed: i128, + pub(crate) unconfirmed: i128, +} + +impl ElectrumBalance { + #[inline] + pub fn to_big_decimal(&self, decimals: u8) -> BigDecimal { + let balance_sat = BigInt::from(self.confirmed) + BigInt::from(self.unconfirmed); + BigDecimal::from(balance_sat) / BigDecimal::from(10u64.pow(decimals as u32)) + } } +#[inline] fn sha_256(input: &[u8]) -> Vec { let mut sha = Sha256::new(); - sha.input(input); - sha.result().to_vec() + sha.update(input); + sha.finalize().to_vec() } +#[inline] pub fn electrum_script_hash(script: &[u8]) -> Vec { let mut result = sha_256(script); result.reverse(); @@ -991,7 +1303,7 @@ pub fn electrum_script_hash(script: &[u8]) -> Vec { } #[allow(clippy::upper_case_acronyms)] -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] /// Deserializable Electrum protocol representation for RPC pub enum ElectrumProtocol { /// TCP @@ -1022,7 +1334,7 @@ pub struct ElectrumProtocolVersion { pub protocol_version: String, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] /// Electrum request RPC representation pub struct ElectrumRpcRequest { pub url: String, @@ -1075,7 +1387,7 @@ pub fn spawn_electrum( .ok_or(ERRL!("Couldn't retrieve host from addr {}", req.url))?; // check the dns name - try_s!(DNSNameRef::try_from_ascii_str(host)); + try_s!(DnsNameRef::try_from_ascii_str(host)); ElectrumConfig::SSL { dns_name: host.into(), @@ -1131,13 +1443,14 @@ pub struct ElectrumConnection { /// The client connected to this SocketAddr addr: String, /// Configuration + #[allow(dead_code)] config: ElectrumConfig, /// The Sender forwarding requests to writing part of underlying stream tx: Arc>>>>, /// The Sender used to shutdown the background connection loop when ElectrumConnection is dropped shutdown_tx: Option>, /// Responses are stored here - responses: Arc>>>, + responses: JsonRpcPendingRequestsShared, /// Selected protocol version. The value is initialized after the server.version RPC call. protocol_version: AsyncMutex>, } @@ -1230,8 +1543,8 @@ pub struct ElectrumClientImpl { async fn electrum_request_multi( client: ElectrumClient, - request: JsonRpcRequest, -) -> Result<(JsonRpcRemoteAddr, JsonRpcResponse), String> { + request: JsonRpcRequestEnum, +) -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String> { let mut futures = vec![]; let connections = client.connections.lock().await; for (i, connection) in connections.iter().enumerate() { @@ -1254,31 +1567,32 @@ async fn electrum_request_multi( if futures.is_empty() { return ERR!("All electrums are currently disconnected"); } - if request.method != "server.ping" { - match select_ok_sequential(futures).compat().await { - Ok((res, no_of_failed_requests)) => { - client.clone().rotate_servers(no_of_failed_requests).await; - Ok(res) - }, - Err(e) => return ERR!("{:?}", e), - } - } else { - // server.ping must be sent to all servers to keep all connections alive - Ok(try_s!( - select_ok(futures) + + match request { + JsonRpcRequestEnum::Single(single) if single.method == "server.ping" => { + // server.ping must be sent to all servers to keep all connections alive + return select_ok(futures) .map(|(result, _)| result) .map_err(|e| ERRL!("{:?}", e)) .compat() - .await - )) + .await; + }, + _ => (), } + + let (res, no_of_failed_requests) = select_ok_sequential(futures) + .compat() + .await + .map_err(|e| ERRL!("{:?}", e))?; + client.rotate_servers(no_of_failed_requests).await; + Ok(res) } async fn electrum_request_to( client: ElectrumClient, - request: JsonRpcRequest, + request: JsonRpcRequestEnum, to_addr: String, -) -> Result<(JsonRpcRemoteAddr, JsonRpcResponse), String> { +) -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String> { let (tx, responses) = { let connections = client.connections.lock().await; let connection = connections @@ -1387,13 +1701,15 @@ impl JsonRpcClient for ElectrumClient { fn client_info(&self) -> String { UtxoJsonRpcClientInfo::client_info(self) } - fn transport(&self, request: JsonRpcRequest) -> JsonRpcResponseFut { + fn transport(&self, request: JsonRpcRequestEnum) -> JsonRpcResponseFut { Box::new(electrum_request_multi(self.clone(), request).boxed().compat()) } } +impl JsonRpcBatchClient for ElectrumClient {} + impl JsonRpcMultiClient for ElectrumClient { - fn transport_exact(&self, to_addr: String, request: JsonRpcRequest) -> JsonRpcResponseFut { + fn transport_exact(&self, to_addr: String, request: JsonRpcRequestEnum) -> JsonRpcResponseFut { Box::new(electrum_request_to(self.clone(), request, to_addr).boxed().compat()) } } @@ -1422,7 +1738,7 @@ impl ElectrumClient { let mut map: HashMap<(H256Json, u32), bool> = HashMap::new(); let unspents = unspents .into_iter() - .filter(|unspent| match map.entry((unspent.tx_hash.clone(), unspent.tx_pos)) { + .filter(|unspent| match map.entry((unspent.tx_hash, unspent.tx_pos)) { Entry::Occupied(_) => false, Entry::Vacant(e) => { e.insert(true); @@ -1439,6 +1755,27 @@ impl ElectrumClient { Box::new(fut.boxed().compat()) } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-listunspent + /// It can return duplicates sometimes: https://github.com/artemii235/SuperNET/issues/269 + /// We should remove them to build valid transactions. + /// Please note the function returns `ScriptHashUnspents` elements in the same order in which they were requested. + pub fn scripthash_list_unspent_batch(&self, hashes: Vec) -> RpcRes> { + let requests = hashes + .iter() + .map(|hash| rpc_req!(self, "blockchain.scripthash.listunspent", hash)); + Box::new(self.batch_rpc(requests).map(move |unspents: Vec| { + unspents + .into_iter() + .map(|hash_unspents| { + hash_unspents + .into_iter() + .unique_by(|unspent| (unspent.tx_hash, unspent.tx_pos)) + .collect::>() + }) + .collect() + })) + } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-history pub fn scripthash_get_history(&self, hash: &str) -> RpcRes> { rpc_func!(self, "blockchain.scripthash.get_history", hash) @@ -1455,13 +1792,25 @@ impl ElectrumClient { Box::new(fut.boxed().compat()) } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-gethistory + /// Requests balances in a batch and returns them in the same order they were requested. + pub fn scripthash_get_balances(&self, hashes: I) -> RpcRes> + where + I: IntoIterator, + { + let requests = hashes + .into_iter() + .map(|hash| rpc_req!(self, "blockchain.scripthash.get_balance", &hash)); + self.batch_rpc(requests) + } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe pub fn blockchain_headers_subscribe(&self) -> RpcRes { rpc_func!(self, "blockchain.headers.subscribe") } /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-broadcast - fn blockchain_transaction_broadcast(&self, tx: BytesJson) -> RpcRes { + pub fn blockchain_transaction_broadcast(&self, tx: BytesJson) -> RpcRes { rpc_func!(self, "blockchain.transaction.broadcast", tx) } @@ -1469,19 +1818,78 @@ impl ElectrumClient { /// It is recommended to set n_blocks as low as possible. /// However, in some cases, n_blocks = 1 leads to an unreasonably high fee estimation. /// https://github.com/KomodoPlatform/atomicDEX-API/issues/656#issuecomment-743759659 - fn estimate_fee(&self, mode: &Option, n_blocks: u32) -> RpcRes { + pub fn estimate_fee(&self, mode: &Option, n_blocks: u32) -> UtxoRpcFut { match mode { - Some(m) => rpc_func!(self, "blockchain.estimatefee", n_blocks, m), - None => rpc_func!(self, "blockchain.estimatefee", n_blocks), + Some(m) => { + Box::new(rpc_func!(self, "blockchain.estimatefee", n_blocks, m).map_to_mm_fut(UtxoRpcError::from)) + }, + None => Box::new(rpc_func!(self, "blockchain.estimatefee", n_blocks).map_to_mm_fut(UtxoRpcError::from)), } } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-block-header + pub fn blockchain_block_header(&self, height: u64) -> RpcRes { + rpc_func!(self, "blockchain.block.header", height) + } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-block-headers pub fn blockchain_block_headers(&self, start_height: u64, count: NonZeroU64) -> RpcRes { rpc_func!(self, "blockchain.block.headers", start_height, count) } + + pub fn retrieve_last_headers( + &self, + blocks_limit_to_check: NonZeroU64, + block_height: u64, + ) -> UtxoRpcFut<(HashMap, Vec)> { + let coin_name = self.coin_ticker.clone(); + let (from, count) = { + let from = if block_height < blocks_limit_to_check.get() { + 0 + } else { + block_height - blocks_limit_to_check.get() + }; + (from, blocks_limit_to_check) + }; + Box::new( + self.blockchain_block_headers(from, count) + .map_to_mm_fut(UtxoRpcError::from) + .and_then(move |headers| { + let (block_registry, block_headers) = { + if headers.count == 0 { + return MmError::err(UtxoRpcError::Internal("No headers available".to_string())); + } + let len = CompactInteger::from(headers.count); + let mut serialized = serialize(&len).take(); + serialized.extend(headers.hex.0.into_iter()); + let coin_variant = coin_variant_by_ticker(&coin_name); + let mut reader = Reader::new_with_coin_variant(serialized.as_slice(), coin_variant); + let maybe_block_headers = reader.read_list::(); + let block_headers = match maybe_block_headers { + Ok(headers) => headers, + Err(e) => return MmError::err(UtxoRpcError::InvalidResponse(format!("{:?}", e))), + }; + let mut block_registry: HashMap = HashMap::new(); + let mut starting_height = from; + for block_header in &block_headers { + block_registry.insert(starting_height, block_header.clone()); + starting_height += 1; + } + (block_registry, block_headers) + }; + Ok((block_registry, block_headers)) + }), + ) + } + + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get-merkle + pub fn blockchain_transaction_get_merkle(&self, txid: H256Json, height: u64) -> RpcRes { + rpc_func!(self, "blockchain.transaction.get_merkle", txid, height) + } } +// if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt +#[async_trait] #[cfg_attr(test, mockable)] impl UtxoRpcClientOps for ElectrumClient { fn list_unspent(&self, address: &Address, _decimals: u8) -> UtxoRpcFut> { @@ -1506,13 +1914,43 @@ impl UtxoRpcClientOps for ElectrumClient { ) } - fn send_transaction(&self, tx: &UtxoTx) -> Box + Send + 'static> { + fn list_unspent_group(&self, addresses: Vec
, _decimals: u8) -> UtxoRpcFut { + let script_hashes = addresses + .iter() + .map(|addr| { + let script = output_script(addr, ScriptType::P2PKH); + let script_hash = electrum_script_hash(&script); + hex::encode(script_hash) + }) + .collect(); + + let this = self.clone(); + let fut = async move { + let unspents = this.scripthash_list_unspent_batch(script_hashes).compat().await?; + + let unspent_map = addresses + .into_iter() + // `scripthash_list_unspent_batch` returns `ScriptHashUnspents` elements in the same order in which they were requested. + // So we can zip `addresses` and `unspents` into one iterator. + .zip(unspents) + // Map `(Address, Vec)` pairs into `(Address, Vec)`. + .map(|(address, electrum_unspents)| (address, electrum_unspents.collect_into())) + .collect(); + Ok(unspent_map) + }; + Box::new(fut.boxed().compat()) + } + + fn send_transaction(&self, tx: &UtxoTx) -> UtxoRpcFut { let bytes = if tx.has_witness() { BytesJson::from(serialize_with_flags(tx, SERIALIZE_TRANSACTION_WITNESS)) } else { BytesJson::from(serialize(tx)) }; - Box::new(self.blockchain_transaction_broadcast(bytes).map_err(|e| ERRL!("{}", e))) + Box::new( + self.blockchain_transaction_broadcast(bytes) + .map_to_mm_fut(UtxoRpcError::from), + ) } fn send_raw_transaction(&self, tx: BytesJson) -> UtxoRpcFut { @@ -1524,16 +1962,26 @@ impl UtxoRpcClientOps for ElectrumClient { /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get /// returns transaction bytes by default - fn get_transaction_bytes(&self, txid: H256Json) -> UtxoRpcFut { + fn get_transaction_bytes(&self, txid: &H256Json) -> UtxoRpcFut { let verbose = false; Box::new(rpc_func!(self, "blockchain.transaction.get", txid, verbose).map_to_mm_fut(UtxoRpcError::from)) } /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get /// returns verbose transaction by default - fn get_verbose_transaction(&self, txid: H256Json) -> RpcRes { + fn get_verbose_transaction(&self, txid: &H256Json) -> UtxoRpcFut { let verbose = true; - rpc_func!(self, "blockchain.transaction.get", txid, verbose) + Box::new(rpc_func!(self, "blockchain.transaction.get", txid, verbose).map_to_mm_fut(UtxoRpcError::from)) + } + + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get + /// Returns verbose transactions in a batch. + fn get_verbose_transactions(&self, tx_ids: &[H256Json]) -> UtxoRpcFut> { + let verbose = true; + let requests = tx_ids + .iter() + .map(|txid| rpc_req!(self, "blockchain.transaction.get", txid, verbose)); + Box::new(self.batch_rpc(requests).map_to_mm_fut(UtxoRpcError::from)) } fn get_block_count(&self) -> UtxoRpcFut { @@ -1547,9 +1995,32 @@ impl UtxoRpcClientOps for ElectrumClient { fn display_balance(&self, address: Address, decimals: u8) -> RpcRes { let hash = electrum_script_hash(&output_script(&address, ScriptType::P2PKH)); let hash_str = hex::encode(hash); - Box::new(self.scripthash_get_balance(&hash_str).map(move |result| { - BigDecimal::from(result.confirmed + result.unconfirmed) / BigDecimal::from(10u64.pow(decimals as u32)) - })) + Box::new( + self.scripthash_get_balance(&hash_str) + .map(move |electrum_balance| electrum_balance.to_big_decimal(decimals)), + ) + } + + fn display_balances(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut> { + let this = self.clone(); + let fut = async move { + let hashes = addresses.iter().map(|address| { + let hash = electrum_script_hash(&output_script(address, ScriptType::P2PKH)); + hex::encode(hash) + }); + + let electrum_balances = this.scripthash_get_balances(hashes).compat().await?; + let balances = electrum_balances + .into_iter() + // `scripthash_get_balances` returns `ElectrumBalance` elements in the same order in which they were requested. + // So we can zip `addresses` and the balances into one iterator. + .zip(addresses) + .map(|(electrum_balance, address)| (address, electrum_balance.to_big_decimal(decimals))) + .collect(); + Ok(balances) + }; + + Box::new(fut.boxed().compat()) } fn estimate_fee_sat( @@ -1558,7 +2029,7 @@ impl UtxoRpcClientOps for ElectrumClient { _fee_method: &EstimateFeeMethod, mode: &Option, n_blocks: u32, - ) -> RpcRes { + ) -> UtxoRpcFut { Box::new(self.estimate_fee(mode, n_blocks).map(move |fee| { if fee > 0.00001 { (fee * 10.0_f64.powf(decimals as f64)) as u64 @@ -1572,13 +2043,13 @@ impl UtxoRpcClientOps for ElectrumClient { fn find_output_spend( &self, - tx: &UtxoTx, + tx_hash: H256, + script_pubkey: &[u8], vout: usize, - _from_block: u64, - ) -> Box, Error = String> + Send> { + _from_block: BlockHashOrHeight, + ) -> Box, Error = String> + Send> { let selfi = self.clone(); - let script_hash = hex::encode(electrum_script_hash(&tx.outputs[vout].script_pubkey)); - let tx = tx.clone(); + let script_hash = hex::encode(electrum_script_hash(script_pubkey)); let fut = async move { let history = try_s!(selfi.scripthash_get_history(&script_hash).compat().await); @@ -1587,13 +2058,17 @@ impl UtxoRpcClientOps for ElectrumClient { } for item in history.iter() { - let transaction = try_s!(selfi.get_transaction_bytes(item.tx_hash.clone()).compat().await); + let transaction = try_s!(selfi.get_transaction_bytes(&item.tx_hash).compat().await); let maybe_spend_tx: UtxoTx = try_s!(deserialize(transaction.as_slice()).map_err(|e| ERRL!("{:?}", e))); - for input in maybe_spend_tx.inputs.iter() { - if input.previous_output.hash == tx.hash() && input.previous_output.index == vout as u32 { - return Ok(Some(maybe_spend_tx)); + for (index, input) in maybe_spend_tx.inputs.iter().enumerate() { + if input.previous_output.hash == tx_hash && input.previous_output.index == vout as u32 { + return Ok(Some(SpentOutputInfo { + spending_tx: maybe_spend_tx, + input_index: index, + spent_in_block: BlockHashOrHeight::Height(item.height), + })); } } } @@ -1631,6 +2106,13 @@ impl UtxoRpcClientOps for ElectrumClient { }), ) } + + async fn get_block_timestamp(&self, height: u64) -> Result> { + let header_bytes = self.blockchain_block_header(height).compat().await?; + let header: BlockHeader = + deserialize(header_bytes.0.as_slice()).map_to_mm(|e| UtxoRpcError::InvalidResponse(format!("{:?}", e)))?; + Ok(header.time as u64) + } } #[cfg_attr(test, mockable)] @@ -1666,62 +2148,56 @@ fn rx_to_stream(rx: mpsc::Receiver>) -> impl Stream, Erro rx.map_err(|_| panic!("errors not possible on rx")) } -async fn electrum_process_json( - raw_json: Json, - arc: &Arc>>>, -) { +async fn electrum_process_json(raw_json: Json, arc: &JsonRpcPendingRequestsShared) { // detect if we got standard JSONRPC response or subscription response as JSONRPC request - if raw_json["method"].is_null() && raw_json["params"].is_null() { - let response: JsonRpcResponse = match json::from_value(raw_json) { - Ok(res) => res, - Err(e) => { - error!("{}", e); - return; - }, - }; - let mut resp = arc.lock().await; - // the corresponding sender may not exist, receiver may be dropped - // these situations are not considered as errors so we just silently skip them - if let Some(tx) = resp.remove(&response.id.to_string()) { - tx.send(response).unwrap_or(()) - } - drop(resp); - } else { - let request: JsonRpcRequest = match json::from_value(raw_json) { - Ok(res) => res, - Err(e) => { - error!("{}", e); - return; - }, - }; - let id = match request.method.as_ref() { - BLOCKCHAIN_HEADERS_SUB_ID => BLOCKCHAIN_HEADERS_SUB_ID, - _ => { - error!("Couldn't get id of request {:?}", request); - return; - }, - }; + #[derive(Deserialize)] + #[serde(untagged)] + enum ElectrumRpcResponseEnum { + /// The standard JSONRPC single response. + SingleResponse(JsonRpcResponse), + /// The batch of standard JSONRPC responses. + BatchResponses(JsonRpcBatchResponse), + /// The subscription response as JSONRPC request. + SubscriptionNotification(JsonRpcRequest), + } + + let response: ElectrumRpcResponseEnum = match json::from_value(raw_json) { + Ok(res) => res, + Err(e) => { + error!("{}", e); + return; + }, + }; - let response = JsonRpcResponse { - id: id.into(), - jsonrpc: "2.0".into(), - result: request.params[0].clone(), - error: Json::Null, - }; - let mut resp = arc.lock().await; - // the corresponding sender may not exist, receiver may be dropped - // these situations are not considered as errors so we just silently skip them - if let Some(tx) = resp.remove(&response.id.to_string()) { - tx.send(response).unwrap_or(()) - } - drop(resp); + let response = match response { + ElectrumRpcResponseEnum::SingleResponse(single) => JsonRpcResponseEnum::Single(single), + ElectrumRpcResponseEnum::BatchResponses(batch) => JsonRpcResponseEnum::Batch(batch), + ElectrumRpcResponseEnum::SubscriptionNotification(req) => { + let id = match req.method.as_ref() { + BLOCKCHAIN_HEADERS_SUB_ID => BLOCKCHAIN_HEADERS_SUB_ID, + _ => { + error!("Couldn't get id of request {:?}", req); + return; + }, + }; + JsonRpcResponseEnum::Single(JsonRpcResponse { + id: id.into(), + jsonrpc: "2.0".into(), + result: req.params[0].clone(), + error: Json::Null, + }) + }, + }; + + // the corresponding sender may not exist, receiver may be dropped + // these situations are not considered as errors so we just silently skip them + let mut pending = arc.lock().await; + if let Some(tx) = pending.remove(&response.rpc_id()) { + tx.send(response).ok(); } } -async fn electrum_process_chunk( - chunk: &[u8], - arc: &Arc>>>, -) { +async fn electrum_process_chunk(chunk: &[u8], arc: &JsonRpcPendingRequestsShared) { // we should split the received chunk because we can get several responses in 1 chunk. let split = chunk.split(|item| *item == b'\n'); for chunk in split { @@ -1826,11 +2302,41 @@ async fn electrum_last_chunk_loop(last_chunk: Arc) { } } +#[cfg(not(target_arch = "wasm32"))] +fn rustls_client_config(unsafe_conf: bool) -> Arc { + let mut cert_store = RootCertStore::empty(); + + cert_store.add_server_trust_anchors( + TLS_SERVER_ROOTS + .0 + .iter() + .map(|ta| OwnedTrustAnchor::from_subject_spki_name_constraints(ta.subject, ta.spki, ta.name_constraints)), + ); + + let mut tls_config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(cert_store) + .with_no_client_auth(); + + if unsafe_conf { + tls_config + .dangerous() + .set_certificate_verifier(Arc::new(NoCertificateVerification {})); + } + Arc::new(tls_config) +} + +#[cfg(not(target_arch = "wasm32"))] +lazy_static! { + static ref SAFE_TLS_CONFIG: Arc = rustls_client_config(false); + static ref UNSAFE_TLS_CONFIG: Arc = rustls_client_config(true); +} + #[cfg(not(target_arch = "wasm32"))] async fn connect_loop( config: ElectrumConfig, addr: String, - responses: Arc>>>, + responses: JsonRpcPendingRequestsShared, connection_tx: Arc>>>>, event_handlers: Vec, ) -> Result<(), ()> { @@ -1850,19 +2356,16 @@ async fn connect_loop( dns_name, skip_validation, } => { - let mut ssl_config = rustls::ClientConfig::new(); - ssl_config.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS); - if skip_validation { - ssl_config - .dangerous() - .set_certificate_verifier(Arc::new(NoCertificateVerification {})); - } - let tls_connector = TlsConnector::from(Arc::new(ssl_config)); + let tls_connector = if skip_validation { + TlsConnector::from(UNSAFE_TLS_CONFIG.clone()) + } else { + TlsConnector::from(SAFE_TLS_CONFIG.clone()) + }; Either::Right(TcpStream::connect(&socket_addr).and_then(move |stream| { // Can use `unwrap` cause `dns_name` is pre-checked. - let dns = DNSNameRef::try_from_ascii_str(&dns_name) - .map_err(|e| fomat!([e])) + let dns = ServerName::try_from(dns_name.as_str()) + .map_err(|e| format!("{:?}", e)) .unwrap(); tls_connector.connect(dns, stream).map_ok(ElectrumStream::Tls) })) @@ -1951,7 +2454,7 @@ async fn connect_loop( async fn connect_loop( _config: ElectrumConfig, addr: String, - responses: Arc>>>, + responses: JsonRpcPendingRequestsShared, connection_tx: Arc>>>>, event_handlers: Vec, ) -> Result<(), ()> { @@ -1961,7 +2464,7 @@ async fn connect_loop( static ref CONN_IDX: Arc = Arc::new(AtomicUsize::new(0)); } - use common::wasm_ws::ws_transport; + use mm2_net::wasm_ws::ws_transport; let delay = Arc::new(AtomicU64::new(0)); loop { @@ -2058,7 +2561,7 @@ fn electrum_connect( event_handlers: Vec, ) -> ElectrumConnection { let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); - let responses = Arc::new(AsyncMutex::new(HashMap::new())); + let responses = Arc::new(AsyncMutex::new(JsonRpcPendingRequests::default())); let tx = Arc::new(AsyncMutex::new(None)); let connect_loop = connect_loop( @@ -2082,11 +2585,11 @@ fn electrum_connect( } fn electrum_request( - request: JsonRpcRequest, + request: JsonRpcRequestEnum, tx: mpsc::Sender>, - responses: Arc>>>, + responses: JsonRpcPendingRequestsShared, timeout: u64, -) -> Box + Send + 'static> { +) -> Box + Send + 'static> { let send_fut = async move { let mut json = try_s!(json::to_string(&request)); #[cfg(not(target_arch = "wasm"))] @@ -2096,12 +2599,11 @@ fn electrum_request( json.push('\n'); } - let request_id = request.get_id().to_string(); let (req_tx, resp_rx) = async_oneshot::channel(); - responses.lock().await.insert(request_id, req_tx); + responses.lock().await.insert(request.rpc_id(), req_tx); try_s!(tx.send(json.into_bytes()).compat().await); - let response = try_s!(resp_rx.await); - Ok(response) + let resps = try_s!(resp_rx.await); + Ok(resps) }; let send_fut = send_fut .boxed() @@ -2114,3 +2616,15 @@ fn electrum_request( .map_err(|e| ERRL!("{}", e)); Box::new(send_fut) } + +fn address_balance_from_unspent_map(address: &Address, unspent_map: &UnspentMap, decimals: u8) -> BigDecimal { + let unspents = match unspent_map.get(address) { + Some(unspents) => unspents, + // If `balances` doesn't contain `address`, there are no unspents related to the address. + // Consider the balance of that address equal to 0. + None => return BigDecimal::from(0), + }; + unspents.iter().fold(BigDecimal::from(0), |sum, unspent| { + sum + big_decimal_from_sat_unsigned(unspent.value, decimals) + }) +} diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index a7769e7557..9ed15d4843 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -1,40 +1,61 @@ -use super::p2pkh_spend; -use super::utxo_standard::UtxoStandardCoin; - -use crate::utxo::rpc_clients::{UnspentInfo, UtxoRpcClientEnum, UtxoRpcError}; -use crate::utxo::utxo_common::{self, big_decimal_from_sat_unsigned, generate_transaction, p2sh_spend, payment_script}; -use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, FeePolicy, GenerateTxError, RecentlySpentOutPoints, - UtxoCommonOps, UtxoTx}; -use crate::{BalanceError, BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, - MmCoin, NegotiateSwapContractAddrErr, NumConversError, SwapOps, TradeFee, TradePreimageFut, - TradePreimageValue, TransactionEnum, TransactionFut, ValidateAddressResult, WithdrawFut, WithdrawRequest}; - -use bitcoin_cash_slp::{slp_send_output, SlpTokenType, TokenId}; +//! The module implementing Simple Ledger Protocol (SLP) support. +//! It's a custom token format mostly used on the Bitcoin Cash blockchain. +//! Tracking issue: https://github.com/KomodoPlatform/atomicDEX-API/issues/701 +//! More info about the protocol and implementation guides can be found at https://slp.dev/ + +use crate::my_tx_history_v2::CoinWithTxHistoryV2; +use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; +use crate::utxo::bch::BchCoin; +use crate::utxo::bchd_grpc::{check_slp_transaction, validate_slp_utxos, ValidateSlpUtxosErr}; +use crate::utxo::rpc_clients::{UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcResult}; +use crate::utxo::utxo_common::{self, big_decimal_from_sat_unsigned, payment_script, UtxoTxBuilder}; +use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualTxFee, AdditionalTxData, BroadcastTxErr, + FeePolicy, GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, + UtxoCommonOps, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps}; +use crate::{BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, + NegotiateSwapContractAddrErr, NumConversError, PrivKeyNotAllowed, RawTransactionFut, + RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradeFee, TradePreimageError, + TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, + TransactionErr, TransactionFut, TxFeeDetails, UnexpectedDerivationMethod, ValidateAddressResult, + ValidatePaymentInput, VerificationError, VerificationResult, WithdrawError, WithdrawFee, WithdrawFut, + WithdrawRequest}; +use async_trait::async_trait; use bitcrypto::dhash160; use chain::constants::SEQUENCE_FINAL; use chain::{OutPoint, TransactionOutput}; -use common::mm_ctx::MmArc; -use common::mm_error::prelude::*; -use common::mm_number::{BigDecimal, MmNumber}; +use common::log::warn; +use common::now_ms; use derive_more::Display; use futures::compat::Future01CompatExt; -use futures::lock::MutexGuard as AsyncMutexGuard; use futures::{FutureExt, TryFutureExt}; use futures01::Future; -use keys::Public; +use hex::FromHexError; +use keys::hash::H160; +use keys::{AddressHashEnum, CashAddrType, CashAddress, CompactSignature, KeyPair, NetworkPrefix as CashAddrPrefix, + Public}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::{BigDecimal, MmNumber}; use primitives::hash::H256; -use rpc::v1::types::Bytes as BytesJson; +use rpc::v1::types::{Bytes as BytesJson, ToTxHash, H256 as H256Json}; use script::bytes::Bytes; -use script::{Builder as ScriptBuilder, Opcode, Script}; +use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; use serialization::{deserialize, serialize, Deserializable, Error, Reader}; use serialization_derive::Deserializable; use std::convert::TryInto; -use std::sync::atomic::AtomicU64; +use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; use std::sync::Arc; +use utxo_signer::with_key_pair::{p2pkh_spend, p2sh_spend, sign_tx, UtxoSignWithKeyPairError}; const SLP_SWAP_VOUT: usize = 1; const SLP_FEE_VOUT: usize = 1; +const SLP_HTLC_SPEND_SIZE: u64 = 555; +const SLP_LOKAD_ID: &str = "SLP\x00"; +const SLP_FUNGIBLE: u8 = 1; +const SLP_SEND: &str = "SEND"; +const SLP_MINT: &str = "MINT"; +const SLP_GENESIS: &str = "GENESIS"; #[derive(Debug)] pub struct SlpTokenConf { @@ -44,29 +65,37 @@ pub struct SlpTokenConf { required_confirmations: AtomicU64, } +/// Minimalistic info that is used to be stored outside of the token's context +/// E.g. in the platform BCHCoin +#[derive(Debug)] +pub struct SlpTokenInfo { + pub token_id: H256, + pub decimals: u8, +} + #[derive(Clone, Debug)] pub struct SlpToken { conf: Arc, - platform_utxo: UtxoStandardCoin, + platform_coin: BchCoin, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct SlpUnspent { pub bch_unspent: UnspentInfo, pub slp_amount: u64, } #[derive(Clone, Debug)] -struct SlpOutput { - amount: u64, - script_pubkey: Bytes, +pub struct SlpOutput { + pub amount: u64, + pub script_pubkey: Bytes, } /// The SLP transaction preimage -struct SlpTxPreimage<'a> { - inputs: Vec, +struct SlpTxPreimage { + slp_inputs: Vec, + available_bch_inputs: Vec, outputs: Vec, - recently_spent: AsyncMutexGuard<'a, RecentlySpentOutPoints>, } #[derive(Debug, Display)] @@ -75,21 +104,36 @@ enum ValidateHtlcError { #[display(fmt = "TxParseError: {:?}", _0)] TxParseError(Error), #[display(fmt = "OpReturnParseError: {:?}", _0)] - OpReturnParseError(Error), + OpReturnParseError(ParseSlpScriptError), InvalidSlpDetails, + InvalidSlpUtxo(ValidateSlpUtxosErr), NumConversionErr(NumConversError), ValidatePaymentError(String), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), + OtherPubInvalid(keys::Error), } impl From for ValidateHtlcError { fn from(err: NumConversError) -> ValidateHtlcError { ValidateHtlcError::NumConversionErr(err) } } +impl From for ValidateHtlcError { + fn from(err: ParseSlpScriptError) -> Self { ValidateHtlcError::OpReturnParseError(err) } +} + +impl From for ValidateHtlcError { + fn from(err: ValidateSlpUtxosErr) -> Self { ValidateHtlcError::InvalidSlpUtxo(err) } +} + +impl From for ValidateHtlcError { + fn from(e: UnexpectedDerivationMethod) -> Self { ValidateHtlcError::UnexpectedDerivationMethod(e) } +} + #[derive(Debug, Display)] enum ValidateDexFeeError { TxLackOfOutputs, #[display(fmt = "OpReturnParseError: {:?}", _0)] - OpReturnParseError(Error), + OpReturnParseError(ParseSlpScriptError), InvalidSlpDetails, NumConversionErr(NumConversError), ValidatePaymentError(String), @@ -99,12 +143,18 @@ impl From for ValidateDexFeeError { fn from(err: NumConversError) -> ValidateDexFeeError { ValidateDexFeeError::NumConversionErr(err) } } -#[allow(clippy::upper_case_acronyms)] +impl From for ValidateDexFeeError { + fn from(err: ParseSlpScriptError) -> Self { ValidateDexFeeError::OpReturnParseError(err) } +} + +#[allow(clippy::upper_case_acronyms, clippy::large_enum_variant)] #[derive(Debug, Display)] pub enum SpendP2SHError { GenerateTxErr(GenerateTxError), Rpc(UtxoRpcError), - GetUnspentsErr(SlpUnspentsErr), + SignTxErr(UtxoSignWithKeyPairError), + PrivKeyNotAllowed(PrivKeyNotAllowed), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), String(String), } @@ -116,8 +166,16 @@ impl From for SpendP2SHError { fn from(err: UtxoRpcError) -> SpendP2SHError { SpendP2SHError::Rpc(err) } } -impl From for SpendP2SHError { - fn from(err: SlpUnspentsErr) -> SpendP2SHError { SpendP2SHError::GetUnspentsErr(err) } +impl From for SpendP2SHError { + fn from(sign: UtxoSignWithKeyPairError) -> SpendP2SHError { SpendP2SHError::SignTxErr(sign) } +} + +impl From for SpendP2SHError { + fn from(e: PrivKeyNotAllowed) -> Self { SpendP2SHError::PrivKeyNotAllowed(e) } +} + +impl From for SpendP2SHError { + fn from(e: UnexpectedDerivationMethod) -> Self { SpendP2SHError::UnexpectedDerivationMethod(e) } } impl From for SpendP2SHError { @@ -136,6 +194,12 @@ pub enum SpendHtlcError { RpcErr(UtxoRpcError), #[allow(clippy::upper_case_acronyms)] SpendP2SHErr(SpendP2SHError), + OpReturnParseError(ParseSlpScriptError), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), +} + +impl From for SpendHtlcError { + fn from(e: UnexpectedDerivationMethod) -> Self { SpendHtlcError::UnexpectedDerivationMethod(e) } } impl From for SpendHtlcError { @@ -158,12 +222,86 @@ impl From for SpendHtlcError { fn from(err: UtxoRpcError) -> SpendHtlcError { SpendHtlcError::RpcErr(err) } } +impl From for SpendHtlcError { + fn from(err: ParseSlpScriptError) -> Self { SpendHtlcError::OpReturnParseError(err) } +} + +fn slp_send_output(token_id: &H256, amounts: &[u64]) -> TransactionOutput { + let mut script_builder = ScriptBuilder::default() + .push_opcode(Opcode::OP_RETURN) + .push_data(SLP_LOKAD_ID.as_bytes()) + .push_data(&[SLP_FUNGIBLE]) + .push_data(SLP_SEND.as_bytes()) + .push_data(token_id.as_slice()); + for amount in amounts { + script_builder = script_builder.push_data(&amount.to_be_bytes()); + } + TransactionOutput { + value: 0, + script_pubkey: script_builder.into_bytes(), + } +} + +pub fn slp_genesis_output( + ticker: &str, + name: &str, + token_document_url: Option<&str>, + token_document_hash: Option, + decimals: u8, + mint_baton_vout: Option, + initial_token_mint_quantity: u64, +) -> TransactionOutput { + let mut script_builder = ScriptBuilder::default() + .push_opcode(Opcode::OP_RETURN) + .push_data(SLP_LOKAD_ID.as_bytes()) + .push_data(&[SLP_FUNGIBLE]) + .push_data(SLP_GENESIS.as_bytes()) + .push_data(ticker.as_bytes()) + .push_data(name.as_bytes()); + + script_builder = match token_document_url { + Some(url) => script_builder.push_data(url.as_bytes()), + None => script_builder + .push_opcode(Opcode::OP_PUSHDATA1) + .push_opcode(Opcode::OP_0), + }; + + script_builder = match token_document_hash { + Some(hash) => script_builder.push_data(hash.as_slice()), + None => script_builder + .push_opcode(Opcode::OP_PUSHDATA1) + .push_opcode(Opcode::OP_0), + }; + + script_builder = script_builder.push_data(&[decimals]); + script_builder = match mint_baton_vout { + Some(vout) => script_builder.push_data(&[vout]), + None => script_builder + .push_opcode(Opcode::OP_PUSHDATA1) + .push_opcode(Opcode::OP_0), + }; + + script_builder = script_builder.push_data(&initial_token_mint_quantity.to_be_bytes()); + TransactionOutput { + value: 0, + script_pubkey: script_builder.into_bytes(), + } +} + +#[derive(Debug)] +pub struct SlpProtocolConf { + pub platform_coin_ticker: String, + pub token_id: H256, + pub decimals: u8, + pub required_confirmations: Option, +} + impl SlpToken { pub fn new( decimals: u8, ticker: String, token_id: H256, - platform_utxo: UtxoStandardCoin, + platform_coin: BchCoin, required_confirmations: u64, ) -> SlpToken { let conf = Arc::new(SlpTokenConf { @@ -172,95 +310,39 @@ impl SlpToken { token_id, required_confirmations: AtomicU64::new(required_confirmations), }); - SlpToken { conf, platform_utxo } + SlpToken { conf, platform_coin } } - fn rpc(&self) -> &UtxoRpcClientEnum { &self.platform_utxo.as_ref().rpc_client } + /// Returns the OP_RETURN output for SLP Send transaction + fn send_op_return_output(&self, amounts: &[u64]) -> TransactionOutput { + slp_send_output(&self.conf.token_id, amounts) + } + + fn rpc(&self) -> &UtxoRpcClientEnum { &self.platform_coin.as_ref().rpc_client } /// Returns unspents of the SLP token plus plain BCH UTXOs plus RecentlySpentOutPoints mutex guard - async fn slp_unspents( + async fn slp_unspents_for_spend( &self, - ) -> Result< - ( - Vec, - Vec, - AsyncMutexGuard<'_, RecentlySpentOutPoints>, - ), - MmError, - > { - let (unspents, recently_spent) = self - .platform_utxo - .list_unspent_ordered(&self.platform_utxo.as_ref().my_address) - .await?; - - let mut slp_unspents = vec![]; - let mut bch_unspents = vec![]; - - for unspent in unspents { - let prev_tx_bytes = self - .rpc() - .get_transaction_bytes(unspent.outpoint.hash.reversed().into()) - .compat() - .await?; - let prev_tx: UtxoTx = deserialize(prev_tx_bytes.0.as_slice())?; - match parse_slp_script(&prev_tx.outputs[0].script_pubkey) { - Ok(slp_data) => match slp_data.transaction { - SlpTransaction::Send { token_id, amounts } => { - if token_id == self.token_id() && unspent.outpoint.index > 0 { - match amounts.get(unspent.outpoint.index as usize - 1) { - Some(slp_amount) => slp_unspents.push(SlpUnspent { - bch_unspent: unspent, - slp_amount: *slp_amount, - }), - None => bch_unspents.push(unspent), - } - } - }, - SlpTransaction::Genesis { - initial_token_mint_quantity, - .. - } => { - if prev_tx.hash().reversed() == self.token_id() - && initial_token_mint_quantity.len() == 8 - && unspent.outpoint.index == 1 - { - let slp_amount = u64::from_be_bytes(initial_token_mint_quantity.try_into().unwrap()); - slp_unspents.push(SlpUnspent { - bch_unspent: unspent, - slp_amount, - }); - } else { - bch_unspents.push(unspent) - } - }, - SlpTransaction::Mint { - token_id, - additional_token_quantity, - .. - } => { - if token_id == self.token_id() && additional_token_quantity.len() == 8 { - let slp_amount = u64::from_be_bytes(additional_token_quantity.try_into().unwrap()); - slp_unspents.push(SlpUnspent { - bch_unspent: unspent, - slp_amount, - }); - } - }, - }, - Err(_) => bch_unspents.push(unspent), - } - } + ) -> UtxoRpcResult<(Vec, Vec, RecentlySpentOutPointsGuard<'_>)> { + self.platform_coin.get_token_utxos_for_spend(&self.conf.token_id).await + } - slp_unspents.sort_by(|a, b| a.slp_amount.cmp(&b.slp_amount)); - Ok((slp_unspents, bch_unspents, recently_spent)) + async fn slp_unspents_for_display(&self) -> UtxoRpcResult<(Vec, Vec)> { + self.platform_coin + .get_token_utxos_for_display(&self.conf.token_id) + .await } /// Generates the tx preimage that spends the SLP from my address to the desired destinations (script pubkeys) async fn generate_slp_tx_preimage( &self, slp_outputs: Vec, - ) -> Result, MmError> { - let (slp_unspents, bch_unspents, recently_spent) = self.slp_unspents().await?; + ) -> Result<(SlpTxPreimage, RecentlySpentOutPointsGuard<'_>), MmError> { + // the limit is 19, but we may require the change to be added + if slp_outputs.len() > 18 { + return MmError::err(GenSlpSpendErr::TooManyOutputs); + } + let (slp_unspents, bch_unspents, recently_spent) = self.slp_unspents_for_spend().await?; let total_slp_output = slp_outputs.iter().fold(0, |cur, slp_out| cur + slp_out.amount); let mut total_slp_input = 0; @@ -271,91 +353,108 @@ impl SlpToken { } total_slp_input += slp_utxo.slp_amount; - inputs.push(slp_utxo.bch_unspent); + inputs.push(slp_utxo); } if total_slp_input < total_slp_output { - return MmError::err(GenSlpSpendErr::InsufficientSlpBalance); + return MmError::err(GenSlpSpendErr::InsufficientSlpBalance { + coin: self.ticker().into(), + required: big_decimal_from_sat_unsigned(total_slp_output, self.decimals()), + available: big_decimal_from_sat_unsigned(total_slp_input, self.decimals()), + }); } let change = total_slp_input - total_slp_output; - inputs.extend(bch_unspents); - let mut amounts_for_op_return: Vec<_> = slp_outputs.iter().map(|spend_to| spend_to.amount).collect(); if change > 0 { amounts_for_op_return.push(change); } - // TODO generate the script in MM2 instead of using the external library - let op_return_out = slp_send_output( - SlpTokenType::Fungible, - &TokenId::from_slice(self.token_id().as_slice()).unwrap(), - &amounts_for_op_return, - ); - let op_return_out_mm = TransactionOutput { - value: 0, - script_pubkey: op_return_out.script.serialize().unwrap().to_vec().into(), - }; + let op_return_out_mm = self.send_op_return_output(&amounts_for_op_return); let mut outputs = vec![op_return_out_mm]; outputs.extend(slp_outputs.into_iter().map(|spend_to| TransactionOutput { - value: self.dust(), + value: self.platform_dust(), script_pubkey: spend_to.script_pubkey, })); if change > 0 { + let my_public_key = self.platform_coin.my_public_key()?; let slp_change_out = TransactionOutput { - value: self.dust(), - script_pubkey: ScriptBuilder::build_p2pkh(&self.platform_utxo.my_public_key().address_hash()) - .to_bytes(), + value: self.platform_dust(), + script_pubkey: ScriptBuilder::build_p2pkh(&my_public_key.address_hash().into()).to_bytes(), }; outputs.push(slp_change_out); } - Ok(SlpTxPreimage { - inputs, + validate_slp_utxos(self.platform_coin.bchd_urls(), &inputs, self.token_id()).await?; + let preimage = SlpTxPreimage { + slp_inputs: inputs, + available_bch_inputs: bch_unspents, outputs, + }; + Ok((preimage, recently_spent)) + } + + pub async fn send_slp_outputs(&self, slp_outputs: Vec) -> Result { + let (preimage, recently_spent) = try_tx_s!(self.generate_slp_tx_preimage(slp_outputs).await); + generate_and_send_tx( + self, + preimage.available_bch_inputs, + Some(preimage.slp_inputs.into_iter().map(|slp| slp.bch_unspent).collect()), + FeePolicy::SendExact, recently_spent, - }) + preimage.outputs, + ) + .await } async fn send_htlc( &self, + my_pub: &Public, other_pub: &Public, time_lock: u32, secret_hash: &[u8], amount: u64, - ) -> Result { - let payment_script = payment_script(time_lock, secret_hash, self.platform_utxo.my_public_key(), other_pub); - let script_pubkey = ScriptBuilder::build_p2sh(&dhash160(&payment_script)).to_bytes(); + ) -> Result { + let payment_script = payment_script(time_lock, secret_hash, my_pub, other_pub); + let script_pubkey = ScriptBuilder::build_p2sh(&dhash160(&payment_script).into()).to_bytes(); let slp_out = SlpOutput { amount, script_pubkey }; - let preimage = try_s!(self.generate_slp_tx_preimage(vec![slp_out]).await); + let (preimage, recently_spent) = try_tx_s!(self.generate_slp_tx_preimage(vec![slp_out]).await); generate_and_send_tx( - &self.platform_utxo, - preimage.inputs, - preimage.outputs, + self, + preimage.available_bch_inputs, + Some(preimage.slp_inputs.into_iter().map(|slp| slp.bch_unspent).collect()), FeePolicy::SendExact, - preimage.recently_spent, + recently_spent, + preimage.outputs, ) .await } - async fn validate_htlc( - &self, - tx: &[u8], - other_pub: &Public, - time_lock: u32, - secret_hash: &[u8], - amount: BigDecimal, - ) -> Result<(), MmError> { - let mut tx: UtxoTx = deserialize(tx).map_to_mm(ValidateHtlcError::TxParseError)?; - tx.tx_hash_algo = self.platform_utxo.as_ref().tx_hash_algo; + async fn validate_htlc(&self, input: ValidatePaymentInput) -> Result<(), MmError> { + let mut tx: UtxoTx = deserialize(input.payment_tx.as_slice()).map_to_mm(ValidateHtlcError::TxParseError)?; + tx.tx_hash_algo = self.platform_coin.as_ref().tx_hash_algo; if tx.outputs.len() < 2 { return MmError::err(ValidateHtlcError::TxLackOfOutputs); } - let slp_tx: SlpTxDetails = - deserialize(tx.outputs[0].script_pubkey.as_slice()).map_to_mm(ValidateHtlcError::OpReturnParseError)?; + let slp_satoshis = sat_from_big_decimal(&input.amount, self.decimals())?; + + let slp_unspent = SlpUnspent { + bch_unspent: UnspentInfo { + outpoint: OutPoint { + hash: tx.hash(), + index: 1, + }, + value: 0, + height: None, + }, + slp_amount: slp_satoshis, + }; + validate_slp_utxos(self.platform_coin.bchd_urls(), &[slp_unspent], self.token_id()).await?; + + let slp_tx: SlpTxDetails = parse_slp_script(tx.outputs[0].script_pubkey.as_slice())?; match slp_tx.transaction { SlpTransaction::Send { token_id, amounts } => { @@ -367,25 +466,25 @@ impl SlpToken { return MmError::err(ValidateHtlcError::InvalidSlpDetails); } - let expected = sat_from_big_decimal(&amount, self.decimals())?; - - if amounts[0] != expected { + if amounts[0] != slp_satoshis { return MmError::err(ValidateHtlcError::InvalidSlpDetails); } }, _ => return MmError::err(ValidateHtlcError::InvalidSlpDetails), } - let dust_decimal = big_decimal_from_sat_unsigned(self.dust(), self.platform_utxo.decimals()); + let htlc_keypair = self.derive_htlc_key_pair(&input.unique_swap_data); let validate_fut = utxo_common::validate_payment( - self.platform_utxo.clone(), + self.platform_coin.clone(), tx, SLP_SWAP_VOUT, - other_pub, - self.platform_utxo.my_public_key(), - secret_hash, - dust_decimal, - time_lock, + &Public::from_slice(&input.other_pub).map_to_mm(ValidateHtlcError::OtherPubInvalid)?, + htlc_keypair.public(), + &input.secret_hash, + self.platform_dust_dec(), + input.time_lock, + now_ms() / 1000 + 60, + input.confirmations, ); validate_fut @@ -402,16 +501,18 @@ impl SlpToken { other_pub: &Public, time_lock: u32, secret_hash: &[u8], + htlc_keypair: &KeyPair, ) -> Result> { let tx: UtxoTx = deserialize(htlc_tx)?; if tx.outputs.is_empty() { return MmError::err(SpendHtlcError::TxLackOfOutputs); } - let slp_tx: SlpTxDetails = deserialize(tx.outputs[0].script_pubkey.as_slice())?; + let slp_tx: SlpTxDetails = parse_slp_script(tx.outputs[0].script_pubkey.as_slice())?; let other_pub = Public::from_slice(other_pub)?; - let redeem_script = payment_script(time_lock, secret_hash, self.platform_utxo.my_public_key(), &other_pub); + let my_public_key = self.platform_coin.my_public_key()?; + let redeem_script = payment_script(time_lock, secret_hash, my_public_key, &other_pub); let slp_amount = match slp_tx.transaction { SlpTransaction::Send { token_id, amounts } => { @@ -434,10 +535,17 @@ impl SlpToken { slp_amount, }; - let tx_locktime = self.platform_utxo.p2sh_tx_locktime(time_lock).await?; + let tx_locktime = self.platform_coin.p2sh_tx_locktime(time_lock).await?; let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_1).into_script(); let tx = self - .spend_p2sh(slp_utxo, tx_locktime, SEQUENCE_FINAL - 1, script_data, redeem_script) + .spend_p2sh( + slp_utxo, + tx_locktime, + SEQUENCE_FINAL - 1, + script_data, + redeem_script, + htlc_keypair, + ) .await?; Ok(tx) } @@ -448,17 +556,13 @@ impl SlpToken { other_pub: &Public, time_lock: u32, secret: &[u8], + keypair: &KeyPair, ) -> Result> { let tx: UtxoTx = deserialize(htlc_tx)?; let slp_tx: SlpTxDetails = deserialize(tx.outputs[0].script_pubkey.as_slice())?; let other_pub = Public::from_slice(other_pub)?; - let redeem_script = payment_script( - time_lock, - &*dhash160(secret), - &other_pub, - self.platform_utxo.my_public_key(), - ); + let redeem = payment_script(time_lock, &*dhash160(secret), &other_pub, keypair.public()); let slp_amount = match slp_tx.transaction { SlpTransaction::Send { token_id, amounts } => { @@ -481,13 +585,13 @@ impl SlpToken { slp_amount, }; - let tx_locktime = self.platform_utxo.p2sh_tx_locktime(time_lock).await?; + let tx_locktime = self.platform_coin.p2sh_tx_locktime(time_lock).await?; let script_data = ScriptBuilder::default() .push_data(secret) .push_opcode(Opcode::OP_0) .into_script(); let tx = self - .spend_p2sh(slp_utxo, tx_locktime, SEQUENCE_FINAL, script_data, redeem_script) + .spend_p2sh(slp_utxo, tx_locktime, SEQUENCE_FINAL, script_data, redeem, keypair) .await?; Ok(tx) } @@ -499,49 +603,40 @@ impl SlpToken { input_sequence: u32, script_data: Script, redeem_script: Script, + htlc_keypair: &KeyPair, ) -> Result> { - let op_return = slp_send_output( - SlpTokenType::Fungible, - &TokenId::from_slice(self.token_id().as_slice()).unwrap(), - &[p2sh_utxo.slp_amount], - ); - let op_return_out_mm = TransactionOutput { - value: 0, - script_pubkey: op_return.script.serialize().unwrap().to_vec().into(), - }; + let op_return_out_mm = self.send_op_return_output(&[p2sh_utxo.slp_amount]); let mut outputs = Vec::with_capacity(3); outputs.push(op_return_out_mm); - let my_script_pubkey = ScriptBuilder::build_p2pkh(&self.platform_utxo.my_public_key().address_hash()); + let my_public_key = self.platform_coin.my_public_key()?; + let my_script_pubkey = ScriptBuilder::build_p2pkh(&my_public_key.address_hash().into()); let slp_output = TransactionOutput { - value: self.dust(), + value: self.platform_dust(), script_pubkey: my_script_pubkey.to_bytes(), }; outputs.push(slp_output); - let (_, mut bch_inputs, _recently_spent) = self.slp_unspents().await?; - bch_inputs.insert(0, p2sh_utxo.bch_unspent); - let (mut unsigned, _) = generate_transaction( - &self.platform_utxo, - bch_inputs, - outputs, - FeePolicy::SendExact, - None, - None, - ) - .await?; + let (_, bch_inputs, _recently_spent) = self.slp_unspents_for_spend().await?; + let (mut unsigned, _) = UtxoTxBuilder::new(&self.platform_coin) + .add_required_inputs(std::iter::once(p2sh_utxo.bch_unspent)) + .add_available_inputs(bch_inputs) + .add_outputs(outputs) + .build() + .await?; unsigned.lock_time = tx_locktime; unsigned.inputs[0].sequence = input_sequence; + let my_key_pair = self.platform_coin.as_ref().priv_key_policy.key_pair_or_err()?; let signed_p2sh_input = p2sh_spend( &unsigned, 0, - &self.platform_utxo.as_ref().key_pair, + htlc_keypair, script_data, redeem_script, - self.platform_utxo.as_ref().conf.signature_version, - self.platform_utxo.as_ref().conf.fork_id, + self.platform_coin.as_ref().conf.signature_version, + self.platform_coin.as_ref().conf.fork_id, )?; let signed_inputs: Result, _> = unsigned @@ -553,10 +648,10 @@ impl SlpToken { p2pkh_spend( &unsigned, i, - &self.platform_utxo.as_ref().key_pair, - &my_script_pubkey, - self.platform_utxo.as_ref().conf.signature_version, - self.platform_utxo.as_ref().conf.fork_id, + my_key_pair, + my_script_pubkey.clone(), + self.platform_coin.as_ref().conf.signature_version, + self.platform_coin.as_ref().conf.fork_id, ) }) .collect(); @@ -583,7 +678,7 @@ impl SlpToken { binding_sig: Default::default(), zcash: unsigned.zcash, str_d_zeel: unsigned.str_d_zeel, - tx_hash_algo: self.platform_utxo.as_ref().tx_hash_algo, + tx_hash_algo: self.platform_coin.as_ref().tx_hash_algo, }; let _broadcast = self @@ -606,8 +701,7 @@ impl SlpToken { return MmError::err(ValidateDexFeeError::TxLackOfOutputs); } - let slp_tx: SlpTxDetails = - deserialize(tx.outputs[0].script_pubkey.as_slice()).map_to_mm(ValidateDexFeeError::OpReturnParseError)?; + let slp_tx: SlpTxDetails = parse_slp_script(tx.outputs[0].script_pubkey.as_slice())?; match slp_tx.transaction { SlpTransaction::Send { token_id, amounts } => { @@ -628,13 +722,12 @@ impl SlpToken { _ => return MmError::err(ValidateDexFeeError::InvalidSlpDetails), } - let dust_decimal = big_decimal_from_sat_unsigned(self.dust(), self.platform_utxo.decimals()); let validate_fut = utxo_common::validate_fee( - self.platform_utxo.clone(), + self.platform_coin.clone(), tx, SLP_FEE_VOUT, expected_sender, - &dust_decimal, + &self.platform_dust_dec(), min_block_number, fee_addr, ); @@ -647,36 +740,80 @@ impl SlpToken { Ok(()) } - pub fn dust(&self) -> u64 { self.platform_utxo.as_ref().dust_amount } + pub fn platform_dust(&self) -> u64 { self.platform_coin.as_ref().dust_amount } + + pub fn platform_decimals(&self) -> u8 { self.platform_coin.as_ref().decimals } + + pub fn platform_dust_dec(&self) -> BigDecimal { + big_decimal_from_sat_unsigned(self.platform_dust(), self.platform_decimals()) + } pub fn decimals(&self) -> u8 { self.conf.decimals } pub fn token_id(&self) -> &H256 { &self.conf.token_id } + + fn platform_conf(&self) -> &UtxoCoinConf { &self.platform_coin.as_ref().conf } + + async fn my_balance_sat(&self) -> UtxoRpcResult { + let (slp_unspents, _) = self.slp_unspents_for_display().await?; + let satoshi = slp_unspents.iter().fold(0, |cur, unspent| cur + unspent.slp_amount); + Ok(satoshi) + } + + pub async fn my_coin_balance(&self) -> UtxoRpcResult { + let balance_sat = self.my_balance_sat().await?; + let spendable = big_decimal_from_sat_unsigned(balance_sat, self.decimals()); + Ok(CoinBalance { + spendable, + unspendable: 0.into(), + }) + } + + fn slp_prefix(&self) -> &CashAddrPrefix { self.platform_coin.slp_prefix() } + + pub fn get_info(&self) -> SlpTokenInfo { + SlpTokenInfo { + token_id: self.conf.token_id, + decimals: self.conf.decimals, + } + } +} + +#[derive(Debug, Eq, PartialEq)] +pub struct SlpGenesisParams { + pub(super) token_ticker: String, + token_name: String, + token_document_url: String, + token_document_hash: Vec, + pub(super) decimals: Vec, + pub(super) mint_baton_vout: Option, + pub(super) initial_token_mint_quantity: u64, } /// https://slp.dev/specs/slp-token-type-1/#transaction-detail #[derive(Debug, Eq, PartialEq)] -enum SlpTransaction { +pub enum SlpTransaction { /// https://slp.dev/specs/slp-token-type-1/#genesis-token-genesis-transaction - Genesis { - token_ticker: String, - token_name: String, - token_document_url: String, - token_document_hash: Vec, - decimals: Vec, - mint_baton_vout: Vec, - initial_token_mint_quantity: Vec, - }, + Genesis(SlpGenesisParams), /// https://slp.dev/specs/slp-token-type-1/#mint-extended-minting-transaction Mint { token_id: H256, - mint_baton_vout: Vec, - additional_token_quantity: Vec, + mint_baton_vout: Option, + additional_token_quantity: u64, }, /// https://slp.dev/specs/slp-token-type-1/#send-spend-transaction Send { token_id: H256, amounts: Vec }, } +impl SlpTransaction { + pub fn token_id(&self) -> Option { + match self { + SlpTransaction::Send { token_id, .. } | SlpTransaction::Mint { token_id, .. } => Some(*token_id), + SlpTransaction::Genesis(_) => None, + } + } +} + impl Deserializable for SlpTransaction { fn deserialize(reader: &mut Reader) -> Result where @@ -685,7 +822,7 @@ impl Deserializable for SlpTransaction { { let transaction_type: String = reader.read()?; match transaction_type.as_str() { - "GENESIS" => { + SLP_GENESIS => { let token_ticker = reader.read()?; let token_name = reader.read()?; let maybe_push_op_code: u8 = reader.read()?; @@ -708,15 +845,18 @@ impl Deserializable for SlpTransaction { let decimals = reader.read_list()?; let maybe_push_op_code: u8 = reader.read()?; let mint_baton_vout = if maybe_push_op_code == Opcode::OP_PUSHDATA1 as u8 { - reader.read_list()? + let _zero: u8 = reader.read()?; + None } else { - let mut baton = vec![0; maybe_push_op_code as usize]; - reader.read_slice(&mut baton)?; - baton + Some(reader.read()?) }; - let initial_token_mint_quantity = reader.read_list()?; + let bytes: Vec = reader.read_list()?; + if bytes.len() != 8 { + return Err(Error::Custom(format!("Expected 8 bytes, got {}", bytes.len()))); + } + let initial_token_mint_quantity = u64::from_be_bytes(bytes.try_into().expect("length is 8 bytes")); - Ok(SlpTransaction::Genesis { + Ok(SlpTransaction::Genesis(SlpGenesisParams { token_ticker, token_name, token_document_url, @@ -724,21 +864,35 @@ impl Deserializable for SlpTransaction { decimals, mint_baton_vout, initial_token_mint_quantity, - }) + })) }, - "MINT" => { + SLP_MINT => { let maybe_id: Vec = reader.read_list()?; if maybe_id.len() != 32 { return Err(Error::Custom(format!("Unexpected token id length {}", maybe_id.len()))); } + let maybe_push_op_code: u8 = reader.read()?; + let mint_baton_vout = if maybe_push_op_code == Opcode::OP_PUSHDATA1 as u8 { + let _zero: u8 = reader.read()?; + None + } else { + Some(reader.read()?) + }; + + let bytes: Vec = reader.read_list()?; + if bytes.len() != 8 { + return Err(Error::Custom(format!("Expected 8 bytes, got {}", bytes.len()))); + } + let additional_token_quantity = u64::from_be_bytes(bytes.try_into().expect("length is 8 bytes")); + Ok(SlpTransaction::Mint { token_id: H256::from(maybe_id.as_slice()), - mint_baton_vout: reader.read_list()?, - additional_token_quantity: reader.read_list()?, + mint_baton_vout, + additional_token_quantity, }) }, - "SEND" => { + SLP_SEND => { let maybe_id: Vec = reader.read_list()?; if maybe_id.len() != 32 { return Err(Error::Custom(format!("Unexpected token id length {}", maybe_id.len()))); @@ -755,6 +909,12 @@ impl Deserializable for SlpTransaction { amounts.push(amount) } + if amounts.len() > 19 { + return Err(Error::Custom(format!( + "Expected at most 19 token amounts, got {}", + amounts.len() + ))); + } Ok(SlpTransaction::Send { token_id, amounts }) }, _ => Err(Error::Custom(format!( @@ -766,16 +926,20 @@ impl Deserializable for SlpTransaction { } #[derive(Debug, Deserializable)] -struct SlpTxDetails { +pub struct SlpTxDetails { op_code: u8, lokad_id: String, - token_type: String, - transaction: SlpTransaction, + token_type: Vec, + pub transaction: SlpTransaction, } -#[derive(Debug)] -enum ParseSlpScriptError { +#[derive(Debug, Display, PartialEq)] +pub enum ParseSlpScriptError { NotOpReturn, + UnexpectedLokadId(String), + #[display(fmt = "UnexpectedTokenType: {:?}", _0)] + UnexpectedTokenType(Vec), + #[display(fmt = "DeserializeFailed: {:?}", _0)] DeserializeFailed(Error), } @@ -783,74 +947,183 @@ impl From for ParseSlpScriptError { fn from(err: Error) -> ParseSlpScriptError { ParseSlpScriptError::DeserializeFailed(err) } } -fn parse_slp_script(script: &[u8]) -> Result> { - let details: SlpTxDetails = deserialize(script).map_to_mm(ParseSlpScriptError::from)?; +pub fn parse_slp_script(script: &[u8]) -> Result> { + let details: SlpTxDetails = deserialize(script)?; if Opcode::from_u8(details.op_code) != Some(Opcode::OP_RETURN) { return MmError::err(ParseSlpScriptError::NotOpReturn); } + + if details.lokad_id != SLP_LOKAD_ID { + return MmError::err(ParseSlpScriptError::UnexpectedLokadId(details.lokad_id)); + } + + if details.token_type.first() != Some(&SLP_FUNGIBLE) { + return MmError::err(ParseSlpScriptError::UnexpectedTokenType(details.token_type)); + } + Ok(details) } #[derive(Debug, Display)] -pub enum SlpUnspentsErr { +enum GenSlpSpendErr { RpcError(UtxoRpcError), - #[display(fmt = "TxDeserializeError: {:?}", _0)] - TxDeserializeError(Error), + TooManyOutputs, + #[display( + fmt = "Not enough {} to generate SLP spend: available {}, required at least {}", + coin, + available, + required + )] + InsufficientSlpBalance { + coin: String, + available: BigDecimal, + required: BigDecimal, + }, + InvalidSlpUtxos(ValidateSlpUtxosErr), + Internal(String), +} + +impl From for GenSlpSpendErr { + fn from(err: UtxoRpcError) -> GenSlpSpendErr { GenSlpSpendErr::RpcError(err) } } -impl From for SlpUnspentsErr { - fn from(err: UtxoRpcError) -> SlpUnspentsErr { SlpUnspentsErr::RpcError(err) } +impl From for GenSlpSpendErr { + fn from(err: ValidateSlpUtxosErr) -> GenSlpSpendErr { GenSlpSpendErr::InvalidSlpUtxos(err) } } -impl From for SlpUnspentsErr { - fn from(err: Error) -> SlpUnspentsErr { SlpUnspentsErr::TxDeserializeError(err) } +impl From for GenSlpSpendErr { + fn from(e: UnexpectedDerivationMethod) -> Self { GenSlpSpendErr::Internal(e.to_string()) } } -impl From for BalanceError { - fn from(err: SlpUnspentsErr) -> BalanceError { +impl From for WithdrawError { + fn from(err: GenSlpSpendErr) -> WithdrawError { match err { - SlpUnspentsErr::RpcError(e) => BalanceError::Transport(e.to_string()), - SlpUnspentsErr::TxDeserializeError(e) => BalanceError::Internal(format!("{:?}", e)), + GenSlpSpendErr::RpcError(e) => e.into(), + GenSlpSpendErr::TooManyOutputs | GenSlpSpendErr::InvalidSlpUtxos(_) => { + WithdrawError::InternalError(err.to_string()) + }, + GenSlpSpendErr::InsufficientSlpBalance { + coin, + available, + required, + } => WithdrawError::NotSufficientBalance { + coin, + available, + required, + }, + GenSlpSpendErr::Internal(internal) => WithdrawError::InternalError(internal), } } } -#[derive(Debug, Display)] -enum GenSlpSpendErr { - GetUnspentsErr(SlpUnspentsErr), - InsufficientSlpBalance, +impl AsRef for SlpToken { + fn as_ref(&self) -> &UtxoCoinFields { self.platform_coin.as_ref() } +} + +#[async_trait] +impl UtxoTxBroadcastOps for SlpToken { + async fn broadcast_tx(&self, tx: &UtxoTx) -> Result> { + let tx_bytes = serialize(tx); + check_slp_transaction(self.platform_coin.bchd_urls(), tx_bytes.clone().take()) + .await + .mm_err(|e| BroadcastTxErr::Other(e.to_string()))?; + + let hash = self.rpc().send_raw_transaction(tx_bytes.into()).compat().await?; + + Ok(hash) + } } -impl From for GenSlpSpendErr { - fn from(err: SlpUnspentsErr) -> GenSlpSpendErr { GenSlpSpendErr::GetUnspentsErr(err) } +#[async_trait] +impl UtxoTxGenerationOps for SlpToken { + async fn get_tx_fee(&self) -> UtxoRpcResult { self.platform_coin.get_tx_fee().await } + + async fn calc_interest_if_required( + &self, + unsigned: TransactionInputSigner, + data: AdditionalTxData, + my_script_pub: Bytes, + ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { + self.platform_coin + .calc_interest_if_required(unsigned, data, my_script_pub) + .await + } } impl MarketCoinOps for SlpToken { fn ticker(&self) -> &str { &self.conf.ticker } - fn my_address(&self) -> Result { unimplemented!() } + fn my_address(&self) -> Result { + let my_address = try_s!(self.as_ref().derivation_method.iguana_or_err()); + let slp_address = try_s!(self.platform_coin.slp_address(my_address)); + slp_address.encode() + } + + fn get_public_key(&self) -> Result> { + let pubkey = utxo_common::my_public_key(self.platform_coin.as_ref())?; + Ok(pubkey.to_string()) + } + + fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { + utxo_common::sign_message_hash(self.as_ref(), message) + } + + fn sign_message(&self, message: &str) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message) + } + + fn verify_message(&self, signature: &str, message: &str, address: &str) -> VerificationResult { + let message_hash = self + .sign_message_hash(message) + .ok_or(VerificationError::PrefixNotFound)?; + let signature = CompactSignature::from(base64::decode(signature)?); + let pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; + let address_from_pubkey = self.platform_coin.address_from_pubkey(&pubkey); + let slp_address = self + .platform_coin + .slp_address(&address_from_pubkey) + .map_err(VerificationError::InternalError)? + .encode() + .map_err(VerificationError::InternalError)?; + Ok(slp_address == address) + } fn my_balance(&self) -> BalanceFut { let coin = self.clone(); - let fut = async move { - let (slp_unspents, _, _) = coin.slp_unspents().await?; - let spendable_sat = slp_unspents.iter().fold(0, |cur, unspent| cur + unspent.slp_amount); - let spendable = big_decimal_from_sat_unsigned(spendable_sat, coin.decimals()); - Ok(CoinBalance { - spendable, - unspendable: 0.into(), - }) - }; + let fut = async move { Ok(coin.my_coin_balance().await?) }; Box::new(fut.boxed().compat()) } fn base_coin_balance(&self) -> BalanceFut { - Box::new(self.platform_utxo.my_balance().map(|res| res.spendable)) + Box::new(self.platform_coin.my_balance().map(|res| res.spendable)) } + fn platform_ticker(&self) -> &str { self.platform_coin.ticker() } + /// Receives raw transaction bytes in hexadecimal format as input and returns tx hash in hexadecimal format fn send_raw_tx(&self, tx: &str) -> Box + Send> { - self.platform_utxo.send_raw_tx(tx) + let selfi = self.clone(); + let tx = tx.to_owned(); + let fut = async move { + let bytes = hex::decode(tx).map_to_mm(|e| e).map_err(|e| format!("{:?}", e))?; + let tx = try_s!(deserialize(bytes.as_slice())); + let hash = selfi.broadcast_tx(&tx).await.map_err(|e| format!("{:?}", e))?; + Ok(format!("{:?}", hash)) + }; + + Box::new(fut.boxed().compat()) + } + + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + let selfi = self.clone(); + let bytes = tx.to_owned(); + let fut = async move { + let tx = try_s!(deserialize(bytes.as_slice())); + let hash = selfi.broadcast_tx(&tx).await.map_err(|e| format!("{:?}", e))?; + Ok(format!("{:?}", hash)) + }; + + Box::new(fut.boxed().compat()) } fn wait_for_confirmations( @@ -861,7 +1134,7 @@ impl MarketCoinOps for SlpToken { wait_until: u64, check_every: u64, ) -> Box + Send> { - self.platform_utxo + self.platform_coin .wait_for_confirmations(tx, confirmations, requires_nota, wait_until, check_every) } @@ -873,7 +1146,7 @@ impl MarketCoinOps for SlpToken { _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::wait_for_output_spend( - self.platform_utxo.as_ref(), + self.platform_coin.as_ref(), transaction, SLP_SWAP_VOUT, from_block, @@ -882,34 +1155,36 @@ impl MarketCoinOps for SlpToken { } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result { - self.platform_utxo.tx_enum_from_bytes(bytes) + self.platform_coin.tx_enum_from_bytes(bytes) } - fn current_block(&self) -> Box + Send> { self.platform_utxo.current_block() } + fn current_block(&self) -> Box + Send> { self.platform_coin.current_block() } - fn display_priv_key(&self) -> String { self.platform_utxo.display_priv_key() } + fn display_priv_key(&self) -> Result { self.platform_coin.display_priv_key() } fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat_unsigned(1, self.decimals()) } fn min_trading_vol(&self) -> MmNumber { big_decimal_from_sat_unsigned(1, self.decimals()).into() } } +#[async_trait] impl SwapOps for SlpToken { - fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { let coin = self.clone(); - let fee_pubkey = try_fus!(Public::from_slice(fee_addr)); - let script_pubkey = ScriptBuilder::build_p2pkh(&fee_pubkey.address_hash()).into(); - let amount = try_fus!(sat_from_big_decimal(&amount, self.decimals())); + let fee_pubkey = try_tx_fus!(Public::from_slice(fee_addr)); + let script_pubkey = ScriptBuilder::build_p2pkh(&fee_pubkey.address_hash().into()).into(); + let amount = try_tx_fus!(sat_from_big_decimal(&amount, self.decimals())); let fut = async move { let slp_out = SlpOutput { amount, script_pubkey }; - let preimage = try_s!(coin.generate_slp_tx_preimage(vec![slp_out]).await); + let (preimage, recently_spent) = try_tx_s!(coin.generate_slp_tx_preimage(vec![slp_out]).await); generate_and_send_tx( - &coin.platform_utxo, - preimage.inputs, - preimage.outputs, + &coin, + preimage.available_bch_inputs, + Some(preimage.slp_inputs.into_iter().map(|slp| slp.bch_unspent).collect()), FeePolicy::SendExact, - preimage.recently_spent, + recently_spent, + preimage.outputs, ) .await }; @@ -923,14 +1198,19 @@ impl SwapOps for SlpToken { secret_hash: &[u8], amount: BigDecimal, _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - let taker_pub = try_fus!(Public::from_slice(taker_pub)); - let amount = try_fus!(sat_from_big_decimal(&amount, self.decimals())); + let taker_pub = try_tx_fus!(Public::from_slice(taker_pub)); + let amount = try_tx_fus!(sat_from_big_decimal(&amount, self.decimals())); let secret_hash = secret_hash.to_owned(); + let maker_htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); let coin = self.clone(); let fut = async move { - let tx = try_s!(coin.send_htlc(&taker_pub, time_lock, &secret_hash, amount).await); + let tx = try_tx_s!( + coin.send_htlc(maker_htlc_keypair.public(), &taker_pub, time_lock, &secret_hash, amount) + .await + ); Ok(tx.into()) }; Box::new(fut.boxed().compat()) @@ -943,14 +1223,20 @@ impl SwapOps for SlpToken { secret_hash: &[u8], amount: BigDecimal, _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - let maker_pub = try_fus!(Public::from_slice(maker_pub)); - let amount = try_fus!(sat_from_big_decimal(&amount, self.decimals())); + let maker_pub = try_tx_fus!(Public::from_slice(maker_pub)); + let amount = try_tx_fus!(sat_from_big_decimal(&amount, self.decimals())); let secret_hash = secret_hash.to_owned(); + let taker_htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); + let coin = self.clone(); let fut = async move { - let tx = try_s!(coin.send_htlc(&maker_pub, time_lock, &secret_hash, amount).await); + let tx = try_tx_s!( + coin.send_htlc(taker_htlc_keypair.public(), &maker_pub, time_lock, &secret_hash, amount) + .await + ); Ok(tx.into()) }; Box::new(fut.boxed().compat()) @@ -963,14 +1249,19 @@ impl SwapOps for SlpToken { taker_pub: &[u8], secret: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { let tx = taker_payment_tx.to_owned(); - let taker_pub = try_fus!(Public::from_slice(taker_pub)); + let taker_pub = try_tx_fus!(Public::from_slice(taker_pub)); let secret = secret.to_owned(); + let htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); let coin = self.clone(); let fut = async move { - let tx = try_s!(coin.spend_htlc(&tx, &taker_pub, time_lock, &secret).await); + let tx = try_tx_s!( + coin.spend_htlc(&tx, &taker_pub, time_lock, &secret, &htlc_keypair) + .await + ); Ok(tx.into()) }; Box::new(fut.boxed().compat()) @@ -983,14 +1274,19 @@ impl SwapOps for SlpToken { maker_pub: &[u8], secret: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { let tx = maker_payment_tx.to_owned(); - let maker_pub = try_fus!(Public::from_slice(maker_pub)); + let maker_pub = try_tx_fus!(Public::from_slice(maker_pub)); let secret = secret.to_owned(); + let htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); let coin = self.clone(); let fut = async move { - let tx = try_s!(coin.spend_htlc(&tx, &maker_pub, time_lock, &secret).await); + let tx = try_tx_s!( + coin.spend_htlc(&tx, &maker_pub, time_lock, &secret, &htlc_keypair) + .await + ); Ok(tx.into()) }; Box::new(fut.boxed().compat()) @@ -1003,17 +1299,22 @@ impl SwapOps for SlpToken { maker_pub: &[u8], secret_hash: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { let tx = taker_payment_tx.to_owned(); - let maker_pub = try_fus!(Public::from_slice(maker_pub)); + let maker_pub = try_tx_fus!(Public::from_slice(maker_pub)); let secret_hash = secret_hash.to_owned(); + let htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); let coin = self.clone(); let fut = async move { - let tx = try_s!(coin.refund_htlc(&tx, &maker_pub, time_lock, &secret_hash).await); + let tx = try_s!( + coin.refund_htlc(&tx, &maker_pub, time_lock, &secret_hash, &htlc_keypair) + .await + ); Ok(tx.into()) }; - Box::new(fut.boxed().compat()) + Box::new(fut.boxed().compat().map_err(TransactionErr::Plain)) } fn send_maker_refunds_payment( @@ -1023,14 +1324,19 @@ impl SwapOps for SlpToken { taker_pub: &[u8], secret_hash: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { let tx = maker_payment_tx.to_owned(); - let taker_pub = try_fus!(Public::from_slice(taker_pub)); + let taker_pub = try_tx_fus!(Public::from_slice(taker_pub)); let secret_hash = secret_hash.to_owned(); + let htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); let coin = self.clone(); let fut = async move { - let tx = try_s!(coin.refund_htlc(&tx, &taker_pub, time_lock, &secret_hash).await); + let tx = try_tx_s!( + coin.refund_htlc(&tx, &taker_pub, time_lock, &secret_hash, &htlc_keypair) + .await + ); Ok(tx.into()) }; Box::new(fut.boxed().compat()) @@ -1043,6 +1349,7 @@ impl SwapOps for SlpToken { fee_addr: &[u8], amount: &BigDecimal, min_block_number: u64, + _uuid: &[u8], ) -> Box + Send> { let tx = match fee_tx { TransactionEnum::UtxoTx(tx) => tx.clone(), @@ -1063,47 +1370,19 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - fn validate_maker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - secret_hash: &[u8], - amount: BigDecimal, - _swap_contract_address: &Option, - ) -> Box + Send> { - let maker_pub = try_fus!(Public::from_slice(maker_pub)); - let tx = payment_tx.to_owned(); - let secret_hash = secret_hash.to_owned(); + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { let coin = self.clone(); let fut = async move { - try_s!( - coin.validate_htlc(&tx, &maker_pub, time_lock, &secret_hash, amount) - .await - ); + try_s!(coin.validate_htlc(input).await); Ok(()) }; Box::new(fut.boxed().compat()) } - fn validate_taker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - secret_hash: &[u8], - amount: BigDecimal, - _swap_contract_address: &Option, - ) -> Box + Send> { - let taker_pub = try_fus!(Public::from_slice(taker_pub)); - let tx = payment_tx.to_owned(); - let secret_hash = secret_hash.to_owned(); + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { let coin = self.clone(); let fut = async move { - try_s!( - coin.validate_htlc(&tx, &taker_pub, time_lock, &secret_hash, amount) - .await - ); + try_s!(coin.validate_htlc(input).await); Ok(()) }; Box::new(fut.boxed().compat()) @@ -1116,48 +1395,29 @@ impl SwapOps for SlpToken { secret_hash: &[u8], _search_from_block: u64, _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> Box, Error = String> + Send> { - utxo_common::check_if_my_payment_sent(self.platform_utxo.clone(), time_lock, other_pub, secret_hash) - } - - fn search_for_swap_tx_spend_my( - &self, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - _swap_contract_address: &Option, - ) -> Result, String> { - utxo_common::search_for_swap_tx_spend_my( - self.platform_utxo.as_ref(), + utxo_common::check_if_my_payment_sent( + self.platform_coin.clone(), time_lock, other_pub, secret_hash, - tx, - SLP_SWAP_VOUT, - search_from_block, + swap_unique_data, ) } - fn search_for_swap_tx_spend_other( + async fn search_for_swap_tx_spend_my( &self, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - _swap_contract_address: &Option, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { - utxo_common::search_for_swap_tx_spend_other( - self.platform_utxo.as_ref(), - time_lock, - other_pub, - secret_hash, - tx, - SLP_SWAP_VOUT, - search_from_block, - ) + utxo_common::search_for_swap_tx_spend_my(&self.platform_coin, input, SLP_SWAP_VOUT).await + } + + async fn search_for_swap_tx_spend_other( + &self, + input: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + utxo_common::search_for_swap_tx_spend_other(&self.platform_coin, input, SLP_SWAP_VOUT).await } fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { @@ -1170,64 +1430,354 @@ impl SwapOps for SlpToken { ) -> Result, MmError> { Ok(None) } + + fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { + utxo_common::derive_htlc_key_pair(self.platform_coin.as_ref(), swap_unique_data) + } +} + +impl From for TradePreimageError { + fn from(slp: GenSlpSpendErr) -> TradePreimageError { + match slp { + GenSlpSpendErr::InsufficientSlpBalance { + coin, + available, + required, + } => TradePreimageError::NotSufficientBalance { + coin, + available, + required, + }, + GenSlpSpendErr::RpcError(e) => e.into(), + GenSlpSpendErr::TooManyOutputs | GenSlpSpendErr::InvalidSlpUtxos(_) => { + TradePreimageError::InternalError(slp.to_string()) + }, + GenSlpSpendErr::Internal(internal) => TradePreimageError::InternalError(internal), + } + } } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct SlpFeeDetails { + pub amount: BigDecimal, + pub coin: String, +} + +impl From for TxFeeDetails { + fn from(slp: SlpFeeDetails) -> TxFeeDetails { TxFeeDetails::Slp(slp) } +} + +#[async_trait] impl MmCoin for SlpToken { fn is_asset_chain(&self) -> bool { false } - fn withdraw(&self, _req: WithdrawRequest) -> WithdrawFut { unimplemented!() } + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { + Box::new( + utxo_common::get_raw_transaction(self.platform_coin.as_ref(), req) + .boxed() + .compat(), + ) + } + + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { + let coin = self.clone(); + let fut = async move { + let my_address = coin.platform_coin.as_ref().derivation_method.iguana_or_err()?; + let key_pair = coin.platform_coin.as_ref().priv_key_policy.key_pair_or_err()?; + + let address = CashAddress::decode(&req.to).map_to_mm(WithdrawError::InvalidAddress)?; + if address.prefix != *coin.slp_prefix() { + return MmError::err(WithdrawError::InvalidAddress(format!( + "Expected {} address prefix, not {}", + coin.slp_prefix(), + address.prefix + ))); + }; + let amount = if req.max { + coin.my_balance_sat().await? + } else { + sat_from_big_decimal(&req.amount, coin.decimals())? + }; + + if address.hash.len() != 20 { + return MmError::err(WithdrawError::InvalidAddress(format!( + "Expected 20 address hash len, not {}", + address.hash.len() + ))); + } + + // TODO clarify with community whether we should support withdrawal to SLP P2SH addresses + let script_pubkey = match address.address_type { + CashAddrType::P2PKH => { + ScriptBuilder::build_p2pkh(&AddressHashEnum::AddressHash(address.hash.as_slice().into())).to_bytes() + }, + CashAddrType::P2SH => { + return MmError::err(WithdrawError::InvalidAddress( + "Withdrawal to P2SH is not supported".into(), + )) + }, + }; + let slp_output = SlpOutput { amount, script_pubkey }; + let (slp_preimage, _) = coin.generate_slp_tx_preimage(vec![slp_output]).await?; + let mut tx_builder = UtxoTxBuilder::new(&coin.platform_coin) + .add_required_inputs(slp_preimage.slp_inputs.into_iter().map(|slp| slp.bch_unspent)) + .add_available_inputs(slp_preimage.available_bch_inputs) + .add_outputs(slp_preimage.outputs); + + let platform_decimals = coin.platform_decimals(); + match req.fee { + Some(WithdrawFee::UtxoFixed { amount }) => { + let fixed = sat_from_big_decimal(&amount, platform_decimals)?; + tx_builder = tx_builder.with_fee(ActualTxFee::FixedPerKb(fixed)) + }, + Some(WithdrawFee::UtxoPerKbyte { amount }) => { + let dynamic = sat_from_big_decimal(&amount, platform_decimals)?; + tx_builder = tx_builder.with_fee(ActualTxFee::Dynamic(dynamic)); + }, + Some(fee_policy) => { + let error = format!( + "Expected 'UtxoFixed' or 'UtxoPerKbyte' fee types, found {:?}", + fee_policy + ); + return MmError::err(WithdrawError::InvalidFeePolicy(error)); + }, + None => (), + }; + + let (unsigned, tx_data) = tx_builder.build().await.mm_err(|gen_tx_error| { + WithdrawError::from_generate_tx_error(gen_tx_error, coin.platform_ticker().into(), platform_decimals) + })?; + + let prev_script = ScriptBuilder::build_p2pkh(&my_address.hash); + let signed = sign_tx( + unsigned, + key_pair, + prev_script, + coin.platform_conf().signature_version, + coin.platform_conf().fork_id, + )?; + let fee_details = SlpFeeDetails { + amount: big_decimal_from_sat_unsigned(tx_data.fee_amount, coin.platform_decimals()), + coin: coin.platform_coin.ticker().into(), + }; + let my_address_string = coin.my_address().map_to_mm(WithdrawError::InternalError)?; + let to_address = address.encode().map_to_mm(WithdrawError::InternalError)?; + + let total_amount = big_decimal_from_sat_unsigned(amount, coin.decimals()); + let spent_by_me = total_amount.clone(); + let (received_by_me, my_balance_change) = if my_address_string == to_address { + (total_amount.clone(), 0.into()) + } else { + (0.into(), &total_amount * &BigDecimal::from(-1)) + }; + + let tx_hash: BytesJson = signed.hash().reversed().take().to_vec().into(); + let details = TransactionDetails { + tx_hex: serialize(&signed).into(), + internal_id: tx_hash.clone(), + tx_hash: tx_hash.to_tx_hash(), + from: vec![my_address_string], + to: vec![to_address], + total_amount, + spent_by_me, + received_by_me, + my_balance_change, + block_height: 0, + timestamp: now_ms() / 1000, + fee_details: Some(fee_details.into()), + coin: coin.ticker().into(), + kmd_rewards: None, + transaction_type: Default::default(), + }; + Ok(details) + }; + Box::new(fut.boxed().compat()) + } fn decimals(&self) -> u8 { self.decimals() } - fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { unimplemented!() } + fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { + utxo_common::convert_to_address(&self.platform_coin, from, to_address_format) + } - fn validate_address(&self, _address: &str) -> ValidateAddressResult { unimplemented!() } + fn validate_address(&self, address: &str) -> ValidateAddressResult { + let cash_address = match CashAddress::decode(address) { + Ok(a) => a, + Err(e) => { + return ValidateAddressResult { + is_valid: false, + reason: Some(format!("Error {} on parsing the {} as cash address", e, address)), + } + }, + }; - fn process_history_loop(&self, _ctx: MmArc) -> Box + Send> { unimplemented!() } + if cash_address.prefix == *self.slp_prefix() { + ValidateAddressResult { + is_valid: true, + reason: None, + } + } else { + ValidateAddressResult { + is_valid: false, + reason: Some(format!( + "Address {} has invalid prefix {}, expected {}", + address, + cash_address.prefix, + self.slp_prefix() + )), + } + } + } - fn history_sync_status(&self) -> HistorySyncState { unimplemented!() } + fn process_history_loop(&self, _ctx: MmArc) -> Box + Send> { + warn!("process_history_loop is not implemented for SLP yet!"); + Box::new(futures01::future::err(())) + } + + fn history_sync_status(&self) -> HistorySyncState { self.platform_coin.history_sync_status() } /// Get fee to be paid per 1 swap transaction - fn get_trade_fee(&self) -> Box + Send> { unimplemented!() } + fn get_trade_fee(&self) -> Box + Send> { + utxo_common::get_trade_fee(self.platform_coin.clone()) + } - fn get_sender_trade_fee(&self, _value: TradePreimageValue, _stage: FeeApproxStage) -> TradePreimageFut { - unimplemented!() + async fn get_sender_trade_fee( + &self, + value: TradePreimageValue, + stage: FeeApproxStage, + ) -> TradePreimageResult { + let slp_amount = match value { + TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => { + sat_from_big_decimal(&decimal, self.decimals())? + }, + }; + // can use dummy P2SH script_pubkey here + let script_pubkey = ScriptBuilder::build_p2sh(&H160::default().into()).into(); + let slp_out = SlpOutput { + amount: slp_amount, + script_pubkey, + }; + let (preimage, _) = self.generate_slp_tx_preimage(vec![slp_out]).await?; + let fee = utxo_common::preimage_trade_fee_required_to_send_outputs( + &self.platform_coin, + self.platform_ticker(), + preimage.outputs, + FeePolicy::SendExact, + None, + &stage, + ) + .await?; + Ok(TradeFee { + coin: self.platform_coin.ticker().into(), + amount: fee.into(), + paid_from_trading_vol: false, + }) } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + let coin = self.clone(); - fn get_fee_to_send_taker_fee( + let fut = async move { + let htlc_fee = coin.platform_coin.get_htlc_spend_fee(SLP_HTLC_SPEND_SIZE).await?; + let amount = + (big_decimal_from_sat_unsigned(htlc_fee, coin.platform_decimals()) + coin.platform_dust_dec()).into(); + Ok(TradeFee { + coin: coin.platform_coin.ticker().into(), + amount, + paid_from_trading_vol: false, + }) + }; + + Box::new(fut.boxed().compat()) + } + + async fn get_fee_to_send_taker_fee( &self, - _dex_fee_amount: BigDecimal, - _stage: FeeApproxStage, - ) -> TradePreimageFut { - unimplemented!() + dex_fee_amount: BigDecimal, + stage: FeeApproxStage, + ) -> TradePreimageResult { + let slp_amount = sat_from_big_decimal(&dex_fee_amount, self.decimals())?; + // can use dummy P2PKH script_pubkey here + let script_pubkey = ScriptBuilder::build_p2pkh(&H160::default().into()).into(); + let slp_out = SlpOutput { + amount: slp_amount, + script_pubkey, + }; + let (preimage, _) = self.generate_slp_tx_preimage(vec![slp_out]).await?; + let fee = utxo_common::preimage_trade_fee_required_to_send_outputs( + &self.platform_coin, + self.platform_ticker(), + preimage.outputs, + FeePolicy::SendExact, + None, + &stage, + ) + .await?; + Ok(TradeFee { + coin: self.platform_coin.ticker().into(), + amount: fee.into(), + paid_from_trading_vol: false, + }) } - fn required_confirmations(&self) -> u64 { 1 } + fn required_confirmations(&self) -> u64 { self.conf.required_confirmations.load(AtomicOrdering::Relaxed) } fn requires_notarization(&self) -> bool { false } - fn set_required_confirmations(&self, _confirmations: u64) { unimplemented!() } + fn set_required_confirmations(&self, confirmations: u64) { + self.conf + .required_confirmations + .store(confirmations, AtomicOrdering::Relaxed); + } - fn set_requires_notarization(&self, _requires_nota: bool) { unimplemented!() } + fn set_requires_notarization(&self, _requires_nota: bool) { + warn!("set_requires_notarization has no effect on SLPTOKEN!") + } fn swap_contract_address(&self) -> Option { None } - fn mature_confirmations(&self) -> Option { self.platform_utxo.mature_confirmations() } + fn mature_confirmations(&self) -> Option { self.platform_coin.mature_confirmations() } + + fn coin_protocol_info(&self) -> Vec { Vec::new() } + + fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } +} - fn coin_protocol_info(&self) -> Vec { unimplemented!() } +impl CoinWithTxHistoryV2 for SlpToken { + fn history_wallet_id(&self) -> WalletId { WalletId::new(self.platform_ticker().to_owned()) } - fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { unimplemented!() } + fn get_tx_history_filters(&self) -> GetTxHistoryFilters { + GetTxHistoryFilters::new().with_token_id(self.token_id().to_string()) + } +} + +#[derive(Debug, Display)] +pub enum SlpAddrFromPubkeyErr { + InvalidHex(hex::FromHexError), + CashAddrError(String), + EncodeError(String), +} + +impl From for SlpAddrFromPubkeyErr { + fn from(err: FromHexError) -> SlpAddrFromPubkeyErr { SlpAddrFromPubkeyErr::InvalidHex(err) } +} + +pub fn slp_addr_from_pubkey_str(pubkey: &str, prefix: &str) -> Result> { + let pubkey_bytes = hex::decode(pubkey)?; + let hash = dhash160(&pubkey_bytes); + let addr = + CashAddress::new(prefix, hash.to_vec(), CashAddrType::P2PKH).map_to_mm(SlpAddrFromPubkeyErr::CashAddrError)?; + addr.encode().map_to_mm(SlpAddrFromPubkeyErr::EncodeError) } #[cfg(test)] mod slp_tests { use super::*; - use crate::utxo::utxo_standard::utxo_standard_coin_from_conf_and_request; - use common::mm_ctx::MmCtxBuilder; - use common::privkey::key_pair_from_seed; - use common::{block_on, now_ms}; + use crate::utxo::GetUtxoListOps; + use crate::{utxo::bch::tbch_coin_for_test, TransactionErr}; + use common::block_on; + use mocktopus::mocking::{MockResult, Mockable}; + use std::mem::discriminant; // https://slp.dev/specs/slp-token-type-1/#examples #[test] @@ -1250,16 +1800,16 @@ mod slp_tests { .unwrap(); let slp_data = parse_slp_script(&script).unwrap(); assert_eq!(slp_data.lokad_id, "SLP\0"); - let initial_token_mint_quantity = 1000_0000_0000u64.to_be_bytes().to_vec(); - let expected_transaction = SlpTransaction::Genesis { + let initial_token_mint_quantity = 1000_0000_0000u64; + let expected_transaction = SlpTransaction::Genesis(SlpGenesisParams { token_ticker: "ADEX".to_string(), token_name: "ADEX".to_string(), token_document_url: "".to_string(), token_document_hash: vec![], decimals: vec![8], - mint_baton_vout: vec![], + mint_baton_vout: None, initial_token_mint_quantity, - }; + }); assert_eq!(expected_transaction, slp_data.transaction); @@ -1268,17 +1818,17 @@ mod slp_tests { hex::decode("6a04534c500001010747454e45534953045553445423546574686572204c74642e20555320646f6c6c6172206261636b656420746f6b656e734168747470733a2f2f7465746865722e746f2f77702d636f6e74656e742f75706c6f6164732f323031362f30362f546574686572576869746550617065722e70646620db4451f11eda33950670aaf59e704da90117ff7057283b032cfaec77793139160108010208002386f26fc10000").unwrap(); let slp_data = parse_slp_script(&script).unwrap(); assert_eq!(slp_data.lokad_id, "SLP\0"); - let initial_token_mint_quantity = 10000000000000000u64.to_be_bytes().to_vec(); - let expected_transaction = SlpTransaction::Genesis { + let initial_token_mint_quantity = 10000000000000000u64; + let expected_transaction = SlpTransaction::Genesis(SlpGenesisParams { token_ticker: "USDT".to_string(), token_name: "Tether Ltd. US dollar backed tokens".to_string(), token_document_url: "https://tether.to/wp-content/uploads/2016/06/TetherWhitePaper.pdf".to_string(), token_document_hash: hex::decode("db4451f11eda33950670aaf59e704da90117ff7057283b032cfaec7779313916") .unwrap(), decimals: vec![8], - mint_baton_vout: vec![2], + mint_baton_vout: Some(2), initial_token_mint_quantity, - }; + }); assert_eq!(expected_transaction, slp_data.transaction); @@ -1289,12 +1839,13 @@ mod slp_tests { assert_eq!(slp_data.lokad_id, "SLP\0"); let expected_transaction = SlpTransaction::Mint { token_id: "550d19eb820e616a54b8a73372c4420b5a0567d8dc00f613b71c5234dc884b35".into(), - mint_baton_vout: vec![2], - additional_token_quantity: hex::decode("002386f26fc10000").unwrap(), + mint_baton_vout: Some(2), + additional_token_quantity: 10000000000000000, }; assert_eq!(expected_transaction, slp_data.transaction); + // SEND with 3 outputs let script = hex::decode("6a04534c500001010453454e4420550d19eb820e616a54b8a73372c4420b5a0567d8dc00f613b71c5234dc884b350800000000000003e80800000000000003e90800000000000003ea").unwrap(); let token_id = "550d19eb820e616a54b8a73372c4420b5a0567d8dc00f613b71c5234dc884b35".into(); @@ -1305,131 +1856,286 @@ mod slp_tests { amounts: vec![1000, 1001, 1002], }; assert_eq!(expected_transaction, slp_data.transaction); + + // NFT Genesis, unsupported token type + // https://explorer.bitcoin.com/bch/tx/3dc17770ff832726aace53d305e087601d8b27cf881089d7849173736995f43e + let script = hex::decode("6a04534c500001410747454e45534953055357454443174573736b65657469742043617264204e6f2e20313136302b68747470733a2f2f636f6c6c65637469626c652e73776565742e696f2f7365726965732f35382f313136302040f8d39b6fc8725d9c766d66643d8ec644363ba32391c1d9a89a3edbdea8866a01004c00080000000000000001").unwrap(); + + let actual_err = parse_slp_script(&script).unwrap_err().into_inner(); + let expected_err = ParseSlpScriptError::UnexpectedTokenType(vec![0x41]); + assert_eq!(expected_err, actual_err); } #[test] - #[ignore] - fn send_and_spend_htlc_on_testnet() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let keypair = key_pair_from_seed("BCH SLP test").unwrap(); - - let conf = json!({"coin":"BCH","pubtype":0,"p2shtype":5,"mm2":1,"fork_id":"0x40","protocol":{"type":"UTXO"}, - "address_format":{"format":"cashaddress","network":"bchtest"}}); - let req = json!({ - "method": "electrum", - "coin": "BCH", - "servers": [{"url":"blackie.c3-soft.com:60001"},{"url":"testnet.imaginary.cash:50001"}], - }); - let bch = block_on(utxo_standard_coin_from_conf_and_request( - &ctx, - "BCH", - &conf, - &req, - &*keypair.private().secret, - )) - .unwrap(); + fn test_slp_send_output() { + // Send single output + let expected_script = hex::decode("6a04534c500001010453454e4420e73b2b28c14db8ebbf97749988b539508990e1708021067f206f49d55807dbf4080000000005f5e100").unwrap(); + let expected_output = TransactionOutput { + value: 0, + script_pubkey: expected_script.into(), + }; - let balance = bch.my_balance().wait().unwrap(); - println!("{}", balance.spendable); + let actual_output = slp_send_output( + &"e73b2b28c14db8ebbf97749988b539508990e1708021067f206f49d55807dbf4".into(), + &[100000000], + ); - let address = bch.my_address().unwrap(); - println!("{}", address); + assert_eq!(expected_output, actual_output); - let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); - let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch, 0); + let expected_script = hex::decode("6a04534c500001010453454e4420550d19eb820e616a54b8a73372c4420b5a0567d8dc00f613b71c5234dc884b350800005af3107a40000800232bff5f46c000").unwrap(); + let expected_output = TransactionOutput { + value: 0, + script_pubkey: expected_script.into(), + }; - let fusd_balance = fusd.my_balance().wait().unwrap(); - println!("FUSD {}", fusd_balance.spendable); + let actual_output = slp_send_output( + &"550d19eb820e616a54b8a73372c4420b5a0567d8dc00f613b71c5234dc884b35".into(), + &[100000000000000, 9900000000000000], + ); - let secret = [0; 32]; - let secret_hash = dhash160(&secret); - let time_lock = (now_ms() / 1000) as u32; - let amount: BigDecimal = "0.1".parse().unwrap(); + assert_eq!(expected_output, actual_output); + } - let tx = fusd - .send_taker_payment(time_lock, &*keypair.public(), &*secret_hash, amount.clone(), &None) - .wait() - .unwrap(); - println!("{}", hex::encode(tx.tx_hex())); + #[test] + fn test_slp_genesis_output() { + let expected_script = + hex::decode("6a04534c500001010747454e45534953044144455804414445584c004c0001084c0008000000174876e800") + .unwrap(); + let expected_output = TransactionOutput { + value: 0, + script_pubkey: expected_script.into(), + }; - fusd.validate_taker_payment( - &tx.tx_hex(), - time_lock, - &*keypair.public(), - &*secret_hash, - amount, - &None, - ) - .wait() - .unwrap(); + let actual_output = slp_genesis_output("ADEX", "ADEX", None, None, 8, None, 1000_0000_0000); + assert_eq!(expected_output, actual_output); - let spending_tx = fusd - .send_maker_spends_taker_payment(&tx.tx_hex(), time_lock, &*keypair.public(), &secret, &None) - .wait() - .unwrap(); - println!("spend hex {}", hex::encode(spending_tx.tx_hex())); - println!("spend hash {}", hex::encode(spending_tx.tx_hash().0)); + let expected_script = + hex::decode("6a04534c500001010747454e45534953045553445423546574686572204c74642e20555320646f6c6c6172206261636b656420746f6b656e734168747470733a2f2f7465746865722e746f2f77702d636f6e74656e742f75706c6f6164732f323031362f30362f546574686572576869746550617065722e70646620db4451f11eda33950670aaf59e704da90117ff7057283b032cfaec77793139160108010208002386f26fc10000") + .unwrap(); + let expected_output = TransactionOutput { + value: 0, + script_pubkey: expected_script.into(), + }; - let wait_for_spend = fusd - .wait_for_tx_spend(&tx.tx_hex(), (now_ms() / 1000) + 60, 0, &None) - .wait() - .unwrap(); - println!("spend hex {}", hex::encode(wait_for_spend.tx_hex())); - println!("spend hash {}", hex::encode(wait_for_spend.tx_hash().0)); + let actual_output = slp_genesis_output( + "USDT", + "Tether Ltd. US dollar backed tokens", + Some("https://tether.to/wp-content/uploads/2016/06/TetherWhitePaper.pdf"), + Some("db4451f11eda33950670aaf59e704da90117ff7057283b032cfaec7779313916".into()), + 8, + Some(2), + 10000000000000000, + ); + assert_eq!(expected_output, actual_output); + } - let secret = fusd.extract_secret(&*secret_hash, &wait_for_spend.tx_hex()).unwrap(); - println!("{:?}", secret); + #[test] + fn test_slp_address() { + let bch = tbch_coin_for_test(); + let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); + let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch, 0); + + let slp_address = fusd.my_address().unwrap(); + assert_eq!("slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8", slp_address); } #[test] - #[ignore] - fn send_and_refund_htlc_on_testnet() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let keypair = key_pair_from_seed("BCH SLP test").unwrap(); - - let conf = json!({"coin":"BCH","pubtype":0,"p2shtype":5,"mm2":1,"fork_id":"0x40","protocol":{"type":"UTXO"}, - "address_format":{"format":"cashaddress","network":"bchtest"}}); - let req = json!({ - "method": "electrum", - "coin": "BCH", - "servers": [{"url":"blackie.c3-soft.com:60001"},{"url":"testnet.imaginary.cash:50001"}], + fn test_validate_htlc_valid() { + let bch = tbch_coin_for_test(); + let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); + let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch, 0); + + // https://testnet.simpleledger.info/tx/e935160bfb5b45007a0fc6f8fbe8da618f28df6573731f1ffb54d9560abb49b2 + let payment_tx = hex::decode("0100000002736cf584f877ec7b6b95974bc461a9cfb9f126655b5d335471683154cc6cf4c5020000006a47304402206be99fe56a98e7a8c2ffe6f2d05c5c1f46a6577064b84d27d45fe0e959f6e77402201c512629313b48cd4df873222aa49046ae9a3a6e34e359d10d4308cb40438fba4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff736cf584f877ec7b6b95974bc461a9cfb9f126655b5d335471683154cc6cf4c5030000006a473044022020d774d045bbe3dce5b04af836f6a5629c6c4ce75b0b5ba8a1da0ae9a4ecc0530220522f86d20c9e4142e40f9a9c8d25db16fde91d4a0ad6f6ff2107e201386131b64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000001f3ee80300000000000017a914b0ca1fea17cf522c7e858416093fc6d95e55824087e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88accf614801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8c83d460").unwrap(); + + let other_pub = hex::decode("036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202c").unwrap(); + + utxo_common::validate_payment::.mock_safe(|coin, tx, out_i, pub0, _, h, a, lock, spv, conf| { + // replace the second public because payment was sent with privkey that is currently unknown + let my_pub = hex::decode("03c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3ed").unwrap(); + let my_pub = Box::leak(Box::new(Public::from_slice(&my_pub).unwrap())); + MockResult::Continue((coin, tx, out_i, pub0, my_pub, h, a, lock, spv, conf)) }); - let bch = block_on(utxo_standard_coin_from_conf_and_request( - &ctx, - "BCH", - &conf, - &req, - &*keypair.private().secret, + + let lock_time = 1624547837; + let secret_hash = hex::decode("5d9e149ad9ccb20e9f931a69b605df2ffde60242").unwrap(); + let amount: BigDecimal = "0.1".parse().unwrap(); + let input = ValidatePaymentInput { + payment_tx, + other_pub, + time_lock: lock_time, + secret_hash, + amount, + confirmations: 1, + try_spv_proof_until: now_ms() / 1000 + 60, + unique_swap_data: Vec::new(), + swap_contract_address: None, + }; + block_on(fusd.validate_htlc(input)).unwrap(); + } + + #[test] + fn construct_and_send_invalid_slp_htlc_should_fail() { + let bch = tbch_coin_for_test(); + let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); + let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch.clone(), 0); + + let bch_address = bch.as_ref().derivation_method.unwrap_iguana(); + let (unspents, recently_spent) = block_on(bch.get_unspent_ordered_list(bch_address)).unwrap(); + + let secret_hash = hex::decode("5d9e149ad9ccb20e9f931a69b605df2ffde60242").unwrap(); + let other_pub = hex::decode("036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202c").unwrap(); + let other_pub = Public::from_slice(&other_pub).unwrap(); + + let my_public_key = bch.my_public_key().unwrap(); + let htlc_script = payment_script(1624547837, &secret_hash, &other_pub, my_public_key); + + let slp_send_op_return_out = slp_send_output(&token_id, &[1000]); + + let invalid_slp_send_out = TransactionOutput { + value: 1000, + script_pubkey: ScriptBuilder::build_p2sh(&dhash160(&htlc_script).into()).into(), + }; + + let tx_err = block_on(generate_and_send_tx( + &fusd, + unspents, + None, + FeePolicy::SendExact, + recently_spent, + vec![slp_send_op_return_out, invalid_slp_send_out], )) - .unwrap(); + .unwrap_err(); + + let err = match tx_err.clone() { + TransactionErr::TxRecoverable(_tx, err) => err, + TransactionErr::Plain(err) => err, + }; - let balance = bch.my_balance().wait().unwrap(); - println!("{}", balance.spendable); + println!("{:?}", err); + assert!(err.contains("is not valid with reason outputs greater than inputs")); + + // this is invalid tx bytes generated by one of this test runs, ensure that FUSD won't broadcast it using + // different methods + let tx_bytes: &[u8] = &[ + 1, 0, 0, 0, 1, 105, 91, 221, 196, 250, 138, 113, 118, 165, 149, 181, 70, 15, 224, 124, 67, 133, 237, 31, + 88, 125, 178, 69, 166, 27, 211, 32, 54, 1, 238, 134, 102, 2, 0, 0, 0, 106, 71, 48, 68, 2, 32, 103, 105, + 238, 187, 198, 194, 7, 162, 250, 17, 240, 45, 93, 168, 223, 35, 92, 23, 84, 70, 193, 234, 183, 130, 114, + 49, 198, 118, 69, 22, 128, 118, 2, 32, 127, 44, 73, 98, 217, 254, 44, 181, 87, 175, 114, 138, 223, 173, + 201, 168, 38, 198, 49, 23, 9, 101, 50, 154, 55, 236, 126, 253, 37, 114, 111, 218, 65, 33, 3, 104, 121, 223, + 35, 6, 99, 219, 76, 208, 131, 200, 238, 176, 242, 147, 244, 106, 188, 70, 10, 211, 194, 153, 176, 8, 155, + 114, 230, 212, 114, 32, 44, 255, 255, 255, 255, 3, 0, 0, 0, 0, 0, 0, 0, 0, 55, 106, 4, 83, 76, 80, 0, 1, 1, + 4, 83, 69, 78, 68, 32, 187, 48, 158, 72, 147, 6, 113, 88, 43, 234, 80, 143, 154, 29, 155, 73, 30, 73, 182, + 155, 227, 214, 243, 114, 220, 8, 218, 42, 198, 233, 14, 183, 8, 0, 0, 0, 0, 0, 0, 3, 232, 232, 3, 0, 0, 0, + 0, 0, 0, 23, 169, 20, 149, 59, 57, 9, 255, 106, 162, 105, 248, 93, 163, 76, 19, 42, 146, 66, 68, 64, 225, + 142, 135, 205, 228, 173, 0, 0, 0, 0, 0, 25, 118, 169, 20, 140, 255, 252, 36, 9, 208, 99, 67, 125, 106, 168, + 183, 90, 0, 155, 155, 165, 27, 113, 252, 136, 172, 216, 36, 92, 97, + ]; + + let tx_bytes_str = hex::encode(tx_bytes); + let err = fusd.send_raw_tx(&tx_bytes_str).wait().unwrap_err(); + println!("{:?}", err); + assert!(err.contains("is not valid with reason outputs greater than inputs")); + + let err2 = fusd.send_raw_tx_bytes(tx_bytes).wait().unwrap_err(); + println!("{:?}", err2); + assert!(err2.contains("is not valid with reason outputs greater than inputs")); + assert_eq!(err, err2); + + let utxo_tx: UtxoTx = deserialize(tx_bytes).unwrap(); + let err = block_on(fusd.broadcast_tx(&utxo_tx)).unwrap_err(); + match err.into_inner() { + BroadcastTxErr::Other(err) => assert!(err.contains("is not valid with reason outputs greater than inputs")), + e @ _ => panic!("Unexpected err {:?}", e), + }; - let address = bch.my_address().unwrap(); - println!("{}", address); + // The error variant should equal to `TxRecoverable` + assert_eq!( + discriminant(&tx_err), + discriminant(&TransactionErr::TxRecoverable( + TransactionEnum::from(utxo_tx), + String::new() + )) + ); + } + #[test] + fn test_validate_htlc_invalid_slp_utxo() { + let bch = tbch_coin_for_test(); let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); - let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch, 0); + let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch.clone(), 0); - let fusd_balance = fusd.my_balance().wait().unwrap(); - println!("FUSD {}", fusd_balance.spendable); + // https://www.blockchain.com/ru/bch-testnet/tx/6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69 + let payment_tx = hex::decode("0100000001ce59a734f33811afcc00c19dcb12202ed00067a50efed80424fabd2b723678c0020000006b483045022100ec1fecff9c60fb7e821b9a412bd8c4ce4a757c68287f9cf9e0f461165492d6530220222f020dd05d65ba35cddd0116c99255612ec90d63019bb1cea45e2cf09a62a94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914953b3909ff6aa269f85da34c132a92424440e18e879decad00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acd1215c61").unwrap(); - let secret = [0; 32]; - let secret_hash = dhash160(&secret); - let time_lock = (now_ms() / 1000) as u32 - 7200; + let other_pub_bytes = + hex::decode("036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202c").unwrap(); + let other_pub = Public::from_slice(&other_pub_bytes).unwrap(); - let tx = fusd - .send_taker_payment(time_lock, &[1; 33], &*secret_hash, 1.into(), &None) - .wait() - .unwrap(); - println!("{}", hex::encode(tx.tx_hex())); + let lock_time = 1624547837; + let secret_hash = hex::decode("5d9e149ad9ccb20e9f931a69b605df2ffde60242").unwrap(); + let amount: BigDecimal = "0.1".parse().unwrap(); + let my_pub = bch.my_public_key().unwrap(); + + // standard BCH validation should pass as the output itself is correct + utxo_common::validate_payment( + bch.clone(), + deserialize(payment_tx.as_slice()).unwrap(), + SLP_SWAP_VOUT, + my_pub, + &other_pub, + &secret_hash, + fusd.platform_dust_dec(), + lock_time, + now_ms() / 1000 + 60, + 1, + ) + .wait() + .unwrap(); + + let input = ValidatePaymentInput { + payment_tx, + other_pub: other_pub_bytes, + time_lock: lock_time, + secret_hash, + amount, + swap_contract_address: None, + try_spv_proof_until: now_ms() / 1000 + 60, + confirmations: 1, + unique_swap_data: Vec::new(), + }; + let validity_err = block_on(fusd.validate_htlc(input)).unwrap_err(); + match validity_err.into_inner() { + ValidateHtlcError::InvalidSlpUtxo(e) => println!("{:?}", e), + err @ _ => panic!("Unexpected err {:?}", err), + }; + } - let refund_tx = fusd - .send_taker_refunds_payment(&tx.tx_hex(), time_lock, &[1; 33], &*secret_hash, &None) - .wait() + #[test] + fn test_sign_message() { + let bch = tbch_coin_for_test(); + let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); + let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch, 0); + let signature = fusd.sign_message("test").unwrap(); + assert_eq!( + signature, + "ILuePKMsycXwJiNDOT7Zb7TfIlUW7Iq+5ylKd15AK72vGVYXbnf7Gj9Lk9MFV+6Ub955j7MiAkp0wQjvuIoRPPA=" + ); + } + + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn test_verify_message() { + let bch = tbch_coin_for_test(); + let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); + let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch, 0); + let is_valid = fusd + .verify_message( + "ILuePKMsycXwJiNDOT7Zb7TfIlUW7Iq+5ylKd15AK72vGVYXbnf7Gj9Lk9MFV+6Ub955j7MiAkp0wQjvuIoRPPA=", + "test", + "slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8", + ) .unwrap(); - println!("refund hex {}", hex::encode(refund_tx.tx_hex())); - println!("refund hash {}", hex::encode(refund_tx.tx_hash().0)); + assert!(is_valid); } } diff --git a/mm2src/coins/utxo/tx_cache.rs b/mm2src/coins/utxo/tx_cache.rs deleted file mode 100644 index 83d7bc9521..0000000000 --- a/mm2src/coins/utxo/tx_cache.rs +++ /dev/null @@ -1,44 +0,0 @@ -use common::safe_slurp; -use futures::lock::Mutex as AsyncMutex; -use rpc::v1::types::{Transaction as RpcTransaction, H256 as H256Json}; -use std::path::{Path, PathBuf}; - -lazy_static! { - static ref TX_CACHE_LOCK: AsyncMutex<()> = AsyncMutex::new(()); -} - -/// Try load transaction from cache. -/// Note: tx.confirmations can be out-of-date. -pub async fn load_transaction_from_cache( - tx_cache_path: &Path, - txid: &H256Json, -) -> Result, String> { - let _lock = TX_CACHE_LOCK.lock().await; - - let path = cached_transaction_path(tx_cache_path, txid); - let data = try_s!(safe_slurp(&path)); - if data.is_empty() { - // couldn't find corresponding file - return Ok(None); - } - - let data = try_s!(String::from_utf8(data)); - serde_json::from_str(&data).map(Some).map_err(|e| ERRL!("{}", e)) -} - -/// Upload transaction to cache. -pub async fn cache_transaction(tx_cache_path: &Path, tx: &RpcTransaction) -> Result<(), String> { - let _lock = TX_CACHE_LOCK.lock().await; - let path = cached_transaction_path(tx_cache_path, &tx.txid); - let tmp_path = format!("{}.tmp", path.display()); - - let content = try_s!(serde_json::to_string(tx)); - - try_s!(std::fs::write(&tmp_path, content)); - try_s!(std::fs::rename(tmp_path, path)); - Ok(()) -} - -fn cached_transaction_path(tx_cache_path: &Path, txid: &H256Json) -> PathBuf { - tx_cache_path.join(format!("{:?}", txid)) -} diff --git a/mm2src/coins/utxo/tx_cache/dummy_tx_cache.rs b/mm2src/coins/utxo/tx_cache/dummy_tx_cache.rs new file mode 100644 index 0000000000..e18244ca39 --- /dev/null +++ b/mm2src/coins/utxo/tx_cache/dummy_tx_cache.rs @@ -0,0 +1,20 @@ +use crate::utxo::tx_cache::{TxCacheResult, UtxoVerboseCacheOps}; +use async_trait::async_trait; +use rpc::v1::types::{Transaction as RpcTransaction, H256 as H256Json}; +use std::collections::{HashMap, HashSet}; + +/// The dummy TX cache. +#[derive(Debug, Default)] +pub struct DummyVerboseCache; + +#[async_trait] +impl UtxoVerboseCacheOps for DummyVerboseCache { + async fn load_transactions_from_cache_concurrently( + &self, + tx_ids: HashSet, + ) -> HashMap>> { + tx_ids.into_iter().map(|txid| (txid, Ok(None))).collect() + } + + async fn cache_transactions_concurrently(&self, _txs: &HashMap) {} +} diff --git a/mm2src/coins/utxo/tx_cache/fs_tx_cache.rs b/mm2src/coins/utxo/tx_cache/fs_tx_cache.rs new file mode 100644 index 0000000000..9327bd5163 --- /dev/null +++ b/mm2src/coins/utxo/tx_cache/fs_tx_cache.rs @@ -0,0 +1,111 @@ +use crate::utxo::tx_cache::{TxCacheError, TxCacheResult, UtxoVerboseCacheOps}; +use async_trait::async_trait; +use common::log::LogOnError; +use futures::lock::Mutex as AsyncMutex; +use futures::FutureExt; +use mm2_err_handle::prelude::*; +use mm2_io::fs::{read_json, write_json, FsJsonError}; +use parking_lot::Mutex as PaMutex; +use rpc::v1::types::{Transaction as RpcTransaction, H256 as H256Json}; +use std::collections::hash_map::RawEntryMut; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use std::sync::Arc; + +lazy_static! { + static ref TX_CACHE_LOCK: TxCacheLock = TxCacheLock::default(); +} + +impl From for TxCacheError { + fn from(e: FsJsonError) -> Self { + match e { + FsJsonError::IoReading(loading) => TxCacheError::ErrorLoading(loading.to_string()), + FsJsonError::IoWriting(writing) => TxCacheError::ErrorSaving(writing.to_string()), + FsJsonError::Serializing(ser) => TxCacheError::ErrorSerializing(ser.to_string()), + FsJsonError::Deserializing(de) => TxCacheError::ErrorDeserializing(de.to_string()), + } + } +} + +/// The cache lock is used to avoid reading and writing the same files at the same time. +#[derive(Default)] +struct TxCacheLock { + /// The collection of `Ticker -> Mutex` pairs. + mutexes: PaMutex>>>, +} + +impl TxCacheLock { + /// Get the mutex corresponding to the specified `ticker`. + pub fn mutex_by_ticker(&self, ticker: &str) -> Arc> { + let mut locks = self.mutexes.lock(); + + match locks.raw_entry_mut().from_key(ticker) { + RawEntryMut::Occupied(mutex) => mutex.get().clone(), + RawEntryMut::Vacant(vacant_mutex) => { + let (_key, mutex) = vacant_mutex.insert(ticker.to_owned(), Arc::new(AsyncMutex::new(()))); + mutex.clone() + }, + } + } +} + +/// The cache instance that assigned to a specified coin. +/// +/// Please note [`UtxoVerboseCache::ticker`] may not equal to [`Coin::ticker`]. +/// In particular, `QRC20` tokens have the same transactions as `Qtum` coin, +/// so [`Qrc20Coin::platform_ticker`] is used as [`UtxoVerboseCache::ticker`]. +#[derive(Debug)] +pub struct FsVerboseCache { + ticker: String, + tx_cache_path: PathBuf, +} + +#[async_trait] +impl UtxoVerboseCacheOps for FsVerboseCache { + async fn load_transactions_from_cache_concurrently( + &self, + tx_ids: HashSet, + ) -> HashMap>> { + let mutex = TX_CACHE_LOCK.mutex_by_ticker(&self.ticker); + let _lock = mutex.lock().await; + + let it = tx_ids + .into_iter() + .map(|txid| self.load_transaction_from_cache(txid).map(move |res| (txid, res))); + futures::future::join_all(it).await.into_iter().collect() + } + + async fn cache_transactions_concurrently(&self, txs: &HashMap) { + let mutex = TX_CACHE_LOCK.mutex_by_ticker(&self.ticker); + let _lock = mutex.lock().await; + + let it = txs.iter().map(|(_txid, tx)| self.cache_transaction(tx)); + futures::future::join_all(it) + .await + .into_iter() + .for_each(|tx| tx.error_log()); + } +} + +impl FsVerboseCache { + #[inline] + pub fn new(ticker: String, tx_cache_path: PathBuf) -> FsVerboseCache { FsVerboseCache { ticker, tx_cache_path } } + + /// Tries to load transaction from cache. + /// Note: `tx.confirmations` can be out-of-date. + async fn load_transaction_from_cache(&self, txid: H256Json) -> TxCacheResult> { + let path = self.cached_transaction_path(&txid); + read_json(&path).await.mm_err(TxCacheError::from) + } + + /// Uploads transaction to cache. + async fn cache_transaction(&self, tx: &RpcTransaction) -> TxCacheResult<()> { + const USE_TMP_FILE: bool = true; + + let path = self.cached_transaction_path(&tx.txid); + write_json(tx, &path, USE_TMP_FILE).await.mm_err(TxCacheError::from) + } + + #[inline] + fn cached_transaction_path(&self, txid: &H256Json) -> PathBuf { self.tx_cache_path.join(format!("{:?}", txid)) } +} diff --git a/mm2src/coins/utxo/tx_cache/mod.rs b/mm2src/coins/utxo/tx_cache/mod.rs new file mode 100644 index 0000000000..32d6e67ee7 --- /dev/null +++ b/mm2src/coins/utxo/tx_cache/mod.rs @@ -0,0 +1,47 @@ +use async_trait::async_trait; +use derive_more::Display; +use mm2_err_handle::prelude::*; +use rpc::v1::types::{Transaction as RpcTransaction, H256 as H256Json}; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::sync::Arc; + +pub mod dummy_tx_cache; +#[cfg(not(target_arch = "wasm32"))] pub mod fs_tx_cache; + +#[cfg(target_arch = "wasm32")] +pub mod wasm_tx_cache { + pub type WasmVerboseCache = crate::utxo::tx_cache::dummy_tx_cache::DummyVerboseCache; +} + +pub type TxCacheResult = MmResult; +pub type UtxoVerboseCacheShared = Arc; + +#[derive(Debug, Display)] +pub enum TxCacheError { + ErrorLoading(String), + ErrorSaving(String), + ErrorDeserializing(String), + ErrorSerializing(String), +} + +#[async_trait] +pub trait UtxoVerboseCacheOps: fmt::Debug { + #[inline] + fn into_shared(self) -> UtxoVerboseCacheShared + where + Self: Sized + Send + Sync + 'static, + { + Arc::new(self) + } + + /// Tries to load transactions from cache concurrently. + /// Please note `tx.confirmations` can be out-of-date. + async fn load_transactions_from_cache_concurrently( + &self, + tx_ids: HashSet, + ) -> HashMap>>; + + /// Uploads transactions to cache concurrently. + async fn cache_transactions_concurrently(&self, txs: &HashMap); +} diff --git a/mm2src/coins/utxo/utxo_block_header_storage.rs b/mm2src/coins/utxo/utxo_block_header_storage.rs new file mode 100644 index 0000000000..925a0c80c3 --- /dev/null +++ b/mm2src/coins/utxo/utxo_block_header_storage.rs @@ -0,0 +1,151 @@ +use crate::utxo::rpc_clients::ElectrumBlockHeader; +#[cfg(target_arch = "wasm32")] +use crate::utxo::utxo_indexedb_block_header_storage::IndexedDBBlockHeadersStorage; +#[cfg(not(target_arch = "wasm32"))] +use crate::utxo::utxo_sql_block_header_storage::SqliteBlockHeadersStorage; +use crate::utxo::UtxoBlockHeaderVerificationParams; +use async_trait::async_trait; +use chain::BlockHeader; +use derive_more::Display; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; + +#[derive(Debug, Display)] +pub enum BlockHeaderStorageError { + #[display(fmt = "Can't add to the storage for {} - reason: {}", ticker, reason)] + AddToStorageError { ticker: String, reason: String }, + #[display(fmt = "Can't get from the storage for {} - reason: {}", ticker, reason)] + GetFromStorageError { ticker: String, reason: String }, + #[display( + fmt = "Can't retrieve the table from the storage for {} - reason: {}", + ticker, + reason + )] + CantRetrieveTableError { ticker: String, reason: String }, + #[display(fmt = "Can't query from the storage - query: {} - reason: {}", query, reason)] + QueryError { query: String, reason: String }, + #[display(fmt = "Can't init from the storage - ticker: {} - reason: {}", ticker, reason)] + InitializationError { ticker: String, reason: String }, + #[display(fmt = "Can't decode/deserialize from storage for {} - reason: {}", ticker, reason)] + DecodeError { ticker: String, reason: String }, +} + +pub struct BlockHeaderStorage { + pub inner: Box, + pub params: UtxoBlockHeaderVerificationParams, +} + +impl Debug for BlockHeaderStorage { + fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result { Ok(()) } +} + +pub trait InitBlockHeaderStorageOps: Send + Sync + 'static { + fn new_from_ctx(ctx: MmArc, params: UtxoBlockHeaderVerificationParams) -> Option + where + Self: Sized; +} + +#[async_trait] +pub trait BlockHeaderStorageOps: Send + Sync + 'static { + /// Initializes collection/tables in storage for a specified coin + async fn init(&self, for_coin: &str) -> Result<(), MmError>; + + async fn is_initialized_for(&self, for_coin: &str) -> Result>; + + // Adds multiple block headers to the selected coin's header storage + // Should store it as `TICKER_HEIGHT=hex_string` + // use this function for headers that comes from `blockchain_headers_subscribe` + async fn add_electrum_block_headers_to_storage( + &self, + for_coin: &str, + headers: Vec, + ) -> Result<(), MmError>; + + // Adds multiple block headers to the selected coin's header storage + // Should store it as `TICKER_HEIGHT=hex_string` + // use this function for headers that comes from `blockchain_block_headers` + async fn add_block_headers_to_storage( + &self, + for_coin: &str, + headers: HashMap, + ) -> Result<(), MmError>; + + /// Gets the block header by height from the selected coin's storage as BlockHeader + async fn get_block_header( + &self, + for_coin: &str, + height: u64, + ) -> Result, MmError>; + + /// Gets the block header by height from the selected coin's storage as hex + async fn get_block_header_raw( + &self, + for_coin: &str, + height: u64, + ) -> Result, MmError>; +} + +impl InitBlockHeaderStorageOps for BlockHeaderStorage { + #[cfg(not(target_arch = "wasm32"))] + fn new_from_ctx(ctx: MmArc, params: UtxoBlockHeaderVerificationParams) -> Option { + ctx.sqlite_connection.as_option().map(|connection| BlockHeaderStorage { + inner: Box::new(SqliteBlockHeadersStorage(connection.clone())), + params, + }) + } + + #[cfg(target_arch = "wasm32")] + fn new_from_ctx(_ctx: MmArc, params: UtxoBlockHeaderVerificationParams) -> Option { + Some(BlockHeaderStorage { + inner: Box::new(IndexedDBBlockHeadersStorage {}), + params, + }) + } +} + +#[async_trait] +impl BlockHeaderStorageOps for BlockHeaderStorage { + async fn init(&self, for_coin: &str) -> Result<(), MmError> { + self.inner.init(for_coin).await + } + + async fn is_initialized_for(&self, for_coin: &str) -> Result> { + self.inner.is_initialized_for(for_coin).await + } + + async fn add_electrum_block_headers_to_storage( + &self, + for_coin: &str, + headers: Vec, + ) -> Result<(), MmError> { + self.inner + .add_electrum_block_headers_to_storage(for_coin, headers) + .await + } + + async fn add_block_headers_to_storage( + &self, + for_coin: &str, + headers: HashMap, + ) -> Result<(), MmError> { + self.inner.add_block_headers_to_storage(for_coin, headers).await + } + + async fn get_block_header( + &self, + for_coin: &str, + height: u64, + ) -> Result, MmError> { + self.inner.get_block_header(for_coin, height).await + } + + async fn get_block_header_raw( + &self, + for_coin: &str, + height: u64, + ) -> Result, MmError> { + self.inner.get_block_header_raw(for_coin, height).await + } +} diff --git a/mm2src/coins/utxo/utxo_builder/mod.rs b/mm2src/coins/utxo/utxo_builder/mod.rs new file mode 100644 index 0000000000..9c1cf135d0 --- /dev/null +++ b/mm2src/coins/utxo/utxo_builder/mod.rs @@ -0,0 +1,9 @@ +mod utxo_arc_builder; +mod utxo_coin_builder; +mod utxo_conf_builder; + +pub use utxo_arc_builder::{MergeUtxoArcOps, UtxoArcBuilder}; +pub use utxo_coin_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, + UtxoCoinWithIguanaPrivKeyBuilder, UtxoFieldsWithHardwareWalletBuilder, + UtxoFieldsWithIguanaPrivKeyBuilder}; +pub use utxo_conf_builder::{UtxoConfBuilder, UtxoConfError, UtxoConfResult}; diff --git a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs new file mode 100644 index 0000000000..d4964b12c4 --- /dev/null +++ b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs @@ -0,0 +1,163 @@ +use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; +use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, + UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaPrivKeyBuilder}; +use crate::utxo::utxo_common::{block_header_utxo_loop, merge_utxo_loop}; +use crate::utxo::{GetUtxoListOps, UtxoArc, UtxoCommonOps, UtxoWeak}; +use crate::{PrivKeyBuildPolicy, UtxoActivationParams}; +use async_trait::async_trait; +use common::executor::spawn; +use common::log::info; +use futures::future::{abortable, AbortHandle}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use serde_json::Value as Json; + +pub struct UtxoArcBuilder<'a, F, T> +where + F: Fn(UtxoArc) -> T + Send + Sync + 'static, +{ + ctx: &'a MmArc, + ticker: &'a str, + conf: &'a Json, + activation_params: &'a UtxoActivationParams, + priv_key_policy: PrivKeyBuildPolicy<'a>, + constructor: F, +} + +impl<'a, F, T> UtxoArcBuilder<'a, F, T> +where + F: Fn(UtxoArc) -> T + Send + Sync + 'static, +{ + pub fn new( + ctx: &'a MmArc, + ticker: &'a str, + conf: &'a Json, + activation_params: &'a UtxoActivationParams, + priv_key_policy: PrivKeyBuildPolicy<'a>, + constructor: F, + ) -> UtxoArcBuilder<'a, F, T> { + UtxoArcBuilder { + ctx, + ticker, + conf, + activation_params, + priv_key_policy, + constructor, + } + } +} + +#[async_trait] +impl<'a, F, T> UtxoCoinBuilderCommonOps for UtxoArcBuilder<'a, F, T> +where + F: Fn(UtxoArc) -> T + Send + Sync + 'static, +{ + fn ctx(&self) -> &MmArc { self.ctx } + + fn conf(&self) -> &Json { self.conf } + + fn activation_params(&self) -> &UtxoActivationParams { self.activation_params } + + fn ticker(&self) -> &str { self.ticker } +} + +impl<'a, F, T> UtxoFieldsWithIguanaPrivKeyBuilder for UtxoArcBuilder<'a, F, T> where + F: Fn(UtxoArc) -> T + Send + Sync + 'static +{ +} + +impl<'a, F, T> UtxoFieldsWithHardwareWalletBuilder for UtxoArcBuilder<'a, F, T> where + F: Fn(UtxoArc) -> T + Send + Sync + 'static +{ +} + +#[async_trait] +impl<'a, F, T> UtxoCoinBuilder for UtxoArcBuilder<'a, F, T> +where + F: Fn(UtxoArc) -> T + Clone + Send + Sync + 'static, + T: UtxoCommonOps + GetUtxoListOps, +{ + type ResultCoin = T; + type Error = UtxoCoinBuildError; + + fn priv_key_policy(&self) -> PrivKeyBuildPolicy<'_> { self.priv_key_policy.clone() } + + async fn build(self) -> MmResult { + let utxo = self.build_utxo_fields().await?; + let utxo_arc = UtxoArc::new(utxo); + let utxo_weak = utxo_arc.downgrade(); + let result_coin = (self.constructor)(utxo_arc); + + self.spawn_merge_utxo_loop_if_required(utxo_weak.clone(), self.constructor.clone()); + if let Some(abort_handler) = self.spawn_block_header_utxo_loop_if_required( + utxo_weak, + &result_coin.as_ref().block_headers_storage, + self.constructor.clone(), + ) { + self.ctx.abort_handlers.lock().unwrap().push(abort_handler); + } + Ok(result_coin) + } +} + +impl<'a, F, T> MergeUtxoArcOps for UtxoArcBuilder<'a, F, T> +where + F: Fn(UtxoArc) -> T + Send + Sync + 'static, + T: UtxoCommonOps + GetUtxoListOps, +{ +} + +impl<'a, F, T> BlockHeaderUtxoArcOps for UtxoArcBuilder<'a, F, T> +where + F: Fn(UtxoArc) -> T + Send + Sync + 'static, + T: UtxoCommonOps, +{ +} + +pub trait MergeUtxoArcOps: UtxoCoinBuilderCommonOps { + fn spawn_merge_utxo_loop_if_required(&self, weak: UtxoWeak, constructor: F) + where + F: Fn(UtxoArc) -> T + Send + Sync + 'static, + { + if let Some(ref merge_params) = self.activation_params().utxo_merge_params { + let fut = merge_utxo_loop( + weak, + merge_params.merge_at, + merge_params.check_every, + merge_params.max_merge_at_once, + constructor, + ); + info!("Starting UTXO merge loop for coin {}", self.ticker()); + spawn(fut); + } + } +} + +pub trait BlockHeaderUtxoArcOps: UtxoCoinBuilderCommonOps { + fn spawn_block_header_utxo_loop_if_required( + &self, + weak: UtxoWeak, + maybe_storage: &Option, + constructor: F, + ) -> Option + where + F: Fn(UtxoArc) -> T + Send + Sync + 'static, + T: UtxoCommonOps, + { + if maybe_storage.is_some() { + let ticker = self.ticker().to_owned(); + let (fut, abort_handle) = abortable(block_header_utxo_loop(weak, constructor)); + info!("Starting UTXO block header loop for coin {}", ticker); + spawn(async move { + if let Err(e) = fut.await { + info!( + "spawn_block_header_utxo_loop_if_required stopped for {}, reason {}", + ticker, e + ); + } + }); + return Some(abort_handle); + } + None + } +} diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs new file mode 100644 index 0000000000..dac910db93 --- /dev/null +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -0,0 +1,756 @@ +use crate::hd_wallet::{HDAccountsMap, HDAccountsMutex}; +use crate::hd_wallet_storage::{HDWalletCoinStorage, HDWalletStorageError}; +use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientImpl, ElectrumRpcRequest, EstimateFeeMethod, + UtxoRpcClientEnum}; +use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; +use crate::utxo::utxo_block_header_storage::{BlockHeaderStorage, InitBlockHeaderStorageOps}; +use crate::utxo::utxo_builder::utxo_conf_builder::{UtxoConfBuilder, UtxoConfError, UtxoConfResult}; +use crate::utxo::{output_script, utxo_common, ElectrumBuilderArgs, ElectrumProtoVerifier, RecentlySpentOutPoints, + TxFee, UtxoCoinConf, UtxoCoinFields, UtxoHDAccount, UtxoHDWallet, UtxoRpcMode, DEFAULT_GAP_LIMIT, + UTXO_DUST_AMOUNT}; +use crate::{BlockchainNetwork, CoinTransportMetrics, DerivationMethod, HistorySyncState, PrivKeyBuildPolicy, + PrivKeyPolicy, RpcClientType, UtxoActivationParams}; +use async_trait::async_trait; +use chain::TxHashAlgo; +use common::executor::{spawn, Timer}; +use common::log::{error, info}; +use common::small_rng; +use crypto::{Bip32DerPathError, Bip44DerPathError, Bip44PathToCoin, CryptoCtx, CryptoInitError, HwWalletType}; +use derive_more::Display; +use futures::channel::mpsc; +use futures::compat::Future01CompatExt; +use futures::lock::Mutex as AsyncMutex; +use futures::StreamExt; +use keys::bytes::Bytes; +pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, KeyPair, Private, Public, Secret, + Type as ScriptType}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use primitives::hash::H256; +use rand::seq::SliceRandom; +use serde_json::{self as json, Value as Json}; +use std::sync::{Arc, Mutex, Weak}; + +cfg_native! { + use crate::utxo::coin_daemon_data_dir; + use crate::utxo::rpc_clients::{ConcurrentRequestMap, NativeClient, NativeClientImpl}; + use dirs::home_dir; + use std::path::{Path, PathBuf}; +} + +pub type UtxoCoinBuildResult = Result>; + +#[derive(Debug, Display)] +pub enum UtxoCoinBuildError { + ConfError(UtxoConfError), + #[display(fmt = "Native RPC client is only supported in native mode")] + NativeRpcNotSupportedInWasm, + ErrorReadingNativeModeConf(String), + #[display(fmt = "Rpc port is not set neither in `coins` file nor in native daemon config")] + RpcPortIsNotSet, + ErrorDetectingFeeMethod(String), + ErrorDetectingDecimals(String), + InvalidBlockchainNetwork(String), + #[display( + fmt = "Failed to connect to at least 1 of {:?} in {} seconds.", + electrum_servers, + seconds + )] + FailedToConnectToElectrums { + electrum_servers: Vec, + seconds: u64, + }, + ElectrumProtocolVersionCheckError(String), + #[display(fmt = "Can not detect the user home directory")] + CantDetectUserHome, + #[display(fmt = "Unexpected derivation method: {}", _0)] + UnexpectedDerivationMethod(String), + #[display(fmt = "Hardware Wallet context is not initialized")] + HwContextNotInitialized, + HDWalletStorageError(HDWalletStorageError), + #[display( + fmt = "Coin should be activated with Hardware Wallet. Please consider using `\"priv_key_policy\": \"Trezor\"` in the activation request" + )] + CoinShouldBeActivatedWithHw, + #[display( + fmt = "Coin doesn't support Trezor hardware wallet. Please consider adding the 'trezor_coin' field to the coins config" + )] + CoinDoesntSupportTrezor, + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for UtxoCoinBuildError { + fn from(e: UtxoConfError) -> Self { UtxoCoinBuildError::ConfError(e) } +} + +impl From for UtxoCoinBuildError { + /// `CryptoCtx` is expected to be initialized already. + fn from(crypto_err: CryptoInitError) -> Self { UtxoCoinBuildError::Internal(crypto_err.to_string()) } +} + +impl From for UtxoCoinBuildError { + fn from(e: Bip32DerPathError) -> Self { UtxoCoinBuildError::Internal(Bip44DerPathError::from(e).to_string()) } +} + +impl From for UtxoCoinBuildError { + fn from(e: HDWalletStorageError) -> Self { UtxoCoinBuildError::HDWalletStorageError(e) } +} + +#[async_trait] +pub trait UtxoCoinBuilder: UtxoFieldsWithIguanaPrivKeyBuilder + UtxoFieldsWithHardwareWalletBuilder { + type ResultCoin; + type Error: NotMmError; + + fn priv_key_policy(&self) -> PrivKeyBuildPolicy<'_>; + + async fn build(self) -> MmResult; + + async fn build_utxo_fields(&self) -> UtxoCoinBuildResult { + match self.priv_key_policy() { + PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => self.build_utxo_fields_with_iguana_priv_key(priv_key).await, + PrivKeyBuildPolicy::Trezor => self.build_utxo_fields_with_trezor().await, + } + } +} + +#[async_trait] +pub trait UtxoCoinWithIguanaPrivKeyBuilder: UtxoFieldsWithIguanaPrivKeyBuilder { + type ResultCoin; + type Error: NotMmError; + + fn priv_key(&self) -> &[u8]; + + async fn build(self) -> MmResult; +} + +#[async_trait] +pub trait UtxoFieldsWithIguanaPrivKeyBuilder: UtxoCoinBuilderCommonOps { + async fn build_utxo_fields_with_iguana_priv_key(&self, priv_key: &[u8]) -> UtxoCoinBuildResult { + let conf = UtxoConfBuilder::new(self.conf(), self.activation_params(), self.ticker()).build()?; + + if self.is_hw_coin(&conf) { + return MmError::err(UtxoCoinBuildError::CoinShouldBeActivatedWithHw); + } + + let private = Private { + prefix: conf.wif_prefix, + secret: H256::from(priv_key), + compressed: true, + checksum_type: conf.checksum_type, + }; + let key_pair = KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; + let addr_format = self.address_format()?; + let my_address = Address { + prefix: conf.pub_addr_prefix, + t_addr_prefix: conf.pub_t_addr_prefix, + hash: AddressHashEnum::AddressHash(key_pair.public().address_hash()), + checksum_type: conf.checksum_type, + hrp: conf.bech32_hrp.clone(), + addr_format, + }; + + let my_script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let derivation_method = DerivationMethod::Iguana(my_address); + let priv_key_policy = PrivKeyPolicy::KeyPair(key_pair); + + let rpc_client = self.rpc_client().await?; + let tx_fee = self.tx_fee(&rpc_client).await?; + let decimals = self.decimals(&rpc_client).await?; + let dust_amount = self.dust_amount(); + + let initial_history_state = self.initial_history_state(); + let tx_hash_algo = self.tx_hash_algo(); + let check_utxo_maturity = self.check_utxo_maturity(); + let tx_cache = self.tx_cache(); + let block_headers_storage = self.block_headers_storage()?; + + let coin = UtxoCoinFields { + conf, + decimals, + dust_amount, + rpc_client, + priv_key_policy, + derivation_method, + history_sync_state: Mutex::new(initial_history_state), + tx_cache, + block_headers_storage, + recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), + tx_fee, + tx_hash_algo, + check_utxo_maturity, + }; + Ok(coin) + } +} + +#[async_trait] +pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { + async fn build_utxo_fields_with_trezor(&self) -> UtxoCoinBuildResult { + let ticker = self.ticker().to_owned(); + let conf = UtxoConfBuilder::new(self.conf(), self.activation_params(), &ticker).build()?; + + if !self.supports_trezor(&conf) { + return MmError::err(UtxoCoinBuildError::CoinDoesntSupportTrezor); + } + self.check_if_trezor_is_initialized()?; + + // For now, use a default script pubkey. + // TODO change the type of `recently_spent_outpoints` to `AsyncMutex>` + let my_script_pubkey = Bytes::new(); + let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)); + + let address_format = self.address_format()?; + let derivation_path = self.derivation_path()?; + + let hd_wallet_storage = HDWalletCoinStorage::init(self.ctx(), ticker).await?; + + let accounts = self + .load_hd_wallet_accounts(&hd_wallet_storage, &derivation_path) + .await?; + let gap_limit = self.gap_limit(); + let hd_wallet = UtxoHDWallet { + hd_wallet_storage, + address_format, + derivation_path, + accounts: HDAccountsMutex::new(accounts), + gap_limit, + }; + + let rpc_client = self.rpc_client().await?; + let tx_fee = self.tx_fee(&rpc_client).await?; + let decimals = self.decimals(&rpc_client).await?; + let dust_amount = self.dust_amount(); + + let initial_history_state = self.initial_history_state(); + let tx_hash_algo = self.tx_hash_algo(); + let check_utxo_maturity = self.check_utxo_maturity(); + let tx_cache = self.tx_cache(); + let block_headers_storage = self.block_headers_storage()?; + + let coin = UtxoCoinFields { + conf, + decimals, + dust_amount, + rpc_client, + priv_key_policy: PrivKeyPolicy::Trezor, + derivation_method: DerivationMethod::HDWallet(hd_wallet), + history_sync_state: Mutex::new(initial_history_state), + block_headers_storage, + tx_cache, + recently_spent_outpoints, + tx_fee, + tx_hash_algo, + check_utxo_maturity, + }; + Ok(coin) + } + + async fn load_hd_wallet_accounts( + &self, + hd_wallet_storage: &HDWalletCoinStorage, + derivation_path: &Bip44PathToCoin, + ) -> UtxoCoinBuildResult> { + utxo_common::load_hd_accounts_from_storage(hd_wallet_storage, derivation_path) + .await + .mm_err(UtxoCoinBuildError::from) + } + + fn derivation_path(&self) -> UtxoConfResult { + if self.conf()["derivation_path"].is_null() { + return MmError::err(UtxoConfError::DerivationPathIsNotSet); + } + json::from_value(self.conf()["derivation_path"].clone()) + .map_to_mm(|e| UtxoConfError::ErrorDeserializingDerivationPath(e.to_string())) + } + + fn gap_limit(&self) -> u32 { self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) } + + fn supports_trezor(&self, conf: &UtxoCoinConf) -> bool { conf.trezor_coin.is_some() } + + fn check_if_trezor_is_initialized(&self) -> UtxoCoinBuildResult<()> { + let crypto_ctx = CryptoCtx::from_ctx(self.ctx())?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| UtxoCoinBuildError::HwContextNotInitialized)?; + match hw_ctx.hw_wallet_type() { + HwWalletType::Trezor => Ok(()), + } + } +} + +#[async_trait] +pub trait UtxoCoinBuilderCommonOps { + fn ctx(&self) -> &MmArc; + + fn conf(&self) -> &Json; + + fn activation_params(&self) -> &UtxoActivationParams; + + fn ticker(&self) -> &str; + + fn block_headers_storage(&self) -> UtxoCoinBuildResult> { + let params: Option<_> = json::from_value(self.conf()["block_header_params"].clone()) + .map_to_mm(|e| UtxoConfError::InvalidBlockHeaderParams(e.to_string()))?; + match params { + None => Ok(None), + Some(params) => Ok(BlockHeaderStorage::new_from_ctx(self.ctx().clone(), params)), + } + } + + fn address_format(&self) -> UtxoCoinBuildResult { + let format_from_req = self.activation_params().address_format.clone(); + let format_from_conf = json::from_value::>(self.conf()["address_format"].clone()) + .map_to_mm(|e| UtxoConfError::InvalidAddressFormat(e.to_string()))? + .unwrap_or(UtxoAddressFormat::Standard); + + let mut address_format = match format_from_req { + Some(from_req) => { + if from_req.is_segwit() != format_from_conf.is_segwit() { + let error = format!( + "Both conf {:?} and request {:?} must be either Segwit or Standard/CashAddress", + format_from_conf, from_req + ); + return MmError::err(UtxoCoinBuildError::from(UtxoConfError::InvalidAddressFormat(error))); + } else { + from_req + } + }, + None => format_from_conf, + }; + + if let UtxoAddressFormat::CashAddress { + network: _, + ref mut pub_addr_prefix, + ref mut p2sh_addr_prefix, + } = address_format + { + *pub_addr_prefix = self.pub_addr_prefix(); + *p2sh_addr_prefix = self.p2sh_address_prefix(); + } + + let is_segwit_in_conf = self.conf()["segwit"].as_bool().unwrap_or(false); + if address_format.is_segwit() && (!is_segwit_in_conf || self.conf()["bech32_hrp"].is_null()) { + let error = + "Cannot use Segwit address format for coin without segwit support or bech32_hrp in config".to_owned(); + return MmError::err(UtxoCoinBuildError::from(UtxoConfError::InvalidAddressFormat(error))); + } + Ok(address_format) + } + + fn pub_addr_prefix(&self) -> u8 { + let pubtype = self.conf()["pubtype"] + .as_u64() + .unwrap_or(if self.ticker() == "BTC" { 0 } else { 60 }); + pubtype as u8 + } + + fn p2sh_address_prefix(&self) -> u8 { + self.conf()["p2shtype"] + .as_u64() + .unwrap_or(if self.ticker() == "BTC" { 5 } else { 85 }) as u8 + } + + fn dust_amount(&self) -> u64 { json::from_value(self.conf()["dust"].clone()).unwrap_or(UTXO_DUST_AMOUNT) } + + fn network(&self) -> UtxoCoinBuildResult { + let conf = self.conf(); + if !conf["network"].is_null() { + return json::from_value(conf["network"].clone()) + .map_to_mm(|e| UtxoCoinBuildError::InvalidBlockchainNetwork(e.to_string())); + } + Ok(BlockchainNetwork::Mainnet) + } + + async fn decimals(&self, _rpc_client: &UtxoRpcClientEnum) -> UtxoCoinBuildResult { + Ok(self.conf()["decimals"].as_u64().unwrap_or(8) as u8) + } + + async fn tx_fee(&self, rpc_client: &UtxoRpcClientEnum) -> UtxoCoinBuildResult { + let tx_fee = match self.conf()["txfee"].as_u64() { + None => TxFee::FixedPerKb(1000), + Some(0) => { + let fee_method = match &rpc_client { + UtxoRpcClientEnum::Electrum(_) => EstimateFeeMethod::Standard, + UtxoRpcClientEnum::Native(client) => client + .detect_fee_method() + .compat() + .await + .map_to_mm(UtxoCoinBuildError::ErrorDetectingFeeMethod)?, + }; + TxFee::Dynamic(fee_method) + }, + Some(fee) => TxFee::FixedPerKb(fee), + }; + Ok(tx_fee) + } + + fn initial_history_state(&self) -> HistorySyncState { + if self.activation_params().tx_history { + HistorySyncState::NotStarted + } else { + HistorySyncState::NotEnabled + } + } + + async fn rpc_client(&self) -> UtxoCoinBuildResult { + match self.activation_params().mode.clone() { + UtxoRpcMode::Native => { + #[cfg(target_arch = "wasm32")] + { + MmError::err(UtxoCoinBuildError::NativeRpcNotSupportedInWasm) + } + #[cfg(not(target_arch = "wasm32"))] + { + let native = self.native_client()?; + Ok(UtxoRpcClientEnum::Native(native)) + } + }, + UtxoRpcMode::Electrum { servers } => { + let electrum = self.electrum_client(ElectrumBuilderArgs::default(), servers).await?; + Ok(UtxoRpcClientEnum::Electrum(electrum)) + }, + } + } + + async fn electrum_client( + &self, + args: ElectrumBuilderArgs, + mut servers: Vec, + ) -> UtxoCoinBuildResult { + let (on_connect_tx, on_connect_rx) = mpsc::unbounded(); + let ticker = self.ticker().to_owned(); + let ctx = self.ctx(); + let mut event_handlers = vec![]; + if args.collect_metrics { + event_handlers.push( + CoinTransportMetrics::new(ctx.metrics.weak(), ticker.clone(), RpcClientType::Electrum).into_shared(), + ); + } + + if args.negotiate_version { + event_handlers.push(ElectrumProtoVerifier { on_connect_tx }.into_shared()); + } + + let mut rng = small_rng(); + servers.as_mut_slice().shuffle(&mut rng); + let client = ElectrumClientImpl::new(ticker, event_handlers); + for server in servers.iter() { + match client.add_server(server).await { + Ok(_) => (), + Err(e) => error!("Error {:?} connecting to {:?}. Address won't be used", e, server), + }; + } + + let mut attempts = 0i32; + while !client.is_connected().await { + if attempts >= 10 { + return MmError::err(UtxoCoinBuildError::FailedToConnectToElectrums { + electrum_servers: servers.clone(), + seconds: 5, + }); + } + + Timer::sleep(0.5).await; + attempts += 1; + } + + let client = Arc::new(client); + + if args.negotiate_version { + let weak_client = Arc::downgrade(&client); + let client_name = format!("{} GUI/MM2 {}", ctx.gui().unwrap_or("UNKNOWN"), ctx.mm_version()); + spawn_electrum_version_loop(weak_client, on_connect_rx, client_name); + + wait_for_protocol_version_checked(&client) + .await + .map_to_mm(UtxoCoinBuildError::ElectrumProtocolVersionCheckError)?; + } + + if args.spawn_ping { + let weak_client = Arc::downgrade(&client); + spawn_electrum_ping_loop(weak_client, servers); + } + + Ok(ElectrumClient(client)) + } + + #[cfg(not(target_arch = "wasm32"))] + fn native_client(&self) -> UtxoCoinBuildResult { + use base64::{encode_config as base64_encode, URL_SAFE}; + + let native_conf_path = self.confpath()?; + let network = self.network()?; + let (rpc_port, rpc_user, rpc_password) = read_native_mode_conf(&native_conf_path, &network) + .map_to_mm(UtxoCoinBuildError::ErrorReadingNativeModeConf)?; + let auth_str = format!("{}:{}", rpc_user, rpc_password); + let rpc_port = match rpc_port { + Some(p) => p, + None => self.conf()["rpcport"] + .as_u64() + .or_mm_err(|| UtxoCoinBuildError::RpcPortIsNotSet)? as u16, + }; + + let ctx = self.ctx(); + let coin_ticker = self.ticker().to_owned(); + let event_handlers = + vec![ + CoinTransportMetrics::new(ctx.metrics.weak(), coin_ticker.clone(), RpcClientType::Native).into_shared(), + ]; + let client = Arc::new(NativeClientImpl { + coin_ticker, + uri: format!("http://127.0.0.1:{}", rpc_port), + auth: format!("Basic {}", base64_encode(&auth_str, URL_SAFE)), + event_handlers, + request_id: 0u64.into(), + list_unspent_concurrent_map: ConcurrentRequestMap::new(), + }); + + Ok(NativeClient(client)) + } + + #[cfg(not(target_arch = "wasm32"))] + fn confpath(&self) -> UtxoCoinBuildResult { + let conf = self.conf(); + // Documented at https://github.com/jl777/coins#bitcoin-protocol-specific-json + // "USERHOME/" prefix should be replaced with the user's home folder. + let declared_confpath = match self.conf()["confpath"].as_str() { + Some(path) if !path.is_empty() => path.trim(), + _ => { + let (name, is_asset_chain) = { + match conf["asset"].as_str() { + Some(a) => (a, true), + None => { + let name = conf["name"] + .as_str() + .or_mm_err(|| UtxoConfError::CurrencyNameIsNotSet)?; + (name, false) + }, + } + }; + let data_dir = coin_daemon_data_dir(name, is_asset_chain); + let confname = format!("{}.conf", name); + + return Ok(data_dir.join(&confname[..])); + }, + }; + + let (confpath, rel_to_home) = match declared_confpath.strip_prefix("~/") { + Some(stripped) => (stripped, true), + None => match declared_confpath.strip_prefix("USERHOME/") { + Some(stripped) => (stripped, true), + None => (declared_confpath, false), + }, + }; + + if rel_to_home { + let home = home_dir().or_mm_err(|| UtxoCoinBuildError::CantDetectUserHome)?; + Ok(home.join(confpath)) + } else { + Ok(confpath.into()) + } + } + + fn tx_hash_algo(&self) -> TxHashAlgo { + if self.ticker() == "GRS" { + TxHashAlgo::SHA256 + } else { + TxHashAlgo::DSHA256 + } + } + + fn check_utxo_maturity(&self) -> bool { + // First, check if the flag is set in the activation params. + if let Some(check_utxo_maturity) = self.activation_params().check_utxo_maturity { + return check_utxo_maturity; + } + self.conf()["check_utxo_maturity"].as_bool().unwrap_or_default() + } + + fn is_hw_coin(&self, conf: &UtxoCoinConf) -> bool { conf.trezor_coin.is_some() } + + #[cfg(target_arch = "wasm32")] + fn tx_cache(&self) -> UtxoVerboseCacheShared { + crate::utxo::tx_cache::wasm_tx_cache::WasmVerboseCache::default().into_shared() + } + + #[cfg(not(target_arch = "wasm32"))] + fn tx_cache(&self) -> UtxoVerboseCacheShared { + crate::utxo::tx_cache::fs_tx_cache::FsVerboseCache::new(self.ticker().to_owned(), self.tx_cache_path()) + .into_shared() + } + + #[cfg(not(target_arch = "wasm32"))] + fn tx_cache_path(&self) -> PathBuf { self.ctx().dbdir().join("TX_CACHE") } +} + +/// Attempts to parse native daemon conf file and return rpcport, rpcuser and rpcpassword +#[cfg(not(target_arch = "wasm32"))] +fn read_native_mode_conf( + filename: &dyn AsRef, + network: &BlockchainNetwork, +) -> Result<(Option, String, String), String> { + use ini::Ini; + + fn read_property<'a>(conf: &'a ini::Ini, network: &BlockchainNetwork, property: &str) -> Option<&'a String> { + let subsection = match network { + BlockchainNetwork::Mainnet => None, + BlockchainNetwork::Testnet => conf.section(Some("test")), + BlockchainNetwork::Regtest => conf.section(Some("regtest")), + }; + subsection + .and_then(|props| props.get(property)) + .or_else(|| conf.general_section().get(property)) + } + + let conf: Ini = match Ini::load_from_file(&filename) { + Ok(ini) => ini, + Err(err) => { + return ERR!( + "Error parsing the native wallet configuration '{}': {}", + filename.as_ref().display(), + err + ) + }, + }; + let rpc_port = match read_property(&conf, network, "rpcport") { + Some(port) => port.parse::().ok(), + None => None, + }; + let rpc_user = try_s!(read_property(&conf, network, "rpcuser").ok_or(ERRL!( + "Conf file {} doesn't have the rpcuser key", + filename.as_ref().display() + ))); + let rpc_password = try_s!(read_property(&conf, network, "rpcpassword").ok_or(ERRL!( + "Conf file {} doesn't have the rpcpassword key", + filename.as_ref().display() + ))); + Ok((rpc_port, rpc_user.clone(), rpc_password.clone())) +} + +/// Ping the electrum servers every 30 seconds to prevent them from disconnecting us. +/// According to docs server can do it if there are no messages in ~10 minutes. +/// https://electrumx.readthedocs.io/en/latest/protocol-methods.html?highlight=keep#server-ping +/// Weak reference will allow to stop the thread if client is dropped. +fn spawn_electrum_ping_loop(weak_client: Weak, servers: Vec) { + spawn(async move { + loop { + if let Some(client) = weak_client.upgrade() { + if let Err(e) = ElectrumClient(client).server_ping().compat().await { + error!("Electrum servers {:?} ping error: {}", servers, e); + } + } else { + info!("Electrum servers {:?} ping loop stopped", servers); + break; + } + Timer::sleep(30.).await + } + }); +} + +/// Follow the `on_connect_rx` stream and verify the protocol version of each connected electrum server. +/// https://electrumx.readthedocs.io/en/latest/protocol-methods.html?highlight=keep#server-version +/// Weak reference will allow to stop the thread if client is dropped. +fn spawn_electrum_version_loop( + weak_client: Weak, + mut on_connect_rx: mpsc::UnboundedReceiver, + client_name: String, +) { + spawn(async move { + while let Some(electrum_addr) = on_connect_rx.next().await { + spawn(check_electrum_server_version( + weak_client.clone(), + client_name.clone(), + electrum_addr, + )); + } + + info!("Electrum server.version loop stopped"); + }); +} + +async fn check_electrum_server_version( + weak_client: Weak, + client_name: String, + electrum_addr: String, +) { + // client.remove_server() is called too often + async fn remove_server(client: ElectrumClient, electrum_addr: &str) { + if let Err(e) = client.remove_server(electrum_addr).await { + error!("Error on remove server: {}", e); + } + } + + if let Some(c) = weak_client.upgrade() { + let client = ElectrumClient(c); + let available_protocols = client.protocol_version(); + let version = match client + .server_version(&electrum_addr, &client_name, available_protocols) + .compat() + .await + { + Ok(version) => version, + Err(e) => { + error!("Electrum {} server.version error: {:?}", electrum_addr, e); + if !e.error.is_transport() { + remove_server(client, &electrum_addr).await; + }; + return; + }, + }; + + // check if the version is allowed + let actual_version = match version.protocol_version.parse::() { + Ok(v) => v, + Err(e) => { + error!("Error on parse protocol_version: {:?}", e); + remove_server(client, &electrum_addr).await; + return; + }, + }; + + if !available_protocols.contains(&actual_version) { + error!( + "Received unsupported protocol version {:?} from {:?}. Remove the connection", + actual_version, electrum_addr + ); + remove_server(client, &electrum_addr).await; + return; + } + + match client.set_protocol_version(&electrum_addr, actual_version).await { + Ok(()) => info!( + "Use protocol version {:?} for Electrum {:?}", + actual_version, electrum_addr + ), + Err(e) => error!("Error on set protocol_version: {}", e), + }; + } +} + +/// Wait until the protocol version of at least one client's Electrum is checked. +async fn wait_for_protocol_version_checked(client: &ElectrumClientImpl) -> Result<(), String> { + let mut attempts = 0; + loop { + if attempts >= 10 { + return ERR!("Failed protocol version verifying of at least 1 of Electrums in 5 seconds."); + } + + if client.count_connections().await == 0 { + // All of the connections were removed because of server.version checking + return ERR!( + "There are no Electrums with the required protocol version {:?}", + client.protocol_version() + ); + } + + if client.is_protocol_version_checked().await { + break; + } + + Timer::sleep(0.5).await; + attempts += 1; + } + + Ok(()) +} diff --git a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs new file mode 100644 index 0000000000..3cc89efe7b --- /dev/null +++ b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs @@ -0,0 +1,292 @@ +use crate::utxo::rpc_clients::EstimateFeeMode; +use crate::utxo::{parse_hex_encoded_u32, UtxoCoinConf, DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT, KMD_MTP_BLOCK_COUNT, + MATURE_CONFIRMATIONS_DEFAULT}; +use crate::UtxoActivationParams; +use bitcrypto::ChecksumType; +use crypto::trezor::utxo::TrezorUtxoCoin; +use crypto::{Bip32Error, ChildNumber}; +use derive_more::Display; +pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, KeyPair, Private, Public, Secret, + Type as ScriptType}; +use mm2_err_handle::prelude::*; +use script::SignatureVersion; +use serde_json::{self as json, Value as Json}; +use std::num::NonZeroU64; +use std::sync::atomic::AtomicBool; + +pub type UtxoConfResult = Result>; + +#[derive(Debug, Display)] +pub enum UtxoConfError { + #[display(fmt = "'name' field is not found in config")] + CurrencyNameIsNotSet, + #[display(fmt = "'derivation_path' field is not found in config")] + DerivationPathIsNotSet, + #[display(fmt = "'trezor_coin' field is not found in config")] + TrezorCoinIsNotSet, + #[display(fmt = "Invalid 'derivation_path' purpose {}. BIP44 is supported only", found)] + InvalidDerivationPathPurpose { + found: ChildNumber, + }, + #[display( + fmt = "Invalid length '{}' of 'derivation_path'. Expected \"m/purpose'/coin_type'/\" path, i.e 2 children", + found_children + )] + InvalidDerivationPathLen { + found_children: usize, + }, + #[display(fmt = "Error deserializing 'derivation_path': {}", _0)] + ErrorDeserializingDerivationPath(String), + InvalidConsensusBranchId(String), + InvalidVersionGroupId(String), + InvalidAddressFormat(String), + InvalidBlockHeaderParams(String), + InvalidDecimals(String), +} + +impl From for UtxoConfError { + fn from(e: Bip32Error) -> Self { UtxoConfError::ErrorDeserializingDerivationPath(e.to_string()) } +} + +pub struct UtxoConfBuilder<'a> { + conf: &'a Json, + ticker: &'a str, + params: &'a UtxoActivationParams, +} + +impl<'a> UtxoConfBuilder<'a> { + pub fn new(conf: &'a Json, params: &'a UtxoActivationParams, ticker: &'a str) -> Self { + UtxoConfBuilder { conf, ticker, params } + } + + pub fn build(&self) -> UtxoConfResult { + let checksum_type = self.checksum_type(); + let pub_addr_prefix = self.pub_addr_prefix(); + let p2sh_addr_prefix = self.p2sh_address_prefix(); + let pub_t_addr_prefix = self.pub_t_address_prefix(); + let p2sh_t_addr_prefix = self.p2sh_t_address_prefix(); + let sign_message_prefix = self.sign_message_prefix(); + + let wif_prefix = self.wif_prefix(); + + let bech32_hrp = self.bech32_hrp(); + + let default_address_format = self.default_address_format(); + + let asset_chain = self.asset_chain(); + let tx_version = self.tx_version(); + let overwintered = self.overwintered(); + + let tx_fee_volatility_percent = self.tx_fee_volatility_percent(); + let version_group_id = self.version_group_id(tx_version, overwintered)?; + let consensus_branch_id = self.consensus_branch_id(tx_version)?; + let signature_version = self.signature_version(); + let fork_id = self.fork_id(); + + // should be sufficient to detect zcash by overwintered flag + let zcash = overwintered; + + let required_confirmations = self.required_confirmations(); + let requires_notarization = self.requires_notarization(); + + let mature_confirmations = self.mature_confirmations(); + + let is_pos = self.is_pos(); + let segwit = self.segwit(); + let force_min_relay_fee = self.conf["force_min_relay_fee"].as_bool().unwrap_or(false); + let mtp_block_count = self.mtp_block_count(); + let estimate_fee_mode = self.estimate_fee_mode(); + let estimate_fee_blocks = self.estimate_fee_blocks(); + let trezor_coin = self.trezor_coin(); + let enable_spv_proof = self.enable_spv_proof(); + + Ok(UtxoCoinConf { + ticker: self.ticker.to_owned(), + is_pos, + requires_notarization, + overwintered, + pub_addr_prefix, + p2sh_addr_prefix, + pub_t_addr_prefix, + p2sh_t_addr_prefix, + sign_message_prefix, + bech32_hrp, + segwit, + wif_prefix, + tx_version, + default_address_format, + asset_chain, + tx_fee_volatility_percent, + version_group_id, + consensus_branch_id, + zcash, + checksum_type, + signature_version, + fork_id, + required_confirmations: required_confirmations.into(), + force_min_relay_fee, + mtp_block_count, + estimate_fee_mode, + mature_confirmations, + estimate_fee_blocks, + trezor_coin, + enable_spv_proof, + }) + } + + fn checksum_type(&self) -> ChecksumType { + match self.ticker { + "GRS" => ChecksumType::DGROESTL512, + "SMART" => ChecksumType::KECCAK256, + _ => ChecksumType::DSHA256, + } + } + + fn pub_addr_prefix(&self) -> u8 { + let pubtype = self.conf["pubtype"] + .as_u64() + .unwrap_or(if self.ticker == "BTC" { 0 } else { 60 }); + pubtype as u8 + } + + fn p2sh_address_prefix(&self) -> u8 { + self.conf["p2shtype"] + .as_u64() + .unwrap_or(if self.ticker == "BTC" { 5 } else { 85 }) as u8 + } + + fn pub_t_address_prefix(&self) -> u8 { self.conf["taddr"].as_u64().unwrap_or(0) as u8 } + + fn p2sh_t_address_prefix(&self) -> u8 { self.conf["taddr"].as_u64().unwrap_or(0) as u8 } + + fn sign_message_prefix(&self) -> Option { + json::from_value(self.conf["sign_message_prefix"].clone()).unwrap_or(None) + } + + fn wif_prefix(&self) -> u8 { + let wiftype = self.conf["wiftype"] + .as_u64() + .unwrap_or(if self.ticker == "BTC" { 128 } else { 188 }); + wiftype as u8 + } + + fn bech32_hrp(&self) -> Option { json::from_value(self.conf["bech32_hrp"].clone()).unwrap_or(None) } + + fn default_address_format(&self) -> UtxoAddressFormat { + let mut address_format: UtxoAddressFormat = + json::from_value(self.conf["address_format"].clone()).unwrap_or(UtxoAddressFormat::Standard); + + if let UtxoAddressFormat::CashAddress { + network: _, + ref mut pub_addr_prefix, + ref mut p2sh_addr_prefix, + } = address_format + { + *pub_addr_prefix = self.pub_addr_prefix(); + *p2sh_addr_prefix = self.p2sh_address_prefix(); + } + + address_format + } + + fn asset_chain(&self) -> bool { self.conf["asset"].as_str().is_some() } + + fn tx_version(&self) -> i32 { self.conf["txversion"].as_i64().unwrap_or(1) as i32 } + + fn overwintered(&self) -> bool { self.conf["overwintered"].as_u64().unwrap_or(0) == 1 } + + fn tx_fee_volatility_percent(&self) -> f64 { + match self.conf["txfee_volatility_percent"].as_f64() { + Some(volatility) => volatility, + None => DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT, + } + } + + fn version_group_id(&self, tx_version: i32, overwintered: bool) -> UtxoConfResult { + let version_group_id = match self.conf["version_group_id"].as_str() { + Some(s) => parse_hex_encoded_u32(s).mm_err(UtxoConfError::InvalidVersionGroupId)?, + None => { + if tx_version == 3 && overwintered { + 0x03c4_8270 + } else if tx_version == 4 && overwintered { + 0x892f_2085 + } else { + 0 + } + }, + }; + Ok(version_group_id) + } + + fn consensus_branch_id(&self, tx_version: i32) -> UtxoConfResult { + let consensus_branch_id = match self.conf["consensus_branch_id"].as_str() { + Some(s) => parse_hex_encoded_u32(s).mm_err(UtxoConfError::InvalidConsensusBranchId)?, + None => match tx_version { + 3 => 0x5ba8_1b19, + 4 => 0x76b8_09bb, + _ => 0, + }, + }; + Ok(consensus_branch_id) + } + + fn signature_version(&self) -> SignatureVersion { + let default_signature_version = if self.ticker == "BCH" || self.fork_id() != 0 { + SignatureVersion::ForkId + } else { + SignatureVersion::Base + }; + json::from_value(self.conf["signature_version"].clone()).unwrap_or(default_signature_version) + } + + fn fork_id(&self) -> u32 { + let default_fork_id = match self.ticker { + "BCH" => "0x40", + _ => "0x0", + }; + let hex_string = self.conf["fork_id"].as_str().unwrap_or(default_fork_id); + let fork_id = u32::from_str_radix(hex_string.trim_start_matches("0x"), 16).unwrap(); + fork_id + } + + fn required_confirmations(&self) -> u64 { + // param from request should override the config + self.params + .required_confirmations + .unwrap_or_else(|| self.conf["required_confirmations"].as_u64().unwrap_or(1)) + } + + fn requires_notarization(&self) -> AtomicBool { + self.params + .requires_notarization + .unwrap_or_else(|| self.conf["requires_notarization"].as_bool().unwrap_or(false)) + .into() + } + + fn mature_confirmations(&self) -> u32 { + self.conf["mature_confirmations"] + .as_u64() + .map(|x| x as u32) + .unwrap_or(MATURE_CONFIRMATIONS_DEFAULT) + } + + fn is_pos(&self) -> bool { self.conf["isPoS"].as_u64() == Some(1) } + + fn segwit(&self) -> bool { self.conf["segwit"].as_bool().unwrap_or(false) } + + fn mtp_block_count(&self) -> NonZeroU64 { + json::from_value(self.conf["mtp_block_count"].clone()).unwrap_or(KMD_MTP_BLOCK_COUNT) + } + + fn estimate_fee_mode(&self) -> Option { + json::from_value(self.conf["estimate_fee_mode"].clone()).unwrap_or(None) + } + + fn estimate_fee_blocks(&self) -> u32 { json::from_value(self.conf["estimate_fee_blocks"].clone()).unwrap_or(1) } + + fn trezor_coin(&self) -> Option { + json::from_value(self.conf["trezor_coin"].clone()).unwrap_or_default() + } + + fn enable_spv_proof(&self) -> bool { self.conf["enable_spv_proof"].as_bool().unwrap_or(false) } +} diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 32e089e2e6..a67c9136c4 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1,42 +1,65 @@ +use super::rpc_clients::TxMerkleBranch; use super::*; -use bigdecimal::{BigDecimal, Zero}; +use crate::coin_balance::{AddressBalanceStatus, HDAddressBalance, HDWalletBalanceOps}; +use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; +use crate::hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountMut, HDAccountsMap, + NewAccountCreatingError}; +use crate::hd_wallet_storage::{HDWalletCoinWithStorageOps, HDWalletStorageResult}; +use crate::rpc_command::init_withdraw::WithdrawTaskHandle; +use crate::utxo::rpc_clients::{electrum_script_hash, BlockHashOrHeight, UnspentInfo, UnspentMap, UtxoRpcClientEnum, + UtxoRpcClientOps, UtxoRpcResult}; +use crate::utxo::tx_cache::TxCacheResult; +use crate::utxo::utxo_withdraw::{InitUtxoWithdraw, StandardUtxoWithdraw, UtxoWithdraw}; +use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, GetWithdrawSenderAddress, HDAddressId, + RawTransactionError, RawTransactionRequest, RawTransactionRes, SearchForSwapTxSpendInput, SignatureError, + SignatureResult, SwapOps, TradePreimageValue, TransactionFut, TxFeeDetails, ValidateAddressResult, + ValidatePaymentInput, VerificationError, VerificationResult, WithdrawFrom, WithdrawResult, + WithdrawSenderAddress}; +use bitcrypto::dhash256; pub use bitcrypto::{dhash160, sha256, ChecksumType}; use chain::constants::SEQUENCE_FINAL; -use chain::{OutPoint, TransactionInput, TransactionOutput}; +use chain::{BlockHeader, OutPoint, RawBlockHeader, TransactionOutput}; use common::executor::Timer; -use common::jsonrpc_client::{JsonRpcError, JsonRpcErrorType}; -use common::log::{error, info, warn}; -use common::mm_ctx::MmArc; -use common::mm_error::prelude::*; +use common::jsonrpc_client::JsonRpcErrorType; +use common::log::{debug, error, info, warn}; use common::mm_metrics::MetricsArc; -use common::mm_number::MmNumber; -use common::{block_on, now_ms}; +use common::{now_ms, one_hundred, ten_f64}; +use crypto::{Bip32DerPathOps, Bip44Chain, Bip44DerPathError, Bip44DerivationPath, RpcDerivationPath}; use futures::compat::Future01CompatExt; use futures::future::{FutureExt, TryFutureExt}; use futures01::future::Either; +use itertools::Itertools; use keys::bytes::Bytes; -use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHash, KeyPair, Public, SegwitAddress, +use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, CompactSignature, Public, SegwitAddress, Type as ScriptType}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::{BigDecimal, MmNumber}; use primitives::hash::H512; -use rpc::v1::types::{Bytes as BytesJson, TransactionInputEnum, H256 as H256Json}; -use script::{Builder, Opcode, Script, ScriptAddress, SignatureVersion, TransactionInputSigner, - UnsignedTransactionInput}; +use rpc::v1::types::{Bytes as BytesJson, ToTxHash, TransactionInputEnum, H256 as H256Json}; +use script::{Builder, Opcode, Script, ScriptAddress, TransactionInputSigner, UnsignedTransactionInput}; use secp256k1::{PublicKey, Signature}; use serde_json::{self as json}; -use serialization::{deserialize, serialize, serialize_with_flags, CoinVariant, SERIALIZE_TRANSACTION_WITNESS}; +use serialization::{deserialize, serialize, serialize_list, serialize_with_flags, CoinVariant, CompactInteger, + Serializable, Stream, SERIALIZE_TRANSACTION_WITNESS}; +use spv_validation::helpers_validation::validate_headers; +use spv_validation::helpers_validation::SPVError; +use spv_validation::spv_proof::{SPVProof, TRY_SPV_PROOF_INTERVAL}; use std::cmp::Ordering; use std::collections::hash_map::{Entry, HashMap}; use std::str::FromStr; -use std::sync::atomic::Ordering as AtomicOrderding; +use std::sync::atomic::Ordering as AtomicOrdering; +use utxo_block_header_storage::BlockHeaderStorageOps; +use utxo_signer::with_key_pair::p2sh_spend; +use utxo_signer::UtxoSignerOps; pub use chain::Transaction as UtxoTx; -use self::rpc_clients::{electrum_script_hash, UnspentInfo, UtxoRpcClientEnum, UtxoRpcClientOps, UtxoRpcResult}; -use crate::{CanRefundHtlc, CoinBalance, TradePreimageValue, TxFeeDetails, ValidateAddressResult, WithdrawResult}; - -const MIN_BTC_TRADING_VOL: &str = "0.00777"; -pub const DEFAULT_SWAP_VOUT: usize = 0; pub const DEFAULT_FEE_VOUT: usize = 0; +pub const DEFAULT_SWAP_TX_SPEND_SIZE: u64 = 305; +pub const DEFAULT_SWAP_VOUT: usize = 0; +const MIN_BTC_TRADING_VOL: &str = "0.00777"; +pub const NO_TX_ERROR_CODE: &str = "'code': -5"; macro_rules! true_or { ($cond: expr, $etype: expr) => { @@ -55,119 +78,407 @@ lazy_static! { pub const HISTORY_TOO_LARGE_ERR_CODE: i64 = -1; -pub struct UtxoArcBuilder<'a> { - ctx: &'a MmArc, - ticker: &'a str, - conf: &'a Json, - req: &'a Json, - priv_key: &'a [u8], -} - -impl<'a> UtxoArcBuilder<'a> { - pub fn new( - ctx: &'a MmArc, - ticker: &'a str, - conf: &'a Json, - req: &'a Json, - priv_key: &'a [u8], - ) -> UtxoArcBuilder<'a> { - UtxoArcBuilder { - ctx, - ticker, - conf, - req, - priv_key, - } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UtxoMergeParams { + merge_at: usize, + #[serde(default = "ten_f64")] + check_every: f64, + #[serde(default = "one_hundred")] + max_merge_at_once: usize, +} + +pub async fn get_tx_fee(coin: &UtxoCoinFields) -> UtxoRpcResult { + let conf = &coin.conf; + match &coin.tx_fee { + TxFee::Dynamic(method) => { + let fee = coin + .rpc_client + .estimate_fee_sat(coin.decimals, method, &conf.estimate_fee_mode, conf.estimate_fee_blocks) + .compat() + .await?; + Ok(ActualTxFee::Dynamic(fee)) + }, + TxFee::FixedPerKb(satoshis) => Ok(ActualTxFee::FixedPerKb(*satoshis)), } } -#[async_trait] -impl UtxoCoinBuilder for UtxoArcBuilder<'_> { - type ResultCoin = UtxoArc; +pub fn derive_address( + coin: &T, + hd_account: &UtxoHDAccount, + chain: Bip44Chain, + address_id: u32, +) -> MmResult, AddressDerivingError> { + let change_child = chain.to_child_number(); + let address_id_child = ChildNumber::from(address_id); + + let derived_pubkey = hd_account + .extended_pubkey + .derive_child(change_child)? + .derive_child(address_id_child)?; + let address = coin.address_from_extended_pubkey(&derived_pubkey); + let pubkey = Public::Compressed(H264::from(derived_pubkey.public_key().serialize())); + + let mut derivation_path = hd_account.account_derivation_path.to_derivation_path(); + derivation_path.push(change_child); + derivation_path.push(address_id_child); + Ok(HDAddress { + address, + pubkey, + derivation_path, + }) +} - async fn build(self) -> Result { - let utxo = try_s!(self.build_utxo_fields().await); - Ok(UtxoArc(Arc::new(utxo))) +pub async fn create_new_account<'a, Coin, XPubExtractor>( + coin: &Coin, + hd_wallet: &'a UtxoHDWallet, + xpub_extractor: &XPubExtractor, +) -> MmResult, NewAccountCreatingError> +where + Coin: ExtractExtendedPubkey + + HDWalletCoinWithStorageOps + + Sync, + XPubExtractor: HDXPubExtractor + Sync, +{ + const INIT_ACCOUNT_ID: u32 = 0; + let new_account_id = hd_wallet + .accounts + .lock() + .await + .iter() + // The last element of the BTreeMap has the max account index. + .last() + .map(|(account_id, _account)| *account_id + 1) + .unwrap_or(INIT_ACCOUNT_ID); + if new_account_id >= ChildNumber::HARDENED_FLAG { + return MmError::err(NewAccountCreatingError::AccountLimitReached { + max_accounts_number: ChildNumber::HARDENED_FLAG, + }); } - fn ctx(&self) -> &MmArc { self.ctx } + let account_child_hardened = true; + let account_child = ChildNumber::new(new_account_id, account_child_hardened) + .map_to_mm(|e| NewAccountCreatingError::Internal(e.to_string()))?; - fn conf(&self) -> &Json { self.conf } + let account_derivation_path: Bip44PathToAccount = hd_wallet.derivation_path.derive(account_child)?; + let account_pubkey = coin + .extract_extended_pubkey(xpub_extractor, account_derivation_path.to_derivation_path()) + .await?; + + let new_account = UtxoHDAccount { + account_id: new_account_id, + extended_pubkey: account_pubkey, + account_derivation_path, + // We don't know how many addresses are used by the user at this moment. + external_addresses_number: 0, + internal_addresses_number: 0, + }; - fn req(&self) -> &Json { self.req } + let accounts = hd_wallet.accounts.lock().await; + if accounts.contains_key(&new_account_id) { + let error = format!( + "Account '{}' has been activated while we proceed the 'create_new_account' function", + new_account_id + ); + return MmError::err(NewAccountCreatingError::Internal(error)); + } - fn ticker(&self) -> &str { self.ticker } + coin.upload_new_account(hd_wallet, new_account.to_storage_item()) + .await?; - fn priv_key(&self) -> &[u8] { self.priv_key } + Ok(AsyncMutexGuard::map(accounts, |accounts| { + accounts + .entry(new_account_id) + // the `entry` method should return [`Entry::Vacant`] due to the checks above + .or_insert(new_account) + })) } -pub async fn utxo_arc_from_conf_and_request( - ctx: &MmArc, - ticker: &str, - conf: &Json, - req: &Json, - priv_key: &[u8], -) -> Result +pub async fn set_known_addresses_number( + coin: &T, + hd_wallet: &UtxoHDWallet, + hd_account: &mut UtxoHDAccount, + chain: Bip44Chain, + new_known_addresses_number: u32, +) -> MmResult<(), AccountUpdatingError> where - T: From + AsRef + UtxoCommonOps + Send + Sync + 'static, + T: HDWalletCoinWithStorageOps + Sync, { - let builder = UtxoArcBuilder::new(ctx, ticker, conf, req, priv_key); - let utxo_arc = try_s!(builder.build().await); - - let merge_params: Option = try_s!(json::from_value(req["utxo_merge_params"].clone())); - if let Some(merge_params) = merge_params { - let weak = utxo_arc.downgrade(); - let merge_loop = merge_utxo_loop::( - weak, - merge_params.merge_at, - merge_params.check_every, - merge_params.max_merge_at_once, - ); - info!("Starting UTXO merge loop for coin {}", ticker); - spawn(merge_loop); + if new_known_addresses_number >= ChildNumber::HARDENED_FLAG { + return MmError::err(AccountUpdatingError::AddressLimitReached { + max_addresses_number: ChildNumber::HARDENED_FLAG, + }); + } + match chain { + Bip44Chain::External => { + coin.update_external_addresses_number(hd_wallet, hd_account.account_id, new_known_addresses_number) + .await?; + hd_account.external_addresses_number = new_known_addresses_number; + }, + Bip44Chain::Internal => { + coin.update_internal_addresses_number(hd_wallet, hd_account.account_id, new_known_addresses_number) + .await?; + hd_account.internal_addresses_number = new_known_addresses_number; + }, } - Ok(T::from(utxo_arc)) + Ok(()) } -fn ten_f64() -> f64 { 10. } +pub async fn produce_hd_address_scanner(coin: &T) -> BalanceResult +where + T: AsRef, +{ + Ok(UtxoAddressScanner::init(coin.as_ref().rpc_client.clone()).await?) +} -fn one_hundred() -> usize { 100 } +pub async fn scan_for_new_addresses( + coin: &T, + hd_wallet: &T::HDWallet, + hd_account: &mut T::HDAccount, + address_scanner: &T::HDAddressScanner, + gap_limit: u32, +) -> BalanceResult> +where + T: HDWalletBalanceOps + Sync, + T::Address: std::fmt::Display, +{ + let mut addresses = scan_for_new_addresses_impl( + coin, + hd_wallet, + hd_account, + address_scanner, + Bip44Chain::External, + gap_limit, + ) + .await?; + addresses.extend( + scan_for_new_addresses_impl( + coin, + hd_wallet, + hd_account, + address_scanner, + Bip44Chain::Internal, + gap_limit, + ) + .await?, + ); -#[derive(Debug, Deserialize)] -struct UtxoMergeParams { - merge_at: usize, - #[serde(default = "ten_f64")] - check_every: f64, - #[serde(default = "one_hundred")] - max_merge_at_once: usize, + Ok(addresses) } -pub async fn get_tx_fee(coin: &UtxoCoinFields) -> Result { - let conf = &coin.conf; - match &coin.tx_fee { - TxFee::Dynamic(method) => { - let fee = coin - .rpc_client - .estimate_fee_sat(coin.decimals, method, &conf.estimate_fee_mode, conf.estimate_fee_blocks) - .compat() - .await?; - Ok(ActualTxFee::Dynamic(fee)) +/// Checks addresses that either had empty transaction history last time we checked or has not been checked before. +/// The checking stops at the moment when we find `gap_limit` consecutive empty addresses. +pub async fn scan_for_new_addresses_impl( + coin: &T, + hd_wallet: &T::HDWallet, + hd_account: &mut T::HDAccount, + address_scanner: &T::HDAddressScanner, + chain: Bip44Chain, + gap_limit: u32, +) -> BalanceResult> +where + T: HDWalletBalanceOps + Sync, + T::Address: std::fmt::Display, +{ + let mut balances = Vec::with_capacity(gap_limit as usize); + + // Get the first unknown address id. + let mut checking_address_id = hd_account + .known_addresses_number(chain) + // A UTXO coin should support both [`Bip44Chain::External`] and [`Bip44Chain::Internal`]. + .mm_err(|e| BalanceError::Internal(e.to_string()))?; + + let mut unused_addresses_counter = 0; + while checking_address_id < ChildNumber::HARDENED_FLAG && unused_addresses_counter < gap_limit { + let HDAddress { + address: checking_address, + derivation_path: checking_address_der_path, + .. + } = coin.derive_address(hd_account, chain, checking_address_id)?; + + match coin.is_address_used(&checking_address, address_scanner).await? { + // We found a non-empty address, so we have to fill up the balance list + // with zeros starting from `last_non_empty_address_id = checking_address_id - unused_addresses_counter`. + AddressBalanceStatus::Used(non_empty_balance) => { + let last_non_empty_address_id = checking_address_id - unused_addresses_counter; + for empty_address_id in last_non_empty_address_id..checking_address_id { + let empty_address = coin.derive_address(hd_account, chain, empty_address_id)?; + + balances.push(HDAddressBalance { + address: empty_address.address.to_string(), + derivation_path: RpcDerivationPath(empty_address.derivation_path), + chain, + balance: CoinBalance::default(), + }); + } + + balances.push(HDAddressBalance { + address: checking_address.to_string(), + derivation_path: RpcDerivationPath(checking_address_der_path), + chain, + balance: non_empty_balance, + }); + // Reset the counter of unused addresses to zero since we found a non-empty address. + unused_addresses_counter = 0; + }, + AddressBalanceStatus::NotUsed => unused_addresses_counter += 1, + } + + checking_address_id += 1; + } + + coin.set_known_addresses_number( + hd_wallet, + hd_account, + chain, + checking_address_id - unused_addresses_counter, + ) + .await?; + + Ok(balances) +} + +pub async fn all_known_addresses_balances( + coin: &T, + hd_account: &T::HDAccount, +) -> BalanceResult> +where + T: HDWalletBalanceOps + Sync, + T::Address: std::fmt::Display + Clone, +{ + let external_addresses = hd_account + .known_addresses_number(Bip44Chain::External) + // A UTXO coin should support both [`Bip44Chain::External`] and [`Bip44Chain::Internal`]. + .mm_err(|e| BalanceError::Internal(e.to_string()))?; + let internal_addresses = hd_account + .known_addresses_number(Bip44Chain::Internal) + // A UTXO coin should support both [`Bip44Chain::External`] and [`Bip44Chain::Internal`]. + .mm_err(|e| BalanceError::Internal(e.to_string()))?; + + let mut balances = coin + .known_addresses_balances_with_ids(hd_account, Bip44Chain::External, 0..external_addresses) + .await?; + balances.extend( + coin.known_addresses_balances_with_ids(hd_account, Bip44Chain::Internal, 0..internal_addresses) + .await?, + ); + + Ok(balances) +} + +pub async fn load_hd_accounts_from_storage( + hd_wallet_storage: &HDWalletCoinStorage, + derivation_path: &Bip44PathToCoin, +) -> HDWalletStorageResult> { + let accounts = hd_wallet_storage.load_all_accounts().await?; + let res: HDWalletStorageResult> = accounts + .iter() + .map(|account_info| { + let account = UtxoHDAccount::try_from_storage_item(derivation_path, account_info)?; + Ok((account.account_id, account)) + }) + .collect(); + match res { + Ok(accounts) => Ok(accounts), + Err(e) if e.get_inner().is_deserializing_err() => { + warn!("Error loading HD accounts from the storage: '{}'. Clear accounts", e); + hd_wallet_storage.clear_accounts().await?; + Ok(HDAccountsMap::new()) }, - TxFee::FixedPerKb(satoshis) => Ok(ActualTxFee::FixedPerKb(*satoshis)), + Err(e) => Err(e), } } -/// returns the fee required to be paid for HTLC spend transaction -pub async fn get_htlc_spend_fee(coin: &T) -> UtxoRpcResult +/// Requests balance of the given `address`. +pub async fn address_balance(coin: &T, address: &Address) -> BalanceResult +where + T: UtxoCommonOps + GetUtxoListOps + MarketCoinOps, +{ + if coin.as_ref().check_utxo_maturity { + let (unspents, _) = coin.get_mature_unspent_ordered_list(address).await?; + return Ok(unspents.to_coin_balance(coin.as_ref().decimals)); + } + + let balance = coin + .as_ref() + .rpc_client + .display_balance(address.clone(), coin.as_ref().decimals) + .compat() + .await?; + + Ok(CoinBalance { + spendable: balance, + unspendable: BigDecimal::from(0), + }) +} + +/// Requests balances of the given `addresses`. +/// The pairs `(Address, CoinBalance)` are guaranteed to be in the same order in which they were requested. +pub async fn addresses_balances(coin: &T, addresses: Vec
) -> BalanceResult> +where + T: UtxoCommonOps + GetUtxoMapOps + MarketCoinOps, +{ + if coin.as_ref().check_utxo_maturity { + let (unspents_map, _) = coin.get_mature_unspent_ordered_map(addresses.clone()).await?; + addresses + .into_iter() + .map(|address| { + let unspents = unspents_map.get(&address).or_mm_err(|| { + let error = format!("'get_mature_unspent_ordered_map' should have returned '{}'", address); + BalanceError::Internal(error) + })?; + let balance = unspents.to_coin_balance(coin.as_ref().decimals); + Ok((address, balance)) + }) + .collect() + } else { + Ok(coin + .as_ref() + .rpc_client + .display_balances(addresses.clone(), coin.as_ref().decimals) + .compat() + .await? + .into_iter() + .map(|(address, spendable)| { + let unspendable = BigDecimal::from(0); + let balance = CoinBalance { spendable, unspendable }; + (address, balance) + }) + .collect()) + } +} + +pub fn derivation_method(coin: &UtxoCoinFields) -> &DerivationMethod { &coin.derivation_method } + +pub async fn extract_extended_pubkey( + conf: &UtxoCoinConf, + xpub_extractor: &XPubExtractor, + derivation_path: DerivationPath, +) -> MmResult where - T: AsRef + UtxoCommonOps, + XPubExtractor: HDXPubExtractor, { + let trezor_coin = conf + .trezor_coin + .or_mm_err(|| HDExtractPubkeyError::CoinDoesntSupportTrezor)?; + let xpub = xpub_extractor.extract_utxo_xpub(trezor_coin, derivation_path).await?; + Secp256k1ExtendedPublicKey::from_str(&xpub).map_to_mm(HDExtractPubkeyError::InvalidXpub) +} + +/// returns the fee required to be paid for HTLC spend transaction +pub async fn get_htlc_spend_fee(coin: &T, tx_size: u64) -> UtxoRpcResult { let coin_fee = coin.get_tx_fee().await?; let mut fee = match coin_fee { // atomic swap payment spend transaction is slightly more than 300 bytes in average as of now - ActualTxFee::Dynamic(fee_per_kb) => (fee_per_kb * SWAP_TX_SPEND_SIZE) / KILO_BYTE, + ActualTxFee::Dynamic(fee_per_kb) => (fee_per_kb * tx_size) / KILO_BYTE, // return satoshis here as swap spend transaction size is always less than 1 kb - ActualTxFee::FixedPerKb(satoshis) => satoshis, + ActualTxFee::FixedPerKb(satoshis) => { + let tx_size_kb = if tx_size % KILO_BYTE == 0 { + tx_size / KILO_BYTE + } else { + tx_size / KILO_BYTE + 1 + }; + satoshis * tx_size_kb + }, }; if coin.as_ref().conf.force_min_relay_fee { let relay_fee = coin.as_ref().rpc_client.get_relay_fee().compat().await?; @@ -179,10 +490,7 @@ where Ok(fee) } -pub fn addresses_from_script + UtxoCommonOps>( - coin: &T, - script: &Script, -) -> Result, String> { +pub fn addresses_from_script(coin: &T, script: &Script) -> Result, String> { let destinations: Vec = try_s!(script.extract_destinations()); let conf = &coin.as_ref().conf; @@ -202,6 +510,7 @@ pub fn addresses_from_script + UtxoCommonOps>( coin.addr_format_for_standard_scripts(), ), ScriptType::P2WPKH => (conf.pub_addr_prefix, conf.pub_t_addr_prefix, UtxoAddressFormat::Segwit), + ScriptType::P2WSH => (conf.pub_addr_prefix, conf.pub_t_addr_prefix, UtxoAddressFormat::Segwit), }; Address { @@ -237,8 +546,8 @@ pub fn address_from_str_unchecked(coin: &UtxoCoinFields, address: &str) -> Resul if let Ok(segwit) = Address::from_segwitaddress( address, coin.conf.checksum_type, - coin.my_address.prefix, - coin.my_address.t_addr_prefix, + coin.conf.pub_addr_prefix, + coin.conf.pub_t_addr_prefix, ) { return Ok(segwit); } @@ -248,7 +557,7 @@ pub fn address_from_str_unchecked(coin: &UtxoCoinFields, address: &str) -> Resul coin.conf.checksum_type, coin.conf.pub_addr_prefix, coin.conf.p2sh_addr_prefix, - coin.my_address.t_addr_prefix, + coin.conf.pub_t_addr_prefix, ) { return Ok(cashaddress); } @@ -256,9 +565,17 @@ pub fn address_from_str_unchecked(coin: &UtxoCoinFields, address: &str) -> Resul return ERR!("Invalid address: {}", address); } -pub fn checked_address_from_str(coin: &UtxoCoinFields, address: &str) -> Result { - let addr = try_s!(address_from_str_unchecked(coin, address)); - try_s!(coin.check_withdraw_address_supported(&addr)); +pub fn my_public_key(coin: &UtxoCoinFields) -> Result<&Public, MmError> { + match coin.priv_key_policy { + PrivKeyPolicy::KeyPair(ref key_pair) => Ok(key_pair.public()), + // Hardware Wallets requires BIP39/BIP44 derivation path to extract a public key. + PrivKeyPolicy::Trezor => MmError::err(UnexpectedDerivationMethod::IguanaPrivKeyUnavailable), + } +} + +pub fn checked_address_from_str(coin: &T, address: &str) -> Result { + let addr = try_s!(address_from_str_unchecked(coin.as_ref(), address)); + try_s!(check_withdraw_address_supported(coin, &addr)); Ok(addr) } @@ -272,7 +589,7 @@ pub async fn get_current_mtp(coin: &UtxoCoinFields, coin_variant: CoinVariant) - pub fn send_outputs_from_my_address(coin: T, outputs: Vec) -> TransactionFut where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, + T: UtxoCommonOps + GetUtxoListOps, { let fut = send_outputs_from_my_address_impl(coin, outputs); Box::new(fut.boxed().compat().map(|tx| tx.into())) @@ -296,115 +613,109 @@ pub fn tx_size_in_v_bytes(from_addr_format: &UtxoAddressFormat, tx: &UtxoTx) -> } } -/// Generates unsigned transaction (TransactionInputSigner) from specified utxos and outputs. -/// This function expects that utxos are sorted by amounts in ascending order -/// Consider sorting before calling this function -/// Sends the change (inputs amount - outputs amount) to "my_address" -/// Also returns additional transaction data -/// -/// Note `gas_fee` should be enough to execute all of the contract calls within UTXO outputs. -/// QRC20 specific: `gas_fee` should be calculated by: gas_limit * gas_price * (count of contract calls), -/// or should be sum of gas fee of all contract calls. -pub async fn generate_transaction( - coin: &T, - utxos: Vec, - outputs: Vec, +pub struct UtxoTxBuilder<'a, T: AsRef + UtxoTxGenerationOps> { + coin: &'a T, + from: Option
, + /// The available inputs that *can* be included in the resulting tx + available_inputs: Vec, fee_policy: FeePolicy, fee: Option, gas_fee: Option, -) -> GenerateTxResult -where - T: AsRef + UtxoCommonOps, -{ - let dust: u64 = coin.as_ref().dust_amount; - let lock_time = (now_ms() / 1000) as u32; + tx: TransactionInputSigner, + change: u64, + sum_inputs: u64, + sum_outputs_value: u64, + tx_fee: u64, + min_relay_fee: Option, + dust: Option, +} + +impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { + pub fn new(coin: &'a T) -> Self { + UtxoTxBuilder { + tx: coin.as_ref().transaction_preimage(), + coin, + from: coin.as_ref().derivation_method.iguana().cloned(), + available_inputs: vec![], + fee_policy: FeePolicy::SendExact, + fee: None, + gas_fee: None, + change: 0, + sum_inputs: 0, + sum_outputs_value: 0, + tx_fee: 0, + min_relay_fee: None, + dust: None, + } + } - let change_script_pubkey = output_script(&coin.as_ref().my_address, ScriptType::P2PKH).to_bytes(); - let coin_tx_fee = match fee { - Some(f) => f, - None => coin.get_tx_fee().await?, - }; + pub fn with_from_address(mut self, from: Address) -> Self { + self.from = Some(from); + self + } - true_or!(!outputs.is_empty(), GenerateTxError::EmptyOutputs); + pub fn with_dust(mut self, dust_amount: u64) -> Self { + self.dust = Some(dust_amount); + self + } - let mut sum_outputs_value = 0; - let mut received_by_me = 0; - for output in outputs.iter() { - let script: Script = output.script_pubkey.clone().into(); - if script.opcodes().next() != Some(Ok(Opcode::OP_RETURN)) { - true_or!(output.value >= dust, GenerateTxError::OutputValueLessThanDust { - value: output.value, - dust - }); - } - sum_outputs_value += output.value; - if output.script_pubkey == change_script_pubkey { - received_by_me += output.value; - } + pub fn add_required_inputs(mut self, inputs: impl IntoIterator) -> Self { + self.tx + .inputs + .extend(inputs.into_iter().map(|input| UnsignedTransactionInput { + previous_output: input.outpoint, + sequence: SEQUENCE_FINAL, + amount: input.value, + witness: Vec::new(), + })); + self } - if let Some(gas_fee) = gas_fee { - sum_outputs_value += gas_fee; + /// This function expects that utxos are sorted by amounts in ascending order + /// Consider sorting before calling this function + pub fn add_available_inputs(mut self, inputs: impl IntoIterator) -> Self { + self.available_inputs.extend(inputs); + self } - true_or!(!utxos.is_empty(), GenerateTxError::EmptyUtxoSet { - required: sum_outputs_value - }); + pub fn add_outputs(mut self, outputs: impl IntoIterator) -> Self { + self.tx.outputs.extend(outputs); + self + } - let str_d_zeel = if coin.as_ref().conf.ticker == "NAV" { - Some("".into()) - } else { - None - }; - let hash_algo = coin.as_ref().tx_hash_algo.into(); - let mut tx = TransactionInputSigner { - inputs: vec![], - outputs, - lock_time, - version: coin.as_ref().conf.tx_version, - n_time: if coin.as_ref().conf.is_pos { - Some((now_ms() / 1000) as u32) - } else { - None - }, - overwintered: coin.as_ref().conf.overwintered, - expiry_height: 0, - join_splits: vec![], - shielded_spends: vec![], - shielded_outputs: vec![], - value_balance: 0, - version_group_id: coin.as_ref().conf.version_group_id, - consensus_branch_id: coin.as_ref().conf.consensus_branch_id, - zcash: coin.as_ref().conf.zcash, - str_d_zeel, - hash_algo, - }; - let mut sum_inputs = 0; - let mut tx_fee = 0; - let min_relay_fee = if coin.as_ref().conf.force_min_relay_fee { - let fee_dec = coin.as_ref().rpc_client.get_relay_fee().compat().await?; - let min_relay_fee = sat_from_big_decimal(&fee_dec, coin.as_ref().decimals)?; - Some(min_relay_fee) - } else { - None - }; - for utxo in utxos.iter() { - sum_inputs += utxo.value; - tx.inputs.push(UnsignedTransactionInput { - previous_output: utxo.outpoint.clone(), - sequence: SEQUENCE_FINAL, - amount: utxo.value, - witness: Vec::new(), - }); - tx_fee = match &coin_tx_fee { + pub fn with_fee_policy(mut self, new_policy: FeePolicy) -> Self { + self.fee_policy = new_policy; + self + } + + pub fn with_fee(mut self, fee: ActualTxFee) -> Self { + self.fee = Some(fee); + self + } + + /// Note `gas_fee` should be enough to execute all of the contract calls within UTXO outputs. + /// QRC20 specific: `gas_fee` should be calculated by: gas_limit * gas_price * (count of contract calls), + /// or should be sum of gas fee of all contract calls. + pub fn with_gas_fee(mut self, gas_fee: u64) -> Self { + self.gas_fee = Some(gas_fee); + self + } + + /// Recalculates fee and checks whether transaction is complete (inputs collected cover the outputs) + fn update_fee_and_check_completeness( + &mut self, + from_addr_format: &UtxoAddressFormat, + actual_tx_fee: &ActualTxFee, + ) -> bool { + self.tx_fee = match &actual_tx_fee { ActualTxFee::Dynamic(f) => { - let transaction = UtxoTx::from(tx.clone()); - let v_size = tx_size_in_v_bytes(&coin.as_ref().my_address.addr_format, &transaction); + let transaction = UtxoTx::from(self.tx.clone()); + let v_size = tx_size_in_v_bytes(from_addr_format, &transaction); (f * v_size as u64) / KILO_BYTE }, ActualTxFee::FixedPerKb(f) => { - let transaction = UtxoTx::from(tx.clone()); - let v_size = tx_size_in_v_bytes(&coin.as_ref().my_address.addr_format, &transaction) as u64; + let transaction = UtxoTx::from(self.tx.clone()); + let v_size = tx_size_in_v_bytes(from_addr_format, &transaction) as u64; let v_size_kb = if v_size % KILO_BYTE == 0 { v_size / KILO_BYTE } else { @@ -414,109 +725,189 @@ where }, }; - match fee_policy { + match self.fee_policy { FeePolicy::SendExact => { - let mut outputs_plus_fee = sum_outputs_value + tx_fee; - if sum_inputs >= outputs_plus_fee { - let change = sum_inputs - outputs_plus_fee; - if change > dust { + let mut outputs_plus_fee = self.sum_outputs_value + self.tx_fee; + if self.sum_inputs >= outputs_plus_fee { + self.change = self.sum_inputs - outputs_plus_fee; + if self.change > self.dust() { // there will be change output - if let ActualTxFee::Dynamic(ref f) = coin_tx_fee { - tx_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; + if let ActualTxFee::Dynamic(ref f) = actual_tx_fee { + self.tx_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; outputs_plus_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; } } - if let Some(min_relay) = min_relay_fee { - if tx_fee < min_relay { - outputs_plus_fee -= tx_fee; + if let Some(min_relay) = self.min_relay_fee { + if self.tx_fee < min_relay { + outputs_plus_fee -= self.tx_fee; outputs_plus_fee += min_relay; - tx_fee = min_relay; + self.tx_fee = min_relay; } } - if sum_inputs >= outputs_plus_fee { - break; - } + self.sum_inputs >= outputs_plus_fee + } else { + false } }, FeePolicy::DeductFromOutput(_) => { - if sum_inputs >= sum_outputs_value { - let change = sum_inputs - sum_outputs_value; - if change > dust { - if let ActualTxFee::Dynamic(ref f) = coin_tx_fee { - tx_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; + if self.sum_inputs >= self.sum_outputs_value { + self.change = self.sum_inputs - self.sum_outputs_value; + if self.change > self.dust() { + if let ActualTxFee::Dynamic(ref f) = actual_tx_fee { + self.tx_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; } } - if let Some(min_relay) = min_relay_fee { - if tx_fee < min_relay { - tx_fee = min_relay; + if let Some(min_relay) = self.min_relay_fee { + if self.tx_fee < min_relay { + self.tx_fee = min_relay; } } - break; + true + } else { + false } }, - }; + } + } + + fn dust(&self) -> u64 { + match self.dust { + Some(dust) => dust, + None => self.coin.as_ref().dust_amount, + } } - match fee_policy { - FeePolicy::SendExact => sum_outputs_value += tx_fee, - FeePolicy::DeductFromOutput(i) => { - let min_output = tx_fee + dust; - let val = tx.outputs[i].value; - true_or!(val >= min_output, GenerateTxError::DeductFeeFromOutputFailed { - output_idx: i, - output_value: val, - required: min_output, + + /// Generates unsigned transaction (TransactionInputSigner) from specified utxos and outputs. + /// Sends the change (inputs amount - outputs amount) to the [`UtxoTxBuilder::from`] address. + /// Also returns additional transaction data + pub async fn build(mut self) -> GenerateTxResult { + let coin = self.coin; + let dust: u64 = self.dust(); + let from = self + .from + .clone() + .or_mm_err(|| GenerateTxError::Internal("'from' address is not specified".to_owned()))?; + let change_script_pubkey = output_script(&from, ScriptType::P2PKH).to_bytes(); + + let actual_tx_fee = match self.fee { + Some(fee) => fee, + None => coin.get_tx_fee().await?, + }; + + true_or!(!self.tx.outputs.is_empty(), GenerateTxError::EmptyOutputs); + + let mut received_by_me = 0; + for output in self.tx.outputs.iter() { + let script: Script = output.script_pubkey.clone().into(); + if script.opcodes().next() != Some(Ok(Opcode::OP_RETURN)) { + true_or!(output.value >= dust, GenerateTxError::OutputValueLessThanDust { + value: output.value, + dust + }); + } + self.sum_outputs_value += output.value; + if output.script_pubkey == change_script_pubkey { + received_by_me += output.value; + } + } + + if let Some(gas_fee) = self.gas_fee { + self.sum_outputs_value += gas_fee; + } + + true_or!( + !self.available_inputs.is_empty() || !self.tx.inputs.is_empty(), + GenerateTxError::EmptyUtxoSet { + required: self.sum_outputs_value + } + ); + + self.min_relay_fee = if coin.as_ref().conf.force_min_relay_fee { + let fee_dec = coin.as_ref().rpc_client.get_relay_fee().compat().await?; + let min_relay_fee = sat_from_big_decimal(&fee_dec, coin.as_ref().decimals)?; + Some(min_relay_fee) + } else { + None + }; + + for utxo in self.available_inputs.clone() { + self.tx.inputs.push(UnsignedTransactionInput { + previous_output: utxo.outpoint, + sequence: SEQUENCE_FINAL, + amount: utxo.value, + witness: vec![], }); - tx.outputs[i].value -= tx_fee; - if tx.outputs[i].script_pubkey == change_script_pubkey { - received_by_me -= tx_fee; + self.sum_inputs += utxo.value; + + if self.update_fee_and_check_completeness(&from.addr_format, &actual_tx_fee) { + break; } - }, - }; - true_or!(sum_inputs >= sum_outputs_value, GenerateTxError::NotEnoughUtxos { - sum_utxos: sum_inputs, - required: sum_outputs_value - }); + } - let change = sum_inputs - sum_outputs_value; - let unused_change = if change > dust { - tx.outputs.push({ - TransactionOutput { - value: change, - script_pubkey: change_script_pubkey.clone(), + match self.fee_policy { + FeePolicy::SendExact => self.sum_outputs_value += self.tx_fee, + FeePolicy::DeductFromOutput(i) => { + let min_output = self.tx_fee + dust; + let val = self.tx.outputs[i].value; + true_or!(val >= min_output, GenerateTxError::DeductFeeFromOutputFailed { + output_idx: i, + output_value: val, + required: min_output, + }); + self.tx.outputs[i].value -= self.tx_fee; + if self.tx.outputs[i].script_pubkey == change_script_pubkey { + received_by_me -= self.tx_fee; + } + }, + }; + true_or!( + self.sum_inputs >= self.sum_outputs_value, + GenerateTxError::NotEnoughUtxos { + sum_utxos: self.sum_inputs, + required: self.sum_outputs_value } - }); - received_by_me += change; - None - } else if change > 0 { - Some(change) - } else { - None - }; + ); - let data = AdditionalTxData { - fee_amount: tx_fee, - received_by_me, - spent_by_me: sum_inputs, - unused_change, - // will be changed if the ticker is KMD - kmd_rewards: None, - }; + let change = self.sum_inputs - self.sum_outputs_value; + let unused_change = if change > dust { + self.tx.outputs.push({ + TransactionOutput { + value: change, + script_pubkey: change_script_pubkey.clone(), + } + }); + received_by_me += change; + None + } else if change > 0 { + Some(change) + } else { + None + }; + + let data = AdditionalTxData { + fee_amount: self.tx_fee, + received_by_me, + spent_by_me: self.sum_inputs, + unused_change, + // will be changed if the ticker is KMD + kmd_rewards: None, + }; - Ok(coin.calc_interest_if_required(tx, data, change_script_pubkey).await?) + Ok(coin + .calc_interest_if_required(self.tx, data, change_script_pubkey) + .await?) + } } /// Calculates interest if the coin is KMD /// Adds the value to existing output to my_script_pub or creates additional interest output /// returns transaction and data as is if the coin is not KMD -pub async fn calc_interest_if_required( +pub async fn calc_interest_if_required( coin: &T, mut unsigned: TransactionInputSigner, mut data: AdditionalTxData, my_script_pub: Bytes, -) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> -where - T: AsRef + UtxoCommonOps, -{ +) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { if coin.as_ref().conf.ticker != "KMD" { return Ok((unsigned, data)); } @@ -527,7 +918,7 @@ where let tx = coin .as_ref() .rpc_client - .get_verbose_transaction(prev_hash) + .get_verbose_transaction(&prev_hash) .compat() .await?; if let Ok(output_interest) = @@ -563,19 +954,18 @@ where Ok((unsigned, data)) } -pub async fn p2sh_spending_tx( - coin: &T, +pub struct P2SHSpendingTxInput<'a> { prev_transaction: UtxoTx, redeem_script: Bytes, outputs: Vec, script_data: Script, sequence: u32, lock_time: u32, -) -> Result -where - T: AsRef + UtxoCommonOps, -{ - let lock_time = try_s!(coin.p2sh_tx_locktime(lock_time).await); + keypair: &'a KeyPair, +} + +pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxInput<'_>) -> Result { + let lock_time = try_s!(coin.p2sh_tx_locktime(input.lock_time).await); let n_time = if coin.as_ref().conf.is_pos { Some((now_ms() / 1000) as u32) } else { @@ -593,15 +983,15 @@ where n_time, overwintered: coin.as_ref().conf.overwintered, inputs: vec![UnsignedTransactionInput { - sequence, + sequence: input.sequence, previous_output: OutPoint { - hash: prev_transaction.hash(), + hash: input.prev_transaction.hash(), index: DEFAULT_SWAP_VOUT as u32, }, - amount: prev_transaction.outputs[0].value, + amount: input.prev_transaction.outputs[0].value, witness: Vec::new(), }], - outputs: outputs.clone(), + outputs: input.outputs, expiry_height: 0, join_splits: vec![], shielded_spends: vec![], @@ -616,9 +1006,9 @@ where let signed_input = try_s!(p2sh_spend( &unsigned, DEFAULT_SWAP_VOUT, - &coin.as_ref().key_pair, - script_data, - redeem_script.into(), + input.keypair, + input.script_data, + input.redeem_script.into(), coin.as_ref().conf.signature_version, coin.as_ref().conf.fork_id )); @@ -628,7 +1018,7 @@ where overwintered: unsigned.overwintered, lock_time: unsigned.lock_time, inputs: vec![signed_input], - outputs, + outputs: unsigned.outputs, expiry_height: unsigned.expiry_height, join_splits: vec![], shielded_spends: vec![], @@ -646,17 +1036,17 @@ where pub fn send_taker_fee(coin: T, fee_pub_key: &[u8], amount: BigDecimal) -> TransactionFut where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, + T: UtxoCommonOps + GetUtxoListOps, { - let address = try_fus!(address_from_raw_pubkey( + let address = try_tx_fus!(address_from_raw_pubkey( fee_pub_key, coin.as_ref().conf.pub_addr_prefix, coin.as_ref().conf.pub_t_addr_prefix, coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), - coin.as_ref().my_address.addr_format.clone() + coin.addr_format().clone(), )); - let amount = try_fus!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); + let amount = try_tx_fus!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); let output = TransactionOutput { value: amount, script_pubkey: Builder::build_p2pkh(&address.hash).to_bytes(), @@ -670,16 +1060,19 @@ pub fn send_maker_payment( taker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_unique_data: &[u8], ) -> TransactionFut where - T: AsRef + UtxoCommonOps + Clone + Send + Sync + 'static, + T: UtxoCommonOps + GetUtxoListOps + SwapOps, { + let maker_htlc_key_pair = coin.derive_htlc_key_pair(swap_unique_data); let SwapPaymentOutputsResult { payment_address, outputs, - } = try_fus!(generate_swap_payment_outputs( + } = try_tx_fus!(generate_swap_payment_outputs( &coin, time_lock, + maker_htlc_key_pair.public_slice(), taker_pub, secret_hash, amount @@ -687,11 +1080,11 @@ where let send_fut = match &coin.as_ref().rpc_client { UtxoRpcClientEnum::Electrum(_) => Either::A(send_outputs_from_my_address(coin, outputs)), UtxoRpcClientEnum::Native(client) => { - let addr_string = try_fus!(payment_address.display_address()); + let addr_string = try_tx_fus!(payment_address.display_address()); Either::B( client .import_address(&addr_string, &addr_string, false) - .map_err(|e| ERRL!("{}", e)) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e))) .and_then(move |_| send_outputs_from_my_address(coin, outputs)), ) }, @@ -705,28 +1098,32 @@ pub fn send_taker_payment( maker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_unique_data: &[u8], ) -> TransactionFut where - T: AsRef + UtxoCommonOps + Clone + Send + Sync + 'static, + T: UtxoCommonOps + GetUtxoListOps + SwapOps, { + let taker_htlc_key_pair = coin.derive_htlc_key_pair(swap_unique_data); let SwapPaymentOutputsResult { payment_address, outputs, - } = try_fus!(generate_swap_payment_outputs( + } = try_tx_fus!(generate_swap_payment_outputs( &coin, time_lock, + taker_htlc_key_pair.public_slice(), maker_pub, secret_hash, amount )); + let send_fut = match &coin.as_ref().rpc_client { UtxoRpcClientEnum::Electrum(_) => Either::A(send_outputs_from_my_address(coin, outputs)), UtxoRpcClientEnum::Native(client) => { - let addr_string = try_fus!(payment_address.display_address()); + let addr_string = try_tx_fus!(payment_address.display_address()); Either::B( client .import_address(&addr_string, &addr_string, false) - .map_err(|e| ERRL!("{}", e)) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e))) .and_then(move |_| send_outputs_from_my_address(coin, outputs)), ) }, @@ -734,18 +1131,19 @@ where Box::new(send_fut) } -pub fn send_maker_spends_taker_payment( +pub fn send_maker_spends_taker_payment( coin: T, taker_payment_tx: &[u8], time_lock: u32, taker_pub: &[u8], secret: &[u8], -) -> TransactionFut -where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, -{ - let mut prev_tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); - prev_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; + swap_unique_data: &[u8], +) -> TransactionFut { + let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; + + let key_pair = coin.derive_htlc_key_pair(swap_unique_data); let script_data = Builder::default() .push_data(secret) .push_opcode(Opcode::OP_0) @@ -753,46 +1151,51 @@ where let redeem_script = payment_script( time_lock, &*dhash160(secret), - &try_fus!(Public::from_slice(taker_pub)), - coin.as_ref().key_pair.public(), - ); + &try_tx_fus!(Public::from_slice(taker_pub)), + key_pair.public(), + ) + .into(); let fut = async move { - let fee = try_s!(coin.get_htlc_spend_fee().await); - let script_pubkey = output_script(&coin.as_ref().my_address, ScriptType::P2PKH).to_bytes(); + let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { - value: prev_tx.outputs[0].value - fee, + value: prev_transaction.outputs[0].value - fee, script_pubkey, }; - let transaction = try_s!( - coin.p2sh_spending_tx( - prev_tx, - redeem_script.into(), - vec![output], - script_data, - SEQUENCE_FINAL, - time_lock - ) - .await - ); + + let input = P2SHSpendingTxInput { + prev_transaction, + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_s!(tx_fut.await); + try_tx_s!(tx_fut.await, transaction); + Ok(transaction.into()) }; Box::new(fut.boxed().compat()) } -pub fn send_taker_spends_maker_payment( +pub fn send_taker_spends_maker_payment( coin: T, maker_payment_tx: &[u8], time_lock: u32, maker_pub: &[u8], secret: &[u8], -) -> TransactionFut -where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, -{ - let mut prev_tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); - prev_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; + swap_unique_data: &[u8], +) -> TransactionFut { + let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; + + let key_pair = coin.derive_htlc_key_pair(swap_unique_data); + let script_data = Builder::default() .push_data(secret) .push_opcode(Opcode::OP_0) @@ -800,117 +1203,129 @@ where let redeem_script = payment_script( time_lock, &*dhash160(secret), - &try_fus!(Public::from_slice(maker_pub)), - coin.as_ref().key_pair.public(), - ); + &try_tx_fus!(Public::from_slice(maker_pub)), + key_pair.public(), + ) + .into(); let fut = async move { - let fee = try_s!(coin.get_htlc_spend_fee().await); - let script_pubkey = output_script(&coin.as_ref().my_address, ScriptType::P2PKH).to_bytes(); + let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { - value: prev_tx.outputs[0].value - fee, + value: prev_transaction.outputs[0].value - fee, script_pubkey, }; - let transaction = try_s!( - coin.p2sh_spending_tx( - prev_tx, - redeem_script.into(), - vec![output], - script_data, - SEQUENCE_FINAL, - time_lock - ) - .await - ); + + let input = P2SHSpendingTxInput { + prev_transaction, + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_s!(tx_fut.await); + try_tx_s!(tx_fut.await, transaction); + Ok(transaction.into()) }; Box::new(fut.boxed().compat()) } -pub fn send_taker_refunds_payment( +pub fn send_taker_refunds_payment( coin: T, taker_payment_tx: &[u8], time_lock: u32, maker_pub: &[u8], secret_hash: &[u8], -) -> TransactionFut -where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, -{ - let mut prev_tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); - prev_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; + swap_unique_data: &[u8], +) -> TransactionFut { + let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let mut prev_transaction: UtxoTx = + try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); + prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; + + let key_pair = coin.derive_htlc_key_pair(swap_unique_data); let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); let redeem_script = payment_script( time_lock, secret_hash, - coin.as_ref().key_pair.public(), - &try_fus!(Public::from_slice(maker_pub)), - ); + key_pair.public(), + &try_tx_fus!(Public::from_slice(maker_pub)), + ) + .into(); let fut = async move { - let fee = try_s!(coin.get_htlc_spend_fee().await); - let script_pubkey = output_script(&coin.as_ref().my_address, ScriptType::P2PKH).to_bytes(); + let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { - value: prev_tx.outputs[0].value - fee, + value: prev_transaction.outputs[0].value - fee, script_pubkey, }; - let transaction = try_s!( - coin.p2sh_spending_tx( - prev_tx, - redeem_script.into(), - vec![output], - script_data, - SEQUENCE_FINAL - 1, - time_lock, - ) - .await - ); + + let input = P2SHSpendingTxInput { + prev_transaction, + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL - 1, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_s!(tx_fut.await); + try_tx_s!(tx_fut.await, transaction); + Ok(transaction.into()) }; Box::new(fut.boxed().compat()) } -pub fn send_maker_refunds_payment( +pub fn send_maker_refunds_payment( coin: T, maker_payment_tx: &[u8], time_lock: u32, taker_pub: &[u8], secret_hash: &[u8], -) -> TransactionFut -where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, -{ - let mut prev_tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); - prev_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; + swap_unique_data: &[u8], +) -> TransactionFut { + let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; + + let key_pair = coin.derive_htlc_key_pair(swap_unique_data); let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); let redeem_script = payment_script( time_lock, secret_hash, - coin.as_ref().key_pair.public(), - &try_fus!(Public::from_slice(taker_pub)), - ); + key_pair.public(), + &try_tx_fus!(Public::from_slice(taker_pub)), + ) + .into(); let fut = async move { - let fee = try_s!(coin.get_htlc_spend_fee().await); - let script_pubkey = output_script(&coin.as_ref().my_address, ScriptType::P2PKH).to_bytes(); + let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { - value: prev_tx.outputs[0].value - fee, + value: prev_transaction.outputs[0].value - fee, script_pubkey, }; - let transaction = try_s!( - coin.p2sh_spending_tx( - prev_tx, - redeem_script.into(), - vec![output], - script_data, - SEQUENCE_FINAL - 1, - time_lock, - ) - .await - ); + + let input = P2SHSpendingTxInput { + prev_transaction, + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL - 1, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_s!(tx_fut.await); + try_tx_s!(tx_fut.await, transaction); + Ok(transaction.into()) }; Box::new(fut.boxed().compat()) @@ -967,7 +1382,7 @@ fn pubkey_from_witness_script(witness_script: &[Bytes]) -> Result pub async fn is_tx_confirmed_before_block(coin: &T, tx: &RpcTransaction, block_number: u64) -> Result where - T: AsRef + Send + Sync + 'static, + T: UtxoCommonOps, { match tx.height { Some(confirmed_at) => Ok(confirmed_at <= block_number), @@ -1000,7 +1415,7 @@ pub fn check_all_inputs_signed_by_pub(tx: &UtxoTx, expected_pub: &[u8]) -> Resul Ok(true) } -pub fn validate_fee( +pub fn validate_fee( coin: T, tx: UtxoTx, output_index: usize, @@ -1008,10 +1423,7 @@ pub fn validate_fee( amount: &BigDecimal, min_block_number: u64, fee_addr: &[u8], -) -> Box + Send> -where - T: AsRef + Send + Sync + 'static, -{ +) -> Box + Send> { let amount = amount.clone(); let address = try_fus!(address_from_raw_pubkey( fee_addr, @@ -1019,7 +1431,7 @@ where coin.as_ref().conf.pub_t_addr_prefix, coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), - coin.as_ref().my_address.addr_format.clone() + coin.addr_format().clone(), )); if !try_fus!(check_all_inputs_signed_by_pub(&tx, sender_pubkey)) { @@ -1030,7 +1442,7 @@ where let tx_from_rpc = try_s!( coin.as_ref() .rpc_client - .get_verbose_transaction(tx.hash().reversed().into()) + .get_verbose_transaction(&tx.hash().reversed().into()) .compat() .await ); @@ -1079,77 +1491,66 @@ where Box::new(fut.boxed().compat()) } -pub fn validate_maker_payment( +pub fn validate_maker_payment( coin: &T, - payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - priv_bn_hash: &[u8], - amount: BigDecimal, -) -> Box + Send> -where - T: AsRef + Clone + Send + Sync + 'static, -{ - let my_public = coin.as_ref().key_pair.public(); - let mut tx: UtxoTx = try_fus!(deserialize(payment_tx).map_err(|e| ERRL!("{:?}", e))); + input: ValidatePaymentInput, +) -> Box + Send> { + let mut tx: UtxoTx = try_fus!(deserialize(input.payment_tx.as_slice()).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.as_ref().tx_hash_algo; + let htlc_keypair = coin.derive_htlc_key_pair(&input.unique_swap_data); validate_payment( coin.clone(), tx, DEFAULT_SWAP_VOUT, - &try_fus!(Public::from_slice(maker_pub)), - my_public, - priv_bn_hash, - amount, - time_lock, + &try_fus!(Public::from_slice(&input.other_pub)), + htlc_keypair.public(), + &input.secret_hash, + input.amount, + input.time_lock, + input.try_spv_proof_until, + input.confirmations, ) } -pub fn validate_taker_payment( +pub fn validate_taker_payment( coin: &T, - payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - priv_bn_hash: &[u8], - amount: BigDecimal, -) -> Box + Send> -where - T: AsRef + Clone + Send + Sync + 'static, -{ - let my_public = coin.as_ref().key_pair.public(); - let mut tx: UtxoTx = try_fus!(deserialize(payment_tx).map_err(|e| ERRL!("{:?}", e))); + input: ValidatePaymentInput, +) -> Box + Send> { + let mut tx: UtxoTx = try_fus!(deserialize(input.payment_tx.as_slice()).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.as_ref().tx_hash_algo; + let htlc_keypair = coin.derive_htlc_key_pair(&input.unique_swap_data); validate_payment( coin.clone(), tx, DEFAULT_SWAP_VOUT, - &try_fus!(Public::from_slice(taker_pub)), - my_public, - priv_bn_hash, - amount, - time_lock, + &try_fus!(Public::from_slice(&input.other_pub)), + htlc_keypair.public(), + &input.secret_hash, + input.amount, + input.time_lock, + input.try_spv_proof_until, + input.confirmations, ) } -pub fn check_if_my_payment_sent( +pub fn check_if_my_payment_sent( coin: T, time_lock: u32, other_pub: &[u8], secret_hash: &[u8], -) -> Box, Error = String> + Send> -where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, -{ + swap_unique_data: &[u8], +) -> Box, Error = String> + Send> { + let my_htlc_keypair = coin.derive_htlc_key_pair(swap_unique_data); let script = payment_script( time_lock, secret_hash, - coin.as_ref().key_pair.public(), + my_htlc_keypair.public(), &try_fus!(Public::from_slice(other_pub)), ); let hash = dhash160(&script); - let p2sh = Builder::build_p2sh(&hash); + let p2sh = Builder::build_p2sh(&hash.into()); let script_hash = electrum_script_hash(&p2sh); let fut = async move { match &coin.as_ref().rpc_client { @@ -1157,7 +1558,7 @@ where let history = try_s!(client.scripthash_get_history(&hex::encode(script_hash)).compat().await); match history.first() { Some(item) => { - let tx_bytes = try_s!(client.get_transaction_bytes(item.tx_hash.clone()).compat().await); + let tx_bytes = try_s!(client.get_transaction_bytes(&item.tx_hash).compat().await); let mut tx: UtxoTx = try_s!(deserialize(tx_bytes.0.as_slice()).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.as_ref().tx_hash_algo; Ok(Some(tx.into())) @@ -1169,10 +1570,10 @@ where let target_addr = Address { t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, prefix: coin.as_ref().conf.p2sh_addr_prefix, - hash, + hash: hash.into(), checksum_type: coin.as_ref().conf.checksum_type, hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: coin.as_ref().my_address.addr_format.clone(), + addr_format: coin.addr_format().clone(), }; let target_addr = target_addr.to_string(); let is_imported = try_s!(client.is_address_imported(&target_addr).await); @@ -1182,7 +1583,7 @@ where let received_by_addr = try_s!(client.list_received_by_address(0, true, true).compat().await); for item in received_by_addr { if item.address == target_addr && !item.txids.is_empty() { - let tx_bytes = try_s!(client.get_transaction_bytes(item.txids[0].clone()).compat().await); + let tx_bytes = try_s!(client.get_transaction_bytes(&item.txids[0]).compat().await); let mut tx: UtxoTx = try_s!(deserialize(tx_bytes.0.as_slice()).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.as_ref().tx_hash_algo; return Ok(Some(tx.into())); @@ -1195,46 +1596,40 @@ where Box::new(fut.boxed().compat()) } -pub fn search_for_swap_tx_spend_my( - coin: &UtxoCoinFields, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], +pub async fn search_for_swap_tx_spend_my + SwapOps>( + coin: &T, + input: SearchForSwapTxSpendInput<'_>, output_index: usize, - search_from_block: u64, ) -> Result, String> { - block_on(search_for_swap_output_spend( - coin, - time_lock, - coin.key_pair.public(), - &try_s!(Public::from_slice(other_pub)), - secret_hash, - tx, + search_for_swap_output_spend( + coin.as_ref(), + input.time_lock, + coin.derive_htlc_key_pair(input.swap_unique_data).public(), + &try_s!(Public::from_slice(input.other_pub)), + input.secret_hash, + input.tx, output_index, - search_from_block, - )) + input.search_from_block, + ) + .await } -pub fn search_for_swap_tx_spend_other( - coin: &UtxoCoinFields, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], +pub async fn search_for_swap_tx_spend_other + SwapOps>( + coin: &T, + input: SearchForSwapTxSpendInput<'_>, output_index: usize, - search_from_block: u64, ) -> Result, String> { - block_on(search_for_swap_output_spend( - coin, - time_lock, - &try_s!(Public::from_slice(other_pub)), - coin.key_pair.public(), - secret_hash, - tx, + search_for_swap_output_spend( + coin.as_ref(), + input.time_lock, + &try_s!(Public::from_slice(input.other_pub)), + coin.derive_htlc_key_pair(input.swap_unique_data).public(), + input.secret_hash, + input.tx, output_index, - search_from_block, - )) + input.search_from_block, + ) + .await } /// Extract a secret from the `spend_tx`. @@ -1246,31 +1641,39 @@ pub fn extract_secret(secret_hash: &[u8], spend_tx: &[u8]) -> Result, St let instruction = match script.get_instruction(1) { Some(Ok(instr)) => instr, Some(Err(e)) => { - log!("Warning: "[e]); + warn!("{:?}", e); continue; }, None => { - log!("Warning: couldn't find secret in "[input_idx]" input"); + warn!("Couldn't find secret in {:?} input", input_idx); continue; }, }; if instruction.opcode != Opcode::OP_PUSHBYTES_32 { - log!("Warning: expected "[Opcode::OP_PUSHBYTES_32]" opcode, found "[instruction.opcode] " in "[input_idx]" input"); + warn!( + "Expected {:?} opcode, found {:?} in {:?} input", + Opcode::OP_PUSHBYTES_32, + instruction.opcode, + input_idx + ); continue; } let secret = match instruction.data { Some(data) => data.to_vec(), None => { - log!("Warning: secret is empty in "[input_idx] " input"); + warn!("Secret is empty in {:?} input", input_idx); continue; }, }; let actual_secret_hash = &*dhash160(&secret); if actual_secret_hash != secret_hash { - log!("Warning: invalid 'dhash160(secret)' "[actual_secret_hash]", expected "[secret_hash]); + warn!( + "Invalid 'dhash160(secret)' {:?}, expected {:?}", + actual_secret_hash, secret_hash + ); continue; } return Ok(secret); @@ -1278,31 +1681,80 @@ pub fn extract_secret(secret_hash: &[u8], spend_tx: &[u8]) -> Result, St ERR!("Couldn't extract secret") } -pub fn my_address(coin: &T) -> Result +pub fn my_address(coin: &T) -> Result { + match coin.as_ref().derivation_method { + DerivationMethod::Iguana(ref my_address) => my_address.display_address(), + DerivationMethod::HDWallet(_) => ERR!("'my_address' is deprecated for HD wallets"), + } +} + +/// Hash message for signature using Bitcoin's message signing format. +/// sha256(sha256(PREFIX_LENGTH + PREFIX + MESSAGE_LENGTH + MESSAGE)) +pub fn sign_message_hash(coin: &UtxoCoinFields, message: &str) -> Option<[u8; 32]> { + let message_prefix = coin.conf.sign_message_prefix.clone()?; + let mut stream = Stream::new(); + let prefix_len = CompactInteger::from(message_prefix.len()); + prefix_len.serialize(&mut stream); + stream.append_slice(message_prefix.as_bytes()); + let msg_len = CompactInteger::from(message.len()); + msg_len.serialize(&mut stream); + stream.append_slice(message.as_bytes()); + Some(dhash256(&stream.out()).take()) +} + +pub fn sign_message(coin: &UtxoCoinFields, message: &str) -> SignatureResult { + let message_hash = sign_message_hash(coin, message).ok_or(SignatureError::PrefixNotFound)?; + let private_key = coin.priv_key_policy.key_pair_or_err()?.private(); + let signature = private_key.sign_compact(&H256::from(message_hash))?; + Ok(base64::encode(&*signature)) +} + +pub fn verify_message( + coin: &T, + signature_base64: &str, + message: &str, + address: &str, +) -> VerificationResult { + let message_hash = sign_message_hash(coin.as_ref(), message).ok_or(VerificationError::PrefixNotFound)?; + let signature = CompactSignature::from(base64::decode(signature_base64)?); + let recovered_pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; + let received_address = checked_address_from_str(coin, address).map_err(VerificationError::AddressDecodingError)?; + Ok(AddressHashEnum::from(recovered_pubkey.address_hash()) == received_address.hash) +} + +pub fn my_balance(coin: T) -> BalanceFut where - T: AsRef + UtxoCommonOps, + T: UtxoCommonOps + GetUtxoListOps + MarketCoinOps, { - coin.as_ref().my_address.display_address() + let my_address = try_f!(coin + .as_ref() + .derivation_method + .iguana_or_err() + .mm_err(BalanceError::from)) + .clone(); + let fut = async move { address_balance(&coin, &my_address).await }; + Box::new(fut.boxed().compat()) } -pub fn my_balance(coin: &UtxoCoinFields) -> BalanceFut { +/// Takes raw transaction as input and returns tx hash in hexadecimal format +pub fn send_raw_tx(coin: &UtxoCoinFields, tx: &str) -> Box + Send> { + let bytes = try_fus!(hex::decode(tx)); Box::new( coin.rpc_client - .display_balance(coin.my_address.clone(), coin.decimals) - .map_to_mm_fut(BalanceError::from) - // at the moment standard UTXO coins do not have an unspendable balance - .map(|spendable| CoinBalance { - spendable, - unspendable: BigDecimal::from(0), - }), + .send_raw_transaction(bytes.into()) + .map_err(|e| ERRL!("{}", e)) + .map(|hash| format!("{:?}", hash)), ) } -pub fn send_raw_tx(coin: &UtxoCoinFields, tx: &str) -> Box + Send> { - let bytes = try_fus!(hex::decode(tx)); +/// Takes raw transaction bytes as input and returns tx hash in hexadecimal format +pub fn send_raw_tx_bytes( + coin: &UtxoCoinFields, + tx_bytes: &[u8], +) -> Box + Send> { Box::new( coin.rpc_client - .send_raw_transaction(bytes.into()) + .send_raw_transaction(tx_bytes.into()) .map_err(|e| ERRL!("{}", e)) .map(|hash| format!("{:?}", hash)), ) @@ -1318,8 +1770,14 @@ pub fn wait_for_confirmations( ) -> Box + Send> { let mut tx: UtxoTx = try_fus!(deserialize(tx).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.tx_hash_algo; - coin.rpc_client - .wait_for_confirmations(&tx, confirmations as u32, requires_nota, wait_until, check_every) + coin.rpc_client.wait_for_confirmations( + tx.hash().reversed().into(), + tx.expiry_height, + confirmations as u32, + requires_nota, + wait_until, + check_every, + ) } pub fn wait_for_output_spend( @@ -1329,25 +1787,33 @@ pub fn wait_for_output_spend( from_block: u64, wait_until: u64, ) -> TransactionFut { - let mut tx: UtxoTx = try_fus!(deserialize(tx_bytes).map_err(|e| ERRL!("{:?}", e))); + let mut tx: UtxoTx = try_tx_fus!(deserialize(tx_bytes).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.tx_hash_algo; let client = coin.rpc_client.clone(); let tx_hash_algo = coin.tx_hash_algo; let fut = async move { loop { - match client.find_output_spend(&tx, output_index, from_block).compat().await { - Ok(Some(mut tx)) => { + match client + .find_output_spend( + tx.hash(), + &tx.outputs[output_index].script_pubkey, + output_index, + BlockHashOrHeight::Height(from_block as i64), + ) + .compat() + .await + { + Ok(Some(spent_output_info)) => { + let mut tx = spent_output_info.spending_tx; tx.tx_hash_algo = tx_hash_algo; return Ok(tx.into()); }, Ok(None) => (), - Err(e) => { - log!("Error " (e) " on find_output_spend of tx " [e]); - }, + Err(e) => error!("Error on find_output_spend_of_tx: {}", e), }; if now_ms() / 1000 > wait_until { - return ERR!( + return TX_PLAIN_ERR!( "Waited too long until {} for transaction {:?} {} to be spent ", wait_until, tx, @@ -1370,7 +1836,12 @@ pub fn current_block(coin: &UtxoCoinFields) -> Box String { format!("{}", coin.key_pair.private()) } +pub fn display_priv_key(coin: &UtxoCoinFields) -> Result { + match coin.priv_key_policy { + PrivKeyPolicy::KeyPair(ref key_pair) => Ok(key_pair.private().to_string()), + PrivKeyPolicy::Trezor => ERR!("'display_priv_key' doesn't support Hardware Wallets"), + } +} pub fn min_tx_amount(coin: &UtxoCoinFields) -> BigDecimal { big_decimal_from_sat(coin.dust_amount as i64, coin.decimals) @@ -1386,116 +1857,126 @@ pub fn min_trading_vol(coin: &UtxoCoinFields) -> MmNumber { pub fn is_asset_chain(coin: &UtxoCoinFields) -> bool { coin.conf.asset_chain } +pub async fn get_raw_transaction(coin: &UtxoCoinFields, req: RawTransactionRequest) -> RawTransactionResult { + let hash = H256Json::from_str(&req.tx_hash).map_to_mm(|e| RawTransactionError::InvalidHashError(e.to_string()))?; + let hex = coin + .rpc_client + .get_transaction_bytes(&hash) + .compat() + .await + .map_err(|e| RawTransactionError::Transport(e.to_string()))?; + Ok(RawTransactionRes { tx_hex: hex }) +} + pub async fn withdraw(coin: T, req: WithdrawRequest) -> WithdrawResult where - T: AsRef + UtxoCommonOps + MarketCoinOps, + T: UtxoCommonOps + GetUtxoListOps + MarketCoinOps, { - let decimals = coin.as_ref().decimals; - - let conf = &coin.as_ref().conf; - - let to = coin - .address_from_str(&req.to) - .map_to_mm(WithdrawError::InvalidAddress)?; - - let is_p2pkh = to.prefix == conf.pub_addr_prefix && to.t_addr_prefix == conf.pub_t_addr_prefix; - let is_p2sh = to.prefix == conf.p2sh_addr_prefix && to.t_addr_prefix == conf.p2sh_t_addr_prefix && conf.segwit; + StandardUtxoWithdraw::new(coin, req)?.build().await +} - let script_type = if is_p2pkh { - ScriptType::P2PKH - } else if is_p2sh { - ScriptType::P2SH - } else { - return MmError::err(WithdrawError::InvalidAddress("Expected either P2PKH or P2SH".into())); - }; +pub async fn init_withdraw( + ctx: MmArc, + coin: T, + req: WithdrawRequest, + task_handle: &WithdrawTaskHandle, +) -> WithdrawResult +where + T: UtxoCommonOps + + GetUtxoListOps + + UtxoSignerOps + + CoinWithDerivationMethod + + GetWithdrawSenderAddress
, +{ + InitUtxoWithdraw::new(ctx, coin, req, task_handle).await?.build().await +} - let script_pubkey = output_script(&to, script_type).to_bytes(); +pub async fn get_withdraw_from_address( + coin: &T, + req: &WithdrawRequest, +) -> MmResult, WithdrawError> +where + T: CoinWithDerivationMethod
::HDWallet> + + HDWalletCoinOps
+ + UtxoCommonOps, +{ + match coin.derivation_method() { + DerivationMethod::Iguana(my_address) => get_withdraw_iguana_sender(coin, req, my_address), + DerivationMethod::HDWallet(hd_wallet) => get_withdraw_hd_sender(coin, req, hd_wallet).await, + } +} - let signature_version = match coin.as_ref().my_address.addr_format { - UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, - _ => conf.signature_version, - }; +pub fn get_withdraw_iguana_sender( + coin: &T, + req: &WithdrawRequest, + my_address: &Address, +) -> MmResult, WithdrawError> { + if req.from.is_some() { + let error = "'from' is not supported if the coin is initialized with an Iguana private key"; + return MmError::err(WithdrawError::UnexpectedFromAddress(error.to_owned())); + } + let pubkey = coin + .my_public_key() + .mm_err(|e| WithdrawError::InternalError(e.to_string()))?; + Ok(WithdrawSenderAddress { + address: my_address.clone(), + pubkey: *pubkey, + derivation_path: None, + }) +} - let _utxo_lock = UTXO_LOCK.lock().await; - let (unspents, _) = coin.ordered_mature_unspents(&coin.as_ref().my_address).await?; - let (value, fee_policy) = if req.max { - ( - unspents.iter().fold(0, |sum, unspent| sum + unspent.value), - FeePolicy::DeductFromOutput(0), - ) - } else { - let value = sat_from_big_decimal(&req.amount, decimals)?; - (value, FeePolicy::SendExact) - }; - let outputs = vec![TransactionOutput { value, script_pubkey }]; - let fee = match req.fee { - Some(WithdrawFee::UtxoFixed { amount }) => { - let fixed = sat_from_big_decimal(&amount, decimals)?; - Some(ActualTxFee::FixedPerKb(fixed)) - }, - Some(WithdrawFee::UtxoPerKbyte { amount }) => { - let dynamic = sat_from_big_decimal(&amount, decimals)?; - Some(ActualTxFee::Dynamic(dynamic)) - }, - Some(fee_policy) => { - let error = format!( - "Expected 'UtxoFixed' or 'UtxoPerKbyte' fee types, found {:?}", - fee_policy - ); - return MmError::err(WithdrawError::InvalidFeePolicy(error)); +pub async fn get_withdraw_hd_sender( + coin: &T, + req: &WithdrawRequest, + hd_wallet: &T::HDWallet, +) -> MmResult, WithdrawError> +where + T: HDWalletCoinOps
, +{ + let HDAddressId { + account_id, + chain, + address_id, + } = match req.from.clone().or_mm_err(|| WithdrawError::FromAddressNotFound)? { + WithdrawFrom::AddressId(id) => id, + WithdrawFrom::DerivationPath { derivation_path } => { + let derivation_path = Bip44DerivationPath::from_str(&derivation_path) + .map_to_mm(Bip44DerPathError::from) + .mm_err(|e| WithdrawError::UnexpectedFromAddress(e.to_string()))?; + let coin_type = derivation_path.coin_type(); + let expected_coin_type = hd_wallet.coin_type(); + if coin_type != expected_coin_type { + let error = format!( + "Derivation path '{}' must has '{}' coin type", + derivation_path, expected_coin_type + ); + return MmError::err(WithdrawError::UnexpectedFromAddress(error)); + } + HDAddressId::from(derivation_path) }, - None => None, }; - let gas_fee = None; - let (unsigned, data) = coin - .generate_transaction(unspents, outputs, fee_policy, fee, gas_fee) + + let hd_account = hd_wallet + .get_account(account_id) .await - .mm_err(|gen_tx_error| { - WithdrawError::from_generate_tx_error(gen_tx_error, coin.ticker().to_owned(), decimals) - })?; - let prev_script = Builder::build_p2pkh(&coin.as_ref().my_address.hash); - let signed = sign_tx( - unsigned, - &coin.as_ref().key_pair, - prev_script, - signature_version, - coin.as_ref().conf.fork_id, - ) - .map_to_mm(WithdrawError::InternalError)?; + .or_mm_err(|| WithdrawError::UnknownAccount { account_id })?; + let hd_address = coin.derive_address(&hd_account, chain, address_id)?; + + let is_address_activated = hd_account + .is_address_activated(chain, address_id) + // If [`HDWalletCoinOps::derive_address`] succeeds, [`HDAccountOps::is_address_activated`] shouldn't fails with an `InvalidBip44ChainError`. + .mm_err(|e| WithdrawError::InternalError(e.to_string()))?; + if !is_address_activated { + let error = format!("'{}' address is not activated", hd_address.address); + return MmError::err(WithdrawError::UnexpectedFromAddress(error)); + } - let fee_amount = data.fee_amount + data.unused_change.unwrap_or_default(); - let fee_details = UtxoFeeDetails { - amount: big_decimal_from_sat(fee_amount as i64, decimals), - }; - let my_address = coin.my_address().map_to_mm(WithdrawError::InternalError)?; - let tx_hex = match coin.as_ref().my_address.addr_format { - UtxoAddressFormat::Segwit => serialize_with_flags(&signed, SERIALIZE_TRANSACTION_WITNESS).into(), - _ => serialize(&signed).into(), - }; - Ok(TransactionDetails { - from: vec![my_address], - to: vec![req.to], - total_amount: big_decimal_from_sat(data.spent_by_me as i64, decimals), - spent_by_me: big_decimal_from_sat(data.spent_by_me as i64, decimals), - received_by_me: big_decimal_from_sat(data.received_by_me as i64, decimals), - my_balance_change: big_decimal_from_sat(data.received_by_me as i64 - data.spent_by_me as i64, decimals), - tx_hash: signed.hash().reversed().to_vec().into(), - tx_hex, - fee_details: Some(fee_details.into()), - block_height: 0, - coin: coin.as_ref().conf.ticker.clone(), - internal_id: vec![].into(), - timestamp: now_ms() / 1000, - kmd_rewards: data.kmd_rewards, - }) + Ok(WithdrawSenderAddress::from(hd_address)) } pub fn decimals(coin: &UtxoCoinFields) -> u8 { coin.decimals } -pub fn convert_to_address(coin: &T, from: &str, to_address_format: Json) -> Result -where - T: AsRef + UtxoCommonOps, -{ +pub fn convert_to_address(coin: &T, from: &str, to_address_format: Json) -> Result { let to_address_format: UtxoAddressFormat = json::from_value(to_address_format).map_err(|e| ERRL!("Error on parse UTXO address format {:?}", e))?; let mut from_address = try_s!(coin.address_from_str(from)); @@ -1521,10 +2002,7 @@ where } } -pub fn validate_address(coin: &T, address: &str) -> ValidateAddressResult -where - T: AsRef + UtxoCommonOps, -{ +pub fn validate_address(coin: &T, address: &str) -> ValidateAddressResult { let result = coin.address_from_str(address); let address = match result { Ok(addr) => addr, @@ -1539,8 +2017,7 @@ where let is_p2pkh = address.prefix == coin.as_ref().conf.pub_addr_prefix && address.t_addr_prefix == coin.as_ref().conf.pub_t_addr_prefix; let is_p2sh = address.prefix == coin.as_ref().conf.p2sh_addr_prefix - && address.t_addr_prefix == coin.as_ref().conf.p2sh_t_addr_prefix - && coin.as_ref().conf.segwit; + && address.t_addr_prefix == coin.as_ref().conf.p2sh_t_addr_prefix; let is_segwit = address.hrp.is_some() && address.hrp == coin.as_ref().conf.bech32_hrp && coin.as_ref().conf.segwit; if is_p2pkh || is_p2sh || is_segwit { @@ -1559,23 +2036,29 @@ where #[allow(clippy::cognitive_complexity)] pub async fn process_history_loop(coin: T, ctx: MmArc) where - T: AsRef + UtxoStandardOps + UtxoCommonOps + MmCoin + MarketCoinOps, + T: UtxoStandardOps + UtxoCommonOps + MmCoin + MarketCoinOps, { let mut my_balance: Option = None; let history = match coin.load_history_from_file(&ctx).compat().await { Ok(history) => history, Err(e) => { - ctx.log.log( + log_tag!( + ctx, "", - &[&"tx_history", &coin.as_ref().conf.ticker], - &ERRL!("Error {} on 'load_history_from_file', stop the history loop", e), + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "Error {} on 'load_history_from_file', stop the history loop", e ); return; }, }; + let mut history_map: HashMap = history .into_iter() - .map(|tx| (H256Json::from(tx.tx_hash.as_slice()), tx)) + .filter_map(|tx| { + let tx_hash = H256Json::from_str(&tx.tx_hash).ok()?; + Some((tx_hash, tx)) + }) .collect(); let mut success_iteration = 0i32; @@ -1587,8 +2070,7 @@ where let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); let coins = coins_ctx.coins.lock().await; if !coins.contains_key(&coin.as_ref().conf.ticker) { - ctx.log - .log("", &[&"tx_history", &coin.as_ref().conf.ticker], "Loop stopped"); + log_tag!(ctx, "", "tx_history", "coin" => coin.as_ref().conf.ticker; fmt = "Loop stopped"); break; }; } @@ -1596,10 +2078,12 @@ where let actual_balance = match coin.my_balance().compat().await { Ok(actual_balance) => Some(actual_balance), Err(err) => { - ctx.log.log( + log_tag!( + ctx, "", - &[&"tx_history", &coin.as_ref().conf.ticker], - &ERRL!("Error {:?} on getting balance", err), + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "Error {:?} on getting balance", err ); None }, @@ -1619,19 +2103,23 @@ where let tx_ids = match coin.request_tx_history(metrics).await { RequestTxHistoryResult::Ok(tx_ids) => tx_ids, RequestTxHistoryResult::Retry { error } => { - ctx.log.log( + log_tag!( + ctx, "", - &[&"tx_history", &coin.as_ref().conf.ticker], - &ERRL!("{}, retrying", error), + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "{}, retrying", error ); Timer::sleep(10.).await; continue; }, RequestTxHistoryResult::HistoryTooLarge => { - ctx.log.log( + log_tag!( + ctx, "", - &[&"tx_history", &coin.as_ref().conf.ticker], - &ERRL!("Got `history too large`, stopping further attempts to retrieve it"), + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "Got `history too large`, stopping further attempts to retrieve it" ); *coin.as_ref().history_sync_state.lock().unwrap() = HistorySyncState::Error(json!({ "code": HISTORY_TOO_LARGE_ERR_CODE, @@ -1639,15 +2127,37 @@ where })); break; }, - RequestTxHistoryResult::UnknownError(e) => { - ctx.log.log( + RequestTxHistoryResult::CriticalError(e) => { + log_tag!( + ctx, "", - &[&"tx_history", &coin.as_ref().conf.ticker], - &ERRL!("{}, stopping futher attempts to retreive it", e), + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "{}, stopping futher attempts to retreive it", e ); break; }, }; + + // Remove transactions in the history_map that are not in the requested transaction list anymore + let history_length = history_map.len(); + let requested_ids: HashSet = tx_ids.iter().map(|x| x.0).collect(); + history_map.retain(|hash, _| requested_ids.contains(hash)); + + if history_map.len() < history_length { + let to_write: Vec = history_map.iter().map(|(_, value)| value.clone()).collect(); + if let Err(e) = coin.save_history_to_file(&ctx, to_write).compat().await { + log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "Error {} on 'save_history_to_file', stop the history loop", e + ); + return; + }; + } + let mut transactions_left = if tx_ids.len() > history_map.len() { *coin.as_ref().history_sync_state.lock().unwrap() = HistorySyncState::InProgress(json!({ "transactions_left": tx_ids.len() - history_map.len() @@ -1664,7 +2174,7 @@ where let mut input_transactions = HistoryUtxoTxMap::default(); for (txid, height) in tx_ids { let mut updated = false; - match history_map.entry(txid.clone()) { + match history_map.entry(txid) { Entry::Vacant(e) => { mm_counter!(ctx.metrics, "tx.history.request.count", 1, "coin" => coin.as_ref().conf.ticker.clone(), "method" => "tx_detail_by_hash"); @@ -1684,10 +2194,12 @@ where } updated = true; }, - Err(e) => ctx.log.log( + Err(e) => log_tag!( + ctx, "", - &[&"tx_history", &coin.as_ref().conf.ticker], - &ERRL!("Error {:?} on getting the details of {:?}, skipping the tx", e, txid), + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "Error {:?} on getting the details of {:?}, skipping the tx", e, txid ), } }, @@ -1707,42 +2219,17 @@ where updated = true; } } - // TODO uncomment this when `update_kmd_rewards` works correctly - // if e.get().should_update_kmd_rewards() && e.get().block_height > 0 { - // mm_counter!(ctx.metrics, "tx.history.update.kmd_rewards", 1); - // match coin.update_kmd_rewards(e.get_mut(), &mut input_transactions).await { - // Ok(()) => updated = true, - // Err(e) => ctx.log.log( - // "😟", - // &[&"tx_history", &coin.as_ref().conf.ticker], - // &ERRL!( - // "Error {:?} on updating the KMD rewards of {:?}, skipping the tx", - // e, - // txid - // ), - // ), - // } - // } }, } if updated { - let mut to_write: Vec = - history_map.iter().map(|(_, value)| value.clone()).collect(); - // the transactions with block_height == 0 are the most recent so we need to separately handle them while sorting - to_write.sort_unstable_by(|a, b| { - if a.block_height == 0 { - Ordering::Less - } else if b.block_height == 0 { - Ordering::Greater - } else { - b.block_height.cmp(&a.block_height) - } - }); + let to_write: Vec = history_map.iter().map(|(_, value)| value.clone()).collect(); if let Err(e) = coin.save_history_to_file(&ctx, to_write).compat().await { - ctx.log.log( + log_tag!( + ctx, "", - &[&"tx_history", &coin.as_ref().conf.ticker], - &ERRL!("Error {} on 'save_history_to_file', stop the history loop", e), + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "Error {} on 'save_history_to_file', stop the history loop", e ); return; }; @@ -1751,10 +2238,12 @@ where *coin.as_ref().history_sync_state.lock().unwrap() = HistorySyncState::Finished; if success_iteration == 0 { - ctx.log.log( + log_tag!( + ctx, "😅", - &[&"tx_history", &("coin", coin.as_ref().conf.ticker.clone().as_str())], - "history has been loaded successfully", + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "history has been loaded successfully" ); } @@ -1766,12 +2255,15 @@ where pub async fn request_tx_history(coin: &T, metrics: MetricsArc) -> RequestTxHistoryResult where - T: AsRef + MmCoin + MarketCoinOps, + T: UtxoCommonOps + MmCoin + MarketCoinOps, { let my_address = match coin.my_address() { Ok(addr) => addr, Err(e) => { - return RequestTxHistoryResult::UnknownError(ERRL!("Error on getting self address: {}. Stop tx history", e)) + return RequestTxHistoryResult::CriticalError(ERRL!( + "Error on getting self address: {}. Stop tx history", + e + )) }, }; @@ -1817,7 +2309,11 @@ where .collect() }, UtxoRpcClientEnum::Electrum(client) => { - let script = output_script(&coin.as_ref().my_address, ScriptType::P2PKH); + let my_address = match coin.as_ref().derivation_method.iguana_or_err() { + Ok(my_address) => my_address, + Err(e) => return RequestTxHistoryResult::CriticalError(e.to_string()), + }; + let script = output_script(my_address, ScriptType::P2PKH); let script_hash = electrum_script_hash(&script); mm_counter!(metrics, "tx.history.request.count", 1, @@ -1826,7 +2322,9 @@ where let electrum_history = match client.scripthash_get_history(&hex::encode(script_hash)).compat().await { Ok(value) => value, Err(e) => match &e.error { - JsonRpcErrorType::Transport(e) | JsonRpcErrorType::Parse(_, e) => { + JsonRpcErrorType::InvalidRequest(e) + | JsonRpcErrorType::Transport(e) + | JsonRpcErrorType::Parse(_, e) => { return RequestTxHistoryResult::Retry { error: ERRL!("Error {} on scripthash_get_history", e), }; @@ -1863,25 +2361,17 @@ where RequestTxHistoryResult::Ok(tx_ids) } -pub async fn tx_details_by_hash( +pub async fn tx_details_by_hash( coin: &T, hash: &[u8], input_transactions: &mut HistoryUtxoTxMap, -) -> Result -where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, -{ +) -> Result { let ticker = &coin.as_ref().conf.ticker; let hash = H256Json::from(hash); - let verbose_tx = try_s!( - coin.as_ref() - .rpc_client - .get_verbose_transaction(hash.clone()) - .compat() - .await - ); + let verbose_tx = try_s!(coin.as_ref().rpc_client.get_verbose_transaction(&hash).compat().await); let mut tx: UtxoTx = try_s!(deserialize(verbose_tx.hex.as_slice()).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.as_ref().tx_hash_algo; + let my_address = try_s!(coin.as_ref().derivation_method.iguana_or_err()); input_transactions.insert(hash, HistoryUtxoTx { tx: tx.clone(), @@ -1903,7 +2393,7 @@ where let prev_tx_hash: H256Json = input.previous_output.hash.reversed().into(); let prev_tx = try_s!( - coin.get_mut_verbose_transaction_from_map_or_rpc(prev_tx_hash.clone(), input_transactions) + coin.get_mut_verbose_transaction_from_map_or_rpc(prev_tx_hash, input_transactions) .await ); let prev_tx = &mut prev_tx.tx; @@ -1917,7 +2407,7 @@ where .clone() .into() )); - if from.contains(&coin.as_ref().my_address) { + if from.contains(my_address) { spent_by_me += prev_tx_value; } from_addresses.extend(from.into_iter()); @@ -1926,7 +2416,7 @@ where for output in tx.outputs.iter() { output_amount += output.value; let to = try_s!(coin.addresses_from_script(&output.script_pubkey.clone().into())); - if to.contains(&coin.as_ref().my_address) { + if to.contains(my_address) { received_by_me += output.value; } to_addresses.extend(to.into_iter()); @@ -1972,7 +2462,7 @@ where }; cur + fee }); - (fee.into(), None) + (try_s!(fee.try_into()), None) } else { let fee = input_amount as i64 - output_amount as i64; (big_decimal_from_sat(fee, coin.as_ref().decimals), None) @@ -1988,6 +2478,11 @@ where to_addresses.sort(); to_addresses.dedup(); + let fee_details = UtxoFeeDetails { + coin: Some(coin.as_ref().conf.ticker.clone()), + amount: fee, + }; + Ok(TransactionDetails { from: from_addresses, to: to_addresses, @@ -1995,14 +2490,15 @@ where spent_by_me: big_decimal_from_sat_unsigned(spent_by_me, coin.as_ref().decimals), my_balance_change: big_decimal_from_sat(received_by_me as i64 - spent_by_me as i64, coin.as_ref().decimals), total_amount: big_decimal_from_sat_unsigned(input_amount, coin.as_ref().decimals), - tx_hash: tx.hash().reversed().to_vec().into(), + tx_hash: tx.hash().reversed().to_vec().to_tx_hash(), tx_hex: verbose_tx.hex, - fee_details: Some(UtxoFeeDetails { amount: fee }.into()), + fee_details: Some(fee_details.into()), block_height: verbose_tx.height.unwrap_or(0), coin: ticker.clone(), internal_id: tx.hash().reversed().to_vec().into(), timestamp: verbose_tx.time.into(), kmd_rewards, + transaction_type: Default::default(), }) } @@ -2014,12 +2510,12 @@ pub async fn get_mut_verbose_transaction_from_map_or_rpc<'a, 'b, T>( where T: AsRef, { - let tx = match utxo_tx_map.entry(tx_hash.clone()) { + let tx = match utxo_tx_map.entry(tx_hash) { Entry::Vacant(e) => { let verbose = coin .as_ref() .rpc_client - .get_verbose_transaction(tx_hash.clone()) + .get_verbose_transaction(&tx_hash) .compat() .await?; let tx = HistoryUtxoTx { @@ -2047,7 +2543,7 @@ pub async fn update_kmd_rewards( input_transactions: &mut HistoryUtxoTxMap, ) -> UtxoRpcResult<()> where - T: AsRef + UtxoCommonOps + UtxoStandardOps + MarketCoinOps + Send + Sync + 'static, + T: UtxoCommonOps + UtxoStandardOps + MarketCoinOps, { if !tx_details.should_update_kmd_rewards() { let error = "There is no need to update KMD rewards".to_owned(); @@ -2062,9 +2558,10 @@ where let kmd_rewards = coin.calc_interest_of_tx(&tx, input_transactions).await?; let kmd_rewards = big_decimal_from_sat_unsigned(kmd_rewards, coin.as_ref().decimals); - if let Some(TxFeeDetails::Utxo(UtxoFeeDetails { ref amount })) = tx_details.fee_details { + if let Some(TxFeeDetails::Utxo(UtxoFeeDetails { ref amount, .. })) = tx_details.fee_details { let actual_fee_amount = amount + &kmd_rewards; tx_details.fee_details = Some(TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some(coin.as_ref().conf.ticker.clone()), amount: actual_fee_amount, })); } @@ -2079,14 +2576,11 @@ where Ok(()) } -pub async fn calc_interest_of_tx( +pub async fn calc_interest_of_tx( coin: &T, tx: &UtxoTx, input_transactions: &mut HistoryUtxoTxMap, -) -> UtxoRpcResult -where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, -{ +) -> UtxoRpcResult { if coin.as_ref().conf.ticker != "KMD" { let error = format!("Expected KMD ticker, found {}", coin.as_ref().conf.ticker); return MmError::err(UtxoRpcError::Internal(error)); @@ -2101,7 +2595,7 @@ where let prev_tx_hash: H256Json = input.previous_output.hash.reversed().into(); let prev_tx = coin - .get_mut_verbose_transaction_from_map_or_rpc(prev_tx_hash.clone(), input_transactions) + .get_mut_verbose_transaction_from_map_or_rpc(prev_tx_hash, input_transactions) .await?; let prev_tx_value = prev_tx.tx.outputs[input.previous_output.index as usize].value; @@ -2118,10 +2612,7 @@ pub fn history_sync_status(coin: &UtxoCoinFields) -> HistorySyncState { coin.history_sync_state.lock().unwrap().clone() } -pub fn get_trade_fee(coin: T) -> Box + Send> -where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, -{ +pub fn get_trade_fee(coin: T) -> Box + Send> { let ticker = coin.as_ref().conf.ticker.clone(); let decimals = coin.as_ref().decimals; let fut = async move { @@ -2153,19 +2644,20 @@ where /// So we should always return a fee as if a transaction includes the change output. pub async fn preimage_trade_fee_required_to_send_outputs( coin: &T, + ticker: &str, outputs: Vec, fee_policy: FeePolicy, gas_fee: Option, stage: &FeeApproxStage, ) -> TradePreimageResult where - T: AsRef + UtxoCommonOps, + T: UtxoCommonOps + GetUtxoListOps, { - let ticker = coin.as_ref().conf.ticker.clone(); let decimals = coin.as_ref().decimals; let tx_fee = coin.get_tx_fee().await?; // [`FeePolicy::DeductFromOutput`] is used if the value is [`TradePreimageValue::UpperBound`] only let is_amount_upper_bound = matches!(fee_policy, FeePolicy::DeductFromOutput(_)); + let my_address = coin.as_ref().derivation_method.iguana_or_err()?; match tx_fee { // if it's a dynamic fee, we should generate a swap transaction to get an actual trade fee @@ -2174,12 +2666,21 @@ where let dynamic_fee = coin.increase_dynamic_fee_by_stage(fee, stage); let outputs_count = outputs.len(); - let (unspents, _recently_sent_txs) = coin.list_unspent_ordered(&coin.as_ref().my_address).await?; + let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(my_address).await?; - let actual_tx_fee = Some(ActualTxFee::Dynamic(dynamic_fee)); - let (tx, data) = generate_transaction(coin, unspents, outputs, fee_policy, actual_tx_fee, gas_fee) - .await - .mm_err(|e| TradePreimageError::from_generate_tx_error(e, ticker, decimals, is_amount_upper_bound))?; + let actual_tx_fee = ActualTxFee::Dynamic(dynamic_fee); + + let mut tx_builder = UtxoTxBuilder::new(coin) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_fee_policy(fee_policy) + .with_fee(actual_tx_fee); + if let Some(gas) = gas_fee { + tx_builder = tx_builder.with_gas_fee(gas); + } + let (tx, data) = tx_builder.build().await.mm_err(|e| { + TradePreimageError::from_generate_tx_error(e, ticker.to_owned(), decimals, is_amount_upper_bound) + })?; let total_fee = if tx.outputs.len() == outputs_count { // take into account the change output @@ -2193,11 +2694,19 @@ where }, ActualTxFee::FixedPerKb(fee) => { let outputs_count = outputs.len(); - let (unspents, _recently_sent_txs) = coin.list_unspent_ordered(&coin.as_ref().my_address).await?; - - let (tx, data) = generate_transaction(coin, unspents, outputs, fee_policy, Some(tx_fee), gas_fee) - .await - .mm_err(|e| TradePreimageError::from_generate_tx_error(e, ticker, decimals, is_amount_upper_bound))?; + let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(my_address).await?; + + let mut tx_builder = UtxoTxBuilder::new(coin) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_fee_policy(fee_policy) + .with_fee(tx_fee); + if let Some(gas) = gas_fee { + tx_builder = tx_builder.with_gas_fee(gas); + } + let (tx, data) = tx_builder.build().await.mm_err(|e| { + TradePreimageError::from_generate_tx_error(e, ticker.to_string(), decimals, is_amount_upper_bound) + })?; let total_fee = if tx.outputs.len() == outputs_count { // take into account the change output if tx_size_kb(tx with change) > tx_size_kb(tx without change) @@ -2222,45 +2731,44 @@ where /// Even if refund will be required the fee will be deducted from P2SH input. /// Please note the `get_sender_trade_fee` satisfies the following condition: /// `get_sender_trade_fee(x) <= get_sender_trade_fee(y)` for any `x < y`. -pub fn get_sender_trade_fee(coin: T, value: TradePreimageValue, stage: FeeApproxStage) -> TradePreimageFut +pub async fn get_sender_trade_fee( + coin: &T, + value: TradePreimageValue, + stage: FeeApproxStage, +) -> TradePreimageResult where - T: AsRef + MarketCoinOps + UtxoCommonOps + Send + Sync + 'static, + T: MarketCoinOps + UtxoCommonOps, { - let fut = async move { - let (amount, fee_policy) = match value { - TradePreimageValue::UpperBound(upper_bound) => (upper_bound, FeePolicy::DeductFromOutput(0)), - TradePreimageValue::Exact(amount) => (amount, FeePolicy::SendExact), - }; - - // pass the dummy params - let time_lock = (now_ms() / 1000) as u32; - let other_pub = &[0; 33]; // H264 is 33 bytes - let secret_hash = &[0; 20]; // H160 is 20 bytes - - // `generate_swap_payment_outputs` may fail due to either invalid `other_pub` or a number conversation error - let SwapPaymentOutputsResult { outputs, .. } = - generate_swap_payment_outputs(&coin, time_lock, other_pub, secret_hash, amount) - .map_to_mm(TradePreimageError::InternalError)?; - let gas_fee = None; - let fee_amount = coin - .preimage_trade_fee_required_to_send_outputs(outputs, fee_policy, gas_fee, &stage) - .await?; - Ok(TradeFee { - coin: coin.as_ref().conf.ticker.clone(), - amount: fee_amount.into(), - paid_from_trading_vol: false, - }) + let (amount, fee_policy) = match value { + TradePreimageValue::UpperBound(upper_bound) => (upper_bound, FeePolicy::DeductFromOutput(0)), + TradePreimageValue::Exact(amount) => (amount, FeePolicy::SendExact), }; - Box::new(fut.boxed().compat()) + + // pass the dummy params + let time_lock = (now_ms() / 1000) as u32; + let my_pub = &[0; 33]; // H264 is 33 bytes + let other_pub = &[0; 33]; // H264 is 33 bytes + let secret_hash = &[0; 20]; // H160 is 20 bytes + + // `generate_swap_payment_outputs` may fail due to either invalid `other_pub` or a number conversation error + let SwapPaymentOutputsResult { outputs, .. } = + generate_swap_payment_outputs(&coin, time_lock, my_pub, other_pub, secret_hash, amount) + .map_to_mm(TradePreimageError::InternalError)?; + let gas_fee = None; + let fee_amount = coin + .preimage_trade_fee_required_to_send_outputs(outputs, fee_policy, gas_fee, &stage) + .await?; + Ok(TradeFee { + coin: coin.as_ref().conf.ticker.clone(), + amount: fee_amount.into(), + paid_from_trading_vol: false, + }) } /// The fee to spend (receive) other payment is deducted from the trading amount so we should display it -pub fn get_receiver_trade_fee(coin: T) -> TradePreimageFut -where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, -{ +pub fn get_receiver_trade_fee(coin: T) -> TradePreimageFut { let fut = async move { - let amount_sat = get_htlc_spend_fee(&coin).await?; + let amount_sat = get_htlc_spend_fee(&coin, DEFAULT_SWAP_TX_SPEND_SIZE).await?; let amount = big_decimal_from_sat_unsigned(amount_sat, coin.as_ref().decimals).into(); Ok(TradeFee { coin: coin.as_ref().conf.ticker.clone(), @@ -2271,78 +2779,114 @@ where Box::new(fut.boxed().compat()) } -pub fn get_fee_to_send_taker_fee( - coin: T, +pub async fn get_fee_to_send_taker_fee( + coin: &T, dex_fee_amount: BigDecimal, stage: FeeApproxStage, -) -> TradePreimageFut +) -> TradePreimageResult where - T: AsRef + MarketCoinOps + UtxoCommonOps + Send + Sync + 'static, + T: MarketCoinOps + UtxoCommonOps, { let decimals = coin.as_ref().decimals; - let fut = async move { - let value = sat_from_big_decimal(&dex_fee_amount, decimals)?; - let output = TransactionOutput { - value, - script_pubkey: Builder::build_p2pkh(&AddressHash::default()).to_bytes(), - }; - let gas_fee = None; - let fee_amount = coin - .preimage_trade_fee_required_to_send_outputs(vec![output], FeePolicy::SendExact, gas_fee, &stage) - .await?; - Ok(TradeFee { - coin: coin.ticker().to_owned(), - amount: fee_amount.into(), - paid_from_trading_vol: false, - }) + let value = sat_from_big_decimal(&dex_fee_amount, decimals)?; + let output = TransactionOutput { + value, + script_pubkey: Builder::build_p2pkh(&AddressHashEnum::default_address_hash()).to_bytes(), }; - Box::new(fut.boxed().compat()) + let gas_fee = None; + let fee_amount = coin + .preimage_trade_fee_required_to_send_outputs(vec![output], FeePolicy::SendExact, gas_fee, &stage) + .await?; + Ok(TradeFee { + coin: coin.ticker().to_owned(), + amount: fee_amount.into(), + paid_from_trading_vol: false, + }) } pub fn required_confirmations(coin: &UtxoCoinFields) -> u64 { - coin.conf.required_confirmations.load(AtomicOrderding::Relaxed) + coin.conf.required_confirmations.load(AtomicOrdering::Relaxed) } pub fn requires_notarization(coin: &UtxoCoinFields) -> bool { - coin.conf.requires_notarization.load(AtomicOrderding::Relaxed) + coin.conf.requires_notarization.load(AtomicOrdering::Relaxed) } pub fn set_required_confirmations(coin: &UtxoCoinFields, confirmations: u64) { coin.conf .required_confirmations - .store(confirmations, AtomicOrderding::Relaxed); + .store(confirmations, AtomicOrdering::Relaxed); } pub fn set_requires_notarization(coin: &UtxoCoinFields, requires_nota: bool) { coin.conf .requires_notarization - .store(requires_nota, AtomicOrderding::Relaxed); + .store(requires_nota, AtomicOrdering::Relaxed); } -pub fn coin_protocol_info(coin: &UtxoCoinFields) -> Vec { - rmp_serde::to_vec(&coin.my_address.addr_format).expect("Serialization should not fail") +pub fn coin_protocol_info(coin: &T) -> Vec { + rmp_serde::to_vec(coin.addr_format()).expect("Serialization should not fail") } -pub fn is_coin_protocol_supported(coin: &UtxoCoinFields, info: &Option>) -> bool { +pub fn is_coin_protocol_supported(coin: &T, info: &Option>) -> bool { match info { Some(format) => rmp_serde::from_read_ref::<_, UtxoAddressFormat>(format).is_ok(), - None => !coin.my_address.addr_format.is_segwit(), + None => !coin.addr_format().is_segwit(), } } -#[allow(clippy::needless_lifetimes)] -pub async fn ordered_mature_unspents<'a, T>( +/// [`GetUtxoListOps::get_mature_unspent_ordered_list`] implementation. +/// Returns available mature and immature unspents in ascending order +/// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). +pub async fn get_mature_unspent_ordered_list<'a, T>( coin: &'a T, address: &Address, -) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> +) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'a>)> +where + T: UtxoCommonOps + GetUtxoListOps, +{ + let (unspents, recently_spent) = coin.get_all_unspent_ordered_list(address).await?; + let mature_unspents = identify_mature_unspents(coin, unspents).await?; + Ok((mature_unspents, recently_spent)) +} + +/// [`GetUtxoMapOps::get_mature_unspent_ordered_map`] implementation. +/// Returns available mature and immature unspents in ascending order for every given `addresses` +/// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). +pub async fn get_mature_unspent_ordered_map( + coin: &T, + addresses: Vec
, +) -> UtxoRpcResult<(MatureUnspentMap, RecentlySpentOutPointsGuard<'_>)> where - T: AsRef + UtxoCommonOps, + T: UtxoCommonOps + GetUtxoMapOps, { + let (unspents_map, recently_spent) = coin.get_all_unspent_ordered_map(addresses).await?; + // Get an iterator of futures: `Future>` + let fut_it = unspents_map.into_iter().map(|(address, unspents)| { + identify_mature_unspents(coin, unspents).map(|res| -> UtxoRpcResult<(Address, MatureUnspentList)> { + let mature_unspents = res?; + Ok((address, mature_unspents)) + }) + }); + // Poll the `fut_it` futures concurrently. + let result_map = futures::future::try_join_all(fut_it).await?.into_iter().collect(); + Ok((result_map, recently_spent)) +} + +/// Splits the given `unspents` outputs into mature and immature. +pub async fn identify_mature_unspents(coin: &T, unspents: Vec) -> UtxoRpcResult +where + T: UtxoCommonOps, +{ + /// Returns `true` if the given transaction has a known non-zero height. + fn can_tx_be_cached(tx: &RpcTransaction) -> bool { tx.height > Some(0) } + + /// Calculates actual confirmations number of the given `tx` transaction loaded from cache. fn calc_actual_cached_tx_confirmations(tx: &RpcTransaction, block_count: u64) -> UtxoRpcResult { let tx_height = tx.height.or_mm_err(|| { UtxoRpcError::Internal(format!(r#"Warning, height of cached "{:?}" tx is unknown"#, tx.txid)) })?; - // utxo_common::cache_transaction_if_possible() shouldn't cache transaction with height == 0 + // There shouldn't be cached transactions with height == 0 if tx_height == 0 { let error = format!( r#"Warning, height of cached "{:?}" tx is expected to be non-zero"#, @@ -2362,24 +2906,31 @@ where Ok(confirmations as u32) } - let (unspents, recently_spent) = list_unspent_ordered(coin, address).await?; let block_count = coin.as_ref().rpc_client.get_block_count().compat().await?; - let mut result = Vec::with_capacity(unspents.len()); + let to_verbose: HashSet = unspents + .iter() + .map(|unspent| unspent.outpoint.hash.reversed().into()) + .collect(); + let verbose_txs = coin + .get_verbose_transactions_from_cache_or_rpc(to_verbose) + .compat() + .await?; + // Transactions that should be cached. + let mut txs_to_cache = HashMap::with_capacity(verbose_txs.len()); + + let mut result = MatureUnspentList::with_capacity(unspents.len()); for unspent in unspents { let tx_hash: H256Json = unspent.outpoint.hash.reversed().into(); - let tx_info = match coin - .get_verbose_transaction_from_cache_or_rpc(tx_hash.clone()) - .compat() - .await - { - Ok(x) => x, - Err(err) => { - log!("Error " [err] " getting the transaction " [tx_hash] ", skip the unspent output"); - continue; - }, - }; - + let tx_info = verbose_txs + .get(&tx_hash) + .or_mm_err(|| { + UtxoRpcError::Internal(format!( + "'get_verbose_transactions_from_cache_or_rpc' should have returned '{:?}'", + tx_hash + )) + })? + .clone(); let tx_info = match tx_info { VerboseTransactionFrom::Cache(mut tx) => { if unspent.height.is_some() { @@ -2389,7 +2940,7 @@ where Ok(conf) => tx.confirmations = conf, // do not skip the transaction with unknown confirmations, // because the transaction can be matured - Err(e) => log!((e)), + Err(e) => error!("{}", e), } tx }, @@ -2397,19 +2948,25 @@ where if tx.height.is_none() { tx.height = unspent.height; } - if let Err(e) = coin.cache_transaction_if_possible(&tx).await { - log!((e)); + if can_tx_be_cached(&tx) { + txs_to_cache.insert(tx_hash, tx.clone()); } tx }, }; if coin.is_unspent_mature(&tx_info) { - result.push(unspent); + result.mature.push(unspent); + } else { + result.immature.push(unspent); } } - Ok((result, recently_spent)) + coin.as_ref() + .tx_cache + .cache_transactions_concurrently(&txs_to_cache) + .await; + Ok(result) } pub fn is_unspent_mature(mature_confirmations: u32, output: &RpcTransaction) -> bool { @@ -2417,107 +2974,70 @@ pub fn is_unspent_mature(mature_confirmations: u32, output: &RpcTransaction) -> !output.is_coinbase() || output.confirmations >= mature_confirmations } -#[cfg(not(target_arch = "wasm32"))] -pub async fn get_verbose_transaction_from_cache_or_rpc( +/// [`UtxoCommonOps::get_verbose_transactions_from_cache_or_rpc`] implementation. +/// Loads verbose transactions from cache or requests it using RPC client. +pub async fn get_verbose_transactions_from_cache_or_rpc( coin: &UtxoCoinFields, - txid: H256Json, -) -> Result { - let tx_cache_path = match &coin.tx_cache_directory { - Some(p) => p.clone(), - _ => { - // the coin doesn't support TX local cache, don't try to load from cache and don't cache it - let tx = try_s!(coin.rpc_client.get_verbose_transaction(txid.clone()).compat().await); - return Ok(VerboseTransactionFrom::Rpc(tx)); - }, - }; - - match tx_cache::load_transaction_from_cache(&tx_cache_path, &txid).await { - Ok(Some(tx)) => return Ok(VerboseTransactionFrom::Cache(tx)), - Err(err) => log!("Error " [err] " loading the " [txid] " transaction. Try request tx using Rpc client"), - // txid just not found - _ => (), + tx_ids: HashSet, +) -> UtxoRpcResult> { + /// Determines whether the transaction is needed to be requested through RPC or not. + /// Puts the inner `RpcTransaction` transaction into `result_map` if it has been loaded successfully, + /// otherwise puts `txid` into `to_request`. + fn on_cached_transaction_result( + result_map: &mut HashMap, + to_request: &mut Vec, + txid: H256Json, + res: TxCacheResult>, + ) { + match res { + Ok(Some(tx)) => { + result_map.insert(txid, VerboseTransactionFrom::Cache(tx)); + }, + // txid not found + Ok(None) => { + to_request.push(txid); + }, + Err(err) => { + error!( + "Error loading the {:?} transaction: {:?}. Trying to request tx using RPC client", + err, txid + ); + to_request.push(txid); + }, + } } - let tx = try_s!(coin.rpc_client.get_verbose_transaction(txid).compat().await); - Ok(VerboseTransactionFrom::Rpc(tx)) -} + let mut result_map = HashMap::with_capacity(tx_ids.len()); + let mut to_request = Vec::with_capacity(tx_ids.len()); -#[cfg(target_arch = "wasm32")] -pub async fn get_verbose_transaction_from_cache_or_rpc( - coin: &UtxoCoinFields, - txid: H256Json, -) -> Result { - let tx = try_s!(coin.rpc_client.get_verbose_transaction(txid.clone()).compat().await); - Ok(VerboseTransactionFrom::Rpc(tx)) -} - -#[cfg(not(target_arch = "wasm32"))] -pub async fn cache_transaction_if_possible(coin: &UtxoCoinFields, tx: &RpcTransaction) -> Result<(), String> { - let tx_cache_path = match &coin.tx_cache_directory { - Some(p) => p.clone(), - _ => { - return Ok(()); - }, - }; - // check if the transaction height is set and not zero - match tx.height { - Some(0) => return Ok(()), - Some(_) => (), - None => return Ok(()), - } - - tx_cache::cache_transaction(&tx_cache_path, tx) + coin.tx_cache + .load_transactions_from_cache_concurrently(tx_ids) .await - .map_err(|e| ERRL!("Error {:?} on caching transaction {:?}", e, tx.txid)) -} - -#[cfg(target_arch = "wasm32")] -pub async fn cache_transaction_if_possible(_coin: &UtxoCoinFields, _tx: &RpcTransaction) -> Result<(), String> { - Ok(()) -} - -pub async fn my_unspendable_balance(coin: &T, total_balance: &BigDecimal) -> BalanceResult -where - T: AsRef + UtxoCommonOps + MarketCoinOps + ?Sized, -{ - let mut attempts = 0i32; - loop { - let (mature_unspents, _) = coin.ordered_mature_unspents(&coin.as_ref().my_address).await?; - let spendable_balance = mature_unspents.iter().fold(BigDecimal::zero(), |acc, x| { - acc + big_decimal_from_sat(x.value as i64, coin.as_ref().decimals) - }); - if total_balance >= &spendable_balance { - return Ok(total_balance - spendable_balance); - } - - if attempts == 2 { - let error = format!( - "Spendable balance {} greater than total balance {}", - spendable_balance, total_balance - ); - return MmError::err(BalanceError::Internal(error)); - } - - warn!( - "Attempt N{}: spendable balance {} greater than total balance {}", - attempts, spendable_balance, total_balance - ); + .into_iter() + .for_each(|(txid, res)| on_cached_transaction_result(&mut result_map, &mut to_request, txid, res)); - // the balance could be changed by other instance between my_balance() and ordered_mature_unspents() calls - // try again - attempts += 1; - Timer::sleep(0.3).await; - } + result_map.extend( + coin.rpc_client + .get_verbose_transactions(&to_request) + .compat() + .await? + .into_iter() + .map(|tx| (tx.txid, VerboseTransactionFrom::Rpc(tx))), + ); + Ok(result_map) } /// Swap contract address is not used by standard UTXO coins. +#[inline] pub fn swap_contract_address() -> Option { None } /// Convert satoshis to BigDecimal amount of coin units +#[inline] pub fn big_decimal_from_sat(satoshis: i64, decimals: u8) -> BigDecimal { BigDecimal::from(satoshis) / BigDecimal::from(10u64.pow(decimals as u32)) } +#[inline] pub fn big_decimal_from_sat_unsigned(satoshis: u64, decimals: u8) -> BigDecimal { BigDecimal::from(satoshis) / BigDecimal::from(10u64.pow(decimals as u32)) } @@ -2533,15 +3053,158 @@ pub fn address_from_raw_pubkey( Ok(Address { t_addr_prefix, prefix, - hash: try_s!(Public::from_slice(pub_key)).address_hash(), + hash: try_s!(Public::from_slice(pub_key)).address_hash().into(), checksum_type, hrp, addr_format, }) } +pub fn address_from_pubkey( + pub_key: &Public, + prefix: u8, + t_addr_prefix: u8, + checksum_type: ChecksumType, + hrp: Option, + addr_format: UtxoAddressFormat, +) -> Address { + Address { + t_addr_prefix, + prefix, + hash: pub_key.address_hash().into(), + checksum_type, + hrp, + addr_format, + } +} + +pub async fn validate_spv_proof( + coin: T, + tx: UtxoTx, + try_spv_proof_until: u64, +) -> Result<(), MmError> { + let client = match &coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(_) => return Ok(()), + UtxoRpcClientEnum::Electrum(electrum_client) => electrum_client, + }; + if tx.outputs.is_empty() { + return MmError::err(SPVError::InvalidVout); + } + + let (merkle_branch, block_header) = spv_proof_retry_pool(&coin, client, &tx, try_spv_proof_until).await?; + let raw_header = RawBlockHeader::new(block_header.raw().take())?; + let intermediate_nodes: Vec = merkle_branch + .merkle + .into_iter() + .map(|hash| hash.reversed().into()) + .collect(); + + let proof = SPVProof { + tx_id: tx.hash(), + vin: serialize_list(&tx.inputs).take(), + vout: serialize_list(&tx.outputs).take(), + index: merkle_branch.pos as u64, + confirming_header: block_header, + raw_header, + intermediate_nodes, + }; + + proof.validate().map_err(MmError::new) +} + +async fn spv_proof_retry_pool( + coin: &T, + client: &ElectrumClient, + tx: &UtxoTx, + try_spv_proof_until: u64, +) -> Result<(TxMerkleBranch, BlockHeader), MmError> { + let mut height: Option = None; + let mut merkle_branch: Option = None; + + loop { + if now_ms() / 1000 > try_spv_proof_until { + error!( + "Waited too long until {} for transaction {:?} to validate spv proof", + try_spv_proof_until, + tx.hash(), + ); + return Err(SPVError::Timeout.into()); + } + + if height.is_none() { + match get_tx_height(tx, client).await { + Ok(h) => height = Some(h), + Err(e) => { + debug!("`get_tx_height` returned an error {:?}", e); + error!("{:?} for tx {:?}", SPVError::InvalidHeight, tx); + }, + } + } + + if height.is_some() && merkle_branch.is_none() { + match client + .blockchain_transaction_get_merkle(tx.hash().reversed().into(), height.unwrap()) + .compat() + .await + { + Ok(m) => merkle_branch = Some(m), + Err(e) => { + debug!("`blockchain_transaction_get_merkle` returned an error {:?}", e); + error!( + "{:?} by tx: {:?}, height: {}", + SPVError::UnableToGetMerkle, + H256Json::from(tx.hash().reversed()), + height.unwrap() + ); + }, + } + } + + if height.is_some() && merkle_branch.is_some() { + match block_header_from_storage_or_rpc(&coin, height.unwrap(), &coin.as_ref().block_headers_storage, client) + .await + { + Ok(block_header) => { + return Ok((merkle_branch.unwrap(), block_header)); + }, + Err(e) => { + debug!("`block_header_from_storage_or_rpc` returned an error {:?}", e); + error!( + "{:?}, Received header likely not compatible with header format in mm2", + SPVError::UnableToGetHeader + ); + }, + } + } + + error!( + "Failed spv proof validation for transaction {:?}, retrying in {} seconds.", + tx.hash(), + TRY_SPV_PROOF_INTERVAL, + ); + + Timer::sleep(TRY_SPV_PROOF_INTERVAL as f64).await; + } +} + +pub async fn get_tx_height(tx: &UtxoTx, client: &ElectrumClient) -> Result> { + for output in tx.outputs.clone() { + let script_pubkey_str = hex::encode(electrum_script_hash(&output.script_pubkey)); + if let Ok(history) = client.scripthash_get_history(script_pubkey_str.as_str()).compat().await { + if let Some(item) = history + .into_iter() + .find(|item| item.tx_hash.reversed() == H256Json(*tx.hash()) && item.height > 0) + { + return Ok(item.height as u64); + } + } + } + MmError::err(GetTxHeightError::HeightNotFound) +} + #[allow(clippy::too_many_arguments)] -pub fn validate_payment( +#[cfg_attr(test, mockable)] +pub fn validate_payment( coin: T, tx: UtxoTx, output_index: usize, @@ -2550,10 +3213,9 @@ pub fn validate_payment( priv_bn_hash: &[u8], amount: BigDecimal, time_lock: u32, -) -> Box + Send> -where - T: AsRef + Send + Sync + 'static, -{ + try_spv_proof_until: u64, + confirmations: u64, +) -> Box + Send> { let amount = try_fus!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); let expected_redeem = payment_script(time_lock, priv_bn_hash, first_pub0, second_pub0); @@ -2563,7 +3225,7 @@ where let tx_from_rpc = match coin .as_ref() .rpc_client - .get_transaction_bytes(tx.hash().reversed().into()) + .get_transaction_bytes(&tx.hash().reversed().into()) .compat() .await { @@ -2577,7 +3239,7 @@ where ); }; attempts += 1; - log!("Error " [e] " getting the tx " [tx.tx_hash()] " from rpc"); + error!("Error getting tx {:?} from rpc: {:?}", tx.tx_hash(), e); Timer::sleep(10.).await; continue; }, @@ -2594,7 +3256,7 @@ where let expected_output = TransactionOutput { value: amount, - script_pubkey: Builder::build_p2sh(&dhash160(&expected_redeem)).into(), + script_pubkey: Builder::build_p2sh(&dhash160(&expected_redeem).into()).into(), }; let actual_output = tx.outputs.get(output_index); @@ -2605,7 +3267,17 @@ where expected_output ); } - return Ok(()); + + if !coin.as_ref().conf.enable_spv_proof { + return Ok(()); + } + + return match confirmations { + 0 => Ok(()), + _ => validate_spv_proof(coin, tx, try_spv_proof_until) + .await + .map_err(|e| format!("{:?}", e)), + }; } }; Box::new(fut.boxed().compat()) @@ -2625,7 +3297,7 @@ async fn search_for_swap_output_spend( let mut tx: UtxoTx = try_s!(deserialize(tx).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.tx_hash_algo; let script = payment_script(time_lock, secret_hash, first_pub, second_pub); - let expected_script_pubkey = Builder::build_p2sh(&dhash160(&script)).to_bytes(); + let expected_script_pubkey = Builder::build_p2sh(&dhash160(&script).into()).to_bytes(); if tx.outputs[0].script_pubkey != expected_script_pubkey { return ERR!( "Transaction {:?} output 0 script_pubkey doesn't match expected {:?}", @@ -2636,12 +3308,18 @@ async fn search_for_swap_output_spend( let spend = try_s!( coin.rpc_client - .find_output_spend(&tx, output_index, search_from_block) + .find_output_spend( + tx.hash(), + &tx.outputs[output_index].script_pubkey, + output_index, + BlockHashOrHeight::Height(search_from_block as i64) + ) .compat() .await ); match spend { - Some(mut tx) => { + Some(spent_output_info) => { + let mut tx = spent_output_info.spending_tx; tx.tx_hash_algo = coin.tx_hash_algo; let script: Script = tx.inputs[0].script_sig.clone().into(); if let Some(Ok(ref i)) = script.iter().nth(2) { @@ -2673,6 +3351,7 @@ struct SwapPaymentOutputsResult { fn generate_swap_payment_outputs( coin: T, time_lock: u32, + my_pub: &[u8], other_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, @@ -2680,17 +3359,18 @@ fn generate_swap_payment_outputs( where T: AsRef, { + let my_public = try_s!(Public::from_slice(my_pub)); let redeem_script = payment_script( time_lock, secret_hash, - coin.as_ref().key_pair.public(), + &my_public, &try_s!(Public::from_slice(other_pub)), ); let redeem_script_hash = dhash160(&redeem_script); let amount = try_s!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); let htlc_out = TransactionOutput { value: amount, - script_pubkey: Builder::build_p2sh(&redeem_script_hash).into(), + script_pubkey: Builder::build_p2sh(&redeem_script_hash.into()).into(), }; // record secret hash to blockchain too making it impossible to lose // lock time may be easily brute forced so it is not mandatory to record it @@ -2712,7 +3392,7 @@ where let payment_address = Address { checksum_type: coin.as_ref().conf.checksum_type, - hash: redeem_script_hash, + hash: redeem_script_hash.into(), prefix: coin.as_ref().conf.p2sh_addr_prefix, t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, hrp: coin.as_ref().conf.bech32_hrp.clone(), @@ -2765,73 +3445,92 @@ pub fn dex_fee_script(uuid: [u8; 16], time_lock: u32, watcher_pub: &Public, send .into_script() } -/// Creates signed input spending hash time locked p2sh output -pub fn p2sh_spend( - signer: &TransactionInputSigner, - input_index: usize, - key_pair: &KeyPair, - script_data: Script, - redeem_script: Script, - signature_version: SignatureVersion, - fork_id: u32, -) -> Result { - let sighash = signer.signature_hash( - input_index, - signer.inputs[input_index].amount, - &redeem_script, - signature_version, - 1 | fork_id, - ); - - let sig = try_s!(script_sig(&sighash, key_pair, fork_id)); - - let mut resulting_script = Builder::default().push_data(&sig).into_bytes(); - if !script_data.is_empty() { - resulting_script.extend_from_slice(&script_data); +/// [`GetUtxoListOps::get_unspent_ordered_list`] implementation. +/// Returns available unspents in ascending order +/// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). +pub async fn get_unspent_ordered_list<'a, T>( + coin: &'a T, + address: &Address, +) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'a>)> +where + T: UtxoCommonOps + GetUtxoListOps, +{ + if coin.as_ref().check_utxo_maturity { + coin.get_mature_unspent_ordered_list(address) + .await + // Convert `MatureUnspentList` into `Vec` by discarding immature unspents. + .map(|(mature_unspents, recently_spent)| (mature_unspents.only_mature(), recently_spent)) + } else { + coin.get_all_unspent_ordered_list(address).await } +} - let redeem_part = Builder::default().push_data(&redeem_script).into_bytes(); - resulting_script.extend_from_slice(&redeem_part); - - Ok(TransactionInput { - script_sig: resulting_script, - sequence: signer.inputs[input_index].sequence, - script_witness: vec![], - previous_output: signer.inputs[input_index].previous_output.clone(), - }) +/// [`GetUtxoMapOps::get_unspent_ordered_map`] implementation. +/// Returns available unspents in ascending order + `RecentlySpentOutPoints` MutexGuard for further interaction +/// (e.g. to add new transaction to it). +pub async fn get_unspent_ordered_map( + coin: &T, + addresses: Vec
, +) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> +where + T: UtxoCommonOps + GetUtxoMapOps, +{ + if coin.as_ref().check_utxo_maturity { + coin.get_mature_unspent_ordered_map(addresses) + .await + // Convert `MatureUnspentMap` into `UnspentMap` by discarding immature unspents. + .map(|(mature_unspents_map, recently_spent)| { + let unspents_map = mature_unspents_map + .into_iter() + .map(|(address, unspents)| (address, unspents.only_mature())) + .collect(); + (unspents_map, recently_spent) + }) + } else { + coin.get_all_unspent_ordered_map(addresses).await + } } -#[allow(clippy::needless_lifetimes)] -pub async fn list_unspent_ordered<'a, T>( +/// [`GetUtxoListOps::get_all_unspent_ordered_list`] implementation. +/// Returns available mature and immature unspents in ascending +/// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). +pub async fn get_all_unspent_ordered_list<'a, T: UtxoCommonOps>( coin: &'a T, address: &Address, -) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> -where - T: AsRef, -{ +) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'a>)> { let decimals = coin.as_ref().decimals; - let mut unspents = coin + let unspents = coin .as_ref() .rpc_client .list_unspent(address, decimals) .compat() .await?; let recently_spent = coin.as_ref().recently_spent_outpoints.lock().await; - unspents = recently_spent - .replace_spent_outputs_with_cache(unspents.into_iter().collect()) - .into_iter() - .collect(); - unspents.sort_unstable_by(|a, b| { - if a.value < b.value { - Ordering::Less - } else { - Ordering::Greater - } - }); - // dedup just in case we add duplicates of same unspent out - // all duplicates will be removed because vector in sorted before dedup - unspents.dedup_by(|one, another| one.outpoint == another.outpoint); - Ok((unspents, recently_spent)) + let unordered_unspents = recently_spent.replace_spent_outputs_with_cache(unspents.into_iter().collect()); + let ordered_unspents = sort_dedup_unspents(unordered_unspents); + Ok((ordered_unspents, recently_spent)) +} + +/// [`GetUtxoMapOps::get_all_unspent_ordered_map`] implementation. +/// Returns available mature and immature unspents in ascending order for every given `addresses` +/// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). +pub async fn get_all_unspent_ordered_map( + coin: &T, + addresses: Vec
, +) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> { + let decimals = coin.as_ref().decimals; + let mut unspents_map = coin + .as_ref() + .rpc_client + .list_unspent_group(addresses, decimals) + .compat() + .await?; + let recently_spent = coin.as_ref().recently_spent_outpoints.lock().await; + for (_address, unspents) in unspents_map.iter_mut() { + let unordered_unspents = recently_spent.replace_spent_outputs_with_cache(unspents.iter().cloned().collect()); + *unspents = sort_dedup_unspents(unordered_unspents); + } + Ok((unspents_map, recently_spent)) } /// Increase the given `dynamic_fee` according to the fee approximation `stage` using the [`UtxoCoinFields::tx_fee_volatility_percent`]. @@ -2862,23 +3561,158 @@ fn increase_by_percent(num: u64, percent: f64) -> u64 { num + (percent.round() as u64) } -async fn merge_utxo_loop(weak: UtxoWeak, merge_at: usize, check_every: f64, max_merge_at_once: usize) +pub async fn valid_block_header_from_storage( + coin: &T, + height: u64, + storage: &BlockHeaderStorage, + client: &ElectrumClient, +) -> Result> where - T: From + AsRef + UtxoCommonOps, + T: AsRef, +{ + match storage + .get_block_header(coin.as_ref().conf.ticker.as_str(), height) + .await? + { + None => { + let bytes = client.blockchain_block_header(height).compat().await?; + let header: BlockHeader = deserialize(bytes.0.as_slice())?; + let params = &storage.params; + let blocks_limit = params.blocks_limit_to_check; + let (headers_registry, headers) = client.retrieve_last_headers(blocks_limit, height).compat().await?; + match spv_validation::helpers_validation::validate_headers( + headers, + params.difficulty_check, + params.constant_difficulty, + ) { + Ok(_) => { + storage + .add_block_headers_to_storage(coin.as_ref().conf.ticker.as_str(), headers_registry) + .await?; + Ok(header) + }, + Err(err) => MmError::err(GetBlockHeaderError::SPVError(err)), + } + }, + Some(header) => Ok(header), + } +} + +#[inline] +pub async fn block_header_from_storage_or_rpc( + coin: &T, + height: u64, + storage: &Option, + client: &ElectrumClient, +) -> Result> +where + T: AsRef, +{ + match storage { + Some(ref storage) => valid_block_header_from_storage(&coin, height, storage, client).await, + None => Ok(deserialize( + client.blockchain_block_header(height).compat().await?.as_slice(), + )?), + } +} + +pub async fn block_header_utxo_loop(weak: UtxoWeak, constructor: impl Fn(UtxoArc) -> T) { + { + let coin = match weak.upgrade() { + Some(arc) => constructor(arc), + None => return, + }; + let ticker = coin.as_ref().conf.ticker.as_str(); + let storage = match &coin.as_ref().block_headers_storage { + None => return, + Some(storage) => storage, + }; + match storage.is_initialized_for(ticker).await { + Ok(true) => info!("Block Header Storage already initialized for {}", ticker), + Ok(false) => { + if let Err(e) = storage.init(ticker).await { + error!( + "Couldn't initiate storage - aborting the block_header_utxo_loop: {:?}", + e + ); + return; + } + info!("Block Header Storage successfully initialized for {}", ticker); + }, + Err(_e) => return, + }; + } + while let Some(arc) = weak.upgrade() { + let coin = constructor(arc); + let storage = match &coin.as_ref().block_headers_storage { + None => break, + Some(storage) => storage, + }; + let params = storage.params.clone(); + let (check_every, blocks_limit_to_check, difficulty_check, constant_difficulty) = ( + params.check_every, + params.blocks_limit_to_check, + params.difficulty_check, + params.constant_difficulty, + ); + let height = + ok_or_continue_after_sleep!(coin.as_ref().rpc_client.get_block_count().compat().await, check_every); + let client = match &coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(_) => break, + UtxoRpcClientEnum::Electrum(client) => client, + }; + let (block_registry, block_headers) = ok_or_continue_after_sleep!( + client + .retrieve_last_headers(blocks_limit_to_check, height) + .compat() + .await, + check_every + ); + ok_or_continue_after_sleep!( + validate_headers(block_headers, difficulty_check, constant_difficulty), + check_every + ); + + let ticker = coin.as_ref().conf.ticker.as_str(); + ok_or_continue_after_sleep!( + storage.add_block_headers_to_storage(ticker, block_registry).await, + check_every + ); + debug!("tick block_header_utxo_loop for {}", coin.as_ref().conf.ticker); + Timer::sleep(check_every).await; + } +} + +pub async fn merge_utxo_loop( + weak: UtxoWeak, + merge_at: usize, + check_every: f64, + max_merge_at_once: usize, + constructor: impl Fn(UtxoArc) -> T, +) where + T: UtxoCommonOps + GetUtxoListOps, { loop { Timer::sleep(check_every).await; let coin = match weak.upgrade() { - Some(arc) => T::from(arc), + Some(arc) => constructor(arc), None => break, }; + let my_address = match coin.as_ref().derivation_method { + DerivationMethod::Iguana(ref my_address) => my_address, + DerivationMethod::HDWallet(_) => { + warn!("'merge_utxo_loop' is currently not used for HD wallets"); + return; + }, + }; + let ticker = &coin.as_ref().conf.ticker; - let (unspents, recently_spent) = match coin.list_unspent_ordered(&coin.as_ref().my_address).await { + let (unspents, recently_spent) = match coin.get_unspent_ordered_list(my_address).await { Ok((unspents, recently_spent)) => (unspents, recently_spent), Err(e) => { - error!("Error {} on list_unspent_ordered of coin {}", e, ticker); + error!("Error {} on get_unspent_ordered_list of coin {}", e, ticker); continue; }, }; @@ -2886,14 +3720,15 @@ where let unspents: Vec<_> = unspents.into_iter().take(max_merge_at_once).collect(); info!("Trying to merge {} UTXOs of coin {}", unspents.len(), ticker); let value = unspents.iter().fold(0, |sum, unspent| sum + unspent.value); - let script_pubkey = Builder::build_p2pkh(&coin.as_ref().my_address.hash).to_bytes(); + let script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); let output = TransactionOutput { value, script_pubkey }; let merge_tx_fut = generate_and_send_tx( &coin, unspents, - vec![output], + None, FeePolicy::DeductFromOutput(0), recently_spent, + vec![output], ); match merge_tx_fut.await { Ok(tx) => info!( @@ -2901,7 +3736,7 @@ where ticker, tx.hash().reversed() ), - Err(e) => error!("Error {} on UTXO merge attempt for coin {}", e, ticker), + Err(e) => error!("Error {:?} on UTXO merge attempt for coin {}", e, ticker), } } } @@ -2940,6 +3775,13 @@ where Ok(lock_time.max(htlc_locktime)) } +pub fn addr_format(coin: &dyn AsRef) -> &UtxoAddressFormat { + match coin.as_ref().derivation_method { + DerivationMethod::Iguana(ref my_address) => &my_address.addr_format, + DerivationMethod::HDWallet(UtxoHDWallet { ref address_format, .. }) => address_format, + } +} + pub fn addr_format_for_standard_scripts(coin: &dyn AsRef) -> UtxoAddressFormat { match &coin.as_ref().conf.default_address_format { UtxoAddressFormat::Segwit => UtxoAddressFormat::Standard, @@ -2947,6 +3789,90 @@ pub fn addr_format_for_standard_scripts(coin: &dyn AsRef) -> Utx } } +fn check_withdraw_address_supported(coin: &T, addr: &Address) -> Result<(), MmError> +where + T: UtxoCommonOps, +{ + let conf = &coin.as_ref().conf; + + match addr.addr_format { + // Considering that legacy is supported with any configured formats + // This can be changed depending on the coins implementation + UtxoAddressFormat::Standard => { + let is_p2pkh = addr.prefix == conf.pub_addr_prefix && addr.t_addr_prefix == conf.pub_t_addr_prefix; + let is_p2sh = addr.prefix == conf.p2sh_addr_prefix && addr.t_addr_prefix == conf.p2sh_t_addr_prefix; + if !is_p2pkh && !is_p2sh { + MmError::err(UnsupportedAddr::PrefixError(conf.ticker.clone())) + } else { + Ok(()) + } + }, + UtxoAddressFormat::Segwit => { + if !conf.segwit { + return MmError::err(UnsupportedAddr::SegwitNotActivated(conf.ticker.clone())); + } + + if addr.hrp != conf.bech32_hrp { + MmError::err(UnsupportedAddr::HrpError { + ticker: conf.ticker.clone(), + hrp: addr.hrp.clone().unwrap_or_default(), + }) + } else { + Ok(()) + } + }, + UtxoAddressFormat::CashAddress { .. } => { + if addr.addr_format == conf.default_address_format || addr.addr_format == *coin.addr_format() { + Ok(()) + } else { + MmError::err(UnsupportedAddr::FormatMismatch { + ticker: conf.ticker.clone(), + activated_format: coin.addr_format().to_string(), + used_format: addr.addr_format.to_string(), + }) + } + }, + } +} + +pub async fn broadcast_tx(coin: &T, tx: &UtxoTx) -> Result> +where + T: AsRef, +{ + coin.as_ref() + .rpc_client + .send_transaction(tx) + .compat() + .await + .mm_err(From::from) +} + +pub fn derive_htlc_key_pair(coin: &UtxoCoinFields, _swap_unique_data: &[u8]) -> KeyPair { + match coin.priv_key_policy { + PrivKeyPolicy::KeyPair(k) => k, + PrivKeyPolicy::Trezor => todo!(), + } +} + +/// Sorts and deduplicates the given `unspents` in ascending order. +fn sort_dedup_unspents(unspents: I) -> Vec +where + I: IntoIterator, +{ + unspents + .into_iter() + // dedup just in case we add duplicates of same unspent out + .unique_by(|unspent| unspent.outpoint) + .sorted_unstable_by(|a, b| { + if a.value < b.value { + Ordering::Less + } else { + Ordering::Greater + } + }) + .collect() +} + #[test] fn test_increase_by_percent() { assert_eq!(increase_by_percent(4300, 1.), 4343); diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs new file mode 100644 index 0000000000..94613eb5c8 --- /dev/null +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -0,0 +1,50 @@ +use super::*; +use crate::utxo::rpc_clients::{ElectrumClient, UtxoRpcClientOps}; +use common::jsonrpc_client::JsonRpcErrorType; +use std::convert::TryFrom; + +pub async fn test_electrum_display_balances(rpc_client: &ElectrumClient) { + let addresses = vec![ + "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), + "RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), + "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), + "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), + ]; + let actual = rpc_client.display_balances(addresses, 8).compat().await.unwrap(); + + let expected: Vec<(Address, BigDecimal)> = vec![ + ( + "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), + BigDecimal::try_from(5.77699).unwrap(), + ), + ("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), BigDecimal::from(0)), + ( + "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), + BigDecimal::try_from(0.77699).unwrap(), + ), + ( + "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), + BigDecimal::try_from(16.55398).unwrap(), + ), + ]; + assert_eq!(actual, expected); + + let invalid_hashes = vec![ + "0128a4ea8c5775039d39a192f8490b35b416f2f194cb6b6ee91a41d01233c3b5".to_owned(), + "!INVALID!".to_owned(), + "457206aa039ed77b223e4623c19152f9aa63aa7845fe93633920607500766931".to_owned(), + ]; + + let rpc_err = rpc_client + .scripthash_get_balances(invalid_hashes) + .compat() + .await + .unwrap_err(); + match rpc_err.error { + JsonRpcErrorType::Response(_, json_err) => { + let expected = json!({"code": 1, "message": "!INVALID! is not a valid script hash"}); + assert_eq!(json_err, expected); + }, + ekind => panic!("Unexpected `JsonRpcErrorType`: {:?}", ekind), + } +} diff --git a/mm2src/coins/utxo/utxo_indexedb_block_header_storage.rs b/mm2src/coins/utxo/utxo_indexedb_block_header_storage.rs new file mode 100644 index 0000000000..d00d4f261f --- /dev/null +++ b/mm2src/coins/utxo/utxo_indexedb_block_header_storage.rs @@ -0,0 +1,49 @@ +use crate::utxo::rpc_clients::ElectrumBlockHeader; +use crate::utxo::utxo_block_header_storage::BlockHeaderStorageError; +use crate::utxo::utxo_block_header_storage::BlockHeaderStorageOps; +use async_trait::async_trait; +use chain::BlockHeader; +use mm2_err_handle::prelude::*; +use std::collections::HashMap; + +#[derive(Debug)] +pub struct IndexedDBBlockHeadersStorage {} + +#[async_trait] +impl BlockHeaderStorageOps for IndexedDBBlockHeadersStorage { + async fn init(&self, _for_coin: &str) -> Result<(), MmError> { Ok(()) } + + async fn is_initialized_for(&self, _for_coin: &str) -> Result> { Ok(true) } + + async fn add_electrum_block_headers_to_storage( + &self, + _for_coin: &str, + _headers: Vec, + ) -> Result<(), MmError> { + Ok(()) + } + + async fn add_block_headers_to_storage( + &self, + _for_coin: &str, + _headers: HashMap, + ) -> Result<(), MmError> { + Ok(()) + } + + async fn get_block_header( + &self, + _for_coin: &str, + _height: u64, + ) -> Result, MmError> { + Ok(None) + } + + async fn get_block_header_raw( + &self, + _for_coin: &str, + _height: u64, + ) -> Result, MmError> { + Ok(None) + } +} diff --git a/mm2src/coins/utxo/utxo_sql_block_header_storage.rs b/mm2src/coins/utxo/utxo_sql_block_header_storage.rs new file mode 100644 index 0000000000..d11b794e54 --- /dev/null +++ b/mm2src/coins/utxo/utxo_sql_block_header_storage.rs @@ -0,0 +1,324 @@ +use crate::utxo::rpc_clients::ElectrumBlockHeader; +use crate::utxo::utxo_block_header_storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; +use async_trait::async_trait; +use chain::BlockHeader; +use common::async_blocking; +use db_common::{sqlite::rusqlite::Error as SqlError, + sqlite::rusqlite::{Connection, Row, ToSql, NO_PARAMS}, + sqlite::string_from_row, + sqlite::validate_table_name, + sqlite::CHECK_TABLE_EXISTS_SQL}; +use mm2_err_handle::prelude::*; +use serialization::deserialize; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +fn block_headers_cache_table(ticker: &str) -> String { ticker.to_owned() + "_block_headers_cache" } + +fn get_table_name_and_validate(for_coin: &str) -> Result> { + let table_name = block_headers_cache_table(for_coin); + validate_table_name(&table_name).map_err(|e| BlockHeaderStorageError::CantRetrieveTableError { + ticker: for_coin.to_string(), + reason: e.to_string(), + })?; + Ok(table_name) +} + +fn create_block_header_cache_table_sql(for_coin: &str) -> Result> { + let table_name = get_table_name_and_validate(for_coin)?; + let sql = format!( + "CREATE TABLE IF NOT EXISTS {} ( + block_height INTEGER NOT NULL UNIQUE, + hex TEXT NOT NULL + );", + table_name + ); + + Ok(sql) +} + +fn insert_block_header_in_cache_sql(for_coin: &str) -> Result> { + let table_name = get_table_name_and_validate(for_coin)?; + // Always update the block headers with new values just in case a chain reorganization occurs. + let sql = format!( + "INSERT OR REPLACE INTO {} (block_height, hex) VALUES (?1, ?2);", + table_name + ); + Ok(sql) +} + +fn get_block_header_by_height(for_coin: &str) -> Result> { + let table_name = get_table_name_and_validate(for_coin)?; + let sql = format!("SELECT hex FROM {} WHERE block_height=?1;", table_name); + + Ok(sql) +} + +#[derive(Clone, Debug)] +pub struct SqliteBlockHeadersStorage(pub Arc>); + +fn query_single_row( + conn: &Connection, + query: &str, + params: P, + map_fn: F, +) -> Result, MmError> +where + P: IntoIterator, + P::Item: ToSql, + F: FnOnce(&Row<'_>) -> Result, +{ + db_common::sqlite::query_single_row(conn, query, params, map_fn).map_err(|e| { + MmError::new(BlockHeaderStorageError::QueryError { + query: query.to_string(), + reason: e.to_string(), + }) + }) +} + +struct SqlBlockHeader { + block_height: String, + block_hex: String, +} + +impl From for SqlBlockHeader { + fn from(header: ElectrumBlockHeader) -> Self { + match header { + ElectrumBlockHeader::V12(h) => { + let block_hex = h.as_hex(); + let block_height = h.block_height.to_string(); + SqlBlockHeader { + block_height, + block_hex, + } + }, + ElectrumBlockHeader::V14(h) => { + let block_hex = format!("{:02x}", h.hex); + let block_height = h.height.to_string(); + SqlBlockHeader { + block_height, + block_hex, + } + }, + } + } +} +async fn common_headers_insert( + for_coin: &str, + storage: SqliteBlockHeadersStorage, + headers: Vec, +) -> Result<(), MmError> { + let for_coin = for_coin.to_owned(); + let mut conn = storage.0.lock().unwrap(); + let sql_transaction = conn + .transaction() + .map_err(|e| BlockHeaderStorageError::AddToStorageError { + ticker: for_coin.to_string(), + reason: e.to_string(), + })?; + for header in headers { + let block_cache_params = [&header.block_height, &header.block_hex]; + sql_transaction + .execute(&insert_block_header_in_cache_sql(&for_coin)?, block_cache_params) + .map_err(|e| BlockHeaderStorageError::AddToStorageError { + ticker: for_coin.to_string(), + reason: e.to_string(), + })?; + } + sql_transaction + .commit() + .map_err(|e| BlockHeaderStorageError::AddToStorageError { + ticker: for_coin.to_string(), + reason: e.to_string(), + })?; + Ok(()) +} + +#[async_trait] +impl BlockHeaderStorageOps for SqliteBlockHeadersStorage { + async fn init(&self, for_coin: &str) -> Result<(), MmError> { + let selfi = self.clone(); + let sql_cache = create_block_header_cache_table_sql(for_coin)?; + let ticker = for_coin.to_owned(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + conn.execute(&sql_cache, NO_PARAMS).map(|_| ()).map_err(|e| { + BlockHeaderStorageError::InitializationError { + ticker, + reason: e.to_string(), + } + })?; + Ok(()) + }) + .await + } + + async fn is_initialized_for(&self, for_coin: &str) -> Result> { + let block_headers_cache_table = get_table_name_and_validate(for_coin)?; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let cache_initialized = query_single_row( + &conn, + CHECK_TABLE_EXISTS_SQL, + [block_headers_cache_table], + string_from_row, + )?; + Ok(cache_initialized.is_some()) + }) + .await + } + + async fn add_electrum_block_headers_to_storage( + &self, + for_coin: &str, + headers: Vec, + ) -> Result<(), MmError> { + let headers_for_sql = headers.into_iter().map(Into::into).collect(); + common_headers_insert(for_coin, self.clone(), headers_for_sql).await + } + + async fn add_block_headers_to_storage( + &self, + for_coin: &str, + headers: HashMap, + ) -> Result<(), MmError> { + let headers_for_sql = headers + .into_iter() + .map(|(height, header)| SqlBlockHeader { + block_height: height.to_string(), + block_hex: hex::encode(header.raw()), + }) + .collect(); + common_headers_insert(for_coin, self.clone(), headers_for_sql).await + } + + async fn get_block_header( + &self, + for_coin: &str, + height: u64, + ) -> Result, MmError> { + if let Some(header_raw) = self.get_block_header_raw(for_coin, height).await? { + let header_bytes = hex::decode(header_raw).map_err(|e| BlockHeaderStorageError::DecodeError { + ticker: for_coin.to_string(), + reason: e.to_string(), + })?; + let header: BlockHeader = + deserialize(header_bytes.as_slice()).map_err(|e| BlockHeaderStorageError::DecodeError { + ticker: for_coin.to_string(), + reason: e.to_string(), + })?; + return Ok(Some(header)); + } + Ok(None) + } + + async fn get_block_header_raw( + &self, + for_coin: &str, + height: u64, + ) -> Result, MmError> { + let params = [height.to_string()]; + let sql = get_block_header_by_height(for_coin)?; + let selfi = self.clone(); + + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + query_single_row(&conn, &sql, params, string_from_row) + }) + .await + .map_err(|e| { + MmError::new(BlockHeaderStorageError::GetFromStorageError { + ticker: for_coin.to_string(), + reason: e.into_inner().to_string(), + }) + }) + } +} + +#[cfg(test)] +impl SqliteBlockHeadersStorage { + pub fn in_memory() -> Self { + SqliteBlockHeadersStorage(Arc::new(Mutex::new(Connection::open_in_memory().unwrap()))) + } + + fn is_table_empty(&self, table_name: &str) -> bool { + validate_table_name(table_name).unwrap(); + let sql = "SELECT COUNT(block_height) FROM ".to_owned() + table_name + ";"; + let conn = self.0.lock().unwrap(); + let rows_count: u32 = conn.query_row(&sql, NO_PARAMS, |row| row.get(0)).unwrap(); + rows_count == 0 + } +} + +#[cfg(test)] +mod sql_block_headers_storage_tests { + use super::*; + use crate::utxo::rpc_clients::ElectrumBlockHeaderV14; + use common::block_on; + use primitives::hash::H256; + + #[test] + fn test_init_collection() { + let for_coin = "init_collection"; + let storage = SqliteBlockHeadersStorage::in_memory(); + let initialized = block_on(storage.is_initialized_for(for_coin)).unwrap(); + assert!(!initialized); + + block_on(storage.init(for_coin)).unwrap(); + // repetitive init must not fail + block_on(storage.init(for_coin)).unwrap(); + + let initialized = block_on(storage.is_initialized_for(for_coin)).unwrap(); + assert!(initialized); + } + + #[test] + fn test_add_block_headers() { + let for_coin = "insert"; + let storage = SqliteBlockHeadersStorage::in_memory(); + let table = block_headers_cache_table(for_coin); + block_on(storage.init(for_coin)).unwrap(); + + let initialized = block_on(storage.is_initialized_for(for_coin)).unwrap(); + assert!(initialized); + + let block_header = ElectrumBlockHeaderV14 { + height: 520481, + hex: "0000002076d41d3e4b0bfd4c0d3b30aa69fdff3ed35d85829efd04000000000000000000b386498b583390959d9bac72346986e3015e83ac0b54bc7747a11a494ac35c94bb3ce65a53fb45177f7e311c".into(), + }.into(); + let headers = vec![ElectrumBlockHeader::V14(block_header)]; + block_on(storage.add_electrum_block_headers_to_storage(for_coin, headers)).unwrap(); + assert!(!storage.is_table_empty(&table)); + } + + #[test] + fn test_get_block_header() { + let for_coin = "get"; + let storage = SqliteBlockHeadersStorage::in_memory(); + let table = block_headers_cache_table(for_coin); + block_on(storage.init(for_coin)).unwrap(); + + let initialized = block_on(storage.is_initialized_for(for_coin)).unwrap(); + assert!(initialized); + + let block_header = ElectrumBlockHeaderV14 { + height: 520481, + hex: "0000002076d41d3e4b0bfd4c0d3b30aa69fdff3ed35d85829efd04000000000000000000b386498b583390959d9bac72346986e3015e83ac0b54bc7747a11a494ac35c94bb3ce65a53fb45177f7e311c".into(), + }.into(); + let headers = vec![ElectrumBlockHeader::V14(block_header)]; + block_on(storage.add_electrum_block_headers_to_storage(for_coin, headers)).unwrap(); + assert!(!storage.is_table_empty(&table)); + + let hex = block_on(storage.get_block_header_raw(for_coin, 520481)) + .unwrap() + .unwrap(); + assert_eq!(hex, "0000002076d41d3e4b0bfd4c0d3b30aa69fdff3ed35d85829efd04000000000000000000b386498b583390959d9bac72346986e3015e83ac0b54bc7747a11a494ac35c94bb3ce65a53fb45177f7e311c".to_string()); + + let block_header = block_on(storage.get_block_header(for_coin, 520481)).unwrap().unwrap(); + assert_eq!( + block_header.hash(), + H256::from_reversed_str("0000000000000000002e31d0714a5ab23100945ff87ba2d856cd566a3c9344ec") + ) + } +} diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 36d3ade158..b48f9bfe59 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -1,10 +1,29 @@ use super::*; -use crate::{CanRefundHtlc, CoinBalance, NegotiateSwapContractAddrErr, SwapOps, TradePreimageValue, - ValidateAddressResult, WithdrawFut}; +use crate::coin_balance::{self, EnableCoinBalanceError, HDAccountBalance, HDAddressBalance, HDWalletBalance, + HDWalletBalanceOps}; +use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; +use crate::hd_wallet::{self, AccountUpdatingError, AddressDerivingError, GetNewHDAddressParams, + GetNewHDAddressResponse, HDAccountMut, HDWalletRpcError, HDWalletRpcOps, + NewAccountCreatingError}; +use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; +use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; +use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; +use crate::rpc_command::init_create_account::{self, CreateNewAccountParams, InitCreateHDAccountRpcOps}; +use crate::rpc_command::init_scan_for_new_addresses::{self, InitScanAddressesRpcOps, ScanAddressesParams, + ScanAddressesResponse}; +use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; +use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; +use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, GetWithdrawSenderAddress, + NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, SignatureResult, SwapOps, + TradePreimageValue, TransactionFut, ValidateAddressResult, ValidatePaymentInput, VerificationResult, + WithdrawFut, WithdrawSenderAddress}; use common::mm_metrics::MetricsArc; -use common::mm_number::MmNumber; +use crypto::trezor::utxo::TrezorUtxoCoin; +use crypto::Bip44Chain; use futures::{FutureExt, TryFutureExt}; -use serialization::CoinVariant; +use mm2_number::MmNumber; +use serialization::coin_variant_by_ticker; +use utxo_signer::UtxoSignerOps; #[derive(Clone, Debug)] pub struct UtxoStandardCoin { @@ -23,25 +42,111 @@ impl From for UtxoArc { fn from(coin: UtxoStandardCoin) -> Self { coin.utxo_arc } } -pub async fn utxo_standard_coin_from_conf_and_request( +pub async fn utxo_standard_coin_with_priv_key( ctx: &MmArc, ticker: &str, conf: &Json, - req: &Json, + activation_params: &UtxoActivationParams, priv_key: &[u8], ) -> Result { - let coin: UtxoStandardCoin = - try_s!(utxo_common::utxo_arc_from_conf_and_request(ctx, ticker, conf, req, priv_key).await); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(priv_key); + let coin = try_s!( + UtxoArcBuilder::new( + ctx, + ticker, + conf, + activation_params, + priv_key_policy, + UtxoStandardCoin::from + ) + .build() + .await + ); Ok(coin) } // if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt #[async_trait] #[cfg_attr(test, mockable)] -impl UtxoCommonOps for UtxoStandardCoin { - async fn get_tx_fee(&self) -> Result { utxo_common::get_tx_fee(&self.utxo_arc).await } +impl UtxoTxBroadcastOps for UtxoStandardCoin { + async fn broadcast_tx(&self, tx: &UtxoTx) -> Result> { + utxo_common::broadcast_tx(self, tx).await + } +} + +// if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt +#[async_trait] +#[cfg_attr(test, mockable)] +impl UtxoTxGenerationOps for UtxoStandardCoin { + async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + + async fn calc_interest_if_required( + &self, + unsigned: TransactionInputSigner, + data: AdditionalTxData, + my_script_pub: Bytes, + ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { + utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub).await + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoListOps for UtxoStandardCoin { + async fn get_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_unspent_ordered_list(self, address).await + } - async fn get_htlc_spend_fee(&self) -> UtxoRpcResult { utxo_common::get_htlc_spend_fee(self).await } + async fn get_all_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_list(self, address).await + } + + async fn get_mature_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_mature_unspent_ordered_list(self, address).await + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoMapOps for UtxoStandardCoin { + async fn get_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_unspent_ordered_map(self, addresses).await + } + + async fn get_all_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_map(self, addresses).await + } + + async fn get_mature_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(MatureUnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_mature_unspent_ordered_map(self, addresses).await + } +} + +// if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt +#[async_trait] +#[cfg_attr(test, mockable)] +impl UtxoCommonOps for UtxoStandardCoin { + async fn get_htlc_spend_fee(&self, tx_size: u64) -> UtxoRpcResult { + utxo_common::get_htlc_spend_fee(self, tx_size).await + } fn addresses_from_script(&self, script: &Script) -> Result, String> { utxo_common::addresses_from_script(self, script) @@ -49,40 +154,23 @@ impl UtxoCommonOps for UtxoStandardCoin { fn denominate_satoshis(&self, satoshi: i64) -> f64 { utxo_common::denominate_satoshis(&self.utxo_arc, satoshi) } - fn my_public_key(&self) -> &Public { self.utxo_arc.key_pair.public() } + fn my_public_key(&self) -> Result<&Public, MmError> { + utxo_common::my_public_key(self.as_ref()) + } fn address_from_str(&self, address: &str) -> Result { - utxo_common::checked_address_from_str(&self.utxo_arc, address) + utxo_common::checked_address_from_str(self, address) } async fn get_current_mtp(&self) -> UtxoRpcResult { - utxo_common::get_current_mtp(&self.utxo_arc, CoinVariant::Standard).await + let coin_variant = coin_variant_by_ticker(self.ticker()); + utxo_common::get_current_mtp(&self.utxo_arc, coin_variant).await } fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { utxo_common::is_unspent_mature(self.utxo_arc.conf.mature_confirmations, output) } - async fn generate_transaction( - &self, - utxos: Vec, - outputs: Vec, - fee_policy: FeePolicy, - fee: Option, - gas_fee: Option, - ) -> GenerateTxResult { - utxo_common::generate_transaction(self, utxos, outputs, fee_policy, fee, gas_fee).await - } - - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub).await - } - async fn calc_interest_of_tx(&self, tx: &UtxoTx, input_transactions: &mut HistoryUtxoTxMap) -> UtxoRpcResult { utxo_common::calc_interest_of_tx(self, tx, input_transactions).await } @@ -95,54 +183,19 @@ impl UtxoCommonOps for UtxoStandardCoin { utxo_common::get_mut_verbose_transaction_from_map_or_rpc(self, tx_hash, utxo_tx_map).await } - async fn p2sh_spending_tx( - &self, - prev_transaction: UtxoTx, - redeem_script: Bytes, - outputs: Vec, - script_data: Script, - sequence: u32, - lock_time: u32, - ) -> Result { - utxo_common::p2sh_spending_tx( - self, - prev_transaction, - redeem_script, - outputs, - script_data, - sequence, - lock_time, - ) - .await - } - - async fn ordered_mature_unspents<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::ordered_mature_unspents(self, address).await + async fn p2sh_spending_tx(&self, input: utxo_common::P2SHSpendingTxInput<'_>) -> Result { + utxo_common::p2sh_spending_tx(self, input).await } - fn get_verbose_transaction_from_cache_or_rpc( + fn get_verbose_transactions_from_cache_or_rpc( &self, - txid: H256Json, - ) -> Box + Send> { + tx_ids: HashSet, + ) -> UtxoRpcFut> { let selfi = self.clone(); - let fut = async move { utxo_common::get_verbose_transaction_from_cache_or_rpc(&selfi.utxo_arc, txid).await }; + let fut = async move { utxo_common::get_verbose_transactions_from_cache_or_rpc(&selfi.utxo_arc, tx_ids).await }; Box::new(fut.boxed().compat()) } - async fn cache_transaction_if_possible(&self, tx: &RpcTransaction) -> Result<(), String> { - utxo_common::cache_transaction_if_possible(&self.utxo_arc, tx).await - } - - async fn list_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_unspent_ordered(self, address).await - } - async fn preimage_trade_fee_required_to_send_outputs( &self, outputs: Vec, @@ -150,7 +203,15 @@ impl UtxoCommonOps for UtxoStandardCoin { gas_fee: Option, stage: &FeeApproxStage, ) -> TradePreimageResult { - utxo_common::preimage_trade_fee_required_to_send_outputs(self, outputs, fee_policy, gas_fee, stage).await + utxo_common::preimage_trade_fee_required_to_send_outputs( + self, + self.ticker(), + outputs, + fee_policy, + gas_fee, + stage, + ) + .await } fn increase_dynamic_fee_by_stage(&self, dynamic_fee: u64, stage: &FeeApproxStage) -> u64 { @@ -161,9 +222,23 @@ impl UtxoCommonOps for UtxoStandardCoin { utxo_common::p2sh_tx_locktime(self, &self.utxo_arc.conf.ticker, htlc_locktime).await } + fn addr_format(&self) -> &UtxoAddressFormat { utxo_common::addr_format(self) } + fn addr_format_for_standard_scripts(&self) -> UtxoAddressFormat { utxo_common::addr_format_for_standard_scripts(self) } + + fn address_from_pubkey(&self, pubkey: &Public) -> Address { + let conf = &self.utxo_arc.conf; + utxo_common::address_from_pubkey( + pubkey, + conf.pub_addr_prefix, + conf.pub_t_addr_prefix, + conf.checksum_type, + conf.bech32_hrp.clone(), + self.addr_format().clone(), + ) + } } #[async_trait] @@ -189,8 +264,9 @@ impl UtxoStandardOps for UtxoStandardCoin { } } +#[async_trait] impl SwapOps for UtxoStandardCoin { - fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { utxo_common::send_taker_fee(self.clone(), fee_addr, amount) } @@ -201,8 +277,16 @@ impl SwapOps for UtxoStandardCoin { secret_hash: &[u8], amount: BigDecimal, _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_maker_payment(self.clone(), time_lock, taker_pub, secret_hash, amount) + utxo_common::send_maker_payment( + self.clone(), + time_lock, + taker_pub, + secret_hash, + amount, + swap_unique_data, + ) } fn send_taker_payment( @@ -212,8 +296,16 @@ impl SwapOps for UtxoStandardCoin { secret_hash: &[u8], amount: BigDecimal, _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_taker_payment(self.clone(), time_lock, maker_pub, secret_hash, amount) + utxo_common::send_taker_payment( + self.clone(), + time_lock, + maker_pub, + secret_hash, + amount, + swap_unique_data, + ) } fn send_maker_spends_taker_payment( @@ -223,41 +315,73 @@ impl SwapOps for UtxoStandardCoin { taker_pub: &[u8], secret: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_maker_spends_taker_payment(self.clone(), taker_payment_tx, time_lock, taker_pub, secret) + utxo_common::send_maker_spends_taker_payment( + self.clone(), + taker_payment_tx, + time_lock, + taker_pub, + secret, + swap_unique_data, + ) } fn send_taker_spends_maker_payment( &self, - maker_payment_tx: &[u8], + maker_tx: &[u8], time_lock: u32, maker_pub: &[u8], secret: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_taker_spends_maker_payment(self.clone(), maker_payment_tx, time_lock, maker_pub, secret) + utxo_common::send_taker_spends_maker_payment( + self.clone(), + maker_tx, + time_lock, + maker_pub, + secret, + swap_unique_data, + ) } fn send_taker_refunds_payment( &self, - taker_payment_tx: &[u8], + taker_tx: &[u8], time_lock: u32, maker_pub: &[u8], secret_hash: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_taker_refunds_payment(self.clone(), taker_payment_tx, time_lock, maker_pub, secret_hash) + utxo_common::send_taker_refunds_payment( + self.clone(), + taker_tx, + time_lock, + maker_pub, + secret_hash, + swap_unique_data, + ) } fn send_maker_refunds_payment( &self, - maker_payment_tx: &[u8], + maker_tx: &[u8], time_lock: u32, taker_pub: &[u8], secret_hash: &[u8], _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> TransactionFut { - utxo_common::send_maker_refunds_payment(self.clone(), maker_payment_tx, time_lock, taker_pub, secret_hash) + utxo_common::send_maker_refunds_payment( + self.clone(), + maker_tx, + time_lock, + taker_pub, + secret_hash, + swap_unique_data, + ) } fn validate_fee( @@ -267,6 +391,7 @@ impl SwapOps for UtxoStandardCoin { fee_addr: &[u8], amount: &BigDecimal, min_block_number: u64, + _uuid: &[u8], ) -> Box + Send> { let tx = match fee_tx { TransactionEnum::UtxoTx(tx) => tx.clone(), @@ -283,28 +408,12 @@ impl SwapOps for UtxoStandardCoin { ) } - fn validate_maker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - priv_bn_hash: &[u8], - amount: BigDecimal, - _swap_contract_address: &Option, - ) -> Box + Send> { - utxo_common::validate_maker_payment(self, payment_tx, time_lock, maker_pub, priv_bn_hash, amount) + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + utxo_common::validate_maker_payment(self, input) } - fn validate_taker_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - priv_bn_hash: &[u8], - amount: BigDecimal, - _swap_contract_address: &Option, - ) -> Box + Send> { - utxo_common::validate_taker_payment(self, payment_tx, time_lock, taker_pub, priv_bn_hash, amount) + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + utxo_common::validate_taker_payment(self, input) } fn check_if_my_payment_sent( @@ -314,48 +423,23 @@ impl SwapOps for UtxoStandardCoin { secret_hash: &[u8], _search_from_block: u64, _swap_contract_address: &Option, + swap_unique_data: &[u8], ) -> Box, Error = String> + Send> { - utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash) + utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash, swap_unique_data) } - fn search_for_swap_tx_spend_my( + async fn search_for_swap_tx_spend_my( &self, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - _swap_contract_address: &Option, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { - utxo_common::search_for_swap_tx_spend_my( - &self.utxo_arc, - time_lock, - other_pub, - secret_hash, - tx, - utxo_common::DEFAULT_SWAP_VOUT, - search_from_block, - ) + utxo_common::search_for_swap_tx_spend_my(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } - fn search_for_swap_tx_spend_other( + async fn search_for_swap_tx_spend_other( &self, - time_lock: u32, - other_pub: &[u8], - secret_hash: &[u8], - tx: &[u8], - search_from_block: u64, - _swap_contract_address: &Option, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { - utxo_common::search_for_swap_tx_spend_other( - &self.utxo_arc, - time_lock, - other_pub, - secret_hash, - tx, - utxo_common::DEFAULT_SWAP_VOUT, - search_from_block, - ) + utxo_common::search_for_swap_tx_spend_other(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { @@ -377,21 +461,50 @@ impl SwapOps for UtxoStandardCoin { ) -> Result, MmError> { Ok(None) } + + fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { + utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) + } } impl MarketCoinOps for UtxoStandardCoin { fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } + fn get_public_key(&self) -> Result> { + let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; + Ok(pubkey.to_string()) + } + fn my_address(&self) -> Result { utxo_common::my_address(self) } - fn my_balance(&self) -> BalanceFut { utxo_common::my_balance(&self.utxo_arc) } + fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { + utxo_common::sign_message_hash(self.as_ref(), message) + } + + fn sign_message(&self, message: &str) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message) + } + + fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { + utxo_common::verify_message(self, signature_base64, message, address) + } + + fn my_balance(&self) -> BalanceFut { utxo_common::my_balance(self.clone()) } fn base_coin_balance(&self) -> BalanceFut { utxo_common::base_coin_balance(self) } + fn platform_ticker(&self) -> &str { self.ticker() } + + #[inline(always)] fn send_raw_tx(&self, tx: &str) -> Box + Send> { utxo_common::send_raw_tx(&self.utxo_arc, tx) } + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) + } + fn wait_for_confirmations( &self, tx: &[u8], @@ -434,16 +547,21 @@ impl MarketCoinOps for UtxoStandardCoin { utxo_common::current_block(&self.utxo_arc) } - fn display_priv_key(&self) -> String { utxo_common::display_priv_key(&self.utxo_arc) } + fn display_priv_key(&self) -> Result { utxo_common::display_priv_key(&self.utxo_arc) } fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } } +#[async_trait] impl MmCoin for UtxoStandardCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { + Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) + } + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { Box::new(utxo_common::withdraw(self.clone(), req).boxed().compat()) } @@ -471,20 +589,24 @@ impl MmCoin for UtxoStandardCoin { utxo_common::get_trade_fee(self.clone()) } - fn get_sender_trade_fee(&self, value: TradePreimageValue, stage: FeeApproxStage) -> TradePreimageFut { - utxo_common::get_sender_trade_fee(self.clone(), value, stage) + async fn get_sender_trade_fee( + &self, + value: TradePreimageValue, + stage: FeeApproxStage, + ) -> TradePreimageResult { + utxo_common::get_sender_trade_fee(self, value, stage).await } fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { utxo_common::get_receiver_trade_fee(self.clone()) } - fn get_fee_to_send_taker_fee( + async fn get_fee_to_send_taker_fee( &self, dex_fee_amount: BigDecimal, stage: FeeApproxStage, - ) -> TradePreimageFut { - utxo_common::get_fee_to_send_taker_fee(self.clone(), dex_fee_amount, stage) + ) -> TradePreimageResult { + utxo_common::get_fee_to_send_taker_fee(self, dex_fee_amount, stage).await } fn required_confirmations(&self) -> u64 { utxo_common::required_confirmations(&self.utxo_arc) } @@ -503,9 +625,212 @@ impl MmCoin for UtxoStandardCoin { fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } - fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(&self.utxo_arc) } + fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } fn is_coin_protocol_supported(&self, info: &Option>) -> bool { - utxo_common::is_coin_protocol_supported(&self.utxo_arc, info) + utxo_common::is_coin_protocol_supported(self, info) + } +} + +#[async_trait] +impl GetWithdrawSenderAddress for UtxoStandardCoin { + type Address = Address; + type Pubkey = Public; + + async fn get_withdraw_sender_address( + &self, + req: &WithdrawRequest, + ) -> MmResult, WithdrawError> { + utxo_common::get_withdraw_from_address(self, req).await + } +} + +#[async_trait] +impl InitWithdrawCoin for UtxoStandardCoin { + async fn init_withdraw( + &self, + ctx: MmArc, + req: WithdrawRequest, + task_handle: &WithdrawTaskHandle, + ) -> Result> { + utxo_common::init_withdraw(ctx, self.clone(), req, task_handle).await + } +} + +impl UtxoSignerOps for UtxoStandardCoin { + type TxGetter = UtxoRpcClientEnum; + + fn trezor_coin(&self) -> UtxoSignTxResult { + self.utxo_arc + .conf + .trezor_coin + .or_mm_err(|| UtxoSignTxError::CoinNotSupportedWithTrezor { + coin: self.utxo_arc.conf.ticker.clone(), + }) + } + + fn fork_id(&self) -> u32 { self.utxo_arc.conf.fork_id } + + fn branch_id(&self) -> u32 { self.utxo_arc.conf.consensus_branch_id } + + fn tx_provider(&self) -> Self::TxGetter { self.utxo_arc.rpc_client.clone() } +} + +impl CoinWithDerivationMethod for UtxoStandardCoin { + type Address = Address; + type HDWallet = UtxoHDWallet; + + fn derivation_method(&self) -> &DerivationMethod { + utxo_common::derivation_method(self.as_ref()) + } +} + +#[async_trait] +impl ExtractExtendedPubkey for UtxoStandardCoin { + type ExtendedPublicKey = Secp256k1ExtendedPublicKey; + + async fn extract_extended_pubkey( + &self, + xpub_extractor: &XPubExtractor, + derivation_path: DerivationPath, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Sync, + { + utxo_common::extract_extended_pubkey(&self.utxo_arc.conf, xpub_extractor, derivation_path).await + } +} + +#[async_trait] +impl HDWalletCoinOps for UtxoStandardCoin { + type Address = Address; + type Pubkey = Public; + type HDWallet = UtxoHDWallet; + type HDAccount = UtxoHDAccount; + + fn derive_address( + &self, + hd_account: &Self::HDAccount, + chain: Bip44Chain, + address_id: u32, + ) -> MmResult, AddressDerivingError> { + utxo_common::derive_address(self, hd_account, chain, address_id) + } + + async fn create_new_account<'a, XPubExtractor>( + &self, + hd_wallet: &'a Self::HDWallet, + xpub_extractor: &XPubExtractor, + ) -> MmResult, NewAccountCreatingError> + where + XPubExtractor: HDXPubExtractor + Sync, + { + utxo_common::create_new_account(self, hd_wallet, xpub_extractor).await + } + + async fn set_known_addresses_number( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut Self::HDAccount, + chain: Bip44Chain, + new_known_addresses_number: u32, + ) -> MmResult<(), AccountUpdatingError> { + utxo_common::set_known_addresses_number(self, hd_wallet, hd_account, chain, new_known_addresses_number).await + } +} + +#[async_trait] +impl HDWalletRpcOps for UtxoStandardCoin { + async fn get_new_address_rpc( + &self, + params: GetNewHDAddressParams, + ) -> MmResult { + hd_wallet::common_impl::get_new_address_rpc(self, params).await + } +} + +#[async_trait] +impl HDWalletBalanceOps for UtxoStandardCoin { + type HDAddressScanner = UtxoAddressScanner; + + async fn produce_hd_address_scanner(&self) -> BalanceResult { + utxo_common::produce_hd_address_scanner(self).await + } + + async fn enable_hd_wallet( + &self, + hd_wallet: &Self::HDWallet, + xpub_extractor: &XPubExtractor, + scan_policy: EnableCoinScanPolicy, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Sync, + { + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, scan_policy).await + } + + async fn scan_for_new_addresses( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut Self::HDAccount, + address_scanner: &Self::HDAddressScanner, + gap_limit: u32, + ) -> BalanceResult> { + utxo_common::scan_for_new_addresses(self, hd_wallet, hd_account, address_scanner, gap_limit).await + } + + async fn all_known_addresses_balances(&self, hd_account: &Self::HDAccount) -> BalanceResult> { + utxo_common::all_known_addresses_balances(self, hd_account).await + } + + async fn known_address_balance(&self, address: &Self::Address) -> BalanceResult { + utxo_common::address_balance(self, address).await + } + + async fn known_addresses_balances( + &self, + addresses: Vec, + ) -> BalanceResult> { + utxo_common::addresses_balances(self, addresses).await + } +} + +impl HDWalletCoinWithStorageOps for UtxoStandardCoin { + fn hd_wallet_storage<'a>(&self, hd_wallet: &'a Self::HDWallet) -> &'a HDWalletCoinStorage { + &hd_wallet.hd_wallet_storage + } +} + +#[async_trait] +impl AccountBalanceRpcOps for UtxoStandardCoin { + async fn account_balance_rpc( + &self, + params: AccountBalanceParams, + ) -> MmResult { + account_balance::common_impl::account_balance_rpc(self, params).await + } +} + +#[async_trait] +impl InitScanAddressesRpcOps for UtxoStandardCoin { + async fn init_scan_for_new_addresses_rpc( + &self, + params: ScanAddressesParams, + ) -> MmResult { + init_scan_for_new_addresses::common_impl::scan_for_new_addresses_rpc(self, params).await + } +} + +#[async_trait] +impl InitCreateHDAccountRpcOps for UtxoStandardCoin { + async fn init_create_account_rpc( + &self, + params: CreateNewAccountParams, + xpub_extractor: &XPubExtractor, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Sync, + { + init_create_account::common_impl::init_create_new_account_rpc(self, params, xpub_extractor).await } } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index fa78d5014e..00f74b03ce 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -1,20 +1,39 @@ -use super::rpc_clients::{ListSinceBlockRes, NetworkInfo}; use super::*; -use crate::utxo::qtum::{qtum_coin_from_conf_and_request, QtumCoin}; -use crate::utxo::rpc_clients::{GetAddressInfoRes, UtxoRpcClientOps, ValidateAddressRes, VerboseBlock}; -use crate::utxo::utxo_common::{generate_transaction, UtxoArcBuilder}; -use crate::utxo::utxo_standard::{utxo_standard_coin_from_conf_and_request, UtxoStandardCoin}; +use crate::coin_balance::HDAddressBalance; +use crate::hd_wallet::HDAccountsMap; +use crate::hd_wallet_storage::{HDWalletMockStorage, HDWalletStorageInternalOps}; +use crate::rpc_command::account_balance::{AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; +use crate::rpc_command::init_scan_for_new_addresses::{InitScanAddressesRpcOps, ScanAddressesParams, + ScanAddressesResponse}; +use crate::utxo::qtum::{qtum_coin_with_priv_key, QtumCoin, QtumDelegationOps, QtumDelegationRequest}; +use crate::utxo::rpc_clients::{BlockHashOrHeight, ElectrumBalance, ElectrumClient, ElectrumClientImpl, + GetAddressInfoRes, ListSinceBlockRes, ListTransactionsItem, NativeClient, + NativeClientImpl, NativeUnspent, NetworkInfo, UtxoRpcClientOps, ValidateAddressRes, + VerboseBlock}; +use crate::utxo::tx_cache::dummy_tx_cache::DummyVerboseCache; +use crate::utxo::tx_cache::UtxoVerboseCacheOps; +use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilderCommonOps}; +use crate::utxo::utxo_common::UtxoTxBuilder; +use crate::utxo::utxo_common_tests; +use crate::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; #[cfg(not(target_arch = "wasm32"))] use crate::WithdrawFee; -use crate::{CoinBalance, SwapOps, TradePreimageValue, TxFeeDetails}; -use bigdecimal::BigDecimal; +use crate::{CoinBalance, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, StakingInfosDetails, SwapOps, + TradePreimageValue, TxFeeDetails}; use chain::OutPoint; -use common::mm_ctx::MmCtxBuilder; -use common::privkey::key_pair_from_seed; -use common::{block_on, now_ms, OrdRange, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::executor::Timer; +use common::{block_on, now_ms, OrdRange, PagingOptionsEnum, DEX_FEE_ADDR_RAW_PUBKEY}; +use crypto::{privkey::key_pair_from_seed, Bip44Chain, RpcDerivationPath}; use futures::future::join_all; +use futures::TryFutureExt; +use mm2_core::mm_ctx::MmCtxBuilder; +use mm2_number::bigdecimal::{BigDecimal, Signed}; use mocktopus::mocking::*; use rpc::v1::types::H256 as H256Json; use serialization::{deserialize, CoinVariant}; +use std::convert::TryFrom; +use std::iter; +use std::mem::discriminant; +use std::num::NonZeroUsize; const TEST_COIN_NAME: &'static str = "RICK"; // Made-up hrp for rick to test p2wpkh script @@ -24,6 +43,7 @@ const RICK_ELECTRUM_ADDRS: &[&'static str] = &[ "electrum2.cipig.net:10017", "electrum3.cipig.net:10017", ]; +const TEST_COIN_DECIMALS: u8 = 8; pub fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let ctx = MmCtxBuilder::default().into_mm_arc(); @@ -32,14 +52,24 @@ pub fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { "method": "electrum", "servers": servers, }); - let builder = UtxoArcBuilder::new(&ctx, TEST_COIN_NAME, &Json::Null, &req, &[]); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(&[]); + let builder = UtxoArcBuilder::new( + &ctx, + TEST_COIN_NAME, + &Json::Null, + ¶ms, + priv_key_policy, + UtxoStandardCoin::from, + ); let args = ElectrumBuilderArgs { spawn_ping: false, negotiate_version: true, collect_metrics: false, }; - block_on(builder.electrum_client(args)).unwrap() + let servers = servers.into_iter().map(|s| json::from_value(s).unwrap()).collect(); + block_on(builder.electrum_client(args, servers)).unwrap() } /// Returned client won't work by default, requires some mocks to be usable @@ -69,13 +99,25 @@ fn utxo_coin_fields_for_test( let key_pair = key_pair_from_seed(&seed).unwrap(); let my_address = Address { prefix: 60, - hash: key_pair.public().address_hash(), + hash: key_pair.public().address_hash().into(), t_addr_prefix: 0, checksum_type, - hrp: None, - addr_format: UtxoAddressFormat::Standard, + hrp: if is_segwit_coin { + Some(TEST_COIN_HRP.to_string()) + } else { + None + }, + addr_format: if is_segwit_coin { + UtxoAddressFormat::Segwit + } else { + UtxoAddressFormat::Standard + }, }; let my_script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); + + let priv_key_policy = PrivKeyPolicy::KeyPair(key_pair); + let derivation_method = DerivationMethod::Iguana(my_address); + let bech32_hrp = if is_segwit_coin { Some(TEST_COIN_HRP.to_string()) } else { @@ -95,6 +137,7 @@ fn utxo_coin_fields_for_test( p2sh_t_addr_prefix: 0, pub_addr_prefix: 60, pub_t_addr_prefix: 0, + sign_message_prefix: Some(String::from("Komodo Signed Message:\n")), bech32_hrp, ticker: TEST_COIN_NAME.into(), wif_prefix: 0, @@ -111,17 +154,21 @@ fn utxo_coin_fields_for_test( estimate_fee_mode: None, mature_confirmations: MATURE_CONFIRMATIONS_DEFAULT, estimate_fee_blocks: 1, + trezor_coin: None, + enable_spv_proof: false, }, - decimals: 8, + decimals: TEST_COIN_DECIMALS, dust_amount: UTXO_DUST_AMOUNT, tx_fee: TxFee::FixedPerKb(1000), rpc_client, - key_pair, - my_address, + priv_key_policy, + derivation_method, history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - tx_cache_directory: None, + tx_cache: DummyVerboseCache::default().into_shared(), + block_headers_storage: None, recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), tx_hash_algo: TxHashAlgo::DSHA256, + check_utxo_maturity: false, } } @@ -150,6 +197,34 @@ fn test_extract_secret() { assert_eq!(secret, expected_secret); } +#[test] +fn test_send_maker_spends_taker_payment_recoverable_tx() { + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + let coin = utxo_coin_for_test(client.into(), None, false); + let tx_hex = hex::decode("0100000001de7aa8d29524906b2b54ee2e0281f3607f75662cbc9080df81d1047b78e21dbc00000000d7473044022079b6c50820040b1fbbe9251ced32ab334d33830f6f8d0bf0a40c7f1336b67d5b0220142ccf723ddabb34e542ed65c395abc1fbf5b6c3e730396f15d25c49b668a1a401209da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365004c6b6304f62b0e5cb175210270e75970bb20029b3879ec76c4acd320a8d0589e003636264d01a7d566504bfbac6782012088a9142fb610d856c19fd57f2d0cffe8dff689074b3d8a882103f368228456c940ac113e53dad5c104cf209f2f102a409207269383b6ab9b03deac68ffffffff01d0dc9800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac40280e5c").unwrap(); + let secret = hex::decode("9da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365").unwrap(); + + let tx_err = coin + .send_maker_spends_taker_payment( + &tx_hex, + 777, + &coin.my_public_key().unwrap().to_vec(), + &secret, + &coin.swap_contract_address(), + &[], + ) + .wait() + .unwrap_err(); + + let tx: UtxoTx = deserialize(tx_hex.as_slice()).unwrap(); + + // The error variant should equal to `TxRecoverable` + assert_eq!( + discriminant(&tx_err), + discriminant(&TransactionErr::TxRecoverable(TransactionEnum::from(tx), String::new())) + ); +} + #[test] fn test_generate_transaction() { let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); @@ -165,7 +240,10 @@ fn test_generate_transaction() { value: 999, }]; - let generated = block_on(coin.generate_transaction(unspents, outputs, FeePolicy::SendExact, None, None)); + let builder = UtxoTxBuilder::new(&coin) + .add_available_inputs(unspents) + .add_outputs(outputs); + let generated = block_on(builder.build()); // must not allow to use output with value < dust generated.unwrap_err(); @@ -180,9 +258,10 @@ fn test_generate_transaction() { value: 98001, }]; - let generated = - block_on(coin.generate_transaction(unspents.clone(), outputs.clone(), FeePolicy::SendExact, None, None)) - .unwrap(); + let builder = UtxoTxBuilder::new(&coin) + .add_available_inputs(unspents) + .add_outputs(outputs); + let generated = block_on(builder.build()).unwrap(); // the change that is less than dust must be included to miner fee // so no extra outputs should appear in generated transaction assert_eq!(generated.0.outputs.len(), 1); @@ -199,13 +278,17 @@ fn test_generate_transaction() { }]; let outputs = vec![TransactionOutput { - script_pubkey: Builder::build_p2pkh(&coin.as_ref().my_address.hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_iguana().hash).to_bytes(), value: 100000, }]; // test that fee is properly deducted from output amount equal to input amount (max withdraw case) - let generated = - block_on(coin.generate_transaction(unspents, outputs, FeePolicy::DeductFromOutput(0), None, None)).unwrap(); + let builder = UtxoTxBuilder::new(&coin) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_fee_policy(FeePolicy::DeductFromOutput(0)); + + let generated = block_on(builder.build()).unwrap(); assert_eq!(generated.0.outputs.len(), 1); assert_eq!(generated.1.fee_amount, 1000); @@ -226,7 +309,11 @@ fn test_generate_transaction() { }]; // test that generate_transaction returns an error when input amount is not sufficient to cover output + fee - block_on(coin.generate_transaction(unspents, outputs, FeePolicy::SendExact, None, None)).unwrap_err(); + let builder = UtxoTxBuilder::new(&coin) + .add_available_inputs(unspents) + .add_outputs(outputs); + + block_on(builder.build()).unwrap_err(); } #[test] @@ -356,7 +443,7 @@ fn test_wait_for_payment_spend_timeout_native() { let client = NativeClientImpl::default(); static mut OUTPUT_SPEND_CALLED: bool = false; - NativeClient::find_output_spend.mock_safe(|_, _, _, _| { + NativeClient::find_output_spend.mock_safe(|_, _, _, _, _| { unsafe { OUTPUT_SPEND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(None))) }); @@ -377,7 +464,7 @@ fn test_wait_for_payment_spend_timeout_native() { #[test] fn test_wait_for_payment_spend_timeout_electrum() { static mut OUTPUT_SPEND_CALLED: bool = false; - ElectrumClient::find_output_spend.mock_safe(|_, _, _, _| { + ElectrumClient::find_output_spend.mock_safe(|_, _, _, _, _| { unsafe { OUTPUT_SPEND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(None))) }); @@ -416,15 +503,16 @@ fn test_search_for_swap_tx_spend_electrum_was_spent() { .unwrap(); let spend_tx = TransactionEnum::UtxoTx(deserialize(spend_tx_bytes.as_slice()).unwrap()); - let found = coin - .search_for_swap_tx_spend_my( - 1591928233, - &*coin.my_public_key(), - &*dhash160(&secret), - &payment_tx_bytes, - 0, - &None, - ) + let search_input = SearchForSwapTxSpendInput { + time_lock: 1591928233, + other_pub: &*coin.my_public_key().unwrap(), + secret_hash: &*dhash160(&secret), + tx: &payment_tx_bytes, + search_from_block: 0, + swap_contract_address: &None, + swap_unique_data: &[], + }; + let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() .unwrap(); assert_eq!(FoundSwapTxSpend::Spent(spend_tx), found); @@ -432,7 +520,7 @@ fn test_search_for_swap_tx_spend_electrum_was_spent() { #[test] fn test_search_for_swap_tx_spend_electrum_was_refunded() { - let secret = [0; 20]; + let secret_hash = [0; 20]; let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); let coin = utxo_coin_for_test( client.into(), @@ -449,15 +537,16 @@ fn test_search_for_swap_tx_spend_electrum_was_refunded() { .unwrap(); let refund_tx = TransactionEnum::UtxoTx(deserialize(refund_tx_bytes.as_slice()).unwrap()); - let found = coin - .search_for_swap_tx_spend_my( - 1591933469, - coin.as_ref().key_pair.public(), - &secret, - &payment_tx_bytes, - 0, - &None, - ) + let search_input = SearchForSwapTxSpendInput { + time_lock: 1591933469, + other_pub: &coin.as_ref().priv_key_policy.key_pair_or_err().unwrap().public(), + secret_hash: &secret_hash, + tx: &payment_tx_bytes, + search_from_block: 0, + swap_contract_address: &None, + swap_unique_data: &[], + }; + let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() .unwrap(); assert_eq!(FoundSwapTxSpend::Refunded(refund_tx), found); @@ -466,7 +555,7 @@ fn test_search_for_swap_tx_spend_electrum_was_refunded() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_set_fixed_fee() { - UtxoStandardCoin::ordered_mature_unspents.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -484,7 +573,8 @@ fn test_withdraw_impl_set_fixed_fee() { let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); let withdraw_req = WithdrawRequest { - amount: 1.into(), + amount: 1u64.into(), + from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), max: false, @@ -494,6 +584,7 @@ fn test_withdraw_impl_set_fixed_fee() { }; let expected = Some( UtxoFeeDetails { + coin: Some(TEST_COIN_NAME.into()), amount: "0.1".parse().unwrap(), } .into(), @@ -505,7 +596,7 @@ fn test_withdraw_impl_set_fixed_fee() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_sat_per_kb_fee() { - UtxoStandardCoin::ordered_mature_unspents.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -523,7 +614,8 @@ fn test_withdraw_impl_sat_per_kb_fee() { let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); let withdraw_req = WithdrawRequest { - amount: 1.into(), + amount: 1u64.into(), + from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), max: false, @@ -536,6 +628,7 @@ fn test_withdraw_impl_sat_per_kb_fee() { // 0.1 * 245 / 1000 ~ 0.0245 let expected = Some( UtxoFeeDetails { + coin: Some(TEST_COIN_NAME.into()), amount: "0.0245".parse().unwrap(), } .into(), @@ -547,7 +640,7 @@ fn test_withdraw_impl_sat_per_kb_fee() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { - UtxoStandardCoin::ordered_mature_unspents.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -566,6 +659,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { let withdraw_req = WithdrawRequest { amount: "9.9789".parse().unwrap(), + from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), max: false, @@ -579,19 +673,20 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { // 0.1 * 211 / 1000 = 0.0211 let expected_fee = Some( UtxoFeeDetails { + coin: Some(TEST_COIN_NAME.into()), amount: "0.0211".parse().unwrap(), } .into(), ); assert_eq!(expected_fee, tx_details.fee_details); - let expected_balance_change = BigDecimal::from(-10); + let expected_balance_change = BigDecimal::from(-10i32); assert_eq!(expected_balance_change, tx_details.my_balance_change); } #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee() { - UtxoStandardCoin::ordered_mature_unspents.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -610,6 +705,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee() let withdraw_req = WithdrawRequest { amount: "9.9789".parse().unwrap(), + from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), max: false, @@ -623,19 +719,20 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee() // 0.1 * 211 / 1000 = 0.0211 let expected_fee = Some( UtxoFeeDetails { + coin: Some(TEST_COIN_NAME.into()), amount: "0.0211".parse().unwrap(), } .into(), ); assert_eq!(expected_fee, tx_details.fee_details); - let expected_balance_change = BigDecimal::from(-10); + let expected_balance_change = BigDecimal::from(-10i32); assert_eq!(expected_balance_change, tx_details.my_balance_change); } #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() { - UtxoStandardCoin::ordered_mature_unspents.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -654,6 +751,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() { let withdraw_req = WithdrawRequest { amount: "9.97939455".parse().unwrap(), + from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), max: false, @@ -667,7 +765,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_sat_per_kb_fee_max() { - UtxoStandardCoin::ordered_mature_unspents.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -685,7 +783,8 @@ fn test_withdraw_impl_sat_per_kb_fee_max() { let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); let withdraw_req = WithdrawRequest { - amount: 0.into(), + amount: 0u64.into(), + from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), max: true, @@ -698,6 +797,7 @@ fn test_withdraw_impl_sat_per_kb_fee_max() { // 0.1 * 211 / 1000 = 0.0211 let expected = Some( UtxoFeeDetails { + coin: Some(TEST_COIN_NAME.into()), amount: "0.0211".parse().unwrap(), } .into(), @@ -716,7 +816,7 @@ fn test_withdraw_kmd_rewards_impl( ) { let verbose: RpcTransaction = json::from_str(verbose_serialized).unwrap(); let unspent_height = verbose.height; - UtxoStandardCoin::ordered_mature_unspents.mock_safe(move |coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(move |coin, _| { let tx: UtxoTx = tx_hex.into(); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -733,7 +833,7 @@ fn test_withdraw_kmd_rewards_impl( .mock_safe(move |_fields| MockResult::Return(Box::pin(futures::future::ok(current_mtp)))); NativeClient::get_verbose_transaction.mock_safe(move |_coin, txid| { let expected: H256Json = hex::decode(tx_hash).unwrap().as_slice().into(); - assert_eq!(txid, expected); + assert_eq!(*txid, expected); MockResult::Return(Box::new(futures01::future::ok(verbose.clone()))) }); @@ -745,12 +845,14 @@ fn test_withdraw_kmd_rewards_impl( let withdraw_req = WithdrawRequest { amount: BigDecimal::from_str("0.00001").unwrap(), + from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: "KMD".to_owned(), max: false, fee: None, }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some("KMD".into()), amount: "0.00001".parse().unwrap(), }); let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); @@ -797,7 +899,7 @@ fn test_withdraw_rick_rewards_none() { // https://rick.explorer.dexstats.info/tx/7181400be323acc6b5f3164240e6c4601ff4c252f40ce7649f87e81634330209 const TX_HEX: &str = "0400008085202f8901df8119c507aa61d32332cd246dbfeb3818a4f96e76492454c1fbba5aa097977e000000004847304402205a7e229ea6929c97fd6dde254c19e4eb890a90353249721701ae7a1c477d99c402206a8b7c5bf42b5095585731d6b4c589ce557f63c20aed69ff242eca22ecfcdc7a01feffffff02d04d1bffbc050000232102afdbba3e3c90db5f0f4064118f79cf308f926c68afd64ea7afc930975663e4c4ac402dd913000000001976a9143e17014eca06281ee600adffa34b4afb0922a22288ac2bdab86035a00e000000000000000000000000"; - UtxoStandardCoin::ordered_mature_unspents.mock_safe(move |coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(move |coin, _| { let tx: UtxoTx = TX_HEX.into(); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -817,12 +919,14 @@ fn test_withdraw_rick_rewards_none() { let withdraw_req = WithdrawRequest { amount: BigDecimal::from_str("0.00001").unwrap(), + from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: "RICK".to_owned(), max: false, fee: None, }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some(TEST_COIN_NAME.into()), amount: "0.00001".parse().unwrap(), }); let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); @@ -830,25 +934,6 @@ fn test_withdraw_rick_rewards_none() { assert_eq!(tx_details.kmd_rewards, None); } -#[test] -fn test_ordered_mature_unspents_without_tx_cache() { - let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); - let coin = utxo_coin_for_test( - client.into(), - Some("spice describe gravity federal blast come thank unfair canal monkey style afraid"), - false, - ); - assert!(coin.as_ref().tx_cache_directory.is_none()); - assert_ne!( - coin.my_spendable_balance().wait().unwrap(), - 0.into(), - "The test address doesn't have unspent outputs" - ); - let (unspents, _) = - block_on(coin.ordered_mature_unspents(&Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"))).unwrap(); - assert!(!unspents.is_empty()); -} - #[test] fn test_utxo_lock() { // send several transactions concurrently to check that they are not using same inputs @@ -856,7 +941,7 @@ fn test_utxo_lock() { let coin = utxo_coin_for_test(client.into(), None, false); let output = TransactionOutput { value: 1000000, - script_pubkey: Builder::build_p2pkh(&coin.as_ref().my_address.hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_iguana().hash).to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -868,6 +953,23 @@ fn test_utxo_lock() { } } +#[test] +fn test_spv_proof() { + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + let coin = utxo_coin_for_test( + client.into(), + Some("spice describe gravity federal blast come thank unfair canal monkey style afraid"), + false, + ); + + // https://rick.explorer.dexstats.info/tx/78ea7839f6d1b0dafda2ba7e34c1d8218676a58bd1b33f03a5f76391f61b72b0 + let tx_str = "0400008085202f8902bf17bf7d1daace52e08f732a6b8771743ca4b1cb765a187e72fd091a0aabfd52000000006a47304402203eaaa3c4da101240f80f9c5e9de716a22b1ec6d66080de6a0cca32011cd77223022040d9082b6242d6acf9a1a8e658779e1c655d708379862f235e8ba7b8ca4e69c6012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffffff023ca13c0e9e085dd13f481f193e8a3e8fd609020936e98b5587342d994f4d020000006b483045022100c0ba56adb8de923975052312467347d83238bd8d480ce66e8b709a7997373994022048507bcac921fdb2302fa5224ce86e41b7efc1a2e20ae63aa738dfa99b7be826012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0300e1f5050000000017a9141ee6d4c38a3c078eab87ad1a5e4b00f21259b10d870000000000000000166a1400000000000000000000000000000000000000001b94d736000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac2d08e35e000000000000000000000000000000"; + let tx: UtxoTx = tx_str.into(); + + let res = block_on(utxo_common::validate_spv_proof(coin.clone(), tx, now_ms() / 1000 + 30)); + res.unwrap() +} + #[test] fn list_since_block_btc_serde() { // https://github.com/KomodoPlatform/atomicDEX-API/issues/563 @@ -875,63 +977,6 @@ fn list_since_block_btc_serde() { let _res: ListSinceBlockRes = json::from_str(input).unwrap(); } -#[test] -#[ignore] -fn get_tx_details_doge() { - let conf = json!( { - "coin": "DOGE", - "name": "dogecoin", - "fname": "Dogecoin", - "rpcport": 22555, - "pubtype": 30, - "p2shtype": 22, - "wiftype": 158, - "txfee": 0, - "mm2": 1, - "required_confirmations": 2 - }); - let req = json!({ - "method": "electrum", - "servers": [{"url":"electrum1.cipig.net:10060"},{"url":"electrum2.cipig.net:10060"},{"url":"electrum3.cipig.net:10060"}] - }); - - let ctx = MmCtxBuilder::new().into_mm_arc(); - - use common::executor::spawn; - let coin = block_on(utxo_standard_coin_from_conf_and_request( - &ctx, "DOGE", &conf, &req, &[1u8; 32], - )) - .unwrap(); - - let coin1 = coin.clone(); - let coin2 = coin.clone(); - let fut1 = async move { - let block = coin1.current_block().compat().await.unwrap(); - log!((block)); - let hash = hex::decode("99caab76bd025d189f10856dc649aad1a191b1cfd9b139ece457c5fedac58132").unwrap(); - loop { - let mut input_transactions = HistoryUtxoTxMap::new(); - let tx_details = coin1.tx_details_by_hash(&hash, &mut input_transactions).await.unwrap(); - log!([tx_details]); - Timer::sleep(1.).await; - } - }; - let fut2 = async move { - let block = coin2.current_block().compat().await.unwrap(); - log!((block)); - let hash = hex::decode("99caab76bd025d189f10856dc649aad1a191b1cfd9b139ece457c5fedac58132").unwrap(); - loop { - let mut input_transactions = HistoryUtxoTxMap::new(); - let tx_details = coin2.tx_details_by_hash(&hash, &mut input_transactions).await.unwrap(); - log!([tx_details]); - Timer::sleep(1.).await; - } - }; - spawn(fut1); - spawn(fut2); - loop {} -} - #[test] // https://github.com/KomodoPlatform/atomicDEX-API/issues/587 fn get_tx_details_coinbase_transaction() { @@ -963,14 +1008,14 @@ fn test_electrum_rpc_client_error() { let client = electrum_client_for_test(&["electrum1.cipig.net:10060"]); let empty_hash = H256Json::default(); - let err = client.get_verbose_transaction(empty_hash).wait().unwrap_err(); + let err = client.get_verbose_transaction(&empty_hash).wait().unwrap_err(); // use the static string instead because the actual error message cannot be obtain // by serde_json serialization let expected = r#"JsonRpcError { client_info: "coin: RICK", request: JsonRpcRequest { jsonrpc: "2.0", id: "1", method: "blockchain.transaction.get", params: [String("0000000000000000000000000000000000000000000000000000000000000000"), Bool(true)] }, error: Response(electrum1.cipig.net:10060, Object({"code": Number(2), "message": String("daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})")})) }"#; let actual = format!("{}", err); - assert_eq!(expected, actual); + assert!(actual.contains(expected)); } #[test] @@ -1083,14 +1128,12 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower() { value: 900000000, }]; - let fut = coin.generate_transaction( - unspents, - outputs, - FeePolicy::SendExact, - Some(ActualTxFee::Dynamic(100)), - None, - ); - let generated = block_on(fut).unwrap(); + let builder = UtxoTxBuilder::new(&coin) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_fee(ActualTxFee::Dynamic(100)); + + let generated = block_on(builder.build()).unwrap(); assert_eq!(generated.0.outputs.len(), 1); // generated transaction fee must be equal to relay fee if calculated dynamic fee is lower than relay @@ -1127,14 +1170,13 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower_and_ded value: 1000000000, }]; - let fut = coin.generate_transaction( - unspents, - outputs, - FeePolicy::DeductFromOutput(0), - Some(ActualTxFee::Dynamic(100)), - None, - ); - let generated = block_on(fut).unwrap(); + let tx_builder = UtxoTxBuilder::new(&coin) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_fee_policy(FeePolicy::DeductFromOutput(0)) + .with_fee(ActualTxFee::Dynamic(100)); + + let generated = block_on(tx_builder.build()).unwrap(); assert_eq!(generated.0.outputs.len(), 1); // `output (= 10.0) - fee_amount (= 1.0)` assert_eq!(generated.0.outputs[0].value, 900000000); @@ -1176,14 +1218,13 @@ fn test_generate_tx_fee_is_correct_when_dynamic_fee_is_larger_than_relay() { value: 19000000000, }]; - let fut = coin.generate_transaction( - unspents, - outputs, - FeePolicy::SendExact, - Some(ActualTxFee::Dynamic(1000)), - None, - ); - let generated = block_on(fut).unwrap(); + let builder = UtxoTxBuilder::new(&coin) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_fee(ActualTxFee::Dynamic(1000)); + + let generated = block_on(builder.build()).unwrap(); + assert_eq!(generated.0.outputs.len(), 2); assert_eq!(generated.0.inputs.len(), 20); @@ -1320,9 +1361,10 @@ fn test_cashaddresses_in_tx_details_by_hash() { }); let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = block_on(utxo_standard_coin_from_conf_and_request( - &ctx, "BCH", &conf, &req, &[1u8; 32], + let coin = block_on(utxo_standard_coin_with_priv_key( + &ctx, "BCH", &conf, ¶ms, &[1u8; 32], )) .unwrap(); @@ -1330,7 +1372,7 @@ fn test_cashaddresses_in_tx_details_by_hash() { let fut = async { let mut input_transactions = HistoryUtxoTxMap::new(); let tx_details = coin.tx_details_by_hash(&hash, &mut input_transactions).await.unwrap(); - log!([tx_details]); + log!("{:?}", tx_details); assert!(tx_details .from @@ -1366,9 +1408,10 @@ fn test_address_from_str_with_cashaddress_activated() { }); let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = block_on(utxo_standard_coin_from_conf_and_request( - &ctx, "BCH", &conf, &req, &[1u8; 32], + let coin = block_on(utxo_standard_coin_with_priv_key( + &ctx, "BCH", &conf, ¶ms, &[1u8; 32], )) .unwrap(); @@ -1400,9 +1443,10 @@ fn test_address_from_str_with_legacy_address_activated() { }); let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = block_on(utxo_standard_coin_from_conf_and_request( - &ctx, "BCH", &conf, &req, &[1u8; 32], + let coin = block_on(utxo_standard_coin_with_priv_key( + &ctx, "BCH", &conf, ¶ms, &[1u8; 32], )) .unwrap(); @@ -1444,12 +1488,13 @@ fn test_unavailable_electrum_proto_version() { }); let ctx = MmCtxBuilder::new().into_mm_arc(); - let error = block_on(utxo_standard_coin_from_conf_and_request( - &ctx, "RICK", &conf, &req, &[1u8; 32], + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let error = block_on(utxo_standard_coin_with_priv_key( + &ctx, "RICK", &conf, ¶ms, &[1u8; 32], )) .err() .unwrap(); - log!("Error: "(error)); + log!("Error: {}", error); assert!(error.contains("There are no Electrums with the required protocol version")); } @@ -1466,18 +1511,19 @@ fn test_spam_rick() { let key_pair = key_pair_from_seed("my_seed").unwrap(); let ctx = MmCtxBuilder::new().into_mm_arc(); - let coin = block_on(utxo_standard_coin_from_conf_and_request( + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(utxo_standard_coin_with_priv_key( &ctx, "RICK", &conf, - &req, + ¶ms, &*key_pair.private().secret, )) .unwrap(); let output = TransactionOutput { value: 1000000, - script_pubkey: Builder::build_p2pkh(&coin.as_ref().my_address.hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_iguana().hash).to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -1517,8 +1563,10 @@ fn test_one_unavailable_electrum_proto_version() { }); let ctx = MmCtxBuilder::new().into_mm_arc(); - let coin = block_on(utxo_standard_coin_from_conf_and_request( - &ctx, "BTC", &conf, &req, &[1u8; 32], + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + + let coin = block_on(utxo_standard_coin_with_priv_key( + &ctx, "BTC", &conf, ¶ms, &[1u8; 32], )) .unwrap(); @@ -1528,80 +1576,156 @@ fn test_one_unavailable_electrum_proto_version() { } #[test] -fn test_qtum_unspendable_balance_failed_once() { - let mut unspents = vec![ - // spendable balance (69.0) > balance (68.0) - vec![ - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 5000000000, - height: Default::default(), - }, - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 1900000000, - height: Default::default(), - }, - ], - // spendable balance (68.0) == balance (68.0) - vec![ - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 5000000000, - height: Default::default(), - }, - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 1800000000, - height: Default::default(), - }, - ], +fn test_qtum_generate_pod() { + let priv_key = [ + 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, + 172, 110, 180, 13, 123, 179, 10, 49, ]; - QtumCoin::ordered_mature_unspents.mock_safe(move |coin, _| { - let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); - let unspents = unspents.pop().unwrap(); - MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) + let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110}); + let req = json!({ + "method": "electrum", + "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], }); - let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110}); + let ctx = MmCtxBuilder::new().into_mm_arc(); + + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(qtum_coin_with_priv_key(&ctx, "tQTUM", &conf, ¶ms, &priv_key)).unwrap(); + let expected_res = "20086d757b34c01deacfef97a391f8ed2ca761c72a08d5000adc3d187b1007aca86a03bc5131b1f99b66873a12b51f8603213cdc1aa74c05ca5d48fe164b82152b"; + let address = Address::from_str("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE").unwrap(); + let res = coin.generate_pod(address.hash).unwrap(); + assert_eq!(expected_res, res.to_string()); +} + +#[test] +fn test_qtum_add_delegation() { + let keypair = key_pair_from_seed("asthma turtle lizard tone genuine tube hunt valley soap cloth urge alpha amazing frost faculty cycle mammal leaf normal bright topple avoid pulse buffalo").unwrap(); + let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110, "mature_confirmations":1}); let req = json!({ "method": "electrum", - "servers": [{"url":"95.217.83.126:10001"}], + "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], }); let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(qtum_coin_with_priv_key( + &ctx, + "tQTUM", + &conf, + ¶ms, + keypair.private().secret.as_slice(), + )) + .unwrap(); + let address = Address::from_str("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE").unwrap(); + let request = QtumDelegationRequest { + address: address.to_string(), + fee: Some(10), + }; + let res = coin.add_delegation(request).wait().unwrap(); + // Eligible for delegation + assert_eq!(res.my_balance_change.is_negative(), true); + assert_eq!(res.total_amount, res.spent_by_me); + assert!(res.spent_by_me > res.received_by_me); + + let request = QtumDelegationRequest { + address: "fake_address".to_string(), + fee: Some(10), + }; + let res = coin.add_delegation(request).wait(); + // Wrong address + assert_eq!(res.is_err(), true); +} - let priv_key = [ - 184, 199, 116, 240, 113, 222, 8, 199, 253, 143, 98, 185, 127, 26, 87, 38, 246, 206, 159, 27, 207, 20, 27, 112, - 184, 102, 137, 37, 78, 214, 113, 78, - ]; - let coin = block_on(qtum_coin_from_conf_and_request(&ctx, "tQTUM", &conf, &req, &priv_key)).unwrap(); +#[test] +fn test_qtum_add_delegation_on_already_delegating() { + let keypair = key_pair_from_seed("federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron").unwrap(); + let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110, "mature_confirmations":1}); + let req = json!({ + "method": "electrum", + "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], + }); - let CoinBalance { spendable, unspendable } = coin.my_balance().wait().unwrap(); - let expected_spendable = BigDecimal::from(68); - let expected_unspendable = BigDecimal::from(0); - assert_eq!(spendable, expected_spendable); - assert_eq!(unspendable, expected_unspendable); + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(qtum_coin_with_priv_key( + &ctx, + "tQTUM", + &conf, + ¶ms, + keypair.private().secret.as_slice(), + )) + .unwrap(); + let address = Address::from_str("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE").unwrap(); + let request = QtumDelegationRequest { + address: address.to_string(), + fee: Some(10), + }; + let res = coin.add_delegation(request).wait(); + // Already Delegating + assert_eq!(res.is_err(), true); +} + +#[test] +fn test_qtum_get_delegation_infos() { + let keypair = + key_pair_from_seed("federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron").unwrap(); + let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110, "mature_confirmations":1}); + let req = json!({ + "method": "electrum", + "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], + }); + + let ctx = MmCtxBuilder::new().into_mm_arc(); + + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(qtum_coin_with_priv_key( + &ctx, + "tQTUM", + &conf, + ¶ms, + keypair.private().secret.as_slice(), + )) + .unwrap(); + let staking_infos = coin.get_delegation_infos().wait().unwrap(); + match staking_infos.staking_infos_details { + StakingInfosDetails::Qtum(staking_details) => { + assert_eq!(staking_details.am_i_staking, true); + assert_eq!(staking_details.staker.unwrap(), "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE"); + // Will return false for segwit. + assert_eq!(staking_details.is_staking_supported, true); + }, + }; +} + +#[test] +fn test_qtum_remove_delegation() { + let keypair = key_pair_from_seed("federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron").unwrap(); + let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110, "mature_confirmations":1}); + let req = json!({ + "method": "electrum", + "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], + }); + + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(qtum_coin_with_priv_key( + &ctx, + "tQTUM", + &conf, + ¶ms, + keypair.private().secret.as_slice(), + )) + .unwrap(); + let res = coin.remove_delegation().wait(); + assert_eq!(res.is_err(), false); } #[test] -fn test_qtum_unspendable_balance_failed() { - QtumCoin::ordered_mature_unspents.mock_safe(move |coin, _| { +fn test_qtum_my_balance() { + QtumCoin::get_mature_unspent_ordered_list.mock_safe(move |coin, _address| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); - // spendable balance (69.0) > balance (68.0) - let unspents = vec![ + // spendable balance (66.0) + let mature = vec![ UnspentInfo { outpoint: OutPoint { hash: 1.into(), @@ -1615,17 +1739,29 @@ fn test_qtum_unspendable_balance_failed() { hash: 1.into(), index: 0, }, - value: 1900000000, + value: 1600000000, height: Default::default(), }, ]; - MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) + // unspendable (2.0) + let immature = vec![UnspentInfo { + outpoint: OutPoint { + hash: 1.into(), + index: 0, + }, + value: 200000000, + height: Default::default(), + }]; + MockResult::Return(Box::pin(futures::future::ok(( + MatureUnspentList { mature, immature }, + cache, + )))) }); let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110}); let req = json!({ "method": "electrum", - "servers": [{"url":"95.217.83.126:10001"}], + "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], }); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -1634,44 +1770,34 @@ fn test_qtum_unspendable_balance_failed() { 184, 199, 116, 240, 113, 222, 8, 199, 253, 143, 98, 185, 127, 26, 87, 38, 246, 206, 159, 27, 207, 20, 27, 112, 184, 102, 137, 37, 78, 214, 113, 78, ]; - let coin = block_on(qtum_coin_from_conf_and_request(&ctx, "tQTUM", &conf, &req, &priv_key)).unwrap(); - let error = coin.my_balance().wait().err().unwrap(); - log!("error: "[error]); - let expected_error = BalanceError::Internal("Spendable balance 69 greater than total balance 68".to_owned()); - assert_eq!(error.get_inner(), &expected_error); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(qtum_coin_with_priv_key(&ctx, "tQTUM", &conf, ¶ms, &priv_key)).unwrap(); + + let CoinBalance { spendable, unspendable } = coin.my_balance().wait().unwrap(); + let expected_spendable = BigDecimal::from(66); + let expected_unspendable = BigDecimal::from(2); + assert_eq!(spendable, expected_spendable); + assert_eq!(unspendable, expected_unspendable); } #[test] -fn test_qtum_my_balance() { - QtumCoin::ordered_mature_unspents.mock_safe(move |coin, _| { - let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); - // spendable balance (66.0) < balance (68.0), then unspendable balance is expected to be (2.0) - let unspents = vec![ - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 5000000000, - height: Default::default(), - }, - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 1600000000, - height: Default::default(), - }, - ]; - MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) +fn test_qtum_my_balance_with_check_utxo_maturity_false() { + const DISPLAY_BALANCE: u64 = 68; + ElectrumClient::display_balance.mock_safe(move |_, _, _| { + MockResult::Return(Box::new(futures01::future::ok(BigDecimal::from(DISPLAY_BALANCE)))) + }); + QtumCoin::get_all_unspent_ordered_list.mock_safe(move |_, _| { + panic!( + "'QtumCoin::get_all_unspent_ordered_list' is not expected to be called when `check_utxo_maturity` is false" + ) }); let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110}); let req = json!({ "method": "electrum", - "servers": [{"url":"95.217.83.126:10001"}], + "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], + "check_utxo_maturity": false, }); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -1680,16 +1806,18 @@ fn test_qtum_my_balance() { 184, 199, 116, 240, 113, 222, 8, 199, 253, 143, 98, 185, 127, 26, 87, 38, 246, 206, 159, 27, 207, 20, 27, 112, 184, 102, 137, 37, 78, 214, 113, 78, ]; - let coin = block_on(qtum_coin_from_conf_and_request(&ctx, "tQTUM", &conf, &req, &priv_key)).unwrap(); + + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(qtum_coin_with_priv_key(&ctx, "tQTUM", &conf, ¶ms, &priv_key)).unwrap(); let CoinBalance { spendable, unspendable } = coin.my_balance().wait().unwrap(); - let expected_spendable = BigDecimal::from(66); - let expected_unspendable = BigDecimal::from(2); + let expected_spendable = BigDecimal::from(DISPLAY_BALANCE); + let expected_unspendable = BigDecimal::from(0); assert_eq!(spendable, expected_spendable); assert_eq!(unspendable, expected_unspendable); } -fn test_ordered_mature_unspents_from_cache_impl( +fn test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height: Option, cached_height: Option, cached_confs: u32, @@ -1700,7 +1828,7 @@ fn test_ordered_mature_unspents_from_cache_impl( const TX_HASH: &str = "0a0fda88364b960000f445351fe7678317a1e0c80584de0413377ede00ba696f"; let tx_hash: H256Json = hex::decode(TX_HASH).unwrap().as_slice().into(); let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); - let mut verbose = client.get_verbose_transaction(tx_hash.clone()).wait().unwrap(); + let mut verbose = client.get_verbose_transaction(&tx_hash).wait().unwrap(); verbose.confirmations = cached_confs; verbose.height = cached_height; @@ -1718,11 +1846,10 @@ fn test_ordered_mature_unspents_from_cache_impl( }); ElectrumClient::get_block_count .mock_safe(move |_| MockResult::Return(Box::new(futures01::future::ok(block_count)))); - UtxoStandardCoin::get_verbose_transaction_from_cache_or_rpc.mock_safe(move |_, txid| { - assert_eq!(txid, tx_hash); - MockResult::Return(Box::new(futures01::future::ok(VerboseTransactionFrom::Cache( - verbose.clone(), - )))) + UtxoStandardCoin::get_verbose_transactions_from_cache_or_rpc.mock_safe(move |_, tx_ids| { + itertools::assert_equal(tx_ids, iter::once(tx_hash)); + let result: HashMap<_, _> = iter::once((tx_hash, VerboseTransactionFrom::Cache(verbose.clone()))).collect(); + MockResult::Return(Box::new(futures01::future::ok(result))) }); static mut IS_UNSPENT_MATURE_CALLED: bool = false; UtxoStandardCoin::is_unspent_mature.mock_safe(move |_, tx: &RpcTransaction| { @@ -1735,22 +1862,24 @@ fn test_ordered_mature_unspents_from_cache_impl( // run test let coin = utxo_coin_for_test(UtxoRpcClientEnum::Electrum(client), None, false); - let (unspents, _) = block_on(coin.ordered_mature_unspents(&Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"))) - .expect("Expected an empty unspent list"); + let (unspents, _) = + block_on(coin.get_mature_unspent_ordered_list(&Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"))) + .expect("Expected an empty unspent list"); // unspents should be empty because `is_unspent_mature()` always returns false - assert!(unspents.is_empty()); assert!(unsafe { IS_UNSPENT_MATURE_CALLED == true }); + assert!(unspents.mature.is_empty()); + assert_eq!(unspents.immature.len(), 1); } #[test] -fn test_ordered_mature_unspents_from_cache() { +fn test_get_mature_unspents_ordered_map_from_cache() { let unspent_height = None; let cached_height = None; let cached_confs = 0; let block_count = 1000; let expected_height = None; // is unknown let expected_confs = 0; // is not changed because height is unknown - test_ordered_mature_unspents_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -1765,7 +1894,7 @@ fn test_ordered_mature_unspents_from_cache() { let block_count = 1000; let expected_height = None; // is unknown let expected_confs = 5; // is not changed because height is unknown - test_ordered_mature_unspents_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -1780,7 +1909,7 @@ fn test_ordered_mature_unspents_from_cache() { let block_count = 1000; let expected_height = Some(998); // as the unspent_height let expected_confs = 3; // 1000 - 998 + 1 - test_ordered_mature_unspents_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -1795,7 +1924,7 @@ fn test_ordered_mature_unspents_from_cache() { let block_count = 1000; let expected_height = Some(998); // as the cached_height let expected_confs = 3; // 1000 - 998 + 1 - test_ordered_mature_unspents_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -1810,7 +1939,7 @@ fn test_ordered_mature_unspents_from_cache() { let block_count = 1000; let expected_height = Some(998); // as the unspent_height let expected_confs = 3; // 1000 - 998 + 1 - test_ordered_mature_unspents_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -1826,7 +1955,7 @@ fn test_ordered_mature_unspents_from_cache() { let block_count = 999; let expected_height = Some(1000); // as the cached_height let expected_confs = 1; // is not changed because height cannot be calculated - test_ordered_mature_unspents_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -1842,7 +1971,7 @@ fn test_ordered_mature_unspents_from_cache() { let block_count = 1000; let expected_height = Some(1000); // as the cached_height let expected_confs = 1; // 1000 - 1000 + 1 - test_ordered_mature_unspents_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -1858,7 +1987,7 @@ fn test_ordered_mature_unspents_from_cache() { let block_count = 1000; let expected_height = Some(0); // as the cached_height let expected_confs = 1; // is not changed because tx_height is expected to be not zero - test_ordered_mature_unspents_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -1901,8 +2030,7 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_tx_in_cache() { NativeClient::list_unspent .mock_safe(move |_, _, _| MockResult::Return(Box::new(futures01::future::ok(spent_by_tx.clone())))); - let address: Address = "RGfFZaaNV68uVe1uMf6Y37Y8E1i2SyYZBN".into(); - let (unspents_ordered, _) = block_on(coin.list_unspent_ordered(&address)).unwrap(); + let (unspents_ordered, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); // output 2 is change so it must be returned let expected_unspent = UnspentInfo { outpoint: OutPoint { @@ -1996,7 +2124,7 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_several_chained_tx NativeClient::list_unspent .mock_safe(move |_, _, _| MockResult::Return(Box::new(futures01::future::ok(unspents_to_return.clone())))); - let (unspents_ordered, _) = block_on(coin.list_unspent_ordered(&address)).unwrap(); + let (unspents_ordered, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); // output 2 is change so it must be returned let expected_unspent = UnspentInfo { @@ -2250,7 +2378,7 @@ fn test_find_output_spend_skips_conflicting_transactions() { static mut GET_RAW_TRANSACTION_BYTES_CALLED: usize = 0; NativeClientImpl::get_raw_transaction_bytes.mock_safe(move |_, txid| { unsafe { GET_RAW_TRANSACTION_BYTES_CALLED += 1 }; - assert_eq!(txid, expected_txid); + assert_eq!(*txid, expected_txid); // no matter what we return here let bytes: BytesJson = hex::decode("0400008085202f890347d329798b508dc28ec99d8c6f6c7ced860a19a364e1bafe391cab89aeaac731020000006a47304402203ea8b380d0a7e64348869ef7c4c2bfa966fc7b148633003332fa8d0ab0c1bc5602202cc63fabdd2a6578c52d8f4f549069b16505f2ead48edc2b8de299be15aadf9a012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff1d1fd3a6b01710647a7f4a08c6de6075cb8e78d5069fa50f10c4a2a10ded2a95000000006a47304402203868945edc0f6dc2ee43d70a69ee4ec46ca188dc493173ce58924ba9bf6ee7a50220648ff99ce458ca72800758f6a1bd3800cd05ff9c3122f23f3653c25e09d22c79012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff7932150df8b4a1852b8b84b89b0d5322bf74665fb7f76a728369fd6895d3fd48000000006a4730440220127918c6f79c11f7f2376a6f3b750ed4c7103183181ad1218afcb2625ece9599022028c05e88d3a2f97cebd84a718cda33b62b48b18f16278fa8e531fd2155e61ee8012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff0329fd12000000000017a914cafb62e3e8bdb8db3735c39b92743ac6ebc9ef20870000000000000000166a14a7416b070c9bb98f4bafae55616f005a2a30bd6014b40c00000000001976a91450f4f098306f988d8843004689fae28c83ef16e888ac8cc5925f000000000000000000000000000000").unwrap().into(); MockResult::Return(Box::new(futures01::future::ok(bytes))) @@ -2261,7 +2389,14 @@ fn test_find_output_spend_skips_conflicting_transactions() { let tx: UtxoTx = "0400008085202f89027f57730fcbbc2c72fb18bcc3766a713044831a117bb1cade3ed88644864f7333020000006a47304402206e3737b2fcf078b61b16fa67340cc3e79c5d5e2dc9ffda09608371552a3887450220460a332aa1b8ad8f2de92d319666f70751078b221199951f80265b4f7cef8543012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff42b916a80430b80a77e114445b08cf120735447a524de10742fac8f6a9d4170f000000006a473044022004aa053edafb9d161ea8146e0c21ed1593aa6b9404dd44294bcdf920a1695fd902202365eac15dbcc5e9f83e2eed56a8f2f0e5aded36206f9c3fabc668fd4665fa2d012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff03547b16000000000017a9143e8ad0e2bf573d32cb0b3d3a304d9ebcd0c2023b870000000000000000166a144e2b3c0323ab3c2dc6f86dc5ec0729f11e42f56103970400000000001976a91450f4f098306f988d8843004689fae28c83ef16e888ac89c5925f000000000000000000000000000000".into(); let vout = 0; let from_block = 0; - let actual = client.find_output_spend(&tx, vout, from_block).wait(); + let actual = client + .find_output_spend( + tx.hash(), + &tx.outputs[vout].script_pubkey, + vout, + BlockHashOrHeight::Height(from_block), + ) + .wait(); assert_eq!(actual, Ok(None)); assert_eq!(unsafe { GET_RAW_TRANSACTION_BYTES_CALLED }, 1); } @@ -2279,7 +2414,7 @@ fn test_qtum_is_unspent_mature() { let coin = QtumCoin::from(arc); let empty_output = SignedTransactionOutput { - value: 0., + value: Some(0.), n: 0, script: TransactionOutputScript { asm: "".into(), @@ -2290,7 +2425,7 @@ fn test_qtum_is_unspent_mature() { }, }; let real_output = SignedTransactionOutput { - value: 117.02430015, + value: Some(117.02430015), n: 1, script: TransactionOutputScript { asm: "03e71b9c152bb233ddfe58f20056715c51b054a1823e0aba108e6f1cea0ceb89c8 OP_CHECKSIG".into(), @@ -2337,7 +2472,7 @@ fn test_qtum_is_unspent_mature() { #[ignore] // TODO it fails at least when fee is 2055837 sat per kbyte, need to investigate fn test_get_sender_trade_fee_dynamic_tx_fee() { - let rpc_client = electrum_client_for_test(&["95.217.83.126:10001"]); + let rpc_client = electrum_client_for_test(&["electrum1.cipig.net:10071"]); let mut coin_fields = utxo_coin_fields_for_test( UtxoRpcClientEnum::Electrum(rpc_client), Some("bob passphrase max taker vol with dynamic trade fee"), @@ -2349,31 +2484,26 @@ fn test_get_sender_trade_fee_dynamic_tx_fee() { let expected_balance = BigDecimal::from_str("2.22222").expect("!BigDecimal::from_str"); assert_eq!(my_balance, expected_balance); - let fee1 = coin - .get_sender_trade_fee( - TradePreimageValue::UpperBound(my_balance.clone()), - FeeApproxStage::WithoutApprox, - ) - .wait() - .expect("!get_sender_trade_fee"); + let fee1 = block_on(coin.get_sender_trade_fee( + TradePreimageValue::UpperBound(my_balance.clone()), + FeeApproxStage::WithoutApprox, + )) + .expect("!get_sender_trade_fee"); let value_without_fee = &my_balance - &fee1.amount.to_decimal(); - log!("value_without_fee "(value_without_fee)); - let fee2 = coin - .get_sender_trade_fee( - TradePreimageValue::Exact(value_without_fee), - FeeApproxStage::WithoutApprox, - ) - .wait() - .expect("!get_sender_trade_fee"); + log!("value_without_fee {}", value_without_fee); + let fee2 = block_on(coin.get_sender_trade_fee( + TradePreimageValue::Exact(value_without_fee), + FeeApproxStage::WithoutApprox, + )) + .expect("!get_sender_trade_fee"); assert_eq!(fee1, fee2); // `2.21934443` value was obtained as a result of executing the `max_taker_vol` RPC call for this wallet let max_taker_vol = BigDecimal::from_str("2.21934443").expect("!BigDecimal::from_str"); - let fee3 = coin - .get_sender_trade_fee(TradePreimageValue::Exact(max_taker_vol), FeeApproxStage::WithoutApprox) - .wait() - .expect("!get_sender_trade_fee"); + let fee3 = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(max_taker_vol), FeeApproxStage::WithoutApprox)) + .expect("!get_sender_trade_fee"); assert_eq!(fee1, fee3); } @@ -2396,6 +2526,7 @@ fn test_validate_fee_wrong_sender() { &*DEX_FEE_ADDR_RAW_PUBKEY, &amount, 0, + &[], ) .wait() .unwrap_err(); @@ -2416,7 +2547,14 @@ fn test_validate_fee_min_block() { let amount: BigDecimal = "0.0014157".parse().unwrap(); let sender_pub = hex::decode("03ad6f89abc2e5beaa8a3ac28e22170659b3209fe2ddf439681b4b8f31508c36fa").unwrap(); let validate_err = coin - .validate_fee(&taker_fee_tx, &sender_pub, &*DEX_FEE_ADDR_RAW_PUBKEY, &amount, 810329) + .validate_fee( + &taker_fee_tx, + &sender_pub, + &*DEX_FEE_ADDR_RAW_PUBKEY, + &amount, + 810329, + &[], + ) .wait() .unwrap_err(); assert!(validate_err.contains("confirmed before min_block")); @@ -2436,7 +2574,7 @@ fn test_validate_fee_bch_70_bytes_signature() { let taker_fee_tx = coin.tx_enum_from_bytes(&tx_bytes).unwrap(); let amount: BigDecimal = "0.0001".parse().unwrap(); let sender_pub = hex::decode("02ae7dc4ef1b49aadeff79cfad56664105f4d114e1716bc4f930cb27dbd309e521").unwrap(); - coin.validate_fee(&taker_fee_tx, &sender_pub, &*DEX_FEE_ADDR_RAW_PUBKEY, &amount, 0) + coin.validate_fee(&taker_fee_tx, &sender_pub, &*DEX_FEE_ADDR_RAW_PUBKEY, &amount, 0, &[]) .wait() .unwrap(); } @@ -2493,7 +2631,7 @@ fn firo_lelantus_tx() { "electrumx02.firo.org:50001", "electrumx03.firo.org:50001", ]); - let _tx = electrum.get_verbose_transaction(tx_hash).wait().unwrap(); + let _tx = electrum.get_verbose_transaction(&tx_hash).wait().unwrap(); } #[test] @@ -2511,6 +2649,7 @@ fn firo_lelantus_tx_details() { let tx_details = block_on(coin.tx_details_by_hash(&tx_hash, &mut map)).unwrap(); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some(TEST_COIN_NAME.into()), amount: "0.00003793".parse().unwrap(), }); assert_eq!(Some(expected_fee), tx_details.fee_details); @@ -2519,6 +2658,7 @@ fn firo_lelantus_tx_details() { let tx_details = block_on(coin.tx_details_by_hash(&tx_hash, &mut map)).unwrap(); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some(TEST_COIN_NAME.into()), amount: "0.00045778".parse().unwrap(), }); assert_eq!(Some(expected_fee), tx_details.fee_details); @@ -2550,9 +2690,12 @@ fn test_generate_tx_doge_fee() { "servers": [{"url": "electrum1.cipig.net:10060"},{"url": "electrum2.cipig.net:10060"},{"url": "electrum3.cipig.net:10060"}], }); let ctx = MmCtxBuilder::default().into_mm_arc(); - let doge: UtxoStandardCoin = block_on(UtxoArcBuilder::new(&ctx, "DOGE", &config, &request, &[1; 32]).build()) - .unwrap() - .into(); + let params = UtxoActivationParams::from_legacy_req(&request).unwrap(); + + let doge = block_on(utxo_standard_coin_with_priv_key( + &ctx, "DOGE", &config, ¶ms, &[1; 32], + )) + .unwrap(); let unspents = vec![UnspentInfo { outpoint: Default::default(), @@ -2563,8 +2706,10 @@ fn test_generate_tx_doge_fee() { value: 100000000, script_pubkey: vec![0; 26].into(), }]; - let policy = FeePolicy::SendExact; - let (_, data) = block_on(generate_transaction(&doge, unspents, outputs, policy, None, None)).unwrap(); + let builder = UtxoTxBuilder::new(&doge) + .add_available_inputs(unspents) + .add_outputs(outputs); + let (_, data) = block_on(builder.build()).unwrap(); let expected_fee = 1000000; assert_eq!(expected_fee, data.fee_amount); @@ -2581,8 +2726,11 @@ fn test_generate_tx_doge_fee() { .clone(); 40 ]; - let policy = FeePolicy::SendExact; - let (_, data) = block_on(generate_transaction(&doge, unspents, outputs, policy, None, None)).unwrap(); + + let builder = UtxoTxBuilder::new(&doge) + .add_available_inputs(unspents) + .add_outputs(outputs); + let (_, data) = block_on(builder.build()).unwrap(); let expected_fee = 2000000; assert_eq!(expected_fee, data.fee_amount); @@ -2599,8 +2747,11 @@ fn test_generate_tx_doge_fee() { .clone(); 60 ]; - let policy = FeePolicy::SendExact; - let (_, data) = block_on(generate_transaction(&doge, unspents, outputs, policy, None, None)).unwrap(); + + let builder = UtxoTxBuilder::new(&doge) + .add_available_inputs(unspents) + .add_outputs(outputs); + let (_, data) = block_on(builder.build()).unwrap(); let expected_fee = 3000000; assert_eq!(expected_fee, data.fee_amount); } @@ -2643,112 +2794,6 @@ fn verus_mtp() { assert_eq!(mtp, 1618579909); } -#[test] -#[ignore] -fn mint_slp_token() { - use bitcoin_cash_slp::{slp_genesis_output, SlpTokenType}; - let ctx = MmCtxBuilder::new().into_mm_arc(); - let conf = json!({ - "decimals": 8, - "network": "regtest", - "confpath": "/home/artem/.bch/bch.conf", - "address_format": { - "format": "cashaddress", - "network": "bchreg", - }, - "pubtype": 111, - "p2shtype": 196, - "dust": 546, - }); - let req = json!({ - "method": "enable", - }); - let priv_key = hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(); - let coin = block_on(utxo_standard_coin_from_conf_and_request( - &ctx, "BCH", &conf, &req, &priv_key, - )) - .unwrap(); - let address = coin.my_address().unwrap(); - println!("{}", address); - - let balance = coin.my_balance().wait().unwrap(); - println!("{}", balance.spendable); - - let output = slp_genesis_output(SlpTokenType::Fungible, "ADEX", "ADEX", "", "", 8, None, 1000_0000_0000); - let script_pubkey = output.script.serialize().unwrap().to_vec().into(); - - println!("{}", hex::encode(&script_pubkey)); - - let op_return_output = TransactionOutput { - value: output.value, - script_pubkey, - }; - let mint_output = TransactionOutput { - value: 546, - script_pubkey: hex::decode("76a91405aab5342166f8594baf17a7d9bef5d56744332788ac") - .unwrap() - .into(), - }; - block_on(send_outputs_from_my_address_impl(coin, vec![ - op_return_output, - mint_output, - ])) - .unwrap(); -} - -#[test] -#[ignore] -fn transfer_slp_token() { - use bitcoin_cash_slp::{slp_send_output, SlpTokenType, TokenId}; - let ctx = MmCtxBuilder::new().into_mm_arc(); - let conf = json!({ - "decimals": 8, - "network": "regtest", - "confpath": "/home/artem/.bch/bch.conf", - "address_format": { - "format": "cashaddress", - "network": "bchreg", - }, - "pubtype": 111, - "p2shtype": 196, - "dust": 546, - }); - let req = json!({ - "method": "enable", - }); - let priv_key = hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(); - let coin = block_on(utxo_standard_coin_from_conf_and_request( - &ctx, "BCH", &conf, &req, &priv_key, - )) - .unwrap(); - let token_id = hex::decode("e73b2b28c14db8ebbf97749988b539508990e1708021067f206f49d55807dbf4").unwrap(); - let token_id = TokenId::from_slice(token_id.as_slice()).unwrap(); - let address = coin.my_address().unwrap(); - println!("{}", address); - - let balance = coin.my_balance().wait().unwrap(); - println!("{}", balance.spendable); - - let output = slp_send_output(SlpTokenType::Fungible, &token_id, &[100000000]); - let script_pubkey = output.script.serialize().unwrap().to_vec(); - println!("{}", hex::encode(&script_pubkey)); - let op_return_output = TransactionOutput { - value: output.value, - script_pubkey: script_pubkey.into(), - }; - let mint_output = TransactionOutput { - value: 546, - script_pubkey: hex::decode("76a91405aab5342166f8594baf17a7d9bef5d56744332788ac") - .unwrap() - .into(), - }; - block_on(send_outputs_from_my_address_impl(coin, vec![ - op_return_output, - mint_output, - ])) - .unwrap(); -} - #[test] fn sys_mtp() { let electrum = electrum_client_for_test(&[ @@ -2829,7 +2874,7 @@ fn test_tx_details_kmd_rewards() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.my_address = Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk"); + fields.derivation_method = DerivationMethod::Iguana(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); let coin = utxo_coin_from_fields(fields); let mut input_transactions = HistoryUtxoTxMap::new(); @@ -2837,6 +2882,7 @@ fn test_tx_details_kmd_rewards() { let tx_details = block_on(coin.tx_details_by_hash(&hash, &mut input_transactions)).expect("!tx_details_by_hash"); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some("KMD".into()), amount: BigDecimal::from_str("0.00001").unwrap(), }); assert_eq!(tx_details.fee_details, Some(expected_fee)); @@ -2862,7 +2908,7 @@ fn test_tx_details_kmd_rewards_claimed_by_other() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.my_address = Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk"); + fields.derivation_method = DerivationMethod::Iguana(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); let coin = utxo_coin_from_fields(fields); let mut input_transactions = HistoryUtxoTxMap::new(); @@ -2870,6 +2916,7 @@ fn test_tx_details_kmd_rewards_claimed_by_other() { let tx_details = block_on(coin.tx_details_by_hash(&hash, &mut input_transactions)).expect("!tx_details_by_hash"); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some("KMD".into()), amount: BigDecimal::from_str("0.00001").unwrap(), }); assert_eq!(tx_details.fee_details, Some(expected_fee)); @@ -2897,6 +2944,7 @@ fn test_tx_details_bch_no_rewards() { let tx_details = block_on(coin.tx_details_by_hash(&hash, &mut input_transactions)).expect("!tx_details_by_hash"); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some(TEST_COIN_NAME.into()), amount: BigDecimal::from_str("0.00000452").unwrap(), }); assert_eq!(tx_details.fee_details, Some(expected_fee)); @@ -2915,7 +2963,7 @@ fn test_update_kmd_rewards() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.my_address = Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk"); + fields.derivation_method = DerivationMethod::Iguana(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); let coin = utxo_coin_from_fields(fields); let mut input_transactions = HistoryUtxoTxMap::default(); @@ -2929,6 +2977,7 @@ fn test_update_kmd_rewards() { assert_eq!(tx_details.kmd_rewards, Some(expected_rewards)); let expected_fee_details = TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some("KMD".into()), amount: BigDecimal::from_str("0.00001").unwrap(), }); assert_eq!(tx_details.fee_details, Some(expected_fee_details)); @@ -2946,7 +2995,7 @@ fn test_update_kmd_rewards_claimed_not_by_me() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.my_address = Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk"); + fields.derivation_method = DerivationMethod::Iguana(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); let coin = utxo_coin_from_fields(fields); let mut input_transactions = HistoryUtxoTxMap::default(); @@ -2960,6 +3009,7 @@ fn test_update_kmd_rewards_claimed_not_by_me() { assert_eq!(tx_details.kmd_rewards, Some(expected_rewards)); let expected_fee_details = TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some("KMD".into()), amount: BigDecimal::from_str("0.00001").unwrap(), }); assert_eq!(tx_details.fee_details, Some(expected_fee_details)); @@ -2973,10 +3023,22 @@ fn test_parse_tx_with_huge_locktime() { let _: UtxoTx = deserialize(verbose_tx.hex.as_slice()).unwrap(); } +#[test] +fn tbch_electroncash_verbose_tx() { + let verbose = r#"{"blockhash":"00000000000d93dbc9c6e95c37044d584be959d24e514533b3a82f0f61dddc03","blocktime":1626262632,"confirmations":3708,"hash":"e64531613f909647651ac3f8fd72f3e6f72ac6e01c5a1d923884a10476f56a7f","height":1456230,"hex":"0100000002ebc10f58f220ec1bad5d634684ae649aa7bdd2f9c9081d36e5384e579caa95c2020000006a4730440220639ac218f572520c7d8addae74be6bfdefa9c86bc91474b6dedd7e117d232085022015a92f45f9ae5cee08c188e01fc614b77c461a41733649a55abfcc3e7ca207444121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffffebc10f58f220ec1bad5d634684ae649aa7bdd2f9c9081d36e5384e579caa95c2030000006a47304402204c27a2c04df44f34bd71ec69cc0a24291a96f265217473affb3c3fce2dbd937202202c2ad2e6cfaac3901c807d9b048ccb2b5e7b0dbd922f2066e637f6bbf459313a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000000f5fee80300000000000017a9146569d9a853a1934c642223a9432f18c3b3f2a64b87e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac67a84601000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac87caee60","locktime":1626262151,"size":477,"time":1626262632,"txid":"e64531613f909647651ac3f8fd72f3e6f72ac6e01c5a1d923884a10476f56a7f","version":1,"vin":[{"coinbase":null,"scriptSig":{"asm":"OP_PUSHBYTES_71 30440220639ac218f572520c7d8addae74be6bfdefa9c86bc91474b6dedd7e117d232085022015a92f45f9ae5cee08c188e01fc614b77c461a41733649a55abfcc3e7ca2074441 OP_PUSHBYTES_33 036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202c","hex":"4730440220639ac218f572520c7d8addae74be6bfdefa9c86bc91474b6dedd7e117d232085022015a92f45f9ae5cee08c188e01fc614b77c461a41733649a55abfcc3e7ca207444121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202c"},"sequence":4294967295,"txid":"c295aa9c574e38e5361d08c9f9d2bda79a64ae8446635dad1bec20f2580fc1eb","vout":2},{"coinbase":null,"scriptSig":{"asm":"OP_PUSHBYTES_71 304402204c27a2c04df44f34bd71ec69cc0a24291a96f265217473affb3c3fce2dbd937202202c2ad2e6cfaac3901c807d9b048ccb2b5e7b0dbd922f2066e637f6bbf459313a41 OP_PUSHBYTES_33 036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202c","hex":"47304402204c27a2c04df44f34bd71ec69cc0a24291a96f265217473affb3c3fce2dbd937202202c2ad2e6cfaac3901c807d9b048ccb2b5e7b0dbd922f2066e637f6bbf459313a4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202c"},"sequence":4294967295,"txid":"c295aa9c574e38e5361d08c9f9d2bda79a64ae8446635dad1bec20f2580fc1eb","vout":3}],"vout":[{"n":0,"scriptPubKey":{"addresses":[],"asm":"OP_RETURN OP_PUSHBYTES_4 534c5000 OP_PUSHBYTES_1 01 OP_PUSHBYTES_4 53454e44 OP_PUSHBYTES_32 bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7 OP_PUSHBYTES_8 00000000000003e8 OP_PUSHBYTES_8 000000000000f5fe","hex":"6a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e808000000000000f5fe","type":"nulldata"},"value_coin":0.0,"value_satoshi":0},{"n":1,"scriptPubKey":{"addresses":["bchtest:ppjknkdg2wsexnryyg36jse0rrpm8u4xfv9hwa0rgl"],"asm":"OP_HASH160 OP_PUSHBYTES_20 6569d9a853a1934c642223a9432f18c3b3f2a64b OP_EQUAL","hex":"a9146569d9a853a1934c642223a9432f18c3b3f2a64b87","type":"scripthash"},"value_coin":0.00001,"value_satoshi":1000},{"n":2,"scriptPubKey":{"addresses":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"asm":"OP_DUP OP_HASH160 OP_PUSHBYTES_20 8cfffc2409d063437d6aa8b75a009b9ba51b71fc OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac","type":"pubkeyhash"},"value_coin":0.00001,"value_satoshi":1000},{"n":3,"scriptPubKey":{"addresses":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"asm":"OP_DUP OP_HASH160 OP_PUSHBYTES_20 8cfffc2409d063437d6aa8b75a009b9ba51b71fc OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac","type":"pubkeyhash"},"value_coin":0.21407847,"value_satoshi":21407847}]}"#; + let _: RpcTransaction = json::from_str(verbose).expect("!json::from_str"); +} + +#[test] +fn tbch_electroncash_verbose_tx_unconfirmed() { + let verbose = r#"{"blockhash":null,"blocktime":null,"confirmations":null,"hash":"e5c9ec5013fca3a62fdf880d1a98f1096a00d20ceaeb6a4cb88ddbea6f1e185a","height":null,"hex":"01000000017f6af57604a18438921d5a1ce0c62af7e6f372fdf8c31a654796903f613145e6030000006b483045022100c335dd0f22e047b806a9d84e02b70aab609093e960888f6f1878e605a173e3da02201c274ce4983d8e519a47c4bd17aeca897b084954ce7a9d77033100e06aa999304121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff0280969800000000001976a914eed5d3ad264ffc68fc0a6454e1696a30d8f405be88acbe0dae00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac7a361261","locktime":1628583546,"size":226,"time":null,"txid":"e5c9ec5013fca3a62fdf880d1a98f1096a00d20ceaeb6a4cb88ddbea6f1e185a","version":1,"vin":[{"coinbase":null,"scriptSig":{"asm":"OP_PUSHBYTES_72 3045022100c335dd0f22e047b806a9d84e02b70aab609093e960888f6f1878e605a173e3da02201c274ce4983d8e519a47c4bd17aeca897b084954ce7a9d77033100e06aa9993041 OP_PUSHBYTES_33 036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202c","hex":"483045022100c335dd0f22e047b806a9d84e02b70aab609093e960888f6f1878e605a173e3da02201c274ce4983d8e519a47c4bd17aeca897b084954ce7a9d77033100e06aa999304121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202c"},"sequence":4294967295,"txid":"e64531613f909647651ac3f8fd72f3e6f72ac6e01c5a1d923884a10476f56a7f","vout":3}],"vout":[{"n":0,"scriptPubKey":{"addresses":["bchtest:qrhdt5adye8lc68upfj9fctfdgcd3aq9hctf8ft6md"],"asm":"OP_DUP OP_HASH160 OP_PUSHBYTES_20 eed5d3ad264ffc68fc0a6454e1696a30d8f405be OP_EQUALVERIFY OP_CHECKSIG","hex":"76a914eed5d3ad264ffc68fc0a6454e1696a30d8f405be88ac","type":"pubkeyhash"},"value_coin":0.1,"value_satoshi":10000000},{"n":1,"scriptPubKey":{"addresses":["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66"],"asm":"OP_DUP OP_HASH160 OP_PUSHBYTES_20 8cfffc2409d063437d6aa8b75a009b9ba51b71fc OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac","type":"pubkeyhash"},"value_coin":0.11406782,"value_satoshi":11406782}]}"#; + let _: RpcTransaction = json::from_str(verbose).expect("!json::from_str"); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_to_p2pkh() { - UtxoStandardCoin::ordered_mature_unspents.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -2996,15 +3058,16 @@ fn test_withdraw_to_p2pkh() { // Create a p2pkh address for the test coin let p2pkh_address = Address { prefix: coin.as_ref().conf.pub_addr_prefix, - hash: coin.as_ref().my_address.hash.clone(), + hash: coin.as_ref().derivation_method.unwrap_iguana().hash.clone(), t_addr_prefix: coin.as_ref().conf.pub_t_addr_prefix, - checksum_type: coin.as_ref().my_address.checksum_type, + checksum_type: coin.as_ref().derivation_method.unwrap_iguana().checksum_type, hrp: coin.as_ref().conf.bech32_hrp.clone(), addr_format: UtxoAddressFormat::Standard, }; let withdraw_req = WithdrawRequest { amount: 1.into(), + from: None, to: p2pkh_address.to_string(), coin: TEST_COIN_NAME.into(), max: false, @@ -3022,7 +3085,7 @@ fn test_withdraw_to_p2pkh() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_to_p2sh() { - UtxoStandardCoin::ordered_mature_unspents.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -3042,15 +3105,16 @@ fn test_withdraw_to_p2sh() { // Create a p2sh address for the test coin let p2sh_address = Address { prefix: coin.as_ref().conf.p2sh_addr_prefix, - hash: coin.as_ref().my_address.hash.clone(), + hash: coin.as_ref().derivation_method.unwrap_iguana().hash.clone(), t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - checksum_type: coin.as_ref().my_address.checksum_type, + checksum_type: coin.as_ref().derivation_method.unwrap_iguana().checksum_type, hrp: coin.as_ref().conf.bech32_hrp.clone(), addr_format: UtxoAddressFormat::Standard, }; let withdraw_req = WithdrawRequest { amount: 1.into(), + from: None, to: p2sh_address.to_string(), coin: TEST_COIN_NAME.into(), max: false, @@ -3068,7 +3132,7 @@ fn test_withdraw_to_p2sh() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_to_p2wpkh() { - UtxoStandardCoin::ordered_mature_unspents.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -3088,15 +3152,16 @@ fn test_withdraw_to_p2wpkh() { // Create a p2wpkh address for the test coin let p2wpkh_address = Address { prefix: coin.as_ref().conf.pub_addr_prefix, - hash: coin.as_ref().my_address.hash.clone(), + hash: coin.as_ref().derivation_method.unwrap_iguana().hash.clone(), t_addr_prefix: coin.as_ref().conf.pub_t_addr_prefix, - checksum_type: coin.as_ref().my_address.checksum_type, + checksum_type: coin.as_ref().derivation_method.unwrap_iguana().checksum_type, hrp: coin.as_ref().conf.bech32_hrp.clone(), addr_format: UtxoAddressFormat::Segwit, }; let withdraw_req = WithdrawRequest { amount: 1.into(), + from: None, to: p2wpkh_address.to_string(), coin: TEST_COIN_NAME.into(), max: false, @@ -3106,7 +3171,834 @@ fn test_withdraw_to_p2wpkh() { let transaction: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); - let expected_script = Builder::build_p2wpkh(&p2wpkh_address.hash); + let expected_script = Builder::build_witness_script(&p2wpkh_address.hash); assert_eq!(output_script, expected_script); } + +/// `UtxoStandardCoin` has to check UTXO maturity if `check_utxo_maturity` is `true`. +/// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 +#[test] +fn test_utxo_standard_with_check_utxo_maturity_true() { + /// Whether [`UtxoStandardCoin::get_mature_unspent_ordered_list`] is called or not. + static mut GET_MATURE_UNSPENT_ORDERED_LIST_CALLED: bool = false; + + UtxoStandardCoin::get_mature_unspent_ordered_list.mock_safe(|coin, _| { + unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED = true }; + let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); + MockResult::Return(Box::pin(futures::future::ok((MatureUnspentList::default(), cache)))) + }); + + let conf = json!({"coin":"RICK","asset":"RICK","rpcport":25435,"txversion":4,"overwintered":1,"mm2":1,"protocol":{"type":"UTXO"}}); + let req = json!({ + "method": "electrum", + "servers": [ + {"url":"electrum1.cipig.net:10017"}, + {"url":"electrum2.cipig.net:10017"}, + {"url":"electrum3.cipig.net:10017"}, + ], + "check_utxo_maturity": true, + }); + + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + + let coin = block_on(utxo_standard_coin_with_priv_key( + &ctx, "RICK", &conf, ¶ms, &[1u8; 32], + )) + .unwrap(); + + let address = Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"); + // Don't use `block_on` here because it's used within a mock of [`GetUtxoListOps::get_mature_unspent_ordered_list`]. + coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); + assert!(unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED }); +} + +/// `UtxoStandardCoin` hasn't to check UTXO maturity if `check_utxo_maturity` is not set. +/// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 +#[test] +fn test_utxo_standard_without_check_utxo_maturity() { + /// Whether [`UtxoStandardCoin::get_all_unspent_ordered_list`] is called or not. + static mut GET_ALL_UNSPENT_ORDERED_LIST_CALLED: bool = false; + + UtxoStandardCoin::get_all_unspent_ordered_list.mock_safe(|coin, _| { + unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED = true }; + let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); + let unspents = Vec::new(); + MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) + }); + + UtxoStandardCoin::get_mature_unspent_ordered_list.mock_safe(|_, _| { + panic!("'UtxoStandardCoin::get_mature_unspent_ordered_list' is not expected to be called when `check_utxo_maturity` is not set") + }); + + let conf = json!({"coin":"RICK","asset":"RICK","rpcport":25435,"txversion":4,"overwintered":1,"mm2":1,"protocol":{"type":"UTXO"}}); + let req = json!({ + "method": "electrum", + "servers": [ + {"url":"electrum1.cipig.net:10017"}, + {"url":"electrum2.cipig.net:10017"}, + {"url":"electrum3.cipig.net:10017"}, + ] + }); + + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + + let coin = block_on(utxo_standard_coin_with_priv_key( + &ctx, "RICK", &conf, ¶ms, &[1u8; 32], + )) + .unwrap(); + + let address = Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"); + // Don't use `block_on` here because it's used within a mock of [`UtxoStandardCoin::get_all_unspent_ordered_list`]. + coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); + assert!(unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED }); +} + +/// `QtumCoin` has to check UTXO maturity if `check_utxo_maturity` is not set. +/// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 +#[test] +fn test_qtum_without_check_utxo_maturity() { + /// Whether [`QtumCoin::get_mature_unspent_ordered_list`] is called or not. + static mut GET_MATURE_UNSPENT_ORDERED_LIST_CALLED: bool = false; + + QtumCoin::get_mature_unspent_ordered_list.mock_safe(|coin, _| { + unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED = true }; + let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); + MockResult::Return(Box::pin(futures::future::ok((MatureUnspentList::default(), cache)))) + }); + + let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110}); + let req = json!({ + "method": "electrum", + "servers": [ + {"url":"electrum1.cipig.net:10071"}, + {"url":"electrum2.cipig.net:10071"}, + {"url":"electrum3.cipig.net:10071"}, + ], + }); + + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + + let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, &[1u8; 32])).unwrap(); + + let address = Address::from("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE"); + // Don't use `block_on` here because it's used within a mock of [`QtumCoin::get_mature_unspent_ordered_list`]. + coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); + assert!(unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED }); +} + +/// The test is for splitting some mature unspent `QTUM` out points into 40 outputs with amount `1 QTUM` in each +#[test] +#[ignore] +fn test_split_qtum() { + let priv_key = [ + 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, + 172, 110, 180, 13, 123, 179, 10, 49, + ]; + let conf = json!({ + "coin": "tQTUM", + "name": "qtumtest", + "fname": "Qtum test", + "rpcport": 13889, + "pubtype": 120, + "p2shtype": 110, + "wiftype": 239, + "txfee": 400000, + "mm2": 1, + "required_confirmations": 1, + "mature_confirmations": 2000, + "avg_blocktime": 0.53, + "protocol": { + "type": "QTUM" + } + }); + let req = json!({ + "method": "electrum", + "servers": [ + {"url":"electrum1.cipig.net:10071"}, + {"url":"electrum2.cipig.net:10071"}, + {"url":"electrum3.cipig.net:10071"}, + ], + }); + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, &priv_key)).unwrap(); + let p2pkh_address = coin.as_ref().derivation_method.unwrap_iguana(); + let script: Script = output_script(p2pkh_address, ScriptType::P2PKH); + let key_pair = coin.as_ref().priv_key_policy.key_pair_or_err().unwrap(); + let (unspents, _) = block_on(coin.get_mature_unspent_ordered_list(p2pkh_address)).expect("Unspent list is empty"); + log!("Mature unspents vec = {:?}", unspents.mature); + let outputs = vec![ + TransactionOutput { + value: 100_000_000, + script_pubkey: script.to_bytes(), + }; + 40 + ]; + let builder = UtxoTxBuilder::new(&coin) + .add_available_inputs(unspents.mature) + .add_outputs(outputs); + let (unsigned, data) = block_on(builder.build()).unwrap(); + // fee_amount must be higher than the minimum fee + assert!(data.fee_amount > 400_000); + log!("Unsigned tx = {:?}", unsigned); + let signature_version = match p2pkh_address.addr_format { + UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, + _ => coin.as_ref().conf.signature_version, + }; + let prev_script = Builder::build_p2pkh(&p2pkh_address.hash); + let signed = sign_tx( + unsigned, + key_pair, + prev_script, + signature_version, + coin.as_ref().conf.fork_id, + ) + .unwrap(); + log!("Signed tx = {:?}", signed); + let res = block_on(coin.broadcast_tx(&signed)).unwrap(); + log!("Res = {:?}", res); +} + +/// `QtumCoin` hasn't to check UTXO maturity if `check_utxo_maturity` is `false`. +/// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 +#[test] +fn test_qtum_with_check_utxo_maturity_false() { + /// Whether [`QtumCoin::get_all_unspent_ordered_list`] is called or not. + static mut GET_ALL_UNSPENT_ORDERED_LIST_CALLED: bool = false; + + QtumCoin::get_all_unspent_ordered_list.mock_safe(|coin, _address| { + unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED = true }; + let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); + let unspents = Vec::new(); + MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) + }); + QtumCoin::get_mature_unspent_ordered_list.mock_safe(|_, _| { + panic!( + "'QtumCoin::get_mature_unspent_ordered_list' is not expected to be called when `check_utxo_maturity` is false" + ) + }); + + let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110}); + let req = json!({ + "method": "electrum", + "servers": [ + {"url":"electrum1.cipig.net:10071"}, + {"url":"electrum2.cipig.net:10071"}, + {"url":"electrum3.cipig.net:10071"}, + ], + "check_utxo_maturity": false, + }); + + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + + let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, &[1u8; 32])).unwrap(); + + let address = Address::from("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE"); + // Don't use `block_on` here because it's used within a mock of [`QtumCoin::get_all_unspent_ordered_list`]. + coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); + assert!(unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED }); +} + +#[test] +fn test_account_balance_rpc() { + let mut addresses_map: HashMap = HashMap::new(); + let mut balances_by_der_path: HashMap = HashMap::new(); + + macro_rules! known_address { + ($der_path:literal, $address:literal, $chain:expr, balance = $balance:literal) => { + addresses_map.insert($address.to_string(), $balance); + balances_by_der_path.insert($der_path.to_string(), HDAddressBalance { + address: $address.to_string(), + derivation_path: RpcDerivationPath(DerivationPath::from_str($der_path).unwrap()), + chain: $chain, + balance: CoinBalance::new(BigDecimal::from($balance)), + }) + }; + } + + macro_rules! get_balances { + ($($der_paths:literal),*) => { + [$($der_paths),*].iter().map(|der_path| balances_by_der_path.get(*der_path).unwrap().clone()).collect() + }; + } + + #[rustfmt::skip] + { + // Account#0, external addresses. + known_address!("m/44'/141'/0'/0/0", "RRqF4cYniMwYs66S4QDUUZ4GJQFQF69rBE", Bip44Chain::External, balance = 0); + known_address!("m/44'/141'/0'/0/1", "RSVLsjXc9LJ8fm9Jq7gXjeubfja3bbgSDf", Bip44Chain::External, balance = 0); + known_address!("m/44'/141'/0'/0/2", "RSSZjtgfnLzvqF4cZQJJEpN5gvK3pWmd3h", Bip44Chain::External, balance = 0); + known_address!("m/44'/141'/0'/0/3", "RU1gRFXWXNx7uPRAEJ7wdZAW1RZ4TE6Vv1", Bip44Chain::External, balance = 98); + known_address!("m/44'/141'/0'/0/4", "RUkEvRzb7mtwfVeKiSFEbYupLkcvU5KJBw", Bip44Chain::External, balance = 1); + known_address!("m/44'/141'/0'/0/5", "RP8deqVfjBbkvxbGbsQ2EGdamMaP1wxizR", Bip44Chain::External, balance = 0); + known_address!("m/44'/141'/0'/0/6", "RSvKMMegKGP5e2EanH7fnD4yNsxdJvLAmL", Bip44Chain::External, balance = 32); + + // Account#0, internal addresses. + known_address!("m/44'/141'/0'/1/0", "RLZxcZSYtKe74JZd1hBAmmD9PNHZqb72oL", Bip44Chain::Internal, balance = 13); + known_address!("m/44'/141'/0'/1/1", "RPj9JXUVnewWwVpxZDeqGB25qVqz5qJzwP", Bip44Chain::Internal, balance = 44); + known_address!("m/44'/141'/0'/1/2", "RSYdSLRYWuzBson2GDbWBa632q2PmFnCaH", Bip44Chain::Internal, balance = 10); + + // Account#1, internal addresses. + known_address!("m/44'/141'/1'/1/0", "RGo7sYzivPtzv8aRQ4A6vRJDxoqkRRBRhZ", Bip44Chain::Internal, balance = 0); + } + + NativeClient::display_balances.mock_safe(move |_, addresses: Vec
, _| { + let result: Vec<_> = addresses + .into_iter() + .map(|address| { + let address_str = address.to_string(); + let balance = addresses_map + .remove(&address_str) + .expect(&format!("Unexpected address: {}", address_str)); + (address, BigDecimal::from(balance)) + }) + .collect(); + MockResult::Return(Box::new(futures01::future::ok(result))) + }); + + let client = NativeClient(Arc::new(NativeClientImpl::default())); + let mut fields = utxo_coin_fields_for_test(UtxoRpcClientEnum::Native(client), None, false); + let mut hd_accounts = HDAccountsMap::new(); + hd_accounts.insert(0, UtxoHDAccount { + account_id: 0, + extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), + account_derivation_path: Bip44PathToAccount::from_str("m/44'/141'/0'").unwrap(), + external_addresses_number: 7, + internal_addresses_number: 3, + }); + hd_accounts.insert(1, UtxoHDAccount { + account_id: 1, + extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPQq2FdGT6JoieiQZUpTZ3WZn8fcuLJhFVmtCpXbuXxp5aPzaokwcLV2V9LE55Dwt8JYkpuMv7jXKwmyD28WbHYjBH2zhbW2p").unwrap(), + account_derivation_path: Bip44PathToAccount::from_str("m/44'/141'/1'").unwrap(), + external_addresses_number: 0, + internal_addresses_number: 1, + }); + fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { + hd_wallet_storage: HDWalletCoinStorage::default(), + address_format: UtxoAddressFormat::Standard, + derivation_path: Bip44PathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + gap_limit: 3, + }); + let coin = utxo_coin_from_fields(fields); + + // Request a balance of Account#0, external addresses, 1st page + + let params = AccountBalanceParams { + account_index: 0, + chain: Bip44Chain::External, + limit: 3, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()), + }; + let actual = block_on(coin.account_balance_rpc(params)).expect("!account_balance_rpc"); + let expected = HDAccountBalanceResponse { + account_index: 0, + derivation_path: DerivationPath::from_str("m/44'/141'/0'").unwrap().into(), + addresses: get_balances!("m/44'/141'/0'/0/0", "m/44'/141'/0'/0/1", "m/44'/141'/0'/0/2"), + page_balance: CoinBalance::new(BigDecimal::from(0)), + limit: 3, + skipped: 0, + total: 7, + total_pages: 3, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()), + }; + assert_eq!(actual, expected); + + // Request a balance of Account#0, external addresses, 2nd page + + let params = AccountBalanceParams { + account_index: 0, + chain: Bip44Chain::External, + limit: 3, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(2).unwrap()), + }; + let actual = block_on(coin.account_balance_rpc(params)).expect("!account_balance_rpc"); + let expected = HDAccountBalanceResponse { + account_index: 0, + derivation_path: DerivationPath::from_str("m/44'/141'/0'").unwrap().into(), + addresses: get_balances!("m/44'/141'/0'/0/3", "m/44'/141'/0'/0/4", "m/44'/141'/0'/0/5"), + page_balance: CoinBalance::new(BigDecimal::from(99)), + limit: 3, + skipped: 3, + total: 7, + total_pages: 3, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(2).unwrap()), + }; + assert_eq!(actual, expected); + + // Request a balance of Account#0, external addresses, 3rd page + + let params = AccountBalanceParams { + account_index: 0, + chain: Bip44Chain::External, + limit: 3, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(3).unwrap()), + }; + let actual = block_on(coin.account_balance_rpc(params)).expect("!account_balance_rpc"); + let expected = HDAccountBalanceResponse { + account_index: 0, + derivation_path: DerivationPath::from_str("m/44'/141'/0'").unwrap().into(), + addresses: get_balances!("m/44'/141'/0'/0/6"), + page_balance: CoinBalance::new(BigDecimal::from(32)), + limit: 3, + skipped: 6, + total: 7, + total_pages: 3, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(3).unwrap()), + }; + assert_eq!(actual, expected); + + // Request a balance of Account#0, external addresses, page 4 (out of bound) + + let params = AccountBalanceParams { + account_index: 0, + chain: Bip44Chain::External, + limit: 3, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(4).unwrap()), + }; + let actual = block_on(coin.account_balance_rpc(params)).expect("!account_balance_rpc"); + let expected = HDAccountBalanceResponse { + account_index: 0, + derivation_path: DerivationPath::from_str("m/44'/141'/0'").unwrap().into(), + addresses: Vec::new(), + page_balance: CoinBalance::default(), + limit: 3, + skipped: 7, + total: 7, + total_pages: 3, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(4).unwrap()), + }; + assert_eq!(actual, expected); + + // Request a balance of Account#0, internal addresses, where idx > 0 + + let params = AccountBalanceParams { + account_index: 0, + chain: Bip44Chain::Internal, + limit: 3, + paging_options: PagingOptionsEnum::FromId(0), + }; + let actual = block_on(coin.account_balance_rpc(params)).expect("!account_balance_rpc"); + let expected = HDAccountBalanceResponse { + account_index: 0, + derivation_path: DerivationPath::from_str("m/44'/141'/0'").unwrap().into(), + addresses: get_balances!("m/44'/141'/0'/1/1", "m/44'/141'/0'/1/2"), + page_balance: CoinBalance::new(BigDecimal::from(54)), + limit: 3, + skipped: 1, + total: 3, + total_pages: 1, + paging_options: PagingOptionsEnum::FromId(0), + }; + assert_eq!(actual, expected); + + // Request a balance of Account#1, external addresses, page 1 (out of bound) + + let params = AccountBalanceParams { + account_index: 1, + chain: Bip44Chain::External, + limit: 3, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()), + }; + let actual = block_on(coin.account_balance_rpc(params)).expect("!account_balance_rpc"); + let expected = HDAccountBalanceResponse { + account_index: 1, + derivation_path: DerivationPath::from_str("m/44'/141'/1'").unwrap().into(), + addresses: Vec::new(), + page_balance: CoinBalance::default(), + limit: 3, + skipped: 0, + total: 0, + total_pages: 0, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()), + }; + assert_eq!(actual, expected); + + // Request a balance of Account#1, external addresses, page 1 + + let params = AccountBalanceParams { + account_index: 1, + chain: Bip44Chain::Internal, + limit: 3, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()), + }; + let actual = block_on(coin.account_balance_rpc(params)).expect("!account_balance_rpc"); + let expected = HDAccountBalanceResponse { + account_index: 1, + derivation_path: DerivationPath::from_str("m/44'/141'/1'").unwrap().into(), + addresses: get_balances!("m/44'/141'/1'/1/0"), + page_balance: CoinBalance::new(BigDecimal::from(0)), + limit: 3, + skipped: 0, + total: 1, + total_pages: 1, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()), + }; + assert_eq!(actual, expected); + + // Request a balance of Account#1, external addresses, where idx > 0 (out of bound) + + let params = AccountBalanceParams { + account_index: 1, + chain: Bip44Chain::Internal, + limit: 3, + paging_options: PagingOptionsEnum::FromId(0), + }; + let actual = block_on(coin.account_balance_rpc(params)).expect("!account_balance_rpc"); + let expected = HDAccountBalanceResponse { + account_index: 1, + derivation_path: DerivationPath::from_str("m/44'/141'/1'").unwrap().into(), + addresses: Vec::new(), + page_balance: CoinBalance::default(), + limit: 3, + skipped: 1, + total: 1, + total_pages: 1, + paging_options: PagingOptionsEnum::FromId(0), + }; + assert_eq!(actual, expected); +} + +#[test] +fn test_scan_for_new_addresses() { + static mut ACCOUNT_ID: u32 = 0; + static mut NEW_EXTERNAL_ADDRESSES_NUMBER: u32 = 0; + static mut NEW_INTERNAL_ADDRESSES_NUMBER: u32 = 0; + + HDWalletMockStorage::update_external_addresses_number.mock_safe( + |_, _, account_id, new_external_addresses_number| { + assert_eq!(account_id, unsafe { ACCOUNT_ID }); + assert_eq!(new_external_addresses_number, unsafe { NEW_EXTERNAL_ADDRESSES_NUMBER }); + MockResult::Return(Box::pin(futures::future::ok(()))) + }, + ); + + HDWalletMockStorage::update_internal_addresses_number.mock_safe( + |_, _, account_id, new_internal_addresses_number| { + assert_eq!(account_id, unsafe { ACCOUNT_ID }); + assert_eq!(new_internal_addresses_number, unsafe { NEW_INTERNAL_ADDRESSES_NUMBER }); + MockResult::Return(Box::pin(futures::future::ok(()))) + }, + ); + + let mut checking_addresses: HashMap> = HashMap::new(); + let mut non_empty_addresses: Vec = Vec::new(); + let mut balances_by_der_path: HashMap = HashMap::new(); + + macro_rules! new_address { + ($der_path:literal, $address:literal, $chain:expr, balance = $balance:expr) => {{ + let balance = $balance; + checking_addresses.insert($address.to_string(), balance); + balances_by_der_path.insert($der_path.to_string(), HDAddressBalance { + address: $address.to_string(), + derivation_path: RpcDerivationPath(DerivationPath::from_str($der_path).unwrap()), + chain: $chain, + balance: CoinBalance::new(BigDecimal::from(balance.unwrap_or(0))), + }); + if balance.is_some() { + non_empty_addresses.push($address.to_string()); + } + }}; + } + + macro_rules! unused_address { + ($_der_path:literal, $address:literal) => { + checking_addresses.insert($address.to_string(), None) + }; + } + + macro_rules! get_balances { + ($($der_paths:literal),*) => { + [$($der_paths),*].iter().map(|der_path| balances_by_der_path.get(*der_path).unwrap().clone()).collect() + }; + } + + // Please note that the order of the `known` and `new` addresses is important. + #[rustfmt::skip] + { + // Account#0, external addresses. + new_address!("m/44'/141'/0'/0/3", "RU1gRFXWXNx7uPRAEJ7wdZAW1RZ4TE6Vv1", Bip44Chain::External, balance = Some(98)); + unused_address!("m/44'/141'/0'/0/4", "RUkEvRzb7mtwfVeKiSFEbYupLkcvU5KJBw"); + unused_address!("m/44'/141'/0'/0/5", "RP8deqVfjBbkvxbGbsQ2EGdamMaP1wxizR"); + unused_address!("m/44'/141'/0'/0/6", "RSvKMMegKGP5e2EanH7fnD4yNsxdJvLAmL"); // Stop searching for a non-empty address (gap_limit = 3). + + // Account#0, internal addresses. + new_address!("m/44'/141'/0'/1/1", "RPj9JXUVnewWwVpxZDeqGB25qVqz5qJzwP", Bip44Chain::Internal, balance = Some(98)); + new_address!("m/44'/141'/0'/1/2", "RSYdSLRYWuzBson2GDbWBa632q2PmFnCaH", Bip44Chain::Internal, balance = None); + new_address!("m/44'/141'/0'/1/3", "RQstQeTUEZLh6c3YWJDkeVTTQoZUsfvNCr", Bip44Chain::Internal, balance = Some(14)); + unused_address!("m/44'/141'/0'/1/4", "RT54m6pfj9scqwSLmYdfbmPcrpxnWGAe9J"); + unused_address!("m/44'/141'/0'/1/5", "RYWfEFxqA6zya9c891Dj7vxiDojCmuWR9T"); + unused_address!("m/44'/141'/0'/1/6", "RSkY6twW8knTcn6wGACUAG9crJHcuQ2kEH"); // Stop searching for a non-empty address (gap_limit = 3). + + // Account#1, external addresses. + new_address!("m/44'/141'/1'/0/0", "RBQFLwJ88gVcnfkYvJETeTAB6AAYLow12K", Bip44Chain::External, balance = Some(9)); + new_address!("m/44'/141'/1'/0/1", "RCyy77sRWFa2oiFPpyimeTQfenM1aRoiZs", Bip44Chain::External, balance = Some(7)); + new_address!("m/44'/141'/1'/0/2", "RDnNa3pQmisfi42KiTZrfYfuxkLC91PoTJ", Bip44Chain::External, balance = None); + new_address!("m/44'/141'/1'/0/3", "RQRGgXcGJz93CoAfQJoLgBz2r9HtJYMX3Z", Bip44Chain::External, balance = None); + new_address!("m/44'/141'/1'/0/4", "RM6cqSFCFZ4J1LngLzqKkwo2ouipbDZUbm", Bip44Chain::External, balance = Some(11)); + unused_address!("m/44'/141'/1'/0/5", "RX2fGBZjNZMNdNcnc5QBRXvmsXTvadvTPN"); + unused_address!("m/44'/141'/1'/0/6", "RJJ7muUETyp59vxVXna9KAZ9uQ1QSqmcjE"); + unused_address!("m/44'/141'/1'/0/7", "RYJ6vbhxFre5yChCMiJJFNTTBhAQbKM9AY"); // Stop searching for a non-empty address (gap_limit = 3). + + // Account#1, internal addresses. + unused_address!("m/44'/141'/1'/0/2", "RCjRDibDAXKYpVYSUeJXrbTzZ1UEKYAwJa"); + unused_address!("m/44'/141'/1'/0/3", "REs1NRzg8XjwN3v8Jp1wQUAyQb3TzeT8EB"); + unused_address!("m/44'/141'/1'/0/4", "RS4UZtkwZ8eYaTL1xodXgFNryJoTbPJYE5"); // Stop searching for a non-empty address (gap_limit = 3). + } + + NativeClient::display_balance.mock_safe(move |_, address: Address, _| { + let address = address.to_string(); + let balance = checking_addresses + .remove(&address) + .expect(&format!("Unexpected address: {}", address)) + .expect(&format!( + "'{}' address is empty. 'NativeClient::display_balance' must not be called for this address", + address + )); + MockResult::Return(Box::new(futures01::future::ok(BigDecimal::from(balance)))) + }); + + NativeClient::list_all_transactions.mock_safe(move |_, _| { + let tx_history = non_empty_addresses + .clone() + .into_iter() + .map(|address| ListTransactionsItem { + address, + ..ListTransactionsItem::default() + }) + .collect(); + MockResult::Return(Box::new(futures01::future::ok(tx_history))) + }); + + let client = NativeClient(Arc::new(NativeClientImpl::default())); + let mut fields = utxo_coin_fields_for_test(UtxoRpcClientEnum::Native(client), None, false); + let mut hd_accounts = HDAccountsMap::new(); + hd_accounts.insert(0, UtxoHDAccount { + account_id: 0, + extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), + account_derivation_path: Bip44PathToAccount::from_str("m/44'/141'/0'").unwrap(), + external_addresses_number: 3, + internal_addresses_number: 1, + }); + hd_accounts.insert(1, UtxoHDAccount { + account_id: 1, + extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPQq2FdGT6JoieiQZUpTZ3WZn8fcuLJhFVmtCpXbuXxp5aPzaokwcLV2V9LE55Dwt8JYkpuMv7jXKwmyD28WbHYjBH2zhbW2p").unwrap(), + account_derivation_path: Bip44PathToAccount::from_str("m/44'/141'/1'").unwrap(), + external_addresses_number: 0, + internal_addresses_number: 2, + }); + fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { + hd_wallet_storage: HDWalletCoinStorage::default(), + address_format: UtxoAddressFormat::Standard, + derivation_path: Bip44PathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + gap_limit: 3, + }); + let coin = utxo_coin_from_fields(fields); + + // Check balance of Account#0 + + unsafe { + ACCOUNT_ID = 0; + NEW_EXTERNAL_ADDRESSES_NUMBER = 4; + NEW_INTERNAL_ADDRESSES_NUMBER = 4; + } + + let params = ScanAddressesParams { + account_index: 0, + gap_limit: Some(3), + }; + let actual = block_on(coin.init_scan_for_new_addresses_rpc(params)).expect("!account_balance_rpc"); + let expected = ScanAddressesResponse { + account_index: 0, + derivation_path: DerivationPath::from_str("m/44'/141'/0'").unwrap().into(), + new_addresses: get_balances!( + "m/44'/141'/0'/0/3", + "m/44'/141'/0'/1/1", + "m/44'/141'/0'/1/2", + "m/44'/141'/0'/1/3" + ), + }; + assert_eq!(actual, expected); + + // Check balance of Account#1 + + unsafe { + ACCOUNT_ID = 1; + NEW_EXTERNAL_ADDRESSES_NUMBER = 5; + NEW_INTERNAL_ADDRESSES_NUMBER = 2; + } + + let params = ScanAddressesParams { + account_index: 1, + gap_limit: None, + }; + let actual = block_on(coin.init_scan_for_new_addresses_rpc(params)).expect("!account_balance_rpc"); + let expected = ScanAddressesResponse { + account_index: 1, + derivation_path: DerivationPath::from_str("m/44'/141'/1'").unwrap().into(), + new_addresses: get_balances!( + "m/44'/141'/1'/0/0", + "m/44'/141'/1'/0/1", + "m/44'/141'/1'/0/2", + "m/44'/141'/1'/0/3", + "m/44'/141'/1'/0/4" + ), + }; + assert_eq!(actual, expected); + + let accounts = match coin.as_ref().derivation_method { + DerivationMethod::HDWallet(UtxoHDWallet { ref accounts, .. }) => block_on(accounts.lock()).clone(), + _ => unreachable!(), + }; + assert_eq!(accounts[&0].external_addresses_number, 4); + assert_eq!(accounts[&0].internal_addresses_number, 4); + assert_eq!(accounts[&1].external_addresses_number, 5); + assert_eq!(accounts[&1].internal_addresses_number, 2); +} + +/// https://github.com/KomodoPlatform/atomicDEX-API/issues/1196 +#[test] +fn test_electrum_balance_deserializing() { + let serialized = r#"{"confirmed": 988937858554305, "unconfirmed": 18446720562229577551}"#; + let actual: ElectrumBalance = json::from_str(serialized).unwrap(); + assert_eq!(actual.confirmed, 988937858554305i128); + assert_eq!(actual.unconfirmed, 18446720562229577551i128); + + let serialized = r#"{"confirmed": -170141183460469231731687303715884105728, "unconfirmed": 170141183460469231731687303715884105727}"#; + let actual: ElectrumBalance = json::from_str(serialized).unwrap(); + assert_eq!(actual.confirmed, i128::MIN); + assert_eq!(actual.unconfirmed, i128::MAX); +} + +#[test] +fn test_electrum_display_balances() { + let rpc_client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + block_on(utxo_common_tests::test_electrum_display_balances(&rpc_client)); +} + +#[test] +fn test_native_display_balances() { + let unspents = vec![ + NativeUnspent { + address: "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".to_owned(), + amount: "4.77699".into(), + ..NativeUnspent::default() + }, + NativeUnspent { + address: "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".to_owned(), + amount: "0.77699".into(), + ..NativeUnspent::default() + }, + NativeUnspent { + address: "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".to_owned(), + amount: "0.99998".into(), + ..NativeUnspent::default() + }, + NativeUnspent { + address: "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".to_owned(), + amount: "1".into(), + ..NativeUnspent::default() + }, + ]; + + NativeClient::list_unspent_impl + .mock_safe(move |_, _, _, _| MockResult::Return(Box::new(futures01::future::ok(unspents.clone())))); + + let rpc_client = native_client_for_test(); + + let addresses = vec![ + "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), + "RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), + "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), + "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), + ]; + let actual = rpc_client + .display_balances(addresses, TEST_COIN_DECIMALS) + .wait() + .unwrap(); + + let expected: Vec<(Address, BigDecimal)> = vec![ + ( + "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), + BigDecimal::try_from(5.77699).unwrap(), + ), + ("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), BigDecimal::from(0)), + ( + "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), + BigDecimal::try_from(0.77699).unwrap(), + ), + ( + "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), + BigDecimal::try_from(0.99998).unwrap(), + ), + ]; + assert_eq!(actual, expected); +} + +#[test] +fn test_message_hash() { + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + let coin = utxo_coin_for_test( + client.into(), + Some("spice describe gravity federal blast come thank unfair canal monkey style afraid"), + false, + ); + let expected = H256::from_reversed_str("5aef9b67485adba55a2cd935269e73f2f9876382f1eada02418797ae76c07e18"); + let result = coin.sign_message_hash("test"); + assert!(result.is_some()); + assert_eq!(H256::from(result.unwrap()), expected); +} + +#[test] +fn test_sign_verify_message() { + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + let coin = utxo_coin_for_test( + client.into(), + Some("spice describe gravity federal blast come thank unfair canal monkey style afraid"), + false, + ); + + let message = "test"; + let signature = coin.sign_message(message).unwrap(); + assert_eq!( + signature, + "HzetbqVj9gnUOznon9bvE61qRlmjH5R+rNgkxu8uyce3UBbOu+2aGh7r/GGSVFGZjRnaYC60hdwtdirTKLb7bE4=" + ); + + let address = "R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"; + let is_valid = coin.verify_message(&signature, message, address).unwrap(); + assert!(is_valid); +} + +#[test] +fn test_sign_verify_message_segwit() { + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + let coin = utxo_coin_for_test( + client.into(), + Some("spice describe gravity federal blast come thank unfair canal monkey style afraid"), + true, + ); + + let message = "test"; + let signature = coin.sign_message(message).unwrap(); + assert_eq!( + signature, + "HzetbqVj9gnUOznon9bvE61qRlmjH5R+rNgkxu8uyce3UBbOu+2aGh7r/GGSVFGZjRnaYC60hdwtdirTKLb7bE4=" + ); + + let is_valid = coin + .verify_message(&signature, message, "rck1qqk4t2dppvmu9jja0z7nan0h464n5gve8h7nhay") + .unwrap(); + assert!(is_valid); + + let is_valid = coin + .verify_message(&signature, message, "R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW") + .unwrap(); + assert!(is_valid); +} diff --git a/mm2src/coins/utxo/utxo_wasm_tests.rs b/mm2src/coins/utxo/utxo_wasm_tests.rs index 9ac222f926..4eaaf01d79 100644 --- a/mm2src/coins/utxo/utxo_wasm_tests.rs +++ b/mm2src/coins/utxo/utxo_wasm_tests.rs @@ -1,6 +1,8 @@ -use super::rpc_clients::ElectrumProtocol; +use super::rpc_clients::{ElectrumClient, ElectrumClientImpl, ElectrumProtocol}; use super::*; use crate::utxo::rpc_clients::UtxoRpcClientOps; +use crate::utxo::utxo_common_tests; +use common::executor::Timer; use serialization::deserialize; use wasm_bindgen_test::*; @@ -43,7 +45,7 @@ async fn test_electrum_rpc_client() { .as_slice() .into(); let verbose_tx = client - .get_verbose_transaction(tx_hash) + .get_verbose_transaction(&tx_hash) .compat() .await .expect("!get_verbose_transaction"); @@ -51,3 +53,9 @@ async fn test_electrum_rpc_client() { let expected = UtxoTx::from("0400008085202f8902358549fe3cf9a66bf61fb57bca1b3b49434a148a4dc29450b5eefe583f2f9ecf000000006a4730440220112aa3737672f8aa16a58426f5e7656ad13d21a219390c7a0b2e266ee6b216a8022008e9f9e94db91f069f831b0d40b7f75938122cddceaa25197146dfb00fe82599012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff358549fe3cf9a66bf61fb57bca1b3b49434a148a4dc29450b5eefe583f2f9ecf010000006b483045022100d054464799246254b09f96333bf52537938abe31c24bacf41c9ef600b28155950220527ec33c4a5bef79dcabf97e38aa240fecdd14c96f698560b2f10ec2abc2e992012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0240420f00000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac66418f00000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac0e2aa85f000000000000000000000000000000"); assert_eq!(actual, expected); } + +#[wasm_bindgen_test] +async fn test_electrum_display_balances() { + let rpc_client = electrum_client_for_test(&["electrum1.cipig.net:30017", "electrum2.cipig.net:30017"]).await; + utxo_common_tests::test_electrum_display_balances(&rpc_client).await; +} diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs new file mode 100644 index 0000000000..b54fd7a5fd --- /dev/null +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -0,0 +1,447 @@ +use crate::rpc_command::init_withdraw::{WithdrawAwaitingStatus, WithdrawInProgressStatus, WithdrawTaskHandle}; +use crate::utxo::utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; +use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, FeePolicy, GetUtxoListOps, PrivKeyPolicy, + UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; +use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionDetails, WithdrawError, + WithdrawFee, WithdrawRequest, WithdrawResult}; +use async_trait::async_trait; +use chain::TransactionOutput; +use common::log::info; +use common::now_ms; +use crypto::hw_rpc_task::{HwConnectStatuses, TrezorRpcTaskConnectProcessor}; +use crypto::trezor::client::TrezorClient; +use crypto::trezor::{TrezorError, TrezorProcessingError}; +use crypto::{Bip32Error, CryptoCtx, CryptoInitError, DerivationPath, HwError, HwProcessingError}; +use keys::{Public as PublicKey, Type as ScriptType}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc::v1::types::ToTxHash; +use rpc_task::RpcTaskError; +use script::{Builder, Script, SignatureVersion, TransactionInputSigner}; +use serialization::{serialize, serialize_with_flags, SERIALIZE_TRANSACTION_WITNESS}; +use std::iter::once; +use std::time::Duration; +use utxo_signer::sign_params::{SendingOutputInfo, SpendingInputInfo, UtxoSignTxParamsBuilder}; +use utxo_signer::{with_key_pair, UtxoSignTxError}; +use utxo_signer::{SignPolicy, UtxoSignerOps}; + +const TREZOR_CONNECT_TIMEOUT: Duration = Duration::from_secs(300); +const TREZOR_PIN_TIMEOUT: Duration = Duration::from_secs(300); + +impl From for WithdrawError { + fn from(sign_err: UtxoSignTxError) -> Self { + match sign_err { + UtxoSignTxError::TrezorError(trezor) => WithdrawError::from(trezor), + UtxoSignTxError::Transport(transport) => WithdrawError::Transport(transport), + UtxoSignTxError::Internal(internal) => WithdrawError::InternalError(internal), + sign_err => WithdrawError::InternalError(sign_err.to_string()), + } + } +} + +impl From> for WithdrawError { + fn from(e: HwProcessingError) -> Self { + match e { + HwProcessingError::HwError(hw) => WithdrawError::from(hw), + HwProcessingError::ProcessorError(rpc_task) => WithdrawError::from(rpc_task), + } + } +} + +impl From> for WithdrawError { + fn from(e: TrezorProcessingError) -> Self { + match e { + TrezorProcessingError::TrezorError(trezor) => WithdrawError::from(trezor), + TrezorProcessingError::ProcessorError(rpc_task) => WithdrawError::from(rpc_task), + } + } +} + +impl From for WithdrawError { + fn from(e: HwError) -> Self { + let error = e.to_string(); + match e { + HwError::NoTrezorDeviceAvailable => WithdrawError::NoTrezorDeviceAvailable, + HwError::FoundUnexpectedDevice { .. } => WithdrawError::FoundUnexpectedDevice(error), + _ => WithdrawError::HardwareWalletInternal(error), + } + } +} + +impl From for WithdrawError { + fn from(e: TrezorError) -> Self { WithdrawError::HardwareWalletInternal(e.to_string()) } +} + +impl From for WithdrawError { + fn from(e: CryptoInitError) -> Self { WithdrawError::InternalError(e.to_string()) } +} + +impl From for WithdrawError { + fn from(e: RpcTaskError) -> Self { + let error = e.to_string(); + match e { + RpcTaskError::Canceled => WithdrawError::InternalError("Canceled".to_owned()), + RpcTaskError::Timeout(timeout) => WithdrawError::Timeout(timeout), + RpcTaskError::NoSuchTask(_) | RpcTaskError::UnexpectedTaskStatus { .. } => { + WithdrawError::InternalError(error) + }, + RpcTaskError::Internal(internal) => WithdrawError::InternalError(internal), + } + } +} + +impl From for WithdrawError { + fn from(e: Bip32Error) -> Self { + WithdrawError::HardwareWalletInternal(format!("Error parsing pubkey received from Hardware Wallet: {}", e)) + } +} + +#[async_trait] +pub trait UtxoWithdraw +where + Self: Sized + Sync, + Coin: UtxoCommonOps + GetUtxoListOps, +{ + fn coin(&self) -> &Coin; + + fn sender_address(&self) -> Address; + + fn sender_address_string(&self) -> String; + + fn request(&self) -> &WithdrawRequest; + + fn signature_version(&self) -> SignatureVersion { + match self.sender_address().addr_format { + UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, + _ => self.coin().as_ref().conf.signature_version, + } + } + + fn prev_script(&self) -> Script { Builder::build_p2pkh(&self.sender_address().hash) } + + fn on_generating_transaction(&self) -> Result<(), MmError>; + + fn on_finishing(&self) -> Result<(), MmError>; + + async fn sign_tx(&self, unsigned_tx: TransactionInputSigner) -> Result>; + + async fn build(self) -> WithdrawResult { + let coin = self.coin(); + let ticker = coin.as_ref().conf.ticker.clone(); + let decimals = coin.as_ref().decimals; + let conf = &self.coin().as_ref().conf; + let req = self.request(); + + let to = coin + .address_from_str(&req.to) + .map_to_mm(WithdrawError::InvalidAddress)?; + + let is_p2pkh = to.prefix == conf.pub_addr_prefix && to.t_addr_prefix == conf.pub_t_addr_prefix; + let is_p2sh = to.prefix == conf.p2sh_addr_prefix && to.t_addr_prefix == conf.p2sh_t_addr_prefix; + + let script_type = if is_p2pkh { + ScriptType::P2PKH + } else if is_p2sh { + ScriptType::P2SH + } else { + return MmError::err(WithdrawError::InvalidAddress("Expected either P2PKH or P2SH".into())); + }; + + // Generate unsigned transaction. + self.on_generating_transaction()?; + + let script_pubkey = output_script(&to, script_type).to_bytes(); + + let _utxo_lock = UTXO_LOCK.lock().await; + let (unspents, _) = coin.get_unspent_ordered_list(&self.sender_address()).await?; + let (value, fee_policy) = if req.max { + ( + unspents.iter().fold(0, |sum, unspent| sum + unspent.value), + FeePolicy::DeductFromOutput(0), + ) + } else { + let value = sat_from_big_decimal(&req.amount, decimals)?; + (value, FeePolicy::SendExact) + }; + let outputs = vec![TransactionOutput { value, script_pubkey }]; + + let mut tx_builder = UtxoTxBuilder::new(coin) + .with_from_address(self.sender_address()) + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_fee_policy(fee_policy); + + match req.fee { + Some(WithdrawFee::UtxoFixed { ref amount }) => { + let fixed = sat_from_big_decimal(amount, decimals)?; + tx_builder = tx_builder.with_fee(ActualTxFee::FixedPerKb(fixed)); + }, + Some(WithdrawFee::UtxoPerKbyte { ref amount }) => { + let dynamic = sat_from_big_decimal(amount, decimals)?; + tx_builder = tx_builder.with_fee(ActualTxFee::Dynamic(dynamic)); + }, + Some(ref fee_policy) => { + let error = format!( + "Expected 'UtxoFixed' or 'UtxoPerKbyte' fee types, found {:?}", + fee_policy + ); + return MmError::err(WithdrawError::InvalidFeePolicy(error)); + }, + None => (), + }; + let (unsigned, data) = tx_builder + .build() + .await + .mm_err(|gen_tx_error| WithdrawError::from_generate_tx_error(gen_tx_error, ticker.clone(), decimals))?; + + // Sign the `unsigned` transaction. + let signed = self.sign_tx(unsigned).await?; + + // Finish by generating `TransactionDetails` from the signed transaction. + self.on_finishing()?; + + let fee_amount = data.fee_amount + data.unused_change.unwrap_or_default(); + let fee_details = UtxoFeeDetails { + coin: Some(ticker.clone()), + amount: big_decimal_from_sat(fee_amount as i64, decimals), + }; + let tx_hex = match coin.addr_format() { + UtxoAddressFormat::Segwit => serialize_with_flags(&signed, SERIALIZE_TRANSACTION_WITNESS).into(), + _ => serialize(&signed).into(), + }; + Ok(TransactionDetails { + from: vec![self.sender_address_string()], + to: vec![req.to.clone()], + total_amount: big_decimal_from_sat(data.spent_by_me as i64, decimals), + spent_by_me: big_decimal_from_sat(data.spent_by_me as i64, decimals), + received_by_me: big_decimal_from_sat(data.received_by_me as i64, decimals), + my_balance_change: big_decimal_from_sat(data.received_by_me as i64 - data.spent_by_me as i64, decimals), + tx_hash: signed.hash().reversed().to_vec().to_tx_hash(), + tx_hex, + fee_details: Some(fee_details.into()), + block_height: 0, + coin: ticker, + internal_id: vec![].into(), + timestamp: now_ms() / 1000, + kmd_rewards: data.kmd_rewards, + transaction_type: Default::default(), + }) + } +} + +pub struct InitUtxoWithdraw<'a, Coin> { + ctx: MmArc, + coin: Coin, + task_handle: &'a WithdrawTaskHandle, + req: WithdrawRequest, + from_address: Address, + /// Displayed [`InitUtxoWithdraw::from_address`]. + from_address_string: String, + /// Derivation path from which [`InitUtxoWithdraw::from_address`] was derived. + from_derivation_path: DerivationPath, + /// Public key corresponding to [`InitUtxoWithdraw::from_address`]. + from_pubkey: PublicKey, +} + +#[async_trait] +impl<'a, Coin> UtxoWithdraw for InitUtxoWithdraw<'a, Coin> +where + Coin: UtxoCommonOps + GetUtxoListOps + UtxoSignerOps, +{ + fn coin(&self) -> &Coin { &self.coin } + + fn sender_address(&self) -> Address { self.from_address.clone() } + + fn sender_address_string(&self) -> String { self.from_address_string.clone() } + + fn request(&self) -> &WithdrawRequest { &self.req } + + fn on_generating_transaction(&self) -> Result<(), MmError> { + let amount_display = if self.req.max { + "MAX".to_owned() + } else { + self.req.amount.to_string() + }; + + // Display the address from which we are trying to withdraw funds. + info!( + "Trying to withdraw {} {} from {} to {}", + amount_display, self.req.coin, self.from_address_string, self.req.to, + ); + + Ok(self + .task_handle + .update_in_progress_status(WithdrawInProgressStatus::GeneratingTransaction)?) + } + + fn on_finishing(&self) -> Result<(), MmError> { + Ok(self + .task_handle + .update_in_progress_status(WithdrawInProgressStatus::Finishing)?) + } + + async fn sign_tx(&self, unsigned_tx: TransactionInputSigner) -> Result> { + self.task_handle + .update_in_progress_status(WithdrawInProgressStatus::SigningTransaction)?; + + let mut sign_params = UtxoSignTxParamsBuilder::new(); + + // TODO refactor [`UtxoTxBuilder::build`] to return `SpendingInputInfo` and `SendingOutputInfo` within `AdditionalTxData`. + sign_params.add_inputs_infos(unsigned_tx.inputs.iter().map(|_input| SpendingInputInfo::P2PKH { + address_derivation_path: self.from_derivation_path.clone(), + address_pubkey: self.from_pubkey, + })); + sign_params.add_outputs_infos(once(SendingOutputInfo { + destination_address: self.req.to.clone(), + })); + match unsigned_tx.outputs.len() { + // There is no change output. + 1 => (), + // There is a change output. + 2 => { + sign_params.add_outputs_infos(once(SendingOutputInfo { + destination_address: self.from_address_string.clone(), + })); + }, + unexpected => { + let error = format!("Unexpected number of outputs: {}", unexpected); + return MmError::err(WithdrawError::InternalError(error)); + }, + } + + sign_params + .with_signature_version(self.signature_version()) + .with_unsigned_tx(unsigned_tx) + .with_prev_script(Builder::build_p2pkh(&self.from_address.hash)); + let sign_params = sign_params.build()?; + + let sign_policy = match self.coin.as_ref().priv_key_policy { + PrivKeyPolicy::KeyPair(ref key_pair) => SignPolicy::WithKeyPair(key_pair), + PrivKeyPolicy::Trezor => { + let trezor_client = self.trezor_client().await?; + SignPolicy::WithTrezor(trezor_client) + }, + }; + + self.task_handle + .update_in_progress_status(WithdrawInProgressStatus::WaitingForUserToConfirmSigning)?; + let signed = self.coin.sign_tx(sign_params, sign_policy).await?; + + Ok(signed) + } +} + +impl<'a, Coin> InitUtxoWithdraw<'a, Coin> { + pub async fn new( + ctx: MmArc, + coin: Coin, + req: WithdrawRequest, + task_handle: &'a WithdrawTaskHandle, + ) -> Result, MmError> + where + Coin: CoinWithDerivationMethod + GetWithdrawSenderAddress
, + { + let from = coin.get_withdraw_sender_address(&req).await?; + let from_address_string = from.address.display_address().map_to_mm(WithdrawError::InternalError)?; + + let from_derivation_path = match from.derivation_path { + Some(der_path) => der_path, + // [`WithdrawSenderAddress::derivation_path`] is not set, but the coin is initialized with an HD wallet derivation method. + None if coin.has_hd_wallet_derivation_method() => { + let error = "Cannot determine 'from' address derivation path".to_owned(); + return MmError::err(WithdrawError::UnexpectedFromAddress(error)); + }, + // Temporary initialize the derivation path by default since this field is not used without Trezor. + None => DerivationPath::default(), + }; + + Ok(InitUtxoWithdraw { + ctx, + coin, + task_handle, + req, + from_address: from.address, + from_address_string, + from_derivation_path, + from_pubkey: from.pubkey, + }) + } + + /// # Fail + /// + /// The method fails if [`CryptoCtx::hw_ctx`] is not initialized yet. + async fn trezor_client(&self) -> MmResult { + let crypto_ctx = CryptoCtx::from_ctx(&self.ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| WithdrawError::NoTrezorDeviceAvailable)?; + + let trezor_connect_processor = TrezorRpcTaskConnectProcessor::new(self.task_handle, HwConnectStatuses { + on_connect: WithdrawInProgressStatus::WaitingForTrezorToConnect, + on_connected: WithdrawInProgressStatus::Preparing, + on_connection_failed: WithdrawInProgressStatus::Finishing, + on_button_request: WithdrawInProgressStatus::WaitingForUserToConfirmPubkey, + on_pin_request: WithdrawAwaitingStatus::WaitForTrezorPin, + on_ready: WithdrawInProgressStatus::Preparing, + }) + .with_connect_timeout(TREZOR_CONNECT_TIMEOUT) + .with_pin_timeout(TREZOR_PIN_TIMEOUT); + + hw_ctx + .trezor(&trezor_connect_processor) + .await + .mm_err(WithdrawError::from) + } +} + +pub struct StandardUtxoWithdraw { + coin: Coin, + req: WithdrawRequest, + my_address: Address, + my_address_string: String, +} + +#[async_trait] +impl UtxoWithdraw for StandardUtxoWithdraw +where + Coin: UtxoCommonOps + GetUtxoListOps, +{ + fn coin(&self) -> &Coin { &self.coin } + + fn sender_address(&self) -> Address { self.my_address.clone() } + + fn sender_address_string(&self) -> String { self.my_address_string.clone() } + + fn request(&self) -> &WithdrawRequest { &self.req } + + fn on_generating_transaction(&self) -> Result<(), MmError> { Ok(()) } + + fn on_finishing(&self) -> Result<(), MmError> { Ok(()) } + + async fn sign_tx(&self, unsigned_tx: TransactionInputSigner) -> Result> { + let key_pair = self.coin.as_ref().priv_key_policy.key_pair_or_err()?; + Ok(with_key_pair::sign_tx( + unsigned_tx, + key_pair, + self.prev_script(), + self.signature_version(), + self.coin.as_ref().conf.fork_id, + )?) + } +} + +impl StandardUtxoWithdraw +where + Coin: AsRef + MarketCoinOps, +{ + pub fn new(coin: Coin, req: WithdrawRequest) -> Result> { + let my_address = coin.as_ref().derivation_method.iguana_or_err()?.clone(); + let my_address_string = coin.my_address().map_to_mm(WithdrawError::InternalError)?; + Ok(StandardUtxoWithdraw { + coin, + req, + my_address, + my_address_string, + }) + } +} diff --git a/mm2src/coins/utxo_signer/Cargo.toml b/mm2src/coins/utxo_signer/Cargo.toml new file mode 100644 index 0000000000..db17cd348d --- /dev/null +++ b/mm2src/coins/utxo_signer/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "utxo_signer" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = "0.1" +chain = { path = "../../mm2_bitcoin/chain" } +common = { path = "../../common" } +mm2_err_handle = { path = "../../mm2_err_handle" } +crypto = { path = "../../crypto" } +derive_more = "0.99" +hex = "0.4.2" +keys = { path = "../../mm2_bitcoin/keys" } +primitives = { path = "../../mm2_bitcoin/primitives" } +rpc = { path = "../../mm2_bitcoin/rpc" } +script = { path = "../../mm2_bitcoin/script" } +serialization = { path = "../../mm2_bitcoin/serialization" } diff --git a/mm2src/coins/utxo_signer/src/lib.rs b/mm2src/coins/utxo_signer/src/lib.rs new file mode 100644 index 0000000000..ce6d716d68 --- /dev/null +++ b/mm2src/coins/utxo_signer/src/lib.rs @@ -0,0 +1,156 @@ +use async_trait::async_trait; +use chain::Transaction as UtxoTx; +use crypto::trezor::client::TrezorClient; +use crypto::trezor::utxo::TrezorUtxoCoin; +use crypto::trezor::TrezorError; +use derive_more::Display; +use keys::bytes::Bytes; +use keys::KeyPair; +use mm2_err_handle::prelude::*; +use rpc::v1::types::{Transaction as RpcTransaction, H256 as H256Json}; +use script::Script; + +mod sign_common; +pub mod sign_params; +pub mod with_key_pair; +pub mod with_trezor; + +use crate::with_key_pair::UtxoSignWithKeyPairError; +use sign_params::UtxoSignTxParams; + +pub type UtxoSignTxResult = Result>; + +type Signature = Bytes; + +pub enum TxProviderError { + Transport(String), + InvalidResponse(String), + Internal(String), +} + +#[derive(Debug, Display)] +pub enum UtxoSignTxError { + #[display(fmt = "Coin '{}' is not supported with Trezor", coin)] + CoinNotSupportedWithTrezor { coin: String }, + #[display(fmt = "Trezor doesn't support P2WPKH outputs yet")] + TrezorDoesntSupportP2WPKH, + #[display(fmt = "Trezor client error: {}", _0)] + TrezorError(TrezorError), + #[display(fmt = "Encountered invalid parameter '{}': {}", param, description)] + InvalidSignParam { param: String, description: String }, + #[display( + fmt = "Hardware Device returned an invalid number of signatures: '{}', number of inputs: '{}'", + actual, + expected + )] + InvalidSignaturesNumber { actual: usize, expected: usize }, + #[display(fmt = "Error signing using a private key")] + ErrorSigning(keys::Error), + #[display( + fmt = "{} script '{}' built from input key pair doesn't match expected prev script '{}'", + script_type, + script, + prev_script + )] + MismatchScript { + script_type: String, + script: Script, + prev_script: Script, + }, + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for UtxoSignTxError { + fn from(e: TrezorError) -> Self { UtxoSignTxError::TrezorError(e) } +} + +impl From for UtxoSignTxError { + fn from(error_with_key: UtxoSignWithKeyPairError) -> Self { + let error = error_with_key.to_string(); + match error_with_key { + UtxoSignWithKeyPairError::MismatchScript { + script_type, + script, + prev_script, + } => UtxoSignTxError::MismatchScript { + script_type, + script, + prev_script, + }, + // `with_key_pair` contains methods that checks parameters + // that are expected to be checked by [`sign_common::UtxoSignTxParamsBuilder::build`] already. + // So if this error happens, it's our internal error. + UtxoSignWithKeyPairError::InputIndexOutOfBound { .. } => UtxoSignTxError::Internal(error), + UtxoSignWithKeyPairError::ErrorSigning(sign) => UtxoSignTxError::ErrorSigning(sign), + } + } +} + +impl From for UtxoSignTxError { + fn from(e: keys::Error) -> Self { UtxoSignTxError::ErrorSigning(e) } +} + +impl From for UtxoSignTxError { + fn from(e: TxProviderError) -> Self { + match e { + TxProviderError::Transport(transport) | TxProviderError::InvalidResponse(transport) => { + UtxoSignTxError::Transport(transport) + }, + TxProviderError::Internal(internal) => UtxoSignTxError::Internal(internal), + } + } +} + +/// The trait declares a transaction getter. +/// The provider can use cache or RPC client. +#[async_trait] +pub trait TxProvider { + async fn get_rpc_transaction(&self, tx_hash: &H256Json) -> Result>; +} + +pub enum SignPolicy<'a> { + WithTrezor(TrezorClient), + WithKeyPair(&'a KeyPair), +} + +#[async_trait] +pub trait UtxoSignerOps { + type TxGetter: TxProvider + Send + Sync; + + fn trezor_coin(&self) -> UtxoSignTxResult; + + fn fork_id(&self) -> u32; + + fn branch_id(&self) -> u32; + + fn tx_provider(&self) -> Self::TxGetter; + + async fn sign_tx(&self, params: UtxoSignTxParams, sign_policy: SignPolicy<'_>) -> UtxoSignTxResult { + match sign_policy { + SignPolicy::WithTrezor(trezor) => { + let signer = with_trezor::TrezorTxSigner { + trezor, + tx_provider: self.tx_provider(), + trezor_coin: self.trezor_coin()?, + params, + fork_id: self.fork_id(), + branch_id: self.branch_id(), + }; + signer.sign_tx().await + }, + SignPolicy::WithKeyPair(key_pair) => { + let signed = with_key_pair::sign_tx( + params.unsigned_tx, + key_pair, + params.prev_script, + params.signature_version, + self.fork_id(), + )?; + Ok(signed) + }, + } + } +} diff --git a/mm2src/coins/utxo_signer/src/sign_common.rs b/mm2src/coins/utxo_signer/src/sign_common.rs new file mode 100644 index 0000000000..723367c7ea --- /dev/null +++ b/mm2src/coins/utxo_signer/src/sign_common.rs @@ -0,0 +1,120 @@ +use crate::Signature; +use chain::{Transaction as UtxoTx, TransactionInput}; +use keys::bytes::Bytes; +use keys::Public as PublicKey; +use primitives::hash::{H256, H512}; +use script::{Builder, Script, TransactionInputSigner, UnsignedTransactionInput}; + +pub(crate) fn complete_tx(unsigned: TransactionInputSigner, signed_inputs: Vec) -> UtxoTx { + UtxoTx { + inputs: signed_inputs, + n_time: unsigned.n_time, + outputs: unsigned.outputs.clone(), + version: unsigned.version, + overwintered: unsigned.overwintered, + lock_time: unsigned.lock_time, + expiry_height: unsigned.expiry_height, + join_splits: vec![], + shielded_spends: vec![], + shielded_outputs: vec![], + value_balance: 0, + version_group_id: unsigned.version_group_id, + binding_sig: H512::default(), + join_split_sig: H512::default(), + join_split_pubkey: H256::default(), + zcash: unsigned.zcash, + str_d_zeel: unsigned.str_d_zeel, + tx_hash_algo: unsigned.hash_algo.into(), + } +} + +pub(crate) fn p2pk_spend_with_signature( + unsigned_input: &UnsignedTransactionInput, + fork_id: u32, + signature: Signature, +) -> TransactionInput { + let script_sig = script_sig(signature, fork_id); + + TransactionInput { + previous_output: unsigned_input.previous_output, + script_sig: Builder::default().push_bytes(&script_sig).into_bytes(), + sequence: unsigned_input.sequence, + script_witness: vec![], + } +} + +pub(crate) fn p2pkh_spend_with_signature( + unsigned_input: &UnsignedTransactionInput, + public_key: &PublicKey, + fork_id: u32, + signature: Signature, +) -> TransactionInput { + let script_sig = script_sig_with_pub(public_key, fork_id, signature); + + TransactionInput { + previous_output: unsigned_input.previous_output, + script_sig, + sequence: unsigned_input.sequence, + script_witness: vec![], + } +} + +pub(crate) fn p2sh_spend_with_signature( + unsigned_input: &UnsignedTransactionInput, + redeem_script: Script, + script_data: Script, + fork_id: u32, + signature: Signature, +) -> TransactionInput { + let script_sig = script_sig(signature, fork_id); + + let mut resulting_script = Builder::default().push_data(&script_sig).into_bytes(); + if !script_data.is_empty() { + resulting_script.extend_from_slice(&script_data); + } + + let redeem_part = Builder::default().push_data(&redeem_script).into_bytes(); + resulting_script.extend_from_slice(&redeem_part); + + TransactionInput { + previous_output: unsigned_input.previous_output, + script_sig: resulting_script, + sequence: unsigned_input.sequence, + script_witness: vec![], + } +} + +pub(crate) fn p2wpkh_spend_with_signature( + unsigned_input: &UnsignedTransactionInput, + public_key: &PublicKey, + fork_id: u32, + signature: Signature, +) -> TransactionInput { + let script_sig = script_sig(signature, fork_id); + + TransactionInput { + previous_output: unsigned_input.previous_output, + script_sig: Bytes::from(Vec::new()), + sequence: unsigned_input.sequence, + script_witness: vec![script_sig, Bytes::from(public_key.to_vec())], + } +} + +pub(crate) fn script_sig_with_pub(public_key: &PublicKey, fork_id: u32, signature: Signature) -> Bytes { + let script_sig = script_sig(signature, fork_id); + let builder = Builder::default(); + builder + .push_data(&script_sig) + .push_data(public_key.to_vec().as_slice()) + .into_bytes() +} + +pub(crate) fn script_sig(mut signature: Signature, fork_id: u32) -> Bytes { + let mut sig_script = Bytes::default(); + + sig_script.append(&mut signature); + // Using SIGHASH_ALL only for now + sig_script.append(&mut Bytes::from(vec![1 | fork_id as u8])); + + sig_script +} diff --git a/mm2src/coins/utxo_signer/src/sign_params.rs b/mm2src/coins/utxo_signer/src/sign_params.rs new file mode 100644 index 0000000000..545b951c67 --- /dev/null +++ b/mm2src/coins/utxo_signer/src/sign_params.rs @@ -0,0 +1,170 @@ +use crate::{UtxoSignTxError, UtxoSignTxResult}; +use chain::TransactionOutput; +use crypto::trezor::utxo::TrezorOutputScriptType; +use crypto::DerivationPath; +use keys::Public as PublicKey; +use mm2_err_handle::prelude::*; +use script::{Script, SignatureVersion, TransactionInputSigner, UnsignedTransactionInput}; + +impl UtxoSignTxError { + fn no_param(param: &str) -> UtxoSignTxError { + UtxoSignTxError::InvalidSignParam { + param: param.to_owned(), + description: "not set".to_owned(), + } + } +} + +/// An additional info of a spending input. +pub enum SpendingInputInfo { + P2PKH { + address_derivation_path: DerivationPath, + address_pubkey: PublicKey, + }, + // The fields are used to generate `trezor::proto::messages_bitcoin::MultisigRedeemScriptType` + // P2SH {} +} + +/// An additional info of a sending output. +pub struct SendingOutputInfo { + pub destination_address: String, +} + +impl SendingOutputInfo { + /// For now, returns [`TrezorOutputScriptType::PayToAddress`] since we don't support SLP tokens yet. + pub fn trezor_output_script_type(&self) -> TrezorOutputScriptType { TrezorOutputScriptType::PayToAddress } +} + +pub struct UtxoSignTxParamsBuilder { + signature_version: Option, + unsigned_tx: Option, + /// The number of elements is expected to be the same as `unsigned_tx.inputs.len()`. + inputs_infos: Vec, + /// The number of elements is expected to be the same as `unsigned_tx.outputs.len()`. + outputs_infos: Vec, + /// This is used to check if a built from key pair matches the expected `prev_script`. + prev_script: Option