diff --git a/.cargo/config b/.cargo/config index f6764a55b4..056307787e 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,3 +1,3 @@ [target.wasm32-unknown-unknown] -runner = 'wasm-bindgen-test-runner-0.2.78' +runner = 'wasm-bindgen-test-runner' rustflags = ["--cfg=web_sys_unstable_apis"] diff --git a/.dockerignore b/.dockerignore index 0e3ba081bc..09656908a3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,7 +5,7 @@ cmake-build-debug /Dockerfile /target -!/target/release/mm2 +!/target/ci/mm2 /mm2src/*/target /build diff --git a/.github/workflows/virustotal_scan.yml b/.github/workflows/virustotal_scan.yml new file mode 100644 index 0000000000..551990ddc0 --- /dev/null +++ b/.github/workflows/virustotal_scan.yml @@ -0,0 +1,18 @@ +name: VirusTotal Scan + +on: + release: + types: [created, edited, released, published] + +jobs: + virustotal: + runs-on: ubuntu-latest + steps: + - + name: VirusTotal Scan + uses: crazy-max/ghaction-virustotal@v3 + with: + vt_api_key: ${{ secrets.VT_API_KEY }} + update_release_body: true + files: | + .zip$ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc6a24afa1..645223074e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,5 +49,5 @@ and we use [rustfmt](https://github.com/rust-lang/rustfmt) to make our code clea 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 + WASM_BINDGEN_TEST_TIMEOUT=180 GECKODRIVER=PATH_TO_GECKO_DRIVER_BIN wasm-pack test --firefox --headless mm2src/mm2_main ``` diff --git a/Cargo.lock b/Cargo.lock index a80490ac72..e7fae16a35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.12.2" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602d785912f476e480434627e8732e6766b760c045bbf897d9dfaa9f4fbd399c" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" dependencies = [ "gimli", ] @@ -28,10 +28,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc9a9dd069569f212bc4330af9f17c4afb5e8ce185e83dbb14f1349dda18b10" [[package]] -name = "adler32" -version = "1.1.0" +name = "adler" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" @@ -131,6 +131,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -158,15 +167,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" -[[package]] -name = "arrayvec" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" -dependencies = [ - "nodrop", -] - [[package]] name = "arrayvec" version = "0.5.1" @@ -210,7 +210,7 @@ dependencies = [ "once_cell", "pin-project-lite 0.1.12", "pin-utils", - "slab 0.4.2", + "slab", "smol", "wasm-bindgen-futures", ] @@ -268,11 +268,11 @@ dependencies = [ [[package]] name = "atomic-shim" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20fdac7156779a1a30d970e838195558b4810dd06aa69e7c7461bdc518edf9b" +checksum = "67cd4b51d303cf3501c301e8125df442128d3c6d7c69f71b27833d253de47e77" dependencies = [ - "crossbeam", + "crossbeam-utils 0.8.8", ] [[package]] @@ -322,15 +322,15 @@ checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" [[package]] name = "autocfg" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.5.6" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2504b827a8bef941ba3dd64bdffe9cf56ca182908a147edd6189c95fbcae7d" +checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43" dependencies = [ "async-trait", "axum-core", @@ -338,13 +338,13 @@ dependencies = [ "bytes 1.1.0", "futures-util", "http 0.2.7", - "http-body 0.4.4", + "http-body 0.4.5", "hyper", "itoa 1.0.1", "matchit", "memchr", "mime", - "percent-encoding 2.1.0", + "percent-encoding", "pin-project-lite 0.2.9", "serde", "sync_wrapper", @@ -357,28 +357,31 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.2.4" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da31c0ed7b4690e2c78fe4b880d21cd7db04a346ebc658b4270251b695437f17" +checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" dependencies = [ "async-trait", "bytes 1.1.0", "futures-util", "http 0.2.7", - "http-body 0.4.4", + "http-body 0.4.5", "mime", + "tower-layer", + "tower-service", ] [[package]] name = "backtrace" -version = "0.3.49" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05100821de9e028f12ae3d189176b41ee198341eb8f369956407fea2f5cc666c" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" dependencies = [ "addr2line", - "cfg-if 0.1.10", + "cc", + "cfg-if 1.0.0", "libc", - "miniz_oxide 0.3.7", + "miniz_oxide 0.5.4", "object", "rustc-demangle", ] @@ -434,6 +437,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851" + [[package]] name = "bech32" version = "0.8.1" @@ -491,7 +500,7 @@ dependencies = [ "hkd32", "hmac 0.11.0", "ripemd160", - "secp256k1", + "secp256k1 0.20.3", "sha2 0.9.9", "subtle 2.4.0", "zeroize", @@ -499,14 +508,13 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.27.1" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a41df6ad9642c5c15ae312dd3d074de38fd3eb7cc87ad4ce10f90292a83fe4d" +checksum = "05bba324e6baf655b882df672453dbbc527bc938cadd27750ae510aaccc3a66a" dependencies = [ "bech32", "bitcoin_hashes", - "bitcoinconsensus", - "secp256k1", + "secp256k1 0.22.1", ] [[package]] @@ -515,16 +523,6 @@ 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 = "1a8aa43b5cd02f856cb126a9af819e77b8910fdd74dd1407be649f2f5fe3a1b5" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "bitcrypto" version = "0.1.0" @@ -535,7 +533,7 @@ dependencies = [ "serialization", "sha-1", "sha2 0.9.9", - "sha3", + "sha3 0.9.1", "siphasher", ] @@ -545,15 +543,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitmaps" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" -dependencies = [ - "typenum", -] - [[package]] name = "bitvec" version = "0.18.5" @@ -773,9 +762,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.4.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bv" @@ -855,9 +844,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "bzip2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" dependencies = [ "bzip2-sys", "libc", @@ -944,23 +933,25 @@ dependencies = [ "bitcoin", "bitcrypto", "primitives", - "rustc-hex 2.1.0", + "rustc-hex", "serialization", "serialization_derive", ] [[package]] name = "chrono" -version = "0.4.11" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ + "iana-time-zone", "js-sys", "num-integer", "num-traits", "serde", "time 0.1.43", "wasm-bindgen", + "winapi", ] [[package]] @@ -1023,6 +1014,16 @@ dependencies = [ "cc", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "coins" version = "0.1.0" @@ -1041,6 +1042,7 @@ dependencies = [ "cfg-if 1.0.0", "chain", "common", + "cosmrs", "crossbeam", "crypto", "db_common", @@ -1048,18 +1050,22 @@ dependencies = [ "dirs", "ed25519-dalek", "ed25519-dalek-bip32 0.2.0", + "enum_from", "ethabi", "ethcore-transaction", - "ethereum-types 0.4.2", + "ethereum-types", "ethkey", "futures 0.1.29", "futures 0.3.15", + "group 0.8.0", "gstuff", - "hex 0.4.2", + "hex 0.4.3", "http 0.2.7", + "hyper", + "hyper-rustls", "itertools", "js-sys", - "jsonrpc-core 8.0.1", + "jsonrpc-core", "keys", "lazy_static", "libc", @@ -1067,12 +1073,13 @@ dependencies = [ "lightning-background-processor", "lightning-invoice", "lightning-net-tokio", - "lightning-persister", - "metrics", + "lightning-rapid-gossip-sync", "mm2_core", "mm2_db", "mm2_err_handle", "mm2_io", + "mm2_metamask", + "mm2_metrics", "mm2_net", "mm2_number", "mm2_test_helpers", @@ -1084,14 +1091,15 @@ dependencies = [ "prost-build", "protobuf", "rand 0.7.3", - "rlp 0.3.0", + "rlp", "rmp-serde", "rpc", "rpc_task", "rust-ini", "rustls 0.20.4", "script", - "secp256k1", + "secp256k1 0.20.3", + "secp256k1 0.22.1", "ser_error", "ser_error_derive", "serde", @@ -1100,19 +1108,22 @@ dependencies = [ "serialization", "serialization_derive", "sha2 0.9.9", - "sha3", + "sha3 0.9.1", "solana-client", "solana-sdk", "solana-transaction-status", "spl-associated-token-account", "spl-token", "spv_validation", + "tendermint-config", + "tendermint-rpc", "tiny-bip39", "tokio", "tokio-rustls", "tonic", "tonic-build", "utxo_signer", + "uuid 0.7.4", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -1136,11 +1147,18 @@ dependencies = [ "common", "crypto", "derive_more", + "ethereum-types", "futures 0.3.15", - "hex 0.4.2", + "hex 0.4.3", + "lightning", + "lightning-background-processor", + "lightning-invoice", "mm2_core", "mm2_err_handle", + "mm2_metamask", + "mm2_metrics", "mm2_number", + "parking_lot 0.12.0", "rpc", "rpc_task", "ser_error", @@ -1158,7 +1176,6 @@ dependencies = [ "arrayref", "async-trait", "backtrace", - "base64 0.10.1", "bytes 1.1.0", "cc", "cfg-if 1.0.0", @@ -1169,10 +1186,10 @@ dependencies = [ "fnv", "futures 0.1.29", "futures 0.3.15", + "futures-timer", "getrandom 0.2.6", "gstuff", - "hdrhistogram 7.1.0", - "hex 0.4.2", + "hex 0.4.3", "http 0.2.7", "http-body 0.1.0", "hyper", @@ -1184,12 +1201,9 @@ dependencies = [ "lightning", "log 0.4.14", "log4rs", - "metrics", - "metrics-core", - "metrics-runtime", - "metrics-util", "parking_lot 0.12.0", "parking_lot_core 0.6.2", + "primitive-types", "rand 0.7.3", "ser_error", "ser_error_derive", @@ -1198,9 +1212,10 @@ dependencies = [ "serde_derive", "serde_json", "serde_repr", + "sha2 0.9.9", "shared_ref_counter", "tokio", - "uuid", + "uuid 0.7.4", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -1271,6 +1286,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "core2" version = "0.4.0" @@ -1280,6 +1301,38 @@ dependencies = [ "memchr", ] +[[package]] +name = "cosmos-sdk-proto" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ca04d3795c18023c221a2143b29de9c70668ecb22d17783bc02ee780c6c404" +dependencies = [ + "prost", + "prost-types", + "tendermint-proto", +] + +[[package]] +name = "cosmrs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6989fdb6267eccb52762530b79ce0b385f4eaeb8b786522a95512e9bebb268c2" +dependencies = [ + "cosmos-sdk-proto", + "ecdsa", + "eyre", + "getrandom 0.2.6", + "k256", + "prost", + "prost-types", + "rand_core 0.6.3", + "serde", + "serde_json", + "subtle-encoding", + "tendermint", + "thiserror", +] + [[package]] name = "cpufeatures" version = "0.1.4" @@ -1369,13 +1422,13 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ - "autocfg 1.0.0", + "autocfg 1.1.0", "cfg-if 0.1.10", "crossbeam-utils 0.7.2", "lazy_static", "maybe-uninit", "memoffset 0.5.4", - "scopeguard 1.1.0", + "scopeguard", ] [[package]] @@ -1388,7 +1441,7 @@ dependencies = [ "crossbeam-utils 0.8.8", "lazy_static", "memoffset 0.6.4", - "scopeguard 1.1.0", + "scopeguard", ] [[package]] @@ -1408,7 +1461,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ - "autocfg 1.0.0", + "autocfg 1.1.0", "cfg-if 0.1.10", "lazy_static", ] @@ -1448,12 +1501,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "crunchy" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2f4a431c5c9f662e1200b7c7f02c34e91361150e382089a8f2dec3ba680cbda" - [[package]] name = "crunchy" version = "0.2.2" @@ -1464,31 +1511,41 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" name = "crypto" version = "1.0.0" dependencies = [ + "arrayref", "async-trait", "bip32", "bitcrypto", + "bs58", "common", "derive_more", "enum-primitive-derive", + "enum_from", "futures 0.3.15", - "hex 0.4.2", + "hex 0.4.3", "http 0.2.7", "hw_common", "keys", + "lazy_static", "mm2_core", "mm2_err_handle", + "mm2_eth", + "mm2_metamask", "num-traits", "parking_lot 0.12.0", "primitives", + "rpc", "rpc_task", - "rustc-hex 2.1.0", - "secp256k1", + "rustc-hex", + "secp256k1 0.20.3", "ser_error", "ser_error_derive", "serde", "serde_derive", "serde_json", + "tiny-bip39", "trezor", + "wasm-bindgen-test", + "web3", ] [[package]] @@ -1634,6 +1691,50 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cxx" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b49af8e551e84f85d6def97c42b8d93bc5bb0817cce96b56a472b1b19b5bfc2" +dependencies = [ + "cc", + "codespan-reporting", + "lazy_static", + "proc-macro2 1.0.39", + "quote 1.0.18", + "scratch", + "syn 1.0.95", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" +dependencies = [ + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + [[package]] name = "dashmap" version = "4.0.2" @@ -1655,11 +1756,12 @@ checksum = "72aa14c04dfae8dd7d8a2b1cb7ca2152618cd01336dbfe704b8dcbf8d41dbd69" name = "db_common" version = "0.1.0" dependencies = [ - "hex 0.4.2", + "common", + "hex 0.4.3", "log 0.4.14", "rusqlite", "sql-builder", - "uuid", + "uuid 0.7.4", ] [[package]] @@ -1869,9 +1971,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.0.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf038a7b6fd7ef78ad3348b63f3a17550877b0e28f8d68bcc94894d1412158bc" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ "signature", ] @@ -1960,6 +2062,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "enum-as-inner" version = "0.4.0" @@ -1983,6 +2091,16 @@ dependencies = [ "syn 1.0.95", ] +[[package]] +name = "enum_from" +version = "0.1.0" +dependencies = [ + "itertools", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -2039,41 +2157,21 @@ dependencies = [ "libc", ] -[[package]] -name = "error-chain" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" -dependencies = [ - "backtrace", - "version_check", -] - [[package]] name = "ethabi" -version = "6.1.0" -source = "git+https://github.com/artemii235/ethabi#d70bfd94e05c49d9b28a5edb4def2ebfaa5e1053" -dependencies = [ - "error-chain", - "ethereum-types 0.4.2", - "rustc-hex 2.1.0", - "serde", - "serde_derive", - "serde_json", - "tiny-keccak 1.4.4", -] - -[[package]] -name = "ethbloom" -version = "0.5.3" +version = "17.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6294da962646baa738414e8e718d1a1f0360a51d92de89ccbf91870418f5360" +checksum = "e4966fba78396ff92db3b817ee71143eccd98acf0f876b8d600e585a670c5d1b" dependencies = [ - "crunchy 0.1.6", - "ethereum-types-serialize", - "fixed-hash 0.2.5", + "ethereum-types", + "hex 0.4.3", + "once_cell", + "regex", "serde", - "tiny-keccak 1.4.4", + "serde_json", + "sha3 0.10.4", + "thiserror", + "uint", ] [[package]] @@ -2082,8 +2180,8 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11da94e443c60508eb62cf256243a64da87304c2802ac2528847f79d750007ef" dependencies = [ - "crunchy 0.2.2", - "fixed-hash 0.7.0", + "crunchy", + "fixed-hash", "impl-rlp", "impl-serde", "tiny-keccak 2.0.2", @@ -2092,71 +2190,58 @@ dependencies = [ [[package]] name = "ethcore-transaction" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-ethereum.git#0a090f9b3efd7e24193265cf0e4109bf2369ad98" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#223da7972113a548531804510708f87b629a48fd" dependencies = [ - "ethereum-types 0.4.2", + "ethereum-types", "ethkey", "keccak-hash", - "rlp 0.3.0", - "rustc-hex 1.0.0", + "rlp", + "rustc-hex", "unexpected", ] -[[package]] -name = "ethereum-types" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e742184dc63a01c8ea0637369f8faa27c40f537949908a237f95c05e68d2c96" -dependencies = [ - "crunchy 0.1.6", - "ethbloom 0.5.3", - "ethereum-types-serialize", - "fixed-hash 0.2.5", - "serde", - "uint 0.4.1", -] - [[package]] name = "ethereum-types" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2827b94c556145446fcce834ca86b7abf0c39a805883fe20e72c5bfdb5a0dc6" dependencies = [ - "ethbloom 0.12.1", - "fixed-hash 0.7.0", + "ethbloom", + "fixed-hash", "impl-rlp", "impl-serde", "primitive-types", - "uint 0.9.1", -] - -[[package]] -name = "ethereum-types-serialize" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1873d77b32bc1891a79dad925f2acbc318ee942b38b9110f9dbc5fbeffcea350" -dependencies = [ - "serde", + "uint", ] [[package]] name = "ethkey" version = "0.3.0" -source = "git+https://github.com/artemii235/parity-ethereum.git#0a090f9b3efd7e24193265cf0e4109bf2369ad98" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#223da7972113a548531804510708f87b629a48fd" dependencies = [ "byteorder 1.4.3", "edit-distance", - "ethereum-types 0.4.2", + "ethereum-types", "log 0.3.9", "mem", "rand 0.6.5", - "rustc-hex 1.0.0", - "secp256k1", + "rustc-hex", + "secp256k1 0.20.3", "serde", "serde_derive", "tiny-keccak 1.4.4", ] +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "failure" version = "0.1.8" @@ -2255,16 +2340,6 @@ dependencies = [ "libc", ] -[[package]] -name = "fixed-hash" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7afe6ce860afb14422711595a7b26ada9ed7de2f43c0b2ab79d09ee196287273" -dependencies = [ - "rand 0.4.6", - "rustc-hex 2.1.0", -] - [[package]] name = "fixed-hash" version = "0.7.0" @@ -2273,7 +2348,7 @@ checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ "byteorder 1.4.3", "rand 0.8.4", - "rustc-hex 2.1.0", + "rustc-hex", "static_assertions", ] @@ -2296,6 +2371,16 @@ dependencies = [ "miniz_oxide 0.4.0", ] +[[package]] +name = "flex-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" +dependencies = [ + "eyre", + "paste", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2309,7 +2394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", - "percent-encoding 2.1.0", + "percent-encoding", ] [[package]] @@ -2466,7 +2551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" dependencies = [ "gloo-timers", - "send_wrapper 0.4.0", + "send_wrapper", ] [[package]] @@ -2485,7 +2570,7 @@ dependencies = [ "memchr", "pin-project-lite 0.2.9", "pin-utils", - "slab 0.4.2", + "slab", ] [[package]] @@ -2567,9 +2652,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.21.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" [[package]] name = "gloo-timers" @@ -2642,9 +2727,9 @@ dependencies = [ "futures-util", "http 0.2.7", "indexmap", - "slab 0.4.2", + "slab", "tokio", - "tokio-util 0.7.2", + "tokio-util", "tracing", ] @@ -2660,7 +2745,7 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" dependencies = [ - "crunchy 0.2.2", + "crunchy", ] [[package]] @@ -2699,27 +2784,6 @@ dependencies = [ "hashbrown 0.9.1", ] -[[package]] -name = "hdrhistogram" -version = "6.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d331ebcdbca4acbefe5da8c3299b2e246f198a8294cc5163354e743398b89d" -dependencies = [ - "byteorder 1.4.3", - "num-traits", -] - -[[package]] -name = "hdrhistogram" -version = "7.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c22708574c44e924720c5b3a116326c688e6d532f438c77c007ec8768644f9" -dependencies = [ - "byteorder 1.4.3", - "crossbeam-channel 0.4.4", - "num-traits", -] - [[package]] name = "heck" version = "0.4.0" @@ -2743,9 +2807,9 @@ checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" [[package]] name = "hex" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hidapi" @@ -2877,9 +2941,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes 1.1.0", "http 0.2.7", @@ -2931,7 +2995,7 @@ dependencies = [ "js-sys", "mm2_err_handle", "rusb", - "secp256k1", + "secp256k1 0.20.3", "serde", "serde_derive", "wasm-bindgen", @@ -2952,7 +3016,7 @@ dependencies = [ "futures-util", "h2", "http 0.2.7", - "http-body 0.4.4", + "http-body 0.4.5", "httparse", "httpdate", "itoa 1.0.1", @@ -2991,14 +3055,27 @@ dependencies = [ ] [[package]] -name = "idna" -version = "0.1.5" +name = "iana-time-zone" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", ] [[package]] @@ -3022,20 +3099,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "im" -version = "15.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" -dependencies = [ - "bitmaps", - "rand_core 0.6.3", - "rand_xoshiro", - "sized-chunks", - "typenum", - "version_check", -] - [[package]] name = "impl-codec" version = "0.6.0" @@ -3051,7 +3114,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" dependencies = [ - "rlp 0.5.0", + "rlp", ] [[package]] @@ -3075,8 +3138,14 @@ dependencies = [ ] [[package]] -name = "index_list" -version = "0.2.7" +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "index_list" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9d968042a4902e08810946fc7cd5851eb75e80301342305af755ca06cb82ce" @@ -3086,7 +3155,7 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ - "autocfg 1.0.0", + "autocfg 1.1.0", "hashbrown 0.11.2", ] @@ -3162,6 +3231,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +[[package]] +name = "jemalloc-sys" +version = "0.5.2+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134163979b6eed9564c98637b710b40979939ba351f59952708234ea11b5f3f8" +dependencies = [ + "cc", + "fs_extra", + "libc", +] + +[[package]] +name = "jemallocator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c2514137880c52b0b4822b563fadd38257c1f380858addb74a400889696ea6" +dependencies = [ + "jemalloc-sys", + "libc", +] + [[package]] name = "jobserver" version = "0.1.24" @@ -3180,19 +3270,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonrpc-core" -version = "8.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf83704f4e79979a424d1082dd2c1e52683058056c9280efa19ac5f6bc9033c" -dependencies = [ - "futures 0.1.29", - "log 0.3.9", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "jsonrpc-core" version = "18.0.0" @@ -3222,6 +3299,19 @@ dependencies = [ "subtle 2.4.0", ] +[[package]] +name = "k256" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c3a5e0a0b8450278feda242592512e09f61c72e018b8cd5c859482802daf2d" +dependencies = [ + "cfg-if 1.0.0", + "ecdsa", + "elliptic-curve", + "sec1", + "sha2 0.9.9", +] + [[package]] name = "keccak" version = "0.1.0" @@ -3230,11 +3320,12 @@ checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" [[package]] name = "keccak-hash" -version = "0.1.2" -source = "git+https://github.com/artemii235/parity-common#986aba04f5e11e855d08a41c40bf03b4ba2862b0" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82bc5d5ca345b067619615f62ac6f93e7daa67eb82d080bc380ed480708ec9e3" dependencies = [ - "ethereum-types 0.4.2", - "tiny-keccak 1.4.4", + "primitive-types", + "tiny-keccak 2.0.2", ] [[package]] @@ -3248,8 +3339,8 @@ dependencies = [ "lazy_static", "primitives", "rand 0.6.5", - "rustc-hex 2.1.0", - "secp256k1", + "rustc-hex", + "secp256k1 0.20.3", "serde", "serde_derive", ] @@ -3300,8 +3391,8 @@ dependencies = [ [[package]] name = "libp2p" -version = "0.45.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +version = "0.45.1" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "bytes 1.1.0", "futures 0.3.15", @@ -3332,7 +3423,7 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.33.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "asn1_der", "bs58", @@ -3367,7 +3458,7 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.33.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "futures 0.3.15", "libp2p-core", @@ -3394,7 +3485,7 @@ dependencies = [ [[package]] name = "libp2p-floodsub" version = "0.36.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "cuckoofilter 0.5.0", "fnv", @@ -3411,7 +3502,7 @@ dependencies = [ [[package]] name = "libp2p-metrics" version = "0.6.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "libp2p-core", "libp2p-ping", @@ -3422,7 +3513,7 @@ dependencies = [ [[package]] name = "libp2p-mplex" version = "0.33.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "asynchronous-codec", "bytes 1.1.0", @@ -3439,7 +3530,7 @@ dependencies = [ [[package]] name = "libp2p-noise" version = "0.36.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "bytes 1.1.0", "curve25519-dalek 3.2.0", @@ -3460,7 +3551,7 @@ dependencies = [ [[package]] name = "libp2p-ping" version = "0.36.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "futures 0.3.15", "futures-timer", @@ -3475,7 +3566,7 @@ dependencies = [ [[package]] name = "libp2p-plaintext" version = "0.33.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "asynchronous-codec", "bytes 1.1.0", @@ -3491,7 +3582,7 @@ dependencies = [ [[package]] name = "libp2p-request-response" version = "0.18.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "async-trait", "bytes 1.1.0", @@ -3507,8 +3598,8 @@ dependencies = [ [[package]] name = "libp2p-swarm" -version = "0.36.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +version = "0.36.1" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "either", "fnv", @@ -3527,7 +3618,7 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" version = "0.27.2" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "quote 1.0.18", "syn 1.0.95", @@ -3536,7 +3627,7 @@ dependencies = [ [[package]] name = "libp2p-tcp" version = "0.33.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "futures 0.3.15", "futures-timer", @@ -3552,7 +3643,7 @@ dependencies = [ [[package]] name = "libp2p-wasm-ext" version = "0.33.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "futures 0.3.15", "js-sys", @@ -3565,7 +3656,7 @@ dependencies = [ [[package]] name = "libp2p-websocket" version = "0.35.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "either", "futures 0.3.15", @@ -3576,14 +3667,14 @@ dependencies = [ "quicksink", "rw-stream-sink", "soketto", - "url 2.2.2", + "url", "webpki-roots", ] [[package]] name = "libp2p-yamux" version = "0.37.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "futures 0.3.15", "libp2p-core", @@ -3636,7 +3727,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" dependencies = [ - "crunchy 0.2.2", + "crunchy", "digest 0.9.0", "subtle 2.4.0", ] @@ -3647,7 +3738,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" dependencies = [ - "crunchy 0.2.2", + "crunchy", "digest 0.9.0", "subtle 2.4.0", ] @@ -3724,42 +3815,43 @@ dependencies = [ [[package]] name = "lightning" -version = "0.0.106" -source = "git+https://github.com/shamardy/rust-lightning?branch=0.0.106#af4a89c08c22d0110d386df0e288b2f825aaebbc" +version = "0.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dce6da860338d5a9ddc3fd42432465310cfab93b342bbd23b41b7c1f7c509d3" dependencies = [ "bitcoin", - "hex 0.4.2", - "regex", - "secp256k1", ] [[package]] name = "lightning-background-processor" -version = "0.0.106" +version = "0.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8de9d0de42bb933ffb8d33c6b0a75302f08b35126bfc74398ba01ad0c201f8d" dependencies = [ "bitcoin", - "db_common", "lightning", - "lightning-invoice", - "lightning-persister", + "lightning-rapid-gossip-sync", ] [[package]] name = "lightning-invoice" -version = "0.14.0" -source = "git+https://github.com/shamardy/rust-lightning?branch=0.0.106#af4a89c08c22d0110d386df0e288b2f825aaebbc" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32aa02b7fd0bd95e40b6ca8d9d9232b162d5e23b41bd2bc42abe9e9c78d34d72" dependencies = [ "bech32", "bitcoin_hashes", "lightning", "num-traits", - "secp256k1", + "secp256k1 0.22.1", + "serde", ] [[package]] name = "lightning-net-tokio" -version = "0.0.106" -source = "git+https://github.com/shamardy/rust-lightning?branch=0.0.106#af4a89c08c22d0110d386df0e288b2f825aaebbc" +version = "0.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d093fbc643835bc64c0501b52a3531d2511dcb1237d0495d68fea3adc47d" dependencies = [ "bitcoin", "lightning", @@ -3767,50 +3859,29 @@ dependencies = [ ] [[package]] -name = "lightning-persister" -version = "0.0.106" +name = "lightning-rapid-gossip-sync" +version = "0.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391732631b14f7a1d9dc84dc3f644484d9b73190a31087b3856505cf0525bea0" 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" +name = "link-cplusplus" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" dependencies = [ - "owning_ref", - "scopeguard 0.3.3", + "cc", ] [[package]] -name = "lock_api" -version = "0.3.4" +name = "linked-hash-map" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" -dependencies = [ - "scopeguard 1.1.0", -] +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" [[package]] name = "lock_api" @@ -3818,7 +3889,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ - "scopeguard 1.1.0", + "scopeguard", ] [[package]] @@ -3883,6 +3954,15 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -3910,7 +3990,7 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "mem" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-ethereum.git#0a090f9b3efd7e24193265cf0e4109bf2369ad98" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#223da7972113a548531804510708f87b629a48fd" [[package]] name = "memchr" @@ -3933,7 +4013,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" dependencies = [ - "autocfg 1.0.0", + "autocfg 1.1.0", ] [[package]] @@ -3942,7 +4022,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" dependencies = [ - "autocfg 1.0.0", + "autocfg 1.1.0", ] [[package]] @@ -3958,56 +4038,62 @@ dependencies = [ [[package]] name = "metrics" -version = "0.12.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b70227ece8711a1aa2f99655efd795d0cff297a5b9fe39645a93aacf6ad39d" +checksum = "142c53885123b68d94108295a09d4afe1a1388ed95b54d5dacd9a454753030f2" dependencies = [ - "metrics-core", + "ahash 0.7.6", + "metrics-macros", ] [[package]] -name = "metrics-core" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c064b3a1ff41f4bf6c91185c8a0caeccf8a8a27e9d0f92cc54cf3dbec812f48" - -[[package]] -name = "metrics-observer-prometheus" -version = "0.1.4" +name = "metrics-exporter-prometheus" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfe24ad8285ef8b239232135a65f89cc5fa4690bbfaf8907f4bef38f8b08eba" +checksum = "953cbbb6f9ba4b9304f4df79b98cdc9d14071ed93065a9fca11c00c5d9181b66" dependencies = [ - "hdrhistogram 6.3.4", - "metrics-core", + "hyper", + "indexmap", + "ipnet", + "metrics", "metrics-util", + "parking_lot 0.11.1", + "quanta", + "thiserror", + "tokio", + "tracing", ] [[package]] -name = "metrics-runtime" -version = "0.13.1" +name = "metrics-macros" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce0e4f69639ccc0c6b2f0612164f9817349eb25545ed1ffb5ef3e1e1c1d220b4" +checksum = "49e30813093f757be5cf21e50389a24dc7dbb22c49f23b7e8f51d69b508a5ffa" dependencies = [ - "arc-swap", - "atomic-shim", - "crossbeam-utils 0.7.2", - "im", - "metrics", - "metrics-core", - "metrics-observer-prometheus", - "metrics-util", - "parking_lot 0.10.2", - "quanta", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", ] [[package]] name = "metrics-util" -version = "0.3.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f8090a8886339f9468a04eeea0711e4cf27538b134014664308041307a1c5" +checksum = "fd1f4b69bef1e2b392b2d4a12902f2af90bb438ba4a66aa222d1023fa6561b50" dependencies = [ - "crossbeam-epoch 0.8.2", - "serde", + "aho-corasick", + "atomic-shim", + "crossbeam-epoch 0.9.5", + "crossbeam-utils 0.8.8", + "hashbrown 0.11.2", + "indexmap", + "metrics", + "num_cpus", + "ordered-float", + "parking_lot 0.11.1", + "quanta", + "radix_trie", + "sketches-ddsketch", ] [[package]] @@ -4018,20 +4104,20 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "miniz_oxide" -version = "0.3.7" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f" dependencies = [ - "adler32", + "adler 0.2.2", ] [[package]] name = "miniz_oxide" -version = "0.4.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ - "adler", + "adler 1.0.2", ] [[package]] @@ -4049,16 +4135,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log 0.4.14", - "miow", - "ntapi", "wasi 0.11.0+wasi-snapshot-preview1", - "winapi", + "windows-sys 0.45.0", ] [[package]] @@ -4077,12 +4161,13 @@ dependencies = [ "async-std", "async-trait", "atomicdex-gossipsub", + "common", "derive_more", "env_logger 0.7.1", "futures 0.3.15", "futures-rustls 0.21.1", "getrandom 0.2.6", - "hex 0.4.2", + "hex 0.4.3", "lazy_static", "libp2p", "libp2p-floodsub 0.22.0", @@ -4090,7 +4175,7 @@ dependencies = [ "rand 0.7.3", "regex", "rmp-serde", - "secp256k1", + "secp256k1 0.20.3", "serde", "serde_bytes", "serde_json", @@ -4101,6 +4186,28 @@ dependencies = [ "wasm-timer", ] +[[package]] +name = "mm2_bin_lib" +version = "1.0.0-beta" +dependencies = [ + "chrono", + "common", + "enum-primitive-derive", + "gstuff", + "jemallocator", + "js-sys", + "libc", + "mm2_core", + "mm2_main", + "mm2_rpc", + "num-traits", + "regex", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", +] + [[package]] name = "mm2_core" version = "0.1.0" @@ -4113,17 +4220,16 @@ dependencies = [ "derive_more", "futures 0.3.15", "gstuff", - "hex 0.4.2", - "keys", + "hex 0.4.3", "lazy_static", + "mm2_metrics", "mm2_rpc", "primitives", "rand 0.7.3", "serde", - "serde_bytes", "serde_json", "shared_ref_counter", - "uuid", + "uuid 0.7.4", ] [[package]] @@ -4134,7 +4240,7 @@ dependencies = [ "common", "derive_more", "futures 0.3.15", - "hex 0.4.2", + "hex 0.4.3", "itertools", "js-sys", "lazy_static", @@ -4167,6 +4273,44 @@ dependencies = [ "serde_json", ] +[[package]] +name = "mm2_eth" +version = "0.1.0" +dependencies = [ + "ethabi", + "ethkey", + "hex 0.4.3", + "indexmap", + "itertools", + "mm2_err_handle", + "secp256k1 0.20.3", + "serde", + "serde_json", + "web3", +] + +[[package]] +name = "mm2_gui_storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "common", + "db_common", + "derive_more", + "mm2_core", + "mm2_db", + "mm2_err_handle", + "mm2_number", + "mm2_test_helpers", + "rpc", + "ser_error", + "ser_error_derive", + "serde", + "serde_json", + "serde_repr", + "wasm-bindgen-test", +] + [[package]] name = "mm2_io" version = "0.1.0" @@ -4205,14 +4349,15 @@ dependencies = [ "dirs", "either", "enum-primitive-derive", - "ethereum-types 0.4.2", + "enum_from", + "ethereum-types", "futures 0.1.29", "futures 0.3.15", "futures-rustls 0.21.1", "gstuff", "hash-db", "hash256-std-hasher", - "hex 0.4.2", + "hex 0.4.3", "http 0.2.7", "hw_common", "hyper", @@ -4222,12 +4367,13 @@ dependencies = [ "keys", "lazy_static", "libc", - "metrics", "mm2-libp2p", "mm2_core", "mm2_db", "mm2_err_handle", + "mm2_gui_storage", "mm2_io", + "mm2_metrics", "mm2_net", "mm2_number", "mm2_rpc", @@ -4244,7 +4390,7 @@ dependencies = [ "rpc", "rpc_task", "script", - "secp256k1", + "secp256k1 0.20.3", "ser_error", "ser_error_derive", "serde", @@ -4259,7 +4405,7 @@ dependencies = [ "tokio", "trie-db", "trie-root 0.16.0", - "uuid", + "uuid 0.7.4", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -4268,6 +4414,50 @@ dependencies = [ "winapi", ] +[[package]] +name = "mm2_metamask" +version = "0.1.0" +dependencies = [ + "async-trait", + "common", + "derive_more", + "futures 0.3.15", + "itertools", + "js-sys", + "jsonrpc-core", + "lazy_static", + "mm2_err_handle", + "mm2_eth", + "parking_lot 0.12.0", + "serde", + "serde_derive", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "web3", +] + +[[package]] +name = "mm2_metrics" +version = "0.1.0" +dependencies = [ + "base64 0.10.1", + "common", + "derive_more", + "futures 0.3.15", + "hyper", + "hyper-rustls", + "itertools", + "metrics", + "metrics-exporter-prometheus", + "metrics-util", + "mm2_err_handle", + "serde", + "serde_derive", + "serde_json", + "wasm-timer", +] + [[package]] name = "mm2_net" version = "0.1.0" @@ -4277,6 +4467,7 @@ dependencies = [ "cfg-if 1.0.0", "common", "derive_more", + "ethkey", "futures 0.3.15", "gstuff", "http 0.2.7", @@ -4333,6 +4524,7 @@ dependencies = [ "chrono", "common", "crossterm", + "crypto", "db_common", "futures 0.3.15", "gstuff", @@ -4340,20 +4532,23 @@ dependencies = [ "lazy_static", "mm2_core", "mm2_io", + "mm2_metrics", "mm2_net", "mm2_number", "rand 0.7.3", "regex", + "rpc", "serde", + "serde_derive", "serde_json", - "uuid", + "uuid 0.7.4", ] [[package]] name = "mocktopus" -version = "0.7.11" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1e54a5bbecd61a064cb9c6ef396f8c896aee14e5baba8d1d555f35167dfd7c3" +checksum = "1e4f0d5a1621fea252541cf67533c4b9c32ee892d790768f4ad48f1063059537" dependencies = [ "mocktopus_macros", ] @@ -4380,11 +4575,11 @@ dependencies = [ "byteorder 1.4.3", "data-encoding", "multihash", - "percent-encoding 2.1.0", + "percent-encoding", "serde", "static_assertions", "unsigned-varint 0.7.1", - "url 2.2.2", + "url", ] [[package]] @@ -4423,7 +4618,7 @@ checksum = "d8883adfde9756c1d30b0f519c9b8c502a94b41ac62f696453c37c7fc0a958ce" [[package]] name = "multistream-select" version = "0.11.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "bytes 1.1.0", "futures 0.3.15", @@ -4433,6 +4628,15 @@ dependencies = [ "unsigned-varint 0.7.1", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec 1.6.1", +] + [[package]] name = "nix" version = "0.23.1" @@ -4446,12 +4650,6 @@ dependencies = [ "memoffset 0.6.4", ] -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - [[package]] name = "nohash-hasher" version = "0.2.0" @@ -4486,7 +4684,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d0a3d5e207573f948a9e5376662aa743a2ea13f7c50a554d7af443a73fbfeba" dependencies = [ - "autocfg 1.0.0", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -4497,7 +4695,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" dependencies = [ - "autocfg 1.0.0", + "autocfg 1.1.0", "num-integer", "num-traits", "serde", @@ -4520,7 +4718,7 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" dependencies = [ - "autocfg 1.0.0", + "autocfg 1.1.0", "num-traits", ] @@ -4530,7 +4728,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" dependencies = [ - "autocfg 1.0.0", + "autocfg 1.1.0", "num-bigint 0.4.3", "num-integer", "num-traits", @@ -4543,7 +4741,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" dependencies = [ - "autocfg 1.0.0", + "autocfg 1.1.0", ] [[package]] @@ -4578,6 +4776,15 @@ dependencies = [ "syn 1.0.95", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -4586,15 +4793,18 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.20.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] [[package]] name = "once_cell" -version = "1.8.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "opaque-debug" @@ -4608,6 +4818,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "ordered-float" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +dependencies = [ + "num-traits", +] + [[package]] name = "ouroboros" version = "0.13.0" @@ -4702,7 +4921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c32561d248d352148124f036cac253a644685a21dc9fea383eb4907d7bd35a8f" dependencies = [ "cfg-if 1.0.0", - "ethereum-types 0.13.1", + "ethereum-types", "hashbrown 0.12.1", "impl-trait-for-tuples", "lru", @@ -4730,37 +4949,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bcaa58ee64f8e4a3d02f5d8e6ed0340eae28fed6fdabd984ad1776e3b43848a" -[[package]] -name = "parking_lot" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337" -dependencies = [ - "lock_api 0.1.5", - "parking_lot_core 0.4.0", -] - -[[package]] -name = "parking_lot" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" -dependencies = [ - "lock_api 0.3.4", - "parking_lot_core 0.6.2", - "rustc_version 0.2.3", -] - -[[package]] -name = "parking_lot" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" -dependencies = [ - "lock_api 0.3.4", - "parking_lot_core 0.7.2", -] - [[package]] name = "parking_lot" version = "0.11.1" @@ -4768,7 +4956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" dependencies = [ "instant", - "lock_api 0.4.6", + "lock_api", "parking_lot_core 0.8.0", ] @@ -4778,23 +4966,10 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" dependencies = [ - "lock_api 0.4.6", + "lock_api", "parking_lot_core 0.9.1", ] -[[package]] -name = "parking_lot_core" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9" -dependencies = [ - "libc", - "rand 0.6.5", - "rustc_version 0.2.3", - "smallvec 0.6.14", - "winapi", -] - [[package]] name = "parking_lot_core" version = "0.6.2" @@ -4810,20 +4985,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "parking_lot_core" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" -dependencies = [ - "cfg-if 0.1.10", - "cloudabi 0.0.3", - "libc", - "redox_syscall 0.1.56", - "smallvec 1.6.1", - "winapi", -] - [[package]] name = "parking_lot_core" version = "0.8.0" @@ -4849,14 +5010,14 @@ dependencies = [ "libc", "redox_syscall 0.2.10", "smallvec 1.6.1", - "windows-sys", + "windows-sys 0.32.0", ] [[package]] name = "paste" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" [[package]] name = "pbkdf2" @@ -4877,10 +5038,31 @@ dependencies = [ ] [[package]] -name = "percent-encoding" -version = "1.0.1" +name = "peg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" +dependencies = [ + "peg-runtime", + "proc-macro2 1.0.39", + "quote 1.0.18", +] + +[[package]] +name = "peg-runtime" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" +checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" [[package]] name = "percent-encoding" @@ -4956,6 +5138,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der", + "spki", + "zeroize", +] + [[package]] name = "pkg-config" version = "0.3.17" @@ -5007,21 +5200,22 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28720988bff275df1f51b171e1b2a18c30d194c4d2b61defdacecd625a5d94a" dependencies = [ - "fixed-hash 0.7.0", + "fixed-hash", "impl-codec", "impl-rlp", "impl-serde", "scale-info", - "uint 0.9.1", + "uint", ] [[package]] name = "primitives" version = "0.1.0" dependencies = [ + "bitcoin_hashes", "byteorder 1.4.3", - "rustc-hex 2.1.0", - "uint 0.9.1", + "rustc-hex", + "uint", ] [[package]] @@ -5200,16 +5394,22 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" dependencies = [ - "percent-encoding 2.1.0", + "percent-encoding", ] [[package]] name = "quanta" -version = "0.3.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7a1905379198075914bc93d32a5465c40474f90a078bb13439cb00c547bcc" +checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8" dependencies = [ + "crossbeam-utils 0.8.8", "libc", + "mach", + "once_cell", + "raw-cpuid", + "wasi 0.10.2+wasi-snapshot-preview1", + "web-sys", "winapi", ] @@ -5284,6 +5484,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.3.23" @@ -5505,12 +5715,12 @@ dependencies = [ ] [[package]] -name = "rand_xoshiro" -version = "0.6.0" +name = "raw-cpuid" +version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +checksum = "2c49596760fce12ca21550ac21dc5a9617b2ea4b6e0aa7d8dab8ff2824fc2bba" dependencies = [ - "rand_core 0.6.3", + "bitflags", ] [[package]] @@ -5519,7 +5729,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" dependencies = [ - "autocfg 1.0.0", + "autocfg 1.1.0", "crossbeam-deque 0.8.1", "either", "rayon-core", @@ -5642,7 +5852,7 @@ dependencies = [ "futures-util", "h2", "http 0.2.7", - "http-body 0.4.4", + "http-body 0.4.5", "hyper", "hyper-rustls", "ipnet", @@ -5650,7 +5860,7 @@ dependencies = [ "lazy_static", "log 0.4.14", "mime", - "percent-encoding 2.1.0", + "percent-encoding", "pin-project-lite 0.2.9", "rustls 0.20.4", "rustls-pemfile 0.2.1", @@ -5659,7 +5869,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-rustls", - "url 2.2.2", + "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -5716,22 +5926,12 @@ dependencies = [ [[package]] name = "rlp" -version = "0.3.0" -source = "git+https://github.com/artemii235/parity-common#986aba04f5e11e855d08a41c40bf03b4ba2862b0" -dependencies = [ - "byteorder 1.4.3", - "ethereum-types 0.4.2", - "rustc-hex 2.1.0", -] - -[[package]] -name = "rlp" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54369147e3e7796c9b885c7304db87ca3d09a0a98f72843d532868675bbfba8" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" dependencies = [ "bytes 1.1.0", - "rustc-hex 2.1.0", + "rustc-hex", ] [[package]] @@ -5771,9 +5971,10 @@ version = "0.1.0" dependencies = [ "chain", "keys", + "lazy_static", "log 0.4.14", "primitives", - "rustc-hex 2.1.0", + "rustc-hex", "script", "serde", "serde_derive", @@ -5790,7 +5991,6 @@ dependencies = [ "derive_more", "futures 0.3.15", "mm2_err_handle", - "mm2_rpc", "ser_error", "ser_error_derive", "serde", @@ -5853,12 +6053,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-hex" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceb8ce7a5e520de349e1fa172baeba4a9e8d5ef06c47471863530bc4972ee1e" - [[package]] name = "rustc-hex" version = "2.1.0" @@ -5935,7 +6129,7 @@ checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "rw-stream-sink" version = "0.3.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#ef2afcd41eac1459c0408a7a6eb599bfc2035938" +source = "git+https://github.com/libp2p/rust-libp2p.git?tag=v0.45.1#802d00e645894d8895f2f9f665b921452d992b86" dependencies = [ "futures 0.3.15", "pin-project 1.0.10", @@ -5989,15 +6183,15 @@ checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" [[package]] name = "scopeguard" -version = "0.3.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] -name = "scopeguard" -version = "1.1.0" +name = "scratch" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" [[package]] name = "script" @@ -6041,6 +6235,7 @@ checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" dependencies = [ "der", "generic-array 0.14.5", + "pkcs8", "subtle 2.4.0", "zeroize", ] @@ -6052,14 +6247,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" dependencies = [ "rand 0.6.5", - "secp256k1-sys", + "secp256k1-sys 0.4.2", +] + +[[package]] +name = "secp256k1" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +dependencies = [ + "secp256k1-sys 0.5.2", ] [[package]] name = "secp256k1-sys" -version = "0.4.0" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-sys" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e4b6455ee49f5901c8985b88f98fb0a0e1d90a6661f5a03f4888bd987dad29" +checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" dependencies = [ "cc", ] @@ -6094,12 +6307,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -[[package]] -name = "send_wrapper" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eddf2e8f50ced781f288c19f18621fa72a3779e3cb58dbf23b07469b0abeb4" - [[package]] name = "send_wrapper" version = "0.4.0" @@ -6296,6 +6503,16 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "sha3" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaedf34ed289ea47c2b741bb72e5357a209512d67bcd4bda44359e5bf0470f56" +dependencies = [ + "digest 0.10.3", + "keccak", +] + [[package]] name = "shared_ref_counter" version = "0.1.0" @@ -6350,20 +6567,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "833011ca526bd88f16778d32c699d325a9ad302fa06381cd66f7be63351d3f6d" [[package]] -name = "sized-chunks" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" -dependencies = [ - "bitmaps", - "typenum", -] - -[[package]] -name = "slab" -version = "0.3.0" +name = "sketches-ddsketch" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23" +checksum = "04d2ecae5fcf33b122e2e6bd520a57ccf152d2dde3b38c71039df1a6867264ee" [[package]] name = "slab" @@ -6401,7 +6608,7 @@ dependencies = [ "libc", "once_cell", "scoped-tls", - "slab 0.4.2", + "slab", "socket2 0.3.19", "wepoll-sys-stjepang", "winapi", @@ -6555,7 +6762,7 @@ dependencies = [ "thiserror", "tiny-bip39", "uriparse", - "url 2.2.2", + "url", ] [[package]] @@ -6569,7 +6776,7 @@ dependencies = [ "serde", "serde_derive", "serde_yaml", - "url 2.2.2", + "url", ] [[package]] @@ -6583,7 +6790,7 @@ dependencies = [ "bs58", "clap", "indicatif", - "jsonrpc-core 18.0.0", + "jsonrpc-core", "log 0.4.14", "rayon", "reqwest", @@ -6603,7 +6810,7 @@ dependencies = [ "thiserror", "tokio", "tungstenite", - "url 2.2.2", + "url", ] [[package]] @@ -6738,7 +6945,7 @@ dependencies = [ "solana-sdk", "solana-version", "tokio", - "url 2.2.2", + "url", ] [[package]] @@ -6804,7 +7011,7 @@ dependencies = [ "serde_bytes", "serde_derive", "sha2 0.9.9", - "sha3", + "sha3 0.9.1", "solana-frozen-abi", "solana-frozen-abi-macro", "solana-logger", @@ -6963,7 +7170,7 @@ dependencies = [ "serde_derive", "serde_json", "sha2 0.9.9", - "sha3", + "sha3 0.9.1", "solana-frozen-abi", "solana-frozen-abi-macro", "solana-logger", @@ -7203,6 +7410,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "spl-associated-token-account" version = "1.0.3" @@ -7240,10 +7457,15 @@ dependencies = [ name = "spv_validation" version = "0.1.0" dependencies = [ + "async-trait", "chain", + "common", + "derive_more", + "keys", + "lazy_static", "primitives", "ripemd160", - "rustc-hex 2.1.0", + "rustc-hex", "serde", "serde_json", "serialization", @@ -7363,6 +7585,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + [[package]] name = "symlink" version = "0.1.0" @@ -7449,7 +7680,7 @@ dependencies = [ [[package]] name = "tc_cli_client" version = "0.2.0" -source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "log 0.4.14", "serde", @@ -7461,7 +7692,7 @@ dependencies = [ [[package]] name = "tc_coblox_bitcoincore" version = "0.5.0" -source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "hex 0.3.2", "hmac 0.7.1", @@ -7474,7 +7705,7 @@ dependencies = [ [[package]] name = "tc_core" version = "0.3.0" -source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "debug_stub_derive", "log 0.4.14", @@ -7483,7 +7714,7 @@ dependencies = [ [[package]] name = "tc_dynamodb_local" version = "0.2.0" -source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "log 0.4.14", "tc_core", @@ -7492,7 +7723,7 @@ dependencies = [ [[package]] name = "tc_elasticmq" version = "0.2.0" -source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "tc_core", ] @@ -7500,7 +7731,7 @@ dependencies = [ [[package]] name = "tc_generic" version = "0.2.0" -source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "tc_core", ] @@ -7508,7 +7739,7 @@ dependencies = [ [[package]] name = "tc_parity_parity" version = "0.5.0" -source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "log 0.4.14", "tc_core", @@ -7517,7 +7748,7 @@ dependencies = [ [[package]] name = "tc_postgres" version = "0.2.0" -source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "log 0.4.14", "tc_core", @@ -7526,7 +7757,7 @@ dependencies = [ [[package]] name = "tc_redis" version = "0.2.0" -source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "tc_core", ] @@ -7534,7 +7765,7 @@ dependencies = [ [[package]] name = "tc_trufflesuite_ganachecli" version = "0.4.0" -source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "tc_core", ] @@ -7553,6 +7784,94 @@ dependencies = [ "winapi", ] +[[package]] +name = "tendermint" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca881fa4dedd2b46334f13be7fbc8cc1549ba4be5a833fe4e73d1a1baaf7949" +dependencies = [ + "async-trait", + "bytes 1.1.0", + "ed25519", + "ed25519-dalek", + "flex-error", + "futures 0.3.15", + "k256", + "num-traits", + "once_cell", + "prost", + "prost-types", + "ripemd160", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.9.9", + "signature", + "subtle 2.4.0", + "subtle-encoding", + "tendermint-proto", + "time 0.3.11", + "zeroize", +] + +[[package]] +name = "tendermint-config" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c56ee93f4e9b7e7daba86d171f44572e91b741084384d0ae00df7991873dfd" +dependencies = [ + "flex-error", + "serde", + "serde_json", + "tendermint", + "toml", + "url", +] + +[[package]] +name = "tendermint-proto" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71f925d74903f4abbdc4af0110635a307b3cb05b175fdff4a7247c14a4d0874" +dependencies = [ + "bytes 1.1.0", + "flex-error", + "num-derive", + "num-traits", + "prost", + "prost-types", + "serde", + "serde_bytes", + "subtle-encoding", + "time 0.3.11", +] + +[[package]] +name = "tendermint-rpc" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e63f57ee05a1e927887191c76d1b139de9fa40c180b9f8727ee44377242a6" +dependencies = [ + "bytes 1.1.0", + "flex-error", + "getrandom 0.2.6", + "peg", + "pin-project 1.0.10", + "serde", + "serde_bytes", + "serde_json", + "subtle-encoding", + "tendermint", + "tendermint-config", + "tendermint-proto", + "thiserror", + "time 0.3.11", + "url", + "uuid 0.8.2", + "walkdir", +] + [[package]] name = "termcolor" version = "1.1.0" @@ -7576,13 +7895,13 @@ dependencies = [ name = "test_helpers" version = "0.1.0" dependencies = [ - "hex 0.4.2", + "hex 0.4.3", ] [[package]] name = "testcontainers" version = "0.7.0" -source = "git+https://github.com/artemii235/testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" dependencies = [ "tc_cli_client", "tc_coblox_bitcoincore", @@ -7656,11 +7975,22 @@ dependencies = [ "libc", "standback", "stdweb", - "time-macros", + "time-macros 0.1.1", "version_check", "winapi", ] +[[package]] +name = "time" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +dependencies = [ + "libc", + "num_threads", + "time-macros 0.2.4", +] + [[package]] name = "time-macros" version = "0.1.1" @@ -7671,6 +8001,12 @@ dependencies = [ "time-macros-impl", ] +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + [[package]] name = "time-macros-impl" version = "0.1.2" @@ -7709,7 +8045,7 @@ version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f11c56c1b46016bb1129db9399f905385490f3e17907e4a8430e57f9a5b979c" dependencies = [ - "crunchy 0.2.2", + "crunchy", ] [[package]] @@ -7718,7 +8054,7 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ - "crunchy 0.2.2", + "crunchy", ] [[package]] @@ -7738,22 +8074,22 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.18.2" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" +checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" dependencies = [ + "autocfg 1.1.0", "bytes 1.1.0", "libc", "memchr", - "mio 0.8.2", + "mio 0.8.6", "num_cpus", - "once_cell", "parking_lot 0.12.0", "pin-project-lite 0.2.9", "signal-hook-registry", "socket2 0.4.4", "tokio-macros", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -7809,30 +8145,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-timer" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6131e780037787ff1b3f8aad9da83bca02438b72277850dd6ad0d455e0e20efc" -dependencies = [ - "futures 0.1.29", - "slab 0.3.0", -] - -[[package]] -name = "tokio-util" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" -dependencies = [ - "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" @@ -7872,10 +8184,10 @@ dependencies = [ "futures-util", "h2", "http 0.2.7", - "http-body 0.4.4", + "http-body 0.4.5", "hyper", "hyper-timeout", - "percent-encoding 2.1.0", + "percent-encoding", "pin-project 1.0.10", "prost", "prost-derive", @@ -7883,7 +8195,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-stream", - "tokio-util 0.7.2", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -7907,9 +8219,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5651b5f6860a99bd1adb59dbfe1db8beb433e73709d9032b413a77e2fb7c066a" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", @@ -7917,10 +8229,9 @@ dependencies = [ "pin-project 1.0.10", "pin-project-lite 0.2.9", "rand 0.8.4", - "slab 0.4.2", + "slab", "tokio", - "tokio-stream", - "tokio-util 0.6.7", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -7937,7 +8248,7 @@ dependencies = [ "futures-core", "futures-util", "http 0.2.7", - "http-body 0.4.4", + "http-body 0.4.5", "http-range-header", "pin-project-lite 0.2.9", "tower", @@ -7953,9 +8264,9 @@ checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" [[package]] name = "tower-service" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" @@ -8067,7 +8378,7 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "idna 0.2.3", + "idna", "ipnet", "lazy_static", "log 0.4.14", @@ -8076,7 +8387,7 @@ dependencies = [ "thiserror", "tinyvec", "tokio", - "url 2.2.2", + "url", ] [[package]] @@ -8121,7 +8432,7 @@ dependencies = [ "rustls 0.20.4", "sha-1", "thiserror", - "url 2.2.2", + "url", "utf-8", "webpki 0.22.0", "webpki-roots", @@ -8135,31 +8446,20 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "uint" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754ba11732b9161b94c41798e5197e5e75388d012f760c42adb5000353e98646" -dependencies = [ - "byteorder 1.4.3", - "crunchy 0.1.6", - "rustc-hex 2.1.0", -] - -[[package]] -name = "uint" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0" dependencies = [ "byteorder 1.4.3", - "crunchy 0.2.2", - "hex 0.4.2", + "crunchy", + "hex 0.4.3", "static_assertions", ] [[package]] name = "unexpected" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-ethereum.git#0a090f9b3efd7e24193265cf0e4109bf2369ad98" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#223da7972113a548531804510708f87b629a48fd" [[package]] name = "unicode-bidi" @@ -8257,17 +8557,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "url" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" -dependencies = [ - "idna 0.1.5", - "matches", - "percent-encoding 1.0.1", -] - [[package]] name = "url" version = "2.2.2" @@ -8275,9 +8564,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ "form_urlencoded", - "idna 0.2.3", + "idna", "matches", - "percent-encoding 2.1.0", + "percent-encoding", ] [[package]] @@ -8295,7 +8584,7 @@ dependencies = [ "common", "crypto", "derive_more", - "hex 0.4.2", + "hex 0.4.3", "keys", "mm2_err_handle", "primitives", @@ -8314,6 +8603,12 @@ dependencies = [ "serde", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" + [[package]] name = "value-bag" version = "1.0.0-alpha.8" @@ -8485,15 +8780,14 @@ dependencies = [ [[package]] name = "wasm-timer" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324c5e65a08699c9c4334ba136597ab22b85dccd4b65dd1e36ccf8f723a95b54" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" dependencies = [ "futures 0.3.15", "js-sys", - "parking_lot 0.9.0", + "parking_lot 0.11.1", "pin-utils", - "send_wrapper 0.2.0", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -8511,24 +8805,31 @@ dependencies = [ [[package]] name = "web3" -version = "0.6.0" -source = "git+https://github.com/artemii235/rust-web3#9357249d31c258c5a7d47dfc51e9bb7d980056f1" +version = "0.19.0" +source = "git+https://github.com/KomodoPlatform/rust-web3?tag=v0.19.0#ec5e72a5c95e3935ea0c9ab77b501e3926686fa9" dependencies = [ - "arrayvec 0.4.12", - "base64 0.10.1", - "error-chain", + "arrayvec 0.7.1", + "derive_more", "ethabi", - "ethereum-types 0.4.2", - "futures 0.1.29", - "jsonrpc-core 8.0.1", + "ethereum-types", + "futures 0.3.15", + "futures-timer", + "getrandom 0.2.6", + "hex 0.4.3", + "idna", + "js-sys", + "jsonrpc-core", "log 0.4.14", - "parking_lot 0.7.1", - "rustc-hex 1.0.0", + "parking_lot 0.12.0", + "pin-project 1.0.10", + "rand 0.8.4", + "rlp", "serde", - "serde_derive", + "serde-wasm-bindgen", "serde_json", - "tokio-timer", - "url 1.7.2", + "tiny-keccak 2.0.2", + "wasm-bindgen", + "wasm-bindgen-futures", ] [[package]] @@ -8622,43 +8923,124 @@ 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", + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + [[package]] name = "windows_aarch64_msvc" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + [[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_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + [[package]] name = "windows_i686_msvc" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + [[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_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + [[package]] name = "windows_x86_64_msvc" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + [[package]] name = "winreg" version = "0.7.0" @@ -8743,10 +9125,10 @@ dependencies = [ "bs58", "ff 0.8.0", "group 0.8.0", - "hex 0.4.2", + "hex 0.4.3", "jubjub", "nom", - "percent-encoding 2.1.0", + "percent-encoding", "protobuf", "protobuf-codegen-pure", "rand_core 0.5.1", @@ -8805,14 +9187,14 @@ dependencies = [ "fpe", "funty 1.1.0", "group 0.8.0", - "hex 0.4.2", + "hex 0.4.3", "jubjub", "lazy_static", "log 0.4.14", "rand 0.7.3", "rand_core 0.5.1", "ripemd160", - "secp256k1", + "secp256k1 0.20.3", "sha2 0.9.9", "subtle 2.4.0", "zcash_note_encryption", @@ -8885,13 +9267,3 @@ dependencies = [ "cc", "libc", ] - -[[patch.unused]] -name = "backtrace" -version = "0.3.32" -source = "git+https://github.com/artemii235/backtrace-rs.git#6f9ac910252ef7833105080c9cb4d5948dcb74c2" - -[[patch.unused]] -name = "backtrace-sys" -version = "0.1.30" -source = "git+https://github.com/artemii235/backtrace-rs.git#6f9ac910252ef7833105080c9cb4d5948dcb74c2" diff --git a/Cargo.toml b/Cargo.toml index 4e27bed80d..9c3c237abc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,19 @@ [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/enum_from", "mm2src/derives/ser_error", "mm2src/derives/ser_error_derive", "mm2src/floodsub", "mm2src/gossipsub", + "mm2src/mm2_gui_storage", "mm2src/hw_common", + "mm2src/mm2_bin_lib", "mm2src/mm2_bitcoin/crypto", "mm2src/mm2_bitcoin/chain", "mm2src/mm2_bitcoin/keys", @@ -25,14 +26,17 @@ members = [ "mm2src/mm2_core", "mm2src/mm2_db", "mm2src/mm2_err_handle", - "mm2src/mm2_test_helpers", + "mm2src/mm2_eth", + "mm2src/mm2_io", "mm2src/mm2_libp2p", + "mm2src/mm2_metamask", + "mm2src/mm2_metrics", "mm2src/mm2_main", "mm2src/mm2_net", "mm2src/mm2_number", - "mm2src/mm2_io", "mm2src/mm2_rpc", "mm2src/rpc_task", + "mm2src/mm2_test_helpers", "mm2src/trezor", ] @@ -40,11 +44,11 @@ members = [ resolver = "2" [profile.release] -# Due to the "overrides" only affects our workspace crates, as intended. -debug = true +debug = 1 debug-assertions = false -# For better or worse, might affect the stack traces in our portion of the code. -#opt-level = 1 +# For some reason, opt-level 3 started causing infinite Windows builds after Cosmos integration +# TODO troubleshoot it +opt-level = 2 [profile.test] # required to avoid a long running process of librustcash additional chain validation that is enabled with debug assertions @@ -54,11 +58,8 @@ debug-assertions = false # 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. -# Consider removing once this issue is solved and fix applied to upstream repository -# https://github.com/rust-lang/backtrace-rs/issues/227 -[patch.crates-io] -backtrace = { git = "https://github.com/artemii235/backtrace-rs.git" } -backtrace-sys = { git = "https://github.com/artemii235/backtrace-rs.git" } +[profile.ci] +inherits = "dev" +# full debug info is not required +debug = 1 +debug-assertions = false \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a5f8b65065..1fdc5226c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,9 @@ RUN cd /mm2 && cargo fetch # Only needed when we're developing or changing something locally. #COPY . /mm2 +# Important for x86_64 builds +ENV JEMALLOC_SYS_WITH_MALLOC_CONF="background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" + # Build MM1 and MM2. # Increased verbosity here allows us to see the MM1 CMake logs. RUN cd /mm2 &&\ diff --git a/Dockerfile.dev-release b/Dockerfile.dev-release index 036c3c6195..48e3f47e72 100644 --- a/Dockerfile.dev-release +++ b/Dockerfile.dev-release @@ -1,5 +1,5 @@ FROM debian:stable-slim WORKDIR /mm2 -COPY target/release/mm2 /usr/local/bin/mm2 +COPY target/ci/mm2 /usr/local/bin/mm2 EXPOSE 7783 CMD ["mm2"] diff --git a/README.md b/README.md index da88000275..da537c40f6 100755 --- a/README.md +++ b/README.md @@ -84,11 +84,17 @@ If you want to build from source, the following prerequisites are required: - [Download](https://github.com/protocolbuffers/protobuf/releases) or [compile](https://github.com/protocolbuffers/protobuf) `protoc 3.21.x+` and add it to your PATH env. It is also available via package managers depending on the OS. - Additional Rust Components ``` - rustup install nightly-2022-02-01 - rustup default nightly-2022-02-01 + rustup install nightly-2022-10-29 + rustup default nightly-2022-10-29 rustup component add rustfmt-preview ``` - + +**Note for x86_64 UNIX systems**: +To have more efficient memory consumption please execute the following command before building mm2. (It's also good to have before launching mm2.) +```sh +export JEMALLOC_SYS_WITH_MALLOC_CONF="background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" +``` + 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). diff --git a/azure-pipelines-android-job.yml b/azure-pipelines-android-job.yml index 8b8f3ead86..817cc9582a 100644 --- a/azure-pipelines-android-job.yml +++ b/azure-pipelines-android-job.yml @@ -23,28 +23,36 @@ jobs: echo "##vso[task.setvariable variable=COMMIT_HASH]${TAG}" displayName: Setup ENV - bash: | - VERSION=2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_android_armv7_Release + VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_android_armv7_CI if ! grep -q $VERSION MM_VERSION; then echo $VERSION > MM_VERSION fi cat MM_VERSION export PATH=$PATH:/home/azureagent/android-ndk/arch-ndk/x86_64/android-ndk/bin - CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo build --target=armv7-linux-androideabi --lib --release + CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --profile ci --crate-type=staticlib --package mm2_bin_lib displayName: 'Build armv7' + env: + MANUAL_MM_VERSION: true - bash: | - VERSION=2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_android_aarch64_Release + VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_android_aarch64_CI if ! grep -q $VERSION MM_VERSION; then echo $VERSION > MM_VERSION fi cat MM_VERSION export PATH=$PATH:/home/azureagent/android-ndk/arch-ndk/x86_64/android-ndk/bin - CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo build --features native --target=aarch64-linux-android --lib --release + CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --profile ci --crate-type=staticlib --package mm2_bin_lib displayName: 'Build aarch64' + env: + MANUAL_MM_VERSION: true - bash: | rm -rf upload mkdir upload - zip upload/mm2-$(COMMIT_HASH)-android-armv7-Release target/armv7-linux-androideabi/release/libmm2.a -j - zip upload/mm2-$(COMMIT_HASH)-android-aarch64-Release target/aarch64-linux-android/release/libmm2.a -j + mv target/armv7-linux-androideabi/ci/libmm2lib.a upload/libmm2.a + zip upload/mm2-$(COMMIT_HASH)-android-armv7-CI upload/libmm2.a -j + rm upload/libmm2.a + mv target/aarch64-linux-android/ci/libmm2lib.a upload/libmm2.a + zip upload/mm2-$(COMMIT_HASH)-android-aarch64-CI upload/libmm2.a -j + rm upload/libmm2.a displayName: 'Prepare upload' - task: CopyFilesOverSSH@0 inputs: diff --git a/azure-pipelines-build-stage-job.yml b/azure-pipelines-build-stage-job.yml index 2efa0c5b58..8cdcc82e9e 100644 --- a/azure-pipelines-build-stage-job.yml +++ b/azure-pipelines-build-stage-job.yml @@ -35,21 +35,22 @@ jobs: - bash: | rm -rf upload mkdir upload - VERSION=2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Release + VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_CI if ! grep -q $VERSION MM_VERSION; then echo $VERSION > MM_VERSION fi cat MM_VERSION if [ $AGENT_OS = "Darwin" ] then - cargo build --bin mm2 --release --target x86_64-apple-darwin + cargo build --bin mm2 --profile ci --target x86_64-apple-darwin else - cargo build --bin mm2 --release + cargo build --bin mm2 --profile ci fi - displayName: 'Build MM2 Release' + displayName: 'Build MM2' condition: ne ( variables['Build.Reason'], 'PullRequest' ) env: MANUAL_MM_VERSION: true + JEMALLOC_SYS_WITH_MALLOC_CONF: "background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" - task: Docker@2 displayName: Build & Push container of dev branch condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['Build.SourceBranchName'], 'dev' ) ) @@ -79,9 +80,9 @@ jobs: - bash: | if [ $AGENT_OS = "Darwin" ] then - cargo test --all --target x86_64-apple-darwin -- --test-threads=16 + cargo test --all --target x86_64-apple-darwin --profile ci -- --test-threads=16 else - cargo test --all -- --test-threads=32 + cargo test --all --profile ci -- --test-threads=32 fi displayName: 'Test MM2' timeoutInMinutes: 22 @@ -104,17 +105,16 @@ jobs: # 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' + zip upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-CI target/ci/mm2 -j + displayName: 'Prepare CI build upload Linux' condition: and ( eq( variables['Agent.OS'], 'Linux' ), ne ( variables['Build.Reason'], 'PullRequest' ) ) - bash: | - 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' + zip upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-CI target/x86_64-apple-darwin/ci/mm2 -j + displayName: 'Prepare CI build upload MacOS' 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' + 7z a .\upload\mm2-$(COMMIT_HASH)-$(Agent.OS)-CI.zip .\target\ci\mm2.exe .\target\ci\*.dll "$Env:windir\system32\msvcr100.dll" "$Env:windir\system32\msvcp140.dll" "$Env:windir\system32\vcruntime140.dll" + displayName: 'Prepare CI build upload Windows' 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 diff --git a/azure-pipelines-ios-job.yml b/azure-pipelines-ios-job.yml index 60cfa8cf29..d513287742 100644 --- a/azure-pipelines-ios-job.yml +++ b/azure-pipelines-ios-job.yml @@ -23,17 +23,21 @@ jobs: echo "##vso[task.setvariable variable=COMMIT_HASH]${TAG}" displayName: Setup ENV - bash: | - VERSION=2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_ios_aarch64_Release + VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_ios_aarch64_CI if ! grep -q $VERSION MM_VERSION; then echo $VERSION > MM_VERSION fi cat MM_VERSION - cargo build --target aarch64-apple-ios --lib --release + cargo rustc --target aarch64-apple-ios --lib --profile ci --package mm2_bin_lib --crate-type=staticlib displayName: 'Build iOS lib' + env: + MANUAL_MM_VERSION: true - bash: | rm -rf upload mkdir upload - zip upload/mm2-$(COMMIT_HASH)-ios-aarch64-Release target/aarch64-apple-ios/release/libmm2.a -j + mv target/aarch64-apple-ios/ci/libmm2lib.a upload/libmm2.a + zip upload/mm2-$(COMMIT_HASH)-ios-aarch64-CI upload/libmm2.a -j + rm upload/libmm2.a displayName: 'Prepare upload' - task: CopyFilesOverSSH@0 inputs: diff --git a/azure-pipelines-lint-stage-job.yml b/azure-pipelines-lint-stage-job.yml index 3f2ab6f495..8f696eb8b2 100644 --- a/azure-pipelines-lint-stage-job.yml +++ b/azure-pipelines-lint-stage-job.yml @@ -32,12 +32,22 @@ jobs: env: MANUAL_MM_VERSION: true - bash: | - cargo clippy -- -D warnings + if [ $AGENT_OS = "Darwin" ] + then + cargo clippy --profile ci --target x86_64-apple-darwin -- -D warnings + else + cargo clippy --profile ci -- -D warnings + fi displayName: 'Check Clippy warnings' env: MANUAL_MM_VERSION: true - bash: | - cargo check --tests + if [ $AGENT_OS = "Darwin" ] + then + cargo check --tests --profile ci --target x86_64-apple-darwin + else + cargo check --tests --profile ci + fi displayName: 'Check Tests' env: MANUAL_MM_VERSION: true diff --git a/azure-pipelines-release-stage-job.yml b/azure-pipelines-release-stage-job.yml index 3ea90d3059..06ef60c34e 100644 --- a/azure-pipelines-release-stage-job.yml +++ b/azure-pipelines-release-stage-job.yml @@ -41,7 +41,7 @@ jobs: mkdir upload displayName: 'Recreate upload dir' - bash: | - VERSION=2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Debug + VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Debug if ! grep -q $VERSION MM_VERSION; then echo $VERSION > MM_VERSION fi @@ -67,6 +67,7 @@ jobs: condition: eq( variables['DEBUG_UPLOADED'], '' ) env: MANUAL_MM_VERSION: true + JEMALLOC_SYS_WITH_MALLOC_CONF: "background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" - 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' @@ -82,7 +83,7 @@ jobs: condition: and( eq( variables['Agent.OS'], 'Windows_NT' ), eq( variables['DEBUG_UPLOADED'], '' ) ) - bash: | rm -f MM_VERSION - VERSION=2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Release + VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Release if ! grep -q $VERSION MM_VERSION; then echo $VERSION > MM_VERSION fi diff --git a/azure-pipelines-wasm-stage-job.yml b/azure-pipelines-wasm-stage-job.yml index 65519832df..350b194eae 100644 --- a/azure-pipelines-wasm-stage-job.yml +++ b/azure-pipelines-wasm-stage-job.yml @@ -35,21 +35,21 @@ jobs: - bash: | rm -rf upload mkdir upload - VERSION=2.1.$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Release + VERSION=$(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 + CC=clang-8 wasm-pack build mm2src/mm2_bin_lib --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 + CC=clang-8 cargo test --package mm2_main --target wasm32-unknown-unknown --release displayName: 'Test MM2 WASM' env: - WASM_BINDGEN_TEST_TIMEOUT: 120 + WASM_BINDGEN_TEST_TIMEOUT: 180 GECKODRIVER: '/home/azureagent/wasm/geckodriver' BOB_PASSPHRASE: $(${{ parameters.bob_passphrase }}) ALICE_PASSPHRASE: $(${{ parameters.alice_passphrase }}) @@ -57,7 +57,7 @@ jobs: 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 + zip ../../upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Wasm-Release mm2lib_bg.wasm mm2lib.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 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6ec3792170..44e1a63d57 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,6 +22,16 @@ trigger: pr: # https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema%2Cparameter-schema#pr-trigger drafts: false +# https://docs.microsoft.com/ru-ru/azure/devops/pipelines/process/scheduled-triggers?view=azure-devops&tabs=yaml#scheduled-triggers +# Triggers clean checkout to compile from scratch, remove old builds artifacts, and free disk space on CI agents. +# https://github.com/KomodoPlatform/atomicDEX-API/blob/957fd856fb74d462058a5ad47ec46d79e3bf1d83/azure-pipelines-build-stage-job.yml#L20 +schedules: + - cron: "0 0 * * *" + displayName: Scheduled clean midnight UTC build + branches: + include: + - dev + stages: - stage: Lint displayName: Formatting, Clippy, and other checks diff --git a/deny.toml b/deny.toml index 6bb0b0f430..0120610616 100644 --- a/deny.toml +++ b/deny.toml @@ -48,20 +48,25 @@ 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 # RUSTSEC-2022-0040 is related to owning-ref, which seems unmaintained. We need to find a way to get rid of it. https://github.com/KomodoPlatform/atomicDEX-API/issues/1429 -# RUSTSEC-2020-0159 is related to chrono, updated in dev # RUSTSEC-2022-0055 is axum/axum-core vulnerability, which seems to be related only to server-side, which we don't utilize. # RUSTSEC-2023-0001 is tokio Windows-specific bug in the code that we don't use +# Unignore RUSTSEC-2022-0084 after updating libp2p ignore = [ - "RUSTSEC-2021-0113", "RUSTSEC-2020-0071", "RUSTSEC-2022-0040", - "RUSTSEC-2020-0159", - "RUSTSEC-2022-0055", - "RUSTSEC-2023-0001", - #"RUSTSEC-0000-0000", + "RUSTSEC-2022-0041", + "RUSTSEC-2022-0070", + "RUSTSEC-2021-0145", + "RUSTSEC-2020-0056", + "RUSTSEC-2022-0080", + "RUSTSEC-2020-0036", + "RUSTSEC-2021-0139", + "RUSTSEC-2021-0059", + "RUSTSEC-2021-0060", + "RUSTSEC-2022-0090", + "RUSTSEC-2023-0018", ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score # lower than the range specified will be ignored. Note that ignored advisories @@ -183,6 +188,7 @@ deny = [ # The goal is to reduce this list as much as possible skip = [ { name = "aes", version = "*" }, + { name = "adler", version = "*" }, { name = "ahash", version = "*" }, { name = "arrayvec", version = "*" }, { name = "autocfg", version = "*" }, @@ -267,6 +273,8 @@ skip = [ { name = "rustls-pemfile", version = "*" }, { name = "scopeguard", version = "*" }, { name = "sct", version = "*" }, + { name = "secp256k1", version = "*" }, + { name = "secp256k1-sys", version = "*" }, { name = "semver", version = "*" }, { name = "send_wrapper", version = "*" }, { name = "sha2", version = "*" }, @@ -276,6 +284,7 @@ skip = [ { name = "subtle", version = "*" }, { name = "syn", version = "*" }, { name = "time", version = "*" }, + { name = "time-macros", version = "*" }, { name = "tiny-keccak", version = "*" }, { name = "tokio-util", version = "*" }, { name = "trie-root", version = "*" }, @@ -283,6 +292,7 @@ skip = [ { name = "unicode-xid", version = "*" }, { name = "unsigned-varint", version = "*" }, { name = "url", version = "*" }, + { name = "uuid", version = "*" }, { name = "wasi", version = "*" }, { name = "webpki", version = "*" }, { name = "wyz", version = "*" }, diff --git a/dev-logs/2023-feb/enhancements/.gitkeep b/dev-logs/2023-feb/enhancements/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dev-logs/2023-feb/features/.gitkeep b/dev-logs/2023-feb/features/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dev-logs/2023-feb/fixes/.gitkeep b/dev-logs/2023-feb/fixes/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dev-logs/2023-feb/fixes/fix_swap_time_difference_err_message b/dev-logs/2023-feb/fixes/fix_swap_time_difference_err_message new file mode 100644 index 0000000000..91d0075a1a --- /dev/null +++ b/dev-logs/2023-feb/fixes/fix_swap_time_difference_err_message @@ -0,0 +1,4 @@ +Changed the error message for failing swaps due to a time difference between maker and taker to a more informative one + + +author: @shamardy \ No newline at end of file diff --git a/dev-logs/2023-feb/upgrades/.gitkeep b/dev-logs/2023-feb/upgrades/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dev-logs/2023-feb/upgrades/axum_core_upgrade_to_0_2_9 b/dev-logs/2023-feb/upgrades/axum_core_upgrade_to_0_2_9 new file mode 100644 index 0000000000..2dd6415bca --- /dev/null +++ b/dev-logs/2023-feb/upgrades/axum_core_upgrade_to_0_2_9 @@ -0,0 +1,6 @@ +axum-core upgraded to 0.2.9 because current version(0.2.4) considered as vulnerable + +further informations https://rustsec.org/advisories/RUSTSEC-2022-0055.html + + +author: @ozkanonur \ No newline at end of file diff --git a/dev-logs/2023-feb/upgrades/bumpalo_upgrade_to_3_12_0 b/dev-logs/2023-feb/upgrades/bumpalo_upgrade_to_3_12_0 new file mode 100644 index 0000000000..fcb627621b --- /dev/null +++ b/dev-logs/2023-feb/upgrades/bumpalo_upgrade_to_3_12_0 @@ -0,0 +1,6 @@ +bumpalo upgraded to 3.12.0 because current version(3.4.0) considered as vulnerable + +further informations https://rustsec.org/advisories/RUSTSEC-2022-0078.html + + +author: @ozkanonur \ No newline at end of file diff --git a/dev-logs/2023-feb/upgrades/libp2p_upgrade_to_0_45_1 b/dev-logs/2023-feb/upgrades/libp2p_upgrade_to_0_45_1 new file mode 100644 index 0000000000..60933a2681 --- /dev/null +++ b/dev-logs/2023-feb/upgrades/libp2p_upgrade_to_0_45_1 @@ -0,0 +1,6 @@ +libp2p upgraded to 0.45.1 because current version(0.45.0) considered as vulnerable + +further informations https://rustsec.org/advisories/RUSTSEC-2022-0084.html + + +author: @ozkanonur \ No newline at end of file diff --git a/dev-logs/2023-feb/upgrades/tokio_upgrade_to_1_25_0 b/dev-logs/2023-feb/upgrades/tokio_upgrade_to_1_25_0 new file mode 100644 index 0000000000..9aff385837 --- /dev/null +++ b/dev-logs/2023-feb/upgrades/tokio_upgrade_to_1_25_0 @@ -0,0 +1,6 @@ +tokio upgraded to 1.25.0 because current version(1.18.2) considered as vulnerable + +further informations https://rustsec.org/advisories/RUSTSEC-2023-0001.html and https://rustsec.org/advisories/RUSTSEC-2023-0005.html + + +author: @ozkanonur \ No newline at end of file diff --git a/docs/ANDROID.md b/docs/ANDROID.md index e833d70d39..3a665493cf 100644 --- a/docs/ANDROID.md +++ b/docs/ANDROID.md @@ -53,16 +53,16 @@ by setting the NDK_HOME variable. #### armeabi-v7a - cross build --features native --target=armv7-linux-androideabi --release --lib + cross build --target=armv7-linux-androideabi --release --lib #### arm64-v8a - cross build --features native --target=aarch64-linux-android --release --lib + cross build --target=aarch64-linux-android --release --lib #### x86 - cross build --features native --target=i686-linux-android --release --lib + cross build --target=i686-linux-android --release --lib #### x86_64 - cross build --features native --target=x86_64-linux-android --release --lib + cross build --target=x86_64-linux-android --release --lib diff --git a/docs/ANDROID_CROSS_ON_M1_MAC.md b/docs/ANDROID_CROSS_ON_M1_MAC.md index 8ab0f24415..3d9321a49e 100644 --- a/docs/ANDROID_CROSS_ON_M1_MAC.md +++ b/docs/ANDROID_CROSS_ON_M1_MAC.md @@ -18,17 +18,17 @@ rustup target add x86_64-linux-android ``` Build armv7-linux-androideabi target ```shell - CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang AR_armv7_linux_androideabi=llvm-ar CXX_armv7_linux_androideabi=armv7a-linux-androideabi21-clang++ ANDROID_NDK_HOME="/opt/homebrew/share/android-ndk" cargo build --target=armv7-linux-androideabi --release --lib + CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang AR_armv7_linux_androideabi=llvm-ar CXX_armv7_linux_androideabi=armv7a-linux-androideabi21-clang++ ANDROID_NDK_HOME="/opt/homebrew/share/android-ndk" cargo rustc --target=armv7-linux-androideabi --release --lib --crate-type=staticlib --package mm2_bin_lib ``` Build aarch64-linux-android target ```shell - CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang CC_aarch64_linux_android=aarch64-linux-android21-clang AR_aarch64_linux_android=llvm-ar CXX_aarch64_linux_android=aarch64-linux-android21-clang++ ANDROID_NDK_HOME="/opt/homebrew/share/android-ndk" cargo build --target=aarch64-linux-android --release --lib + CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang CC_aarch64_linux_android=aarch64-linux-android21-clang AR_aarch64_linux_android=llvm-ar CXX_aarch64_linux_android=aarch64-linux-android21-clang++ ANDROID_NDK_HOME="/opt/homebrew/share/android-ndk" cargo rustc --target=aarch64-linux-android --release --lib --crate-type=staticlib --package mm2_bin_lib ``` Build i686-linux-android target ```shell - CARGO_TARGET_I686_LINUX_ANDROID_LINKER=i686-linux-android21-clang CC_i686_linux_android=i686-linux-android21-clang AR_i686_linux_android=llvm-ar CXX_i686_linux_android=i686-linux-android21-clang++ ANDROID_NDK_HOME="/opt/homebrew/share/android-ndk" cargo build --target=i686-linux-android --release --lib + CARGO_TARGET_I686_LINUX_ANDROID_LINKER=i686-linux-android21-clang CC_i686_linux_android=i686-linux-android21-clang AR_i686_linux_android=llvm-ar CXX_i686_linux_android=i686-linux-android21-clang++ ANDROID_NDK_HOME="/opt/homebrew/share/android-ndk" cargo rustc --target=i686-linux-android --release --lib --crate-type=staticlib --package mm2_bin_lib ``` Build x86_64-linux-android target ```shell - CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=x86_64-linux-android21-clang CC_x86_64_linux_android=x86_64-linux-android21-clang CXX_x86_64_linux_android=x86_64-linux-android21-clang++ AR_x86_64_linux_android=llvm-ar ANDROID_NDK_HOME="/opt/homebrew/share/android-ndk" cargo build --target=x86_64-linux-android --release --lib + CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=x86_64-linux-android21-clang CC_x86_64_linux_android=x86_64-linux-android21-clang CXX_x86_64_linux_android=x86_64-linux-android21-clang++ AR_x86_64_linux_android=llvm-ar ANDROID_NDK_HOME="/opt/homebrew/share/android-ndk" cargo rustc --target=x86_64-linux-android --release --lib --crate-type=staticlib --package mm2_bin_lib ``` diff --git a/docs/DEV_ENVIRONMENT.md b/docs/DEV_ENVIRONMENT.md index 990a502ea7..3c827d6fdc 100644 --- a/docs/DEV_ENVIRONMENT.md +++ b/docs/DEV_ENVIRONMENT.md @@ -46,7 +46,7 @@ 4. Set environment variables required to run WASM tests ```shell # wasm-bindgen specific variables - export WASM_BINDGEN_TEST_TIMEOUT=120 + export WASM_BINDGEN_TEST_TIMEOUT=180 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" diff --git a/docs/WASM_BUILD.md b/docs/WASM_BUILD.md index c2e5bd7bee..13d568dffc 100644 --- a/docs/WASM_BUILD.md +++ b/docs/WASM_BUILD.md @@ -19,24 +19,24 @@ To build WASM release binary run one of the following commands according to your - for Linux users: ``` - wasm-pack build mm2src/mm2_main --target web --out-dir wasm_build/deps/pkg/ + wasm-pack build mm2src/mm2_bin_lib --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/ + CC=/usr/local/opt/llvm/bin/clang AR=/usr/local/opt/llvm/bin/llvm-ar wasm-pack build mm2src/mm2_bin_lib --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/ + CC=/opt/homebrew/opt/llvm/bin/clang AR=/opt/homebrew/opt/llvm/bin/llvm-ar wasm-pack build mm2src/mm2_bin_lib --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: +If you want to disable optimizations to reduce the compilation time, run `wasm-pack build mm2src/mm2_bin_lib` with an additional `--dev` flag: ``` -wasm-pack build mm2src/mm2_main --target web --out-dir wasm_build/deps/pkg/ --dev +wasm-pack build mm2src/mm2_bin_lib --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/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 8dd4474b98..1d77bb0001 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -5,6 +5,10 @@ edition = "2018" [features] zhtlc-native-tests = [] +# TODO +# Remove this once the solana integration becomes stable/completed. +disable-solana-tests = [] +default = ["disable-solana-tests"] [lib] name = "coins" @@ -17,7 +21,6 @@ async-trait = "0.1.52" base64 = "0.10.0" 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" @@ -26,46 +29,46 @@ bytes = "0.4" cfg-if = "1.0" chain = { path = "../mm2_bitcoin/chain" } common = { path = "../common" } +cosmrs = { version = "0.7", default-features = false } 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" } +enum_from = { path = "../derives/enum_from" } +ethabi = { version = "17.0.0" } +ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } +ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } # Waiting for https://github.com/rust-lang/rust/issues/54725 to use on Stable. #enum_dispatch = "0.1" 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"] } +group = "0.8.0" gstuff = { version = "0.7", features = ["nightly"] } hex = "0.4.2" http = "0.2" itertools = { version = "0.10", features = ["use_std"] } -jsonrpc-core = "8.0.1" +jsonrpc-core = "18.0.0" 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_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } -mocktopus = "0.7.0" +mocktopus = "0.8.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" } +rlp = { version = "0.5" } rmp-serde = "0.14.3" rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } @@ -75,17 +78,20 @@ ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } serde = "1.0" serde_derive = "1.0" -serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } serialization = { path = "../mm2_bitcoin/serialization" } serialization_derive = { path = "../mm2_bitcoin/serialization_derive" } spv_validation = { path = "../mm2_bitcoin/spv_validation" } sha2 = "0.9" sha3 = "0.9" utxo_signer = { path = "utxo_signer" } +# using the same version as cosmrs +tendermint-rpc = { version = "=0.23.7", default-features = false } tiny-bip39 = "0.8.0" +uuid = { version = "0.7", features = ["serde", "v4"] } # 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 } +web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.19.0", default-features = false } zbase32 = "0.1.2" [target.'cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))'.dependencies] @@ -98,6 +104,7 @@ spl-associated-token-account = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = { version = "0.3.27" } mm2_db = { path = "../mm2_db" } +mm2_metamask = { path = "../mm2_metamask" } mm2_test_helpers = { path = "../mm2_test_helpers" } wasm-bindgen = { version = "0.2.50", features = ["nightly"] } wasm-bindgen-futures = { version = "0.4.1" } @@ -106,11 +113,21 @@ web-sys = { version = "0.3.55", features = ["console", "Headers", "Request", "Re [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" } +bitcoin = "0.28.1" +hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } +# using webpki-tokio to avoid rejecting valid certificates +# got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features +hyper-rustls = { version = "0.23", default-features = false, features = ["http1", "http2", "webpki-tokio"] } +lightning = "0.0.110" +lightning-background-processor = "0.0.110" +lightning-invoice = { version = "0.18.0", features = ["serde"] } +lightning-net-tokio = "0.0.110" +lightning-rapid-gossip-sync = "0.0.110" rust-ini = { version = "0.13" } rustls = { version = "0.20", features = ["dangerous_configuration"] } -tokio = { version = "1.7" } +secp256k1v22 = { version = "0.22", package = "secp256k1" } +tendermint-config = { version = "0.23.7", default-features = false } +tokio = { version = "1.20" } tokio-rustls = { version = "0.23" } tonic = { version = "0.7", features = ["tls", "tls-webpki-roots", "compression"] } webpki-roots = { version = "0.22" } diff --git a/mm2src/coins/coin_balance.rs b/mm2src/coins/coin_balance.rs index da1b49eef2..8dca163419 100644 --- a/mm2src/coins/coin_balance.rs +++ b/mm2src/coins/coin_balance.rs @@ -1,25 +1,31 @@ use crate::hd_pubkey::HDXPubExtractor; -use crate::hd_wallet::{HDWalletCoinOps, NewAccountCreatingError}; +use crate::hd_wallet::{HDAccountOps, HDAddressId, HDWalletCoinOps, HDWalletOps, NewAccountCreatingError, + NewAddressDerivingError}; 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 mm2_number::BigDecimal; +#[cfg(test)] use mocktopus::macros::*; +use std::collections::HashMap; use std::ops::Range; +use std::{fmt, iter}; pub type AddressIdRange = Range; -#[derive(Display)] pub enum EnableCoinBalanceError { + NewAddressDerivingError(NewAddressDerivingError), NewAccountCreatingError(NewAccountCreatingError), BalanceError(BalanceError), } +impl From for EnableCoinBalanceError { + fn from(e: NewAddressDerivingError) -> Self { EnableCoinBalanceError::NewAddressDerivingError(e) } +} + impl From for EnableCoinBalanceError { fn from(e: NewAccountCreatingError) -> Self { EnableCoinBalanceError::NewAccountCreatingError(e) } } @@ -30,11 +36,32 @@ impl From for EnableCoinBalanceError { #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(tag = "wallet_type")] -pub enum EnableCoinBalance { +pub enum CoinBalanceReport { Iguana(IguanaWalletBalance), HD(HDWalletBalance), } +impl CoinBalanceReport { + /// Returns a map where the key is address, and the value is the address's total balance [`CoinBalance::total`]. + pub fn to_addresses_total_balances(&self) -> HashMap { + match self { + CoinBalanceReport::Iguana(IguanaWalletBalance { + ref address, + ref balance, + }) => iter::once((address.clone(), balance.get_total())).collect(), + CoinBalanceReport::HD(HDWalletBalance { ref accounts }) => accounts + .iter() + .flat_map(|account_balance| { + account_balance + .addresses + .iter() + .map(|addr_balance| (addr_balance.address.clone(), addr_balance.balance.get_total())) + }) + .collect(), + } + } +} + #[derive(Clone, Debug, PartialEq, Serialize)] pub struct IguanaWalletBalance { pub address: String, @@ -78,15 +105,57 @@ impl Default for EnableCoinScanPolicy { fn default() -> Self { EnableCoinScanPolicy::ScanIfNewWallet } } +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct EnabledCoinBalanceParams { + #[serde(default)] + pub scan_policy: EnableCoinScanPolicy, + pub min_addresses_number: Option, +} + +#[async_trait] +pub trait CoinBalanceReportOps { + async fn coin_balance_report(&self) -> BalanceResult; +} + +#[async_trait] +impl CoinBalanceReportOps for Coin +where + Coin: CoinWithDerivationMethod::HDWallet> + + HDWalletBalanceOps + + MarketCoinOps + + Sync, + ::Address: fmt::Display + Sync, +{ + async fn coin_balance_report(&self) -> BalanceResult { + match self.derivation_method() { + DerivationMethod::SingleAddress(my_address) => self + .my_balance() + .compat() + .await + .map(|balance| { + CoinBalanceReport::Iguana(IguanaWalletBalance { + address: my_address.to_string(), + balance, + }) + }) + .mm_err(BalanceError::from), + DerivationMethod::HDWallet(hd_wallet) => self + .all_accounts_balances(hd_wallet) + .await + .map(|accounts| CoinBalanceReport::HD(HDWalletBalance { accounts })), + } + } +} + #[async_trait] pub trait EnableCoinBalanceOps { async fn enable_coin_balance( &self, xpub_extractor: &XPubExtractor, - scan_policy: EnableCoinScanPolicy, - ) -> MmResult + params: EnabledCoinBalanceParams, + ) -> MmResult where - XPubExtractor: HDXPubExtractor + Sync; + XPubExtractor: HDXPubExtractor; } #[async_trait] @@ -101,27 +170,27 @@ where async fn enable_coin_balance( &self, xpub_extractor: &XPubExtractor, - scan_policy: EnableCoinScanPolicy, - ) -> MmResult + params: EnabledCoinBalanceParams, + ) -> MmResult where - XPubExtractor: HDXPubExtractor + Sync, + XPubExtractor: HDXPubExtractor, { match self.derivation_method() { - DerivationMethod::Iguana(my_address) => self + DerivationMethod::SingleAddress(my_address) => self .my_balance() .compat() .await .map(|balance| { - EnableCoinBalance::Iguana(IguanaWalletBalance { + CoinBalanceReport::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) + .enable_hd_wallet(hd_wallet, xpub_extractor, params) .await - .map(EnableCoinBalance::HD), + .map(CoinBalanceReport::HD), } } } @@ -139,10 +208,10 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { &self, hd_wallet: &Self::HDWallet, xpub_extractor: &XPubExtractor, - scan_policy: EnableCoinScanPolicy, + params: EnabledCoinBalanceParams, ) -> MmResult where - XPubExtractor: HDXPubExtractor + Sync; + XPubExtractor: HDXPubExtractor; /// Scans for the new addresses of the specified `hd_account` using the given `address_scanner`. /// Returns balances of the new addresses. @@ -154,6 +223,30 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { gap_limit: u32, ) -> BalanceResult>; + /// Requests balances of every activated HD account. + async fn all_accounts_balances(&self, hd_wallet: &Self::HDWallet) -> BalanceResult> { + let accounts = hd_wallet.get_accounts().await; + + let mut result = Vec::with_capacity(accounts.len()); + for (_account_id, hd_account) in accounts { + let addresses = self.all_known_addresses_balances(&hd_account).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, + }; + + result.push(account_balance); + } + + Ok(result) + } + /// Requests balances of every known addresses of the given `hd_account`. async fn all_known_addresses_balances(&self, hd_account: &Self::HDAccount) -> BalanceResult>; @@ -168,18 +261,21 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { Self::Address: fmt::Display + Clone, Ids: Iterator + Send, { - let (addresses, der_paths) = address_ids + let address_ids = address_ids.map(|address_id| HDAddressId { chain, address_id }); + + // Derive HD addresses and split addresses and their derivation paths into two collections. + let (addresses, der_paths): (Vec<_>, Vec<_>) = self + .derive_addresses(hd_account, address_ids) + .await? .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<_>>()?; + .map( + |HDAddress { + address, + derivation_path, + .. + }| (address, derivation_path), + ) + .unzip(); let balances = self .known_addresses_balances(addresses) @@ -228,6 +324,7 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { } #[async_trait] +#[cfg_attr(test, mockable)] pub trait HDAddressBalanceScanner: Sync { type Address; @@ -249,9 +346,11 @@ pub mod common_impl { hd_account: &mut Coin::HDAccount, address_scanner: &Coin::HDAddressScanner, scan_new_addresses: bool, + min_addresses_number: Option, ) -> MmResult where - Coin: HDWalletBalanceOps + Sync, + Coin: HDWalletBalanceOps + MarketCoinOps + Sync, + Coin::Address: fmt::Display, { let gap_limit = hd_wallet.gap_limit(); let mut addresses = coin.all_known_addresses_balances(hd_account).await?; @@ -262,6 +361,10 @@ pub mod common_impl { ); } + if let Some(min_addresses_number) = min_addresses_number { + gen_new_addresses(coin, hd_wallet, hd_account, &mut addresses, min_addresses_number).await? + } + let total_balance = addresses.iter().fold(CoinBalance::default(), |total, addr_balance| { total + addr_balance.balance.clone() }); @@ -279,11 +382,12 @@ pub mod common_impl { coin: &Coin, hd_wallet: &Coin::HDWallet, xpub_extractor: &XPubExtractor, - scan_policy: EnableCoinScanPolicy, + params: EnabledCoinBalanceParams, ) -> MmResult where Coin: HDWalletBalanceOps + MarketCoinOps + Sync, - XPubExtractor: HDXPubExtractor + Sync, + Coin::Address: fmt::Display, + XPubExtractor: HDXPubExtractor, { let mut accounts = hd_wallet.get_accounts_mut().await; let address_scanner = coin.produce_hd_address_scanner().await?; @@ -303,12 +407,19 @@ pub mod common_impl { // Create new HD account. let mut new_account = coin.create_new_account(hd_wallet, xpub_extractor).await?; let scan_new_addresses = matches!( - scan_policy, + params.scan_policy, EnableCoinScanPolicy::ScanIfNewWallet | EnableCoinScanPolicy::Scan ); - let account_balance = - enable_hd_account(coin, hd_wallet, &mut new_account, &address_scanner, scan_new_addresses).await?; + let account_balance = enable_hd_account( + coin, + hd_wallet, + &mut new_account, + &address_scanner, + scan_new_addresses, + params.min_addresses_number, + ) + .await?; result.accounts.push(account_balance); return Ok(result); } @@ -318,13 +429,85 @@ pub mod common_impl { accounts.len(), coin.ticker() ); - let scan_new_addresses = matches!(scan_policy, EnableCoinScanPolicy::Scan); + let scan_new_addresses = matches!(params.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?; + let account_balance = enable_hd_account( + coin, + hd_wallet, + hd_account, + &address_scanner, + scan_new_addresses, + params.min_addresses_number, + ) + .await?; result.accounts.push(account_balance); } Ok(result) } + + /// Generate new address so that the total number of `result_addresses` addresses is at least `min_addresses_number`. + async fn gen_new_addresses( + coin: &Coin, + hd_wallet: &Coin::HDWallet, + hd_account: &mut Coin::HDAccount, + result_addresses: &mut Vec, + min_addresses_number: u32, + ) -> MmResult<(), EnableCoinBalanceError> + where + Coin: HDWalletBalanceOps + MarketCoinOps + Sync, + Coin::Address: fmt::Display, + { + let max_addresses_number = hd_wallet.address_limit(); + if min_addresses_number >= max_addresses_number { + return MmError::err(EnableCoinBalanceError::NewAddressDerivingError( + NewAddressDerivingError::AddressLimitReached { max_addresses_number }, + )); + } + + let min_addresses_number = min_addresses_number as usize; + let actual_addresses_number = result_addresses.len(); + if actual_addresses_number >= min_addresses_number { + // There are more or equal to the `min_addresses_number` known addresses already. + return Ok(()); + } + + let to_generate = min_addresses_number - actual_addresses_number; + let chain = hd_wallet.default_receiver_chain(); + let ticker = coin.ticker(); + let account_id = hd_account.account_id(); + info!("Generate '{to_generate}' addresses: ticker={ticker} account_id={account_id}, chain={chain:?}"); + + let mut new_addresses = Vec::with_capacity(to_generate); + let mut addresses_to_request = Vec::with_capacity(to_generate); + for _ in 0..to_generate { + let HDAddress { + address, + derivation_path, + .. + } = coin.generate_new_address(hd_wallet, hd_account, chain).await?; + + new_addresses.push(HDAddressBalance { + address: address.to_string(), + derivation_path: RpcDerivationPath(derivation_path), + chain, + balance: CoinBalance::default(), + }); + addresses_to_request.push(address); + } + + let to_extend = coin + .known_addresses_balances(addresses_to_request) + .await? + .into_iter() + // The balances are guaranteed to be in the same order as they were requests. + .zip(new_addresses) + .map(|((_address, balance), mut address_info)| { + address_info.balance = balance; + address_info + }); + + result_addresses.extend(to_extend); + Ok(()) + } } diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs new file mode 100644 index 0000000000..a296f68adb --- /dev/null +++ b/mm2src/coins/coin_errors.rs @@ -0,0 +1,96 @@ +use crate::{eth::Web3RpcError, my_tx_history_v2::MyTxHistoryErrorV2, utxo::rpc_clients::UtxoRpcError, DelegationError, + NumConversError, TxHistoryError, UnexpectedDerivationMethod, WithdrawError}; +use futures01::Future; +use mm2_err_handle::prelude::MmError; +use spv_validation::helpers_validation::SPVError; + +pub type ValidatePaymentFut = Box> + Send>; + +#[derive(Debug, Display)] +pub enum ValidatePaymentError { + InternalError(String), + // Problem with deserializing the transaction, or one of the transaction parts is invalid. + TxDeserializationError(String), + InvalidParameter(String), + InvalidRpcResponse(String), + TxDoesNotExist(String), + SPVError(SPVError), + UnexpectedPaymentState(String), + Transport(String), + // Transaction has wrong properties, for example, it has been sent to a wrong address + WrongPaymentTx(String), +} + +impl From for ValidatePaymentError { + fn from(err: rlp::DecoderError) -> Self { Self::TxDeserializationError(err.to_string()) } +} + +impl From for ValidatePaymentError { + fn from(err: web3::Error) -> Self { Self::Transport(err.to_string()) } +} + +impl From for ValidatePaymentError { + fn from(err: NumConversError) -> Self { Self::InternalError(err.to_string()) } +} + +impl From for ValidatePaymentError { + fn from(err: SPVError) -> Self { Self::SPVError(err) } +} + +impl From for ValidatePaymentError { + fn from(err: serialization::Error) -> Self { Self::TxDeserializationError(err.to_string()) } +} + +impl From for ValidatePaymentError { + fn from(err: UnexpectedDerivationMethod) -> Self { Self::InternalError(err.to_string()) } +} + +impl From for ValidatePaymentError { + fn from(err: UtxoRpcError) -> Self { + match err { + UtxoRpcError::Transport(e) => Self::Transport(e.to_string()), + UtxoRpcError::Internal(e) => Self::InternalError(e), + _ => Self::InvalidRpcResponse(err.to_string()), + } + } +} + +impl From for ValidatePaymentError { + fn from(e: Web3RpcError) -> Self { + match e { + Web3RpcError::Transport(tr) => ValidatePaymentError::Transport(tr), + Web3RpcError::InvalidResponse(resp) => ValidatePaymentError::InvalidRpcResponse(resp), + Web3RpcError::Internal(internal) => ValidatePaymentError::InternalError(internal), + } + } +} + +#[derive(Debug, Display)] +pub enum MyAddressError { + UnexpectedDerivationMethod(String), + InternalError(String), +} + +impl From for MyAddressError { + fn from(err: UnexpectedDerivationMethod) -> Self { Self::UnexpectedDerivationMethod(err.to_string()) } +} + +impl From for WithdrawError { + fn from(err: MyAddressError) -> Self { Self::InternalError(err.to_string()) } +} + +impl From for UtxoRpcError { + fn from(err: MyAddressError) -> Self { Self::Internal(err.to_string()) } +} + +impl From for DelegationError { + fn from(err: MyAddressError) -> Self { Self::InternalError(err.to_string()) } +} + +impl From for TxHistoryError { + fn from(err: MyAddressError) -> Self { Self::InternalError(err.to_string()) } +} + +impl From for MyTxHistoryErrorV2 { + fn from(err: MyAddressError) -> Self { Self::Internal(err.to_string()) } +} diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 1a2f6dc78e..6d7ecc942d 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -20,26 +20,31 @@ // // Copyright © 2022 AtomicDEX. All rights reserved. // +use super::eth::Action::{Call, Create}; use async_trait::async_trait; -use bitcrypto::{keccak256, sha256}; -use common::executor::Timer; -use common::log::{error, info, warn}; -use common::{now_ms, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; +use bitcrypto::{keccak256, ripemd160, sha256}; +use common::custom_futures::repeatable::{Ready, Retry}; +use common::custom_futures::timeout::FutureTimerExt; +use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError, Timer}; +use common::log::{debug, error, info, warn}; +use common::number_type_casting::SafeTypeCastingNumbers; +use common::{get_utc_timestamp, now_ms, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::privkey::key_pair_from_secret; +use crypto::{CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; use derive_more::Display; -use ethabi::{Contract, Token}; +use ethabi::{Contract, Function, Token}; pub use ethcore_transaction::SignedTransaction as SignedEthTx; use ethcore_transaction::{Action, Transaction as UnSignedEthTx, UnverifiedTransaction}; 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 futures::future::{join_all, 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_net::transport::{slurp_url, GuiAuthValidation, GuiAuthValidationGenerator, SlurpError}; use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; use rand::seq::SliceRandom; @@ -49,40 +54,61 @@ use serde_json::{self as json, Value as Json}; use serialization::{CompactInteger, Serializable, Stream}; use sha3::{Digest, Keccak256}; use std::collections::HashMap; +use std::convert::TryFrom; use std::ops::Deref; -use std::path::PathBuf; +#[cfg(not(target_arch = "wasm32"))] use std::path::PathBuf; use std::str::FromStr; 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}; + TraceFilterBuilder, Transaction as Web3Transaction, TransactionId, U64}; use web3::{self, Web3}; -use web3_transport::{EthFeeHistoryNamespace, Web3Transport}; - -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}; +use web3_transport::{http_transport::HttpTransportNode, EthFeeHistoryNamespace, Web3Transport}; +cfg_wasm32! { + use crypto::MetamaskArc; + use ethereum_types::{H264, H520}; + use mm2_metamask::MetamaskError; + use web3::types::TransactionRequest; +} + +use super::{coin_conf, AsyncMutex, BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, + CoinProtocol, CoinTransportMetrics, CoinsContext, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, + IguanaPrivKey, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MyAddressError, NegotiateSwapContractAddrErr, + NumConversError, NumConversResult, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, + RawTransactionResult, RefundError, RefundResult, RpcClientType, RpcTransportEventHandler, + RpcTransportEventHandlerShared, SearchForSwapTxSpendInput, SendMakerPaymentArgs, + SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, + SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, + SendWatcherRefundsPaymentArgs, SignatureError, SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, + TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationError, + VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult, + EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_RECEIVER_ERR_LOG, + INVALID_SENDER_ERR_LOG}; pub use rlp; #[cfg(test)] mod eth_tests; #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; mod web3_transport; +#[path = "eth/v2_activation.rs"] pub mod v2_activation; +use v2_activation::build_address_and_priv_key_policy; + +mod nonce; +use nonce::ParityNonce; + /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol /// Dev chain (195.201.0.6:8565) contract address: 0xa09ad3cd7e96586ebd05a2607ee56b56fb2db8fd /// Ropsten: https://ropsten.etherscan.io/address/0x7bc1bbdd6a0a722fc9bffc49c921b685ecb84b94 /// ETH mainnet: https://etherscan.io/address/0x8500AFc0bc5214728082163326C2FF0C73f4a871 -const SWAP_CONTRACT_ABI: &str = r#"[{"constant":false,"inputs":[{"name":"_id","type":"bytes32"},{"name":"_amount","type":"uint256"},{"name":"_secret","type":"bytes32"},{"name":"_tokenAddress","type":"address"},{"name":"_sender","type":"address"}],"name":"receiverSpend","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"payments","outputs":[{"name":"paymentHash","type":"bytes20"},{"name":"lockTime","type":"uint64"},{"name":"state","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"bytes32"},{"name":"_receiver","type":"address"},{"name":"_secretHash","type":"bytes20"},{"name":"_lockTime","type":"uint64"}],"name":"ethPayment","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"bytes32"},{"name":"_amount","type":"uint256"},{"name":"_paymentHash","type":"bytes20"},{"name":"_tokenAddress","type":"address"},{"name":"_receiver","type":"address"}],"name":"senderRefund","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"bytes32"},{"name":"_amount","type":"uint256"},{"name":"_tokenAddress","type":"address"},{"name":"_receiver","type":"address"},{"name":"_secretHash","type":"bytes20"},{"name":"_lockTime","type":"uint64"}],"name":"erc20Payment","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"bytes32"}],"name":"PaymentSent","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"bytes32"},{"indexed":false,"name":"secret","type":"bytes32"}],"name":"ReceiverSpent","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"bytes32"}],"name":"SenderRefunded","type":"event"}]"#; +const SWAP_CONTRACT_ABI: &str = include_str!("eth/swap_contract_abi.json"); /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md -const ERC20_ABI: &str = r#"[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_subtractedValue","type":"uint256"}],"name":"decreaseApproval","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_addedValue","type":"uint256"}],"name":"increaseApproval","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]"#; - +const ERC20_ABI: &str = include_str!("eth/erc20_abi.json"); /// Payment states from etomic swap smart contract: https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol#L5 pub const PAYMENT_STATE_UNINITIALIZED: u8 = 0; pub const PAYMENT_STATE_SENT: u8 = 1; @@ -95,8 +121,14 @@ const GAS_PRICE_PERCENT: u64 = 10; const BASE_BLOCK_FEE_DIFF_PCT: u64 = 13; const DEFAULT_LOGS_BLOCK_RANGE: u64 = 1000; +const DEFAULT_REQUIRED_CONFIRMATIONS: u8 = 1; + +const ETH_DECIMALS: u8 = 18; + /// Take into account that the dynamic fee may increase by 3% during the swap. const GAS_PRICE_APPROXIMATION_PERCENT_ON_START_SWAP: u64 = 3; +/// Take into account that the dynamic fee may increase until the locktime is expired +const GAS_PRICE_APPROXIMATION_PERCENT_ON_WATCHER_PREIMAGE: u64 = 3; /// Take into account that the dynamic fee may increase at each of the following stages: /// - it may increase by 2% until a swap is started; /// - it may increase by 3% during the swap. @@ -107,6 +139,11 @@ const GAS_PRICE_APPROXIMATION_PERCENT_ON_ORDER_ISSUE: u64 = 5; /// - it may increase by 3% during the swap. const GAS_PRICE_APPROXIMATION_PERCENT_ON_TRADE_PREIMAGE: u64 = 7; +const ETH_GAS: u64 = 150_000; + +/// Lifetime of generated signed message for gui-auth requests +const GUI_AUTH_SIGNED_MESSAGE_LIFETIME_SEC: i64 = 90; + lazy_static! { pub static ref SWAP_CONTRACT: Contract = Contract::load(SWAP_CONTRACT_ABI.as_bytes()).unwrap(); pub static ref ERC20_CONTRACT: Contract = Contract::load(ERC20_ABI.as_bytes()).unwrap(); @@ -172,12 +209,13 @@ impl From for Web3RpcError { impl From for Web3RpcError { fn from(e: web3::Error) -> Self { let error_str = e.to_string(); - match e.kind() { - web3::ErrorKind::InvalidResponse(_) - | web3::ErrorKind::Decoder(_) - | web3::ErrorKind::Msg(_) - | web3::ErrorKind::Rpc(_) => Web3RpcError::InvalidResponse(error_str), - web3::ErrorKind::Transport(_) | web3::ErrorKind::Io(_) => Web3RpcError::Transport(error_str), + match e { + web3::Error::InvalidResponse(_) | web3::Error::Decoder(_) | web3::Error::Rpc(_) => { + Web3RpcError::InvalidResponse(error_str) + }, + web3::Error::Unreachable | web3::Error::Transport(_) | web3::Error::Io(_) => { + Web3RpcError::Transport(error_str) + }, _ => Web3RpcError::Internal(error_str), } } @@ -187,6 +225,15 @@ impl From for RawTransactionError { fn from(e: web3::Error) -> Self { RawTransactionError::Transport(e.to_string()) } } +impl From for RawTransactionError { + fn from(e: Web3RpcError) -> Self { + match e { + Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => RawTransactionError::Transport(tr), + Web3RpcError::Internal(internal) => RawTransactionError::InternalError(internal), + } + } +} + 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. @@ -195,6 +242,16 @@ impl From for Web3RpcError { } } +#[cfg(target_arch = "wasm32")] +impl From for Web3RpcError { + fn from(e: MetamaskError) -> Self { + match e { + MetamaskError::Internal(internal) => Web3RpcError::Internal(internal), + other => Web3RpcError::Transport(other.to_string()), + } + } +} + impl From for WithdrawError { fn from(e: ethabi::Error) -> Self { // Currently, we use the `ethabi` crate to work with a smart contract ABI known at compile time. @@ -246,7 +303,16 @@ impl From for BalanceError { } impl From for BalanceError { - fn from(e: web3::Error) -> Self { BalanceError::Transport(e.to_string()) } + fn from(e: web3::Error) -> Self { BalanceError::from(Web3RpcError::from(e)) } +} + +impl From for BalanceError { + fn from(e: Web3RpcError) -> Self { + match e { + Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => BalanceError::Transport(tr), + Web3RpcError::Internal(internal) => BalanceError::Internal(internal), + } + } } #[derive(Debug, Deserialize, Serialize)] @@ -254,9 +320,9 @@ struct SavedTraces { /// ETH traces for my_address traces: Vec, /// Earliest processed block - earliest_block: U256, + earliest_block: U64, /// Latest processed block - latest_block: U256, + latest_block: U64, } #[derive(Debug, Deserialize, Serialize)] @@ -264,13 +330,13 @@ struct SavedErc20Events { /// ERC20 events for my_address events: Vec, /// Earliest processed block - earliest_block: U256, + earliest_block: U64, /// Latest processed block - latest_block: U256, + latest_block: U64, } #[derive(Debug, PartialEq, Eq)] -enum EthCoinType { +pub enum EthCoinType { /// Ethereum itself or it's forks: ETC/others Eth, /// ERC20 token with smart contract address @@ -278,12 +344,78 @@ enum EthCoinType { Erc20 { platform: String, token_addr: Address }, } +/// An alternative to `crate::PrivKeyBuildPolicy`, typical only for ETH coin. +pub enum EthPrivKeyBuildPolicy { + IguanaPrivKey(IguanaPrivKey), + GlobalHDAccount(GlobalHDAccountArc), + #[cfg(target_arch = "wasm32")] + Metamask(MetamaskArc), +} + +impl EthPrivKeyBuildPolicy { + /// Detects the `EthPrivKeyBuildPolicy` with which the given `MmArc` is initialized. + pub fn detect_priv_key_policy(ctx: &MmArc) -> MmResult { + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + + match crypto_ctx.key_pair_policy() { + KeyPairPolicy::Iguana => { + // Use an internal private key as the coin secret. + let priv_key = crypto_ctx.mm2_internal_privkey_secret(); + Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(priv_key)) + }, + KeyPairPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd.clone())), + } + } +} + +impl TryFrom for EthPrivKeyBuildPolicy { + type Error = PrivKeyPolicyNotAllowed; + + /// Converts `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` + /// taking into account that ETH doesn't support `Trezor` yet. + fn try_from(policy: PrivKeyBuildPolicy) -> Result { + match policy { + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(iguana)), + PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd)), + PrivKeyBuildPolicy::Trezor => Err(PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), + } + } +} + +/// An alternative to `crate::PrivKeyPolicy`, typical only for ETH coin. +#[derive(Clone)] +pub enum EthPrivKeyPolicy { + KeyPair(KeyPair), + #[cfg(target_arch = "wasm32")] + Metamask(EthMetamaskPolicy), +} + +#[cfg(target_arch = "wasm32")] +#[derive(Clone)] +pub struct EthMetamaskPolicy { + pub(crate) public_key: H264, + pub(crate) public_key_uncompressed: H520, +} + +impl From for EthPrivKeyPolicy { + fn from(key_pair: KeyPair) -> Self { EthPrivKeyPolicy::KeyPair(key_pair) } +} + +impl EthPrivKeyPolicy { + pub fn key_pair_or_err(&self) -> MmResult<&KeyPair, PrivKeyPolicyNotAllowed> { + match self { + EthPrivKeyPolicy::KeyPair(key_pair) => Ok(key_pair), + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(_) => MmError::err(PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), + } + } +} + /// pImpl idiom. -#[derive(Debug)] pub struct EthCoinImpl { ticker: String, coin_type: EthCoinType, - key_pair: KeyPair, + priv_key_policy: EthPrivKeyPolicy, my_address: Address, sign_message_prefix: Option, swap_contract_address: Address, @@ -299,11 +431,15 @@ pub struct EthCoinImpl { required_confirmations: AtomicU64, /// Coin needs access to the context in order to reuse the logging and shutdown facilities. /// Using a weak reference by default in order to avoid circular references and leaks. - ctx: MmWeak, + pub ctx: MmWeak, chain_id: Option, /// the block range used for eth_getLogs logs_block_range: u64, nonce_lock: Arc>, + erc20_tokens_infos: Arc>>, + /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation + /// and on [`MmArc::stop`]. + pub abortable_system: AbortableQueue, } #[derive(Clone, Debug)] @@ -312,6 +448,12 @@ pub struct Web3Instance { is_parity: bool, } +#[derive(Clone, Debug)] +pub struct Erc20TokenInfo { + pub token_address: Address, + pub decimals: u8, +} + #[derive(Deserialize, Serialize)] #[serde(tag = "format")] pub enum EthAddressFormat { @@ -338,7 +480,6 @@ async fn make_gas_station_request(url: &str) -> GasStationResult { Ok(result) } -#[cfg_attr(test, mockable)] impl EthCoinImpl { /// Gets Transfer events from ERC20 smart contract `addr` between `from_block` and `to_block` fn erc20_transfer_events( @@ -364,7 +505,13 @@ impl EthCoinImpl { filter = filter.limit(l); } - Box::new(self.web3.eth().logs(filter.build()).map_err(|e| ERRL!("{}", e))) + Box::new( + self.web3 + .eth() + .logs(filter.build()) + .compat() + .map_err(|e| ERRL!("{}", e)), + ) } /// Gets ETH traces from ETH node between addresses in `from_block` and `to_block` @@ -386,10 +533,16 @@ impl EthCoinImpl { filter = filter.count(l); } - Box::new(self.web3.trace().filter(filter.build()).map_err(|e| ERRL!("{}", e))) + Box::new( + self.web3 + .trace() + .filter(filter.build()) + .compat() + .map_err(|e| ERRL!("{}", e)), + ) } - #[cfg_attr(target_arch = "wasm32", allow(dead_code))] + #[cfg(not(target_arch = "wasm32"))] fn eth_traces_path(&self, ctx: &MmArc) -> PathBuf { ctx.dbdir() .join("TRANSACTIONS") @@ -433,7 +586,7 @@ impl EthCoinImpl { unreachable!() } - #[cfg_attr(target_arch = "wasm32", allow(dead_code))] + #[cfg(not(target_arch = "wasm32"))] fn erc20_events_path(&self, ctx: &MmArc) -> PathBuf { ctx.dbdir() .join("TRANSACTIONS") @@ -485,29 +638,6 @@ impl EthCoinImpl { sha256(&input).to_vec() } - fn estimate_gas(&self, req: CallRequest) -> Box + Send> { - // always using None block number as old Geth version accept only single argument in this RPC - Box::new(self.web3.eth().estimate_gas(req, None)) - } - - /// Gets `ReceiverSpent` events from etomic swap smart contract since `from_block` - fn spend_events( - &self, - swap_contract_address: Address, - from_block: u64, - to_block: u64, - ) -> Box, Error = String> + Send> { - let contract_event = try_fus!(SWAP_CONTRACT.event("ReceiverSpent")); - let filter = FilterBuilder::default() - .topics(Some(vec![contract_event.signature()]), None, None, None) - .from_block(BlockNumber::Number(from_block)) - .to_block(BlockNumber::Number(to_block)) - .address(vec![swap_contract_address]) - .build(); - - Box::new(self.web3.eth().logs(filter).map_err(|e| ERRL!("{}", e))) - } - /// Gets `SenderRefunded` events from etomic swap smart contract since `from_block` fn refund_events( &self, @@ -518,18 +648,37 @@ impl EthCoinImpl { let contract_event = try_fus!(SWAP_CONTRACT.event("SenderRefunded")); let filter = FilterBuilder::default() .topics(Some(vec![contract_event.signature()]), None, None, None) - .from_block(BlockNumber::Number(from_block)) - .to_block(BlockNumber::Number(to_block)) + .from_block(BlockNumber::Number(from_block.into())) + .to_block(BlockNumber::Number(to_block.into())) .address(vec![swap_contract_address]) .build(); - Box::new(self.web3.eth().logs(filter).map_err(|e| ERRL!("{}", e))) + Box::new(self.web3.eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) } /// Try to parse address from string. pub fn address_from_str(&self, address: &str) -> Result { Ok(try_s!(valid_addr_from_str(address))) } + + pub fn erc20_token_address(&self) -> Option
{ + match self.coin_type { + EthCoinType::Erc20 { token_addr, .. } => Some(token_addr), + EthCoinType::Eth => None, + } + } + + pub fn add_erc_token_info(&self, ticker: String, info: Erc20TokenInfo) { + self.erc20_tokens_infos.lock().unwrap().insert(ticker, info); + } + + /// # Warning + /// Be very careful using this function since it returns dereferenced clone + /// of value behind the MutexGuard and makes it non-thread-safe. + pub fn get_erc_tokens_infos(&self) -> HashMap { + let guard = self.erc20_tokens_infos.lock().unwrap(); + (*guard).clone() + } } async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> RawTransactionResult { @@ -538,11 +687,19 @@ async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> 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))?; + get_tx_hex_by_hash_impl(coin, hash).await +} + +async fn get_tx_hex_by_hash_impl(coin: EthCoin, tx_hash: H256) -> RawTransactionResult { + let web3_tx = coin + .web3 + .eth() + .transaction(TransactionId::Hash(tx_hash)) + .await? + .or_mm_err(|| RawTransactionError::HashNotExist(tx_hash.to_string()))?; let raw = signed_tx_from_web3_tx(web3_tx).map_to_mm(RawTransactionError::InternalError)?; Ok(RawTransactionRes { - tx_hex: BytesJson(rlp::encode(&raw)), + tx_hex: BytesJson(rlp::encode(&raw).to_vec()), }) } @@ -597,11 +754,12 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { value: Some(eth_value_for_estimate), data: Some(data.clone().into()), from: Some(coin.my_address), - to: call_addr, + to: Some(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), + ..CallRequest::default() }; // TODO Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. // TODO Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. @@ -622,23 +780,70 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { eth_value -= total_fee; wei_amount -= total_fee; }; - 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)?, - Either::Right(_) => return MmError::err(WithdrawError::Transport("Get address nonce timed out".to_owned())), - }; - let tx = UnSignedEthTx { - nonce, - value: eth_value, - action: Action::Call(call_addr), - data, - gas, - gas_price, + + let (tx_hash, tx_hex) = match coin.priv_key_policy { + EthPrivKeyPolicy::KeyPair(ref key_pair) => { + let _nonce_lock = coin.nonce_lock.lock().await; + let nonce = get_addr_nonce(coin.my_address, coin.web3_instances.clone()) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; + + let tx = UnSignedEthTx { + nonce, + value: eth_value, + action: Action::Call(call_addr), + data, + gas, + gas_price, + }; + + let signed = tx.sign(key_pair.secret(), coin.chain_id); + let bytes = rlp::encode(&signed); + + (signed.hash, BytesJson::from(bytes.to_vec())) + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(_) => { + if !req.broadcast { + let error = "Set 'broadcast' to generate, sign and broadcast a transaction with MetaMask".to_string(); + return MmError::err(WithdrawError::BroadcastExpected(error)); + } + + let tx_to_send = TransactionRequest { + from: coin.my_address, + to: Some(to_addr), + gas: Some(gas), + gas_price: Some(gas_price), + value: Some(eth_value), + data: Some(data.clone().into()), + nonce: None, + ..TransactionRequest::default() + }; + + // Wait for 10 seconds for the transaction to appear on the RPC node. + let wait_rpc_timeout = 10_000; + let check_every = 1.; + + // Please note that this method may take a long time + // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. + let tx_hash = coin.web3.eth().send_transaction(tx_to_send).await?; + + let signed_tx = coin + .wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) + .await?; + let tx_hex = signed_tx + .map(|tx| BytesJson::from(rlp::encode(&tx).to_vec())) + // Return an empty `tx_hex` if the transaction is still not appeared on the RPC node. + .unwrap_or_default(); + (tx_hash, tx_hex) + }, }; - let signed = tx.sign(coin.key_pair.secret(), coin.chain_id); - let bytes = rlp::encode(&signed); + let tx_hash_bytes = BytesJson::from(tx_hash.0.to_vec()); + let tx_hash_str = format!("{:02x}", tx_hash_bytes); + let amount_decimal = u256_to_big_decimal(wei_amount, coin.decimals)?; let mut spent_by_me = amount_decimal.clone(); let received_by_me = if to_addr == coin.my_address { @@ -650,7 +855,7 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { if coin.coin_type == EthCoinType::Eth { spent_by_me += &fee_details.total_fee; } - let my_address = coin.my_address().map_to_mm(WithdrawError::InternalError)?; + let my_address = coin.my_address()?; Ok(TransactionDetails { to: vec![checksum_address(&format!("{:#02x}", to_addr))], from: vec![my_address], @@ -658,8 +863,8 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { my_balance_change: &received_by_me - &spent_by_me, spent_by_me, received_by_me, - tx_hex: bytes.into(), - tx_hash: format!("{:02x}", signed.tx_hash()), + tx_hex, + tx_hash: tx_hash_str, block_height: 0, fee_details: Some(fee_details.into()), coin: coin.ticker.clone(), @@ -667,14 +872,15 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { timestamp: now_ms() / 1000, kmd_rewards: None, transaction_type: Default::default(), + memo: None, }) } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct EthCoin(Arc); impl Deref for EthCoin { type Target = EthCoinImpl; - fn deref(&self) -> &EthCoinImpl { &*self.0 } + fn deref(&self) -> &EthCoinImpl { &self.0 } } #[async_trait] @@ -688,24 +894,16 @@ impl SwapOps for EthCoin { ) } - 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 { - 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()); + fn send_maker_payment(&self, maker_payment: SendMakerPaymentArgs) -> TransactionFut { + let taker_addr = try_tx_fus!(addr_from_raw_pubkey(maker_payment.other_pubkey)); + let swap_contract_address = try_tx_fus!(maker_payment.swap_contract_address.try_to_address()); Box::new( self.send_hash_time_locked_payment( - self.etomic_swap_id(time_lock, secret_hash), - try_tx_fus!(wei_from_big_decimal(&amount, self.decimals)), - time_lock, - secret_hash, + self.etomic_swap_id(maker_payment.time_lock, maker_payment.secret_hash), + try_tx_fus!(wei_from_big_decimal(&maker_payment.amount, self.decimals)), + maker_payment.time_lock, + maker_payment.secret_hash, taker_addr, swap_contract_address, ) @@ -713,24 +911,16 @@ impl SwapOps for EthCoin { ) } - 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 { - 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()); + fn send_taker_payment(&self, taker_payment: SendTakerPaymentArgs) -> TransactionFut { + let maker_addr = try_tx_fus!(addr_from_raw_pubkey(taker_payment.other_pubkey)); + let swap_contract_address = try_tx_fus!(taker_payment.swap_contract_address.try_to_address()); Box::new( self.send_hash_time_locked_payment( - self.etomic_swap_id(time_lock, secret_hash), - try_tx_fus!(wei_from_big_decimal(&amount, self.decimals)), - time_lock, - secret_hash, + self.etomic_swap_id(taker_payment.time_lock, taker_payment.secret_hash), + try_tx_fus!(wei_from_big_decimal(&taker_payment.amount, self.decimals)), + taker_payment.time_lock, + taker_payment.secret_hash, maker_addr, swap_contract_address, ) @@ -740,113 +930,87 @@ impl SwapOps for EthCoin { 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], + maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, ) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_payment_tx)); + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_spends_payment_args.other_payment_tx)); let signed = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address(), signed); + let swap_contract_address = + try_tx_fus!(maker_spends_payment_args.swap_contract_address.try_to_address(), signed); Box::new( - self.spend_hash_time_locked_payment(signed, swap_contract_address, secret) - .map(TransactionEnum::from), + self.spend_hash_time_locked_payment( + signed, + maker_spends_payment_args.secret_hash, + swap_contract_address, + maker_spends_payment_args.secret, + ) + .map(TransactionEnum::from), ) } 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], + taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, ) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_payment_tx)); + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_spends_payment_args.other_payment_tx)); let signed = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); + let swap_contract_address = try_tx_fus!(taker_spends_payment_args.swap_contract_address.try_to_address()); Box::new( - self.spend_hash_time_locked_payment(signed, swap_contract_address, secret) - .map(TransactionEnum::from), + self.spend_hash_time_locked_payment( + signed, + taker_spends_payment_args.secret_hash, + swap_contract_address, + taker_spends_payment_args.secret, + ) + .map(TransactionEnum::from), ) } - 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 { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_payment_tx)); + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_refunds_payment_args.payment_tx)); let signed = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); + let swap_contract_address = try_tx_fus!(taker_refunds_payment_args.swap_contract_address.try_to_address()); Box::new( - self.refund_hash_time_locked_payment(swap_contract_address, signed) + self.refund_hash_time_locked_payment(swap_contract_address, signed, taker_refunds_payment_args.secret_hash) .map(TransactionEnum::from), ) } - 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 { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_payment_tx)); + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_refunds_payment_args.payment_tx)); let signed = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); + let swap_contract_address = try_tx_fus!(maker_refunds_payment_args.swap_contract_address.try_to_address()); Box::new( - self.refund_hash_time_locked_payment(swap_contract_address, signed) + self.refund_hash_time_locked_payment(swap_contract_address, signed, maker_refunds_payment_args.secret_hash) .map(TransactionEnum::from), ) } fn validate_fee( &self, - fee_tx: &TransactionEnum, - expected_sender: &[u8], - fee_addr: &[u8], - amount: &BigDecimal, - min_block_number: u64, - _uuid: &[u8], + validate_fee_args: ValidateFeeArgs<'_>, ) -> Box + Send> { let selfi = self.clone(); - let tx = match fee_tx { + let tx = match validate_fee_args.fee_tx { TransactionEnum::SignedEthTx(t) => t.clone(), _ => panic!(), }; - let sender_addr = try_fus!(addr_from_raw_pubkey(expected_sender)); - let fee_addr = try_fus!(addr_from_raw_pubkey(fee_addr)); - let amount = amount.clone(); + let sender_addr = try_fus!(addr_from_raw_pubkey(validate_fee_args.expected_sender)); + let fee_addr = try_fus!(addr_from_raw_pubkey(validate_fee_args.fee_addr)); + let amount = validate_fee_args.amount.clone(); + let min_block_number = validate_fee_args.min_block_number; let fut = async move { let expected_value = try_s!(wei_from_big_decimal(&amount, selfi.decimals)); - let tx_from_rpc = try_s!( - selfi - .web3 - .eth() - .transaction(TransactionId::Hash(tx.hash)) - .compat() - .await - ); + let tx_from_rpc = try_s!(selfi.web3.eth().transaction(TransactionId::Hash(tx.hash)).await); let tx_from_rpc = match tx_from_rpc { Some(t) => t, None => return ERR!("Didn't find provided tx {:?} on ETH node", tx), }; - if tx_from_rpc.from != sender_addr { + if tx_from_rpc.from != Some(sender_addr) { return ERR!( "Fee tx {:?} was sent from wrong address, expected {:?}", tx_from_rpc, @@ -894,7 +1058,7 @@ impl SwapOps for EthCoin { } let function = try_s!(ERC20_CONTRACT.function("transfer")); - let decoded_input = try_s!(function.decode_input(&tx_from_rpc.input.0)); + let decoded_input = try_s!(decode_contract_call(function, &tx_from_rpc.input.0)); if decoded_input[0] != Token::Address(fee_addr) { return ERR!( @@ -920,8 +1084,11 @@ impl SwapOps for EthCoin { Box::new(fut.boxed().compat()) } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { - let swap_contract_address = try_fus!(input.swap_contract_address.try_to_address()); + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + let swap_contract_address = try_f!(input + .swap_contract_address + .try_to_address() + .map_to_mm(ValidatePaymentError::InvalidParameter)); self.validate_payment( &input.payment_tx, input.time_lock, @@ -932,8 +1099,11 @@ impl SwapOps for EthCoin { ) } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { - let swap_contract_address = try_fus!(input.swap_contract_address.try_to_address()); + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + let swap_contract_address = try_f!(input + .swap_contract_address + .try_to_address() + .map_to_mm(ValidatePaymentError::InvalidParameter)); self.validate_payment( &input.payment_tx, input.time_lock, @@ -946,16 +1116,12 @@ impl SwapOps for EthCoin { fn check_if_my_payment_sent( &self, - time_lock: u32, - _other_pub: &[u8], - secret_hash: &[u8], - from_block: u64, - swap_contract_address: &Option, - _swap_unique_data: &[u8], + if_my_payment_sent_args: CheckIfMyPaymentSentArgs, ) -> 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()); + let id = self.etomic_swap_id(if_my_payment_sent_args.time_lock, if_my_payment_sent_args.secret_hash); + let swap_contract_address = try_fus!(if_my_payment_sent_args.swap_contract_address.try_to_address()); let selfi = self.clone(); + let from_block = if_my_payment_sent_args.search_from_block; let fut = async move { let status = try_s!( selfi @@ -994,7 +1160,6 @@ impl SwapOps for EthCoin { .web3 .eth() .transaction(TransactionId::Hash(event.transaction_hash.unwrap())) - .compat() .await ); match transaction { @@ -1019,8 +1184,13 @@ impl SwapOps for EthCoin { input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { 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 + self.search_for_swap_tx_spend( + input.tx, + swap_contract_address, + input.secret_hash, + input.search_from_block, + ) + .await } async fn search_for_swap_tx_spend_other( @@ -1028,14 +1198,36 @@ impl SwapOps for EthCoin { input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { 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 + self.search_for_swap_tx_spend( + input.tx, + swap_contract_address, + input.secret_hash, + input.search_from_block, + ) + .await } - fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + + async fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { let unverified: UnverifiedTransaction = try_s!(rlp::decode(spend_tx)); let function = try_s!(SWAP_CONTRACT.function("receiverSpend")); - let tokens = try_s!(function.decode_input(&unverified.data)); + + // Validate contract call; expected to be receiverSpend. + // https://www.4byte.directory/signatures/?bytes4_signature=02ed292b. + let expected_signature = function.short_signature(); + let actual_signature = &unverified.data[0..4]; + if actual_signature != expected_signature { + return ERR!( + "Expected 'receiverSpend' contract call signature: {:?}, found {:?}", + expected_signature, + actual_signature + ); + }; + + let tokens = try_s!(decode_contract_call(function, &unverified.data)); if tokens.len() < 3 { return ERR!("Invalid arguments in 'receiverSpend' call: {:?}", tokens); } @@ -1048,34 +1240,462 @@ impl SwapOps for EthCoin { } } - fn negotiate_swap_contract_addr( + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } + + fn negotiate_swap_contract_addr( + &self, + other_side_address: Option<&[u8]>, + ) -> Result, MmError> { + match other_side_address { + Some(bytes) => { + if bytes.len() != 20 { + return MmError::err(NegotiateSwapContractAddrErr::InvalidOtherAddrLen(bytes.into())); + } + let other_addr = Address::from_slice(bytes); + + if other_addr == self.swap_contract_address { + return Ok(Some(self.swap_contract_address.0.to_vec().into())); + } + + if Some(other_addr) == self.fallback_swap_contract { + return Ok(self.fallback_swap_contract.map(|addr| addr.0.to_vec().into())); + } + MmError::err(NegotiateSwapContractAddrErr::UnexpectedOtherAddr(bytes.into())) + }, + None => self + .fallback_swap_contract + .map(|addr| Some(addr.0.to_vec().into())) + .ok_or_else(|| MmError::new(NegotiateSwapContractAddrErr::NoOtherAddrAndNoFallback)), + } + } + + #[inline] + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> keys::KeyPair { + match self.priv_key_policy { + EthPrivKeyPolicy::KeyPair(ref key_pair) => { + key_pair_from_secret(key_pair.secret().as_bytes()).expect("valid key") + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(_) => todo!(), + } + } + + #[inline] + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { + match self.priv_key_policy { + EthPrivKeyPolicy::KeyPair(ref key_pair) => key_pair_from_secret(key_pair.secret().as_bytes()) + .expect("valid key") + .public_slice() + .to_vec(), + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(ref metamask_policy) => metamask_policy.public_key.as_bytes().to_vec(), + } + } + + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + if let Err(e) = PublicKey::from_slice(raw_pubkey) { + return MmError::err(ValidateOtherPubKeyErr::InvalidPubKey(e.to_string())); + }; + Ok(()) + } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { true } +} + +#[async_trait] +impl TakerSwapMakerCoin for EthCoin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for EthCoin { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for EthCoin { + fn send_maker_payment_spend_preimage(&self, input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(input.preimage)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + + Box::new( + self.watcher_spend_hash_time_locked_payment(signed, input.secret_hash, input.secret, input.taker_pub) + .map(TransactionEnum::from), + ) + } + + fn create_maker_payment_spend_preimage( + &self, + maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_payment_tx)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + let fut = async move { Ok(TransactionEnum::from(signed)) }; + + Box::new(fut.boxed().compat()) + } + + fn create_taker_payment_refund_preimage( + &self, + taker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_payment_tx)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + let fut = async move { Ok(TransactionEnum::from(signed)) }; + + Box::new(fut.boxed().compat()) + } + + fn send_taker_payment_refund_preimage( + &self, + watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(watcher_refunds_payment_args.payment_tx)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + + Box::new( + self.watcher_refunds_hash_time_locked_payment( + signed, + watcher_refunds_payment_args.secret_hash, + watcher_refunds_payment_args.other_pubkey, + ) + .map(TransactionEnum::from), + ) + } + + fn watcher_validate_taker_fee(&self, validate_fee_args: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + let selfi = self.clone(); + let sender_addr = + try_f!(addr_from_raw_pubkey(&validate_fee_args.sender_pubkey) + .map_to_mm(ValidatePaymentError::InvalidParameter)); + let fee_addr = + try_f!(addr_from_raw_pubkey(&validate_fee_args.fee_addr).map_to_mm(ValidatePaymentError::InvalidParameter)); + let min_block_number = validate_fee_args.min_block_number; + let taker_fee_hash = validate_fee_args.taker_fee_hash; + + let fut = async move { + let tx_from_rpc = selfi + .web3 + .eth() + .transaction(TransactionId::Hash(H256::from_slice(taker_fee_hash.as_slice()))) + .await + .map_to_mm(|e| ValidatePaymentError::InvalidRpcResponse(e.to_string()))?; + + let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { + ValidatePaymentError::TxDoesNotExist(format!( + "Didn't find provided tx {:?} on ETH node", + H256::from_slice(taker_fee_hash.as_slice()) + )) + })?; + + if tx_from_rpc.from != Some(sender_addr) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Fee tx {:?} was sent from wrong address, expected {:?}", + INVALID_SENDER_ERR_LOG, tx_from_rpc, sender_addr + ))); + } + + if let Some(block_number) = tx_from_rpc.block_number { + if block_number <= min_block_number.into() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Fee tx {:?} confirmed before min_block {}", + EARLY_CONFIRMATION_ERR_LOG, tx_from_rpc, min_block_number + ))); + } + } + + //TODO: Validate if taker fee is old + + match &selfi.coin_type { + EthCoinType::Eth => { + if tx_from_rpc.to != Some(fee_addr) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Fee tx {:?} was sent to wrong address, expected {:?}", + INVALID_RECEIVER_ERR_LOG, tx_from_rpc, fee_addr + ))); + } + }, + EthCoinType::Erc20 { + platform: _, + token_addr, + } => { + if tx_from_rpc.to != Some(*token_addr) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: ERC20 Fee tx {:?} called wrong smart contract, expected {:?}", + INVALID_CONTRACT_ADDRESS_ERR_LOG, tx_from_rpc, token_addr + ))); + } + + let function = ERC20_CONTRACT + .function("transfer") + .map_to_mm(|e| ValidatePaymentError::InternalError(e.to_string()))?; + let decoded_input = function + .decode_input(&tx_from_rpc.input.0) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; + let address_input = get_function_input_data(&decoded_input, function, 0) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if address_input != Token::Address(fee_addr) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: ERC20 Fee tx was sent to wrong address {:?}, expected {:?}", + INVALID_RECEIVER_ERR_LOG, address_input, fee_addr + ))); + } + }, + } + + Ok(()) + }; + + Box::new(fut.boxed().compat()) + } + + fn watcher_validate_taker_payment(&self, input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + let unsigned: UnverifiedTransaction = try_f!(rlp::decode(&input.payment_tx)); + let tx = + try_f!(SignedEthTx::new(unsigned) + .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))); + let sender = try_f!(addr_from_raw_pubkey(&input.taker_pub).map_to_mm(ValidatePaymentError::InvalidParameter)); + let receiver = try_f!(addr_from_raw_pubkey(&input.maker_pub).map_to_mm(ValidatePaymentError::InvalidParameter)); + + let selfi = self.clone(); + let swap_id = selfi.etomic_swap_id(input.time_lock, &input.secret_hash); + let secret_hash = if input.secret_hash.len() == 32 { + ripemd160(&input.secret_hash).to_vec() + } else { + input.secret_hash.to_vec() + }; + let expected_swap_contract_address = self.swap_contract_address; + let fallback_swap_contract = self.fallback_swap_contract; + let fut = async move { + let tx_from_rpc = selfi.web3.eth().transaction(TransactionId::Hash(tx.hash)).await?; + + let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { + ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx)) + })?; + + if tx_from_rpc.from != Some(sender) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx {:?} was sent from wrong address, expected {:?}", + tx_from_rpc, sender + ))); + } + + let swap_contract_address = tx_from_rpc.to.ok_or_else(|| { + ValidatePaymentError::TxDeserializationError(format!( + "Swap contract address not found in payment Tx {tx_from_rpc:?}" + )) + })?; + + if swap_contract_address != expected_swap_contract_address + && Some(swap_contract_address) != fallback_swap_contract + { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx {tx_from_rpc:?} was sent to wrong address, expected either {expected_swap_contract_address:?} or the fallback {fallback_swap_contract:?}" + ))); + } + + let status = selfi + .payment_status(swap_contract_address, Token::FixedBytes(swap_id.clone())) + .compat() + .await + .map_to_mm(ValidatePaymentError::Transport)?; + if status != PAYMENT_STATE_SENT.into() { + return MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( + "Payment state is not PAYMENT_STATE_SENT, got {}", + status + ))); + } + + match &selfi.coin_type { + EthCoinType::Eth => { + let function = SWAP_CONTRACT + .function("ethPayment") + .map_to_mm(|err| ValidatePaymentError::InternalError(err.to_string()))?; + let decoded = function + .decode_input(&tx_from_rpc.input.0) + .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))?; + + let swap_id_input = get_function_input_data(&decoded, function, 0) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if swap_id_input != Token::FixedBytes(swap_id.clone()) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid 'swap_id' {:?}, expected {:?}", + decoded, swap_id + ))); + } + + let receiver_input = get_function_input_data(&decoded, function, 1) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if receiver_input != Token::Address(receiver) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx receiver arg {:?} is invalid, expected {:?}", + receiver_input, + Token::Address(receiver) + ))); + } + + let secret_hash_input = get_function_input_data(&decoded, function, 2) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if secret_hash_input != Token::FixedBytes(secret_hash.to_vec()) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx secret_hash arg {:?} is invalid, expected {:?}", + secret_hash_input, + Token::FixedBytes(secret_hash.to_vec()), + ))); + } + + let time_lock_input = get_function_input_data(&decoded, function, 3) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if time_lock_input != Token::Uint(U256::from(input.time_lock)) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx time_lock arg {:?} is invalid, expected {:?}", + time_lock_input, + Token::Uint(U256::from(input.time_lock)), + ))); + } + }, + EthCoinType::Erc20 { + platform: _, + token_addr, + } => { + let function = SWAP_CONTRACT + .function("erc20Payment") + .map_to_mm(|err| ValidatePaymentError::InternalError(err.to_string()))?; + let decoded = function + .decode_input(&tx_from_rpc.input.0) + .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))?; + + let swap_id_input = get_function_input_data(&decoded, function, 0) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if swap_id_input != Token::FixedBytes(swap_id.clone()) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid 'swap_id' {:?}, expected {:?}", + decoded, swap_id + ))); + } + + let token_addr_input = get_function_input_data(&decoded, function, 2) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if token_addr_input != Token::Address(*token_addr) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx token_addr arg {:?} is invalid, expected {:?}", + token_addr_input, + Token::Address(*token_addr) + ))); + } + + let receiver_addr_input = get_function_input_data(&decoded, function, 3) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if receiver_addr_input != Token::Address(receiver) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx receiver arg {:?} is invalid, expected {:?}", + receiver_addr_input, + Token::Address(receiver), + ))); + } + + let secret_hash_input = get_function_input_data(&decoded, function, 4) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if secret_hash_input != Token::FixedBytes(secret_hash.to_vec()) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx secret_hash arg {:?} is invalid, expected {:?}", + secret_hash_input, + Token::FixedBytes(secret_hash.to_vec()), + ))); + } + + let time_lock_input = get_function_input_data(&decoded, function, 5) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if time_lock_input != Token::Uint(U256::from(input.time_lock)) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx time_lock arg {:?} is invalid, expected {:?}", + time_lock_input, + Token::Uint(U256::from(input.time_lock)), + ))); + } + }, + } + + Ok(()) + }; + Box::new(fut.boxed().compat()) + } + + async fn watcher_search_for_swap_tx_spend( &self, - other_side_address: Option<&[u8]>, - ) -> Result, MmError> { - match other_side_address { - Some(bytes) => { - if bytes.len() != 20 { - return MmError::err(NegotiateSwapContractAddrErr::InvalidOtherAddrLen(bytes.into())); - } - let other_addr = Address::from(bytes); - if other_addr == self.swap_contract_address { - return Ok(Some(self.swap_contract_address.to_vec().into())); - } - - if Some(other_addr) == self.fallback_swap_contract { - return Ok(self.fallback_swap_contract.map(|addr| addr.to_vec().into())); - } - MmError::err(NegotiateSwapContractAddrErr::UnexpectedOtherAddr(bytes.into())) - }, - None => self - .fallback_swap_contract - .map(|addr| Some(addr.to_vec().into())) - .ok_or_else(|| MmError::new(NegotiateSwapContractAddrErr::NoOtherAddrAndNoFallback)), - } - } + input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + let unverified: UnverifiedTransaction = try_s!(rlp::decode(input.tx)); + let tx = try_s!(SignedEthTx::new(unverified)); + let swap_contract_address = match tx.action { + Call(address) => address, + Create => return Err(ERRL!("Invalid payment action: the payment action cannot be create")), + }; - fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> keys::KeyPair { - key_pair_from_secret(self.key_pair.secret()).expect("valid key") + self.search_for_swap_tx_spend( + input.tx, + swap_contract_address, + input.secret_hash, + input.search_from_block, + ) + .await } } @@ -1083,14 +1703,28 @@ impl SwapOps for EthCoin { impl MarketCoinOps for EthCoin { fn ticker(&self) -> &str { &self.ticker[..] } - fn my_address(&self) -> Result { Ok(checksum_address(&format!("{:#02x}", self.my_address))) } + fn my_address(&self) -> MmResult { + Ok(checksum_address(&format!("{:#02x}", self.my_address))) + } - fn get_public_key(&self) -> Result> { unimplemented!() } + fn get_public_key(&self) -> Result> { + match self.priv_key_policy { + EthPrivKeyPolicy::KeyPair(ref key_pair) => { + let uncompressed_without_prefix = hex::encode(key_pair.public()); + Ok(format!("04{}", uncompressed_without_prefix)) + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(ref metamask_policy) => { + Ok(format!("{:02x}", metamask_policy.public_key_uncompressed)) + }, + } + } /// 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); @@ -1102,7 +1736,7 @@ impl MarketCoinOps for EthCoin { 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 privkey = &self.priv_key_policy.key_pair_or_err()?.secret(); let signature = sign(privkey, &H256::from(message_hash))?; Ok(format!("0x{}", signature)) } @@ -1134,7 +1768,7 @@ impl MarketCoinOps for EthCoin { fn base_coin_balance(&self) -> BalanceFut { Box::new( self.eth_balance() - .and_then(move |result| Ok(u256_to_big_decimal(result, 18)?)), + .and_then(move |result| Ok(u256_to_big_decimal(result, ETH_DECIMALS)?)), ) } @@ -1154,6 +1788,7 @@ impl MarketCoinOps for EthCoin { self.web3 .eth() .send_raw_transaction(bytes.into()) + .compat() .map(|res| format!("{:02x}", res)) .map_err(|e| ERRL!("{}", e)), ) @@ -1164,6 +1799,7 @@ impl MarketCoinOps for EthCoin { self.web3 .eth() .send_raw_transaction(tx.into()) + .compat() .map(|res| format!("{:02x}", res)) .map_err(|e| ERRL!("{}", e)), ) @@ -1185,7 +1821,7 @@ impl MarketCoinOps for EthCoin { let unsigned: UnverifiedTransaction = try_fus!(rlp::decode(tx)); let tx = try_fus!(SignedEthTx::new(unsigned)); - let required_confirms = U256::from(confirmations); + let required_confirms = U64::from(confirmations); let selfi = self.clone(); let fut = async move { loop { @@ -1198,7 +1834,7 @@ impl MarketCoinOps for EthCoin { ); } - let web3_receipt = match selfi.web3.eth().transaction_receipt(tx.hash()).compat().await { + let web3_receipt = match selfi.web3.eth().transaction_receipt(tx.hash()).await { Ok(r) => r, Err(e) => { error!( @@ -1223,7 +1859,7 @@ impl MarketCoinOps for EthCoin { } if let Some(confirmed_at) = receipt.block_number { - let current_block = match selfi.web3.eth().block_number().compat().await { + let current_block = match selfi.web3.eth().block_number().await { Ok(b) => b, Err(e) => { error!( @@ -1248,12 +1884,14 @@ impl MarketCoinOps for EthCoin { Box::new(fut.boxed().compat()) } - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, tx_bytes: &[u8], + _secret_hash: &[u8], wait_until: u64, from_block: u64, swap_contract_address: &Option, + check_every: f64, ) -> TransactionFut { let unverified: UnverifiedTransaction = try_tx_fus!(rlp::decode(tx_bytes)); let tx = try_tx_fus!(SignedEthTx::new(unverified)); @@ -1265,10 +1903,15 @@ impl MarketCoinOps for EthCoin { }; 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!(), + let decoded = try_tx_fus!(decode_contract_call(payment_func, &tx.data)); + let id = match decoded.first() { + Some(Token::FixedBytes(bytes)) => bytes.clone(), + invalid_token => { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Expected Token::FixedBytes, got {:?}", + invalid_token + )))) + }, }; let selfi = self.clone(); @@ -1300,22 +1943,16 @@ impl MarketCoinOps for EthCoin { if let Some(event) = found { if let Some(tx_hash) = event.transaction_hash { - let transaction = match selfi - .web3 - .eth() - .transaction(TransactionId::Hash(tx_hash)) - .compat() - .await - { + let transaction = match selfi.web3.eth().transaction(TransactionId::Hash(tx_hash)).await { Ok(Some(t)) => t, Ok(None) => { info!("Tx {} not found yet", tx_hash); - Timer::sleep(5.).await; + Timer::sleep(check_every).await; continue; }, Err(e) => { error!("Get tx {} error: {}", tx_hash, e); - Timer::sleep(5.).await; + Timer::sleep(check_every).await; continue; }, }; @@ -1338,8 +1975,10 @@ impl MarketCoinOps for EthCoin { Box::new(fut.boxed().compat()) } - fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result { - Ok(try_s!(signed_eth_tx_from_bytes(bytes)).into()) + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { + signed_eth_tx_from_bytes(bytes) + .map(TransactionEnum::from) + .map_to_mm(TxMarshalingErr::InvalidInput) } fn current_block(&self) -> Box + Send> { @@ -1347,12 +1986,19 @@ impl MarketCoinOps for EthCoin { self.web3 .eth() .block_number() - .map(|res| res.into()) + .compat() + .map(|res| res.as_u64()) .map_err(|e| ERRL!("{}", e)), ) } - fn display_priv_key(&self) -> Result { Ok(format!("{:#02x}", self.key_pair.secret())) } + fn display_priv_key(&self) -> Result { + match self.priv_key_policy { + EthPrivKeyPolicy::KeyPair(ref key_pair) => Ok(format!("{:#02x}", key_pair.secret())), + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support MetaMask"), + } + } fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } @@ -1376,9 +2022,10 @@ lazy_static! { type EthTxFut = Box + Send + 'static>; -async fn sign_and_send_transaction_impl( +async fn sign_and_send_transaction_with_keypair( ctx: MmArc, - coin: EthCoin, + coin: &EthCoin, + key_pair: &KeyPair, value: U256, action: Action, data: Vec, @@ -1399,6 +2046,7 @@ async fn sign_and_send_transaction_impl( ); status.status(tags!(), "get_gas_price…"); let gas_price = try_tx_s!(coin.get_gas_price().compat().await); + let tx = UnSignedEthTx { nonce, gas_price, @@ -1407,43 +2055,71 @@ async fn sign_and_send_transaction_impl( value, data, }; - let signed = tx.sign(coin.key_pair.secret(), coin.chain_id); - let bytes = web3::types::Bytes(rlp::encode(&signed).to_vec()); + + let signed = tx.sign(key_pair.secret(), coin.chain_id); + let bytes = Bytes(rlp::encode(&signed).to_vec()); status.status(tags!(), "send_raw_transaction…"); try_tx_s!( coin.web3 .eth() .send_raw_transaction(bytes) - .map_err(|e| ERRL!("{}", e)) - .compat() - .await, + .await + .map_err(|e| ERRL!("{}", e)), signed ); status.status(tags!(), "get_addr_nonce…"); - loop { - // Check every second till ETH nodes recognize that nonce is increased - // Parity has reliable "nextNonce" method that always returns correct nonce for address - // But we can't expect that all nodes will always be Parity. - // Some of ETH forks use Geth only so they don't have Parity nodes at all. - let new_nonce = match get_addr_nonce(coin.my_address, coin.web3_instances.clone()) - .compat() + coin.wait_for_addr_nonce_increase(coin.my_address, nonce).await; + Ok(signed) +} + +#[cfg(target_arch = "wasm32")] +async fn sign_and_send_transaction_with_metamask( + coin: EthCoin, + value: U256, + action: Action, + data: Vec, + gas: U256, +) -> Result { + let to = match action { + Action::Create => None, + Action::Call(to) => Some(to), + }; + + let gas_price = try_tx_s!(coin.get_gas_price().compat().await); + + let tx_to_send = TransactionRequest { + from: coin.my_address, + to, + gas: Some(gas), + gas_price: Some(gas_price), + value: Some(value), + data: Some(data.clone().into()), + nonce: None, + ..TransactionRequest::default() + }; + + // It's important to return the transaction hex for the swap, + // so wait up to 60 seconds for the transaction to appear on the RPC node. + let wait_rpc_timeout = 60_000; + let check_every = 1.; + + // Please note that this method may take a long time + // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. + let tx_hash = try_tx_s!(coin.web3.eth().send_transaction(tx_to_send).await); + + let maybe_signed_tx = try_tx_s!( + coin.wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) .await - { - Ok(n) => n, - Err(e) => { - 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; - }, - }; - if new_nonce > nonce { - break; - }; - Timer::sleep(1.).await; + ); + match maybe_signed_tx { + Some(signed_tx) => Ok(signed_tx), + None => TX_PLAIN_ERR!( + "Waited too long until the transaction {:?} appear on the RPC node", + tx_hash + ), } - Ok(signed) } impl EthCoin { @@ -1456,7 +2132,7 @@ impl EthCoin { // Artem Pikulin: by playing a bit with Parity mainnet node I've discovered that trace_filter API responds after reasonable time for 1000 blocks. // I've tried to increase the amount to 10000, but request times out somewhere near 2500000 block. // Also the Parity RPC server seem to get stuck while request in running (other requests performance is also lowered). - let delta = U256::from(1000); + let delta = U64::from(1000); let mut success_iteration = 0i32; loop { @@ -1472,7 +2148,7 @@ impl EthCoin { }; } - let current_block = match self.web3.eth().block_number().compat().await { + let current_block = match self.web3.eth().block_number().await { Ok(block) => block, Err(e) => { ctx.log.log( @@ -1494,7 +2170,7 @@ impl EthCoin { }, }; *self.history_sync_state.lock().unwrap() = HistorySyncState::InProgress(json!({ - "blocks_left": u64::from(saved_traces.earliest_block), + "blocks_left": saved_traces.earliest_block.as_u64(), })); let mut existing_history = match self.load_history_from_file(ctx).compat().await { @@ -1523,8 +2199,8 @@ impl EthCoin { .eth_traces( vec![self.my_address], vec![], - BlockNumber::Number(before_earliest.into()), - BlockNumber::Number((saved_traces.earliest_block).into()), + BlockNumber::Number(before_earliest), + BlockNumber::Number(saved_traces.earliest_block), None, ) .compat() @@ -1546,8 +2222,8 @@ impl EthCoin { .eth_traces( vec![], vec![self.my_address], - BlockNumber::Number(before_earliest.into()), - BlockNumber::Number((saved_traces.earliest_block).into()), + BlockNumber::Number(before_earliest), + BlockNumber::Number(saved_traces.earliest_block), None, ) .compat() @@ -1585,8 +2261,8 @@ impl EthCoin { .eth_traces( vec![self.my_address], vec![], - BlockNumber::Number((saved_traces.latest_block + 1).into()), - BlockNumber::Number(current_block.into()), + BlockNumber::Number(saved_traces.latest_block + 1), + BlockNumber::Number(current_block), None, ) .compat() @@ -1608,8 +2284,8 @@ impl EthCoin { .eth_traces( vec![], vec![self.my_address], - BlockNumber::Number((saved_traces.latest_block + 1).into()), - BlockNumber::Number(current_block.into()), + BlockNumber::Number(saved_traces.latest_block + 1), + BlockNumber::Number(current_block), None, ) .compat() @@ -1658,7 +2334,6 @@ impl EthCoin { .web3 .eth() .transaction(TransactionId::Hash(trace.transaction_hash.unwrap())) - .compat() .await { Ok(tx) => tx, @@ -1693,7 +2368,6 @@ impl EthCoin { .web3 .eth() .transaction_receipt(trace.transaction_hash.unwrap()) - .compat() .await { Ok(r) => r, @@ -1715,14 +2389,18 @@ impl EthCoin { EthCoinType::Erc20 { platform, .. } => platform.as_str(), }; let fee_details: Option = match receipt { - Some(r) => Some( - EthTxFeeDetails::new(r.gas_used.unwrap_or_else(|| 0.into()), web3_tx.gas_price, fee_coin) - .unwrap(), - ), + Some(r) => { + let gas_used = r.gas_used.unwrap_or_default(); + let gas_price = web3_tx.gas_price.unwrap_or_default(); + // It's relatively safe to unwrap `EthTxFeeDetails::new` as it may fail + // due to `u256_to_big_decimal` only. + // Also TX history is not used by any GUI and has significant disadvantages. + Some(EthTxFeeDetails::new(gas_used, gas_price, fee_coin).unwrap()) + }, None => None, }; - let total_amount: BigDecimal = u256_to_big_decimal(call_data.value, 18).unwrap(); + let total_amount: BigDecimal = u256_to_big_decimal(call_data.value, ETH_DECIMALS).unwrap(); let mut received_by_me = 0.into(); let mut spent_by_me = 0.into(); @@ -1747,8 +2425,7 @@ impl EthCoin { let block = match self .web3 .eth() - .block(BlockId::Number(BlockNumber::Number(trace.block_number))) - .compat() + .block(BlockId::Number(BlockNumber::Number(trace.block_number.into()))) .await { Ok(b) => b.unwrap(), @@ -1772,12 +2449,13 @@ impl EthCoin { coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: trace.block_number, - tx_hash: format!("{:02x}", BytesJson(raw.hash.to_vec())), - tx_hex: BytesJson(rlp::encode(&raw)), + tx_hash: format!("{:02x}", BytesJson(raw.hash.as_bytes().to_vec())), + tx_hex: BytesJson(rlp::encode(&raw).to_vec()), internal_id, - timestamp: block.timestamp.into(), + timestamp: block.timestamp.into_or_max(), kmd_rewards: None, transaction_type: Default::default(), + memo: None, }; existing_history.push(details); @@ -1813,7 +2491,7 @@ impl EthCoin { #[allow(clippy::cognitive_complexity)] #[cfg_attr(target_arch = "wasm32", allow(dead_code))] async fn process_erc20_history(&self, token_addr: H160, ctx: &MmArc) { - let delta = U256::from(10000); + let delta = U64::from(10000); let mut success_iteration = 0i32; loop { @@ -1829,7 +2507,7 @@ impl EthCoin { }; } - let current_block = match self.web3.eth().block_number().compat().await { + let current_block = match self.web3.eth().block_number().await { Ok(block) => block, Err(e) => { ctx.log.log( @@ -1851,7 +2529,7 @@ impl EthCoin { }, }; *self.history_sync_state.lock().unwrap() = HistorySyncState::InProgress(json!({ - "blocks_left": u64::from(saved_events.earliest_block), + "blocks_left": saved_events.earliest_block, })); // AP: AFAIK ETH RPC doesn't support conditional filters like `get this OR this` so we have @@ -1869,8 +2547,8 @@ impl EthCoin { token_addr, Some(self.my_address), None, - BlockNumber::Number(before_earliest.into()), - BlockNumber::Number((saved_events.earliest_block - 1).into()), + BlockNumber::Number(before_earliest), + BlockNumber::Number(saved_events.earliest_block - 1), None, ) .compat() @@ -1893,8 +2571,8 @@ impl EthCoin { token_addr, None, Some(self.my_address), - BlockNumber::Number(before_earliest.into()), - BlockNumber::Number((saved_events.earliest_block - 1).into()), + BlockNumber::Number(before_earliest), + BlockNumber::Number(saved_events.earliest_block - 1), None, ) .compat() @@ -1932,8 +2610,8 @@ impl EthCoin { token_addr, Some(self.my_address), None, - BlockNumber::Number((saved_events.latest_block + 1).into()), - BlockNumber::Number(current_block.into()), + BlockNumber::Number(saved_events.latest_block + 1), + BlockNumber::Number(current_block), None, ) .compat() @@ -1956,8 +2634,8 @@ impl EthCoin { token_addr, None, Some(self.my_address), - BlockNumber::Number((saved_events.latest_block + 1).into()), - BlockNumber::Number(current_block.into()), + BlockNumber::Number(saved_events.latest_block + 1), + BlockNumber::Number(current_block), None, ) .compat() @@ -1991,7 +2669,7 @@ impl EthCoin { .filter(|e| e.block_number.is_some() && e.transaction_hash.is_some() && !e.is_removed()) .map(|e| (e.transaction_hash.unwrap(), e)) .collect(); - let mut all_events: Vec<_> = all_events.into_iter().map(|(_, log)| log).collect(); + let mut all_events: Vec<_> = all_events.into_values().collect(); all_events.sort_by(|a, b| b.block_number.unwrap().cmp(&a.block_number.unwrap())); for event in all_events { @@ -2035,7 +2713,6 @@ impl EthCoin { .web3 .eth() .transaction(TransactionId::Hash(event.transaction_hash.unwrap())) - .compat() .await { Ok(tx) => tx, @@ -2072,7 +2749,6 @@ impl EthCoin { .web3 .eth() .transaction_receipt(event.transaction_hash.unwrap()) - .compat() .await { Ok(r) => r, @@ -2094,18 +2770,21 @@ impl EthCoin { EthCoinType::Erc20 { platform, .. } => platform.as_str(), }; let fee_details = match receipt { - Some(r) => Some( - EthTxFeeDetails::new(r.gas_used.unwrap_or_else(|| 0.into()), web3_tx.gas_price, fee_coin) - .unwrap(), - ), + Some(r) => { + let gas_used = r.gas_used.unwrap_or_default(); + let gas_price = web3_tx.gas_price.unwrap_or_default(); + // It's relatively safe to unwrap `EthTxFeeDetails::new` as it may fail + // due to `u256_to_big_decimal` only. + // Also TX history is not used by any GUI and has significant disadvantages. + Some(EthTxFeeDetails::new(gas_used, gas_price, fee_coin).unwrap()) + }, None => None, }; let block_number = event.block_number.unwrap(); let block = match self .web3 .eth() - .block(BlockId::Number(BlockNumber::Number(block_number.into()))) - .compat() + .block(BlockId::Number(BlockNumber::Number(block_number))) .await { Ok(Some(b)) => b, @@ -2137,13 +2816,14 @@ impl EthCoin { from: vec![checksum_address(&format!("{:#02x}", from_addr))], coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), - block_height: block_number.into(), - tx_hash: format!("{:02x}", BytesJson(raw.hash.to_vec())), - tx_hex: BytesJson(rlp::encode(&raw)), + block_height: block_number.as_u64(), + tx_hash: format!("{:02x}", BytesJson(raw.hash.as_bytes().to_vec())), + tx_hex: BytesJson(rlp::encode(&raw).to_vec()), internal_id: BytesJson(internal_id.to_vec()), - timestamp: block.timestamp.into(), + timestamp: block.timestamp.into_or_max(), kmd_rewards: None, transaction_type: Default::default(), + memo: None, }; existing_history.push(details); @@ -2180,15 +2860,19 @@ impl EthCoin { impl EthCoin { fn sign_and_send_transaction(&self, value: U256, action: Action, data: Vec, gas: U256) -> EthTxFut { 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(), - value, - action, - data, - gas, - )); - Box::new(fut.compat()) + let coin = self.clone(); + let fut = async move { + match coin.priv_key_policy { + EthPrivKeyPolicy::KeyPair(ref key_pair) => { + sign_and_send_transaction_with_keypair(ctx, &coin, key_pair, value, action, data, gas).await + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(_) => { + sign_and_send_transaction_with_metamask(coin, value, action, data, gas).await + }, + } + }; + Box::new(fut.boxed().compat()) } pub fn send_to_address(&self, address: Address, value: U256) -> EthTxFut { @@ -2215,58 +2899,268 @@ impl EthCoin { receiver_addr: Address, swap_contract_address: Address, ) -> EthTxFut { + let secret_hash = if secret_hash.len() == 32 { + ripemd160(secret_hash).to_vec() + } else { + secret_hash.to_vec() + }; + match &self.coin_type { EthCoinType::Eth => { 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()), + Token::FixedBytes(secret_hash), Token::Uint(U256::from(time_lock)) ])); - self.sign_and_send_transaction(value, Action::Call(swap_contract_address), data, U256::from(150_000)) + self.sign_and_send_transaction(value, Action::Call(swap_contract_address), data, U256::from(ETH_GAS)) + }, + EthCoinType::Erc20 { + platform: _, + token_addr, + } => { + let allowance_fut = self + .allowance(swap_contract_address) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e))); + + 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), + Token::Address(receiver_addr), + Token::FixedBytes(secret_hash), + Token::Uint(U256::from(time_lock)) + ])); + + let arc = self.clone(); + Box::new(allowance_fut.and_then(move |allowed| -> EthTxFut { + if allowed < value { + Box::new( + arc.approve(swap_contract_address, U256::max_value()) + .and_then(move |_approved| { + arc.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(ETH_GAS), + ) + }), + ) + } else { + Box::new(arc.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(ETH_GAS), + )) + } + })) + }, + } + } + + fn watcher_spend_hash_time_locked_payment( + &self, + payment: SignedEthTx, + _secret_hash: &[u8], + secret: &[u8], + taker_pub: &[u8], + ) -> EthTxFut { + let spend_func = try_tx_fus!(SWAP_CONTRACT.function("watcherSpend")); + let clone = self.clone(); + let secret_vec = secret.to_vec(); + let taker_addr = addr_from_raw_pubkey(taker_pub).unwrap(); + let swap_contract_address = match payment.action { + Call(address) => address, + Create => { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Invalid payment action: the payment action cannot be create" + )))) + }, + }; + + match self.coin_type { + EthCoinType::Eth => { + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); + + let state_f = self.payment_status(swap_contract_address, swap_id_input.clone()); + 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_tx_fus!(spend_func.encode_input(&[ + swap_id_input, + Token::Uint(value), + Token::FixedBytes(secret_vec.clone()), + Token::Address(Address::default()), + Token::Address(payment.sender()), + Token::Address(taker_addr) + ])); + + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(ETH_GAS), + ) + }), + ) + }, + EthCoinType::Erc20 { + platform: _, + token_addr, + } => { + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); + let amount_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); + let state_f = self.payment_status(swap_contract_address, swap_id_input.clone()); + + 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(&[ + swap_id_input.clone(), + amount_input, + Token::FixedBytes(secret_vec.clone()), + Token::Address(token_addr), + Token::Address(payment.sender()), + Token::Address(taker_addr) + ])); + + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(ETH_GAS), + ) + }), + ) + }, + } + } + + fn watcher_refunds_hash_time_locked_payment( + &self, + payment: SignedEthTx, + _secret_hash: &[u8], + taker_pub: &[u8], + ) -> EthTxFut { + let refund_func = try_tx_fus!(SWAP_CONTRACT.function("watcherRefund")); + let clone = self.clone(); + let taker_addr = addr_from_raw_pubkey(taker_pub).unwrap(); + let swap_contract_address = match payment.action { + Call(address) => address, + Create => { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Invalid payment action: the payment action cannot be create" + )))) + }, + }; + + match self.coin_type { + EthCoinType::Eth => { + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); + let amount_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); + let hash_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 2)); + + let state_f = self.payment_status(swap_contract_address, swap_id_input.clone()); + 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_tx_fus!(refund_func.encode_input(&[ + swap_id_input.clone(), + Token::Uint(value), + hash_input.clone(), + Token::Address(Address::default()), + Token::Address(taker_addr), + amount_input.clone(), + ])); + + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(ETH_GAS), + ) + }), + ) }, EthCoinType::Erc20 { platform: _, token_addr, } => { - let allowance_fut = self - .allowance(swap_contract_address) - .map_err(|e| TransactionErr::Plain(ERRL!("{}", e))); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); + let amount_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); + let token_addr_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 3)); + let sender_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 4)); + let state_f = self.payment_status(swap_contract_address, swap_id_input.clone()); + 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 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), - Token::Address(receiver_addr), - Token::FixedBytes(secret_hash.to_vec()), - Token::Uint(U256::from(time_lock)) - ])); + let data = try_tx_fus!(refund_func.encode_input(&[ + swap_id_input.clone(), + amount_input.clone(), + sender_input.clone(), + Token::Address(token_addr), + Token::Address(taker_addr), + token_addr_input.clone(), + ])); - let arc = self.clone(); - Box::new(allowance_fut.and_then(move |allowed| -> EthTxFut { - if allowed < value { - Box::new( - arc.approve(swap_contract_address, U256::max_value()) - .and_then(move |_approved| { - arc.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(150_000), - ) - }), - ) - } else { - Box::new(arc.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(150_000), - )) - } - })) + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(ETH_GAS), + ) + }), + ) }, } } @@ -2274,6 +3168,7 @@ impl EthCoin { fn spend_hash_time_locked_payment( &self, payment: SignedEthTx, + _secret_hash: &[u8], swap_contract_address: Address, secret: &[u8], ) -> EthTxFut { @@ -2284,7 +3179,7 @@ impl EthCoin { match self.coin_type { EthCoinType::Eth => { let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); - let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); Box::new( @@ -2312,7 +3207,7 @@ impl EthCoin { 0.into(), Action::Call(swap_contract_address), data, - U256::from(150_000), + U256::from(ETH_GAS), ) }), ) @@ -2322,7 +3217,8 @@ impl EthCoin { token_addr, } => { let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); - let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + + let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); Box::new( @@ -2348,7 +3244,7 @@ impl EthCoin { 0.into(), Action::Call(swap_contract_address), data, - U256::from(150_000), + U256::from(ETH_GAS), ) }), ) @@ -2356,14 +3252,19 @@ impl EthCoin { } } - fn refund_hash_time_locked_payment(&self, swap_contract_address: Address, payment: SignedEthTx) -> EthTxFut { + fn refund_hash_time_locked_payment( + &self, + swap_contract_address: Address, + payment: SignedEthTx, + _secret_hash: &[u8], + ) -> EthTxFut { let refund_func = try_tx_fus!(SWAP_CONTRACT.function("senderRefund")); let clone = self.clone(); match self.coin_type { EthCoinType::Eth => { let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); - let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); Box::new( @@ -2391,7 +3292,7 @@ impl EthCoin { 0.into(), Action::Call(swap_contract_address), data, - U256::from(150_000), + U256::from(ETH_GAS), ) }), ) @@ -2401,7 +3302,7 @@ impl EthCoin { token_addr, } => { let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); - let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); Box::new( state_f @@ -2427,7 +3328,7 @@ impl EthCoin { 0.into(), Action::Call(swap_contract_address), data, - U256::from(150_000), + U256::from(ETH_GAS), ) }), ) @@ -2443,13 +3344,12 @@ impl EthCoin { .web3 .eth() .balance(coin.my_address, Some(BlockNumber::Latest)) - .compat() .await?), EthCoinType::Erc20 { ref token_addr, .. } => { let function = ERC20_CONTRACT.function("balanceOf")?; let data = function.encode_input(&[Token::Address(coin.my_address)])?; - let res = coin.call_request(*token_addr, None, Some(data.into())).compat().await?; + let res = coin.call_request(*token_addr, None, Some(data.into())).await?; let decoded = function.decode_output(&res.0)?; match decoded[0] { Token::Uint(number) => Ok(number), @@ -2464,6 +3364,43 @@ impl EthCoin { Box::new(fut.boxed().compat()) } + pub async fn get_tokens_balance_list(&self) -> Result, MmError> { + let coin = self.clone(); + let mut token_balances = HashMap::new(); + for (token_ticker, info) in self.get_erc_tokens_infos().iter() { + let balance_as_u256 = coin.get_token_balance_by_address(info.token_address).await?; + let balance_as_big_decimal = u256_to_big_decimal(balance_as_u256, info.decimals)?; + let balance = CoinBalance { + spendable: balance_as_big_decimal, + unspendable: BigDecimal::from(0), + }; + token_balances.insert(token_ticker.clone(), balance); + } + + Ok(token_balances) + } + + async fn get_token_balance_by_address(&self, token_address: Address) -> Result> { + let coin = self.clone(); + let function = ERC20_CONTRACT.function("balanceOf")?; + let data = function.encode_input(&[Token::Address(coin.my_address)])?; + let res = coin.call_request(token_address, None, Some(data.into())).await?; + let decoded = function.decode_output(&res.0)?; + + match decoded[0] { + Token::Uint(number) => Ok(number), + _ => { + let error = format!("Expected U256 as balanceOf result but got {:?}", decoded); + MmError::err(BalanceError::InvalidResponse(error)) + }, + } + } + + fn estimate_gas(&self, req: CallRequest) -> Box + Send> { + // always using None block number as old Geth version accept only single argument in this RPC + Box::new(self.web3.eth().estimate_gas(req, None).compat()) + } + /// Estimates how much gas is necessary to allow the contract call to complete. /// `contract_addr` can be a ERC20 token address or any other contract address. /// @@ -2482,11 +3419,12 @@ impl EthCoin { value: Some(eth_value), data: Some(call_data), from: Some(coin.my_address), - to: contract_addr, + to: Some(contract_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), + ..CallRequest::default() }; coin.estimate_gas(estimate_gas_req).map_to_mm_fut(Web3RpcError::from) })) @@ -2497,26 +3435,26 @@ impl EthCoin { self.web3 .eth() .balance(self.my_address, Some(BlockNumber::Latest)) + .compat() .map_to_mm_fut(BalanceError::from), ) } - fn call_request( - &self, - to: Address, - value: Option, - data: Option, - ) -> impl Future { + async fn call_request(&self, to: Address, value: Option, data: Option) -> Result { let request = CallRequest { from: Some(self.my_address), - to, + to: Some(to), gas: None, gas_price: None, value, data, + ..CallRequest::default() }; - self.web3.eth().call(request, Some(BlockNumber::Latest)) + self.web3 + .eth() + .call(request, Some(BlockId::Number(BlockNumber::Latest))) + .await } fn allowance(&self, spender: Address) -> Web3RpcFut { @@ -2530,7 +3468,7 @@ impl EthCoin { let function = ERC20_CONTRACT.function("allowance")?; let data = function.encode_input(&[Token::Address(coin.my_address), Token::Address(spender)])?; - let res = coin.call_request(*token_addr, None, Some(data.into())).compat().await?; + let res = coin.call_request(*token_addr, None, Some(data.into())).await?; let decoded = function.decode_output(&res.0)?; match decoded[0] { @@ -2579,12 +3517,30 @@ impl EthCoin { let contract_event = try_fus!(SWAP_CONTRACT.event("PaymentSent")); let filter = FilterBuilder::default() .topics(Some(vec![contract_event.signature()]), None, None, None) - .from_block(BlockNumber::Number(from_block)) - .to_block(BlockNumber::Number(to_block)) + .from_block(BlockNumber::Number(from_block.into())) + .to_block(BlockNumber::Number(to_block.into())) + .address(vec![swap_contract_address]) + .build(); + + Box::new(self.web3.eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) + } + + /// Gets `ReceiverSpent` events from etomic swap smart contract since `from_block` + fn spend_events( + &self, + swap_contract_address: Address, + from_block: u64, + to_block: u64, + ) -> Box, Error = String> + Send> { + let contract_event = try_fus!(SWAP_CONTRACT.event("ReceiverSpent")); + let filter = FilterBuilder::default() + .topics(Some(vec![contract_event.signature()]), None, None, None) + .from_block(BlockNumber::Number(from_block.into())) + .to_block(BlockNumber::Number(to_block.into())) .address(vec![swap_contract_address]) .build(); - Box::new(self.web3.eth().logs(filter).map_err(|e| ERRL!("{}", e))) + Box::new(self.web3.eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) } fn validate_payment( @@ -2595,92 +3551,94 @@ impl EthCoin { secret_hash: &[u8], amount: BigDecimal, expected_swap_contract_address: Address, - ) -> Box + Send> { - let unsigned: UnverifiedTransaction = try_fus!(rlp::decode(payment_tx)); - let tx = try_fus!(SignedEthTx::new(unsigned)); - let sender = try_fus!(addr_from_raw_pubkey(sender_pub)); - let expected_value = try_fus!(wei_from_big_decimal(&amount, self.decimals)); + ) -> ValidatePaymentFut<()> { + let unsigned: UnverifiedTransaction = try_f!(rlp::decode(payment_tx)); + let tx = + try_f!(SignedEthTx::new(unsigned) + .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))); + let sender = try_f!(addr_from_raw_pubkey(sender_pub).map_to_mm(ValidatePaymentError::InvalidParameter)); + let expected_value = try_f!(wei_from_big_decimal(&amount, self.decimals)); let selfi = self.clone(); - let secret_hash = secret_hash.to_vec(); + let swap_id = selfi.etomic_swap_id(time_lock, secret_hash); + let secret_hash = if secret_hash.len() == 32 { + ripemd160(secret_hash).to_vec() + } else { + secret_hash.to_vec() + }; let fut = async move { - let swap_id = selfi.etomic_swap_id(time_lock, &secret_hash); - let status = try_s!( - selfi - .payment_status(expected_swap_contract_address, Token::FixedBytes(swap_id.clone())) - .compat() - .await - ); + let status = selfi + .payment_status(expected_swap_contract_address, Token::FixedBytes(swap_id.clone())) + .compat() + .await + .map_to_mm(ValidatePaymentError::Transport)?; if status != PAYMENT_STATE_SENT.into() { - return ERR!("Payment state is not PAYMENT_STATE_SENT, got {}", status); + return MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( + "Payment state is not PAYMENT_STATE_SENT, got {}", + status + ))); } - let tx_from_rpc = try_s!( - selfi - .web3 - .eth() - .transaction(TransactionId::Hash(tx.hash)) - .compat() - .await - ); - let tx_from_rpc = match tx_from_rpc { - Some(t) => t, - None => return ERR!("Didn't find provided tx {:?} on ETH node", tx), - }; + let tx_from_rpc = selfi.web3.eth().transaction(TransactionId::Hash(tx.hash)).await?; + let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { + ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx.hash)) + })?; - if tx_from_rpc.from != sender { - return ERR!( + if tx_from_rpc.from != Some(sender) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx {:?} was sent from wrong address, expected {:?}", - tx_from_rpc, - sender - ); + tx_from_rpc, sender + ))); } match &selfi.coin_type { EthCoinType::Eth => { if tx_from_rpc.to != Some(expected_swap_contract_address) { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx {:?} was sent to wrong address, expected {:?}", - tx_from_rpc, - expected_swap_contract_address - ); + tx_from_rpc, expected_swap_contract_address, + ))); } if tx_from_rpc.value != expected_value { - return ERR!( - "Payment tx {:?} value is invalid, expected {:?}", - tx_from_rpc, - expected_value - ); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx value arg {:?} is invalid, expected {:?}", + tx_from_rpc, expected_value + ))); } - - let function = try_s!(SWAP_CONTRACT.function("ethPayment")); - let decoded = try_s!(function.decode_input(&tx_from_rpc.input.0)); + let function = SWAP_CONTRACT + .function("ethPayment") + .map_to_mm(|err| ValidatePaymentError::InternalError(err.to_string()))?; + let decoded = decode_contract_call(function, &tx_from_rpc.input.0) + .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))?; if decoded[0] != Token::FixedBytes(swap_id.clone()) { - return ERR!("Invalid 'swap_id' {:?}, expected {:?}", decoded, swap_id); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid 'swap_id' {:?}, expected {:?}", + decoded, swap_id + ))); } if decoded[1] != Token::Address(selfi.my_address) { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx receiver arg {:?} is invalid, expected {:?}", decoded[1], Token::Address(selfi.my_address) - ); + ))); } if decoded[2] != Token::FixedBytes(secret_hash.to_vec()) { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx secret_hash arg {:?} is invalid, expected {:?}", decoded[2], - Token::FixedBytes(secret_hash.to_vec()) - ); + Token::FixedBytes(secret_hash.to_vec()), + ))); } if decoded[3] != Token::Uint(U256::from(time_lock)) { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx time_lock arg {:?} is invalid, expected {:?}", decoded[3], - Token::Uint(U256::from(time_lock)) - ); + Token::Uint(U256::from(time_lock)), + ))); } }, EthCoinType::Erc20 { @@ -2688,57 +3646,61 @@ impl EthCoin { token_addr, } => { if tx_from_rpc.to != Some(expected_swap_contract_address) { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx {:?} was sent to wrong address, expected {:?}", - tx_from_rpc, - expected_swap_contract_address - ); + tx_from_rpc, expected_swap_contract_address, + ))); } - - let function = try_s!(SWAP_CONTRACT.function("erc20Payment")); - let decoded = try_s!(function.decode_input(&tx_from_rpc.input.0)); + let function = SWAP_CONTRACT + .function("erc20Payment") + .map_to_mm(|err| ValidatePaymentError::InternalError(err.to_string()))?; + let decoded = decode_contract_call(function, &tx_from_rpc.input.0) + .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))?; if decoded[0] != Token::FixedBytes(swap_id.clone()) { - return ERR!("Invalid 'swap_id' {:?}, expected {:?}", decoded, swap_id); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid 'swap_id' {:?}, expected {:?}", + decoded, swap_id + ))); } if decoded[1] != Token::Uint(expected_value) { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx value arg {:?} is invalid, expected {:?}", decoded[1], - Token::Uint(expected_value) - ); + Token::Uint(expected_value), + ))); } if decoded[2] != Token::Address(*token_addr) { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx token_addr arg {:?} is invalid, expected {:?}", decoded[2], Token::Address(*token_addr) - ); + ))); } if decoded[3] != Token::Address(selfi.my_address) { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx receiver arg {:?} is invalid, expected {:?}", decoded[3], - Token::Address(selfi.my_address) - ); + Token::Address(selfi.my_address), + ))); } if decoded[4] != Token::FixedBytes(secret_hash.to_vec()) { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx secret_hash arg {:?} is invalid, expected {:?}", decoded[4], - Token::FixedBytes(secret_hash.to_vec()) - ); + Token::FixedBytes(secret_hash.to_vec()), + ))); } if decoded[5] != Token::Uint(U256::from(time_lock)) { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx time_lock arg {:?} is invalid, expected {:?}", decoded[5], - Token::Uint(U256::from(time_lock)) - ); + Token::Uint(U256::from(time_lock)), + ))); } }, } @@ -2757,23 +3719,26 @@ impl EthCoin { let data = try_fus!(function.encode_input(&[token])); - Box::new( - self.call_request(swap_contract_address, None, Some(data.into())) - .map_err(|e| ERRL!("{}", e)) - .and_then(move |bytes| { - let decoded_tokens = try_s!(function.decode_output(&bytes.0)); - match decoded_tokens[2] { - Token::Uint(state) => Ok(state), - _ => ERR!("Payment status must be uint, got {:?}", decoded_tokens[2]), - } - }), - ) + let coin = self.clone(); + let fut = async move { coin.call_request(swap_contract_address, None, Some(data.into())).await }; + + Box::new(fut.boxed().compat().map_err(|e| ERRL!("{}", e)).and_then(move |bytes| { + let decoded_tokens = try_s!(function.decode_output(&bytes.0)); + let state = decoded_tokens + .get(2) + .ok_or_else(|| ERRL!("Payment status must contain 'state' as the 2nd token"))?; + match state { + Token::Uint(state) => Ok(*state), + _ => ERR!("Payment status must be uint, got {:?}", state), + } + })) } async fn search_for_swap_tx_spend( &self, tx: &[u8], swap_contract_address: Address, + _secret_hash: &[u8], search_from_block: u64, ) -> Result, String> { let unverified: UnverifiedTransaction = try_s!(rlp::decode(tx)); @@ -2785,10 +3750,10 @@ impl EthCoin { }; let payment_func = try_s!(SWAP_CONTRACT.function(func_name)); - let decoded = try_s!(payment_func.decode_input(&tx.data)); - let id = match &decoded[0] { - Token::FixedBytes(bytes) => bytes.clone(), - _ => panic!(), + let decoded = try_s!(decode_contract_call(payment_func, &tx.data)); + let id = match decoded.first() { + Some(Token::FixedBytes(bytes)) => bytes.clone(), + invalid_token => return ERR!("Expected Token::FixedBytes, got {:?}", invalid_token), }; let mut current_block = try_s!(self.current_block().compat().await); @@ -2811,16 +3776,13 @@ impl EthCoin { 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)).compat().await) { - 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)).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) @@ -2840,16 +3802,13 @@ impl EthCoin { 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)).compat().await) { - 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)).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) @@ -2889,7 +3848,7 @@ impl EthCoin { None => None, }; - let eth_gas_price = match coin.web3.eth().gas_price().compat().await { + let eth_gas_price = match coin.web3.eth().gas_price().await { Ok(eth_gas) => Some(eth_gas), Err(e) => { error!("Error {} on eth_gasPrice request", e); @@ -2900,7 +3859,6 @@ impl EthCoin { let fee_history_namespace: EthFeeHistoryNamespace<_> = coin.web3.api(); let eth_fee_history_price = match fee_history_namespace .eth_fee_history(U256::from(1u64), BlockNumber::Latest, &[]) - .compat() .await { Ok(res) => res @@ -2908,20 +3866,73 @@ impl EthCoin { .first() .map(|val| increase_by_percent_one_gwei(*val, BASE_BLOCK_FEE_DIFF_PCT)), Err(e) => { - error!("Error {} on eth_feeHistory request", e); + debug!("Error {} on eth_feeHistory request", e); None }, }; - let all_prices = vec![gas_station_price, eth_gas_price, eth_fee_history_price]; - all_prices - .into_iter() + // on editions < 2021 the compiler will resolve array.into_iter() as (&array).into_iter() + // https://doc.rust-lang.org/edition-guide/rust-2021/IntoIterator-for-arrays.html#details + IntoIterator::into_iter([gas_station_price, eth_gas_price, eth_fee_history_price]) .flatten() .max() .or_mm_err(|| Web3RpcError::Internal("All requests failed".into())) }; Box::new(fut.boxed().compat()) } + + /// Checks every second till ETH nodes recognize that nonce is increased. + /// Parity has reliable "nextNonce" method that always returns correct nonce for address. + /// But we can't expect that all nodes will always be Parity. + /// Some of ETH forks use Geth only so they don't have Parity nodes at all. + /// + /// Please note that we just keep looping in case of a transport error hoping it will go away. + /// + /// # Warning + /// + /// The function is endless, we just keep looping in case of a transport error hoping it will go away. + async fn wait_for_addr_nonce_increase(&self, addr: Address, prev_nonce: U256) { + repeatable!(async { + match get_addr_nonce(addr, self.web3_instances.clone()).compat().await { + Ok(new_nonce) if new_nonce > prev_nonce => Ready(()), + Ok(_nonce) => Retry(()), + Err(e) => { + error!("Error getting {} {} nonce: {}", self.ticker(), self.my_address, e); + Retry(()) + }, + } + }) + .until_ready() + .repeat_every_secs(1.) + .await + .ok(); + } + + /// Returns `None` if the transaction hasn't appeared on the RPC nodes at the specified time. + #[cfg(target_arch = "wasm32")] + async fn wait_for_tx_appears_on_rpc( + &self, + tx_hash: H256, + wait_rpc_timeout_ms: u64, + check_every: f64, + ) -> Web3RpcResult> { + let wait_until = now_ms() + wait_rpc_timeout_ms; + while now_ms() < wait_until { + let maybe_tx = self.web3.eth().transaction(TransactionId::Hash(tx_hash)).await?; + if let Some(tx) = maybe_tx { + let signed_tx = signed_tx_from_web3_tx(tx).map_to_mm(Web3RpcError::InvalidResponse)?; + return Ok(Some(signed_tx)); + } + + Timer::sleep(check_every).await; + } + + let timeout_s = wait_rpc_timeout_ms / 1000; + warn!( + "Couldn't fetch the '{tx_hash:02x}' transaction hex as it hasn't appeared on the RPC node in {timeout_s}s" + ); + Ok(None) + } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -2937,12 +3948,14 @@ impl EthTxFeeDetails { fn new(gas: U256, gas_price: U256, coin: &str) -> NumConversResult { let total_fee = gas * gas_price; // Fees are always paid in ETH, can use 18 decimals by default - let total_fee = u256_to_big_decimal(total_fee, 18)?; - let gas_price = u256_to_big_decimal(gas_price, 18)?; + let total_fee = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; + let gas_price = u256_to_big_decimal(gas_price, ETH_DECIMALS)?; + + let gas_u64 = u64::try_from(gas).map_to_mm(|e| NumConversError::new(e.to_string()))?; Ok(EthTxFeeDetails { coin: coin.to_owned(), - gas: gas.into(), + gas: gas_u64, gas_price, total_fee, }) @@ -2953,10 +3966,28 @@ impl EthTxFeeDetails { impl MmCoin for EthCoin { fn is_asset_chain(&self) -> bool { false } + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(get_raw_transaction_impl(self.clone(), req).boxed().compat()) } + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { + if tx_hash.len() != H256::len_bytes() { + let error = format!( + "TX hash should have exactly {} bytes, got {}", + H256::len_bytes(), + tx_hash.len(), + ); + return Box::new(futures01::future::err(MmError::new( + RawTransactionError::InvalidHashError(error), + ))); + } + + let tx_hash = H256::from_slice(tx_hash.as_slice()); + Box::new(get_tx_hex_by_hash_impl(self.clone(), tx_hash).boxed().compat()) + } + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { Box::new(Box::pin(withdraw_impl(self.clone(), req)).compat()) } @@ -3013,14 +4044,14 @@ impl MmCoin for EthCoin { self.get_gas_price() .map_err(|e| e.to_string()) .and_then(move |gas_price| { - let fee = gas_price * U256::from(150_000); + let fee = gas_price * U256::from(ETH_GAS); let fee_coin = match &coin.coin_type { EthCoinType::Eth => &coin.ticker, EthCoinType::Erc20 { platform, .. } => platform, }; Ok(TradeFee { coin: fee_coin.into(), - amount: try_s!(u256_to_big_decimal(fee, 18)).into(), + amount: try_s!(u256_to_big_decimal(fee, ETH_DECIMALS)).into(), paid_from_trading_vol: false, }) }), @@ -3068,7 +4099,7 @@ impl MmCoin for EthCoin { }; let total_fee = gas_limit * gas_price; - let amount = u256_to_big_decimal(total_fee, 18)?; + let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; let fee_coin = match &self.coin_type { EthCoinType::Eth => &self.ticker, EthCoinType::Erc20 { platform, .. } => platform, @@ -3080,13 +4111,13 @@ impl MmCoin for EthCoin { }) } - fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, 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 total_fee = gas_price * U256::from(150_000); - let amount = u256_to_big_decimal(total_fee, 18)?; + let total_fee = gas_price * U256::from(ETH_GAS); + let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; let fee_coin = match &coin.coin_type { EthCoinType::Eth => &coin.ticker, EthCoinType::Erc20 { platform, .. } => platform, @@ -3125,18 +4156,19 @@ impl MmCoin for EthCoin { value: Some(eth_value), data: Some(data.clone().into()), from: Some(self.my_address), - to: *call_addr, + to: Some(*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), + ..CallRequest::default() }; // 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)?; + let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; Ok(TradeFee { coin: fee_coin.into(), amount: amount.into(), @@ -3161,11 +4193,23 @@ impl MmCoin for EthCoin { Some(BytesJson::from(self.swap_contract_address.0.as_ref())) } + fn fallback_swap_contract(&self) -> Option { + self.fallback_swap_contract.map(|a| BytesJson::from(a.0.as_ref())) + } + fn mature_confirmations(&self) -> Option { None } fn coin_protocol_info(&self) -> Vec { Vec::new() } fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } + + fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.abortable_system) } + + fn on_token_deactivated(&self, ticker: &str) { + if let Ok(tokens) = self.erc20_tokens_infos.lock().as_deref_mut() { + tokens.remove(ticker); + }; + } } pub trait TryToAddress { @@ -3173,7 +4217,24 @@ pub trait TryToAddress { } impl TryToAddress for BytesJson { - fn try_to_address(&self) -> Result { Ok(Address::from(self.0.as_slice())) } + fn try_to_address(&self) -> Result { self.0.try_to_address() } +} + +impl TryToAddress for [u8] { + fn try_to_address(&self) -> Result { (&self).try_to_address() } +} + +impl<'a> TryToAddress for &'a [u8] { + fn try_to_address(&self) -> Result { + if self.len() != Address::len_bytes() { + return ERR!( + "Cannot construct an Ethereum address from {} bytes slice", + Address::len_bytes() + ); + } + + Ok(Address::from_slice(self)) + } } impl TryToAddress for Option { @@ -3185,9 +4246,55 @@ impl TryToAddress for Option { } } +pub trait GuiAuthMessages { + fn gui_auth_sign_message_hash(message: String) -> Option<[u8; 32]>; + fn generate_gui_auth_signed_validation(generator: GuiAuthValidationGenerator) + -> SignatureResult; +} + +impl GuiAuthMessages for EthCoin { + fn gui_auth_sign_message_hash(message: String) -> Option<[u8; 32]> { + let message_prefix = "atomicDEX Auth Ethereum Signed Message:\n"; + let prefix_len = CompactInteger::from(message_prefix.len()); + + let mut stream = Stream::new(); + 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 generate_gui_auth_signed_validation( + generator: GuiAuthValidationGenerator, + ) -> SignatureResult { + let timestamp_message = get_utc_timestamp() + GUI_AUTH_SIGNED_MESSAGE_LIFETIME_SEC; + + let message_hash = + EthCoin::gui_auth_sign_message_hash(timestamp_message.to_string()).ok_or(SignatureError::PrefixNotFound)?; + let signature = sign(&generator.secret, &H256::from(message_hash))?; + + Ok(GuiAuthValidation { + coin_ticker: generator.coin_ticker, + address: generator.address, + timestamp_message, + signature: format!("0x{}", signature), + }) + } +} + +fn get_function_input_data(decoded: &[Token], func: &Function, index: usize) -> Result { + decoded.get(index).cloned().ok_or(format!( + "Missing input in function {}: No input found at index {}", + func.name.clone(), + index + )) +} + pub fn addr_from_raw_pubkey(pubkey: &[u8]) -> Result { let pubkey = try_s!(PublicKey::from_slice(pubkey).map_err(|e| ERRL!("{:?}", e))); - let eth_public = Public::from(&pubkey.serialize_uncompressed()[1..65]); + let eth_public = Public::from_slice(&pubkey.serialize_uncompressed()[1..65]); Ok(public_to_address(ð_public)) } @@ -3237,18 +4344,28 @@ pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResu impl Transaction for SignedEthTx { fn tx_hex(&self) -> Vec { rlp::encode(self).to_vec() } - fn tx_hash(&self) -> BytesJson { self.hash.to_vec().into() } + fn tx_hash(&self) -> BytesJson { self.hash.0.to_vec().into() } } fn signed_tx_from_web3_tx(transaction: Web3Transaction) -> Result { + let r = transaction.r.ok_or_else(|| ERRL!("'Transaction::r' is not set"))?; + let s = transaction.s.ok_or_else(|| ERRL!("'Transaction::s' is not set"))?; + let v = transaction + .v + .ok_or_else(|| ERRL!("'Transaction::v' is not set"))? + .as_u64(); + let gas_price = transaction + .gas_price + .ok_or_else(|| ERRL!("'Transaction::gas_price' is not set"))?; + let unverified = UnverifiedTransaction { - r: transaction.r, - s: transaction.s, - v: transaction.v.as_u64(), + r, + s, + v, hash: transaction.hash, unsigned: UnSignedEthTx { data: transaction.input.0, - gas_price: transaction.gas_price, + gas_price, gas: transaction.gas, value: transaction.value, nonce: transaction.nonce, @@ -3273,7 +4390,7 @@ pub struct GasStationData { /// Using tagged representation to allow adding variants with coefficients, percentage, etc in the future. #[derive(Clone, Copy, Debug, Deserialize)] #[serde(tag = "policy", content = "additional_data")] -enum GasStationPricePolicy { +pub enum GasStationPricePolicy { /// Use mean between average and fast values, default and recommended to use on ETH mainnet due to /// gas price big spikes. MeanAverageFast, @@ -3311,27 +4428,28 @@ async fn get_token_decimals(web3: &Web3, token_addr: Address) -> let data = try_s!(function.encode_input(&[])); let request = CallRequest { from: Some(Address::default()), - to: token_addr, + to: Some(token_addr), gas: None, gas_price: None, value: Some(0.into()), data: Some(data.into()), + ..CallRequest::default() }; - let f = web3 + let res = web3 .eth() - .call(request, Some(BlockNumber::Latest)) - .map_err(|e| ERRL!("{}", e)); - let res = try_s!(f.compat().await); + .call(request, Some(BlockId::Number(BlockNumber::Latest))) + .map_err(|e| ERRL!("{}", e)) + .await?; let tokens = try_s!(function.decode_output(&res.0)); - let decimals: u64 = match tokens[0] { - Token::Uint(dec) => dec.into(), + let decimals = match tokens[0] { + Token::Uint(dec) => dec.as_u64(), _ => return ERR!("Invalid decimals type {:?}", tokens), }; Ok(decimals as u8) } -fn valid_addr_from_str(addr_str: &str) -> Result { +pub fn valid_addr_from_str(addr_str: &str) -> Result { let addr = try_s!(addr_from_str(addr_str)); if !is_valid_checksum_addr(addr_str) { return ERR!("Invalid address checksum"); @@ -3347,6 +4465,34 @@ pub fn addr_from_str(addr_str: &str) -> Result { Ok(try_s!(Address::from_str(&addr_str[2..]))) } +/// This function fixes a bug appeared on `ethabi` update: +/// 1. `ethabi(6.1.0)::Function::decode_input` had +/// ```rust +/// decode(&self.input_param_types(), &data[4..]) +/// ``` +/// +/// 2. `ethabi(17.2.0)::Function::decode_input` has +/// ```rust +/// decode(&self.input_param_types(), data) +/// ``` +pub fn decode_contract_call(function: &Function, contract_call_bytes: &[u8]) -> Result, ethabi::Error> { + if contract_call_bytes.len() < 4 { + return Err(ethabi::Error::Other( + "Contract call should contain at least 4 bytes known as a function signature".into(), + )); + } + + let actual_signature = &contract_call_bytes[..4]; + let expected_signature = &function.short_signature(); + if actual_signature != expected_signature { + let error = + format!("Unexpected contract call signature: expected {expected_signature:?}, found {actual_signature:?}"); + return Err(ethabi::Error::Other(error.into())); + } + + function.decode_input(&contract_call_bytes[4..]) +} + fn rpc_event_handlers_for_eth_transport(ctx: &MmArc, ticker: String) -> Vec { let metrics = ctx.metrics.weak(); vec![CoinTransportMetrics::new(metrics, ticker, RpcClientType::Ethereum).into_shared()] @@ -3360,9 +4506,12 @@ pub async fn eth_coin_from_conf_and_request( ticker: &str, conf: &Json, req: &Json, - priv_key: &[u8], protocol: CoinProtocol, + priv_key_policy: PrivKeyBuildPolicy, ) -> Result { + // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. + let priv_key_policy = try_s!(EthPrivKeyBuildPolicy::try_from(priv_key_policy)); + let mut urls: Vec = try_s!(json::from_value(req["urls"].clone())); if urls.is_empty() { return ERR!("Enable request for ETH coin must have at least 1 node URL"); @@ -3370,6 +4519,15 @@ pub async fn eth_coin_from_conf_and_request( let mut rng = small_rng(); urls.as_mut_slice().shuffle(&mut rng); + let mut nodes = vec![]; + for url in urls.iter() { + nodes.push(HttpTransportNode { + uri: try_s!(url.parse()), + gui_auth: false, + }); + } + drop_mutability!(nodes); + let swap_contract_address: Address = try_s!(json::from_value(req["swap_contract_address"].clone())); if swap_contract_address == Address::default() { return ERR!("swap_contract_address can't be zero address"); @@ -3382,21 +4540,17 @@ pub async fn eth_coin_from_conf_and_request( } } - let key_pair: KeyPair = try_s!(KeyPair::from_secret_slice(priv_key)); - let my_address = key_pair.address(); + let (my_address, key_pair) = try_s!(build_address_and_priv_key_policy(conf, priv_key_policy).await); let mut web3_instances = vec![]; let event_handlers = rpc_event_handlers_for_eth_transport(ctx, ticker.to_string()); - for url in urls.iter() { - let transport = try_s!(Web3Transport::with_event_handlers( - vec![url.clone()], - event_handlers.clone() - )); + for node in nodes.iter() { + let transport = Web3Transport::new_http(vec![node.clone()], event_handlers.clone()); let web3 = Web3::new(transport); - let version = match web3.web3().client_version().compat().await { + let version = match web3.web3().client_version().await { Ok(v) => v, Err(e) => { - error!("Couldn't get client version for url {}: {}", url, e); + error!("Couldn't get client version for url {}: {}", node.uri, e); continue; }, }; @@ -3410,11 +4564,11 @@ pub async fn eth_coin_from_conf_and_request( return ERR!("Failed to get client version for all urls"); } - let transport = try_s!(Web3Transport::with_event_handlers(urls, event_handlers)); + let transport = Web3Transport::new_http(nodes, event_handlers); let web3 = Web3::new(transport); let (coin_type, decimals) = match protocol { - CoinProtocol::ETH => (EthCoinType::Eth, 18), + CoinProtocol::ETH => (EthCoinType::Eth, ETH_DECIMALS), CoinProtocol::ERC20 { platform, contract_address, @@ -3432,7 +4586,11 @@ pub async fn eth_coin_from_conf_and_request( // param from request should override the config let required_confirmations = req["required_confirmations"] .as_u64() - .unwrap_or_else(|| conf["required_confirmations"].as_u64().unwrap_or(1)) + .unwrap_or_else(|| { + conf["required_confirmations"] + .as_u64() + .unwrap_or(DEFAULT_REQUIRED_CONFIRMATIONS as u64) + }) .into(); if req["requires_notarization"].as_bool().is_some() { @@ -3460,8 +4618,12 @@ pub async fn eth_coin_from_conf_and_request( let nonce_lock = map.entry(key_lock).or_insert_with(new_nonce_lock).clone(); + // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, + // all spawned futures related to `ETH` coin will be aborted as well. + let abortable_system = try_s!(ctx.abortable_system.create_subsystem()); + let coin = EthCoinImpl { - key_pair, + priv_key_policy: key_pair, my_address, coin_type, sign_message_prefix, @@ -3480,6 +4642,8 @@ pub async fn eth_coin_from_conf_and_request( chain_id: conf["chain_id"].as_u64(), logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), nonce_lock, + erc20_tokens_infos: Default::default(), + abortable_system, }; Ok(EthCoin(Arc::new(coin))) } @@ -3497,7 +4661,7 @@ fn checksum_address(addr: &str) -> String { let hash = hasher.finalize(); let mut result: String = "0x".into(); for (i, c) in addr.chars().enumerate() { - if c.is_digit(10) { + if c.is_ascii_digit() { result.push(c); } else { // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md#specification @@ -3531,12 +4695,10 @@ fn get_addr_nonce(addr: Address, web3s: Vec) -> Box = web3.web3.api(); + Either::Left(parity.parity_next_nonce(addr)) } else { - web3.web3 - .eth() - .transaction_count(addr, Some(BlockNumber::Pending)) - .compat() + Either::Right(web3.web3.eth().transaction_count(addr, Some(BlockNumber::Pending))) } }) .collect(); @@ -3595,5 +4757,8 @@ fn increase_gas_price_by_stage(gas_price: U256, level: &FeeApproxStage) -> U256 FeeApproxStage::TradePreimage => { increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_TRADE_PREIMAGE) }, + FeeApproxStage::WatcherPreimage => { + increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_WATCHER_PREIMAGE) + }, } } diff --git a/mm2src/coins/eth/erc20_abi.json b/mm2src/coins/eth/erc20_abi.json new file mode 100644 index 0000000000..b690fbd479 --- /dev/null +++ b/mm2src/coins/eth/erc20_abi.json @@ -0,0 +1,279 @@ +[ + { + "constant":true, + "inputs":[ + + ], + "name":"name", + "outputs":[ + { + "name":"", + "type":"string" + } + ], + "payable":false, + "stateMutability":"view", + "type":"function" + }, + { + "constant":false, + "inputs":[ + { + "name":"_spender", + "type":"address" + }, + { + "name":"_value", + "type":"uint256" + } + ], + "name":"approve", + "outputs":[ + { + "name":"", + "type":"bool" + } + ], + "payable":false, + "stateMutability":"nonpayable", + "type":"function" + }, + { + "constant":true, + "inputs":[ + + ], + "name":"totalSupply", + "outputs":[ + { + "name":"", + "type":"uint256" + } + ], + "payable":false, + "stateMutability":"view", + "type":"function" + }, + { + "constant":false, + "inputs":[ + { + "name":"_from", + "type":"address" + }, + { + "name":"_to", + "type":"address" + }, + { + "name":"_value", + "type":"uint256" + } + ], + "name":"transferFrom", + "outputs":[ + { + "name":"", + "type":"bool" + } + ], + "payable":false, + "stateMutability":"nonpayable", + "type":"function" + }, + { + "constant":true, + "inputs":[ + + ], + "name":"decimals", + "outputs":[ + { + "name":"", + "type":"uint8" + } + ], + "payable":false, + "stateMutability":"view", + "type":"function" + }, + { + "constant":false, + "inputs":[ + { + "name":"_spender", + "type":"address" + }, + { + "name":"_subtractedValue", + "type":"uint256" + } + ], + "name":"decreaseApproval", + "outputs":[ + { + "name":"", + "type":"bool" + } + ], + "payable":false, + "stateMutability":"nonpayable", + "type":"function" + }, + { + "constant":true, + "inputs":[ + { + "name":"_owner", + "type":"address" + } + ], + "name":"balanceOf", + "outputs":[ + { + "name":"balance", + "type":"uint256" + } + ], + "payable":false, + "stateMutability":"view", + "type":"function" + }, + { + "constant":true, + "inputs":[ + + ], + "name":"symbol", + "outputs":[ + { + "name":"", + "type":"string" + } + ], + "payable":false, + "stateMutability":"view", + "type":"function" + }, + { + "constant":false, + "inputs":[ + { + "name":"_to", + "type":"address" + }, + { + "name":"_value", + "type":"uint256" + } + ], + "name":"transfer", + "outputs":[ + { + "name":"", + "type":"bool" + } + ], + "payable":false, + "stateMutability":"nonpayable", + "type":"function" + }, + { + "constant":false, + "inputs":[ + { + "name":"_spender", + "type":"address" + }, + { + "name":"_addedValue", + "type":"uint256" + } + ], + "name":"increaseApproval", + "outputs":[ + { + "name":"", + "type":"bool" + } + ], + "payable":false, + "stateMutability":"nonpayable", + "type":"function" + }, + { + "constant":true, + "inputs":[ + { + "name":"_owner", + "type":"address" + }, + { + "name":"_spender", + "type":"address" + } + ], + "name":"allowance", + "outputs":[ + { + "name":"", + "type":"uint256" + } + ], + "payable":false, + "stateMutability":"view", + "type":"function" + }, + { + "inputs":[ + + ], + "payable":false, + "stateMutability":"nonpayable", + "type":"constructor" + }, + { + "anonymous":false, + "inputs":[ + { + "indexed":true, + "name":"owner", + "type":"address" + }, + { + "indexed":true, + "name":"spender", + "type":"address" + }, + { + "indexed":false, + "name":"value", + "type":"uint256" + } + ], + "name":"Approval", + "type":"event" + }, + { + "anonymous":false, + "inputs":[ + { + "indexed":true, + "name":"from", + "type":"address" + }, + { + "indexed":true, + "name":"to", + "type":"address" + }, + { + "indexed":false, + "name":"value", + "type":"uint256" + } + ], + "name":"Transfer", + "type":"event" + } +] \ No newline at end of file diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 5c205cddad..954080fdf3 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1,6 +1,8 @@ use super::*; +use crate::IguanaPrivKey; use common::block_on; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +use mm2_test_helpers::for_tests::{ETH_MAINNET_NODE, ETH_MAINNET_SWAP_CONTRACT}; use mocktopus::mocking::*; /// The gas price for the tests @@ -12,6 +14,8 @@ const GAS_PRICE_APPROXIMATION_ON_ORDER_ISSUE: u64 = 52_500_000_000; // `GAS_PRICE` increased by 7% const GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE: u64 = 53_500_000_000; +const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; + fn check_sum(addr: &str, expected: &str) { let actual = checksum_address(addr); assert_eq!(expected, actual); @@ -26,7 +30,17 @@ fn eth_coin_for_test( &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::new(urls).unwrap(); + + let mut nodes = vec![]; + for url in urls.iter() { + nodes.push(HttpTransportNode { + uri: url.parse().unwrap(), + gui_auth: false, + }); + } + drop_mutability!(nodes); + + let transport = Web3Transport::with_nodes(nodes); let web3 = Web3::new(transport); let conf = json!({ "coins":[ @@ -49,8 +63,8 @@ fn eth_coin_for_test( gas_station_policy: GasStationPricePolicy::MeanAverageFast, my_address: key_pair.address(), sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - key_pair, - swap_contract_address: Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"), + priv_key_policy: key_pair.into(), + swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract, ticker, web3_instances: vec![Web3Instance { @@ -63,6 +77,8 @@ fn eth_coin_for_test( chain_id: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), + erc20_tokens_infos: Default::default(), + abortable_system: AbortableQueue::default(), })); (ctx, eth_coin) } @@ -202,19 +218,19 @@ fn send_and_refund_erc20_payment() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::new(vec!["http://195.201.0.6:8545".into()]).unwrap(); + let transport = Web3Transport::single_node("http://195.201.0.6:8545", false); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); let coin = EthCoin(Arc::new(EthCoinImpl { ticker: "ETH".into(), coin_type: EthCoinType::Erc20 { platform: "ETH".to_string(), - token_addr: Address::from("0xc0eb7AeD740E1796992A08962c15661bDEB58003"), + token_addr: Address::from_str("0xc0eb7AeD740E1796992A08962c15661bDEB58003").unwrap(), }, my_address: key_pair.address(), sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - key_pair, - swap_contract_address: Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"), + priv_key_policy: key_pair.into(), + swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -231,36 +247,36 @@ fn send_and_refund_erc20_payment() { chain_id: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), + erc20_tokens_infos: Default::default(), + abortable_system: AbortableQueue::default(), })); - - let payment = coin - .send_maker_payment( - (now_ms() / 1000) as u32 - 200, - &DEX_FEE_ADDR_RAW_PUBKEY, - &[1; 20], - "0.001".parse().unwrap(), - &coin.swap_contract_address(), - &[], - ) - .wait() - .unwrap(); - + let maker_payment_args = SendMakerPaymentArgs { + time_lock_duration: 0, + time_lock: (now_ms() / 1000) as u32 - 200, + other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, + secret_hash: &[1; 20], + amount: "0.001".parse().unwrap(), + swap_contract_address: &coin.swap_contract_address(), + swap_unique_data: &[], + payment_instructions: &None, + }; + let payment = coin.send_maker_payment(maker_payment_args).wait().unwrap(); log!("{:?}", payment); block_on(Timer::sleep(60.)); + let maker_refunds_payment_args = SendMakerRefundsPaymentArgs { + payment_tx: &payment.tx_hex(), + time_lock: (now_ms() / 1000) as u32 - 200, + other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, + secret_hash: &[1; 20], + swap_contract_address: &coin.swap_contract_address(), + swap_unique_data: &[], + }; let refund = coin - .send_maker_refunds_payment( - &payment.tx_hex(), - (now_ms() / 1000) as u32 - 200, - &DEX_FEE_ADDR_RAW_PUBKEY, - &[1; 20], - &coin.swap_contract_address(), - &[], - ) + .send_maker_refunds_payment(maker_refunds_payment_args) .wait() .unwrap(); - log!("{:?}", refund); } @@ -272,7 +288,7 @@ fn send_and_refund_eth_payment() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::new(vec!["http://195.201.0.6:8545".into()]).unwrap(); + let transport = Web3Transport::single_node("http://195.201.0.6:8545", false); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); let coin = EthCoin(Arc::new(EthCoinImpl { @@ -280,8 +296,8 @@ fn send_and_refund_eth_payment() { 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"), + priv_key_policy: key_pair.into(), + swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -298,33 +314,34 @@ fn send_and_refund_eth_payment() { chain_id: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), + erc20_tokens_infos: Default::default(), + abortable_system: AbortableQueue::default(), })); - - let payment = coin - .send_maker_payment( - (now_ms() / 1000) as u32 - 200, - &DEX_FEE_ADDR_RAW_PUBKEY, - &[1; 20], - "0.001".parse().unwrap(), - &coin.swap_contract_address(), - &[], - ) - .wait() - .unwrap(); + let send_maker_payment_args = SendMakerPaymentArgs { + time_lock_duration: 0, + time_lock: (now_ms() / 1000) as u32 - 200, + other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, + secret_hash: &[1; 20], + amount: "0.001".parse().unwrap(), + swap_contract_address: &coin.swap_contract_address(), + swap_unique_data: &[], + payment_instructions: &None, + }; + let payment = coin.send_maker_payment(send_maker_payment_args).wait().unwrap(); log!("{:?}", payment); block_on(Timer::sleep(60.)); - + let maker_refunds_payment_args = SendMakerRefundsPaymentArgs { + payment_tx: &payment.tx_hex(), + time_lock: (now_ms() / 1000) as u32 - 200, + other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, + secret_hash: &[1; 20], + swap_contract_address: &coin.swap_contract_address(), + swap_unique_data: &[], + }; let refund = coin - .send_maker_refunds_payment( - &payment.tx_hex(), - (now_ms() / 1000) as u32 - 200, - &DEX_FEE_ADDR_RAW_PUBKEY, - &[1; 20], - &coin.swap_contract_address(), - &[], - ) + .send_maker_refunds_payment(maker_refunds_payment_args) .wait() .unwrap(); @@ -338,13 +355,11 @@ fn test_nonce_several_urls() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let infura_transport = Web3Transport::new(vec![ - "https://ropsten.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b".into() - ]) - .unwrap(); - let linkpool_transport = Web3Transport::new(vec!["https://ropsten-rpc.linkpool.io".into()]).unwrap(); + let infura_transport = + Web3Transport::single_node("https://ropsten.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b", false); + let linkpool_transport = Web3Transport::single_node("https://ropsten-rpc.linkpool.io", false); // get nonce must succeed if some nodes are down at the moment for some reason - let failing_transport = Web3Transport::new(vec!["http://195.201.0.6:8989".into()]).unwrap(); + let failing_transport = Web3Transport::single_node("http://195.201.0.6:8989", false); let web3_infura = Web3::new(infura_transport); let web3_linkpool = Web3::new(linkpool_transport); @@ -356,8 +371,8 @@ fn test_nonce_several_urls() { 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"), + priv_key_policy: key_pair.into(), + swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, web3_instances: vec![ Web3Instance { @@ -384,6 +399,8 @@ fn test_nonce_several_urls() { chain_id: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), + erc20_tokens_infos: Default::default(), + abortable_system: AbortableQueue::default(), })); log!("My address {:?}", coin.my_address); @@ -399,14 +416,14 @@ fn test_nonce_several_urls() { #[test] fn test_wait_for_payment_spend_timeout() { - EthCoinImpl::spend_events.mock_safe(|_, _, _, _| MockResult::Return(Box::new(futures01::future::ok(vec![])))); + EthCoin::spend_events.mock_safe(|_, _, _, _| MockResult::Return(Box::new(futures01::future::ok(vec![])))); EthCoin::current_block.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(900)))); let key_pair = KeyPair::from_secret_slice( &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::new(vec!["http://195.201.0.6:8555".into()]).unwrap(); + let transport = Web3Transport::single_node("http://195.201.0.6:8555", false); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -419,8 +436,8 @@ fn test_wait_for_payment_spend_timeout() { history_sync_state: Mutex::new(HistorySyncState::NotEnabled), my_address: key_pair.address(), sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - key_pair, - swap_contract_address: Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"), + priv_key_policy: key_pair.into(), + swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, ticker: "ETH".into(), web3_instances: vec![Web3Instance { @@ -433,6 +450,8 @@ fn test_wait_for_payment_spend_timeout() { chain_id: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), + erc20_tokens_infos: Default::default(), + abortable_system: AbortableQueue::default(), }; let coin = EthCoin(Arc::new(coin)); @@ -453,7 +472,14 @@ fn test_wait_for_payment_spend_timeout() { ]; assert!(coin - .wait_for_tx_spend(&tx_bytes, wait_until, from_block, &coin.swap_contract_address()) + .wait_for_htlc_tx_spend( + &tx_bytes, + &[], + wait_until, + from_block, + &coin.swap_contract_address(), + TAKER_PAYMENT_SPEND_SEARCH_INTERVAL + ) .wait() .is_err()); } @@ -466,14 +492,11 @@ fn test_search_for_swap_tx_spend_was_spent() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::new(vec![ - "https://ropsten.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b".into() - ]) - .unwrap(); + let transport = Web3Transport::single_node(ETH_MAINNET_NODE, false); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); - let swap_contract_address = Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"); + let swap_contract_address = Address::from_str(ETH_MAINNET_SWAP_CONTRACT).unwrap(); let coin = EthCoin(Arc::new(EthCoinImpl { coin_type: EthCoinType::Eth, decimals: 18, @@ -483,13 +506,13 @@ fn test_search_for_swap_tx_spend_was_spent() { history_sync_state: Mutex::new(HistorySyncState::NotEnabled), my_address: key_pair.address(), sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - key_pair, + priv_key_policy: key_pair.into(), swap_contract_address, fallback_swap_contract: None, ticker: "ETH".into(), web3_instances: vec![Web3Instance { web3: web3.clone(), - is_parity: true, + is_parity: false, }], web3, ctx: ctx.weak(), @@ -497,37 +520,40 @@ fn test_search_for_swap_tx_spend_was_spent() { chain_id: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), + erc20_tokens_infos: Default::default(), + abortable_system: AbortableQueue::default(), })); - // raw transaction bytes of https://ropsten.etherscan.io/tx/0xb1c987e2ac79581bb8718267b5cb49a18274890494299239d1d0dfdb58d6d76a + // raw transaction bytes of https://etherscan.io/tx/0x2814718945e90fe4301e2a74eaaa46b4fdbdba1536e1d94e3b0bd665b2dd091d let payment_tx = [ - 248, 240, 52, 132, 119, 53, 148, 0, 131, 2, 73, 240, 148, 123, 193, 187, 221, 106, 10, 114, 47, 201, 191, 252, - 73, 201, 33, 182, 133, 236, 184, 75, 148, 135, 71, 13, 228, 223, 130, 0, 0, 184, 132, 21, 44, 243, 175, 188, - 96, 248, 252, 165, 132, 81, 30, 243, 34, 85, 165, 46, 224, 176, 90, 137, 30, 19, 123, 224, 67, 83, 53, 74, 57, - 148, 140, 95, 45, 70, 147, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 117, 244, 28, 175, 51, 95, 91, 184, 141, 201, - 45, 116, 26, 102, 210, 119, 151, 124, 143, 52, 215, 128, 89, 116, 30, 25, 35, 128, 122, 186, 177, 228, 149, - 250, 55, 53, 62, 196, 51, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 56, 62, 80, 28, 160, 65, 22, 195, 212, 184, 202, 226, 151, 224, 111, - 174, 31, 160, 219, 39, 69, 137, 37, 8, 127, 177, 4, 104, 248, 27, 41, 245, 176, 131, 188, 215, 136, 160, 91, - 134, 199, 67, 1, 58, 57, 103, 23, 215, 176, 64, 124, 1, 44, 88, 161, 200, 160, 64, 110, 13, 145, 127, 180, 27, - 171, 131, 253, 90, 48, 147, + 248, 241, 1, 133, 8, 158, 68, 19, 192, 131, 2, 73, 240, 148, 36, 171, 228, 199, 31, 198, 88, 201, 19, 19, 182, + 85, 44, 212, 12, 216, 8, 179, 234, 128, 135, 29, 133, 195, 185, 99, 4, 0, 184, 132, 21, 44, 243, 175, 130, 126, + 209, 71, 198, 107, 13, 87, 207, 36, 150, 22, 77, 57, 198, 35, 248, 38, 203, 5, 242, 55, 219, 79, 252, 124, 162, + 67, 251, 160, 210, 247, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 229, 230, 210, 113, 0, 71, 77, 52, 204, 15, 135, + 238, 56, 119, 86, 57, 80, 25, 1, 156, 70, 83, 37, 132, 127, 196, 109, 164, 129, 132, 149, 187, 70, 120, 38, 83, + 173, 7, 235, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 54, 210, 77, 38, 160, 254, 78, 202, 143, 121, 136, 202, 110, 251, 121, 110, 25, + 124, 62, 205, 40, 168, 154, 212, 180, 118, 59, 28, 135, 255, 44, 20, 62, 49, 109, 170, 215, 160, 72, 251, 237, + 69, 215, 60, 8, 59, 204, 150, 18, 163, 242, 159, 79, 115, 146, 19, 78, 61, 142, 91, 221, 195, 178, 80, 197, + 162, 242, 179, 182, 235, ]; - // raw transaction bytes of https://ropsten.etherscan.io/tx/0xcb7c14d3ff309996d582400369393b6fa42314c52245115d4a3f77f072c36da9 + + // raw transaction bytes of https://etherscan.io/tx/0xe9c2c8126e8b947eb3bbc6008ef9e3880e7c54f5bc5ccdc34ad412c4d271c76b let spend_tx = [ - 249, 1, 9, 37, 132, 119, 53, 148, 0, 131, 2, 73, 240, 148, 123, 193, 187, 221, 106, 10, 114, 47, 201, 191, 252, - 73, 201, 33, 182, 133, 236, 184, 75, 148, 128, 184, 164, 2, 237, 41, 43, 188, 96, 248, 252, 165, 132, 81, 30, - 243, 34, 85, 165, 46, 224, 176, 90, 137, 30, 19, 123, 224, 67, 83, 53, 74, 57, 148, 140, 95, 45, 70, 147, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 13, 228, 223, 130, 0, 0, 168, 151, 11, - 232, 224, 253, 63, 180, 26, 114, 23, 184, 27, 10, 161, 80, 178, 251, 73, 204, 80, 174, 97, 118, 149, 204, 186, - 187, 243, 185, 19, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 157, 73, 251, 238, 138, 245, 142, 240, 85, 44, 209, 63, 194, 242, - 109, 242, 246, 6, 76, 176, 27, 160, 29, 157, 226, 23, 81, 174, 34, 82, 93, 182, 41, 248, 119, 42, 221, 214, 38, - 243, 128, 2, 235, 208, 193, 192, 74, 208, 242, 26, 221, 83, 54, 74, 160, 111, 29, 92, 8, 75, 61, 97, 103, 199, - 100, 189, 72, 74, 221, 144, 66, 170, 68, 121, 29, 105, 19, 194, 35, 245, 196, 131, 236, 29, 105, 101, 30, + 249, 1, 10, 4, 133, 8, 154, 252, 216, 0, 131, 2, 73, 240, 148, 36, 171, 228, 199, 31, 198, 88, 201, 19, 19, + 182, 85, 44, 212, 12, 216, 8, 179, 234, 128, 128, 184, 164, 2, 237, 41, 43, 130, 126, 209, 71, 198, 107, 13, + 87, 207, 36, 150, 22, 77, 57, 198, 35, 248, 38, 203, 5, 242, 55, 219, 79, 252, 124, 162, 67, 251, 160, 210, + 247, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 133, 195, 185, 99, 4, 0, + 50, 250, 104, 200, 70, 202, 119, 58, 239, 14, 250, 118, 21, 252, 240, 40, 50, 95, 151, 187, 141, 226, 240, 198, + 32, 99, 37, 100, 241, 251, 122, 89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 82, 6, 91, 85, 191, 21, 5, 181, 176, 40, 104, 25, + 86, 135, 213, 121, 230, 186, 218, 38, 160, 19, 239, 26, 4, 109, 84, 68, 160, 43, 178, 4, 249, 52, 209, 146, 13, + 53, 179, 63, 117, 17, 184, 115, 83, 75, 59, 89, 18, 198, 47, 37, 101, 160, 85, 163, 23, 247, 219, 101, 69, 138, + 8, 152, 81, 205, 76, 253, 225, 123, 167, 12, 147, 151, 215, 248, 198, 91, 254, 47, 99, 203, 102, 5, 212, 217, ]; let spend_tx = FoundSwapTxSpend::Spent(signed_eth_tx_from_bytes(&spend_tx).unwrap().into()); - let found_tx = block_on(coin.search_for_swap_tx_spend(&payment_tx, swap_contract_address, 6051857)) + let found_tx = block_on(coin.search_for_swap_tx_spend(&payment_tx, swap_contract_address, &[0; 20], 15643279)) .unwrap() .unwrap(); assert_eq!(spend_tx, found_tx); @@ -574,18 +600,15 @@ fn test_search_for_swap_tx_spend_was_refunded() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::new(vec![ - "https://ropsten.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b".into() - ]) - .unwrap(); + let transport = Web3Transport::single_node(ETH_MAINNET_NODE, false); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); - let swap_contract_address = Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"); + let swap_contract_address = Address::from_str(ETH_MAINNET_SWAP_CONTRACT).unwrap(); let coin = EthCoin(Arc::new(EthCoinImpl { coin_type: EthCoinType::Erc20 { platform: "ETH".to_string(), - token_addr: Address::from("0xc0eb7aed740e1796992a08962c15661bdeb58003"), + token_addr: Address::from_str("0x0D8775F648430679A709E98d2b0Cb6250d2887EF").unwrap(), }, decimals: 18, gas_station_url: None, @@ -594,13 +617,13 @@ fn test_search_for_swap_tx_spend_was_refunded() { history_sync_state: Mutex::new(HistorySyncState::NotEnabled), my_address: key_pair.address(), sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - key_pair, + priv_key_policy: key_pair.into(), swap_contract_address, fallback_swap_contract: None, - ticker: "ETH".into(), + ticker: "BAT".into(), web3_instances: vec![Web3Instance { web3: web3.clone(), - is_parity: true, + is_parity: false, }], web3, ctx: ctx.weak(), @@ -608,38 +631,43 @@ fn test_search_for_swap_tx_spend_was_refunded() { chain_id: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), + erc20_tokens_infos: Default::default(), + abortable_system: AbortableQueue::default(), })); - // raw transaction bytes of https://ropsten.etherscan.io/tx/0xe18bbca69dea9a4624e1f5b0b2021d5fe4c8daa03f36084a8ba011b08e5cd938 + // raw transaction bytes of https://etherscan.io/tx/0x02c261dcb1c8615c029b9abc712712b80ef8c1ef20d2cbcdd9bde859e7913476 let payment_tx = [ - 249, 1, 43, 130, 10, 96, 132, 149, 2, 249, 0, 131, 2, 73, 240, 148, 123, 193, 187, 221, 106, 10, 114, 47, 201, - 191, 252, 73, 201, 33, 182, 133, 236, 184, 75, 148, 128, 184, 196, 155, 65, 91, 42, 192, 158, 192, 175, 210, - 198, 159, 244, 116, 46, 255, 28, 236, 147, 240, 68, 91, 16, 19, 6, 59, 187, 149, 138, 179, 151, 121, 47, 14, - 80, 251, 147, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 141, 126, 164, 198, - 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 235, 122, 237, 116, 14, 23, 150, 153, 42, 8, 150, 44, 21, 102, - 27, 222, 181, 128, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 216, 153, 121, 65, 221, 19, 70, 233, 35, 17, 24, 213, - 104, 93, 134, 98, 148, 245, 158, 91, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, - 23, 98, 207, 27, 160, 4, 198, 61, 242, 141, 248, 157, 72, 229, 2, 162, 163, 250, 159, 26, 66, 37, 42, 159, 35, - 58, 94, 57, 121, 252, 166, 34, 25, 206, 193, 113, 198, 160, 68, 125, 142, 153, 210, 177, 60, 173, 67, 127, 138, - 52, 112, 9, 49, 108, 109, 44, 177, 142, 9, 124, 10, 200, 37, 100, 52, 137, 196, 74, 67, 192, + 249, 1, 42, 25, 133, 26, 13, 225, 144, 65, 131, 2, 73, 240, 148, 36, 171, 228, 199, 31, 198, 88, 201, 19, 19, + 182, 85, 44, 212, 12, 216, 8, 179, 234, 128, 128, 184, 196, 155, 65, 91, 42, 22, 125, 52, 19, 176, 17, 106, + 187, 142, 153, 244, 194, 212, 205, 57, 166, 77, 249, 188, 153, 80, 0, 108, 74, 232, 132, 82, 114, 88, 36, 125, + 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 240, 91, 89, 211, 178, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 135, 117, 246, 72, 67, 6, 121, 167, 9, 233, 141, 43, 12, 182, 37, 13, 40, + 135, 239, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 18, 103, 159, 197, 230, 51, 138, 82, 9, 138, 176, 149, 190, + 225, 233, 161, 91, 198, 48, 186, 149, 40, 18, 123, 207, 245, 36, 103, 114, 54, 243, 115, 156, 239, 1, 51, 17, + 244, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 97, 150, 38, 250, 37, 160, 177, 67, 137, 53, 80, 200, 208, 22, 66, 120, 249, 77, 95, 165, 27, + 167, 30, 61, 254, 250, 17, 46, 111, 83, 165, 117, 188, 180, 148, 99, 58, 7, 160, 12, 198, 11, 101, 228, 74, + 229, 5, 50, 87, 185, 28, 16, 35, 182, 55, 163, 141, 135, 255, 195, 44, 130, 37, 145, 39, 90, 98, 131, 205, 110, + 197, ]; - // raw transaction bytes of https://ropsten.etherscan.io/tx/0x9a50ac4d1737f4f04b94177996da7fa942b09469de52cfdadce891cd85afc37c + + // raw transaction bytes of https://etherscan.io/tx/0x3ce6a40d7ad41bd24055cf4cdd564d42d2f36095ec8b6180717b4f0a922a97f4 let refund_tx = [ - 249, 1, 11, 130, 10, 97, 132, 149, 2, 249, 0, 131, 2, 73, 240, 148, 123, 193, 187, 221, 106, 10, 114, 47, 201, - 191, 252, 73, 201, 33, 182, 133, 236, 184, 75, 148, 128, 184, 164, 70, 252, 2, 148, 192, 158, 192, 175, 210, - 198, 159, 244, 116, 46, 255, 28, 236, 147, 240, 68, 91, 16, 19, 6, 59, 187, 149, 138, 179, 151, 121, 47, 14, - 80, 251, 147, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 141, 126, 164, 198, - 128, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 235, 122, 237, 116, 14, 23, 150, 153, 42, 8, 150, 44, 21, 102, 27, 222, 181, - 128, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 216, 153, 121, 65, 221, 19, 70, 233, 35, 17, 24, 213, 104, 93, 134, - 98, 148, 245, 158, 91, 28, 160, 127, 220, 190, 77, 221, 188, 140, 162, 198, 6, 127, 102, 222, 66, 38, 96, 10, - 19, 27, 208, 119, 219, 60, 231, 2, 118, 91, 169, 99, 78, 209, 135, 160, 51, 115, 90, 189, 124, 172, 205, 134, - 203, 159, 238, 40, 39, 99, 88, 48, 160, 189, 37, 60, 20, 117, 65, 238, 36, 98, 226, 48, 22, 235, 86, 183, + 249, 1, 10, 26, 133, 25, 252, 245, 23, 130, 131, 2, 73, 240, 148, 36, 171, 228, 199, 31, 198, 88, 201, 19, 19, + 182, 85, 44, 212, 12, 216, 8, 179, 234, 128, 128, 184, 164, 70, 252, 2, 148, 22, 125, 52, 19, 176, 17, 106, + 187, 142, 153, 244, 194, 212, 205, 57, 166, 77, 249, 188, 153, 80, 0, 108, 74, 232, 132, 82, 114, 88, 36, 125, + 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 240, 91, 89, 211, 178, 0, 0, + 186, 149, 40, 18, 123, 207, 245, 36, 103, 114, 54, 243, 115, 156, 239, 1, 51, 17, 244, 32, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 135, 117, 246, 72, 67, 6, 121, 167, 9, 233, 141, 43, 12, + 182, 37, 13, 40, 135, 239, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 18, 103, 159, 197, 230, 51, 138, 82, 9, 138, + 176, 149, 190, 225, 233, 161, 91, 198, 48, 37, 160, 175, 56, 178, 83, 9, 93, 241, 61, 203, 189, 163, 249, 203, + 143, 126, 176, 116, 113, 203, 21, 88, 19, 135, 218, 207, 185, 178, 234, 185, 244, 250, 183, 160, 17, 135, 205, + 189, 131, 59, 111, 198, 16, 171, 98, 33, 59, 51, 31, 161, 162, 89, 71, 50, 160, 165, 114, 149, 47, 219, 82, 29, + 183, 80, 80, 157, ]; let refund_tx = FoundSwapTxSpend::Refunded(signed_eth_tx_from_bytes(&refund_tx).unwrap().into()); - let found_tx = block_on(coin.search_for_swap_tx_spend(&payment_tx, swap_contract_address, 5886908)) + let found_tx = block_on(coin.search_for_swap_tx_spend(&payment_tx, swap_contract_address, &[0; 20], 13638713)) .unwrap() .unwrap(); assert_eq!(refund_tx, found_tx); @@ -662,7 +690,7 @@ fn test_withdraw_impl_manual_fee() { coin: "ETH".to_string(), max: false, fee: Some(WithdrawFee::EthGas { - gas: 150000, + gas: ETH_GAS, gas_price: 1.into(), }), memo: None, @@ -674,7 +702,7 @@ fn test_withdraw_impl_manual_fee() { EthTxFeeDetails { coin: "ETH".into(), gas_price: "0.000000001".parse().unwrap(), - gas: 150000, + gas: ETH_GAS, total_fee: "0.00015".parse().unwrap(), } .into(), @@ -687,7 +715,7 @@ fn test_withdraw_impl_fee_details() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { platform: "ETH".to_string(), - token_addr: Address::from("0x2b294F029Fde858b2c62184e8390591755521d8E"), + token_addr: Address::from_str("0x2b294F029Fde858b2c62184e8390591755521d8E").unwrap(), }, vec!["http://dummy.dummy".into()], None, @@ -706,7 +734,7 @@ fn test_withdraw_impl_fee_details() { coin: "JST".to_string(), max: false, fee: Some(WithdrawFee::EthGas { - gas: 150000, + gas: ETH_GAS, gas_price: 1.into(), }), memo: None, @@ -718,7 +746,7 @@ fn test_withdraw_impl_fee_details() { EthTxFeeDetails { coin: "ETH".into(), gas_price: "0.000000001".parse().unwrap(), - gas: 150000, + gas: ETH_GAS, total_fee: "0.00015".parse().unwrap(), } .into(), @@ -737,14 +765,15 @@ fn test_nonce_lock() { let (ctx, coin) = eth_coin_for_test(EthCoinType::Eth, vec!["http://195.201.0.6:8565".into()], None); let mut futures = vec![]; for _ in 0..5 { - futures.push(sign_and_send_transaction_impl( - ctx.clone(), - coin.clone(), - 1000000000000u64.into(), - Action::Call(coin.my_address), - vec![], - 21000.into(), - )); + futures.push( + coin.sign_and_send_transaction( + 1000000000000u64.into(), + Action::Call(coin.my_address), + vec![], + 21000.into(), + ) + .compat(), + ); } let results = block_on(join_all(futures)); for result in results { @@ -778,7 +807,7 @@ fn test_add_ten_pct_one_gwei() { fn get_sender_trade_preimage() { /// Trade fee for the ETH coin is `2 * 150_000 * gas_price` always. fn expected_fee(gas_price: u64) -> TradeFee { - let amount = u256_to_big_decimal((2 * 150_000 * gas_price).into(), 18).expect("!u256_to_big_decimal"); + let amount = u256_to_big_decimal((2 * ETH_GAS * gas_price).into(), 18).expect("!u256_to_big_decimal"); TradeFee { coin: "ETH".to_owned(), amount: amount.into(), @@ -827,7 +856,7 @@ fn get_erc20_sender_trade_preimage() { .mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok(unsafe { ALLOWANCE.into() })))); EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); - EthCoinImpl::estimate_gas.mock_safe(|_, _| { + EthCoin::estimate_gas.mock_safe(|_, _| { unsafe { ESTIMATE_GAS_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(APPROVE_GAS_LIMIT.into()))) }); @@ -905,7 +934,7 @@ fn get_receiver_trade_preimage() { EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, vec!["http://dummy.dummy".into()], None); - let amount = u256_to_big_decimal((150_000 * GAS_PRICE).into(), 18).expect("!u256_to_big_decimal"); + let amount = u256_to_big_decimal((ETH_GAS * GAS_PRICE).into(), 18).expect("!u256_to_big_decimal"); let expected_fee = TradeFee { coin: "ETH".to_owned(), amount: amount.into(), @@ -913,7 +942,7 @@ fn get_receiver_trade_preimage() { }; let actual = coin - .get_receiver_trade_fee(FeeApproxStage::WithoutApprox) + .get_receiver_trade_fee(Default::default(), FeeApproxStage::WithoutApprox) .wait() .expect("!get_sender_trade_fee"); assert_eq!(actual, expected_fee); @@ -925,7 +954,7 @@ fn test_get_fee_to_send_taker_fee() { const TRANSFER_GAS_LIMIT: u64 = 40_000; EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); - EthCoinImpl::estimate_gas + EthCoin::estimate_gas .mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok(TRANSFER_GAS_LIMIT.into())))); // fee to send taker fee is `TRANSFER_GAS_LIMIT * gas_price` always. @@ -946,7 +975,7 @@ fn test_get_fee_to_send_taker_fee() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { platform: "ETH".to_string(), - token_addr: Address::from("0xaD22f63404f7305e4713CcBd4F296f34770513f4"), + token_addr: Address::from_str("0xaD22f63404f7305e4713CcBd4F296f34770513f4").unwrap(), }, vec!["http://dummy.dummy".into()], None, @@ -971,9 +1000,9 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { platform: "ETH".to_string(), - token_addr: Address::from("0xaD22f63404f7305e4713CcBd4F296f34770513f4"), + token_addr: Address::from_str("0xaD22f63404f7305e4713CcBd4F296f34770513f4").unwrap(), }, - vec!["http://eth1.cipig.net:8555".into()], + vec![ETH_MAINNET_NODE.into()], None, ); let dex_fee_amount = u256_to_big_decimal(DEX_FEE_AMOUNT.into(), 18).expect("!u256_to_big_decimal"); @@ -989,35 +1018,25 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { #[test] fn validate_dex_fee_invalid_sender_eth() { - let (_ctx, coin) = eth_coin_for_test( - EthCoinType::Eth, - vec!["https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b".into()], - None, - ); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, vec![ETH_MAINNET_NODE.into()], None); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f - let tx = coin - .web3 - .eth() - .transaction(TransactionId::Hash( - "0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f".into(), - )) - .wait() - .unwrap() - .unwrap(); + let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + H256::from_str("0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f").unwrap(), + ))) + .unwrap() + .unwrap(); let tx = signed_tx_from_web3_tx(tx).unwrap().into(); let amount: BigDecimal = "0.000526435076465".parse().unwrap(); - let validate_err = coin - .validate_fee( - &tx, - &*DEX_FEE_ADDR_RAW_PUBKEY, - &*DEX_FEE_ADDR_RAW_PUBKEY, - &amount, - 0, - &[], - ) - .wait() - .unwrap_err(); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &*DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 0, + uuid: &[], + }; + let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); assert!(validate_err.contains("was sent from wrong address")); } @@ -1026,35 +1045,29 @@ fn validate_dex_fee_invalid_sender_erc() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { platform: "ETH".to_string(), - token_addr: "0xa1d6df714f91debf4e0802a542e13067f31b8262".into(), + token_addr: Address::from_str("0xa1d6df714f91debf4e0802a542e13067f31b8262").unwrap(), }, - vec!["http://eth1.cipig.net:8555".into()], + vec![ETH_MAINNET_NODE.into()], None, ); // the real dex fee sent on mainnet // https://etherscan.io/tx/0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600 - let tx = coin - .web3 - .eth() - .transaction(TransactionId::Hash( - "0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600".into(), - )) - .wait() - .unwrap() - .unwrap(); + let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + H256::from_str("0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600").unwrap(), + ))) + .unwrap() + .unwrap(); let tx = signed_tx_from_web3_tx(tx).unwrap().into(); let amount: BigDecimal = "5.548262548262548262".parse().unwrap(); - let validate_err = coin - .validate_fee( - &tx, - &*DEX_FEE_ADDR_RAW_PUBKEY, - &*DEX_FEE_ADDR_RAW_PUBKEY, - &amount, - 0, - &[], - ) - .wait() - .unwrap_err(); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &*DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 0, + uuid: &[], + }; + let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); assert!(validate_err.contains("was sent from wrong address")); } @@ -1062,44 +1075,34 @@ fn sender_compressed_pub(tx: &SignedEthTx) -> [u8; 33] { let tx_pubkey = tx.public.unwrap(); let mut raw_pubkey = [0; 65]; raw_pubkey[0] = 0x04; - raw_pubkey[1..].copy_from_slice(&tx_pubkey); + raw_pubkey[1..].copy_from_slice(tx_pubkey.as_bytes()); let secp_public = PublicKey::from_slice(&raw_pubkey).unwrap(); secp_public.serialize() } #[test] fn validate_dex_fee_eth_confirmed_before_min_block() { - let (_ctx, coin) = eth_coin_for_test( - EthCoinType::Eth, - vec!["https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b".into()], - None, - ); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, vec![ETH_MAINNET_NODE.into()], None); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f - let tx = coin - .web3 - .eth() - .transaction(TransactionId::Hash( - "0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f".into(), - )) - .wait() - .unwrap() - .unwrap(); + let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + H256::from_str("0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f").unwrap(), + ))) + .unwrap() + .unwrap(); let tx = signed_tx_from_web3_tx(tx).unwrap(); let compressed_public = sender_compressed_pub(&tx); 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, - &[], - ) - .wait() - .unwrap_err(); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &compressed_public, + fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 11784793, + uuid: &[], + }; + let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); assert!(validate_err.contains("confirmed before min_block")); } @@ -1108,44 +1111,38 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { platform: "ETH".to_string(), - token_addr: "0xa1d6df714f91debf4e0802a542e13067f31b8262".into(), + token_addr: Address::from_str("0xa1d6df714f91debf4e0802a542e13067f31b8262").unwrap(), }, - vec!["http://eth1.cipig.net:8555".into()], + vec![ETH_MAINNET_NODE.into()], None, ); // the real dex fee sent on mainnet // https://etherscan.io/tx/0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600 - let tx = coin - .web3 - .eth() - .transaction(TransactionId::Hash( - "0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600".into(), - )) - .wait() - .unwrap() - .unwrap(); + let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + H256::from_str("0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600").unwrap(), + ))) + .unwrap() + .unwrap(); let tx = signed_tx_from_web3_tx(tx).unwrap(); let compressed_public = sender_compressed_pub(&tx); 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, - &[], - ) - .wait() - .unwrap_err(); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &compressed_public, + fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 11823975, + uuid: &[], + }; + let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); assert!(validate_err.contains("confirmed before min_block")); } #[test] fn test_negotiate_swap_contract_addr_no_fallback() { - let (_, coin) = eth_coin_for_test(EthCoinType::Eth, vec!["http://eth1.cipig.net:8555".into()], None); + let (_, coin) = eth_coin_for_test(EthCoinType::Eth, vec![ETH_MAINNET_NODE.into()], None); let input = None; let error = coin.negotiate_swap_contract_addr(input).unwrap_err().into_inner(); @@ -1172,17 +1169,13 @@ fn test_negotiate_swap_contract_addr_no_fallback() { #[test] fn test_negotiate_swap_contract_addr_has_fallback() { - let fallback = "0x8500AFc0bc5214728082163326C2FF0C73f4a871".into(); + let fallback = Address::from_str("0x8500AFc0bc5214728082163326C2FF0C73f4a871").unwrap(); - let (_, coin) = eth_coin_for_test( - EthCoinType::Eth, - vec!["http://eth1.cipig.net:8555".into()], - Some(fallback), - ); + let (_, coin) = eth_coin_for_test(EthCoinType::Eth, vec![ETH_MAINNET_NODE.into()], Some(fallback)); let input = None; let result = coin.negotiate_swap_contract_addr(input).unwrap(); - assert_eq!(Some(fallback.to_vec().into()), result); + assert_eq!(Some(fallback.0.to_vec().into()), result); let slice: &[u8] = &[1; 1]; let error = coin.negotiate_swap_contract_addr(Some(slice)).unwrap_err().into_inner(); @@ -1204,7 +1197,7 @@ fn test_negotiate_swap_contract_addr_has_fallback() { let slice: &[u8] = fallback.as_ref(); let result = coin.negotiate_swap_contract_addr(Some(slice)).unwrap(); - assert_eq!(Some(fallback.to_vec().into()), result); + assert_eq!(Some(fallback.0.to_vec().into()), result); } #[test] @@ -1232,14 +1225,14 @@ fn polygon_check_if_my_payment_sent() { "swap_contract_address": "0x9130b257d37a52e52f21054c4da3450c72f595ce", }); - let priv_key = [1; 32]; + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(IguanaPrivKey::from([1; 32])); let coin = block_on(eth_coin_from_conf_and_request( &ctx, "MATIC", &conf, &request, - &priv_key, CoinProtocol::ETH, + priv_key_policy, )) .unwrap(); @@ -1247,15 +1240,18 @@ fn polygon_check_if_my_payment_sent() { let secret_hash = hex::decode("fc33114b389f0ee1212abf2867e99e89126f4860").unwrap(); let swap_contract_address = "9130b257d37a52e52f21054c4da3450c72f595ce".into(); + let if_my_payment_sent_args = CheckIfMyPaymentSentArgs { + time_lock: 1638764369, + other_pub: &[], + secret_hash: &secret_hash, + search_from_block: 22185109, + swap_contract_address: &Some(swap_contract_address), + swap_unique_data: &[], + amount: &BigDecimal::default(), + payment_instructions: &None, + }; let my_payment = coin - .check_if_my_payment_sent( - 1638764369, - &[], - &secret_hash, - 22185109, - &Some(swap_contract_address), - &[], - ) + .check_if_my_payment_sent(if_my_payment_sent_args) .wait() .unwrap() .unwrap(); @@ -1269,7 +1265,7 @@ fn test_message_hash() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::new(vec!["http://195.201.0.6:8545".into()]).unwrap(); + let transport = Web3Transport::single_node("http://195.201.0.6:8545", false); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); let coin = EthCoin(Arc::new(EthCoinImpl { @@ -1277,8 +1273,8 @@ fn test_message_hash() { 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"), + priv_key_policy: key_pair.into(), + swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -1295,6 +1291,8 @@ fn test_message_hash() { chain_id: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), + erc20_tokens_infos: Default::default(), + abortable_system: AbortableQueue::default(), })); let message_hash = coin.sign_message_hash("test").unwrap(); @@ -1310,7 +1308,8 @@ fn test_sign_verify_message() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::new(vec!["http://195.201.0.6:8545".into()]).unwrap(); + let transport = Web3Transport::single_node("http://195.201.0.6:8545", false); + let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); let coin = EthCoin(Arc::new(EthCoinImpl { @@ -1318,8 +1317,8 @@ fn test_sign_verify_message() { 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"), + priv_key_policy: key_pair.into(), + swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -1336,6 +1335,8 @@ fn test_sign_verify_message() { chain_id: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), + erc20_tokens_infos: Default::default(), + abortable_system: AbortableQueue::default(), })); let message = "test"; @@ -1347,3 +1348,137 @@ fn test_sign_verify_message() { .unwrap(); assert!(is_valid); } + +#[test] +fn test_eth_extract_secret() { + let key_pair = KeyPair::from_secret_slice( + &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), + ) + .unwrap(); + let transport = Web3Transport::single_node("https://ropsten.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b", false); + let web3 = Web3::new(transport); + let ctx = MmCtxBuilder::new().into_mm_arc(); + + let swap_contract_address = Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(); + let coin = EthCoin(Arc::new(EthCoinImpl { + coin_type: EthCoinType::Erc20 { + platform: "ETH".to_string(), + token_addr: Address::from_str("0xc0eb7aed740e1796992a08962c15661bdeb58003").unwrap(), + }, + decimals: 18, + gas_station_url: None, + gas_station_decimals: ETH_GAS_STATION_DECIMALS, + gas_station_policy: GasStationPricePolicy::MeanAverageFast, + history_sync_state: Mutex::new(HistorySyncState::NotEnabled), + my_address: key_pair.address(), + sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), + priv_key_policy: key_pair.into(), + swap_contract_address, + fallback_swap_contract: None, + ticker: "ETH".into(), + web3_instances: vec![Web3Instance { + web3: web3.clone(), + is_parity: true, + }], + web3, + ctx: ctx.weak(), + required_confirmations: 1.into(), + chain_id: None, + logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, + nonce_lock: new_nonce_lock(), + erc20_tokens_infos: Default::default(), + abortable_system: AbortableQueue::default(), + })); + + // raw transaction bytes of https://ropsten.etherscan.io/tx/0xcb7c14d3ff309996d582400369393b6fa42314c52245115d4a3f77f072c36da9 + let tx_bytes = &[ + 249, 1, 9, 37, 132, 119, 53, 148, 0, 131, 2, 73, 240, 148, 123, 193, 187, 221, 106, 10, 114, 47, 201, 191, 252, + 73, 201, 33, 182, 133, 236, 184, 75, 148, 128, 184, 164, 2, 237, 41, 43, 188, 96, 248, 252, 165, 132, 81, 30, + 243, 34, 85, 165, 46, 224, 176, 90, 137, 30, 19, 123, 224, 67, 83, 53, 74, 57, 148, 140, 95, 45, 70, 147, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 13, 228, 223, 130, 0, 0, 168, 151, 11, + 232, 224, 253, 63, 180, 26, 114, 23, 184, 27, 10, 161, 80, 178, 251, 73, 204, 80, 174, 97, 118, 149, 204, 186, + 187, 243, 185, 19, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 157, 73, 251, 238, 138, 245, 142, 240, 85, 44, 209, 63, 194, 242, + 109, 242, 246, 6, 76, 176, 27, 160, 29, 157, 226, 23, 81, 174, 34, 82, 93, 182, 41, 248, 119, 42, 221, 214, 38, + 243, 128, 2, 235, 208, 193, 192, 74, 208, 242, 26, 221, 83, 54, 74, 160, 111, 29, 92, 8, 75, 61, 97, 103, 199, + 100, 189, 72, 74, 221, 144, 66, 170, 68, 121, 29, 105, 19, 194, 35, 245, 196, 131, 236, 29, 105, 101, 30, + ]; + + let secret = block_on(coin.extract_secret(&[0u8; 20], tx_bytes.as_slice())); + assert!(secret.is_ok()); + let expect_secret = &[ + 168, 151, 11, 232, 224, 253, 63, 180, 26, 114, 23, 184, 27, 10, 161, 80, 178, 251, 73, 204, 80, 174, 97, 118, + 149, 204, 186, 187, 243, 185, 19, 128, + ]; + assert_eq!(expect_secret.as_slice(), &secret.unwrap()); + + // Test for unexpected contract signature + // raw transaction bytes of ethPayment contract https://etherscan + // .io/tx/0x0869be3e5d4456a29d488a533ad6c118620fef450f36778aecf31d356ff8b41f + let tx_bytes = [ + 248, 240, 3, 133, 1, 42, 5, 242, 0, 131, 2, 73, 240, 148, 133, 0, 175, 192, 188, 82, 20, 114, 128, 130, 22, 51, + 38, 194, 255, 12, 115, 244, 168, 113, 135, 110, 205, 245, 24, 127, 34, 254, 184, 132, 21, 44, 243, 175, 73, 33, + 143, 82, 117, 16, 110, 27, 133, 82, 200, 114, 233, 42, 140, 198, 35, 21, 201, 249, 187, 180, 20, 46, 148, 40, + 9, 228, 193, 130, 71, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 152, 41, 132, 9, 201, 73, 19, 94, 237, 137, 35, + 61, 4, 194, 207, 239, 152, 75, 175, 245, 157, 174, 10, 214, 161, 207, 67, 70, 87, 246, 231, 212, 47, 216, 119, + 68, 237, 197, 125, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 72, 125, 102, 28, 159, 180, 237, 198, 97, 87, 80, 82, 200, 104, 40, 245, + 221, 7, 28, 122, 104, 91, 99, 1, 159, 140, 25, 131, 101, 74, 87, 50, 168, 146, 187, 90, 160, 51, 1, 123, 247, + 6, 108, 165, 181, 188, 40, 56, 47, 211, 229, 221, 73, 5, 15, 89, 81, 117, 225, 216, 108, 98, 226, 119, 232, 94, + 184, 42, 106, + ]; + let secret = block_on(coin.extract_secret(&[0u8; 20], tx_bytes.as_slice())) + .err() + .unwrap(); + assert!(secret.contains("Expected 'receiverSpend' contract call signature")); +} + +#[test] +fn test_eth_validate_valid_and_invalid_pubkey() { + let ctx = MmCtxBuilder::new().into_mm_arc(); + let conf = json!({ + "coin": "MATIC", + "name": "matic", + "fname": "Polygon", + "rpcport": 80, + "mm2": 1, + "chain_id": 137, + "avg_blocktime": 0.03, + "required_confirmations": 3, + "protocol": { + "type": "ETH" + } + }); + + let request = json!({ + "method": "enable", + "coin": "MATIC", + "urls": ["https://polygon-mainnet.g.alchemy.com/v2/9YYl6iMLmXXLoflMPHnMTC4Dcm2L2tFH"], + "swap_contract_address": "0x9130b257d37a52e52f21054c4da3450c72f595ce", + }); + + 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 priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(IguanaPrivKey::from(priv_key)); + let coin = block_on(eth_coin_from_conf_and_request( + &ctx, + "MATIC", + &conf, + &request, + CoinProtocol::ETH, + priv_key_policy, + )) + .unwrap(); + // Test expected to pass at this point as we're using a valid pubkey to validate against a valid pubkey + assert!(coin + .validate_other_pubkey(&[ + 3, 23, 183, 225, 206, 31, 159, 148, 195, 42, 67, 115, 146, 41, 248, 140, 11, 3, 51, 41, 111, 180, 110, 143, + 114, 134, 88, 73, 198, 174, 52, 184, 78 + ]) + .is_ok()); + // Test expected to fail at this point as we're using a valid pubkey to validate against an invalid pubkeys + assert!(coin.validate_other_pubkey(&[1u8; 20]).is_err()); + assert!(coin.validate_other_pubkey(&[1u8; 8]).is_err()); +} diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index ca1f3f8362..f8e22335df 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -20,7 +20,7 @@ async fn test_send() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::new(vec!["http://195.201.0.6:8565".into()]).unwrap(); + let transport = Web3Transport::single_node("http://195.201.0.6:8565", false); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); let coin = EthCoin(Arc::new(EthCoinImpl { @@ -28,8 +28,8 @@ async fn test_send() { 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"), + priv_key_policy: key_pair.into(), + swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -46,18 +46,20 @@ async fn test_send() { chain_id: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), + erc20_tokens_infos: Default::default(), + abortable_system: AbortableQueue::default(), })); - let tx = coin - .send_maker_payment( - 1000, - &DEX_FEE_ADDR_RAW_PUBKEY, - &[1; 20], - "0.001".parse().unwrap(), - &None, - &[], - ) - .compat() - .await; + let maker_payment_args = SendMakerPaymentArgs { + time_lock_duration: 0, + time_lock: 1000, + other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, + secret_hash: &[1; 20], + amount: "0.001".parse().unwrap(), + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + }; + let tx = coin.send_maker_payment(maker_payment_args).compat().await; console::log_1(&format!("{:?}", tx).into()); let block = coin.current_block().compat().await; diff --git a/mm2src/coins/eth/nonce.rs b/mm2src/coins/eth/nonce.rs new file mode 100644 index 0000000000..48a3d04f02 --- /dev/null +++ b/mm2src/coins/eth/nonce.rs @@ -0,0 +1,29 @@ +use web3::{api::Namespace, + helpers::{self, CallFuture}, + types::{Address, U256}, + Transport}; + +/// `ParityNonce` namespace. +#[derive(Debug, Clone)] +pub(crate) struct ParityNonce { + transport: T, +} + +impl Namespace for ParityNonce { + fn new(transport: T) -> Self + where + Self: Sized, + { + ParityNonce { transport } + } + + fn transport(&self) -> &T { &self.transport } +} + +impl ParityNonce { + /// Parity next nonce. + pub(crate) fn parity_next_nonce(&self, addr: Address) -> CallFuture { + let addr = helpers::serialize(&addr); + CallFuture::new(self.transport.execute("parity_nextNonce", vec![addr])) + } +} diff --git a/mm2src/coins/eth/swap_contract_abi.json b/mm2src/coins/eth/swap_contract_abi.json new file mode 100644 index 0000000000..7003d9a181 --- /dev/null +++ b/mm2src/coins/eth/swap_contract_abi.json @@ -0,0 +1,407 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "PaymentSent", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "secret", + "type": "bytes32" + } + ], + "name": "ReceiverSpent", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "SenderRefunded", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + }, + { + "internalType": "bytes20", + "name": "_secretHash", + "type": "bytes20" + }, + { + "internalType": "uint64", + "name": "_lockTime", + "type": "uint64" + } + ], + "name": "erc20Payment", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_secretHash", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "_lockTime", + "type": "uint64" + } + ], + "name": "erc20PaymentSha256", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + }, + { + "internalType": "bytes20", + "name": "_secretHash", + "type": "bytes20" + }, + { + "internalType": "uint64", + "name": "_lockTime", + "type": "uint64" + } + ], + "name": "ethPayment", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_secretHash", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "_lockTime", + "type": "uint64" + } + ], + "name": "ethPaymentSha256", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "payments", + "outputs": [ + { + "internalType": "bytes20", + "name": "paymentHash", + "type": "bytes20" + }, + { + "internalType": "uint64", + "name": "lockTime", + "type": "uint64" + }, + { + "internalType": "enum EtomicSwap.PaymentState", + "name": "state", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "_secret", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_sender", + "type": "address" + } + ], + "name": "receiverSpend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "secret_hash_algos", + "outputs": [ + { + "internalType": "enum EtomicSwap.SecretHashAlgo", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes20", + "name": "_secretHash", + "type": "bytes20" + }, + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + } + ], + "name": "senderRefund", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "_secretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + } + ], + "name": "senderRefundSha256", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes20", + "name": "_secretHash", + "type": "bytes20" + }, + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + } + ], + "name": "watcherRefund", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "_secret", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + } + ], + "name": "watcherSpend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs new file mode 100644 index 0000000000..3395cbed70 --- /dev/null +++ b/mm2src/coins/eth/v2_activation.rs @@ -0,0 +1,501 @@ +use super::*; +use common::executor::AbortedError; +use crypto::{CryptoCtxError, StandardHDPathToCoin}; +use enum_from::EnumFromTrait; +use mm2_err_handle::common_errors::WithInternal; +#[cfg(target_arch = "wasm32")] +use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; + +#[derive(Display, EnumFromTrait, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum EthActivationV2Error { + InvalidPayload(String), + InvalidSwapContractAddr(String), + InvalidFallbackSwapContract(String), + #[display(fmt = "Expected either 'chain_id' or 'rpc_chain_id' to be set")] + #[cfg(target_arch = "wasm32")] + ExpectedRpcChainId, + #[display(fmt = "Platform coin {} activation failed. {}", ticker, error)] + ActivationFailed { + ticker: String, + error: String, + }, + CouldNotFetchBalance(String), + UnreachableNodes(String), + #[display(fmt = "Enable request for ETH coin must have at least 1 node")] + AtLeastOneNodeRequired, + #[display(fmt = "'derivation_path' field is not found in config")] + DerivationPathIsNotSet, + #[display(fmt = "Error deserializing 'derivation_path': {}", _0)] + ErrorDeserializingDerivationPath(String), + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + #[cfg(target_arch = "wasm32")] + #[from_trait(WithMetamaskRpcError::metamask_rpc_error)] + #[display(fmt = "{}", _0)] + MetamaskError(MetamaskRpcError), + #[from_trait(WithInternal::internal)] + #[display(fmt = "Internal: {}", _0)] + InternalError(String), +} + +impl From for EthActivationV2Error { + fn from(err: MyAddressError) -> Self { Self::InternalError(err.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: AbortedError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: CryptoCtxError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: UnexpectedDerivationMethod) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +#[cfg(target_arch = "wasm32")] +impl From for EthActivationV2Error { + fn from(e: MetamaskError) -> Self { from_metamask_error(e) } +} + +/// An alternative to `crate::PrivKeyActivationPolicy`, typical only for ETH coin. +#[derive(Clone, Deserialize)] +pub enum EthPrivKeyActivationPolicy { + ContextPrivKey, + #[cfg(target_arch = "wasm32")] + Metamask, +} + +impl Default for EthPrivKeyActivationPolicy { + fn default() -> Self { EthPrivKeyActivationPolicy::ContextPrivKey } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum EthRpcMode { + Http, + #[cfg(target_arch = "wasm32")] + Metamask, +} + +impl Default for EthRpcMode { + fn default() -> Self { EthRpcMode::Http } +} + +#[derive(Clone, Deserialize)] +pub struct EthActivationV2Request { + #[serde(default)] + pub nodes: Vec, + #[serde(default)] + pub rpc_mode: EthRpcMode, + pub swap_contract_address: Address, + pub fallback_swap_contract: Option
, + pub gas_station_url: Option, + pub gas_station_decimals: Option, + #[serde(default)] + pub gas_station_policy: GasStationPricePolicy, + pub mm2: Option, + pub required_confirmations: Option, + #[serde(default)] + pub priv_key_policy: EthPrivKeyActivationPolicy, +} + +#[derive(Clone, Deserialize)] +pub struct EthNode { + pub url: String, + #[serde(default)] + pub gui_auth: bool, +} + +#[derive(Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum Erc20TokenActivationError { + InternalError(String), + CouldNotFetchBalance(String), +} + +impl From for Erc20TokenActivationError { + fn from(e: AbortedError) -> Self { Erc20TokenActivationError::InternalError(e.to_string()) } +} + +impl From for Erc20TokenActivationError { + fn from(err: MyAddressError) -> Self { Self::InternalError(err.to_string()) } +} + +#[derive(Clone, Deserialize)] +pub struct Erc20TokenActivationRequest { + pub required_confirmations: Option, +} + +pub struct Erc20Protocol { + pub platform: String, + pub token_addr: Address, +} + +#[cfg_attr(test, mockable)] +impl EthCoin { + pub async fn initialize_erc20_token( + &self, + activation_params: Erc20TokenActivationRequest, + protocol: Erc20Protocol, + ticker: String, + ) -> MmResult { + // TODO + // Check if ctx is required. + // Remove it to avoid circular references if possible + let ctx = MmArc::from_weak(&self.ctx) + .ok_or_else(|| String::from("No context")) + .map_err(Erc20TokenActivationError::InternalError)?; + + let conf = coin_conf(&ctx, &ticker); + + let decimals = match conf["decimals"].as_u64() { + None | Some(0) => get_token_decimals(&self.web3, protocol.token_addr) + .await + .map_err(Erc20TokenActivationError::InternalError)?, + Some(d) => d as u8, + }; + + let web3_instances: Vec = self + .web3_instances + .iter() + .map(|node| { + let mut transport = node.web3.transport().clone(); + if let Some(auth) = transport.gui_auth_validation_generator_as_mut() { + auth.coin_ticker = ticker.clone(); + } + let web3 = Web3::new(transport); + Web3Instance { + web3, + is_parity: node.is_parity, + } + }) + .collect(); + + let mut transport = self.web3.transport().clone(); + if let Some(auth) = transport.gui_auth_validation_generator_as_mut() { + auth.coin_ticker = ticker.clone(); + } + let web3 = Web3::new(transport); + + let required_confirmations = activation_params + .required_confirmations + .unwrap_or_else(|| conf["required_confirmations"].as_u64().unwrap_or(1)) + .into(); + + // Create an abortable system linked to the `MmCtx` so if the app is stopped on `MmArc::stop`, + // all spawned futures related to `ERC20` coin will be aborted as well. + let abortable_system = ctx.abortable_system.create_subsystem()?; + + let token = EthCoinImpl { + priv_key_policy: self.priv_key_policy.clone(), + my_address: self.my_address, + coin_type: EthCoinType::Erc20 { + platform: protocol.platform, + token_addr: protocol.token_addr, + }, + sign_message_prefix: self.sign_message_prefix.clone(), + swap_contract_address: self.swap_contract_address, + fallback_swap_contract: self.fallback_swap_contract, + decimals, + ticker, + gas_station_url: self.gas_station_url.clone(), + gas_station_decimals: self.gas_station_decimals, + gas_station_policy: self.gas_station_policy, + web3, + web3_instances, + history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), + ctx: self.ctx.clone(), + required_confirmations, + chain_id: self.chain_id, + logs_block_range: self.logs_block_range, + nonce_lock: self.nonce_lock.clone(), + erc20_tokens_infos: Default::default(), + abortable_system, + }; + + Ok(EthCoin(Arc::new(token))) + } +} + +pub async fn eth_coin_from_conf_and_request_v2( + ctx: &MmArc, + ticker: &str, + conf: &Json, + req: EthActivationV2Request, + priv_key_policy: EthPrivKeyBuildPolicy, +) -> MmResult { + let ticker = ticker.to_string(); + + if req.swap_contract_address == Address::default() { + return Err(EthActivationV2Error::InvalidSwapContractAddr( + "swap_contract_address can't be zero address".to_string(), + ) + .into()); + } + + if let Some(fallback) = req.fallback_swap_contract { + if fallback == Address::default() { + return Err(EthActivationV2Error::InvalidFallbackSwapContract( + "fallback_swap_contract can't be zero address".to_string(), + ) + .into()); + } + } + + let (my_address, priv_key_policy) = build_address_and_priv_key_policy(conf, priv_key_policy).await?; + let my_address_str = checksum_address(&format!("{:02x}", my_address)); + + let chain_id = conf["chain_id"].as_u64(); + + let (web3, web3_instances) = match (req.rpc_mode, &priv_key_policy) { + (EthRpcMode::Http, EthPrivKeyPolicy::KeyPair(key_pair)) => { + build_http_transport(ctx, ticker.clone(), my_address_str, key_pair, &req.nodes).await? + }, + #[cfg(target_arch = "wasm32")] + (EthRpcMode::Metamask, EthPrivKeyPolicy::Metamask(_)) => { + let chain_id = chain_id + .or_else(|| conf["rpc_chain_id"].as_u64()) + .or_mm_err(|| EthActivationV2Error::ExpectedRpcChainId)?; + build_metamask_transport(ctx, ticker.clone(), chain_id).await? + }, + #[cfg(target_arch = "wasm32")] + (_, _) => { + let error = r#"priv_key_policy="Metamask" and rpc_mode="Metamask" should be used both"#.to_string(); + return MmError::err(EthActivationV2Error::ActivationFailed { ticker, error }); + }, + }; + + // param from request should override the config + let required_confirmations = req + .required_confirmations + .unwrap_or_else(|| { + conf["required_confirmations"] + .as_u64() + .unwrap_or(DEFAULT_REQUIRED_CONFIRMATIONS as u64) + }) + .into(); + + let sign_message_prefix: Option = json::from_value(conf["sign_message_prefix"].clone()).ok(); + + let mut map = NONCE_LOCK.lock().unwrap(); + let nonce_lock = map.entry(ticker.clone()).or_insert_with(new_nonce_lock).clone(); + + // Create an abortable system linked to the `MmCtx` so if the app is stopped on `MmArc::stop`, + // all spawned futures related to `ETH` coin will be aborted as well. + let abortable_system = ctx.abortable_system.create_subsystem()?; + + let coin = EthCoinImpl { + priv_key_policy, + my_address, + coin_type: EthCoinType::Eth, + sign_message_prefix, + swap_contract_address: req.swap_contract_address, + fallback_swap_contract: req.fallback_swap_contract, + decimals: ETH_DECIMALS, + ticker, + gas_station_url: req.gas_station_url, + gas_station_decimals: req.gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), + gas_station_policy: req.gas_station_policy, + web3, + web3_instances, + history_sync_state: Mutex::new(HistorySyncState::NotEnabled), + ctx: ctx.weak(), + required_confirmations, + chain_id, + logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), + nonce_lock, + erc20_tokens_infos: Default::default(), + abortable_system, + }; + + Ok(EthCoin(Arc::new(coin))) +} + +/// Processes the given `priv_key_policy` and generates corresponding `KeyPair`. +/// This function expects either [`PrivKeyBuildPolicy::IguanaPrivKey`] +/// or [`PrivKeyBuildPolicy::GlobalHDAccount`], otherwise returns `PrivKeyPolicyNotAllowed` error. +pub(crate) async fn build_address_and_priv_key_policy( + conf: &Json, + priv_key_policy: EthPrivKeyBuildPolicy, +) -> MmResult<(Address, EthPrivKeyPolicy), EthActivationV2Error> { + let raw_priv_key = match priv_key_policy { + EthPrivKeyBuildPolicy::IguanaPrivKey(iguana) => iguana, + EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd_ctx) => { + // Consider storing `derivation_path` at `EthCoinImpl`. + let derivation_path: Option = json::from_value(conf["derivation_path"].clone()) + .map_to_mm(|e| EthActivationV2Error::ErrorDeserializingDerivationPath(e.to_string()))?; + let derivation_path = derivation_path.or_mm_err(|| EthActivationV2Error::DerivationPathIsNotSet)?; + global_hd_ctx + .derive_secp256k1_secret(&derivation_path) + .mm_err(|e| EthActivationV2Error::InternalError(e.to_string()))? + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyBuildPolicy::Metamask(metamask_ctx) => { + let address = *metamask_ctx.check_active_eth_account().await?; + let public_key_uncompressed = metamask_ctx.eth_account_pubkey_uncompressed(); + let public_key = compress_public_key(public_key_uncompressed)?; + return Ok(( + address, + EthPrivKeyPolicy::Metamask(EthMetamaskPolicy { + public_key, + public_key_uncompressed, + }), + )); + }, + }; + + let key_pair = KeyPair::from_secret_slice(raw_priv_key.as_slice()) + .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; + let address = key_pair.address(); + Ok((address, EthPrivKeyPolicy::KeyPair(key_pair))) +} + +async fn build_http_transport( + ctx: &MmArc, + coin_ticker: String, + address: String, + key_pair: &KeyPair, + eth_nodes: &[EthNode], +) -> MmResult<(Web3, Vec), EthActivationV2Error> { + if eth_nodes.is_empty() { + return MmError::err(EthActivationV2Error::AtLeastOneNodeRequired); + } + + let mut http_nodes = vec![]; + for node in eth_nodes { + let uri = node + .url + .parse() + .map_err(|_| EthActivationV2Error::InvalidPayload(format!("{} could not be parsed.", node.url)))?; + + http_nodes.push(HttpTransportNode { + uri, + gui_auth: node.gui_auth, + }); + } + + let mut rng = small_rng(); + http_nodes.as_mut_slice().shuffle(&mut rng); + + drop_mutability!(http_nodes); + + let mut web3_instances = Vec::with_capacity(http_nodes.len()); + let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); + for node in http_nodes.iter() { + let transport = build_single_http_transport( + coin_ticker.clone(), + address.clone(), + key_pair, + vec![node.clone()], + event_handlers.clone(), + ); + + let web3 = Web3::new(transport); + let version = match web3.web3().client_version().await { + Ok(v) => v, + Err(e) => { + error!("Couldn't get client version for url {}: {}", node.uri, e); + continue; + }, + }; + web3_instances.push(Web3Instance { + web3, + is_parity: version.contains("Parity") || version.contains("parity"), + }) + } + + if web3_instances.is_empty() { + return Err( + EthActivationV2Error::UnreachableNodes("Failed to get client version for all nodes".to_string()).into(), + ); + } + + let transport = build_single_http_transport(coin_ticker, address, key_pair, http_nodes, event_handlers); + let web3 = Web3::new(transport); + + Ok((web3, web3_instances)) +} + +fn build_single_http_transport( + coin_ticker: String, + address: String, + key_pair: &KeyPair, + nodes: Vec, + event_handlers: Vec, +) -> Web3Transport { + use crate::eth::web3_transport::http_transport::HttpTransport; + + let mut http_transport = HttpTransport::with_event_handlers(nodes, event_handlers); + http_transport.gui_auth_validation_generator = Some(GuiAuthValidationGenerator { + coin_ticker, + secret: key_pair.secret().clone(), + address, + }); + Web3Transport::from(http_transport) +} + +#[cfg(target_arch = "wasm32")] +async fn build_metamask_transport( + ctx: &MmArc, + coin_ticker: String, + chain_id: u64, +) -> MmResult<(Web3, Vec), EthActivationV2Error> { + let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); + + let eth_config = web3_transport::metamask_transport::MetamaskEthConfig { chain_id }; + let web3 = Web3::new(Web3Transport::new_metamask(eth_config, event_handlers)?); + + // Check if MetaMask supports the given `chain_id`. + // Please note that this request may take a long time. + check_metamask_supports_chain_id(coin_ticker, &web3, chain_id).await?; + + // MetaMask doesn't use Parity nodes. So `MetamaskTransport` doesn't support `parity_nextNonce` RPC. + // An example of the `web3_clientVersion` RPC - `MetaMask/v10.22.1`. + let web3_instances = vec![Web3Instance { + web3: web3.clone(), + is_parity: false, + }]; + + Ok((web3, web3_instances)) +} + +/// This method is based on the fact that `MetamaskTransport` tries to switch the `ChainId` +/// if the MetaMask is targeted to another ETH chain. +#[cfg(target_arch = "wasm32")] +async fn check_metamask_supports_chain_id( + ticker: String, + web3: &Web3, + expected_chain_id: u64, +) -> MmResult<(), EthActivationV2Error> { + use jsonrpc_core::ErrorCode; + + /// See the documentation: + /// https://docs.metamask.io/guide/rpc-api.html#wallet-switchethereumchain + const CHAIN_IS_NOT_REGISTERED_ERROR: ErrorCode = ErrorCode::ServerError(4902); + + match web3.eth().chain_id().await { + Ok(chain_id) if chain_id == U256::from(expected_chain_id) => Ok(()), + // The RPC client should have returned ChainId with which it has been created on [`Web3Transport::new_metamask`]. + Ok(unexpected_chain_id) => { + let error = format!("Expected '{expected_chain_id}' ChainId, found '{unexpected_chain_id}'"); + MmError::err(EthActivationV2Error::InternalError(error)) + }, + Err(web3::Error::Rpc(rpc_err)) if rpc_err.code == CHAIN_IS_NOT_REGISTERED_ERROR => { + let error = format!("Ethereum chain_id({expected_chain_id}) is not supported"); + MmError::err(EthActivationV2Error::ActivationFailed { ticker, error }) + }, + Err(other) => { + let error = other.to_string(); + MmError::err(EthActivationV2Error::ActivationFailed { ticker, error }) + }, + } +} + +#[cfg(target_arch = "wasm32")] +fn compress_public_key(uncompressed: H520) -> MmResult { + let public_key = PublicKey::from_slice(uncompressed.as_bytes()) + .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; + let compressed = public_key.serialize(); + Ok(H264::from(compressed)) +} diff --git a/mm2src/coins/eth/web3_transport.rs b/mm2src/coins/eth/web3_transport.rs deleted file mode 100644 index 5b0b229a57..0000000000 --- a/mm2src/coins/eth/web3_transport.rs +++ /dev/null @@ -1,283 +0,0 @@ -use super::{RpcTransportEventHandler, RpcTransportEventHandlerShared}; -#[cfg(not(target_arch = "wasm32"))] use futures::FutureExt; -use futures::TryFutureExt; -use futures01::{Future, Poll}; -use jsonrpc_core::{Call, Response}; -use serde_json::Value as Json; -#[cfg(not(target_arch = "wasm32"))] use std::ops::Deref; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use web3::api::Namespace; -use web3::error::{Error, ErrorKind}; -use web3::helpers::{self, build_request, to_result_from_output, to_string, CallFuture}; -use web3::types::{BlockNumber, U256}; -use web3::{RequestId, Transport}; - -/// eth_feeHistory support is missing even in the latest rust-web3 -/// It's the custom namespace implementing it -#[derive(Debug, Clone)] -pub struct EthFeeHistoryNamespace { - transport: T, -} - -impl Namespace for EthFeeHistoryNamespace { - fn new(transport: T) -> Self - where - Self: Sized, - { - Self { transport } - } - - fn transport(&self) -> &T { &self.transport } -} - -#[derive(Debug, Deserialize)] -pub struct FeeHistoryResult { - #[serde(rename = "oldestBlock")] - pub oldest_block: U256, - #[serde(rename = "baseFeePerGas")] - pub base_fee_per_gas: Vec, -} - -impl EthFeeHistoryNamespace { - pub fn eth_fee_history( - &self, - count: U256, - block: BlockNumber, - reward_percentiles: &[f64], - ) -> CallFuture { - let count = helpers::serialize(&count); - let block = helpers::serialize(&block); - let reward_percentiles = helpers::serialize(&reward_percentiles); - let params = vec![count, block, reward_percentiles]; - CallFuture::new(self.transport.execute("eth_feeHistory", params)) - } -} - -/// Parse bytes RPC response into `Result`. -/// Implementation copied from Web3 HTTP transport -#[cfg(not(target_arch = "wasm32"))] -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), - _ => Err(ErrorKind::InvalidResponse("Expected single, got batch.".into()).into()), - } -} - -#[derive(Clone, Debug)] -pub struct Web3Transport { - id: Arc, - uris: Vec, - event_handlers: Vec, -} - -impl Web3Transport { - #[allow(dead_code)] - pub fn new(urls: Vec) -> Result { - let mut uris = vec![]; - for url in urls.iter() { - uris.push(try_s!(url.parse())); - } - Ok(Web3Transport { - id: Arc::new(AtomicUsize::new(0)), - uris, - event_handlers: Default::default(), - }) - } - - pub fn with_event_handlers( - urls: Vec, - event_handlers: Vec, - ) -> Result { - let mut uris = vec![]; - for url in urls.iter() { - uris.push(try_s!(url.parse())); - } - Ok(Web3Transport { - id: Arc::new(AtomicUsize::new(0)), - uris, - event_handlers, - }) - } -} - -struct SendFuture(T); - -impl Future for SendFuture { - type Item = T::Item; - - type Error = T::Error; - - fn poll(&mut self) -> Poll { self.0.poll() } -} - -unsafe impl Send for SendFuture where T: Send {} -unsafe impl Sync for SendFuture {} - -impl Transport for Web3Transport { - type Out = Box + Send>; - - fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { - let id = self.id.fetch_add(1, Ordering::AcqRel); - let request = build_request(id, method, params); - - (id, request) - } - - #[cfg(not(target_arch = "wasm32"))] - fn send(&self, _id: RequestId, request: Call) -> Self::Out { - Box::new( - send_request(request, self.uris.clone(), self.event_handlers.clone()) - .boxed() - .compat(), - ) - } - - #[cfg(target_arch = "wasm32")] - fn send(&self, _id: RequestId, request: Call) -> Self::Out { - let fut = send_request(request, self.uris.clone(), self.event_handlers.clone()); - Box::new(SendFuture(Box::pin(fut).compat())) - } -} - -#[cfg(not(target_arch = "wasm32"))] -async fn send_request( - request: Call, - uris: Vec, - event_handlers: Vec, -) -> Result { - use common::executor::Timer; - 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() { - let request = to_string(&request); - event_handlers.on_outgoing_request(request.as_bytes()); - - let mut req = http::Request::new(request.clone().into_bytes()); - *req.method_mut() = http::Method::POST; - *req.uri_mut() = uri.clone(); - req.headers_mut() - .insert(http::header::CONTENT_TYPE, HeaderValue::from_static("application/json")); - 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)) => { - let error = ERRL!("Error requesting '{}': {}s timeout expired", uri, REQUEST_TIMEOUT_S); - warn!("{}", error); - errors.push(error); - continue; - }, - }; - - let (status, _headers, body) = match res { - Ok(r) => r, - Err(err) => { - errors.push(err.to_string()); - continue; - }, - }; - - event_handlers.on_incoming_response(&body); - - if !status.is_success() { - errors.push(ERRL!( - "Server '{}' response !200: {}, {}", - uri, - status, - binprint(&body, b'.') - )); - continue; - } - - return single_response(body, &uri.to_string()); - } - Err(request_failed_error(&request, &errors)) -} - -#[cfg(target_arch = "wasm32")] -async fn send_request( - request: Call, - uris: Vec, - event_handlers: Vec, -) -> Result { - let request_payload = to_string(&request); - - let mut transport_errors = Vec::new(); - for uri in uris { - 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()); - }, - Err(e) => return Err(e), - } - } - - Err(request_failed_error(&request, &transport_errors)) -} - -#[cfg(target_arch = "wasm32")] -async fn send_request_once( - request_payload: String, - uri: &http::Uri, - event_handlers: &Vec, -) -> Result { - use mm2_net::wasm_http::FetchRequest; - - macro_rules! try_or { - ($exp:expr, $errkind:ident) => { - match $exp { - Ok(x) => x, - Err(e) => return Err(Error::from(ErrorKind::$errkind(ERRL!("{:?}", e)))), - } - }; - } - - // account for outgoing traffic - event_handlers.on_outgoing_request(request_payload.as_bytes()); - - 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 - )))); - } - - // account for incoming traffic - event_handlers.on_incoming_response(response_str.as_bytes()); - - 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( - "Expected single, got batch.".to_owned(), - ))), - } -} - -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/eth/web3_transport/http_transport.rs b/mm2src/coins/eth/web3_transport/http_transport.rs new file mode 100644 index 0000000000..5272974310 --- /dev/null +++ b/mm2src/coins/eth/web3_transport/http_transport.rs @@ -0,0 +1,334 @@ +use crate::eth::{web3_transport::Web3SendOut, EthCoin, GuiAuthMessages, RpcTransportEventHandler, + RpcTransportEventHandlerShared, Web3RpcError}; +use common::APPLICATION_JSON; +use futures::lock::Mutex as AsyncMutex; +use http::header::CONTENT_TYPE; +use jsonrpc_core::{Call, Response}; +use mm2_net::transport::{GuiAuthValidation, GuiAuthValidationGenerator}; +use serde_json::Value as Json; +#[cfg(not(target_arch = "wasm32"))] use std::ops::Deref; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use web3::error::{Error, TransportError}; +use web3::helpers::{build_request, to_result_from_output, to_string}; +use web3::{RequestId, Transport}; + +#[derive(Serialize, Clone)] +pub struct AuthPayload<'a> { + #[serde(flatten)] + pub request: &'a Call, + pub signed_message: GuiAuthValidation, +} + +/// Parse bytes RPC response into `Result`. +/// Implementation copied from Web3 HTTP transport +#[cfg(not(target_arch = "wasm32"))] +fn single_response>(response: T, rpc_url: &str) -> Result { + let response = + serde_json::from_slice(&response).map_err(|e| Error::InvalidResponse(format!("{}: {}", rpc_url, e)))?; + + match response { + Response::Single(output) => to_result_from_output(output), + _ => Err(Error::InvalidResponse("Expected single, got batch.".into())), + } +} + +#[derive(Debug)] +struct HttpTransportRpcClient(AsyncMutex); + +#[derive(Debug)] +struct HttpTransportRpcClientImpl { + nodes: Vec, +} + +#[derive(Clone, Debug)] +pub struct HttpTransport { + id: Arc, + client: Arc, + event_handlers: Vec, + pub(crate) gui_auth_validation_generator: Option, +} + +#[derive(Clone, Debug)] +pub struct HttpTransportNode { + pub(crate) uri: http::Uri, + pub(crate) gui_auth: bool, +} + +impl HttpTransport { + #[cfg(test)] + #[inline] + pub fn new(nodes: Vec) -> Self { + let client_impl = HttpTransportRpcClientImpl { nodes }; + HttpTransport { + id: Arc::new(AtomicUsize::new(0)), + client: Arc::new(HttpTransportRpcClient(AsyncMutex::new(client_impl))), + event_handlers: Default::default(), + gui_auth_validation_generator: None, + } + } + + #[inline] + pub fn with_event_handlers( + nodes: Vec, + event_handlers: Vec, + ) -> Self { + let client_impl = HttpTransportRpcClientImpl { nodes }; + HttpTransport { + id: Arc::new(AtomicUsize::new(0)), + client: Arc::new(HttpTransportRpcClient(AsyncMutex::new(client_impl))), + event_handlers, + gui_auth_validation_generator: None, + } + } + + #[allow(dead_code)] + pub fn single_node(url: &'static str, gui_auth: bool) -> Self { + let nodes = vec![HttpTransportNode { + uri: url.parse().unwrap(), + gui_auth, + }]; + let client_impl = HttpTransportRpcClientImpl { nodes }; + + HttpTransport { + id: Arc::new(AtomicUsize::new(0)), + client: Arc::new(HttpTransportRpcClient(AsyncMutex::new(client_impl))), + event_handlers: Default::default(), + gui_auth_validation_generator: None, + } + } +} + +impl Transport for HttpTransport { + type Out = Web3SendOut; + + fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { + let id = self.id.fetch_add(1, Ordering::AcqRel); + let request = build_request(id, method, params); + + (id, request) + } + + #[cfg(not(target_arch = "wasm32"))] + fn send(&self, _id: RequestId, request: Call) -> Self::Out { + Box::pin(send_request( + request, + self.client.clone(), + self.event_handlers.clone(), + self.gui_auth_validation_generator.clone(), + )) + } + + #[cfg(target_arch = "wasm32")] + fn send(&self, _id: RequestId, request: Call) -> Self::Out { + Box::pin(send_request( + request, + self.client.clone(), + self.event_handlers.clone(), + self.gui_auth_validation_generator.clone(), + )) + } +} + +/// Generates a signed message and inserts it into request +/// payload if gui_auth is activated. Returns false on errors. +fn handle_gui_auth_payload_if_activated( + gui_auth_validation_generator: &Option, + node: &HttpTransportNode, + request: &Call, +) -> Result, Web3RpcError> { + if !node.gui_auth { + return Ok(None); + } + + let generator = match gui_auth_validation_generator.clone() { + Some(gen) => gen, + None => { + return Err(Web3RpcError::Internal(format!( + "GuiAuthValidationGenerator is not provided for {:?} node", + node + ))); + }, + }; + + let signed_message = match EthCoin::generate_gui_auth_signed_validation(generator) { + Ok(t) => t, + Err(e) => { + return Err(Web3RpcError::Internal(format!( + "GuiAuth signed message generation failed for {:?} node, error: {:?}", + node, e + ))); + }, + }; + + let auth_request = AuthPayload { + request, + signed_message, + }; + + Ok(Some(to_string(&auth_request))) +} + +#[cfg(not(target_arch = "wasm32"))] +async fn send_request( + request: Call, + client: Arc, + event_handlers: Vec, + gui_auth_validation_generator: Option, +) -> Result { + use common::executor::Timer; + 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(); + + let serialized_request = to_string(&request); + + let mut client_impl = client.0.lock().await; + + for (i, node) in client_impl.nodes.clone().iter().enumerate() { + let serialized_request = + match handle_gui_auth_payload_if_activated(&gui_auth_validation_generator, node, &request) { + Ok(Some(r)) => r, + Ok(None) => serialized_request.clone(), + Err(e) => { + errors.push(e); + continue; + }, + }; + + event_handlers.on_outgoing_request(serialized_request.as_bytes()); + + let mut req = http::Request::new(serialized_request.clone().into_bytes()); + *req.method_mut() = http::Method::POST; + *req.uri_mut() = node.uri.clone(); + req.headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static(APPLICATION_JSON)); + 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)) => { + let error = format!( + "Error requesting '{}': {}s timeout expired", + node.uri, REQUEST_TIMEOUT_S + ); + warn!("{}", error); + errors.push(Web3RpcError::Transport(error)); + continue; + }, + }; + + let (status, _headers, body) = match res { + Ok(r) => r, + Err(err) => { + errors.push(Web3RpcError::Transport(err.to_string())); + continue; + }, + }; + + event_handlers.on_incoming_response(&body); + + if !status.is_success() { + errors.push(Web3RpcError::Transport(format!( + "Server '{:?}' response !200: {}, {}", + node, + status, + binprint(&body, b'.') + ))); + continue; + } + + client_impl.nodes.rotate_left(i); + + return single_response(body, &node.uri.to_string()); + } + + Err(request_failed_error(&request, &errors)) +} + +#[cfg(target_arch = "wasm32")] +async fn send_request( + request: Call, + client: Arc, + event_handlers: Vec, + gui_auth_validation_generator: Option, +) -> Result { + let serialized_request = to_string(&request); + + let mut transport_errors = Vec::new(); + let mut client_impl = client.0.lock().await; + + for (i, node) in client_impl.nodes.clone().iter().enumerate() { + let serialized_request = + match handle_gui_auth_payload_if_activated(&gui_auth_validation_generator, node, &request) { + Ok(Some(r)) => r, + Ok(None) => serialized_request.clone(), + Err(e) => { + transport_errors.push(e); + continue; + }, + }; + + match send_request_once(serialized_request.clone(), &node.uri, &event_handlers).await { + Ok(response_json) => { + client_impl.nodes.rotate_left(i); + return Ok(response_json); + }, + Err(Error::Transport(e)) => { + transport_errors.push(Web3RpcError::Transport(e.to_string())); + }, + Err(e) => return Err(e), + } + } + + Err(request_failed_error(&request, &transport_errors)) +} + +#[cfg(target_arch = "wasm32")] +async fn send_request_once( + request_payload: String, + uri: &http::Uri, + event_handlers: &Vec, +) -> Result { + use http::header::ACCEPT; + use mm2_net::wasm_http::FetchRequest; + + // account for outgoing traffic + event_handlers.on_outgoing_request(request_payload.as_bytes()); + + let (status_code, response_str) = FetchRequest::post(&uri.to_string()) + .cors() + .body_utf8(request_payload) + .header(ACCEPT.as_str(), APPLICATION_JSON) + .header(CONTENT_TYPE.as_str(), APPLICATION_JSON) + .request_str() + .await + .map_err(|e| Error::Transport(TransportError::Message(ERRL!("{:?}", e))))?; + + if !status_code.is_success() { + let err = ERRL!("!200: {}, {}", status_code, response_str); + return Err(Error::Transport(TransportError::Message(err))); + } + + // account for incoming traffic + event_handlers.on_incoming_response(response_str.as_bytes()); + + let response: Response = serde_json::from_str(&response_str).map_err(|e| Error::InvalidResponse(e.to_string()))?; + match response { + Response::Single(output) => to_result_from_output(output), + Response::Batch(_) => Err(Error::InvalidResponse("Expected single, got batch.".to_owned())), + } +} + +fn request_failed_error(request: &Call, errors: &[Web3RpcError]) -> Error { + let errors: String = errors.iter().map(|e| format!("{:?}; ", e)).collect(); + let error = format!("request {:?} failed: {}", request, errors); + Error::Transport(TransportError::Message(error)) +} diff --git a/mm2src/coins/eth/web3_transport/metamask_transport.rs b/mm2src/coins/eth/web3_transport/metamask_transport.rs new file mode 100644 index 0000000000..5fe71d8dc2 --- /dev/null +++ b/mm2src/coins/eth/web3_transport/metamask_transport.rs @@ -0,0 +1,77 @@ +use crate::eth::web3_transport::Web3SendOut; +use crate::RpcTransportEventHandlerShared; +use jsonrpc_core::Call; +use mm2_metamask::{detect_metamask_provider, Eip1193Provider, MetamaskResult, MetamaskSession}; +use serde_json::Value as Json; +use std::fmt; +use std::sync::Arc; +use web3::{RequestId, Transport}; + +pub(crate) struct MetamaskEthConfig { + /// The `ChainId` that the MetaMask wallet should be targeted on each RPC. + pub chain_id: u64, +} + +#[derive(Clone)] +pub(crate) struct MetamaskTransport { + inner: Arc, +} + +struct MetamaskTransportInner { + eth_config: MetamaskEthConfig, + eip1193: Eip1193Provider, + // TODO use `event_handlers` properly. + _event_handlers: Vec, +} + +impl MetamaskTransport { + pub fn detect( + eth_config: MetamaskEthConfig, + event_handlers: Vec, + ) -> MetamaskResult { + let eip1193 = detect_metamask_provider()?; + let inner = MetamaskTransportInner { + eth_config, + eip1193, + _event_handlers: event_handlers, + }; + Ok(MetamaskTransport { inner: Arc::new(inner) }) + } +} + +impl Transport for MetamaskTransport { + type Out = Web3SendOut; + + fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { + self.inner.eip1193.prepare(method, params) + } + + fn send(&self, id: RequestId, request: Call) -> Self::Out { + let selfi = self.clone(); + let fut = async move { selfi.send_impl(id, request).await }; + Box::pin(fut) + } +} + +impl fmt::Debug for MetamaskTransport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "MetamaskTransport") } +} + +impl MetamaskTransport { + async fn send_impl(&self, id: RequestId, request: Call) -> Result { + // Hold the mutex guard until the request is finished. + let _rpc_lock = self.request_preparation().await?; + self.inner.eip1193.send(id, request).await + } + + /// Ensures that the MetaMask wallet is targeted to [`EthConfig::chain_id`]. + async fn request_preparation(&self) -> Result, web3::Error> { + // Lock the MetaMask session and keep it until the RPC is finished. + let metamask_session = MetamaskSession::lock(&self.inner.eip1193).await; + metamask_session + .wallet_switch_ethereum_chain(self.inner.eth_config.chain_id) + .await?; + + Ok(metamask_session) + } +} diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs new file mode 100644 index 0000000000..f8b7d62fbd --- /dev/null +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -0,0 +1,129 @@ +use crate::RpcTransportEventHandlerShared; +use ethereum_types::U256; +use futures::future::BoxFuture; +use jsonrpc_core::Call; +#[cfg(target_arch = "wasm32")] use mm2_metamask::MetamaskResult; +use mm2_net::transport::GuiAuthValidationGenerator; +use serde_json::Value as Json; +use serde_json::Value; +use web3::api::Namespace; +use web3::helpers::{self, CallFuture}; +use web3::types::BlockNumber; +use web3::{Error, RequestId, Transport}; + +pub(crate) mod http_transport; +#[cfg(target_arch = "wasm32")] pub(crate) mod metamask_transport; + +type Web3SendOut = BoxFuture<'static, Result>; + +#[derive(Clone, Debug)] +pub(crate) enum Web3Transport { + Http(http_transport::HttpTransport), + #[cfg(target_arch = "wasm32")] + Metamask(metamask_transport::MetamaskTransport), +} + +impl Web3Transport { + pub fn new_http( + nodes: Vec, + event_handlers: Vec, + ) -> Web3Transport { + http_transport::HttpTransport::with_event_handlers(nodes, event_handlers).into() + } + + #[cfg(target_arch = "wasm32")] + pub(crate) fn new_metamask( + eth_config: metamask_transport::MetamaskEthConfig, + event_handlers: Vec, + ) -> MetamaskResult { + Ok(metamask_transport::MetamaskTransport::detect(eth_config, event_handlers)?.into()) + } + + #[cfg(test)] + pub fn with_nodes(nodes: Vec) -> Web3Transport { + http_transport::HttpTransport::new(nodes).into() + } + + #[allow(dead_code)] + pub fn single_node(url: &'static str, gui_auth: bool) -> Self { + http_transport::HttpTransport::single_node(url, gui_auth).into() + } + + pub fn gui_auth_validation_generator_as_mut(&mut self) -> Option<&mut GuiAuthValidationGenerator> { + match self { + Web3Transport::Http(http) => http.gui_auth_validation_generator.as_mut(), + #[cfg(target_arch = "wasm32")] + Web3Transport::Metamask(_) => None, + } + } +} + +impl Transport for Web3Transport { + type Out = Web3SendOut; + + fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { + match self { + Web3Transport::Http(http) => http.prepare(method, params), + #[cfg(target_arch = "wasm32")] + Web3Transport::Metamask(metamask) => metamask.prepare(method, params), + } + } + + fn send(&self, id: RequestId, request: Call) -> Self::Out { + match self { + Web3Transport::Http(http) => http.send(id, request), + #[cfg(target_arch = "wasm32")] + Web3Transport::Metamask(metamask) => metamask.send(id, request), + } + } +} + +impl From for Web3Transport { + fn from(http: http_transport::HttpTransport) -> Self { Web3Transport::Http(http) } +} + +#[cfg(target_arch = "wasm32")] +impl From for Web3Transport { + fn from(metamask: metamask_transport::MetamaskTransport) -> Self { Web3Transport::Metamask(metamask) } +} + +/// eth_feeHistory support is missing even in the latest rust-web3 +/// It's the custom namespace implementing it +#[derive(Debug, Clone)] +pub struct EthFeeHistoryNamespace { + transport: T, +} + +impl Namespace for EthFeeHistoryNamespace { + fn new(transport: T) -> Self + where + Self: Sized, + { + Self { transport } + } + + fn transport(&self) -> &T { &self.transport } +} + +#[derive(Debug, Deserialize)] +pub struct FeeHistoryResult { + #[serde(rename = "oldestBlock")] + pub oldest_block: U256, + #[serde(rename = "baseFeePerGas")] + pub base_fee_per_gas: Vec, +} + +impl EthFeeHistoryNamespace { + pub fn eth_fee_history( + &self, + count: U256, + block: BlockNumber, + reward_percentiles: &[f64], + ) -> CallFuture { + let count = helpers::serialize(&count); + let block = helpers::serialize(&block); + let reward_percentiles = helpers::serialize(&reward_percentiles); + let params = vec![count, block, reward_percentiles]; + CallFuture::new(self.transport.execute("eth_feeHistory", params)) + } +} diff --git a/mm2src/coins/for_tests/MORTY_HD_tx_history_fixtures.json b/mm2src/coins/for_tests/MORTY_HD_tx_history_fixtures.json new file mode 100644 index 0000000000..1d35ebcfaa --- /dev/null +++ b/mm2src/coins/for_tests/MORTY_HD_tx_history_fixtures.json @@ -0,0 +1,131 @@ +[ + { + "tx_hex": "0400008085202f890189cb3d4e9d36a7c2c6bb1b76dcfc1ee7c9d3d83b364c50a3f3a791c5efb6f392030000006b483045022100a0b910ecbf5ed1c473507c3e1a5a06ad612c982d3204d2dc066bfbbeb39c5e400220559a82d151cbcdcb097786127f499863e0864662519a9caf758c929f1659bc34012102d09f2cb1693be9c0ea73bb48d45ce61805edd1c43590681b02f877206078a5b3ffffffff0400e1f505000000001976a914ab19b1f2bd2337a58c1b5d198468951ac42d796788ac00c2eb0b000000001976a914ab19b1f2bd2337a58c1b5d198468951ac42d796788aca01f791c000000001976a914ab19b1f2bd2337a58c1b5d198468951ac42d796788ac3091cce2e80000001976a91490a0d8ba62c339ade97a14e81b6f531de03fdbb288ac00000000000000000000000000000000000000", + "tx_hash": "6ca27dd058b939c98a33625b9f68eaeebca5a3058aec062647ca6fd7634bb339", + "from": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB" + ], + "to": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB", + "RQstQeTUEZLh6c3YWJDkeVTTQoZUsfvNCr" + ], + "total_amount": "10010.1518", + "spent_by_me": "0", + "received_by_me": "7.777", + "my_balance_change": "7.777", + "block_height": 1629306, + "timestamp": 1663619097, + "fee_details": { + "type": "Utxo", + "coin": "MORTY", + "amount": "0.0001" + }, + "coin": "MORTY", + "internal_id": "6ca27dd058b939c98a33625b9f68eaeebca5a3058aec062647ca6fd7634bb339", + "transaction_type": "StandardTransfer", + "confirmations": 4 + }, + { + "tx_hex": "0400008085202f89011b8746195a7e80172d948e1eb7f2d6710bd2ecbe7750e653bb8d345b940da55b030000006a4730440220482c7a7762977ed3a8afcf751b7413d5ef978f604e550eb4fd35199e1f4d52400220486fb4f3831a7f512dba1271a4f793e7050ea123bcb4922e3848bb18c7cac4c4012102d09f2cb1693be9c0ea73bb48d45ce61805edd1c43590681b02f877206078a5b3ffffffff0400e1f505000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88ac00c2eb0b000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88aca01f791c000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88acf037389ce90000001976a91490a0d8ba62c339ade97a14e81b6f531de03fdbb288ac00000000000000000000000000000000000000", + "tx_hash": "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", + "from": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB" + ], + "to": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB", + "RSYdSLRYWuzBson2GDbWBa632q2PmFnCaH" + ], + "total_amount": "10041.2602", + "spent_by_me": "0", + "received_by_me": "7.777", + "my_balance_change": "7.777", + "block_height": 1617702, + "timestamp": 1662914954, + "fee_details": { + "type": "Utxo", + "coin": "MORTY", + "amount": "0.0001" + }, + "coin": "MORTY", + "internal_id": "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", + "transaction_type": "StandardTransfer", + "confirmations": 11603 + }, + { + "tx_hex": "0400008085202f890175fb4250e95dc0c89cd89a9487324aa8dd7a2f8fd8581fc90881567ca6be02bf000000006b483045022100be3e58c5d4dbe5ea35ab831d610b42bb1ae01fc0df1786f11cbe6969d8d45c6302207b8747b4012a6c4aefecf670eaf8d787445909ebd7775b85c77d723e9d6445a8012103f7f831c6fbe62b987e4b2f455e5b7f27375cf59b57eb3ffa9e122b2c9b395f6bffffffff0200e1f505000000001976a914c23136f831b15dd4522eb1c6eb4c5cd3abbfbe3b88aca8b66428000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88ac00000000000000000000000000000000000000", + "tx_hash": "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", + "from": [ + "RPj9JXUVnewWwVpxZDeqGB25qVqz5qJzwP" + ], + "to": [ + "RSYdSLRYWuzBson2GDbWBa632q2PmFnCaH", + "RSyz8EJaTzhkT6uZinAtHhFu5bfsvqcLqg" + ], + "total_amount": "7.77699", + "spent_by_me": "7.77699", + "received_by_me": "6.77689", + "my_balance_change": "-1.00010", + "block_height": 1499070, + "timestamp": 1655738171, + "fee_details": { + "type": "Utxo", + "coin": "MORTY", + "amount": "0.0001" + }, + "coin": "MORTY", + "internal_id": "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", + "transaction_type": "StandardTransfer", + "confirmations": 130235 + }, + { + "tx_hex": "0400008085202f89038dea273bcc9194a80b19ea1a8ca076d3d00c590b4571808f10f3eee5aa38c07d000000006b483045022100ff31b7c36145dc9fa06346fe38b75b81273b41b7d236b900322154ba136799bb02204ddd253eb93e95c9d24f8e36a8a5a42022c63d6edd81c453f24cbb5db785037d012102d6a78b71b10459bd0757a614ca1eef62f4a65514225d10e95df31ee9cb23ffd5ffffffff8dea273bcc9194a80b19ea1a8ca076d3d00c590b4571808f10f3eee5aa38c07d010000006a47304402202d3e0fd3ce7b4753725adc0175a050c919f93b564682ffd8d65974c15996b3560220211b8120acb3ad80dbdd9160f639884f0e15c0c5703c86c069b3abe85cb6c4fd012102d6a78b71b10459bd0757a614ca1eef62f4a65514225d10e95df31ee9cb23ffd5ffffffff8dea273bcc9194a80b19ea1a8ca076d3d00c590b4571808f10f3eee5aa38c07d020000006b483045022100c788b064db34e961479393bad19d8c5f7d437b02c64c3172381fbe2b7d104efd022027167b2e5a6f29a7567e43b39a3d5b8f50c20767162ec29328067a6fcbd87409012102d6a78b71b10459bd0757a614ca1eef62f4a65514225d10e95df31ee9cb23ffd5ffffffff01b8be5a2e000000001976a9149e7a424abb2f341d655ce6af1143409edef4d55588acbdb1d661000000000000000000000000000000", + "tx_hash": "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", + "from": [ + "RYM6yDMn8vdqtkYKLzY5dNe7p3T6YmMWvq" + ], + "to": [ + "RPj9JXUVnewWwVpxZDeqGB25qVqz5qJzwP" + ], + "total_amount": "7.777", + "spent_by_me": "7.777", + "received_by_me": "7.77699", + "my_balance_change": "-0.00001", + "block_height": 1263905, + "timestamp": 1641460263, + "fee_details": { + "type": "Utxo", + "coin": "MORTY", + "amount": "0.00001" + }, + "coin": "MORTY", + "internal_id": "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", + "transaction_type": "StandardTransfer", + "confirmations": 365400 + }, + { + "tx_hex": "0400008085202f8901fe0ccc272929c811b20ab910f5478a99229a5304480b5897cf9507149a044c63030000006b483045022100c5b24fbd1ce11736760ebc0edccad6bde2dcbbff090528db4602a485a5ec645f02201340e5f818b9d7ab75e39abaacaa5d94c03fdb8e41698182eeff389a45c5ad15012102d09f2cb1693be9c0ea73bb48d45ce61805edd1c43590681b02f877206078a5b3ffffffff0400e1f505000000001976a914fd084ad97ae0313bba5717acedfba629b8bd426988ac00c2eb0b000000001976a914fd084ad97ae0313bba5717acedfba629b8bd426988aca01f791c000000001976a914fd084ad97ae0313bba5717acedfba629b8bd426988ac40053045260100001976a91490a0d8ba62c339ade97a14e81b6f531de03fdbb288ac00000000000000000000000000000000000000", + "tx_hash": "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", + "from": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB" + ], + "to": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB", + "RYM6yDMn8vdqtkYKLzY5dNe7p3T6YmMWvq" + ], + "total_amount": "12646.5887", + "spent_by_me": "0", + "received_by_me": "7.777", + "my_balance_change": "7.777", + "block_height": 1263875, + "timestamp": 1641458818, + "fee_details": { + "type": "Utxo", + "coin": "MORTY", + "amount": "0.0001" + }, + "coin": "MORTY", + "internal_id": "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", + "transaction_type": "StandardTransfer", + "confirmations": 365430 + } +] \ No newline at end of file diff --git a/mm2src/coins/hd_confirm_address.rs b/mm2src/coins/hd_confirm_address.rs new file mode 100644 index 0000000000..d6ee019855 --- /dev/null +++ b/mm2src/coins/hd_confirm_address.rs @@ -0,0 +1,184 @@ +use async_trait::async_trait; +use bip32::DerivationPath; +use crypto::hw_rpc_task::HwConnectStatuses; +use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProcessor, TryIntoUserAction}; +use crypto::trezor::{ProcessTrezorResponse, TrezorError, TrezorProcessingError}; +use crypto::{CryptoCtx, CryptoCtxError, HardwareWalletArc, HwError, HwProcessingError}; +use enum_from::{EnumFromInner, EnumFromStringify}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandle}; + +const SHOW_ADDRESS_ON_DISPLAY: bool = true; + +#[derive(EnumFromInner, EnumFromStringify)] +pub enum HDConfirmAddressError { + HwContextNotInitialized, + RpcTaskError(RpcTaskError), + #[from_inner] + HardwareWalletError(HwError), + InvalidAddress { + expected: String, + found: String, + }, + #[from_stringify("CryptoCtxError")] + Internal(String), +} + +impl From for HDConfirmAddressError { + fn from(e: TrezorError) -> Self { HDConfirmAddressError::HardwareWalletError(HwError::from(e)) } +} + +impl From> for HDConfirmAddressError { + fn from(e: TrezorProcessingError) -> Self { + match e { + TrezorProcessingError::TrezorError(trezor) => HDConfirmAddressError::from(HwError::from(trezor)), + TrezorProcessingError::ProcessorError(rpc) => HDConfirmAddressError::RpcTaskError(rpc), + } + } +} + +impl From> for HDConfirmAddressError { + fn from(e: HwProcessingError) -> Self { + match e { + HwProcessingError::HwError(hw) => HDConfirmAddressError::from(hw), + HwProcessingError::ProcessorError(rpc) => HDConfirmAddressError::RpcTaskError(rpc), + } + } +} + +/// An `InProgress` status constructor. +pub trait ConfirmAddressStatus: Sized { + /// Returns an `InProgress` RPC status that will be used to ask the user + /// to confirm an `address` on his HW device. + fn confirm_addr_status(address: String) -> Self; +} + +/// An address confirmation interface. +#[async_trait] +pub trait HDConfirmAddress: Sync { + /// Asks the user to confirm if the given `expected_address` is the same as on the HW display. + async fn confirm_utxo_address( + &self, + trezor_utxo_coin: String, + derivation_path: DerivationPath, + expected_address: String, + ) -> MmResult<(), HDConfirmAddressError>; +} + +pub enum RpcTaskConfirmAddress<'task, Task: RpcTask> { + Trezor { + hw_ctx: HardwareWalletArc, + task_handle: &'task RpcTaskHandle, + statuses: HwConnectStatuses, + }, +} + +#[async_trait] +impl<'task, Task> HDConfirmAddress for RpcTaskConfirmAddress<'task, Task> +where + Task: RpcTask, + Task::InProgressStatus: ConfirmAddressStatus, + Task::UserAction: TryIntoUserAction + Send, +{ + async fn confirm_utxo_address( + &self, + trezor_utxo_coin: String, + derivation_path: DerivationPath, + expected_address: String, + ) -> MmResult<(), HDConfirmAddressError> { + match self { + RpcTaskConfirmAddress::Trezor { + hw_ctx, + task_handle, + statuses, + } => { + Self::confirm_utxo_address_with_trezor( + hw_ctx, + task_handle, + statuses, + trezor_utxo_coin, + derivation_path, + expected_address, + ) + .await + }, + } + } +} + +impl<'task, Task> RpcTaskConfirmAddress<'task, Task> +where + Task: RpcTask, + Task::InProgressStatus: ConfirmAddressStatus, + Task::UserAction: TryIntoUserAction + Send, +{ + pub fn new( + ctx: &MmArc, + task_handle: &'task RpcTaskHandle, + statuses: HwConnectStatuses, + ) -> MmResult, HDConfirmAddressError> { + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| HDConfirmAddressError::HwContextNotInitialized)?; + Ok(RpcTaskConfirmAddress::Trezor { + hw_ctx, + task_handle, + statuses, + }) + } + + async fn confirm_utxo_address_with_trezor( + hw_ctx: &HardwareWalletArc, + task_handle: &RpcTaskHandle, + connect_statuses: &HwConnectStatuses, + trezor_coin: String, + derivation_path: DerivationPath, + expected_address: String, + ) -> MmResult<(), HDConfirmAddressError> { + let mut trezor_session = hw_ctx.trezor().await?; + + let confirm_statuses = TrezorRequestStatuses { + on_button_request: Task::InProgressStatus::confirm_addr_status(expected_address.clone()), + ..connect_statuses.to_trezor_request_statuses() + }; + + let pubkey_processor = TrezorRpcTaskProcessor::new(task_handle, confirm_statuses); + let address = trezor_session + .get_utxo_address(derivation_path, trezor_coin, SHOW_ADDRESS_ON_DISPLAY) + .await? + .process(&pubkey_processor) + .await?; + + if address != expected_address { + return MmError::err(HDConfirmAddressError::InvalidAddress { + expected: expected_address, + found: address, + }); + } + Ok(()) + } +} + +#[cfg(test)] +pub(crate) mod for_tests { + use super::*; + use mocktopus::macros::mockable; + + #[derive(Default)] + pub struct MockableConfirmAddress; + + #[async_trait] + #[mockable] + impl HDConfirmAddress for MockableConfirmAddress { + async fn confirm_utxo_address( + &self, + _trezor_utxo_coin: String, + _derivation_path: DerivationPath, + _expected_address: String, + ) -> MmResult<(), HDConfirmAddressError> { + unimplemented!() + } + } +} diff --git a/mm2src/coins/hd_pubkey.rs b/mm2src/coins/hd_pubkey.rs index a90accbc69..9bb122bee1 100644 --- a/mm2src/coins/hd_pubkey.rs +++ b/mm2src/coins/hd_pubkey.rs @@ -1,15 +1,16 @@ -use crate::hd_wallet::{HDWalletRpcError, NewAccountCreatingError}; +use crate::hd_wallet::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 crypto::hw_rpc_task::HwConnectStatuses; +use crypto::trezor::trezor_rpc_task::{TrezorRpcTaskProcessor, TryIntoUserAction}; +use crypto::trezor::utxo::IGNORE_XPUB_MAGIC; +use crypto::trezor::{ProcessTrezorResponse, TrezorError, TrezorProcessingError}; +use crypto::{CryptoCtx, CryptoCtxError, DerivationPath, EcdsaCurve, HardwareWalletArc, HwError, HwProcessingError, + XPub, XPubConverter, XpubError}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandle}; -use std::convert::TryInto; + +const SHOW_PUBKEY_ON_DISPLAY: bool = false; #[derive(Clone)] pub enum HDExtractPubkeyError { @@ -17,12 +18,12 @@ pub enum HDExtractPubkeyError { CoinDoesntSupportTrezor, RpcTaskError(RpcTaskError), HardwareWalletError(HwError), - InvalidXpub(Bip32Error), + InvalidXpub(String), Internal(String), } -impl From for HDExtractPubkeyError { - fn from(e: CryptoInitError) -> Self { HDExtractPubkeyError::Internal(e.to_string()) } +impl From for HDExtractPubkeyError { + fn from(e: CryptoCtxError) -> Self { HDExtractPubkeyError::Internal(e.to_string()) } } impl From for HDExtractPubkeyError { @@ -51,6 +52,10 @@ impl From> for HDExtractPubkeyError { } } +impl From for HDExtractPubkeyError { + fn from(e: XpubError) -> Self { HDExtractPubkeyError::InvalidXpub(e.to_string()) } +} + impl From for NewAccountCreatingError { fn from(e: HDExtractPubkeyError) -> Self { match e { @@ -66,19 +71,6 @@ impl From for NewAccountCreatingError { } } -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; @@ -89,14 +81,14 @@ pub trait ExtractExtendedPubkey { derivation_path: DerivationPath, ) -> MmResult where - XPubExtractor: HDXPubExtractor + Sync; + XPubExtractor: HDXPubExtractor; } #[async_trait] -pub trait HDXPubExtractor { +pub trait HDXPubExtractor: Sync { async fn extract_utxo_xpub( &self, - trezor_utxo_coin: TrezorUtxoCoin, + trezor_utxo_coin: String, derivation_path: DerivationPath, ) -> MmResult; } @@ -113,11 +105,11 @@ pub enum RpcTaskXPubExtractor<'task, Task: RpcTask> { impl<'task, Task> HDXPubExtractor for RpcTaskXPubExtractor<'task, Task> where Task: RpcTask, - Task::UserAction: TryInto + Send, + Task::UserAction: TryIntoUserAction + Send, { async fn extract_utxo_xpub( &self, - trezor_utxo_coin: TrezorUtxoCoin, + trezor_utxo_coin: String, derivation_path: DerivationPath, ) -> MmResult { match self { @@ -136,7 +128,7 @@ where impl<'task, Task> RpcTaskXPubExtractor<'task, Task> where Task: RpcTask, - Task::UserAction: TryInto + Send, + Task::UserAction: TryIntoUserAction + Send, { pub fn new( ctx: &MmArc, @@ -167,20 +159,28 @@ where hw_ctx: &HardwareWalletArc, task_handle: &RpcTaskHandle, statuses: &HwConnectStatuses, - trezor_coin: TrezorUtxoCoin, + trezor_coin: String, 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 mut trezor_session = hw_ctx.trezor().await?; let pubkey_processor = TrezorRpcTaskProcessor::new(task_handle, statuses.to_trezor_request_statuses()); - trezor_session - .get_public_key(derivation_path, trezor_coin, EcdsaCurve::Secp256k1) + let xpub = trezor_session + .get_public_key( + derivation_path, + trezor_coin, + EcdsaCurve::Secp256k1, + SHOW_PUBKEY_ON_DISPLAY, + IGNORE_XPUB_MAGIC, + ) .await? .process(&pubkey_processor) - .await - .mm_err(HDExtractPubkeyError::from) + .await?; + + // Despite we pass `IGNORE_XPUB_MAGIC` to the [`TrezorSession::get_public_key`] method, + // Trezor sometimes returns pubkeys with magic prefixes like `dgub` prefix for DOGE coin. + // So we need to replace the magic prefix manually. + XPubConverter::replace_magic_prefix(xpub).mm_err(HDExtractPubkeyError::from) } } @@ -196,7 +196,7 @@ where { async fn extract_utxo_xpub( &self, - trezor_utxo_coin: TrezorUtxoCoin, + trezor_utxo_coin: String, derivation_path: DerivationPath, ) -> MmResult { self.0 diff --git a/mm2src/coins/hd_wallet.rs b/mm2src/coins/hd_wallet.rs index cb365a01e5..6124393ef1 100644 --- a/mm2src/coins/hd_wallet.rs +++ b/mm2src/coins/hd_wallet.rs @@ -1,20 +1,16 @@ -use crate::coin_balance::HDAddressBalance; +use crate::hd_confirm_address::{HDConfirmAddress, HDConfirmAddressError}; use crate::hd_pubkey::HDXPubExtractor; use crate::hd_wallet_storage::HDWalletStorageError; -use crate::{lp_coinfind_or_err, BalanceError, CoinFindError, CoinWithDerivationMethod, MmCoinEnum, - UnexpectedDerivationMethod, WithdrawError}; +use crate::{BalanceError, WithdrawError}; use async_trait::async_trait; -use common::HttpStatusCode; -use crypto::{Bip32DerPathError, Bip32Error, Bip44Chain, Bip44DerPathError, Bip44DerivationPath, ChildNumber, - DerivationPath, HwError}; +use crypto::{Bip32DerPathError, Bip32Error, Bip44Chain, ChildNumber, DerivationPath, HwError, StandardHDPath, + StandardHDPathError}; use derive_more::Display; -use http::StatusCode; -use mm2_core::mm_ctx::MmArc; +use itertools::Itertools; 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}; @@ -23,10 +19,25 @@ pub type HDAccountsMutex = AsyncMutex>; pub type HDAccountsMut<'a, HDAccount> = AsyncMutexGuard<'a, HDAccountsMap>; pub type HDAccountMut<'a, HDAccount> = AsyncMappedMutexGuard<'a, HDAccountsMap, HDAccount>; -#[derive(Display)] +pub type AddressDerivingResult = MmResult; + +const DEFAULT_ADDRESS_LIMIT: u32 = ChildNumber::HARDENED_FLAG; +const DEFAULT_ACCOUNT_LIMIT: u32 = ChildNumber::HARDENED_FLAG; +const DEFAULT_RECEIVER_CHAIN: Bip44Chain = Bip44Chain::External; + +#[derive(Debug, Display)] pub enum AddressDerivingError { + #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] + InvalidBip44Chain { + chain: Bip44Chain, + }, #[display(fmt = "BIP32 address deriving error: {}", _0)] Bip32Error(Bip32Error), + Internal(String), +} + +impl From for AddressDerivingError { + fn from(e: InvalidBip44ChainError) -> Self { AddressDerivingError::InvalidBip44Chain { chain: e.chain } } } impl From for AddressDerivingError { @@ -34,22 +45,32 @@ impl From for AddressDerivingError { } impl From for BalanceError { + fn from(e: AddressDerivingError) -> Self { BalanceError::Internal(e.to_string()) } +} + +impl From for WithdrawError { fn from(e: AddressDerivingError) -> Self { match e { - AddressDerivingError::Bip32Error(bip32) => BalanceError::Internal(bip32.to_string()), + AddressDerivingError::InvalidBip44Chain { .. } | AddressDerivingError::Bip32Error(_) => { + WithdrawError::UnexpectedFromAddress(e.to_string()) + }, + AddressDerivingError::Internal(internal) => WithdrawError::InternalError(internal), } } } -impl From for WithdrawError { - fn from(e: AddressDerivingError) -> Self { WithdrawError::UnexpectedFromAddress(e.to_string()) } -} - +#[derive(Display)] pub enum NewAddressDerivingError { + #[display(fmt = "Addresses limit reached. Max number of addresses: {}", max_addresses_number)] AddressLimitReached { max_addresses_number: u32 }, + #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] InvalidBip44Chain { chain: Bip44Chain }, + #[display(fmt = "BIP32 address deriving error: {}", _0)] Bip32Error(Bip32Error), + #[display(fmt = "Wallet storage error: {}", _0)] WalletStorageError(HDWalletStorageError), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), } impl From for NewAddressDerivingError { @@ -59,7 +80,9 @@ impl From for NewAddressDerivingError { impl From for NewAddressDerivingError { fn from(e: AddressDerivingError) -> Self { match e { + AddressDerivingError::InvalidBip44Chain { chain } => NewAddressDerivingError::InvalidBip44Chain { chain }, AddressDerivingError::Bip32Error(bip32) => NewAddressDerivingError::Bip32Error(bip32), + AddressDerivingError::Internal(internal) => NewAddressDerivingError::Internal(internal), } } } @@ -80,6 +103,31 @@ impl From for NewAddressDerivingError { } } +pub enum NewAddressDeriveConfirmError { + DeriveError(NewAddressDerivingError), + ConfirmError(HDConfirmAddressError), +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: HDConfirmAddressError) -> Self { NewAddressDeriveConfirmError::ConfirmError(e) } +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: NewAddressDerivingError) -> Self { NewAddressDeriveConfirmError::DeriveError(e) } +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: AccountUpdatingError) -> Self { + NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::from(e)) + } +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: InvalidBip44ChainError) -> Self { + NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::from(e)) + } +} + #[derive(Display)] pub enum NewAccountCreatingError { #[display(fmt = "Hardware Wallet context is not initialized")] @@ -103,7 +151,9 @@ pub enum NewAccountCreatingError { } impl From for NewAccountCreatingError { - fn from(e: Bip32DerPathError) -> Self { NewAccountCreatingError::Internal(Bip44DerPathError::from(e).to_string()) } + fn from(e: Bip32DerPathError) -> Self { + NewAccountCreatingError::Internal(StandardHDPathError::from(e).to_string()) + } } impl From for NewAccountCreatingError { @@ -119,26 +169,6 @@ impl From for NewAccountCreatingError { } } -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)] @@ -174,174 +204,23 @@ impl From for BalanceError { } } -#[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, - } - } -} - +#[derive(Clone)] pub struct HDAddress { pub address: Address, pub pubkey: Pubkey, pub derivation_path: DerivationPath, } -#[derive(Clone, Deserialize, Serialize)] -pub struct HDAddressId { +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HDAccountAddressId { pub account_id: u32, pub chain: Bip44Chain, pub address_id: u32, } -impl From for HDAddressId { - fn from(der_path: Bip44DerivationPath) -> Self { - HDAddressId { +impl From for HDAccountAddressId { + fn from(der_path: StandardHDPath) -> Self { + HDAccountAddressId { account_id: der_path.account_id(), chain: der_path.chain(), address_id: der_path.address_id(), @@ -349,6 +228,12 @@ impl From for HDAddressId { } } +#[derive(Clone, Eq, Hash, PartialEq)] +pub struct HDAddressId { + pub chain: Bip44Chain, + pub address_id: u32, +} + #[async_trait] pub trait HDWalletCoinOps { type Address: Send + Sync; @@ -357,12 +242,41 @@ pub trait HDWalletCoinOps { type HDAccount: HDAccountOps; /// Derives an address from the given info. - fn derive_address( + async fn derive_address( &self, hd_account: &Self::HDAccount, chain: Bip44Chain, address_id: u32, - ) -> MmResult, AddressDerivingError>; + ) -> AddressDerivingResult> { + self.derive_addresses(hd_account, std::iter::once(HDAddressId { chain, address_id })) + .await? + .into_iter() + .exactly_one() + // Unfortunately, we can't use [`MapToMmResult::map_to_mm`] due to unsatisfied trait bounds, + // and it's easier to use [`Result::map_err`] instead of adding more trait bounds to this method. + .map_err(|e| MmError::new(AddressDerivingError::Internal(e.to_string()))) + } + + /// Derives HD addresses from the given info. + async fn derive_addresses( + &self, + hd_account: &Self::HDAccount, + address_ids: Ids, + ) -> AddressDerivingResult>> + where + Ids: Iterator + Send; + + async fn derive_known_addresses( + &self, + hd_account: &Self::HDAccount, + chain: Bip44Chain, + ) -> AddressDerivingResult>> { + let known_addresses_number = hd_account.known_addresses_number(chain)?; + let address_ids = (0..known_addresses_number) + .into_iter() + .map(|address_id| HDAddressId { chain, address_id }); + self.derive_addresses(hd_account, address_ids).await + } /// Generates a new address and updates the corresponding number of used `hd_account` addresses. async fn generate_new_address( @@ -371,22 +285,28 @@ pub trait HDWalletCoinOps { 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) + let inner_impl::NewAddress { + address, + new_known_addresses_number, + } = inner_impl::generate_new_address_immutable(self, hd_wallet, hd_account, chain).await?; + + self.set_known_addresses_number(hd_wallet, hd_account, chain, new_known_addresses_number) .await?; - Ok(new_address) + Ok(address) } + /// Generates a new address, requests the user to confirm if it's the same as on the HW device, + /// and then updates the corresponding number of used `hd_account` addresses. + async fn generate_and_confirm_new_address( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut Self::HDAccount, + chain: Bip44Chain, + confirm_address: &ConfirmAddress, + ) -> MmResult, NewAddressDeriveConfirmError> + where + ConfirmAddress: HDConfirmAddress; + /// 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>( @@ -395,7 +315,7 @@ pub trait HDWalletCoinOps { xpub_extractor: &XPubExtractor, ) -> MmResult, NewAccountCreatingError> where - XPubExtractor: HDXPubExtractor + Sync; + XPubExtractor: HDXPubExtractor; async fn set_known_addresses_number( &self, @@ -414,6 +334,15 @@ pub trait HDWalletOps: Send + Sync { fn gap_limit(&self) -> u32; + /// Returns limit on the number of addresses. + fn address_limit(&self) -> u32 { DEFAULT_ADDRESS_LIMIT } + + /// Returns limit on the number of accounts. + fn account_limit(&self) -> u32 { DEFAULT_ACCOUNT_LIMIT } + + /// Returns a BIP44 chain that is considered as default for receiver addresses. + fn default_receiver_chain(&self) -> Bip44Chain { DEFAULT_RECEIVER_CHAIN } + fn get_accounts_mutex(&self) -> &HDAccountsMutex; /// Returns a copy of an account by the given `account_id` if it's activated. @@ -441,6 +370,17 @@ pub trait HDWalletOps: Send + Sync { /// Returns a mutable reference to all activated accounts. async fn get_accounts_mut(&self) -> HDAccountsMut<'_, Self::HDAccount> { self.get_accounts_mutex().lock().await } + + async fn remove_account_if_last(&self, account_id: u32) -> Option { + let mut x = self.get_accounts_mutex().lock().await; + // `BTreeMap::last_entry` is still unstable. + let (last_account_id, _) = x.iter().last()?; + if *last_account_id == account_id { + x.remove(&account_id) + } else { + None + } + } } pub trait HDAccountOps: Send + Sync { @@ -461,89 +401,35 @@ pub trait HDAccountOps: Send + Sync { } } -#[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(crate) mod inner_impl { + use super::*; -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 struct NewAddress { + pub address: HDAddress, + pub new_known_addresses_number: u32, } -} - -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( + /// Generates a new address without updating a corresponding number of used `hd_account` addresses. + pub async fn generate_new_address_immutable( coin: &Coin, - params: GetNewHDAddressParams, - ) -> MmResult + hd_wallet: &Coin::HDWallet, + hd_account: &Coin::HDAccount, + chain: Bip44Chain, + ) -> MmResult, NewAddressDerivingError> where - Coin: HDWalletBalanceOps - + CoinWithDerivationMethod::HDWallet> - + MarketCoinOps - + Sync - + Send, - ::Address: fmt::Display, + Coin: HDWalletCoinOps + ?Sized + Sync, { - 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 { + 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; + let max_addresses_number = hd_wallet.address_limit(); + if new_address_id >= max_addresses_number { + return MmError::err(NewAddressDerivingError::AddressLimitReached { max_addresses_number }); + } + let address = coin.derive_address(hd_account, chain, new_address_id).await?; + Ok(NewAddress { 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, - }, + new_known_addresses_number: known_addresses_number + 1, }) } } diff --git a/mm2src/coins/hd_wallet_storage/mock_storage.rs b/mm2src/coins/hd_wallet_storage/mock_storage.rs index 67e694ed44..2fbbc19f4c 100644 --- a/mm2src/coins/hd_wallet_storage/mock_storage.rs +++ b/mm2src/coins/hd_wallet_storage/mock_storage.rs @@ -1,12 +1,12 @@ use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageInternalOps, HDWalletStorageResult}; use async_trait::async_trait; use mm2_core::mm_ctx::MmArc; -use mocktopus::macros::*; +#[cfg(test)] use mocktopus::macros::*; pub struct HDWalletMockStorage; #[async_trait] -#[mockable] +#[cfg_attr(test, mockable)] impl HDWalletStorageInternalOps for HDWalletMockStorage { async fn init(_ctx: &MmArc) -> HDWalletStorageResult where diff --git a/mm2src/coins/hd_wallet_storage/mod.rs b/mm2src/coins/hd_wallet_storage/mod.rs index 95506f52fe..2c52cf3895 100644 --- a/mm2src/coins/hd_wallet_storage/mod.rs +++ b/mm2src/coins/hd_wallet_storage/mod.rs @@ -1,6 +1,6 @@ use crate::hd_wallet::HDWalletCoinOps; use async_trait::async_trait; -use crypto::{CryptoCtx, CryptoInitError, XPub}; +use crypto::{CryptoCtx, CryptoCtxError, XPub}; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -14,8 +14,9 @@ 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(any(test, target_arch = "wasm32"))] mod mock_storage; +#[cfg(any(test, target_arch = "wasm32"))] +pub use mock_storage::HDWalletMockStorage; cfg_wasm32! { use wasm_storage::HDWalletIndexedDbStorage as HDWalletStorageInstance; @@ -48,8 +49,8 @@ pub enum HDWalletStorageError { Internal(String), } -impl From for HDWalletStorageError { - fn from(e: CryptoInitError) -> Self { HDWalletStorageError::Internal(e.to_string()) } +impl From for HDWalletStorageError { + fn from(e: CryptoCtxError) -> Self { HDWalletStorageError::Internal(e.to_string()) } } impl HDWalletStorageError { @@ -59,21 +60,15 @@ impl HDWalletStorageError { #[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 { + pub fn new(coin: String, hd_wallet_rmd160: &H160) -> HDWalletId { HDWalletId { coin, - mm2_rmd160: display_rmd160(mm2_rmd160), hd_wallet_rmd160: display_rmd160(hd_wallet_rmd160), } } @@ -187,13 +182,8 @@ pub trait HDWalletCoinWithStorageOps: HDWalletCoinOps { /// 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, } @@ -202,18 +192,16 @@ 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)] +#[cfg(any(test, target_arch = "wasm32"))] impl Default for HDWalletCoinStorage { fn default() -> Self { HDWalletCoinStorage { coin: String::default(), - mm2_rmd160: H160::default(), hd_wallet_rmd160: H160::default(), inner: Box::new(HDWalletMockStorage), } @@ -225,11 +213,10 @@ impl HDWalletCoinStorage { let inner = Box::new(HDWalletStorageInstance::init(ctx).await?); let crypto_ctx = CryptoCtx::from_ctx(ctx)?; let hd_wallet_rmd160 = crypto_ctx - .hd_wallet_rmd160() + .hw_wallet_rmd160() .or_mm_err(|| HDWalletStorageError::HDWalletUnavailable)?; Ok(HDWalletCoinStorage { coin, - mm2_rmd160: *ctx.rmd160(), hd_wallet_rmd160, inner, }) @@ -239,21 +226,17 @@ impl HDWalletCoinStorage { 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 fn wallet_id(&self) -> HDWalletId { HDWalletId::new(self.coin.clone(), &self.hd_wallet_rmd160) } pub async fn load_all_accounts(&self) -> HDWalletStorageResult> { let wallet_id = self.wallet_id(); @@ -320,31 +303,25 @@ mod tests { } async fn test_unique_wallets_impl() { - let rick_user0_device0_account0 = HDAccountStorageItem { + let rick_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 { + let rick_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 { + let rick_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 { + let morty_device0_account0 = HDAccountStorageItem { account_id: 0, account_xpub: "xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU".to_owned(), external_addresses_number: 7, @@ -352,88 +329,68 @@ mod tests { }; 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()) + let rick_device0_db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), device0_rmd160) .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()) + .expect("!HDWalletCoinStorage::new"); + let rick_device1_db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), device1_rmd160) .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()) + .expect("!HDWalletCoinStorage::new"); + let morty_device0_db = HDWalletCoinStorage::init_with_rmd160(&ctx, "MORTY".to_owned(), device0_rmd160) + .await + .expect("!HDWalletCoinStorage::new"); + + rick_device0_db + .upload_new_account(rick_device0_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()) + .expect("!HDWalletCoinStorage::upload_new_account: RICK device=0 account=0"); + rick_device0_db + .upload_new_account(rick_device0_account1.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()) + .expect("!HDWalletCoinStorage::upload_new_account: RICK device=0 account=1"); + rick_device1_db + .upload_new_account(rick_device1_account0.clone()) .await - .expect("!HDWalletCoinStorage::upload_new_account: MORTY user=0 device=0 account=0"); + .expect("!HDWalletCoinStorage::upload_new_account: RICK device=1 account=0"); + morty_device0_db + .upload_new_account(morty_device0_account0.clone()) + .await + .expect("!HDWalletCoinStorage::upload_new_account: MORTY 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. + // Rows in the database must differ by only `coin`, `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() + rick_device0_account0.clone(), + rick_device0_account1.clone(), + rick_device1_account0.clone(), + morty_device0_account0.clone() ]); - let mut actual = rick_user0_device0_db + let mut actual = rick_device0_db .load_all_accounts() .await - .expect("HDWalletCoinStorage::load_all_accounts: RICK user=0 device=0"); + .expect("HDWalletCoinStorage::load_all_accounts: RICK 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]); + assert_eq!(actual, vec![rick_device0_account0, rick_device0_account1]); - let actual = rick_user1_device0_db + let actual = rick_device1_db .load_all_accounts() .await - .expect("HDWalletCoinStorage::load_all_accounts: RICK user=1 device=0"); - assert_eq!(actual, vec![rick_user1_device0_account0]); + .expect("HDWalletCoinStorage::load_all_accounts: RICK device=1"); + assert_eq!(actual, vec![rick_device1_account0]); - let actual = morty_user0_device0_db + let actual = morty_device0_db .load_all_accounts() .await - .expect("HDWalletCoinStorage::load_all_accounts: MORTY user=0 device=0"); - assert_eq!(actual, vec![morty_user0_device0_account0]); + .expect("HDWalletCoinStorage::load_all_accounts: MORTY device=0"); + assert_eq!(actual, vec![morty_device0_account0]); } async fn test_delete_accounts_impl() { @@ -463,18 +420,17 @@ mod tests { }; 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) + let wallet0_db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), device0_rmd160) .await .expect("!HDWalletCoinStorage::new"); - let wallet1_db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), user_rmd160, device1_rmd160) + let wallet1_db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), device1_rmd160) .await .expect("!HDWalletCoinStorage::new"); - let wallet2_db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), user_rmd160, device2_rmd160) + let wallet2_db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), device2_rmd160) .await .expect("!HDWalletCoinStorage::new"); @@ -501,7 +457,7 @@ mod tests { .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. + // Rows in the database must differ by only `coin`, `hd_wallet_rmd160` and `account_id` values. let all_accounts: Vec<_> = get_all_storage_items(&ctx) .await .into_iter() @@ -525,10 +481,9 @@ mod tests { }; 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) + let db = HDWalletCoinStorage::init_with_rmd160(&ctx, "RICK".to_owned(), device_rmd160) .await .expect("!HDWalletCoinStorage::new"); diff --git a/mm2src/coins/hd_wallet_storage/sqlite_storage.rs b/mm2src/coins/hd_wallet_storage/sqlite_storage.rs index 8a061fddb5..38d1201120 100644 --- a/mm2src/coins/hd_wallet_storage/sqlite_storage.rs +++ b/mm2src/coins/hd_wallet_storage/sqlite_storage.rs @@ -2,8 +2,10 @@ use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorage 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 db_common::owned_named_params; +use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Row, NO_PARAMS}; +use db_common::sqlite::{query_single_row_with_named_params, AsSqlNamedParams, OwnedSqlNamedParams, SqliteConnShared, + SqliteConnWeak}; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -12,7 +14,6 @@ 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, @@ -21,23 +22,20 @@ const CREATE_HD_ACCOUNT_TABLE: &str = "CREATE TABLE IF NOT EXISTS hd_account ( );"; 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);"; + (coin, hd_wallet_rmd160, account_id, account_xpub, external_addresses_number, internal_addresses_number) + VALUES (:coin, :hd_wallet_rmd160, :account_id, :account_xpub, :external_addresses_number, :internal_addresses_number);"; const DELETE_ACCOUNTS_BY_WALLET_ID: &str = - "DELETE FROM hd_account WHERE coin=?1 AND mm2_rmd160=?2 AND hd_wallet_rmd160=?3;"; + "DELETE FROM hd_account WHERE coin=:coin AND hd_wallet_rmd160=:hd_wallet_rmd160;"; 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;"; + WHERE coin=:coin AND hd_wallet_rmd160=:hd_wallet_rmd160 AND account_id=:account_id;"; 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; + WHERE coin=:coin AND hd_wallet_rmd160=:hd_wallet_rmd160;"; impl From for HDWalletStorageError { fn from(e: SqlError) -> Self { @@ -69,32 +67,24 @@ impl TryFrom<&Row<'_>> for HDAccountStorageItem { } 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); + fn to_sql_params_with_wallet_id(&self, wallet_id: HDWalletId) -> OwnedSqlNamedParams { + let mut params = wallet_id.to_sql_params(); + params.extend(owned_named_params! { + ":account_id": self.account_id, + ":account_xpub": self.account_xpub.clone(), + ":external_addresses_number": self.external_addresses_number, + ":internal_addresses_number": self.internal_addresses_number, + }); 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()); + fn to_sql_params(&self) -> OwnedSqlNamedParams { + owned_named_params! { + ":coin": self.coin.clone(), + ":hd_wallet_rmd160": self.hd_wallet_rmd160.clone(), + } } } @@ -109,10 +99,9 @@ impl HDWalletStorageInternalOps for HDWalletSqliteStorage { where Self: Sized, { - let shared = ctx - .sqlite_connection - .as_option() - .or_mm_err(|| HDWalletStorageError::Internal("'MmCtx::sqlite_connection' is not initialized".to_owned()))?; + let shared = ctx.shared_sqlite_conn.as_option().or_mm_err(|| { + HDWalletStorageError::Internal("'MmCtx::shared_sqlite_conn' is not initialized".to_owned()) + })?; let storage = HDWalletSqliteStorage { conn: SqliteConnShared::downgrade(shared), }; @@ -124,13 +113,15 @@ impl HDWalletStorageInternalOps for HDWalletSqliteStorage { let selfi = self.clone(); async_blocking(move || { let conn_shared = selfi.get_shared_conn()?; - let conn = Self::lock_conn(&conn_shared)?; + let conn = Self::lock_conn_mutex(&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))? + .query_map_named(¶ms.as_sql_named_params(), |row: &Row<'_>| { + HDAccountStorageItem::try_from(row) + })? .collect::, _>>()?; Ok(rows) }) @@ -145,14 +136,16 @@ impl HDWalletStorageInternalOps for HDWalletSqliteStorage { let selfi = self.clone(); async_blocking(move || { let conn_shared = selfi.get_shared_conn()?; - let conn = Self::lock_conn(&conn_shared)?; + let conn = Self::lock_conn_mutex(&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<'_>| { + params.extend(owned_named_params! { + ":account_id": account_id, + }); + query_single_row_with_named_params(&conn, SELECT_ACCOUNT, ¶ms.as_sql_named_params(), |row: &Row<'_>| { HDAccountStorageItem::try_from(row) }) - .mm_err(HDWalletStorageError::from) + .map_to_mm(HDWalletStorageError::from) }) .await } @@ -195,10 +188,10 @@ impl HDWalletStorageInternalOps for HDWalletSqliteStorage { let selfi = self.clone(); async_blocking(move || { let conn_shared = selfi.get_shared_conn()?; - let conn = Self::lock_conn(&conn_shared)?; + let conn = Self::lock_conn_mutex(&conn_shared)?; let params = account.to_sql_params_with_wallet_id(wallet_id); - conn.execute(INSERT_ACCOUNT, params) + conn.execute_named(INSERT_ACCOUNT, ¶ms.as_sql_named_params()) .map(|_| ()) .map_to_mm(HDWalletStorageError::from) }) @@ -209,10 +202,10 @@ impl HDWalletStorageInternalOps for HDWalletSqliteStorage { let selfi = self.clone(); async_blocking(move || { let conn_shared = selfi.get_shared_conn()?; - let conn = Self::lock_conn(&conn_shared)?; + let conn = Self::lock_conn_mutex(&conn_shared)?; let params = wallet_id.to_sql_params(); - conn.execute(DELETE_ACCOUNTS_BY_WALLET_ID, params) + conn.execute_named(DELETE_ACCOUNTS_BY_WALLET_ID, ¶ms.as_sql_named_params()) .map(|_| ()) .map_to_mm(HDWalletStorageError::from) }) @@ -227,14 +220,14 @@ impl HDWalletSqliteStorage { .or_mm_err(|| HDWalletStorageError::Internal("'HDWalletSqliteStorage::conn' doesn't exist".to_owned())) } - fn lock_conn(conn: &SqliteConnShared) -> HDWalletStorageResult> { + fn lock_conn_mutex(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)?; + let conn = Self::lock_conn_mutex(&conn_shared)?; conn.execute(CREATE_HD_ACCOUNT_TABLE, NO_PARAMS) .map(|_| ()) .map_to_mm(HDWalletStorageError::from) @@ -248,20 +241,21 @@ impl HDWalletSqliteStorage { 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 + "UPDATE hd_account SET {updating_property}=:new_value WHERE coin=:coin AND hd_wallet_rmd160=:hd_wallet_rmd160 AND account_id=:account_id;", ); let selfi = self.clone(); async_blocking(move || { let conn_shared = selfi.get_shared_conn()?; - let conn = Self::lock_conn(&conn_shared)?; + let conn = Self::lock_conn_mutex(&conn_shared)?; - let mut params = vec![new_addresses_number.to_string()]; - wallet_id.fill_sql_params(&mut params); - params.push(account_id.to_string()); + let mut params = owned_named_params! { + ":new_value": new_addresses_number, + ":account_id": account_id, + }; + params.extend(wallet_id.to_sql_params()); - conn.execute(&sql, params) + conn.execute_named(&sql, ¶ms.as_sql_named_params()) .map(|_| ()) .map_to_mm(HDWalletStorageError::from) }) @@ -277,29 +271,13 @@ enum UpdatingProperty { 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 conn = ctx.shared_sqlite_conn(); let mut statement = conn.prepare(SELECT_ALL_ACCOUNTS).unwrap(); statement .query_map(NO_PARAMS, |row: &Row<'_>| HDAccountStorageItem::try_from(row)) diff --git a/mm2src/coins/hd_wallet_storage/wasm_storage.rs b/mm2src/coins/hd_wallet_storage/wasm_storage.rs index fb9dbd2bbd..721a4b09a7 100644 --- a/mm2src/coins/hd_wallet_storage/wasm_storage.rs +++ b/mm2src/coins/hd_wallet_storage/wasm_storage.rs @@ -14,12 +14,10 @@ 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"; @@ -75,8 +73,8 @@ 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`, +/// The table has the following individually non-unique indexes: `coin`, `hd_wallet_rmd160`, `account_id`, +/// one non-unique multi-index `wallet_id` that consists of `coin`, `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)] @@ -84,9 +82,6 @@ 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, @@ -106,10 +101,10 @@ impl TableSignature for HDAccountTable { 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_ID_INDEX, &["coin", "hd_wallet_rmd160"], false)?; table.create_multi_index( WALLET_ACCOUNT_ID_INDEX, - &["coin", "mm2_rmd160", "hd_wallet_rmd160", "account_id"], + &["coin", "hd_wallet_rmd160", "account_id"], true, )?; }, @@ -123,7 +118,6 @@ 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, @@ -180,14 +174,13 @@ impl HDWalletStorageInternalOps for HDWalletIndexedDbStorage { 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 locked_db = Self::lock_db_mutex(&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) @@ -203,7 +196,7 @@ impl HDWalletStorageInternalOps for HDWalletIndexedDbStorage { account_id: u32, ) -> HDWalletStorageResult> { let shared_db = self.get_shared_db()?; - let locked_db = Self::lock_db(&shared_db).await?; + let locked_db = Self::lock_db_mutex(&shared_db).await?; let transaction = locked_db.inner.transaction().await?; let table = transaction.table::().await?; @@ -245,7 +238,7 @@ impl HDWalletStorageInternalOps for HDWalletIndexedDbStorage { account: HDAccountStorageItem, ) -> HDWalletStorageResult<()> { let shared_db = self.get_shared_db()?; - let locked_db = Self::lock_db(&shared_db).await?; + let locked_db = Self::lock_db_mutex(&shared_db).await?; let transaction = locked_db.inner.transaction().await?; let table = transaction.table::().await?; @@ -260,14 +253,13 @@ impl HDWalletStorageInternalOps for HDWalletIndexedDbStorage { 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 locked_db = Self::lock_db_mutex(&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(()) @@ -281,7 +273,7 @@ impl HDWalletIndexedDbStorage { .or_mm_err(|| HDWalletStorageError::Internal("'HDWalletIndexedDbStorage::db' doesn't exist".to_owned())) } - async fn lock_db(db: &SharedDb) -> HDWalletStorageResult> { + async fn lock_db_mutex(db: &SharedDb) -> HDWalletStorageResult> { db.get_or_initialize().await.mm_err(HDWalletStorageError::from) } @@ -292,7 +284,6 @@ impl HDWalletIndexedDbStorage { ) -> 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 @@ -306,7 +297,7 @@ impl HDWalletIndexedDbStorage { F: FnOnce(&mut HDAccountTable), { let shared_db = self.get_shared_db()?; - let locked_db = Self::lock_db(&shared_db).await?; + let locked_db = Self::lock_db_mutex(&shared_db).await?; let transaction = locked_db.inner.transaction().await?; let table = transaction.table::().await?; diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 08e2acbb00..8a69514384 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -1,88 +1,95 @@ pub mod ln_conf; +pub(crate) mod ln_db; 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}; +pub mod ln_events; +mod ln_filesystem_persister; +pub mod ln_p2p; +pub mod ln_platform; +pub(crate) mod ln_serialization; +mod ln_sql; +pub mod ln_storage; +pub mod ln_utils; + +use crate::coin_errors::MyAddressError; +use crate::lightning::ln_utils::{filter_channels, pay_invoice_with_max_total_cltv_expiry_delta, PaymentError}; 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 crate::utxo::utxo_common::{big_decimal_from_sat, big_decimal_from_sat_unsigned}; +use crate::utxo::{sat_from_big_decimal, utxo_common, BlockchainNetwork}; +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, + HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, + PaymentInstructions, PaymentInstructionsErr, RawTransactionError, RawTransactionFut, + RawTransactionRequest, RefundError, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, + SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, + SendSpendPaymentArgs, SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, + SendWatcherRefundsPaymentArgs, SignatureError, SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, + TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, TransactionEnum, TransactionErr, + TransactionFut, TxMarshalingErr, UnexpectedDerivationMethod, UtxoStandardCoin, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, + ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WatcherOps, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WithdrawError, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; +use bitcoin::bech32::ToBase32; 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 bitcrypto::{dhash256, ripemd160}; +use common::custom_futures::repeatable::{Ready, Retry}; +use common::executor::{AbortableSystem, AbortedError, Timer}; +use common::log::{info, LogOnError, LogState}; +use common::{async_blocking, get_local_duration_since_epoch, log, now_ms, PagingOptionsEnum}; +use db_common::sqlite::rusqlite::Error as SqlError; use futures::{FutureExt, TryFutureExt}; use futures01::Future; -use keys::{hash::H256, AddressHashEnum, CompactSignature, KeyPair, Private, Public}; -use lightning::chain::channelmonitor::Balance; +use keys::{hash::H256, CompactSignature, KeyPair, Private, Public}; 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::utils::DefaultRouter; +use lightning_invoice::{payment, CreationError, InvoiceBuilder, SignOrCreationError}; 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_conf::{LightningCoinConf, PlatformCoinConfirmationTargets}; +use ln_db::{DBChannelDetails, HTLCStatus, LightningDB, PaymentInfo, PaymentType}; +use ln_errors::{EnableLightningError, EnableLightningResult}; 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 ln_filesystem_persister::LightningFilesystemPersister; +use ln_p2p::PeerManager; +use ln_platform::Platform; +use ln_serialization::{ChannelDetailsForRPC, PublicKeyForRPC}; +use ln_sql::SqliteLightningDB; +use ln_storage::{NetworkGraph, NodesAddressesMapShared, Scorer, TrustedNodesShared}; +use ln_utils::{ChainMonitor, ChannelManager, Router}; 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 script::TransactionInputSigner; +use secp256k1v22::PublicKey; +use serde::Deserialize; use serde_json::Value as Json; -use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; +use std::convert::TryInto; use std::fmt; use std::net::SocketAddr; use std::str::FromStr; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; + +const WAIT_FOR_REFUND_INTERVAL: f64 = 60.; +pub const DEFAULT_INVOICE_EXPIRY: u32 = 3600; -type Router = DefaultRouter, Arc>; -type InvoicePayer = payment::InvoicePayer, Router, Arc>, Arc, E>; +pub type InvoicePayer = payment::InvoicePayer, Router, Arc, Arc, E>; #[derive(Clone)] pub struct LightningCoin { pub platform: Arc, pub conf: LightningCoinConf, + /// The lightning node background processor that takes care of tasks that need to happen periodically. + pub background_processor: Arc, /// 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, @@ -93,25 +100,76 @@ pub struct LightningCoin { /// 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, + pub persister: Arc, + /// The lightning node db struct that takes care of reading/writing data from/to db. + pub db: SqliteLightningDB, /// 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, + /// The mutex storing the public keys of the nodes that our lightning node trusts to allow 0 confirmation + /// inbound channels from. + pub trusted_nodes: TrustedNodesShared, + /// The lightning node router that takes care of finding routes for payments. + // Todo: this should be removed once pay_invoice_with_max_total_cltv_expiry_delta similar functionality is implemented in rust-lightning + pub router: Arc, + /// The lightning node scorer that takes care of scoring routes. Given the uncertainty of channel liquidity balances, + /// the scorer stores the probabilities that a route is successful based on knowledge learned from successful and unsuccessful attempts. + // Todo: this should be removed once pay_invoice_with_max_total_cltv_expiry_delta similar functionality is implemented in rust-lightning + pub scorer: Arc, } impl fmt::Debug for LightningCoin { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "LightningCoin {{ conf: {:?} }}", self.conf) } } +#[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 is_ready: Option, + pub is_usable: Option, + pub is_public: Option, +} + +pub(crate) struct GetOpenChannelsResult { + pub channels: Vec, + pub skipped: usize, + pub total: usize, +} + +impl Transaction for PaymentHash { + fn tx_hex(&self) -> Vec { self.0.to_vec() } + + fn tx_hash(&self) -> BytesJson { self.0.to_vec().into() } +} + impl LightningCoin { - fn platform_coin(&self) -> &UtxoStandardCoin { &self.platform.coin } + pub fn platform_coin(&self) -> &UtxoStandardCoin { &self.platform.coin } + + #[inline] + fn avg_blocktime(&self) -> u64 { self.platform.avg_blocktime } #[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() + pub(crate) async fn list_channels(&self) -> Vec { + let channel_manager = self.channel_manager.clone(); + async_blocking(move || channel_manager.list_channels()).await + } + + async fn get_balance_msat(&self) -> (u64, u64) { + self.list_channels() + .await .iter() .fold((0, 0), |(spendable, unspendable), chan| { if chan.is_usable { @@ -125,11 +183,19 @@ impl LightningCoin { }) } - 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()); + pub(crate) async fn get_channel_by_rpc_id(&self, rpc_id: u64) -> Option { + self.list_channels() + .await + .into_iter() + .find(|chan| chan.user_channel_id == rpc_id) + } + + pub(crate) async fn pay_invoice( + &self, + invoice: Invoice, + max_total_cltv_expiry_delta: Option, + ) -> Result> { + 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()), }; @@ -137,68 +203,166 @@ impl LightningCoin { 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, - }) + let amt_msat = invoice.amount_milli_satoshis().map(|a| a as i64); + + let selfi = self.clone(); + match max_total_cltv_expiry_delta { + Some(total_cltv) => { + async_blocking(move || { + pay_invoice_with_max_total_cltv_expiry_delta( + selfi.channel_manager, + selfi.router, + selfi.scorer, + &invoice, + total_cltv, + ) + }) + .await? + }, + None => async_blocking(move || selfi.invoice_payer.pay_invoice(&invoice)).await?, + }; + + let payment_info = PaymentInfo::new(payment_hash, payment_type, description, amt_msat); + self.db.add_payment_to_db(&payment_info).await?; + Ok(payment_info) } - fn keysend( + pub(crate) async fn keysend( &self, destination: PublicKey, amount_msat: u64, final_cltv_expiry_delta: u32, - ) -> SendPaymentResult { + ) -> Result> { if final_cltv_expiry_delta < MIN_FINAL_CLTV_EXPIRY { - return MmError::err(SendPaymentError::CLTVExpiryError( - final_cltv_expiry_delta, - MIN_FINAL_CLTV_EXPIRY, - )); + return MmError::err(PaymentError::CLTVExpiry(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 selfi = self.clone(); + async_blocking(move || { + selfi + .invoice_payer + .pay_pubkey(destination, payment_preimage, amount_msat, final_cltv_expiry_delta) + .map_to_mm(|e| PaymentError::Keysend(format!("{:?}", e))) + }) + .await?; + let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).into_inner()); let payment_type = PaymentType::OutboundPayment { destination }; + let payment_info = PaymentInfo::new(payment_hash, payment_type, "".into(), Some(amount_msat as i64)); + self.db.add_payment_to_db(&payment_info).await?; - 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, - }) + Ok(payment_info) } - async fn get_open_channels_by_filter( + pub(crate) 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(); + ) -> GetOpenChannelsResult { + fn apply_open_channel_filter(channel_details: &ChannelDetailsForRPC, filter: &OpenChannelsFilter) -> bool { + // Checking if channel_id is some and not equal + if filter.channel_id.is_some() && Some(&channel_details.channel_id) != filter.channel_id.as_ref() { + return false; + } + + // Checking if counterparty_node_id is some and not equal + if filter.counterparty_node_id.is_some() + && Some(&channel_details.counterparty_node_id) != filter.counterparty_node_id.as_ref() + { + return false; + } + + // Checking if funding_tx is some and not equal + if filter.funding_tx.is_some() && channel_details.funding_tx != filter.funding_tx { + return false; + } + + // Checking if from_funding_value_sats is some and more than funding_tx_value_sats + if filter.from_funding_value_sats.is_some() + && Some(&channel_details.funding_tx_value_sats) < filter.from_funding_value_sats.as_ref() + { + return false; + } + + // Checking if to_funding_value_sats is some and less than funding_tx_value_sats + if filter.to_funding_value_sats.is_some() + && Some(&channel_details.funding_tx_value_sats) > filter.to_funding_value_sats.as_ref() + { + return false; + } + + // Checking if is_outbound is some and not equal + if filter.is_outbound.is_some() && Some(&channel_details.is_outbound) != filter.is_outbound.as_ref() { + return false; + } + + // Checking if from_balance_msat is some and more than balance_msat + if filter.from_balance_msat.is_some() + && Some(&channel_details.balance_msat) < filter.from_balance_msat.as_ref() + { + return false; + } + + // Checking if to_balance_msat is some and less than balance_msat + if filter.to_balance_msat.is_some() && Some(&channel_details.balance_msat) > filter.to_balance_msat.as_ref() + { + return false; + } + + // Checking if from_outbound_capacity_msat is some and more than outbound_capacity_msat + if filter.from_outbound_capacity_msat.is_some() + && Some(&channel_details.outbound_capacity_msat) < filter.from_outbound_capacity_msat.as_ref() + { + return false; + } + + // Checking if to_outbound_capacity_msat is some and less than outbound_capacity_msat + if filter.to_outbound_capacity_msat.is_some() + && Some(&channel_details.outbound_capacity_msat) > filter.to_outbound_capacity_msat.as_ref() + { + return false; + } + + // Checking if from_inbound_capacity_msat is some and more than outbound_capacity_msat + if filter.from_inbound_capacity_msat.is_some() + && Some(&channel_details.inbound_capacity_msat) < filter.from_inbound_capacity_msat.as_ref() + { + return false; + } + + // Checking if to_inbound_capacity_msat is some and less than inbound_capacity_msat + if filter.to_inbound_capacity_msat.is_some() + && Some(&channel_details.inbound_capacity_msat) > filter.to_inbound_capacity_msat.as_ref() + { + return false; + } + + // Checking if is_ready is some and not equal + if filter.is_ready.is_some() && Some(&channel_details.is_ready) != filter.is_ready.as_ref() { + return false; + } + + // Checking if is_usable is some and not equal + if filter.is_usable.is_some() && Some(&channel_details.is_usable) != filter.is_usable.as_ref() { + return false; + } + + // Checking if is_public is some and not equal + if filter.is_public.is_some() && Some(&channel_details.is_public) != filter.is_public.as_ref() { + return false; + } + + // All checks pass + true + } + + let mut total_open_channels: Vec = + self.list_channels().await.into_iter().map(From::from).collect(); total_open_channels.sort_by(|a, b| a.rpc_channel_id.cmp(&b.rpc_channel_id)); + drop_mutability!(total_open_channels); let open_channels_filtered = if let Some(ref f) = filter { total_open_channels @@ -226,161 +390,612 @@ impl LightningCoin { open_channels_filtered[offset..].to_vec() }; - Ok(GetOpenChannelsResult { + GetOpenChannelsResult { channels, skipped: offset, total, - }) + } + } + + async fn create_invoice_for_hash( + &self, + payment_hash: PaymentHash, + amt_msat: Option, + description: String, + min_final_cltv_expiry: u64, + invoice_expiry_delta_secs: u32, + ) -> Result>> { + let open_channels_nodes = self.open_channels_nodes.lock().clone(); + for (node_pubkey, node_addr) in open_channels_nodes { + ln_p2p::connect_to_ln_node(node_pubkey, node_addr, self.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 + )); + } + + // `create_inbound_payment` only returns an error if the amount is greater than the total bitcoin + // supply. + let payment_secret = self + .channel_manager + .create_inbound_payment_for_hash(payment_hash, amt_msat, invoice_expiry_delta_secs) + .map_to_mm(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?; + let our_node_pubkey = self.channel_manager.get_our_node_id(); + // Todo: Check if it's better to use UTC instead of local time for invoice generations + let duration = get_local_duration_since_epoch().expect("for the foreseeable future this shouldn't happen"); + + let mut invoice = InvoiceBuilder::new(self.platform.network.clone().into()) + .description(description) + .duration_since_epoch(duration) + .payee_pub_key(our_node_pubkey) + .payment_hash(Hash::from_inner(payment_hash.0)) + .payment_secret(payment_secret) + .basic_mpp() + // Todo: This should be validated by the other side, right now this is not validated by rust-lightning and the PaymentReceived event doesn't include the final cltv of the payment for us to validate it + // Todo: This needs a PR opened to rust-lightning, I already contacted them about it and there is an issue opened for it https://github.com/lightningdevkit/rust-lightning/issues/1850 + .min_final_cltv_expiry(min_final_cltv_expiry) + .expiry_time(core::time::Duration::from_secs(invoice_expiry_delta_secs.into())); + if let Some(amt) = amt_msat { + invoice = invoice.amount_milli_satoshis(amt); + } + + let route_hints = filter_channels(self.channel_manager.list_usable_channels(), amt_msat); + for hint in route_hints { + invoice = invoice.private_route(hint); + } + + let raw_invoice = match invoice.build_raw() { + Ok(inv) => inv, + Err(e) => return MmError::err(SignOrCreationError::CreationError(e)), + }; + let hrp_str = raw_invoice.hrp.to_string(); + let hrp_bytes = hrp_str.as_bytes(); + let data_without_signature = raw_invoice.data.to_base32(); + let signed_raw_invoice = raw_invoice.sign(|_| { + self.keys_manager + .sign_invoice(hrp_bytes, &data_without_signature, Recipient::Node) + }); + match signed_raw_invoice { + Ok(inv) => Ok(Invoice::from_signed(inv).map_err(|_| SignOrCreationError::SignError(()))?), + Err(e) => MmError::err(SignOrCreationError::SignError(e)), + } + } + + fn estimate_blocks_from_duration(&self, duration: u64) -> u64 { duration / self.avg_blocktime() } + + async fn swap_payment_instructions( + &self, + secret_hash: &[u8], + amount: &BigDecimal, + expires_in: u64, + min_final_cltv_expiry: u64, + ) -> Result, MmError> { + // lightning decimals should be 11 in config since the smallest divisible unit in lightning coin is msat + let amt_msat = sat_from_big_decimal(amount, self.decimals())?; + let payment_hash = + payment_hash_from_slice(secret_hash).map_to_mm(|e| PaymentInstructionsErr::InternalError(e.to_string()))?; + // note: No description is provided in the invoice to reduce the payload + let invoice = self + .create_invoice_for_hash( + payment_hash, + Some(amt_msat), + "".into(), + min_final_cltv_expiry, + expires_in.try_into().expect("expires_in shouldn't exceed u32::MAX"), + ) + .await + .map_err(|e| PaymentInstructionsErr::LightningInvoiceErr(e.to_string()))?; + Ok(invoice.to_string().into_bytes()) + } + + fn validate_swap_instructions( + &self, + instructions: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + min_final_cltv_expiry: u64, + ) -> Result> { + let invoice = Invoice::from_str(&String::from_utf8_lossy(instructions))?; + if invoice.payment_hash().as_inner() != secret_hash + && ripemd160(invoice.payment_hash().as_inner()).as_slice() != secret_hash + { + return MmError::err(ValidateInstructionsErr::ValidateLightningInvoiceErr( + "Invalid invoice payment hash!".into(), + )); + } + + let invoice_amount = invoice + .amount_milli_satoshis() + .or_mm_err(|| ValidateInstructionsErr::ValidateLightningInvoiceErr("No invoice amount!".into()))?; + if big_decimal_from_sat(invoice_amount as i64, self.decimals()) != amount { + return MmError::err(ValidateInstructionsErr::ValidateLightningInvoiceErr( + "Invalid invoice amount!".into(), + )); + } + + if invoice.min_final_cltv_expiry() != min_final_cltv_expiry { + return MmError::err(ValidateInstructionsErr::ValidateLightningInvoiceErr( + "Invalid invoice min_final_cltv_expiry!".into(), + )); + } + + Ok(PaymentInstructions::Lightning(invoice)) + } + + fn spend_swap_payment(&self, spend_payment_args: SendSpendPaymentArgs<'_>) -> TransactionFut { + let payment_hash = try_tx_fus!(payment_hash_from_slice(spend_payment_args.other_payment_tx)); + let mut preimage = [b' '; 32]; + preimage.copy_from_slice(spend_payment_args.secret); + + let coin = self.clone(); + let fut = async move { + let payment_preimage = PaymentPreimage(preimage); + coin.channel_manager.claim_funds(payment_preimage); + coin.db + .update_payment_preimage_in_db(payment_hash, payment_preimage) + .await + .error_log_with_msg(&format!( + "Unable to update payment {} information in DB with preimage: {}!", + hex::encode(payment_hash.0), + hex::encode(preimage) + )); + Ok(TransactionEnum::LightningPayment(payment_hash)) + }; + Box::new(fut.boxed().compat()) + } + + fn validate_swap_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + let payment_hash = try_f!(payment_hash_from_slice(&input.payment_tx) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))); + let payment_hex = hex::encode(payment_hash.0); + + let amt_msat = try_f!(sat_from_big_decimal(&input.amount, self.decimals())); + + let coin = self.clone(); + let fut = async move { + match coin.db.get_payment_from_db(payment_hash).await { + Ok(Some(payment)) => { + let amount_received = payment.amt_msat; + // Note: locktime doesn't need to be validated since min_final_cltv_expiry should be validated in rust-lightning after fixing the below issue + // https://github.com/lightningdevkit/rust-lightning/issues/1850 + // Also, PaymentReceived won't be fired if amount_received < the amount requested in the invoice, this check is probably not needed. + // But keeping it just in case any changes happen in rust-lightning + if amount_received != Some(amt_msat as i64) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided payment {} amount {:?} doesn't match required amount {}", + payment_hex, amount_received, amt_msat + ))); + } + Ok(()) + }, + Ok(None) => MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( + "Payment {} is not in the database when it should be!", + payment_hex + ))), + Err(e) => MmError::err(ValidatePaymentError::InternalError(format!( + "Unable to retrieve payment {} from the database error: {}", + payment_hex, e + ))), + } + }; + Box::new(fut.boxed().compat()) + } + + async fn on_swap_refund(&self, payment: &[u8]) -> RefundResult<()> { + let payment_hash = payment_hash_from_slice(payment).map_err(|e| RefundError::DecodeErr(e.to_string()))?; + // Free the htlc to allow for this inbound liquidity to be used for other inbound payments + self.channel_manager.fail_htlc_backwards(&payment_hash); + self.db + .update_payment_status_in_db(payment_hash, &HTLCStatus::Failed) + .await + .map_to_mm(|e| RefundError::DbError(e.to_string())) } } #[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!() } + // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning + fn send_taker_fee(&self, _fee_addr: &[u8], _amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { + let fut = async move { Ok(TransactionEnum::LightningPayment(PaymentHash([1; 32]))) }; + Box::new(fut.boxed().compat()) + } - 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_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs<'_>) -> TransactionFut { + let PaymentInstructions::Lightning(invoice) = try_tx_fus!(maker_payment_args + .payment_instructions + .clone() + .ok_or("payment_instructions can't be None")); + let coin = self.clone(); + let fut = async move { + // No need for max_total_cltv_expiry_delta for lightning maker payment since the maker is the side that reveals the secret/preimage + let payment = try_tx_s!(coin.pay_invoice(invoice, None).await); + Ok(payment.payment_hash.into()) + }; + Box::new(fut.boxed().compat()) } - 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_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs<'_>) -> TransactionFut { + let PaymentInstructions::Lightning(invoice) = try_tx_fus!(taker_payment_args + .payment_instructions + .clone() + .ok_or("payment_instructions can't be None")); + let max_total_cltv_expiry_delta = self + .estimate_blocks_from_duration(taker_payment_args.time_lock_duration) + .try_into() + .expect("max_total_cltv_expiry_delta shouldn't exceed u32::MAX"); + let coin = self.clone(); + let fut = async move { + // Todo: The path/s used is already logged when PaymentPathSuccessful/PaymentPathFailed events are fired, it might be better to save it to the DB and retrieve it with the payment info. + let payment = try_tx_s!(coin.pay_invoice(invoice, Some(max_total_cltv_expiry_delta)).await); + Ok(payment.payment_hash.into()) + }; + Box::new(fut.boxed().compat()) } + #[inline] 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], + maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs<'_>, ) -> TransactionFut { - unimplemented!() + self.spend_swap_payment(maker_spends_payment_args) } + #[inline] 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], + taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs<'_>, ) -> TransactionFut { - unimplemented!() + self.spend_swap_payment(taker_spends_payment_args) } 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], + _taker_refunds_payment_args: SendTakerRefundsPaymentArgs<'_>, ) -> TransactionFut { - unimplemented!() + Box::new(futures01::future::err(TransactionErr::Plain( + "Doesn't need transaction broadcast to refund lightning HTLC".into(), + ))) } 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], + _maker_refunds_payment_args: SendMakerRefundsPaymentArgs<'_>, ) -> TransactionFut { - unimplemented!() + Box::new(futures01::future::err(TransactionErr::Plain( + "Doesn't need transaction broadcast to refund lightning HTLC".into(), + ))) } + // Todo: This validates the dummy fee for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning fn validate_fee( &self, - _fee_tx: &TransactionEnum, - _expected_sender: &[u8], - _fee_addr: &[u8], - _amount: &BigDecimal, - _min_block_number: u64, - _uuid: &[u8], + _validate_fee_args: ValidateFeeArgs<'_>, ) -> Box + Send> { - unimplemented!() + Box::new(futures01::future::ok(())) } - fn validate_maker_payment( - &self, - _input: ValidatePaymentInput, - ) -> Box + Send> { - unimplemented!() + #[inline] + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + self.validate_swap_payment(input) } - fn validate_taker_payment( - &self, - _input: ValidatePaymentInput, - ) -> Box + Send> { - unimplemented!() + #[inline] + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + self.validate_swap_payment(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], + if_my_payment_sent_args: CheckIfMyPaymentSentArgs<'_>, ) -> Box, Error = String> + Send> { - unimplemented!() + let PaymentInstructions::Lightning(invoice) = try_f!(if_my_payment_sent_args + .payment_instructions + .clone() + .ok_or("payment_instructions can't be None")); + let payment_hash = PaymentHash((invoice.payment_hash()).into_inner()); + let payment_hex = hex::encode(payment_hash.0); + let coin = self.clone(); + let fut = async move { + match coin.db.get_payment_from_db(payment_hash).await { + Ok(maybe_payment) => Ok(maybe_payment.map(|p| p.payment_hash.into())), + Err(e) => ERR!( + "Unable to check if payment {} is in db or not error: {}", + payment_hex, + e + ), + } + }; + Box::new(fut.boxed().compat()) } + // Todo: need to also check on-chain spending async fn search_for_swap_tx_spend_my( &self, - _: SearchForSwapTxSpendInput<'_>, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { - unimplemented!() + let payment_hash = payment_hash_from_slice(input.tx).map_err(|e| e.to_string())?; + let payment_hex = hex::encode(payment_hash.0); + match self.db.get_payment_from_db(payment_hash).await { + Ok(Some(payment)) => { + if !payment.is_outbound() { + return ERR!("Payment {} should be an outbound payment!", payment_hex); + } + match payment.status { + HTLCStatus::Pending => Ok(None), + HTLCStatus::Succeeded => Ok(Some(FoundSwapTxSpend::Spent(TransactionEnum::LightningPayment( + payment_hash, + )))), + HTLCStatus::Received => { + ERR!( + "Payment {} has an invalid status of {} in the db", + payment_hex, + payment.status + ) + }, + HTLCStatus::Failed => Ok(Some(FoundSwapTxSpend::Refunded(TransactionEnum::LightningPayment( + payment_hash, + )))), + } + }, + Ok(None) => ERR!("Payment {} is not in the database when it should be!", payment_hex), + Err(e) => ERR!( + "Unable to retrieve payment {} from the database error: {}", + payment_hex, + e + ), + } } + // Todo: need to also check on-chain spending async fn search_for_swap_tx_spend_other( &self, - _: SearchForSwapTxSpendInput<'_>, + input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { - unimplemented!() + let payment_hash = payment_hash_from_slice(input.tx).map_err(|e| e.to_string())?; + let payment_hex = hex::encode(payment_hash.0); + match self.db.get_payment_from_db(payment_hash).await { + Ok(Some(payment)) => { + if payment.is_outbound() { + return ERR!("Payment {} should be an inbound payment!", payment_hex); + } + match payment.status { + HTLCStatus::Pending | HTLCStatus::Received => Ok(None), + HTLCStatus::Succeeded => Ok(Some(FoundSwapTxSpend::Spent(TransactionEnum::LightningPayment( + payment_hash, + )))), + HTLCStatus::Failed => Ok(Some(FoundSwapTxSpend::Refunded(TransactionEnum::LightningPayment( + payment_hash, + )))), + } + }, + Ok(None) => ERR!("Payment {} is not in the database when it should be!", payment_hex), + Err(e) => ERR!( + "Unable to retrieve payment {} from the database error: {}", + payment_hex, + e + ), + } + } + + fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { + unimplemented!(); } - fn extract_secret(&self, _secret_hash: &[u8], _spend_tx: &[u8]) -> Result, String> { unimplemented!() } + async fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + let payment_hash = payment_hash_from_slice(spend_tx).map_err(|e| e.to_string())?; + let payment_hex = hex::encode(payment_hash.0); + + match self.db.get_payment_from_db(payment_hash).await { + Ok(Some(payment)) => match payment.preimage { + Some(preimage) => Ok(preimage.0.to_vec()), + None => ERR!("Preimage for payment {} should be found on the database", payment_hex), + }, + Ok(None) => ERR!("Payment {} is not in the database when it should be!", payment_hex), + Err(e) => ERR!( + "Unable to retrieve payment {} from the database error: {}", + payment_hex, + e + ), + } + } + + fn is_auto_refundable(&self) -> bool { true } + + async fn wait_for_htlc_refund(&self, tx: &[u8], locktime: u64) -> RefundResult<()> { + let payment_hash = payment_hash_from_slice(tx).map_err(|e| RefundError::DecodeErr(e.to_string()))?; + let payment_hex = hex::encode(payment_hash.0); + repeatable!(async { + match self.db.get_payment_from_db(payment_hash).await { + Ok(Some(payment)) => match payment.status { + HTLCStatus::Failed => Ready(Ok(())), + HTLCStatus::Pending => Retry(()), + _ => Ready(MmError::err(RefundError::Internal(ERRL!( + "Payment {} has an invalid status of {} in the db", + payment_hex, + payment.status + )))), + }, + Ok(None) => Ready(MmError::err(RefundError::Internal(ERRL!( + "Payment {} is not in the database when it should be!", + payment_hex + )))), + Err(e) => Ready(MmError::err(RefundError::DbError(ERRL!( + "Error getting payment {} from db: {}", + payment_hex, + e + )))), + } + }) + .repeat_every_secs(WAIT_FOR_REFUND_INTERVAL) + .until_s(locktime) + .await + .map_err(|e| RefundError::Timeout(format!("{:?}", e)))? + } fn negotiate_swap_contract_addr( &self, _other_side_address: Option<&[u8]>, ) -> Result, MmError> { - unimplemented!() + Ok(None) + } + + // Todo: This can be changed if private swaps were to be implemented for lightning + 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) + } + + #[inline] + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { + self.channel_manager.get_our_node_id().serialize().to_vec() + } + + #[inline] + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + utxo_common::validate_other_pubkey(raw_pubkey) } - fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { unimplemented!() } + async fn maker_payment_instructions( + &self, + secret_hash: &[u8], + amount: &BigDecimal, + maker_lock_duration: u64, + expires_in: u64, + ) -> Result>, MmError> { + let min_final_cltv_expiry = self.estimate_blocks_from_duration(maker_lock_duration); + self.swap_payment_instructions(secret_hash, amount, expires_in, min_final_cltv_expiry) + .await + .map(Some) + } + + #[inline] + async fn taker_payment_instructions( + &self, + secret_hash: &[u8], + amount: &BigDecimal, + expires_in: u64, + ) -> Result>, MmError> { + self.swap_payment_instructions(secret_hash, amount, expires_in, MIN_FINAL_CLTV_EXPIRY as u64) + .await + .map(Some) + } + + fn validate_maker_payment_instructions( + &self, + instructions: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + maker_lock_duration: u64, + ) -> Result> { + let min_final_cltv_expiry = self.estimate_blocks_from_duration(maker_lock_duration); + self.validate_swap_instructions(instructions, secret_hash, amount, min_final_cltv_expiry) + } + + #[inline] + fn validate_taker_payment_instructions( + &self, + instructions: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + ) -> Result> { + self.validate_swap_instructions(instructions, secret_hash, amount, MIN_FINAL_CLTV_EXPIRY as u64) + } + + fn maker_locktime_multiplier(&self) -> f64 { 1.5 } +} + +#[async_trait] +impl TakerSwapMakerCoin for LightningCoin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, maker_payment: &[u8]) -> RefundResult<()> { + self.on_swap_refund(maker_payment).await + } +} + +#[async_trait] +impl MakerSwapTakerCoin for LightningCoin { + async fn on_maker_payment_refund_start(&self, taker_payment: &[u8]) -> RefundResult<()> { + self.on_swap_refund(taker_payment).await + } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[derive(Debug, Display)] +pub enum PaymentHashFromSliceErr { + #[display(fmt = "Invalid data length of {}", _0)] + InvalidLength(usize), +} + +fn payment_hash_from_slice(data: &[u8]) -> Result { + let len = data.len(); + if len != 32 { + return Err(PaymentHashFromSliceErr::InvalidLength(len)); + } + let mut hash = [b' '; 32]; + hash.copy_from_slice(data); + Ok(PaymentHash(hash)) +} + +#[async_trait] +impl WatcherOps for LightningCoin { + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &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_taker_payment_refund_preimage( + &self, + _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + async fn watcher_search_for_swap_tx_spend( + &self, + _input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } } impl MarketCoinOps for LightningCoin { fn ticker(&self) -> &str { &self.conf.ticker } - fn my_address(&self) -> Result { Ok(self.my_node_id()) } + fn my_address(&self) -> MmResult { Ok(self.my_node_id()) } - fn get_public_key(&self) -> Result> { unimplemented!() } + fn get_public_key(&self) -> Result> { Ok(self.my_node_id()) } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { let mut _message_prefix = self.conf.sign_message_prefix.clone()?; @@ -401,7 +1016,7 @@ impl MarketCoinOps for LightningCoin { checksum_type: ChecksumType::DSHA256, }; let signature = private.sign_compact(&H256::from(message_hash))?; - Ok(zbase32::encode_full_bytes(&*signature)) + Ok(zbase32::encode_full_bytes(&signature)) } fn verify_message(&self, signature: &str, message: &str, pubkey: &str) -> VerificationResult { @@ -416,14 +1031,19 @@ impl MarketCoinOps for LightningCoin { Ok(recovered_pubkey.to_string() == pubkey) } + // Todo: max_inbound_in_flight_htlc_percent should be taken in consideration too for max allowed amount, this can be considered the spendable balance, + // Todo: but it's better to refactor the CoinBalance struct to add more info. We can make it 100% in the config for now until this is implemented. fn my_balance(&self) -> BalanceFut { + let coin = self.clone(); 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), + let fut = async move { + let (spendable_msat, unspendable_msat) = coin.get_balance_msat().await; + Ok(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)) + Box::new(fut.boxed().compat()) } fn base_coin_balance(&self) -> BalanceFut { @@ -450,31 +1070,132 @@ impl MarketCoinOps for LightningCoin { )) } - // Todo: Implement this when implementing swaps for lightning as it's is used mainly for swaps + // Todo: Add waiting for confirmations logic for the case of if the channel is closed and the htlc can be claimed on-chain fn wait_for_confirmations( &self, - _tx: &[u8], + tx: &[u8], _confirmations: u64, _requires_nota: bool, - _wait_until: u64, - _check_every: u64, + wait_until: u64, + check_every: u64, ) -> Box + Send> { - unimplemented!() + let payment_hash = try_f!(payment_hash_from_slice(tx).map_err(|e| e.to_string())); + let payment_hex = hex::encode(payment_hash.0); + + let coin = self.clone(); + let fut = async move { + loop { + if now_ms() / 1000 > wait_until { + return ERR!( + "Waited too long until {} for payment {} to be received", + wait_until, + payment_hex + ); + } + + match coin.db.get_payment_from_db(payment_hash).await { + Ok(Some(payment)) => { + match payment.payment_type { + PaymentType::OutboundPayment { .. } => match payment.status { + HTLCStatus::Pending | HTLCStatus::Succeeded => return Ok(()), + HTLCStatus::Received => { + return ERR!( + "Payment {} has an invalid status of {} in the db", + payment_hex, + payment.status + ) + }, + // Todo: PaymentFailed event is fired after 5 retries, maybe timeout should be used instead. + // Todo: Still this doesn't prevent failure if there are no routes + // Todo: JIT channels/routing can be used to solve this issue https://github.com/lightningdevkit/rust-lightning/pull/1835 but it requires some trust. + HTLCStatus::Failed => return ERR!("Lightning swap payment {} failed", payment_hex), + }, + PaymentType::InboundPayment => match payment.status { + HTLCStatus::Received | HTLCStatus::Succeeded => return Ok(()), + HTLCStatus::Pending => info!("Payment {} not received yet!", payment_hex), + HTLCStatus::Failed => return ERR!("Lightning swap payment {} failed", payment_hex), + }, + } + }, + Ok(None) => info!("Payment {} not received yet!", payment_hex), + Err(e) => return ERR!("Error getting payment {} from db: {}", payment_hex, e), + } + + // note: When sleeping for only 1 second the test_send_payment_and_swaps unit test took 20 seconds to complete instead of 37 seconds when WAIT_CONFIRM_INTERVAL (15 seconds) is used + // Todo: In next sprints, should add a mutex for lightning swap payments to avoid overloading the shared db connection with requests when the sleep time is reduced and multiple swaps are ran together + // Todo: The aim is to make lightning swap payments as fast as possible. Running swap payments statuses should be loaded from db on restarts in this case. + Timer::sleep(check_every as f64).await; + } + }; + Box::new(fut.boxed().compat()) } - // Todo: Implement this when implementing swaps for lightning as it's is used mainly for swaps - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, - _transaction: &[u8], - _wait_until: u64, + transaction: &[u8], + _secret_hash: &[u8], + wait_until: u64, _from_block: u64, _swap_contract_address: &Option, + _check_every: f64, ) -> TransactionFut { - unimplemented!() + let payment_hash = try_tx_fus!(payment_hash_from_slice(transaction)); + let payment_hex = hex::encode(payment_hash.0); + + let coin = self.clone(); + let fut = async move { + loop { + if now_ms() / 1000 > wait_until { + return Err(TransactionErr::Plain(ERRL!( + "Waited too long until {} for payment {} to be spent", + wait_until, + payment_hex + ))); + } + + match coin.db.get_payment_from_db(payment_hash).await { + Ok(Some(payment)) => match payment.status { + HTLCStatus::Pending => (), + HTLCStatus::Received => { + return Err(TransactionErr::Plain(ERRL!( + "Payment {} has an invalid status of {} in the db", + payment_hex, + payment.status + ))) + }, + HTLCStatus::Succeeded => return Ok(TransactionEnum::LightningPayment(payment_hash)), + HTLCStatus::Failed => { + return Err(TransactionErr::Plain(ERRL!( + "Lightning swap payment {} failed", + payment_hex + ))) + }, + }, + Ok(None) => return Err(TransactionErr::Plain(ERRL!("Payment {} not found in DB", payment_hex))), + Err(e) => { + return Err(TransactionErr::Plain(ERRL!( + "Error getting payment {} from db: {}", + payment_hex, + e + ))) + }, + } + + // note: When sleeping for only 1 second the test_send_payment_and_swaps unit test took 20 seconds to complete instead of 37 seconds when sleeping for 10 seconds + // Todo: In next sprints, should add a mutex for lightning swap payments to avoid overloading the shared db connection with requests when the sleep time is reduced and multiple swaps are ran together. + // Todo: The aim is to make lightning swap payments as fast as possible, more sleep time can be allowed for maker payment since it waits for the secret to be revealed on another chain first. + // Todo: Running swap payments statuses should be loaded from db on restarts in this case. + Timer::sleep(10.).await; + } + }; + Box::new(fut.boxed().compat()) } - // 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 tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { + Ok(TransactionEnum::LightningPayment( + payment_hash_from_slice(bytes).map_to_mm(|e| TxMarshalingErr::InvalidInput(e.to_string()))?, + )) + } fn current_block(&self) -> Box + Send> { Box::new(futures01::future::ok(0)) } @@ -483,22 +1204,39 @@ impl MarketCoinOps for LightningCoin { .keys_manager .get_node_secret(Recipient::Node) .map_err(|_| "Unsupported recipient".to_string())? + .display_secret() .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: min_tx_amount should depend on inbound_htlc_minimum_msat of the channel/s the payment will be sent through, 1 satoshi is used for for now (1000 of the base unit of lightning which is msat) + fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat(1000, self.decimals()) } - // 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!() } + // Todo: Equals to min_tx_amount for now (1 satoshi), should change this later + fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } } #[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 spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.platform.abortable_system) } + + fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { + let fut = async move { + MmError::err(RawTransactionError::InternalError( + "get_raw_transaction method is not supported for lightning, please use get_payment_details method instead.".into(), + )) + }; + Box::new(fut.boxed().compat()) + } + + fn get_tx_hex_by_hash(&self, _tx_hash: Vec) -> RawTransactionFut { + let fut = async move { + MmError::err(RawTransactionError::InternalError( + "get_tx_hex_by_hash method is not supported for lightning.".into(), + )) + }; + Box::new(fut.boxed().compat()) } fn withdraw(&self, _req: WithdrawRequest) -> WithdrawFut { @@ -538,25 +1276,39 @@ impl MmCoin for LightningCoin { // 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 + // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning async fn get_sender_trade_fee( &self, _value: TradePreimageValue, _stage: FeeApproxStage, ) -> TradePreimageResult { - unimplemented!() + Ok(TradeFee { + coin: self.ticker().to_owned(), + amount: Default::default(), + paid_from_trading_vol: false, + }) } - // 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: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { + Box::new(futures01::future::ok(TradeFee { + coin: self.ticker().to_owned(), + amount: Default::default(), + paid_from_trading_vol: false, + })) + } - // Todo: Implement this when implementing swaps for lightning as it's is used only for swaps + // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning async fn get_fee_to_send_taker_fee( &self, _dex_fee_amount: BigDecimal, _stage: FeeApproxStage, ) -> TradePreimageResult { - unimplemented!() + Ok(TradeFee { + coin: self.ticker().to_owned(), + amount: Default::default(), + paid_from_trading_vol: false, + }) } // Lightning payments are either pending, successful or failed. Once a payment succeeds there is no need to for confirmations @@ -571,1015 +1323,17 @@ impl MmCoin for LightningCoin { 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?; + fn fallback_swap_contract(&self) -> Option { None } - 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)))?; - } + fn mature_confirmations(&self) -> Option { None } - Ok(format!("Initiated closing of channel: {:?}", req.channel_id)) -} + // Todo: This uses default data for now for the sake of swap P.O.C., this should be implemented probably when implementing order matching if it's needed + fn coin_protocol_info(&self) -> Vec { Vec::new() } -/// 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, - }, -} + // Todo: This uses default data for now for the sake of swap P.O.C., this should be implemented probably when implementing order matching if it's needed + fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } -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, -} + fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.platform.abortable_system) } -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) + fn on_token_deactivated(&self, _ticker: &str) {} } diff --git a/mm2src/coins/lightning/ln_conf.rs b/mm2src/coins/lightning/ln_conf.rs index f69a9bca10..dd58946969 100644 --- a/mm2src/coins/lightning/ln_conf.rs +++ b/mm2src/coins/lightning/ln_conf.rs @@ -2,26 +2,20 @@ 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, +pub struct PlatformCoinConfirmationTargets { + pub background: u32, + pub normal: u32, + pub high_priority: u32, } -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct PlatformCoinConfirmations { - pub background: DefaultFeesAndConfirmations, - pub normal: DefaultFeesAndConfirmations, - pub high_priority: DefaultFeesAndConfirmations, -} - -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct LightningProtocolConf { pub platform_coin_ticker: String, pub network: BlockchainNetwork, - pub confirmations: PlatformCoinConfirmations, + pub confirmation_targets: PlatformCoinConfirmationTargets, } -#[derive(Clone, Debug, Deserialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ChannelOptions { /// Amount (in millionths of a satoshi) charged per satoshi for payments forwarded outbound /// over the channel. @@ -30,11 +24,6 @@ pub struct ChannelOptions { /// 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, @@ -44,7 +33,7 @@ pub struct ChannelOptions { } impl ChannelOptions { - pub fn update(&mut self, options: ChannelOptions) { + pub fn update_according_to(&mut self, options: ChannelOptions) { if let Some(fee) = options.proportional_fee_in_millionths_sats { self.proportional_fee_in_millionths_sats = Some(fee); } @@ -57,14 +46,6 @@ impl ChannelOptions { 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); } @@ -91,14 +72,6 @@ impl From for ChannelConfig { 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; } @@ -112,7 +85,7 @@ impl From for ChannelConfig { } #[derive(Clone, Debug, Deserialize)] -pub struct OurChannelsConfig { +pub struct OurChannelsConfigs { /// 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 @@ -124,10 +97,55 @@ pub struct OurChannelsConfig { /// 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, + /// If set, we attempt to negotiate the `scid_privacy` (referred to as `scid_alias` in the + /// BOLTs) option for outbound private channels. This provides better privacy by not including + /// our real on-chain channel UTXO in each invoice and requiring that our counterparty only + /// relay HTLCs to us using the channel's SCID alias. + pub negotiate_scid_privacy: Option, + /// Sets the percentage of the channel value we will cap the total value of outstanding inbound + /// HTLCs to. + pub max_inbound_in_flight_htlc_percent: 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, +} + +impl OurChannelsConfigs { + pub fn update_according_to(&mut self, config: OurChannelsConfigs) { + if let Some(confs) = config.inbound_channels_confirmations { + self.inbound_channels_confirmations = Some(confs); + } + + if let Some(delay) = config.counterparty_locktime { + self.counterparty_locktime = Some(delay); + } + + if let Some(min) = config.our_htlc_minimum_msat { + self.our_htlc_minimum_msat = Some(min); + } + + if let Some(scid_privacy) = config.negotiate_scid_privacy { + self.negotiate_scid_privacy = Some(scid_privacy); + } + + if let Some(max_inbound_htlc) = config.max_inbound_in_flight_htlc_percent { + self.max_inbound_in_flight_htlc_percent = Some(max_inbound_htlc); + } + + if let Some(announce) = config.announced_channel { + self.announced_channel = Some(announce); + } + + if let Some(commit) = config.commit_upfront_shutdown_pubkey { + self.commit_upfront_shutdown_pubkey = Some(commit); + } + } } -impl From for ChannelHandshakeConfig { - fn from(config: OurChannelsConfig) -> Self { +impl From for ChannelHandshakeConfig { + fn from(config: OurChannelsConfigs) -> Self { let mut channel_handshake_config = ChannelHandshakeConfig::default(); if let Some(confs) = config.inbound_channels_confirmations { @@ -142,6 +160,22 @@ impl From for ChannelHandshakeConfig { channel_handshake_config.our_htlc_minimum_msat = min; } + if let Some(scid_privacy) = config.negotiate_scid_privacy { + channel_handshake_config.negotiate_scid_privacy = scid_privacy; + } + + if let Some(max_inbound_htlc) = config.max_inbound_in_flight_htlc_percent { + channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = max_inbound_htlc; + } + + if let Some(announce) = config.announced_channel { + channel_handshake_config.announced_channel = announce; + } + + if let Some(commit) = config.commit_upfront_shutdown_pubkey { + channel_handshake_config.commit_upfront_shutdown_pubkey = commit; + } + channel_handshake_config } } @@ -150,6 +184,8 @@ impl From for ChannelHandshakeConfig { pub struct CounterpartyLimits { /// Minimum allowed satoshis when an inbound channel is funded. pub min_funding_sats: Option, + /// Maximum allowed satoshis when an inbound channel is funded. + pub max_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, @@ -169,6 +205,9 @@ pub struct CounterpartyLimits { 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, + /// When set an outbound channel can be used straight away without waiting for any on-chain confirmations. + /// https://docs.rs/lightning/latest/lightning/util/config/struct.ChannelHandshakeLimits.html#structfield.trust_own_funding_0conf + pub allow_outbound_0conf: Option, } impl From for ChannelHandshakeLimits { @@ -179,6 +218,10 @@ impl From for ChannelHandshakeLimits { channel_handshake_limits.min_funding_satoshis = sats; } + if let Some(sats) = limits.max_funding_sats { + channel_handshake_limits.max_funding_satoshis = sats; + } + if let Some(msat) = limits.max_htlc_minimum_msat { channel_handshake_limits.max_htlc_minimum_msat = msat; } @@ -199,6 +242,10 @@ impl From for ChannelHandshakeLimits { channel_handshake_limits.max_minimum_depth = confs; } + if let Some(is_0conf) = limits.allow_outbound_0conf { + channel_handshake_limits.trust_own_funding_0conf = is_0conf; + } + if let Some(pref) = limits.force_announced_channel_preference { channel_handshake_limits.force_announced_channel_preference = pref; } @@ -219,7 +266,7 @@ pub struct LightningCoinConf { pub accept_inbound_channels: Option, pub accept_forwards_to_priv_channels: Option, pub channel_options: Option, - pub our_channels_config: Option, + pub our_channels_configs: Option, pub counterparty_channel_config_limits: Option, pub sign_message_prefix: Option, } @@ -227,14 +274,14 @@ pub struct LightningCoinConf { 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(config) = conf.our_channels_configs { + user_config.channel_handshake_config = config.into(); } if let Some(limits) = conf.counterparty_channel_config_limits { - user_config.peer_channel_config_limits = limits.into(); + user_config.channel_handshake_limits = limits.into(); } if let Some(options) = conf.channel_options { - user_config.channel_options = options.into(); + user_config.channel_config = options.into(); } if let Some(accept_forwards) = conf.accept_forwards_to_priv_channels { user_config.accept_forwards_to_priv_channels = accept_forwards; diff --git a/mm2src/coins/lightning_persister/src/storage.rs b/mm2src/coins/lightning/ln_db.rs similarity index 63% rename from mm2src/coins/lightning_persister/src/storage.rs rename to mm2src/coins/lightning/ln_db.rs index c0b9ac7e9d..8ad4455a24 100644 --- a/mm2src/coins/lightning_persister/src/storage.rs +++ b/mm2src/coins/lightning/ln_db.rs @@ -1,53 +1,21 @@ 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 lightning::ln::{PaymentHash, PaymentPreimage}; +use secp256k1v22::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 struct DBChannelDetails { + pub rpc_id: i64, 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, + pub funding_value: Option, #[serde(skip_serializing_if = "Option::is_none")] pub closing_tx: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -57,16 +25,16 @@ pub struct SqlChannelDetails { #[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 funding_generated_in_block: Option, pub is_outbound: bool, pub is_public: bool, pub is_closed: bool, - pub created_at: u64, + pub created_at: i64, #[serde(skip_serializing_if = "Option::is_none")] - pub closed_at: Option, + pub closed_at: Option, } -impl SqlChannelDetails { +impl DBChannelDetails { #[inline] pub fn new( rpc_id: u64, @@ -75,8 +43,8 @@ impl SqlChannelDetails { is_outbound: bool, is_public: bool, ) -> Self { - SqlChannelDetails { - rpc_id, + DBChannelDetails { + rpc_id: rpc_id as i64, channel_id: hex::encode(channel_id), counterparty_node_id: counterparty_node_id.to_string(), funding_tx: None, @@ -89,7 +57,7 @@ impl SqlChannelDetails { is_outbound, is_public, is_closed: false, - created_at: now_ms() / 1000, + created_at: (now_ms() / 1000) as i64, closed_at: None, } } @@ -112,8 +80,8 @@ 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 from_funding_value: Option, + pub to_funding_value: Option, pub closing_tx: Option, pub closure_reason: Option, pub claiming_tx: Option, @@ -124,7 +92,7 @@ pub struct ClosedChannelsFilter { } pub struct GetClosedChannelsResult { - pub channels: Vec, + pub channels: Vec, pub skipped: usize, pub total: usize, } @@ -133,6 +101,7 @@ pub struct GetClosedChannelsResult { #[serde(rename_all = "lowercase")] pub enum HTLCStatus { Pending, + Received, Succeeded, Failed, } @@ -143,6 +112,7 @@ impl FromStr for HTLCStatus { fn from_str(s: &str) -> Result { match s { "Pending" => Ok(HTLCStatus::Pending), + "Received" => Ok(HTLCStatus::Received), "Succeeded" => Ok(HTLCStatus::Succeeded), "Failed" => Ok(HTLCStatus::Failed), _ => Err(FromSqlError::InvalidType), @@ -162,25 +132,61 @@ pub struct PaymentInfo { pub payment_type: PaymentType, pub description: String, pub preimage: Option, - pub secret: Option, - pub amt_msat: Option, - pub fee_paid_msat: Option, + pub amt_msat: Option, + pub fee_paid_msat: Option, pub status: HTLCStatus, - pub created_at: u64, - pub last_updated: u64, + pub created_at: i64, + pub last_updated: i64, +} + +impl PaymentInfo { + #[inline] + pub fn new( + payment_hash: PaymentHash, + payment_type: PaymentType, + description: String, + amt_msat: Option, + ) -> PaymentInfo { + PaymentInfo { + payment_hash, + payment_type, + description, + preimage: None, + amt_msat, + fee_paid_msat: None, + status: HTLCStatus::Pending, + created_at: (now_ms() / 1000) as i64, + last_updated: (now_ms() / 1000) as i64, + } + } + + #[inline] + pub fn with_preimage(mut self, preimage: PaymentPreimage) -> Self { + self.preimage = Some(preimage); + self + } + + #[inline] + pub fn with_status(mut self, status: HTLCStatus) -> Self { + self.status = status; + self + } + + pub(crate) fn is_outbound(&self) -> bool { matches!(self.payment_type, PaymentType::OutboundPayment { .. }) } } #[derive(Clone)] -pub struct PaymentsFilter { - pub payment_type: Option, +pub struct DBPaymentsFilter { + pub is_outbound: Option, + pub destination: 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 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 { @@ -190,7 +196,7 @@ pub struct GetPaymentsResult { } #[async_trait] -pub trait DbStorage { +pub trait LightningDB { type Error; /// Initializes tables in DB. @@ -204,35 +210,37 @@ pub trait DbStorage { /// 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>; + async fn add_channel_to_db(&self, details: &DBChannelDetails) -> 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, + rpc_id: i64, funding_tx: String, - funding_value: u64, - funding_generated_in_block: u64, + funding_value: i64, + funding_generated_in_block: i64, ) -> 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>; + async fn update_funding_tx_block_height(&self, funding_tx: String, block_height: i64) -> 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, + rpc_id: i64, closure_reason: String, - close_at: u64, + close_at: i64, ) -> 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>; + /// Gets the list of closed channels records in the DB that have funding tx hashes saved with no closing + /// tx hashes 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>; + async fn add_closing_tx_to_db(&self, rpc_id: i64, 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. @@ -244,7 +252,7 @@ pub trait DbStorage { ) -> 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>; + 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 @@ -256,8 +264,33 @@ pub trait DbStorage { 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>; + /// Inserts a new payment record in the DB. + async fn add_payment_to_db(&self, info: &PaymentInfo) -> Result<(), Self::Error>; + + /// Updates a payment's preimage in DB by the payment's hash. + async fn update_payment_preimage_in_db( + &self, + hash: PaymentHash, + preimage: PaymentPreimage, + ) -> Result<(), Self::Error>; + + /// Updates a payment's status in DB by the payment's hash. + async fn update_payment_status_in_db(&self, hash: PaymentHash, status: &HTLCStatus) -> Result<(), Self::Error>; + + /// Updates a payment's status to received in DB by the payment's hash. Also, adds the payment preimage to the db. + async fn update_payment_to_received_in_db( + &self, + hash: PaymentHash, + preimage: PaymentPreimage, + ) -> Result<(), Self::Error>; + + /// Updates a sent payment status to succeeded in DB by the payment's hash. Also, adds the payment preimage and the amount of fees paid to the db. + async fn update_payment_to_sent_in_db( + &self, + hash: PaymentHash, + preimage: PaymentPreimage, + fee_paid_msat: Option, + ) -> 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>; @@ -267,7 +300,7 @@ pub trait DbStorage { /// 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, + filter: Option, paging: PagingOptionsEnum, limit: usize, ) -> Result; diff --git a/mm2src/coins/lightning/ln_errors.rs b/mm2src/coins/lightning/ln_errors.rs index 72b581f647..5b349fd43a 100644 --- a/mm2src/coins/lightning/ln_errors.rs +++ b/mm2src/coins/lightning/ln_errors.rs @@ -1,31 +1,18 @@ 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 crate::PrivKeyPolicyNotAllowed; +use common::executor::AbortedError; 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; +use rpc_task::RpcTaskError; +use std::num::TryFromIntError; 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)] +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum EnableLightningError { #[display(fmt = "Invalid request: {}", _0)] @@ -40,30 +27,37 @@ pub enum EnableLightningError { InvalidAddress(String), #[display(fmt = "Invalid path: {}", _0)] InvalidPath(String), + #[display(fmt = "Private key policy is not allowed: {}", _0)] + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), #[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), + #[display(fmt = "Rpc task error: {}", _0)] + RpcTaskError(String), ConnectToNodeError(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), } impl HttpStatusCode for EnableLightningError { fn status_code(&self) -> StatusCode { match self { - EnableLightningError::InvalidRequest(_) | EnableLightningError::RpcError(_) => StatusCode::BAD_REQUEST, + EnableLightningError::InvalidRequest(_) + | EnableLightningError::RpcError(_) + | EnableLightningError::PrivKeyPolicyNotAllowed(_) => 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, + | EnableLightningError::DbError(_) + | EnableLightningError::RpcTaskError(_) + | EnableLightningError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } @@ -76,424 +70,23 @@ 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 From for EnableLightningError { + fn from(e: UtxoRpcError) -> Self { EnableLightningError::RpcError(e.to_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 EnableLightningError { + fn from(e: PrivKeyPolicyNotAllowed) -> Self { EnableLightningError::PrivKeyPolicyNotAllowed(e) } } -impl From for ListChannelsError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => ListChannelsError::NoSuchCoin(coin), - } - } +impl From for EnableLightningError { + fn from(e: RpcTaskError) -> Self { EnableLightningError::RpcTaskError(e.to_string()) } } -impl From for ListChannelsError { - fn from(err: SqlError) -> ListChannelsError { ListChannelsError::DbError(err.to_string()) } +impl From for EnableLightningError { + fn from(e: AbortedError) -> Self { EnableLightningError::Internal(e.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)] +#[derive(Display, PartialEq)] pub enum SaveChannelClosingError { #[display(fmt = "DB error: {}", _0)] DbError(String), @@ -507,55 +100,14 @@ pub enum SaveChannelClosingError { FundingTxParseError(String), #[display(fmt = "Error while waiting for the funding transaction to be spent: {}", _0)] WaitForFundingTxSpendError(String), + #[display(fmt = "Error while converting types: {}", _0)] + ConversionError(TryFromIntError), } 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) } +impl From for SaveChannelClosingError { + fn from(err: TryFromIntError) -> SaveChannelClosingError { SaveChannelClosingError::ConversionError(err) } } diff --git a/mm2src/coins/lightning/ln_events.rs b/mm2src/coins/lightning/ln_events.rs index 3f898e4fa3..351191e1c0 100644 --- a/mm2src/coins/lightning/ln_events.rs +++ b/mm2src/coins/lightning/ln_events.rs @@ -1,28 +1,37 @@ use super::*; +use crate::lightning::ln_db::{DBChannelDetails, HTLCStatus, LightningDB, PaymentType}; use crate::lightning::ln_errors::{SaveChannelClosingError, SaveChannelClosingResult}; +use crate::lightning::ln_sql::SqliteLightningDB; use bitcoin::blockdata::script::Script; use bitcoin::blockdata::transaction::Transaction; -use common::executor::{spawn, Timer}; +use bitcoin::consensus::encode::serialize_hex; +use common::executor::{AbortSettings, SpawnAbortable, SpawnFuture, Timer}; use common::log::{error, info}; use common::now_ms; use core::time::Duration; -use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; +use futures::compat::Future01CompatExt; +use lightning::chain::chaininterface::{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 secp256k1v22::Secp256k1; +use std::convert::{TryFrom, TryInto}; use std::sync::Arc; use utxo_signer::with_key_pair::sign_tx; const TRY_LOOP_INTERVAL: f64 = 60.; +/// 1 second. +const CRITICAL_FUTURE_TIMEOUT: f64 = 1.0; +pub const SUCCESSFUL_CLAIM_LOG: &str = "Successfully claimed payment"; +pub const SUCCESSFUL_SEND_LOG: &str = "Successfully sent payment"; pub struct LightningEventHandler { platform: Arc, channel_manager: Arc, keys_manager: Arc, - persister: Arc, + db: SqliteLightningDB, + trusted_nodes: TrustedNodesShared, } impl EventHandler for LightningEventHandler { @@ -33,18 +42,20 @@ impl EventHandler for LightningEventHandler { channel_value_satoshis, output_script, user_channel_id, + counterparty_node_id, } => self.handle_funding_generation_ready( *temporary_channel_id, *channel_value_satoshis, output_script, *user_channel_id, + counterparty_node_id, ), Event::PaymentReceived { payment_hash, - amt, + amount_msat, purpose, - } => self.handle_payment_received(*payment_hash, *amt, purpose), + } => self.handle_payment_received(*payment_hash, *amount_msat, purpose.clone()), Event::PaymentSent { payment_preimage, @@ -53,16 +64,20 @@ impl EventHandler for LightningEventHandler { .. } => self.handle_payment_sent(*payment_preimage, *payment_hash, *fee_paid_msat), + Event::PaymentClaimed { payment_hash, amount_msat, .. } => self.handle_payment_claimed(*payment_hash, *amount_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), + Event::SpendableOutputs { outputs } => self.handle_spendable_outputs(outputs.clone()), // 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?: {}", + Event::PaymentForwarded { fee_earned_msat, claim_from_onchain_tx, prev_channel_id, next_channel_id} => info!( + "Received a fee of {} milli-satoshis for a successfully forwarded payment from {} to {} through our {} lightning node. Was the forwarded HTLC claimed by our counterparty via an on-chain transaction?: {}", fee_earned_msat.unwrap_or_default(), + prev_channel_id.map(hex::encode).unwrap_or_else(|| "unknown".into()), + next_channel_id.map(hex::encode).unwrap_or_else(|| "unknown".into()), self.platform.coin.ticker(), claim_from_onchain_tx, ), @@ -81,6 +96,7 @@ impl EventHandler for LightningEventHandler { ), // Handling updating channel penalties after successfully routing a payment along a path is done by the InvoicePayer. + // Todo: Maybe add information to db about why a payment succeeded using this event Event::PaymentPathSuccessful { payment_id, payment_hash, @@ -93,7 +109,8 @@ impl EventHandler for LightningEventHandler { ), // 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. + // Also abandoning or retrying a payment is handled by the InvoicePayer. + // Todo: Add information to db about why a payment failed using this event Event::PaymentPathFailed { payment_hash, rejected_by_dest, @@ -114,35 +131,74 @@ impl EventHandler for LightningEventHandler { 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. - } - }, + } => self.handle_open_channel_request(*temporary_channel_id, *counterparty_node_id, *funding_satoshis, *push_msat), + + // Just log an error for now, but this event can be used along PaymentForwarded for a new RPC that shows stats about how a node + // forward payments over it's outbound channels which can be useful for a user that wants to run a forwarding node for some profits. + Event::HTLCHandlingFailed { + prev_channel_id, failed_next_destination + } => error!( + "Failed to handle htlc from {} to {:?}", + hex::encode(prev_channel_id), + failed_next_destination, + ), + + // ProbeSuccessful and ProbeFailed are events in response to a send_probe function call which sends a payment that probes a given route for liquidity. + // send_probe is not used for now but may be used in order matching in the future to check if a swap can happen or not. + Event::ProbeSuccessful { .. } => (), + Event::ProbeFailed { .. } => (), } } } +pub async fn init_abortable_events(platform: Arc, db: SqliteLightningDB) -> EnableLightningResult<()> { + let closed_channels_without_closing_tx = db.get_closed_channels_with_no_closing_tx().await?; + for channel_details in closed_channels_without_closing_tx { + let platform_c = platform.clone(); + let db = db.clone(); + let user_channel_id = channel_details.rpc_id; + platform.spawner().spawn(async move { + if let Ok(closing_tx_hash) = platform_c + .get_channel_closing_tx(channel_details) + .await + .error_log_passthrough() + { + if let Err(e) = db.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(()) +} + +#[derive(Display)] +pub enum SignFundingTransactionError { + #[display(fmt = "Internal error: {}", _0)] + Internal(String), + #[display(fmt = "Error converting transaction: {}", _0)] + ConvertTxErr(String), + #[display(fmt = "Error signing transaction: {}", _0)] + TxSignFailed(String), +} + // 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 { +) -> Result { 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!( + SignFundingTransactionError::Internal(format!( "Unsigned funding tx not found for internal channel id: {}", user_channel_id )) @@ -151,8 +207,16 @@ fn sign_funding_transaction( }; 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 my_address = coin + .as_ref() + .derivation_method + .single_addr_or_err() + .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; + let key_pair = coin + .as_ref() + .priv_key_policy + .key_pair_or_err() + .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; let prev_script = Builder::build_p2pkh(&my_address.hash); let signed = sign_tx( @@ -161,45 +225,62 @@ fn sign_funding_transaction( prev_script, SignatureVersion::WitnessV0, coin.as_ref().conf.fork_id, - )?; + ) + .map_err(|e| SignFundingTransactionError::TxSignFailed(e.to_string()))?; - Transaction::try_from(signed).map_to_mm(|e| OpenChannelError::ConvertTxErr(e.to_string())) + Transaction::try_from(signed).map_err(|e| SignFundingTransactionError::ConvertTxErr(e.to_string())) } async fn save_channel_closing_details( - persister: Arc, + db: SqliteLightningDB, platform: Arc, user_channel_id: u64, reason: String, ) -> SaveChannelClosingResult<()> { - persister - .update_channel_to_closed(user_channel_id, reason, now_ms() / 1000) + db.update_channel_to_closed(user_channel_id as i64, reason, (now_ms() / 1000) as i64) .await?; - let channel_details = persister + let channel_details = db .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?; + db.add_closing_tx_to_db(user_channel_id as i64, closing_tx_hash).await?; Ok(()) } +async fn add_claiming_tx_to_db_loop( + db: SqliteLightningDB, + closing_txid: String, + claiming_txid: String, + claimed_balance: f64, +) { + while let Err(e) = db + .add_claiming_tx_to_db(closing_txid.clone(), claiming_txid.clone(), claimed_balance) + .await + { + error!("error {}", e); + Timer::sleep(TRY_LOOP_INTERVAL).await; + } +} + impl LightningEventHandler { pub fn new( platform: Arc, channel_manager: Arc, keys_manager: Arc, - persister: Arc, + db: SqliteLightningDB, + trusted_nodes: TrustedNodesShared, ) -> Self { LightningEventHandler { platform, channel_manager, keys_manager, - persister, + db, + trusted_nodes, } } @@ -209,10 +290,11 @@ impl LightningEventHandler { channel_value_satoshis: u64, output_script: &Script, user_channel_id: u64, + counterparty_node_id: &PublicKey, ) { info!( - "Handling FundingGenerationReady event for internal channel id: {}", - user_channel_id + "Handling FundingGenerationReady event for internal channel id: {} with: {}", + user_channel_id, counterparty_node_id ); let funding_tx = match sign_funding_transaction(user_channel_id, output_script, self.platform.clone()) { Ok(tx) => tx, @@ -227,92 +309,124 @@ impl LightningEventHandler { }; 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) + if let Err(e) = + self.channel_manager + .funding_transaction_generated(&temporary_channel_id, counterparty_node_id, funding_tx) { error!("{:?}", e); return; } let platform = self.platform.clone(); - let persister = self.persister.clone(); - spawn(async move { + let db = self.db.clone(); + + let fut = 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(); - }); + db.add_funding_tx_to_db( + user_channel_id as i64, + funding_txid.to_string(), + channel_value_satoshis as i64, + best_block_height as i64, + ) + .await + .error_log(); + }; + + let settings = AbortSettings::default().critical_timout_s(CRITICAL_FUTURE_TIMEOUT); + self.platform.spawner().spawn_with_settings(fut, settings); } - fn handle_payment_received(&self, payment_hash: PaymentHash, amt: u64, purpose: &PaymentPurpose) { + fn handle_payment_received(&self, payment_hash: PaymentHash, received_amount: u64, purpose: PaymentPurpose) { info!( - "Handling PaymentReceived event for payment_hash: {}", - hex::encode(payment_hash.0) + "Handling PaymentReceived event for payment_hash: {} with amount {}", + hex::encode(payment_hash.0), + received_amount ); - let (payment_preimage, payment_secret) = match purpose { - PaymentPurpose::InvoicePayment { - payment_preimage, - payment_secret, - } => match payment_preimage { - Some(preimage) => (*preimage, Some(*payment_secret)), - None => return, + let db = self.db.clone(); + let payment_preimage = match purpose { + PaymentPurpose::InvoicePayment { payment_preimage, .. } => match payment_preimage { + Some(preimage) => { + let fut = async move { + db.update_payment_to_received_in_db(payment_hash, preimage) + .await + .error_log_with_msg("Unable to update received payment info in DB!"); + }; + let settings = AbortSettings::default().critical_timout_s(CRITICAL_FUTURE_TIMEOUT); + self.platform.spawner().spawn_with_settings(fut, settings); + preimage + }, + // This is a swap related payment since we don't have the preimage yet + None => { + let amt_msat = Some( + received_amount + .try_into() + .expect("received_amount shouldn't exceed i64::MAX"), + ); + let payment_info = PaymentInfo::new( + payment_hash, + PaymentType::InboundPayment, + "Swap Payment".into(), + amt_msat, + ) + .with_status(HTLCStatus::Received); + let fut = async move { + db.add_payment_to_db(&payment_info) + .await + .error_log_with_msg("Unable to add payment information to DB!"); + }; + + let settings = AbortSettings::default().critical_timout_s(CRITICAL_FUTURE_TIMEOUT); + self.platform.spawner().spawn_with_settings(fut, settings); + + 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) + PaymentPurpose::SpontaneousPayment(preimage) => { + let amt_msat = Some( + received_amount + .try_into() + .expect("received_amount shouldn't exceed i64::MAX"), ); - 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, + let payment_info = + PaymentInfo::new(payment_hash, PaymentType::InboundPayment, "keysend".into(), amt_msat) + .with_preimage(preimage) + .with_status(HTLCStatus::Received); + let fut = async move { + db.add_payment_to_db(&payment_info) + .await + .error_log_with_msg("Unable to add payment information to DB!"); }; - 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); - } - }); + + let settings = AbortSettings::default().critical_timout_s(CRITICAL_FUTURE_TIMEOUT); + self.platform.spawner().spawn_with_settings(fut, settings); + preimage }, - } + }; + self.channel_manager.claim_funds(payment_preimage); + } + + fn handle_payment_claimed(&self, payment_hash: PaymentHash, amount_msat: u64) { + info!( + "Received an amount of {} millisatoshis for payment hash {}", + amount_msat, + hex::encode(payment_hash.0) + ); + let db = self.db.clone(); + let fut = async move { + match db + .update_payment_status_in_db(payment_hash, &HTLCStatus::Succeeded) + .await + { + Ok(_) => info!( + "{} of {} millisatoshis with payment hash {}", + SUCCESSFUL_CLAIM_LOG, + amount_msat, + hex::encode(payment_hash.0), + ), + Err(e) => error!("Unable to update payment status in DB error: {}", e), + } + }; + let settings = AbortSettings::default().critical_timout_s(CRITICAL_FUTURE_TIMEOUT); + self.platform.spawner().spawn_with_settings(fut, settings); } fn handle_payment_sent( @@ -325,28 +439,22 @@ impl LightningEventHandler { "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) + let db = self.db.clone(); + let fut = async move { + match db + .update_payment_to_sent_in_db(payment_hash, payment_preimage, fee_paid_msat) .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(), + Ok(_) => info!( + "{} with payment hash {}", + SUCCESSFUL_SEND_LOG, hex::encode(payment_hash.0) - ); + ), + Err(e) => error!("Unable to update sent payment info in DB error: {}", e), } - }); + }; + let settings = AbortSettings::default().critical_timout_s(CRITICAL_FUTURE_TIMEOUT); + self.platform.spawner().spawn_with_settings(fut, settings); } fn handle_channel_closed(&self, channel_id: [u8; 32], user_channel_id: u64, reason: String) { @@ -355,21 +463,23 @@ impl LightningEventHandler { hex::encode(channel_id), reason ); - let persister = self.persister.clone(); + let db = self.db.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 { + + let fut = async move { + if let Err(e) = save_channel_closing_details(db, platform, user_channel_id, reason).await { + // This is the case when a channel is closed before funding is broadcasted due to the counterparty disconnecting or other incompatibility issue. + if e != SaveChannelClosingError::FundingTxNull.into() { error!( "Unable to update channel {} closing details in DB: {}", user_channel_id, e ); } - }); - } + } + }; + + let settings = AbortSettings::default().critical_timout_s(CRITICAL_FUTURE_TIMEOUT); + self.platform.spawner().spawn_with_settings(fut, settings); } fn handle_payment_failed(&self, payment_hash: PaymentHash) { @@ -377,105 +487,207 @@ impl LightningEventHandler { "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) + let db = self.db.clone(); + let fut = async move { + db.update_payment_status_in_db(payment_hash, &HTLCStatus::Failed) .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); - } - } - }); + .error_log_with_msg("Unable to update payment status in DB!"); + }; + let settings = AbortSettings::default().critical_timout_s(CRITICAL_FUTURE_TIMEOUT); + self.platform.spawner().spawn_with_settings(fut, settings); } fn handle_pending_htlcs_forwards(&self, time_forwardable: Duration) { info!("Handling PendingHTLCsForwardable event!"); - let min_wait_time = time_forwardable.as_millis() as u32; + let min_wait_time = time_forwardable.as_millis() as u64; let channel_manager = self.channel_manager.clone(); - spawn(async move { + self.platform.spawner().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]) { + fn handle_spendable_outputs(&self, outputs: Vec) { info!("Handling SpendableOutputs event!"); - let platform_coin = &self.platform.coin; + if outputs.is_empty() { + error!("Received SpendableOutputs event with no outputs!"); + return; + } + // 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, + let my_address = match self.platform.coin.as_ref().derivation_method.single_addr_or_err() { + Ok(addr) => addr.clone(), 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 platform = self.platform.clone(); + let db = self.db.clone(); + let keys_manager = self.keys_manager.clone(); + + let fut = async move { + let change_destination_script = Builder::build_witness_script(&my_address.hash).to_bytes().take().into(); + let feerate_sat_per_1000_weight = platform.get_est_sat_per_1000_weight(ConfirmationTarget::Normal); + let output_descriptors = outputs.iter().collect::>(); + let claiming_tx = match 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_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 + + let claiming_txid = claiming_tx.txid(); + let tx_hex = serialize_hex(&claiming_tx); + + if let Err(e) = platform.coin.send_raw_tx(&tx_hex).compat().await { + // TODO: broadcast transaction through p2p network in this case, we have to check that the transactions is confirmed on-chain after this. + error!( + "Broadcasting of the claiming transaction {} failed: {}", + claiming_txid, e ); + 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 = claiming_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) + }, + }; - self.platform.broadcast_transaction(&spending_tx); - } + // This doesn't need to be respawned on restart unlike add_closing_tx_to_db since Event::SpendableOutputs will be re-fired on restart + // if the spending_tx is not broadcasted. + add_claiming_tx_to_db_loop( + db.clone(), + closing_txid, + claiming_txid.to_string(), + (claimed_balance as f64) - claiming_tx_fee_per_channel, + ) + .await; + } + }; + + let settings = AbortSettings::default().critical_timout_s(CRITICAL_FUTURE_TIMEOUT); + self.platform.spawner().spawn_with_settings(fut, settings); + } + + fn handle_open_channel_request( + &self, + temporary_channel_id: [u8; 32], + counterparty_node_id: PublicKey, + funding_satoshis: u64, + push_msat: u64, + ) { + info!( + "Handling OpenChannelRequest from node: {} with funding value: {} and starting balance: {}", + counterparty_node_id, funding_satoshis, push_msat, + ); + + let db = self.db.clone(); + let trusted_nodes = self.trusted_nodes.clone(); + let channel_manager = self.channel_manager.clone(); + let platform = self.platform.clone(); + let fut = async move { + if let Ok(last_channel_rpc_id) = db.get_last_channel_rpc_id().await.error_log_passthrough() { + let user_channel_id = last_channel_rpc_id as u64 + 1; + + let trusted_nodes = trusted_nodes.lock().clone(); + let accepted_inbound_channel_with_0conf = trusted_nodes.contains(&counterparty_node_id) + && channel_manager + .accept_inbound_channel_from_trusted_peer_0conf( + &temporary_channel_id, + &counterparty_node_id, + user_channel_id, + ) + .is_ok(); + + if accepted_inbound_channel_with_0conf + || channel_manager + .accept_inbound_channel(&temporary_channel_id, &counterparty_node_id, user_channel_id) + .is_ok() + { + let is_public = match channel_manager + .list_channels() + .into_iter() + .find(|chan| chan.user_channel_id == user_channel_id) + { + Some(details) => details.is_public, + None => { + error!( + "Inbound channel {} details should be found by list_channels!", + user_channel_id + ); + return; + }, + }; + + let pending_channel_details = DBChannelDetails::new( + user_channel_id, + temporary_channel_id, + counterparty_node_id, + false, + is_public, + ); + if let Err(e) = db.add_channel_to_db(&pending_channel_details).await { + error!("Unable to add new inbound channel {} to db: {}", user_channel_id, e); + } + + while let Some(details) = channel_manager + .list_channels() + .into_iter() + .find(|chan| chan.user_channel_id == user_channel_id) + { + if let Some(funding_tx) = details.funding_txo { + let best_block_height = platform.best_block_height(); + db.add_funding_tx_to_db( + user_channel_id as i64, + funding_tx.txid.to_string(), + funding_satoshis as i64, + best_block_height as i64, + ) + .await + .error_log(); + break; + } + + Timer::sleep(TRY_LOOP_INTERVAL).await; + } + } + } + }; + + self.platform.spawner().spawn(fut); } } diff --git a/mm2src/coins/lightning/ln_filesystem_persister.rs b/mm2src/coins/lightning/ln_filesystem_persister.rs new file mode 100644 index 0000000000..49ecb111b3 --- /dev/null +++ b/mm2src/coins/lightning/ln_filesystem_persister.rs @@ -0,0 +1,400 @@ +use crate::lightning::ln_storage::{LightningStorage, NetworkGraph, NodesAddressesMap, NodesAddressesMapShared, Scorer, + TrustedNodesShared}; +use async_trait::async_trait; +use bitcoin::blockdata::constants::genesis_block; +use bitcoin::{BlockHash, Network, Txid}; +use bitcoin_hashes::hex::FromHex; +use common::async_blocking; +use common::log::LogState; +use lightning::chain::channelmonitor::ChannelMonitor; +use lightning::chain::keysinterface::{KeysInterface, Sign}; +use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringParameters}; +use lightning::util::persist::KVStorePersister; +use lightning::util::ser::{ReadableArgs, Writeable}; +use mm2_io::fs::{check_dir_operations, invalid_data_err, read_json, write_json}; +use secp256k1v22::PublicKey; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::io::{BufReader, BufWriter, Cursor}; +use std::net::SocketAddr; +use std::ops::Deref; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +#[cfg(target_family = "unix")] use std::os::unix::io::AsRawFd; + +#[cfg(target_family = "windows")] +use {std::ffi::OsStr, std::os::windows::ffi::OsStrExt}; + +const USE_TMP_FILE: bool = true; + +pub struct LightningFilesystemPersister { + main_path: PathBuf, + backup_path: Option, +} + +impl LightningFilesystemPersister { + /// Initialize a new LightningPersister and set the path to the individual channels' + /// files. + #[inline] + pub fn new(main_path: PathBuf, backup_path: Option) -> Self { Self { main_path, backup_path } } + + /// Get the directory which was provided when this persister was initialized. + #[inline] + pub fn main_path(&self) -> PathBuf { self.main_path.clone() } + + /// Get the backup directory which was provided when this persister was initialized. + #[inline] + pub fn backup_path(&self) -> Option { self.backup_path.clone() } + + pub fn nodes_addresses_path(&self) -> PathBuf { + let mut path = self.main_path(); + path.push("channel_nodes_data"); + path + } + + pub fn nodes_addresses_backup_path(&self) -> Option { + self.backup_path().map(|mut backup_path| { + backup_path.push("channel_nodes_data"); + backup_path + }) + } + + pub fn network_graph_path(&self) -> PathBuf { + let mut path = self.main_path(); + path.push("network_graph"); + path + } + + pub fn scorer_path(&self) -> PathBuf { + let mut path = self.main_path(); + path.push("scorer"); + path + } + + pub fn trusted_nodes_path(&self) -> PathBuf { + let mut path = self.main_path(); + path.push("trusted_nodes"); + path + } + + pub fn manager_path(&self) -> PathBuf { + let mut path = self.main_path(); + path.push("manager"); + path + } + + pub fn monitors_path(&self) -> PathBuf { + let mut path = self.main_path(); + path.push("monitors"); + path + } + + pub fn monitors_backup_path(&self) -> Option { + self.backup_path().map(|mut backup_path| { + backup_path.push("monitors"); + backup_path + }) + } + + /// 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.monitors_path(); + if !path.exists() { + return Ok(Vec::new()); + } + let mut res = Vec::new(); + for file_option in fs::read_dir(path)? { + let file = file_option?; + let owned_file_name = file.file_name(); + let filename = owned_file_name.to_str().ok_or_else(|| { + invalid_data_err("Invalid ChannelMonitor file name", format!("{:?}", owned_file_name)) + })?; + if filename == "checkval" { + continue; + } + if !filename.is_ascii() || filename.len() < 65 { + return Err(invalid_data_err("Invalid ChannelMonitor file name", filename)); + } + if filename.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.split_at(64).0) + .map_err(|e| invalid_data_err("Invalid tx ID in filename error", e))?; + + let index = filename + .split_at(65) + .1 + .parse::() + .map_err(|e| invalid_data_err("Invalid tx index in filename error", e))?; + + let contents = fs::read(file.path())?; + let mut buffer = Cursor::new(&contents); + let (blockhash, channel_monitor) = <(BlockHash, ChannelMonitor)>::read(&mut buffer, &*keys_manager) + .map_err(|e| invalid_data_err("Failed to deserialize ChannelMonito", e))?; + + if channel_monitor.get_funding_txo().0.txid != txid || channel_monitor.get_funding_txo().0.index != index { + return Err(invalid_data_err( + "ChannelMonitor was stored in the wrong file", + filename, + )); + } + + res.push((blockhash, channel_monitor)); + } + Ok(res) + } +} + +impl KVStorePersister for LightningFilesystemPersister { + fn persist(&self, key: &str, object: &W) -> std::io::Result<()> { + let mut dest_file = self.main_path(); + dest_file.push(key); + drop_mutability!(dest_file); + write_to_file(dest_file, object)?; + + if !matches!(key, "network_graph" | "scorer") { + if let Some(mut dest_file) = self.backup_path() { + dest_file.push(key); + drop_mutability!(dest_file); + write_to_file(dest_file, object)?; + } + } + + Ok(()) + } +} + +#[cfg(target_family = "windows")] +macro_rules! call { + ($e: expr) => { + if $e != 0 { + return Ok(()); + } else { + return Err(std::io::Error::last_os_error()); + } + }; +} + +#[cfg(target_family = "windows")] +fn path_to_windows_str>(path: T) -> Vec { + path.as_ref().encode_wide().chain(Some(0)).collect() +} + +fn write_to_file(dest_file: PathBuf, data: &W) -> std::io::Result<()> { + let mut tmp_file = dest_file.clone(); + tmp_file.set_extension("tmp"); + drop_mutability!(tmp_file); + + // 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) + { + // Note that going by rust-lang/rust@d602a6b, on MacOS it is only safe to use + // rust stdlib 1.36 or higher. + let mut buf = BufWriter::new(fs::File::create(&tmp_file)?); + data.write(&mut buf)?; + buf.into_inner()?.sync_all()?; + } + // Fsync the parent directory on Unix. + #[cfg(target_family = "unix")] + { + let parent_directory = dest_file.parent().unwrap(); + fs::rename(&tmp_file, &dest_file)?; + let dir_file = fs::OpenOptions::new().read(true).open(parent_directory)?; + unsafe { + libc::fsync(dir_file.as_raw_fd()); + } + } + #[cfg(target_family = "windows")] + { + if dest_file.exists() { + unsafe { + winapi::um::winbase::ReplaceFileW( + path_to_windows_str(dest_file).as_ptr(), + path_to_windows_str(tmp_file).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(tmp_file).as_ptr(), + path_to_windows_str(dest_file).as_ptr(), + winapi::um::winbase::MOVEFILE_WRITE_THROUGH | winapi::um::winbase::MOVEFILE_REPLACE_EXISTING, + ) + }); + } + } + Ok(()) +} + +#[async_trait] +impl LightningStorage for LightningFilesystemPersister { + type Error = std::io::Error; + + async fn init_fs(&self) -> Result<(), Self::Error> { + let path = self.monitors_path(); + let backup_path = self.monitors_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.parent().unwrap())?; + } + check_dir_operations(&path)?; + check_dir_operations(path.parent().unwrap()) + }) + .await + } + + async fn is_fs_initialized(&self) -> Result { + let dir_path = self.monitors_path(); + let backup_dir_path = self.monitors_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::Other, + 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::Other, + "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()); + } + + let nodes_addresses: HashMap = read_json(&path) + .await + .map_err(|e| invalid_data_err("Error", e))? + .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?; + + nodes_addresses + .iter() + .map(|(pubkey_str, addr)| { + let pubkey = PublicKey::from_str(pubkey_str).map_err(|e| invalid_data_err("Error", e))?; + Ok((pubkey, *addr)) + }) + .collect() + } + + 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(); + + let nodes_addresses: HashMap = nodes_addresses + .lock() + .iter() + .map(|(pubkey, addr)| (pubkey.to_string(), *addr)) + .collect(); + + write_json(&nodes_addresses, &path, USE_TMP_FILE) + .await + .map_err(|e| invalid_data_err("Error", e))?; + + if let Some(path) = backup_path { + write_json(&nodes_addresses, &path, USE_TMP_FILE) + .await + .map_err(|e| invalid_data_err("Error", e))?; + } + + Ok(()) + } + + async fn get_network_graph(&self, network: Network, logger: Arc) -> Result { + let path = self.network_graph_path(); + if !path.exists() { + return Ok(NetworkGraph::new(genesis_block(network).header.block_hash(), logger)); + } + 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), logger).map_err(|e| invalid_data_err("Error", e)) + }) + .await + } + + async fn get_scorer(&self, network_graph: Arc, logger: Arc) -> Result { + let path = self.scorer_path(); + if !path.exists() { + return Ok(Mutex::new(ProbabilisticScorer::new( + ProbabilisticScoringParameters::default(), + network_graph, + logger, + ))); + } + async_blocking(move || { + let file = fs::File::open(path)?; + let scorer = ProbabilisticScorer::read( + &mut BufReader::new(file), + (ProbabilisticScoringParameters::default(), network_graph, logger), + ) + .map_err(|e| invalid_data_err("Error", e))?; + Ok(Mutex::new(scorer)) + }) + .await + } + + async fn get_trusted_nodes(&self) -> Result, Self::Error> { + let path = self.trusted_nodes_path(); + if !path.exists() { + return Ok(HashSet::new()); + } + + let trusted_nodes: HashSet = read_json(&path) + .await + .map_err(|e| invalid_data_err("Error", e))? + .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?; + + trusted_nodes + .iter() + .map(|pubkey_str| { + let pubkey = PublicKey::from_str(pubkey_str).map_err(|e| invalid_data_err("Error", e))?; + Ok(pubkey) + }) + .collect() + } + + async fn save_trusted_nodes(&self, trusted_nodes: TrustedNodesShared) -> Result<(), Self::Error> { + let path = self.trusted_nodes_path(); + let trusted_nodes: HashSet = trusted_nodes.lock().iter().map(|pubkey| pubkey.to_string()).collect(); + write_json(&trusted_nodes, &path, USE_TMP_FILE) + .await + .map_err(|e| invalid_data_err("Error", e)) + } +} diff --git a/mm2src/coins/lightning/ln_p2p.rs b/mm2src/coins/lightning/ln_p2p.rs index 00bd5cdd5b..c93c017f06 100644 --- a/mm2src/coins/lightning/ln_p2p.rs +++ b/mm2src/coins/lightning/ln_p2p.rs @@ -1,23 +1,22 @@ use super::*; -use common::executor::{spawn, Timer}; +use common::executor::{spawn_abortable, SpawnFuture, 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::routing::gossip; 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 secp256k1v22::{PublicKey, 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 NetworkGossip = gossip::P2PGossipSync, Arc, Arc>; pub type PeerManager = SimpleArcPeerManager; @@ -30,12 +29,22 @@ pub enum ConnectToNodeRes { ConnectedSuccessfully { pubkey: PublicKey, node_addr: SocketAddr }, } -pub async fn connect_to_node( +#[derive(Display)] +pub enum ConnectionError { + #[display(fmt = "Handshake error: {}", _0)] + HandshakeErr(String), + #[display(fmt = "Timeout error: {}", _0)] + TimeOut(String), +} + +pub async fn connect_to_ln_node( pubkey: PublicKey, node_addr: SocketAddr, peer_manager: Arc, -) -> ConnectToNodeResult { - if peer_manager.get_peer_node_ids().contains(&pubkey) { +) -> Result { + let peer_manager_ref = peer_manager.clone(); + let peer_node_ids = async_blocking(move || peer_manager_ref.get_peer_node_ids()).await; + if peer_node_ids.contains(&pubkey) { return Ok(ConnectToNodeRes::AlreadyConnected { pubkey, node_addr }); } @@ -43,7 +52,7 @@ pub async fn connect_to_node( 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!( + return Err(ConnectionError::TimeOut(format!( "Failed to connect to node: {}", pubkey ))) @@ -54,7 +63,7 @@ pub async fn connect_to_node( // Make sure the connection is still established. match futures::poll!(&mut connection_closed_future) { std::task::Poll::Ready(_) => { - return MmError::err(ConnectToNodeError::ConnectionError(format!( + return Err(ConnectionError::HandshakeErr(format!( "Node {} disconnected before finishing the handshake", pubkey ))); @@ -62,7 +71,9 @@ pub async fn connect_to_node( std::task::Poll::Pending => {}, } - if peer_manager.get_peer_node_ids().contains(&pubkey) { + let peer_manager = peer_manager.clone(); + let peer_node_ids = async_blocking(move || peer_manager.get_peer_node_ids()).await; + if peer_node_ids.contains(&pubkey) { break; } @@ -73,12 +84,12 @@ pub async fn connect_to_node( Ok(ConnectToNodeRes::ConnectedSuccessfully { pubkey, node_addr }) } -pub async fn connect_to_nodes_loop(open_channels_nodes: NodesAddressesMapShared, peer_manager: Arc) { +pub async fn connect_to_ln_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 { + match connect_to_ln_node(pubkey, node_addr, peer_manager.clone()).await { Ok(res) => { if let ConnectToNodeRes::ConnectedSuccessfully { .. } = res { log::info!("{}", res.to_string()); @@ -133,13 +144,18 @@ pub async fn ln_node_announcement_loop( continue; }, }; - channel_manager.broadcast_node_announcement(node_color, node_name, addresses); + let channel_manager = channel_manager.clone(); + async_blocking(move || channel_manager.broadcast_node_announcement(node_color, node_name, addresses)).await; Timer::sleep(BROADCAST_NODE_ANNOUNCEMENT_INTERVAL as f64).await; } } async fn ln_p2p_loop(peer_manager: Arc, listener: TcpListener) { + // This container consists of abort handlers of the spawned inbound connections. + // They will be dropped once `LightningCoin` is dropped because `ln_p2p_loop` is spawned + // via [`Platform::abortable_system`]. + let mut spawned = Vec::new(); loop { let peer_mgr = peer_manager.clone(); let tcp_stream = match listener.accept().await { @@ -153,18 +169,19 @@ async fn ln_p2p_loop(peer_manager: Arc, listener: TcpListener) { }, }; if let Ok(stream) = tcp_stream.into_std() { - spawn(async move { + spawned.push(spawn_abortable(async move { lightning_net_tokio::setup_inbound(peer_mgr.clone(), stream).await; - }); + })); }; } } pub async fn init_peer_manager( ctx: MmArc, + platform: &Arc, listening_port: u16, channel_manager: Arc, - network_gossip: Arc, + gossip_sync: Arc, node_secret: SecretKey, logger: Arc, ) -> EnableLightningResult> { @@ -181,7 +198,7 @@ pub async fn init_peer_manager( rand::thread_rng().fill_bytes(&mut ephemeral_bytes); let lightning_msg_handler = MessageHandler { chan_handler: channel_manager, - route_handler: network_gossip, + route_handler: gossip_sync, }; // IgnoringMessageHandler is used as custom message types (experimental and application-specific messages) is not needed @@ -194,7 +211,6 @@ pub async fn init_peer_manager( )); // Initialize p2p networking - spawn(ln_p2p_loop(peer_manager.clone(), listener)); - + platform.spawner().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 index dfc3f24554..3444b4cb69 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -1,86 +1,39 @@ use super::*; -use crate::lightning::ln_errors::{FindWatchedOutputSpendError, GetHeaderError, GetTxError, SaveChannelClosingError, - SaveChannelClosingResult}; -use crate::utxo::rpc_clients::{electrum_script_hash, BestBlock as RpcBestBlock, BlockHashOrHeight, +use crate::lightning::ln_errors::{SaveChannelClosingError, SaveChannelClosingResult}; +use crate::utxo::rpc_clients::{BestBlock as RpcBestBlock, BlockHashOrHeight, ConfirmedTransactionInfo, ElectrumBlockHeader, ElectrumClient, ElectrumNonce, EstimateFeeMethod, - UtxoRpcClientEnum, UtxoRpcError}; -use crate::utxo::utxo_common; + UtxoRpcClientEnum, UtxoRpcResult}; +use crate::utxo::spv::SimplePaymentVerification; use crate::utxo::utxo_standard::UtxoStandardCoin; -use crate::{MarketCoinOps, MmCoin}; +use crate::utxo::GetConfirmedTxError; +use crate::{CoinFutSpawner, 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::executor::{abortable_queue::AbortableQueue, AbortableSystem, SpawnFuture, Timer}; use common::log::{debug, error, info}; use futures::compat::Future01CompatExt; +use futures::future::join_all; 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}; +use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; +use spv_validation::spv_proof::TRY_SPV_PROOF_INTERVAL; +use std::convert::{TryFrom, TryInto}; +use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering, Ordering}; const CHECK_FOR_NEW_BEST_BLOCK_INTERVAL: f64 = 60.; -const MIN_ALLOWED_FEE_PER_1000_WEIGHT: u32 = 253; const TRY_LOOP_INTERVAL: f64 = 60.; +const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 10.; #[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, - })) -} +#[inline] +pub fn h256_from_txid(txid: Txid) -> H256 { H256::from(txid.as_hash().into_inner()) } pub async fn get_best_header(best_header_listener: &ElectrumClient) -> EnableLightningResult { best_header_listener @@ -91,8 +44,8 @@ pub async fn get_best_header(best_header_listener: &ElectrumClient) -> EnableLig } pub async fn update_best_block( - chain_monitor: &ChainMonitor, - channel_manager: &ChannelManager, + chain_monitor: Arc, + channel_manager: Arc, best_header: ElectrumBlockHeader, ) { { @@ -104,20 +57,8 @@ pub async fn update_best_block( 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; - }, - }; + let prev_blockhash = sha256d::Hash::from_inner(h.prev_block_hash.0); + let merkle_root = sha256d::Hash::from_inner(h.merkle_root.0); ( BlockHeader { version: h.version as i32, @@ -141,14 +82,14 @@ pub async fn update_best_block( (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); + async_blocking(move || channel_manager.best_block_updated(&new_best_header, new_best_height)).await; + async_blocking(move || chain_monitor.best_block_updated(&new_best_header, new_best_height)).await; } } pub async fn ln_best_block_update_loop( platform: Arc, - persister: Arc, + db: SqliteLightningDB, chain_monitor: Arc, channel_manager: Arc, best_header_listener: ElectrumClient, @@ -156,57 +97,81 @@ pub async fn ln_best_block_update_loop( ) { let mut current_best_block = best_block; loop { + // Transactions confirmations check can be done at every CHECK_FOR_NEW_BEST_BLOCK_INTERVAL instead of at every new block + // in case a transaction confirmation fails due to electrums being down. This way there will be no need to wait for a new + // block to confirm such transaction and causing delays. + platform + .process_txs_confirmations( + &best_header_listener, + &db, + Arc::clone(&chain_monitor), + Arc::clone(&channel_manager), + ) + .await; 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) + .process_txs_unconfirmations(Arc::clone(&chain_monitor), Arc::clone(&channel_manager)) .await; current_best_block = best_header.clone().into(); - update_best_block(&chain_monitor, &channel_manager, best_header).await; + update_best_block(Arc::clone(&chain_monitor), Arc::clone(&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, +async fn get_funding_tx_bytes_loop(rpc_client: &UtxoRpcClientEnum, tx_hash: H256Json) -> BytesJson { + loop { + match rpc_client.get_transaction_bytes(&tx_hash).compat().await { + Ok(res) => break res, + Err(e) => { + error!("error {}", e); + Timer::sleep(TRY_LOOP_INTERVAL).await; + continue; + }, } } } +pub struct LatestFees { + background: AtomicU64, + normal: AtomicU64, + high_priority: AtomicU64, +} + +impl LatestFees { + #[inline] + fn set_background_fees(&self, fee: u64) { self.background.store(fee, Ordering::Release); } + + #[inline] + fn set_normal_fees(&self, fee: u64) { self.normal.store(fee, Ordering::Release); } + + #[inline] + fn set_high_priority_fees(&self, fee: u64) { self.high_priority.store(fee, Ordering::Release); } +} + 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 average time in seconds needed to mine a new block for the blockchain network. + pub avg_blocktime: u64, /// 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, + /// Number of blocks for every Confirmation target. This is used in the FeeEstimator. + pub confirmations_targets: PlatformCoinConfirmationTargets, + /// Latest fees are used when the call for estimate_fee_sat fails. + pub latest_fees: LatestFees, /// This cache stores the transactions that the LN node has interest in. - pub registered_txs: PaMutex>>, + 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>, + /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation. + /// and on [`MmArc::stop`]. + pub abortable_system: AbortableQueue, } impl Platform { @@ -214,36 +179,100 @@ impl Platform { pub fn new( coin: UtxoStandardCoin, network: BlockchainNetwork, - default_fees_and_confirmations: PlatformCoinConfirmations, - ) -> Self { - Platform { + confirmations_targets: PlatformCoinConfirmationTargets, + ) -> EnableLightningResult { + // This should never return an error since it's validated that avg_blocktime is in platform coin config in a previous step of lightning activation. + // But an error is returned here just in case. + let avg_blocktime = coin + .as_ref() + .conf + .avg_blocktime + .ok_or_else(|| EnableLightningError::Internal("`avg_blocktime` can't be None!".into()))?; + + // Create an abortable system linked to the base `coin` so if the base coin is disabled, + // all spawned futures related to `LightCoin` will be aborted as well. + let abortable_system = coin.as_ref().abortable_system.create_subsystem()?; + + Ok(Platform { coin, network, + avg_blocktime, best_block_height: AtomicU64::new(0), - default_fees_and_confirmations, - registered_txs: PaMutex::new(HashMap::new()), + confirmations_targets, + latest_fees: LatestFees { + background: AtomicU64::new(0), + normal: AtomicU64::new(0), + high_priority: AtomicU64::new(0), + }, + registered_txs: PaMutex::new(HashSet::new()), registered_outputs: PaMutex::new(Vec::new()), unsigned_funding_txs: PaMutex::new(HashMap::new()), - } + abortable_system, + }) } #[inline] fn rpc_client(&self) -> &UtxoRpcClientEnum { &self.coin.as_ref().rpc_client } + pub fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + + pub async fn set_latest_fees(&self) -> UtxoRpcResult<()> { + let platform_coin = &self.coin; + let conf = &platform_coin.as_ref().conf; + + let latest_background_fees = 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, + self.confirmations_targets.background, + ) + .compat() + .await?; + self.latest_fees.set_background_fees(latest_background_fees); + + let latest_normal_fees = 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, + self.confirmations_targets.normal, + ) + .compat() + .await?; + self.latest_fees.set_normal_fees(latest_normal_fees); + + let latest_high_priority_fees = 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, + self.confirmations_targets.high_priority, + ) + .compat() + .await?; + self.latest_fees.set_high_priority_fees(latest_high_priority_fees); + + Ok(()) + } + #[inline] pub fn update_best_block_height(&self, new_height: u64) { - self.best_block_height.store(new_height, AtomicOrdering::Relaxed); + self.best_block_height.store(new_height, AtomicOrdering::Release); } #[inline] - pub fn best_block_height(&self) -> u64 { self.best_block_height.load(AtomicOrdering::Relaxed) } + pub fn best_block_height(&self) -> u64 { self.best_block_height.load(AtomicOrdering::Acquire) } - pub fn add_tx(&self, txid: Txid, script_pubkey: Script) { + pub fn add_tx(&self, txid: Txid) { let mut registered_txs = self.registered_txs.lock(); - registered_txs - .entry(txid) - .or_insert_with(HashSet::new) - .insert(script_pubkey); + registered_txs.insert(txid); } pub fn add_output(&self, output: WatchedOutput) { @@ -251,43 +280,24 @@ impl Platform { 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) + async fn process_tx_for_unconfirmation(&self, txid: Txid, monitor: Arc) where - T: Confirm, + T: Confirm + Send + Sync + 'static, { - match self.get_tx_if_onchain(txid).await { + let rpc_txid = h256_json_from_txid(txid); + match self.rpc_client().get_tx_if_onchain(&rpc_txid).await { Ok(Some(_)) => {}, Ok(None) => { info!( "Transaction {} is not found on chain. The transaction will be re-broadcasted.", txid, ); - monitor.transaction_unconfirmed(&txid); + let monitor = monitor.clone(); + async_blocking(move || monitor.transaction_unconfirmed(&txid)).await; + // If a transaction is unconfirmed due to a block reorganization; LDK will rebroadcast it. + // In this case, this transaction needs to be added again to the registered transactions + // to start watching for it on the chain again. + self.add_tx(txid); }, Err(e) => error!( "Error while trying to check if the transaction {} is discarded or not :{:?}", @@ -296,64 +306,79 @@ impl Platform { } } - pub async fn process_txs_unconfirmations(&self, chain_monitor: &ChainMonitor, channel_manager: &ChannelManager) { + pub async fn process_txs_unconfirmations( + &self, + chain_monitor: Arc, + channel_manager: Arc, + ) { // 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; + self.process_tx_for_unconfirmation(txid, Arc::clone(&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; + self.process_tx_for_unconfirmation(txid, Arc::clone(&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); - } - } - } + + let on_chain_txs_futs = registered_txs + .into_iter() + .map(|txid| async move { + let rpc_txid = h256_json_from_txid(txid); + self.rpc_client().get_tx_if_onchain(&rpc_txid).await + }) + .collect::>(); + let on_chain_txs = join_all(on_chain_txs_futs) + .await + .into_iter() + .filter_map(|maybe_tx| match maybe_tx { + Ok(maybe_tx) => maybe_tx, + Err(e) => { + error!( + "Error while trying to figure if transaction is on-chain or not: {:?}", + e + ); + None + }, + }); + + let is_spv_enabled = self.coin.as_ref().conf.spv_conf.is_some(); + let confirmed_transactions_futs = on_chain_txs + .map(|transaction| async move { + if is_spv_enabled { + client + // TODO: Should log the spv error if height > 0 + .validate_spv_proof(&transaction, (now_ms() / 1000) + TRY_SPV_PROOF_INTERVAL) + .await + .map_err(GetConfirmedTxError::SPVError) + } else { + client.get_confirmed_tx_info_from_rpc(&transaction).await } - } - } - confirmed_registered_txs + }) + .collect::>(); + join_all(confirmed_transactions_futs) + .await + .into_iter() + .filter_map(|confirmed_transaction| match confirmed_transaction { + Ok(confirmed_tx) => { + let txid = Txid::from_hash(confirmed_tx.tx.hash().reversed().to_sha256d()); + self.registered_txs.lock().remove(&txid); + Some(confirmed_tx) + }, + Err(e) => { + error!("Error verifying transaction: {:?}", e); + None + }, + }) + .collect() } async fn append_spent_registered_output_txs( @@ -361,49 +386,85 @@ impl Platform { 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 + + let spent_outputs_info_fut = registered_outputs + .into_iter() + .map(|output| async move { + self.rpc_client() + .find_output_spend( + h256_from_txid(output.outpoint.txid), + output.script_pubkey.as_ref(), + output.outpoint.index.into(), + BlockHashOrHeight::Hash(Default::default()), ) - .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); + .compat() + .await + }) + .collect::>(); + let mut spent_outputs_info = join_all(spent_outputs_info_fut) + .await + .into_iter() + .filter_map(|maybe_spent| match maybe_spent { + Ok(maybe_spent) => maybe_spent, + Err(e) => { + error!("Error while trying to figure if output is spent or not: {:?}", e); + None + }, + }) + .collect::>(); + spent_outputs_info.retain(|output| { + !transactions_to_confirm + .iter() + .any(|info| info.tx.hash() == output.spending_tx.hash()) + }); + + let is_spv_enabled = self.coin.as_ref().conf.spv_conf.is_some(); + let confirmed_transactions_futs = spent_outputs_info + .into_iter() + .map(|output| async move { + if is_spv_enabled { + client + // TODO: Should log the spv error if height > 0 + .validate_spv_proof(&output.spending_tx, (now_ms() / 1000) + TRY_SPV_PROOF_INTERVAL) + .await + .map_err(GetConfirmedTxError::SPVError) + } else { + client.get_confirmed_tx_info_from_rpc(&output.spending_tx).await } - outputs_to_remove.push(output); - } - } - self.registered_outputs - .lock() - .retain(|output| !outputs_to_remove.contains(output)); + }) + .collect::>(); + let mut confirmed_transaction_info = join_all(confirmed_transactions_futs) + .await + .into_iter() + .filter_map(|confirmed_transaction| match confirmed_transaction { + Ok(confirmed_tx) => { + self.registered_outputs.lock().retain(|output| { + !confirmed_tx + .tx + .clone() + .inputs + .into_iter() + .any(|txin| txin.previous_output.hash == h256_from_txid(output.outpoint.txid)) + }); + Some(confirmed_tx) + }, + Err(e) => { + error!("Error verifying transaction: {:?}", e); + None + }, + }) + .collect(); + + transactions_to_confirm.append(&mut confirmed_transaction_info); } pub async fn process_txs_confirmations( &self, client: &ElectrumClient, - persister: &LightningPersister, - chain_monitor: &ChainMonitor, - channel_manager: &ChannelManager, + db: &SqliteLightningDB, + chain_monitor: Arc, + channel_manager: Arc, ) { let mut transactions_to_confirm = self.get_confirmed_registered_txs(client).await; self.append_spent_registered_output_txs(&mut transactions_to_confirm, client) @@ -412,36 +473,45 @@ impl Platform { 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 + let best_block_height = self.best_block_height() as i64; + if let Err(e) = db .update_funding_tx_block_height( - confirmed_transaction_info.transaction.txid().to_string(), + confirmed_transaction_info.tx.hash().reversed().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, - ); + let channel_manager = channel_manager.clone(); + let confirmed_transaction_info_cloned = confirmed_transaction_info.clone(); + async_blocking(move || { + channel_manager.transactions_confirmed( + &confirmed_transaction_info_cloned.header.clone().into(), + &[( + confirmed_transaction_info_cloned.index as usize, + &confirmed_transaction_info_cloned.tx.clone().into(), + )], + confirmed_transaction_info_cloned.height as u32, + ) + }) + .await; + let chain_monitor = chain_monitor.clone(); + async_blocking(move || { + chain_monitor.transactions_confirmed( + &confirmed_transaction_info.header.into(), + &[( + confirmed_transaction_info.index as usize, + &confirmed_transaction_info.tx.into(), + )], + confirmed_transaction_info.height as u32, + ) + }) + .await; } } - pub async fn get_channel_closing_tx(&self, channel_details: SqlChannelDetails) -> SaveChannelClosingResult { + pub async fn get_channel_closing_tx(&self, channel_details: DBChannelDetails) -> SaveChannelClosingResult { let from_block = channel_details .funding_generated_in_block .ok_or_else(|| MmError::new(SaveChannelClosingError::BlockHeightNull))?; @@ -453,18 +523,18 @@ impl Platform { 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 funding_tx_bytes = get_funding_tx_bytes_loop(self.rpc_client(), tx_hash).await; let closing_tx = self .coin - .wait_for_tx_spend( + // TODO add fn with old wait_for_tx_spend name + .wait_for_htlc_tx_spend( &funding_tx_bytes.into_vec(), + &[], (now_ms() / 1000) + 3600, - from_block, + from_block.try_into()?, &None, + TAKER_PAYMENT_SPEND_SEARCH_INTERVAL, ) .compat() .await @@ -481,17 +551,17 @@ impl FeeEstimator for Platform { 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 latest_fees = match confirmation_target { + ConfirmationTarget::Background => self.latest_fees.background.load(Ordering::Acquire), + ConfirmationTarget::Normal => self.latest_fees.normal.load(Ordering::Acquire), + ConfirmationTarget::HighPriority => self.latest_fees.high_priority.load(Ordering::Acquire), }; 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, + ConfirmationTarget::Background => self.confirmations_targets.background, + ConfirmationTarget::Normal => self.confirmations_targets.normal, + ConfirmationTarget::HighPriority => self.confirmations_targets.high_priority, }; let fee_per_kb = tokio::task::block_in_place(move || { self.rpc_client() @@ -504,11 +574,21 @@ impl FeeEstimator for Platform { n_blocks, ) .wait() - .unwrap_or(default_fee) + .unwrap_or(latest_fees) }); + + // Set default fee to last known fee for the corresponding confirmation target + match confirmation_target { + ConfirmationTarget::Background => self.latest_fees.set_background_fees(fee_per_kb), + ConfirmationTarget::Normal => self.latest_fees.set_normal_fees(fee_per_kb), + ConfirmationTarget::HighPriority => self.latest_fees.set_high_priority_fees(fee_per_kb), + }; + // 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) + // This has changed in rust-lightning v0.0.110 as LDK currently wraps get_est_sat_per_1000_weight to ensure that the value returned is + // no smaller than 253. https://github.com/lightningdevkit/rust-lightning/pull/1552 + (fee_per_kb as f64 / 4.0).ceil() as u32 } } @@ -517,25 +597,28 @@ impl BroadcasterInterface for Platform { 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 { + let fut = async move { match fut.compat().await { Ok(id) => info!("Transaction broadcasted successfully: {:?} ", id), + // TODO: broadcast transaction through p2p network in case of error Err(e) => error!("Broadcast transaction {} failed: {}", txid, e), } - }); + }; + + self.spawner().spawn(fut); } } 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()); } + fn register_tx(&self, txid: &Txid, _script_pubkey: &Script) { self.add_tx(*txid); } // 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, @@ -545,29 +628,20 @@ impl Filter for Platform { // 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 - ) - }); + let output_spend_fut = self.rpc_client().find_output_spend( + h256_from_txid(output.outpoint.txid), + output.script_pubkey.as_ref(), + output.outpoint.index.into(), + BlockHashOrHeight::Hash(block_hash), + ); + let maybe_output_spend_res = + tokio::task::block_in_place(move || output_spend_fut.wait()).error_log_passthrough(); - 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; - }, - }; + if let Ok(Some(spent_output_info)) = maybe_output_spend_res { + match Transaction::try_from(spent_output_info.spending_tx) { + Ok(spending_tx) => return Some((spent_output_info.input_index, spending_tx)), + Err(e) => error!("Can't convert transaction error: {}", e.to_string()), + } } None diff --git a/mm2src/coins/lightning/ln_serialization.rs b/mm2src/coins/lightning/ln_serialization.rs index 82f0a700c7..511522c328 100644 --- a/mm2src/coins/lightning/ln_serialization.rs +++ b/mm2src/coins/lightning/ln_serialization.rs @@ -1,51 +1,14 @@ -use lightning_invoice::Invoice; -use secp256k1::PublicKey; +use crate::lightning::ln_db::{DBPaymentsFilter, HTLCStatus, PaymentInfo, PaymentType}; +use crate::lightning::ln_platform::h256_json_from_txid; +use crate::H256Json; +use lightning::chain::channelmonitor::Balance; +use lightning::ln::channelmanager::ChannelDetails; +use secp256k1v22::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 { @@ -136,26 +99,233 @@ impl<'de> de::Deserialize<'de> for PublicKeyForRPC { } } -#[cfg(test)] -mod tests { - use super::*; - use serde_json as json; +#[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 is_ready: bool, + // Channel is confirmed and channel_ready 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, +} - #[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); +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, + is_ready: details.is_channel_ready, + is_usable: details.is_usable, + is_public: details.is_public, + } } +} - #[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); +#[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: i64, + last_updated: i64, +} + +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(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 DBPaymentsFilter { + fn from(filter: PaymentsFilterForRPC) -> Self { + let (is_outbound, destination) = if let Some(payment_type) = filter.payment_type { + match payment_type { + PaymentTypeForRPC::OutboundPayment { destination } => (Some(true), Some(destination.0.to_string())), + PaymentTypeForRPC::InboundPayment => (Some(false), None), + } + } else { + (None, None) + }; + DBPaymentsFilter { + is_outbound, + destination, + description: filter.description, + status: filter.status.map(|s| s.to_string()), + from_amount_msat: filter.from_amount_msat.map(|a| a as i64), + to_amount_msat: filter.to_amount_msat.map(|a| a as i64), + from_fee_paid_msat: filter.from_fee_paid_msat.map(|f| f as i64), + to_fee_paid_msat: filter.to_fee_paid_msat.map(|f| f as i64), + from_timestamp: filter.from_timestamp.map(|f| f as i64), + to_timestamp: filter.to_timestamp.map(|f| f as i64), + } } +} + +/// 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, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json as json; #[test] fn test_node_address_serialize() { diff --git a/mm2src/coins/lightning/ln_sql.rs b/mm2src/coins/lightning/ln_sql.rs new file mode 100644 index 0000000000..b9ac1e2129 --- /dev/null +++ b/mm2src/coins/lightning/ln_sql.rs @@ -0,0 +1,1607 @@ +use crate::lightning::ln_db::{ChannelType, ChannelVisibility, ClosedChannelsFilter, DBChannelDetails, + DBPaymentsFilter, GetClosedChannelsResult, GetPaymentsResult, HTLCStatus, LightningDB, + PaymentInfo, PaymentType}; +use async_trait::async_trait; +use common::{async_blocking, PagingOptionsEnum}; +use db_common::owned_named_params; +use db_common::sqlite::rusqlite::{params, 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, AsSqlNamedParams, + OwnedSqlNamedParams, SqlNamedParams, SqliteConnShared, CHECK_TABLE_EXISTS_SQL}; +use gstuff::now_ms; +use lightning::ln::{PaymentHash, PaymentPreimage}; +use secp256k1v22::PublicKey; +use std::convert::TryInto; +use std::str::FromStr; + +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), + 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, + channel_detail: &DBChannelDetails, +) -> Result<(String, OwnedSqlNamedParams), SqlError> { + let table_name = channels_history_table(for_coin); + validate_table_name(&table_name)?; + + let rpc_id = channel_detail.rpc_id; + let created_at = channel_detail.created_at; + + let sql = format!( + "INSERT INTO {} ( + rpc_id, + channel_id, + counterparty_node_id, + is_outbound, + is_public, + is_closed, + created_at + ) VALUES ( + :rpc_id, :channel_id, :counterparty_node_id, :is_outbound, :is_public, :is_closed, :created_at + )", + table_name + ); + + let params = owned_named_params! { + ":rpc_id": rpc_id, + ":channel_id": channel_detail.channel_id.clone(), + ":counterparty_node_id": channel_detail.counterparty_node_id.clone(), + ":is_outbound": channel_detail.is_outbound, + ":is_public": channel_detail.is_public, + ":is_closed": channel_detail.is_closed, + ":created_at": created_at, + }; + Ok((sql, params)) +} + +fn insert_payment_sql(for_coin: &str, payment_info: &PaymentInfo) -> Result<(String, OwnedSqlNamedParams), SqlError> { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + + let payment_hash = hex::encode(payment_info.payment_hash.0); + let (is_outbound, destination) = match payment_info.payment_type { + PaymentType::OutboundPayment { destination } => (true, Some(destination.to_string())), + PaymentType::InboundPayment => (false, None), + }; + let preimage = payment_info.preimage.map(|p| hex::encode(p.0)); + let status = payment_info.status.to_string(); + + let sql = format!( + "INSERT INTO {} ( + payment_hash, + destination, + description, + preimage, + amount_msat, + fee_paid_msat, + is_outbound, + status, + created_at, + last_updated + ) VALUES ( + :payment_hash, :destination, :description, :preimage, :amount_msat, :fee_paid_msat, :is_outbound, :status, :created_at, :last_updated + )", + table_name + ); + + let params = owned_named_params! { + ":payment_hash": payment_hash, + ":destination": destination, + ":description": payment_info.description.clone(), + ":preimage": preimage, + ":amount_msat": payment_info.amt_msat, + ":fee_paid_msat": payment_info.fee_paid_msat, + ":is_outbound": is_outbound, + ":status": status, + ":created_at": payment_info.created_at, + ":last_updated": payment_info.last_updated, + }; + Ok((sql, params)) +} + +fn update_payment_preimage_sql(for_coin: &str) -> Result { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "UPDATE {} SET + preimage = ?1, + last_updated = ?2 + WHERE + payment_hash = ?3;", + table_name + ); + + Ok(sql) +} + +fn update_payment_status_sql(for_coin: &str) -> Result { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "UPDATE {} SET + status = ?1, + last_updated = ?2 + WHERE + payment_hash = ?3;", + table_name + ); + + Ok(sql) +} + +fn update_received_payment_sql(for_coin: &str) -> Result { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "UPDATE {} SET + preimage = ?1, + status = ?2, + last_updated = ?3 + WHERE + payment_hash = ?4;", + table_name + ); + + Ok(sql) +} + +fn update_sent_payment_sql(for_coin: &str) -> Result { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "UPDATE {} SET + preimage = ?1, + fee_paid_msat = ?2, + status = ?3, + last_updated = ?4 + WHERE + payment_hash = ?5;", + 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, + 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 = DBChannelDetails { + rpc_id: row.get(0)?, + channel_id: row.get(1)?, + counterparty_node_id: row.get(2)?, + funding_tx: row.get(3)?, + funding_value: row.get(4)?, + funding_generated_in_block: row.get(5)?, + closing_tx: row.get(6)?, + closure_reason: row.get(7)?, + claiming_tx: row.get(8)?, + claimed_balance: row.get(9)?, + is_outbound: row.get(10)?, + is_public: row.get(11)?, + is_closed: row.get(12)?, + created_at: row.get(13)?, + closed_at: row.get(14)?, + }; + Ok(channel_details) +} + +fn payment_info_from_row(row: &Row<'_>) -> Result { + let is_outbound = row.get::<_, bool>(7)?; + 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), + amt_msat: row.get(4)?, + fee_paid_msat: row.get(5)?, + status: HTLCStatus::from_str(&row.get::<_, String>(6)?)?, + created_at: row.get(8)?, + last_updated: row.get(9)?, + }; + 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<'a>( + builder: &mut SqlBuilder, + params: &mut SqlNamedParams<'a>, + filter: &'a 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + if let Some(channel_type) = &filter.channel_type { + let is_outbound = match channel_type { + ChannelType::Outbound => &true, + ChannelType::Inbound => &false, + }; + + builder.and_where("is_outbound = :is_outbound"); + params.push((":is_outbound", is_outbound)); + } + + if let Some(channel_visibility) = &filter.channel_visibility { + let is_public = match channel_visibility { + ChannelVisibility::Public => &true, + ChannelVisibility::Private => &false, + }; + + builder.and_where("is_public = :is_public"); + params.push((":is_public", is_public)); + } +} + +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("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<'a>( + builder: &mut SqlBuilder, + params: &mut SqlNamedParams<'a>, + filter: &'a DBPaymentsFilter, +) { + if let Some(dest) = &filter.destination { + builder.and_where("destination = :dest"); + params.push((":dest", dest)); + } + + if let Some(outbound) = &filter.is_outbound { + builder.and_where("is_outbound = :is_outbound"); + params.push((":is_outbound", outbound)); + } + + 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)); + } + + if let Some(from_amount) = &filter.from_amount_msat { + builder.and_where("amount_msat >= :from_amount"); + params.push((":from_amount", from_amount)); + } + + if let Some(to_amount) = &filter.to_amount_msat { + builder.and_where("amount_msat <= :to_amount"); + params.push((":to_amount", to_amount)); + } + + if let Some(from_fee) = &filter.from_fee_paid_msat { + builder.and_where("fee_paid_msat >= :from_fee"); + params.push((":from_fee", from_fee)); + } + + if let Some(to_fee) = &filter.to_fee_paid_msat { + builder.and_where("fee_paid_msat <= :to_fee"); + params.push((":to_fee", to_fee)); + } + + if let Some(from_time) = &filter.from_timestamp { + builder.and_where("created_at >= :from_time"); + params.push((":from_time", from_time)); + } + + if let Some(to_time) = &filter.to_timestamp { + builder.and_where("created_at <= :to_time"); + params.push((":to_time", to_time)); + } +} + +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) +} + +#[derive(Clone)] +pub struct SqliteLightningDB { + db_ticker: String, + sqlite_connection: SqliteConnShared, +} + +impl SqliteLightningDB { + pub fn new(ticker: String, sqlite_connection: SqliteConnShared) -> Self { + Self { + db_ticker: ticker.replace('-', "_"), + sqlite_connection, + } + } +} + +#[async_trait] +impl LightningDB for SqliteLightningDB { + 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.db_ticker.as_str())?; + let sql_payments_history = create_payments_history_table_sql(self.db_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.db_ticker.as_str()); + validate_table_name(&channels_history_table)?; + let payments_history_table = payments_history_table(self.db_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.db_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: &DBChannelDetails) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + let (sql, params) = insert_channel_sql(&for_coin, details)?; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let conn = sqlite_connection.lock().unwrap(); + conn.execute_named(&sql, ¶ms.as_sql_named_params())?; + Ok(()) + }) + .await + } + + async fn add_funding_tx_to_db( + &self, + rpc_id: i64, + funding_tx: String, + funding_value: i64, + funding_generated_in_block: i64, + ) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = params!(funding_tx, funding_value, funding_generated_in_block, rpc_id); + sql_transaction.execute(&update_funding_tx_sql(&for_coin)?, params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn update_funding_tx_block_height(&self, funding_tx: String, block_height: i64) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = params!(block_height, funding_tx); + sql_transaction.execute(&update_funding_tx_block_height_sql(&for_coin)?, params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn update_channel_to_closed( + &self, + rpc_id: i64, + closure_reason: String, + closed_at: i64, + ) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + let is_closed = true; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = params!(closure_reason, is_closed, closed_at, rpc_id); + sql_transaction.execute(&update_channel_to_closed_sql(&for_coin)?, params)?; + 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.db_ticker.as_str())?; + builder.and_where("funding_tx IS NOT NULL"); + 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: i64, closing_tx: String) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = params!(closing_tx, rpc_id); + sql_transaction.execute(&update_closing_tx_sql(&for_coin)?, params)?; + 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.db_ticker.clone(); + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = params!(claiming_tx, claimed_balance, closing_tx); + sql_transaction.execute(&update_claiming_tx_sql(&for_coin)?, params)?; + 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.db_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.db_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); + } + 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_slice(), channel_details_from_row)? + .collect::>()?; + let result = GetClosedChannelsResult { + channels, + skipped: offset, + total, + }; + Ok(result) + }) + .await + } + + async fn add_payment_to_db(&self, info: &PaymentInfo) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + let (sql, params) = insert_payment_sql(&for_coin, info)?; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let conn = sqlite_connection.lock().unwrap(); + conn.execute_named(&sql, ¶ms.as_sql_named_params())?; + Ok(()) + }) + .await + } + + async fn update_payment_preimage_in_db( + &self, + hash: PaymentHash, + preimage: PaymentPreimage, + ) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + let preimage = hex::encode(preimage.0); + let last_updated = (now_ms() / 1000) as i64; + let payment_hash = hex::encode(hash.0); + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = params!(preimage, last_updated, payment_hash); + sql_transaction.execute(&update_payment_preimage_sql(&for_coin)?, params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn update_payment_status_in_db(&self, hash: PaymentHash, status: &HTLCStatus) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + let status = status.to_string(); + let last_updated = (now_ms() / 1000) as i64; + let payment_hash = hex::encode(hash.0); + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = params!(status, last_updated, payment_hash); + sql_transaction.execute(&update_payment_status_sql(&for_coin)?, params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn update_payment_to_received_in_db( + &self, + hash: PaymentHash, + preimage: PaymentPreimage, + ) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + let preimage = hex::encode(preimage.0); + let status = HTLCStatus::Received.to_string(); + let last_updated = (now_ms() / 1000) as i64; + let payment_hash = hex::encode(hash.0); + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = params!(preimage, status, last_updated, payment_hash); + sql_transaction.execute(&update_received_payment_sql(&for_coin)?, params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn update_payment_to_sent_in_db( + &self, + hash: PaymentHash, + preimage: PaymentPreimage, + fee_paid_msat: Option, + ) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + let preimage = hex::encode(preimage.0); + let fee_paid_msat = fee_paid_msat.map(|f| f as i64); + let status = HTLCStatus::Succeeded.to_string(); + let last_updated = (now_ms() / 1000) as i64; + let payment_hash = hex::encode(hash.0); + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let mut conn = sqlite_connection.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = params!(preimage, fee_paid_msat, status, last_updated, payment_hash); + sql_transaction.execute(&update_sent_payment_sql(&for_coin)?, params)?; + 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.db_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.db_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::*; + use crate::lightning::ln_db::DBChannelDetails; + use common::{block_on, now_ms}; + use db_common::sqlite::rusqlite::Connection; + use rand::distributions::Alphanumeric; + use rand::{Rng, RngCore}; + use secp256k1v22::{Secp256k1, SecretKey}; + use std::num::NonZeroUsize; + use std::sync::{Arc, Mutex}; + + 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 = DBChannelDetails { + rpc_id: (i + 1) as i64, + 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::()), + 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::()), + is_outbound: rng.gen::(), + is_public: rng.gen::(), + is_closed: rand::random(), + created_at: rng.gen::(), + closed_at: Some(rng.gen::()), + }; + 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 rng.gen::() { + rng.fill_bytes(&mut bytes); + let secret = SecretKey::from_slice(&bytes).unwrap(); + PaymentType::OutboundPayment { + destination: PublicKey::from_secret_key(&s, &secret), + } + } else { + PaymentType::InboundPayment + }; + 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)) + }, + amt_msat: Some(rng.gen::()), + fee_paid_msat: Some(rng.gen::()), + status, + created_at: rng.gen::(), + last_updated: rng.gen::(), + }; + payments.push(info); + } + payments + } + + #[test] + fn test_init_sql_collection() { + let db = SqliteLightningDB::new( + "init_sql_collection".into(), + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + let initialized = block_on(db.is_db_initialized()).unwrap(); + assert!(!initialized); + + block_on(db.init_db()).unwrap(); + // repetitive init must not fail + block_on(db.init_db()).unwrap(); + + let initialized = block_on(db.is_db_initialized()).unwrap(); + assert!(initialized); + } + + #[test] + fn test_add_get_channel_sql() { + let db = SqliteLightningDB::new( + "add_get_channel".into(), + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + + block_on(db.init_db()).unwrap(); + + let last_channel_rpc_id = block_on(db.get_last_channel_rpc_id()).unwrap(); + assert_eq!(last_channel_rpc_id, 0); + + let channel = block_on(db.get_channel_from_db(1)).unwrap(); + assert!(channel.is_none()); + + let mut expected_channel_details = DBChannelDetails::new( + 1, + [0; 32], + PublicKey::from_str("038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9").unwrap(), + true, + true, + ); + block_on(db.add_channel_to_db(&expected_channel_details)).unwrap(); + let last_channel_rpc_id = block_on(db.get_last_channel_rpc_id()).unwrap(); + assert_eq!(last_channel_rpc_id, 1); + + let actual_channel_details = block_on(db.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(db.add_channel_to_db(&expected_channel_details)).unwrap_err(); + assert_eq!(last_channel_rpc_id, 1); + + expected_channel_details.rpc_id = 2; + block_on(db.add_channel_to_db(&expected_channel_details)).unwrap(); + let last_channel_rpc_id = block_on(db.get_last_channel_rpc_id()).unwrap(); + assert_eq!(last_channel_rpc_id, 2); + + block_on(db.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(db.get_channel_from_db(2)).unwrap().unwrap(); + assert_eq!(expected_channel_details, actual_channel_details); + + block_on(db.update_funding_tx_block_height( + "9cdafd6d42dcbdc06b0b5bce1866deb82630581285bbfb56870577300c0a8c6e".into(), + 50001, + )) + .unwrap(); + expected_channel_details.funding_generated_in_block = Some(50001); + + let actual_channel_details = block_on(db.get_channel_from_db(2)).unwrap().unwrap(); + assert_eq!(expected_channel_details, actual_channel_details); + + let current_time = (now_ms() / 1000) as i64; + block_on(db.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(db.get_channel_from_db(2)).unwrap().unwrap(); + assert_eq!(expected_channel_details, actual_channel_details); + + let closed_channels = + block_on(db.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(db.update_channel_to_closed( + 1, + "the channel was cooperatively closed".into(), + (now_ms() / 1000) as i64, + )) + .unwrap(); + let closed_channels = + block_on(db.get_closed_channels_by_filter(None, PagingOptionsEnum::default(), 10)).unwrap(); + assert_eq!(closed_channels.channels.len(), 2); + + let actual_channels = block_on(db.get_closed_channels_with_no_closing_tx()).unwrap(); + assert_eq!(actual_channels.len(), 1); + + block_on(db.add_closing_tx_to_db( + 2, + "5557df9ad2c9b3c57a4df8b4a7da0b7a6f4e923b4a01daa98bf9e5a3b33e9c8f".into(), + )) + .unwrap(); + expected_channel_details.closing_tx = + Some("5557df9ad2c9b3c57a4df8b4a7da0b7a6f4e923b4a01daa98bf9e5a3b33e9c8f".into()); + + let actual_channels = block_on(db.get_closed_channels_with_no_closing_tx()).unwrap(); + assert!(actual_channels.is_empty()); + + let actual_channel_details = block_on(db.get_channel_from_db(2)).unwrap().unwrap(); + assert_eq!(expected_channel_details, actual_channel_details); + + block_on(db.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(db.get_channel_from_db(2)).unwrap().unwrap(); + assert_eq!(expected_channel_details, actual_channel_details); + } + + #[test] + fn test_add_get_payment_sql() { + let db = SqliteLightningDB::new( + "add_get_payment".into(), + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + + block_on(db.init_db()).unwrap(); + + let payment = block_on(db.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])), + amt_msat: Some(2000), + fee_paid_msat: Some(100), + status: HTLCStatus::Failed, + created_at: (now_ms() / 1000) as i64, + last_updated: (now_ms() / 1000) as i64, + }; + block_on(db.add_payment_to_db(&expected_payment_info)).unwrap(); + + let actual_payment_info = block_on(db.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.amt_msat = None; + expected_payment_info.status = HTLCStatus::Succeeded; + expected_payment_info.last_updated = (now_ms() / 1000) as i64; + block_on(db.add_payment_to_db(&expected_payment_info)).unwrap(); + + let actual_payment_info = block_on(db.get_payment_from_db(PaymentHash([1; 32]))).unwrap().unwrap(); + assert_eq!(expected_payment_info, actual_payment_info); + + // Test update_payment_preimage_in_db + let new_preimage = PaymentPreimage([4; 32]); + block_on(db.update_payment_preimage_in_db(PaymentHash([1; 32]), new_preimage)).unwrap(); + let preimage_after_update = block_on(db.get_payment_from_db(PaymentHash([1; 32]))) + .unwrap() + .unwrap() + .preimage + .unwrap(); + assert_eq!(new_preimage, preimage_after_update); + + // Test update_payment_status_in_db + let new_status = HTLCStatus::Failed; + block_on(db.update_payment_status_in_db(PaymentHash([1; 32]), &new_status)).unwrap(); + let status_after_update = block_on(db.get_payment_from_db(PaymentHash([1; 32]))) + .unwrap() + .unwrap() + .status; + assert_eq!(new_status, status_after_update); + + // Test update_payment_to_received_in_db + let expected_preimage = PaymentPreimage([5; 32]); + block_on(db.update_payment_to_received_in_db(PaymentHash([1; 32]), expected_preimage)).unwrap(); + let payment_after_update = block_on(db.get_payment_from_db(PaymentHash([1; 32]))).unwrap().unwrap(); + assert_eq!(payment_after_update.status, HTLCStatus::Received); + assert_eq!(payment_after_update.preimage.unwrap(), expected_preimage); + + // Test update_payment_to_sent_in_db + let expected_preimage = PaymentPreimage([7; 32]); + let expected_fee_paid_msat = Some(1000); + block_on(db.update_payment_to_sent_in_db(PaymentHash([1; 32]), expected_preimage, expected_fee_paid_msat)) + .unwrap(); + let payment_after_update = block_on(db.get_payment_from_db(PaymentHash([1; 32]))).unwrap().unwrap(); + assert_eq!(payment_after_update.status, HTLCStatus::Succeeded); + assert_eq!(payment_after_update.preimage.unwrap(), expected_preimage); + assert_eq!( + payment_after_update.fee_paid_msat.map(|f| f as u64), + expected_fee_paid_msat + ); + } + + #[test] + fn test_get_payments_by_filter() { + let db = SqliteLightningDB::new( + "test_get_payments_by_filter".into(), + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + + block_on(db.init_db()).unwrap(); + + let mut payments = generate_random_payments(100); + + for payment in &payments { + block_on(db.add_payment_to_db(payment)).unwrap(); + } + + let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); + let limit = 4; + + let result = block_on(db.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(db.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(db.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 = DBPaymentsFilter { + is_outbound: Some(false), + destination: None, + 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(db.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.to_string()); + let result = block_on(db.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.is_outbound = None; + filter.destination = None; + filter.status = None; + filter.description = Some(substr.to_string()); + let result = block_on(db.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 db = SqliteLightningDB::new( + "test_get_channels_by_filter".into(), + Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + ); + + block_on(db.init_db()).unwrap(); + + let channels = generate_random_channels(100); + + for channel in channels { + block_on(db.add_channel_to_db(&channel)).unwrap(); + block_on(db.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(db.update_channel_to_closed(channel.rpc_id, channel.closure_reason.unwrap(), 1655806080)).unwrap(); + block_on(db.add_closing_tx_to_db(channel.rpc_id, channel.closing_tx.clone().unwrap())).unwrap(); + block_on(db.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(db.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(db.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(db.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(db.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(db.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(db.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(db.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/ln_storage.rs b/mm2src/coins/lightning/ln_storage.rs new file mode 100644 index 0000000000..610720bc3f --- /dev/null +++ b/mm2src/coins/lightning/ln_storage.rs @@ -0,0 +1,39 @@ +use async_trait::async_trait; +use bitcoin::Network; +use common::log::LogState; +use lightning::routing::gossip; +use lightning::routing::scoring::ProbabilisticScorer; +use parking_lot::Mutex as PaMutex; +use secp256k1v22::PublicKey; +use std::collections::{HashMap, HashSet}; +use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; + +pub type NodesAddressesMap = HashMap; +pub type NodesAddressesMapShared = Arc>; +pub type TrustedNodesShared = Arc>>; + +pub type NetworkGraph = gossip::NetworkGraph>; +pub type Scorer = Mutex, Arc>>; + +#[async_trait] +pub trait LightningStorage { + 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; + + async fn save_nodes_addresses(&self, nodes_addresses: NodesAddressesMapShared) -> Result<(), Self::Error>; + + async fn get_network_graph(&self, network: Network, logger: Arc) -> Result; + + async fn get_scorer(&self, network_graph: Arc, logger: Arc) -> Result; + + async fn get_trusted_nodes(&self) -> Result, Self::Error>; + + async fn save_trusted_nodes(&self, trusted_nodes: TrustedNodesShared) -> Result<(), Self::Error>; +} diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index 05e4fd4f63..eeb7e04203 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -1,28 +1,30 @@ use super::*; +use crate::lightning::ln_db::LightningDB; use crate::lightning::ln_platform::{get_best_header, ln_best_block_update_loop, update_best_block}; +use crate::lightning::ln_sql::SqliteLightningDB; +use crate::lightning::ln_storage::{LightningStorage, NodesAddressesMap}; 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::executor::SpawnFuture; 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::ln::channelmanager::{ChainParameters, ChannelManagerReadArgs, PaymentId, PaymentSendFailure, + SimpleArcChannelManager}; +use lightning::routing::gossip::RoutingFees; +use lightning::routing::router::{PaymentParameters, RouteHint, RouteHintHop, RouteParameters}; use lightning::util::config::UserConfig; +use lightning::util::errors::APIError; use lightning::util::ser::ReadableArgs; -use lightning_persister::storage::{DbStorage, FileSystemStorage, NodesAddressesMap, Scorer}; -use lightning_persister::LightningPersister; +use lightning_invoice::payment::{Payer, PaymentError as InvoicePaymentError, Router as RouterTrait}; use mm2_core::mm_ctx::MmArc; +use std::collections::hash_map::Entry; use std::fs::File; use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use std::time::SystemTime; +use std::sync::Arc; -const NETWORK_GRAPH_PERSIST_INTERVAL: u64 = 600; -const SCORER_PERSIST_INTERVAL: u64 = 600; +pub const PAYMENT_RETRY_ATTEMPTS: usize = 5; pub type ChainMonitor = chainmonitor::ChainMonitor< InMemorySigner, @@ -30,10 +32,11 @@ pub type ChainMonitor = chainmonitor::ChainMonitor< Arc, Arc, Arc, - Arc, + Arc, >; pub type ChannelManager = SimpleArcChannelManager; +pub type Router = DefaultRouter, Arc>; #[inline] fn ln_data_dir(ctx: &MmArc, ticker: &str) -> PathBuf { ctx.dbdir().join("LIGHTNING").join(ticker) } @@ -42,7 +45,7 @@ fn ln_data_dir(ctx: &MmArc, ticker: &str) -> PathBuf { ctx.dbdir().join("LIGHTNI fn ln_data_backup_dir(ctx: &MmArc, path: Option, ticker: &str) -> Option { path.map(|p| { PathBuf::from(&p) - .join(&hex::encode(&**ctx.rmd160())) + .join(hex::encode(ctx.rmd160().as_slice())) .join("LIGHTNING") .join(ticker) }) @@ -50,62 +53,50 @@ fn ln_data_backup_dir(ctx: &MmArc, path: Option, ticker: &str) -> Option pub async fn init_persister( ctx: &MmArc, - platform: Arc, ticker: String, backup_path: Option, -) -> EnableLightningResult> { +) -> 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, + let persister = Arc::new(LightningFilesystemPersister::new(ln_data_dir, ln_data_backup_dir)); + + let is_initialized = persister.is_fs_initialized().await?; + if !is_initialized { + persister.init_fs().await?; + } + + Ok(persister) +} + +pub async fn init_db(ctx: &MmArc, ticker: String) -> EnableLightningResult { + let db = SqliteLightningDB::new( + ticker, 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 - ); - } - } - }); + if !db.is_db_initialized().await? { + db.init_db().await?; } - Ok(persister) + Ok(db) } -pub fn init_keys_manager(ctx: &MmArc) -> EnableLightningResult> { +pub fn init_keys_manager(platform: &Platform) -> 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()))?; + // TODO validate that this is right + let seed: [u8; 32] = platform + .coin + .as_ref() + .priv_key_policy + .key_pair_or_err()? + .private() + .secret + .into(); + let cur = get_local_duration_since_epoch().map_to_mm(|e| EnableLightningError::SystemTimeError(e.to_string()))?; Ok(Arc::new(KeysManager::new(&seed, cur.as_secs(), cur.subsec_nanos()))) } @@ -113,7 +104,8 @@ pub fn init_keys_manager(ctx: &MmArc) -> EnableLightningResult> pub async fn init_channel_manager( platform: Arc, logger: Arc, - persister: Arc, + persister: Arc, + db: SqliteLightningDB, keys_manager: Arc, user_config: UserConfig, ) -> EnableLightningResult<(Arc, Arc)> { @@ -134,12 +126,19 @@ pub async fn init_channel_manager( )); // 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()))?; + let channels_persister = persister.clone(); + let channels_keys_manager = keys_manager.clone(); + let mut channelmonitors = async_blocking(move || { + channels_persister + .read_channelmonitors(channels_keys_manager) + .map_to_mm(|e| EnableLightningError::IOError(e.to_string())) + }) + .await?; // This is used for Electrum only to prepare for chain synchronization for (_, chan_mon) in channelmonitors.iter() { + // Although there is a mutex lock inside the load_outputs_to_watch fn + // it shouldn't be held by anything yet, so async_blocking is not needed. chan_mon.load_outputs_to_watch(&platform); } @@ -155,110 +154,97 @@ pub async fn init_channel_manager( 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 best_block_hash = BlockHash::from_hash(sha256d::Hash::from_inner(best_block.hash.0)); + + let channel_manager = if persister.manager_path().exists() { + let chain_monitor_for_args = chain_monitor.clone(); + + let (channel_manager_blockhash, channel_manager, channelmonitors) = async_blocking(move || { + let mut manager_file = 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(), + chain_monitor_for_args, 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); + <(BlockHash, Arc)>::read(&mut manager_file, read_args) + .map(|(h, c)| (h, c, channelmonitors)) + .map_to_mm(|e| EnableLightningError::IOError(e.to_string())) + }) + .await?; - // Sync ChannelMonitors and ChannelManager to chain tip if the node is restarting and has open channels - if channel_manager_blockhash != best_block_hash { + // Sync ChannelMonitors and ChannelManager to chain tip if the node is restarting and has open channels platform - .process_txs_unconfirmations(&chain_monitor, &channel_manager) + .process_txs_confirmations( + &rpc_client, + &db, + Arc::clone(&chain_monitor), + Arc::clone(&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; - } + if channel_manager_blockhash != best_block_hash { + platform + .process_txs_unconfirmations(Arc::clone(&chain_monitor), Arc::clone(&channel_manager)) + .await; + update_best_block(Arc::clone(&chain_monitor), Arc::clone(&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)))?; - } + // Give ChannelMonitors to ChainMonitor + for (_, channel_monitor) in channelmonitors.into_iter() { + let funding_outpoint = channel_monitor.get_funding_txo().0; + let chain_monitor = chain_monitor.clone(); + async_blocking(move || { + chain_monitor + .watch_channel(funding_outpoint, channel_monitor) + .map_to_mm(|e| EnableLightningError::IOError(format!("{:?}", e))) + }) + .await?; + } + channel_manager + } 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), + }; + Arc::new(ChannelManager::new( + fee_estimator.clone(), + chain_monitor.clone(), + broadcaster.clone(), + logger.clone(), + keys_manager.clone(), + user_config, + chain_params, + )) + }; // 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(), + platform.spawner().spawn(ln_best_block_update_loop( + platform.clone(), + db, 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, + persister: Arc, channel_manager: Arc, ) -> EnableLightningResult { - let channels = channel_manager.list_channels(); + let channels = async_blocking(move || channel_manager.list_channels()).await; let mut nodes_addresses = persister.get_nodes_addresses().await?; nodes_addresses.retain(|pubkey, _node_addr| { channels @@ -268,3 +254,273 @@ pub async fn get_open_channels_nodes_addresses( }); Ok(nodes_addresses) } + +// Todo: Make this public in rust-lightning by opening a PR there instead of importing it here +/// Filters the `channels` for an invoice, and returns the corresponding `RouteHint`s to include +/// in the invoice. +/// +/// The filtering is based on the following criteria: +/// * Only one channel per counterparty node +/// * Always select the channel with the highest inbound capacity per counterparty node +/// * Filter out channels with a lower inbound capacity than `min_inbound_capacity_msat`, if any +/// channel with a higher or equal inbound capacity than `min_inbound_capacity_msat` exists +/// * If any public channel exists, the returned `RouteHint`s will be empty, and the sender will +/// need to find the path by looking at the public channels instead +pub(crate) fn filter_channels(channels: Vec, min_inbound_capacity_msat: Option) -> Vec { + let mut filtered_channels: HashMap = HashMap::new(); + let min_inbound_capacity = min_inbound_capacity_msat.unwrap_or(0); + let mut min_capacity_channel_exists = false; + + for channel in channels.iter() { + if channel.get_inbound_payment_scid().is_none() || channel.counterparty.forwarding_info.is_none() { + continue; + } + + // Todo: if all public channels have inbound_capacity_msat less than min_inbound_capacity we need to give the user the option to reveal his/her private channels to the swap counterparty in this case or not + // Todo: the problem with revealing the private channels in the swap message (invoice) is that it can be used by malicious nodes to probe for private channels so maybe there should be a + // Todo: requirement that the other party has the amount required to be sent in the swap first (do we have a way to check if the other side of the swap has the balance required for the swap on-chain or not) + if channel.is_public { + // If any public channel exists, return no hints and let the sender + // look at the public channels instead. + return vec![]; + } + + if channel.inbound_capacity_msat >= min_inbound_capacity { + min_capacity_channel_exists = true; + }; + match filtered_channels.entry(channel.counterparty.node_id) { + Entry::Occupied(entry) if channel.inbound_capacity_msat < entry.get().inbound_capacity_msat => continue, + Entry::Occupied(mut entry) => entry.insert(channel), + Entry::Vacant(entry) => entry.insert(channel), + }; + } + + let route_hint_from_channel = |channel: &ChannelDetails| { + // It's safe to unwrap here since all filtered_channels have forwarding_info + let forwarding_info = channel.counterparty.forwarding_info.as_ref().unwrap(); + RouteHint(vec![RouteHintHop { + src_node_id: channel.counterparty.node_id, + // It's safe to unwrap here since all filtered_channels have inbound_payment_scid + short_channel_id: channel.get_inbound_payment_scid().unwrap(), + fees: RoutingFees { + base_msat: forwarding_info.fee_base_msat, + proportional_millionths: forwarding_info.fee_proportional_millionths, + }, + cltv_expiry_delta: forwarding_info.cltv_expiry_delta, + htlc_minimum_msat: channel.inbound_htlc_minimum_msat, + htlc_maximum_msat: channel.inbound_htlc_maximum_msat, + }]) + }; + // If all channels are private, return the route hint for the highest inbound capacity channel + // per counterparty node. If channels with an higher inbound capacity than the + // min_inbound_capacity exists, filter out the channels with a lower capacity than that. + filtered_channels + .into_iter() + .filter(|(_counterparty_id, channel)| { + !min_capacity_channel_exists || channel.inbound_capacity_msat >= min_inbound_capacity + }) + .map(|(_counterparty_id, channel)| route_hint_from_channel(channel)) + .collect::>() +} + +#[derive(Debug, Display)] +pub enum PaymentError { + #[display(fmt = "Final cltv expiry delta {} is below the required minimum of {}", _0, _1)] + CLTVExpiry(u32, u32), + #[display(fmt = "Error paying invoice: {}", _0)] + Invoice(String), + #[display(fmt = "Keysend error: {}", _0)] + Keysend(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), +} + +impl From for PaymentError { + fn from(err: SqlError) -> PaymentError { PaymentError::DbError(err.to_string()) } +} + +impl From for PaymentError { + fn from(err: InvoicePaymentError) -> PaymentError { PaymentError::Invoice(format!("{:?}", err)) } +} + +// Todo: This is imported from rust-lightning and modified by me, will need to open a PR there with this modification and update the dependency to remove this code and the code it depends on. +pub(crate) fn pay_invoice_with_max_total_cltv_expiry_delta( + channel_manager: Arc, + router: Arc, + scorer: Arc, + invoice: &Invoice, + max_total_cltv_expiry_delta: u32, +) -> Result { + let final_value_msat = invoice + .amount_milli_satoshis() + .ok_or(InvoicePaymentError::Invoice("amount missing"))?; + let expiry_time = (invoice.duration_since_epoch() + invoice.expiry_time()).as_secs(); + + let mut payment_params = PaymentParameters::from_node_id(invoice.recover_payee_pub_key()) + .with_expiry_time(expiry_time) + .with_route_hints(invoice.route_hints()) + .with_max_total_cltv_expiry_delta(max_total_cltv_expiry_delta); + if let Some(features) = invoice.features() { + payment_params = payment_params.with_features(features.clone()); + } + drop_mutability!(payment_params); + let route_params = RouteParameters { + payment_params, + final_value_msat, + final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32, + }; + + pay_internal( + channel_manager, + router, + scorer, + &route_params, + invoice, + &mut 0, + &mut Vec::new(), + ) +} + +fn pay_internal( + channel_manager: Arc, + router: Arc, + scorer: Arc, + params: &RouteParameters, + invoice: &Invoice, + attempts: &mut usize, + errors: &mut Vec, +) -> Result { + let payer = channel_manager.node_id(); + let first_hops = channel_manager.first_hops(); + let payment_hash = PaymentHash((*invoice.payment_hash()).into_inner()); + // Todo: Routes should be checked before order matching also, this might require routing hints to be shared when matching orders. Just-in-time channels can solve this issue as well. + let route = router + .find_route( + &payer, + params, + &payment_hash, + Some(&first_hops.iter().collect::>()), + &scorer.lock().unwrap(), + ) + .map_err(InvoicePaymentError::Routing)?; + + let payment_secret = Some(*invoice.payment_secret()); + match channel_manager.send_payment(&route, payment_hash, &payment_secret) { + Ok(payment_id) => Ok(payment_id), + Err(e) => match e { + PaymentSendFailure::ParameterError(_) => Err(e), + PaymentSendFailure::PathParameterError(_) => Err(e), + PaymentSendFailure::AllFailedRetrySafe(err) => { + if *attempts > PAYMENT_RETRY_ATTEMPTS { + Err(PaymentSendFailure::AllFailedRetrySafe(errors.to_vec())) + } else { + *attempts += 1; + errors.extend(err); + Ok(pay_internal( + channel_manager, + router, + scorer, + params, + invoice, + attempts, + errors, + )?) + } + }, + PaymentSendFailure::PartialFailure { + failed_paths_retry, + payment_id, + .. + } => { + if let Some(retry_data) = failed_paths_retry { + // Some paths were sent, even if we failed to send the full MPP value our + // recipient may misbehave and claim the funds, at which point we have to + // consider the payment sent, so return `Ok()` here, ignoring any retry + // errors. + let _ = retry_payment( + channel_manager, + router, + scorer, + payment_id, + payment_hash, + &retry_data, + &mut 0, + errors, + ); + Ok(payment_id) + } else { + // This may happen if we send a payment and some paths fail, but + // only due to a temporary monitor failure or the like, implying + // they're really in-flight, but we haven't sent the initial + // HTLC-Add messages yet. + Ok(payment_id) + } + }, + }, + } + .map_err(|e| InvoicePaymentError::Sending(e).into()) +} + +#[allow(clippy::too_many_arguments)] +fn retry_payment( + channel_manager: Arc, + router: Arc, + scorer: Arc, + payment_id: PaymentId, + payment_hash: PaymentHash, + params: &RouteParameters, + attempts: &mut usize, + errors: &mut Vec, +) -> Result<(), PaymentError> { + let payer = channel_manager.node_id(); + let first_hops = channel_manager.first_hops(); + let route = router + .find_route( + &payer, + params, + &payment_hash, + Some(&first_hops.iter().collect::>()), + &scorer.lock().unwrap(), + ) + .map_err(InvoicePaymentError::Routing)?; + + match channel_manager.retry_payment(&route, payment_id) { + Ok(()) => Ok(()), + Err(PaymentSendFailure::AllFailedRetrySafe(err)) => { + if *attempts > PAYMENT_RETRY_ATTEMPTS { + let e = PaymentSendFailure::AllFailedRetrySafe(errors.to_vec()); + Err(InvoicePaymentError::Sending(e).into()) + } else { + *attempts += 1; + errors.extend(err); + retry_payment( + channel_manager, + router, + scorer, + payment_id, + payment_hash, + params, + attempts, + errors, + ) + } + }, + Err(PaymentSendFailure::PartialFailure { failed_paths_retry, .. }) => { + if let Some(retry) = failed_paths_retry { + // Always return Ok for the same reason as noted in pay_internal. + let _ = retry_payment( + channel_manager, + router, + scorer, + payment_id, + payment_hash, + &retry, + attempts, + errors, + ); + } + Ok(()) + }, + Err(e) => Err(InvoicePaymentError::Sending(e).into()), + } +} diff --git a/mm2src/coins/lightning_background_processor/Cargo.toml b/mm2src/coins/lightning_background_processor/Cargo.toml deleted file mode 100644 index 5710dcfc2c..0000000000 --- a/mm2src/coins/lightning_background_processor/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[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 deleted file mode 100644 index 4ca2fe9ad4..0000000000 --- a/mm2src/coins/lightning_background_processor/src/lib.rs +++ /dev/null @@ -1,950 +0,0 @@ -//! 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 deleted file mode 100644 index 32b5d7eb1d..0000000000 --- a/mm2src/coins/lightning_persister/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[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 deleted file mode 100644 index 303205c26f..0000000000 --- a/mm2src/coins/lightning_persister/src/lib.rs +++ /dev/null @@ -1,2097 +0,0 @@ -//! 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/util.rs b/mm2src/coins/lightning_persister/src/util.rs deleted file mode 100644 index ac5bc99de5..0000000000 --- a/mm2src/coins/lightning_persister/src/util.rs +++ /dev/null @@ -1,196 +0,0 @@ -#[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 7ba7775128..265b3e69ea 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -24,20 +24,27 @@ #![feature(async_closure)] #![feature(hash_raw_entry)] #![feature(stmt_expr_attributes)] +#![feature(result_flattening)] #[macro_use] extern crate common; #[macro_use] extern crate gstuff; #[macro_use] extern crate lazy_static; +#[macro_use] extern crate mm2_metrics; #[macro_use] extern crate serde_derive; #[macro_use] extern crate serde_json; #[macro_use] extern crate ser_error_derive; use async_trait::async_trait; use base58::FromBase58Error; -use common::mm_metrics::MetricsWeak; +use common::custom_futures::timeout::TimeoutError; +use common::executor::{abortable_queue::{AbortableQueue, WeakSpawner}, + AbortSettings, AbortedError, SpawnAbortable, SpawnFuture}; +use common::log::LogOnError; use common::{calc_total_pages, now_ms, ten, HttpStatusCode}; -use crypto::{Bip32Error, CryptoCtx, DerivationPath}; +use crypto::{Bip32Error, CryptoCtx, CryptoCtxError, DerivationPath, GlobalHDAccountArc, HwRpcError, KeyPairPolicy, + Secp256k1Secret, WithHwRpcError}; use derive_more::Display; +use enum_from::{EnumFromStringify, EnumFromTrait}; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; @@ -46,17 +53,20 @@ use http::{Response, StatusCode}; 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 mm2_metrics::MetricsWeak; +use mm2_number::{bigdecimal::{BigDecimal, ParseBigDecimalError, Zero}, + MmNumber}; +use parking_lot::Mutex as PaMutex; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::{self as json, Value as Json}; use std::cmp::Ordering; use std::collections::hash_map::{HashMap, RawEntryMut}; +use std::collections::HashSet; use std::fmt; +use std::future::Future as Future03; use std::num::NonZeroUsize; use std::ops::{Add, Deref}; -use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -64,19 +74,21 @@ use utxo_signer::with_key_pair::UtxoSignWithKeyPairError; cfg_native! { use crate::lightning::LightningCoin; - use crate::lightning::ln_conf::PlatformCoinConfirmations; + use crate::lightning::ln_conf::PlatformCoinConfirmationTargets; + use ::lightning::ln::PaymentHash as LightningPayment; use async_std::fs; use futures::AsyncWriteExt; + use lightning_invoice::{Invoice, ParseOrSemanticError}; use std::io; + use std::path::PathBuf; use zcash_primitives::transaction::Transaction as ZTransaction; use z_coin::ZcoinProtocolInfo; } cfg_wasm32! { - use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; use hd_wallet_storage::HDWalletDb; + use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; use tx_history_storage::wasm::{clear_tx_history, load_tx_history, save_tx_history, TxHistoryDb}; - pub type TxHistoryDbLocked<'a> = DbLocked<'a, TxHistoryDb>; } @@ -94,7 +106,7 @@ macro_rules! try_f { ($e: expr) => { match $e { Ok(ok) => ok, - Err(e) => return Box::new(futures01::future::err(e)), + Err(e) => return Box::new(futures01::future::err(e.into())), } }; } @@ -178,57 +190,50 @@ macro_rules! ok_or_continue_after_sleep { }; } -#[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; - }, - } - } - }; -} +pub mod coin_balance; -#[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_errors; +use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut}; -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 hd_confirm_address; pub mod hd_pubkey; + pub mod hd_wallet; +use hd_wallet::{HDAccountAddressId, HDAddress}; + 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_with_policy, Qrc20ActivationParams, Qrc20Coin, Qrc20FeeDetails}; + pub mod rpc_command; +use rpc_command::{get_new_address::{GetNewAddressTaskManager, GetNewAddressTaskManagerShared}, + init_account_balance::{AccountBalanceTaskManager, AccountBalanceTaskManagerShared}, + init_create_account::{CreateAccountTaskManager, CreateAccountTaskManagerShared}, + init_scan_for_new_addresses::{ScanAddressesTaskManager, ScanAddressesTaskManagerShared}, + init_withdraw::{WithdrawTaskManager, WithdrawTaskManagerShared}}; + +pub mod tendermint; +use tendermint::{CosmosTransaction, CustomTendermintMsgType, TendermintCoin, TendermintFeeDetails, + TendermintProtocolInfo, TendermintToken, TendermintTokenProtocolInfo}; + #[doc(hidden)] #[allow(unused_variables)] pub mod test_coin; -pub mod tx_history_storage; pub use test_coin::TestCoin; +pub mod tx_history_storage; + #[doc(hidden)] #[allow(unused_variables)] #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] @@ -236,31 +241,24 @@ pub mod solana; #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] pub use solana::spl::SplToken; #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] -pub use solana::{solana_coin_from_conf_and_params, SolanaActivationParams, SolanaCoin, SolanaFeeDetails}; +pub use solana::{SolanaActivationParams, SolanaCoin, SolanaFeeDetails}; 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::bch::{bch_coin_with_policy, BchActivationRequest, BchCoin}; +use utxo::qtum::{self, qtum_coin_with_policy, Qrc20AddressError, QtumCoin, QtumDelegationOps, QtumDelegationRequest, + QtumStakingInfosDetails, ScriptHashTypeNotSupported}; 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::utxo_standard::{utxo_standard_coin_with_policy, UtxoStandardCoin}; use utxo::UtxoActivationParams; use utxo::{BlockchainNetwork, GenerateTxError, UtxoFeeDetails, UtxoTx}; + +#[cfg(not(target_arch = "wasm32"))] pub mod z_coin; #[cfg(not(target_arch = "wasm32"))] use z_coin::ZCoin; +pub type TransactionFut = Box + Send>; pub type BalanceResult = Result>; pub type BalanceFut = Box> + Send>; pub type NonZeroBalanceFut = Box> + Send>; @@ -279,6 +277,23 @@ pub type TxHistoryResult = Result>; pub type RawTransactionResult = Result>; pub type RawTransactionFut<'a> = Box> + Send + 'a>; +pub type RefundResult = Result>; +pub type SendMakerPaymentArgs<'a> = SendSwapPaymentArgs<'a>; +pub type SendTakerPaymentArgs<'a> = SendSwapPaymentArgs<'a>; +pub type SendMakerSpendsTakerPaymentArgs<'a> = SendSpendPaymentArgs<'a>; +pub type SendTakerSpendsMakerPaymentArgs<'a> = SendSpendPaymentArgs<'a>; +pub type SendTakerRefundsPaymentArgs<'a> = SendRefundPaymentArgs<'a>; +pub type SendMakerRefundsPaymentArgs<'a> = SendRefundPaymentArgs<'a>; +pub type SendWatcherRefundsPaymentArgs<'a> = SendRefundPaymentArgs<'a>; + +pub type IguanaPrivKey = Secp256k1Secret; + +// Constants for logs used in tests +pub const INVALID_SENDER_ERR_LOG: &str = "Invalid sender"; +pub const EARLY_CONFIRMATION_ERR_LOG: &str = "Early confirmation"; +pub const OLD_TRANSACTION_ERR_LOG: &str = "Old transaction"; +pub const INVALID_RECEIVER_ERR_LOG: &str = "Invalid receiver"; +pub const INVALID_CONTRACT_ADDRESS_ERR_LOG: &str = "Invalid contract address"; #[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] @@ -346,18 +361,27 @@ pub enum TxHistoryError { InternalError(String), } -#[derive(Debug, Display)] -pub enum PrivKeyNotAllowed { +#[derive(Clone, Debug, Display)] +pub enum PrivKeyPolicyNotAllowed { #[display(fmt = "Hardware Wallet is not supported")] HardwareWalletNotSupported, } -#[derive(Debug, Display, PartialEq, Serialize)] +impl Serialize for PrivKeyPolicyNotAllowed { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[derive(Clone, Debug, Display, PartialEq, Serialize)] pub enum UnexpectedDerivationMethod { - #[display(fmt = "Iguana private key is unavailable")] - IguanaPrivKeyUnavailable, - #[display(fmt = "HD wallet is unavailable")] - HDWalletUnavailable, + #[display(fmt = "Expected 'SingleAddress' derivation method")] + ExpectedSingleAddress, + #[display(fmt = "Expected 'HDWallet' derivationMethod")] + ExpectedHDWallet, } pub trait Transaction: fmt::Debug + 'static { @@ -373,11 +397,25 @@ pub enum TransactionEnum { SignedEthTx(SignedEthTx), #[cfg(not(target_arch = "wasm32"))] ZTransaction(ZTransaction), + CosmosTransaction(CosmosTransaction), + #[cfg(not(target_arch = "wasm32"))] + LightningPayment(LightningPayment), } + ifrom!(TransactionEnum, UtxoTx); ifrom!(TransactionEnum, SignedEthTx); #[cfg(not(target_arch = "wasm32"))] ifrom!(TransactionEnum, ZTransaction); +#[cfg(not(target_arch = "wasm32"))] +ifrom!(TransactionEnum, LightningPayment); + +impl TransactionEnum { + #[cfg(not(target_arch = "wasm32"))] + pub fn supports_tx_helper(&self) -> bool { !matches!(self, TransactionEnum::LightningPayment(_)) } + + #[cfg(target_arch = "wasm32")] + pub fn supports_tx_helper(&self) -> bool { true } +} // NB: When stable and groked by IDEs, `enum_dispatch` can be used instead of `Deref` to speed things up. impl Deref for TransactionEnum { @@ -388,10 +426,23 @@ impl Deref for TransactionEnum { TransactionEnum::SignedEthTx(ref t) => t, #[cfg(not(target_arch = "wasm32"))] TransactionEnum::ZTransaction(ref t) => t, + TransactionEnum::CosmosTransaction(ref t) => t, + #[cfg(not(target_arch = "wasm32"))] + TransactionEnum::LightningPayment(ref p) => p, } } } +/// Error type for handling tx serialization/deserialization operations. +#[derive(Debug, Clone)] +pub enum TxMarshalingErr { + InvalidInput(String), + /// For cases where serialized and deserialized values doesn't verify each other. + CrossCheckFailed(String), + NotSupported(String), + Internal(String), +} + #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum TransactionErr { @@ -421,8 +472,6 @@ impl TransactionErr { } } -pub type TransactionFut = Box + Send>; - #[derive(Debug, PartialEq)] pub enum FoundSwapTxSpend { Spent(TransactionEnum), @@ -444,9 +493,37 @@ pub enum NegotiateSwapContractAddrErr { NoOtherAddrAndNoFallback, } +#[derive(Debug, Display, Eq, PartialEq)] +pub enum ValidateOtherPubKeyErr { + #[display(fmt = "InvalidPubKey: {:?}", _0)] + InvalidPubKey(String), +} + +#[derive(Clone, Debug)] +pub struct WatcherValidateTakerFeeInput { + pub taker_fee_hash: Vec, + pub sender_pubkey: Vec, + pub min_block_number: u64, + pub fee_addr: Vec, + pub lock_duration: u64, +} + +#[derive(Clone, Debug)] +pub struct WatcherValidatePaymentInput { + pub payment_tx: Vec, + pub taker_payment_refund_preimage: Vec, + pub time_lock: u32, + pub taker_pub: Vec, + pub maker_pub: Vec, + pub secret_hash: Vec, + pub try_spv_proof_until: u64, + pub confirmations: u64, +} + #[derive(Clone, Debug)] pub struct ValidatePaymentInput { pub payment_tx: Vec, + pub time_lock_duration: u64, pub time_lock: u32, pub other_pub: Vec, pub secret_hash: Vec, @@ -457,6 +534,24 @@ pub struct ValidatePaymentInput { pub unique_swap_data: Vec, } +#[derive(Clone, Debug)] +pub struct WatcherSearchForSwapTxSpendInput<'a> { + pub time_lock: u32, + pub taker_pub: &'a [u8], + pub maker_pub: &'a [u8], + pub secret_hash: &'a [u8], + pub tx: &'a [u8], + pub search_from_block: u64, +} + +#[derive(Clone, Debug)] +pub struct SendMakerPaymentSpendPreimageInput<'a> { + pub preimage: &'a [u8], + pub secret_hash: &'a [u8], + pub secret: &'a [u8], + pub taker_pub: &'a [u8], +} + pub struct SearchForSwapTxSpendInput<'a> { pub time_lock: u32, pub other_pub: &'a [u8], @@ -467,93 +562,143 @@ pub struct SearchForSwapTxSpendInput<'a> { pub swap_unique_data: &'a [u8], } +#[derive(Clone, Debug)] +pub struct SendSwapPaymentArgs<'a> { + pub time_lock_duration: u64, + pub time_lock: u32, + /// This is either: + /// * Taker's pubkey if this structure is used in [`SwapOps::send_maker_payment`]. + /// * Maker's pubkey if this structure is used in [`SwapOps::send_taker_payment`]. + pub other_pubkey: &'a [u8], + pub secret_hash: &'a [u8], + pub amount: BigDecimal, + pub swap_contract_address: &'a Option, + pub swap_unique_data: &'a [u8], + pub payment_instructions: &'a Option, +} + +#[derive(Clone, Debug)] +pub struct SendSpendPaymentArgs<'a> { + /// This is either: + /// * Taker's payment tx if this structure is used in [`SwapOps::send_maker_spends_taker_payment`]. + /// * Maker's payment tx if this structure is used in [`SwapOps::send_taker_spends_maker_payment`]. + pub other_payment_tx: &'a [u8], + pub time_lock: u32, + /// This is either: + /// * Taker's pubkey if this structure is used in [`SwapOps::send_maker_spends_taker_payment`]. + /// * Maker's pubkey if this structure is used in [`SwapOps::send_taker_spends_maker_payment`]. + pub other_pubkey: &'a [u8], + pub secret: &'a [u8], + pub secret_hash: &'a [u8], + pub swap_contract_address: &'a Option, + pub swap_unique_data: &'a [u8], +} + +#[derive(Clone, Debug)] +pub struct SendRefundPaymentArgs<'a> { + pub payment_tx: &'a [u8], + pub time_lock: u32, + /// This is either: + /// * Taker's pubkey if this structure is used in [`SwapOps::send_maker_refunds_payment`]. + /// * Maker's pubkey if this structure is used in [`SwapOps::send_taker_refunds_payment`]. + pub other_pubkey: &'a [u8], + pub secret_hash: &'a [u8], + pub swap_contract_address: &'a Option, + pub swap_unique_data: &'a [u8], +} + +#[derive(Clone, Debug)] +pub struct CheckIfMyPaymentSentArgs<'a> { + pub time_lock: u32, + pub other_pub: &'a [u8], + pub secret_hash: &'a [u8], + pub search_from_block: u64, + pub swap_contract_address: &'a Option, + pub swap_unique_data: &'a [u8], + pub amount: &'a BigDecimal, + pub payment_instructions: &'a Option, +} + +#[derive(Clone, Debug)] +pub struct ValidateFeeArgs<'a> { + pub fee_tx: &'a TransactionEnum, + pub expected_sender: &'a [u8], + pub fee_addr: &'a [u8], + pub amount: &'a BigDecimal, + pub min_block_number: u64, + pub uuid: &'a [u8], +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum PaymentInstructions { + #[cfg(not(target_arch = "wasm32"))] + Lightning(Invoice), +} + +#[derive(Display)] +pub enum PaymentInstructionsErr { + LightningInvoiceErr(String), + InternalError(String), +} + +impl From for PaymentInstructionsErr { + fn from(e: NumConversError) -> Self { PaymentInstructionsErr::InternalError(e.to_string()) } +} + +#[derive(Display)] +pub enum ValidateInstructionsErr { + ValidateLightningInvoiceErr(String), + UnsupportedCoin(String), +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for ValidateInstructionsErr { + fn from(e: ParseOrSemanticError) -> Self { ValidateInstructionsErr::ValidateLightningInvoiceErr(e.to_string()) } +} + +#[derive(Display)] +pub enum RefundError { + DecodeErr(String), + DbError(String), + Timeout(String), + Internal(String), +} + /// 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, uuid: &[u8]) -> TransactionFut; - 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; + fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs<'_>) -> TransactionFut; - 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; + fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs<'_>) -> TransactionFut; 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], + maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs<'_>, ) -> TransactionFut; 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], + taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs<'_>, ) -> TransactionFut; - 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; + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs<'_>) + -> TransactionFut; - 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; + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs<'_>) + -> TransactionFut; - fn validate_fee( - &self, - fee_tx: &TransactionEnum, - expected_sender: &[u8], - fee_addr: &[u8], - amount: &BigDecimal, - min_block_number: u64, - uuid: &[u8], - ) -> Box + Send>; + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) + -> Box + Send>; - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send>; + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()>; - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send>; + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()>; 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], + if_my_payment_sent_args: CheckIfMyPaymentSentArgs<'_>, ) -> Box, Error = String> + Send>; async fn search_for_swap_tx_spend_my( @@ -566,7 +711,9 @@ pub trait SwapOps { input: SearchForSwapTxSpendInput<'_>, ) -> Result, String>; - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String>; + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String>; + + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result>; /// Whether the refund transaction can be sent now /// For example: there are no additional conditions for ETH, but for some UTXO coins we should wait for @@ -581,12 +728,117 @@ pub trait SwapOps { Box::new(futures01::future::ok(result)) } + /// Whether the swap payment is refunded automatically or not when the locktime expires, or the other side fails the HTLC. + fn is_auto_refundable(&self) -> bool; + + /// Waits for an htlc to be refunded automatically. + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()>; + fn negotiate_swap_contract_addr( &self, other_side_address: Option<&[u8]>, ) -> Result, MmError>; + /// Consider using [`SwapOps::derive_htlc_pubkey`] if you need the public key only. + /// Some coins may not have a private key. fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair; + + /// Derives an HTLC key-pair and returns a public key corresponding to that key. + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec; + + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr>; + + /// Instructions from the taker on how the maker should send his payment. + async fn maker_payment_instructions( + &self, + secret_hash: &[u8], + amount: &BigDecimal, + maker_lock_duration: u64, + expires_in: u64, + ) -> Result>, MmError>; + + /// Instructions from the maker on how the taker should send his payment. + async fn taker_payment_instructions( + &self, + secret_hash: &[u8], + amount: &BigDecimal, + expires_in: u64, + ) -> Result>, MmError>; + + fn validate_maker_payment_instructions( + &self, + instructions: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + maker_lock_duration: u64, + ) -> Result>; + + fn validate_taker_payment_instructions( + &self, + instructions: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + ) -> Result>; + + fn is_supported_by_watchers(&self) -> bool { false } + + fn maker_locktime_multiplier(&self) -> f64 { 2.0 } +} + +/// Operations on maker coin from taker swap side +#[async_trait] +pub trait TakerSwapMakerCoin { + /// Performs an action on Maker coin payment just before the Taker Swap payment refund begins + async fn on_taker_payment_refund_start(&self, maker_payment: &[u8]) -> RefundResult<()>; + /// Performs an action on Maker coin payment after the Taker Swap payment is refunded successfully + async fn on_taker_payment_refund_success(&self, maker_payment: &[u8]) -> RefundResult<()>; +} + +/// Operations on taker coin from maker swap side +#[async_trait] +pub trait MakerSwapTakerCoin { + /// Performs an action on Taker coin payment just before the Maker Swap payment refund begins + async fn on_maker_payment_refund_start(&self, taker_payment: &[u8]) -> RefundResult<()>; + /// Performs an action on Taker coin payment after the Maker Swap payment is refunded successfully + async fn on_maker_payment_refund_success(&self, taker_payment: &[u8]) -> RefundResult<()>; +} + +#[async_trait] +pub trait WatcherOps { + fn send_maker_payment_spend_preimage(&self, input: SendMakerPaymentSpendPreimageInput) -> TransactionFut; + + fn send_taker_payment_refund_preimage( + &self, + watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut; + + fn create_taker_payment_refund_preimage( + &self, + _taker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut; + + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut; + + fn watcher_validate_taker_fee(&self, input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()>; + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()>; + + async fn watcher_search_for_swap_tx_spend( + &self, + input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String>; } /// Operations that coins have independently from the MarketMaker. @@ -594,7 +846,7 @@ pub trait SwapOps { pub trait MarketCoinOps { fn ticker(&self) -> &str; - fn my_address(&self) -> Result; + fn my_address(&self) -> MmResult; fn get_public_key(&self) -> Result>; @@ -622,6 +874,7 @@ 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 @@ -639,15 +892,17 @@ pub trait MarketCoinOps { check_every: u64, ) -> Box + Send>; - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, transaction: &[u8], + secret_hash: &[u8], wait_until: u64, from_block: u64, swap_contract_address: &Option, + check_every: f64, ) -> TransactionFut; - fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result; + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result>; fn current_block(&self) -> Box + Send>; @@ -662,7 +917,7 @@ pub trait MarketCoinOps { fn is_privacy(&self) -> bool { false } } -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(tag = "type")] pub enum WithdrawFee { UtxoFixed { @@ -715,7 +970,7 @@ pub trait GetWithdrawSenderAddress { #[serde(untagged)] pub enum WithdrawFrom { // AccountId { account_id: u32 }, - AddressId(HDAddressId), + AddressId(HDAccountAddressId), /// 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. @@ -724,7 +979,7 @@ pub enum WithdrawFrom { }, } -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] pub struct WithdrawRequest { coin: String, from: Option, @@ -735,6 +990,10 @@ pub struct WithdrawRequest { max: bool, fee: Option, memo: Option, + /// Currently, this flag is used by ETH/ERC20 coins activated with MetaMask **only**. + #[cfg(target_arch = "wasm32")] + #[serde(default)] + broadcast: bool, } #[derive(Debug, Deserialize)] @@ -776,26 +1035,6 @@ pub struct VerificationRequest { } impl WithdrawRequest { - pub fn new( - coin: String, - from: Option, - to: String, - amount: BigDecimal, - max: bool, - fee: Option, - memo: Option, - ) -> WithdrawRequest { - WithdrawRequest { - coin, - from, - to, - amount, - max, - fee, - memo, - } - } - pub fn new_max(coin: String, to: String) -> WithdrawRequest { WithdrawRequest { coin, @@ -805,6 +1044,8 @@ impl WithdrawRequest { max: true, fee: None, memo: None, + #[cfg(target_arch = "wasm32")] + broadcast: false, } } } @@ -843,6 +1084,7 @@ pub enum TxFeeDetails { Eth(EthTxFeeDetails), Qrc20(Qrc20FeeDetails), Slp(SlpFeeDetails), + Tendermint(TendermintFeeDetails), #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] Solana(SolanaFeeDetails), } @@ -861,6 +1103,7 @@ impl<'de> Deserialize<'de> for TxFeeDetails { Qrc20(Qrc20FeeDetails), #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] Solana(SolanaFeeDetails), + Tendermint(TendermintFeeDetails), } match Deserialize::deserialize(deserializer)? { @@ -869,6 +1112,7 @@ impl<'de> Deserialize<'de> for TxFeeDetails { TxFeeDetailsUnTagged::Qrc20(f) => Ok(TxFeeDetails::Qrc20(f)), #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] TxFeeDetailsUnTagged::Solana(f) => Ok(TxFeeDetails::Solana(f)), + TxFeeDetailsUnTagged::Tendermint(f) => Ok(TxFeeDetails::Tendermint(f)), } } } @@ -890,6 +1134,10 @@ impl From for TxFeeDetails { fn from(solana_details: SolanaFeeDetails) -> Self { TxFeeDetails::Solana(solana_details) } } +impl From for TxFeeDetails { + fn from(tendermint_details: TendermintFeeDetails) -> Self { TxFeeDetails::Tendermint(tendermint_details) } +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct KmdRewardsDetails { amount: BigDecimal, @@ -905,16 +1153,18 @@ impl KmdRewardsDetails { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Default, Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum TransactionType { StakingDelegation, RemoveDelegation, + #[default] StandardTransfer, TokenTransfer(BytesJson), -} - -impl Default for TransactionType { - fn default() -> Self { TransactionType::StandardTransfer } + FeeForTokenTx, + CustomTendermintMsg { + msg_type: CustomTendermintMsgType, + token_id: Option, + }, } /// Transaction details @@ -954,6 +1204,7 @@ pub struct TransactionDetails { /// Type of transactions, default is StandardTransfer #[serde(default)] transaction_type: TransactionType, + memo: Option, } #[derive(Clone, Copy, Debug)] @@ -1038,6 +1289,8 @@ pub enum FeeApproxStage { WithoutApprox, /// Increase the trade fee slightly. StartSwap, + /// Increase the trade fee slightly + WatcherPreimage, /// Increase the trade fee significantly. OrderIssue, /// Increase the trade fee largely. @@ -1160,7 +1413,7 @@ impl NumConversError { pub fn description(&self) -> &str { &self.0 } } -#[derive(Debug, Display, PartialEq)] +#[derive(Clone, Debug, Display, PartialEq, Serialize)] pub enum BalanceError { #[display(fmt = "Transport: {}", _0)] Transport(String), @@ -1348,8 +1601,8 @@ impl From for DelegationError { } } -impl From for DelegationError { - fn from(e: PrivKeyNotAllowed) -> Self { DelegationError::DelegationOpsNotSupported { reason: e.to_string() } } +impl From for DelegationError { + fn from(e: PrivKeyPolicyNotAllowed) -> Self { DelegationError::DelegationOpsNotSupported { reason: e.to_string() } } } impl From for DelegationError { @@ -1414,23 +1667,9 @@ impl DelegationError { } } -#[derive(Clone, Debug, Deserialize, Display, Serialize, SerializeErrorType, PartialEq)] +#[derive(Clone, Debug, Display, EnumFromStringify, EnumFromTrait, 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 @@ -1447,6 +1686,17 @@ pub enum WithdrawError { available: BigDecimal, required: BigDecimal, }, + #[display( + fmt = "Not enough {} to afford fee. Available {}, required at least {}", + coin, + available, + required + )] + NotSufficientPlatformBalanceForFee { + coin: String, + available: BigDecimal, + required: BigDecimal, + }, #[display(fmt = "Balance is zero")] ZeroBalanceToWithdrawMax, #[display(fmt = "The amount {} is too small, required at least {}", amount, threshold)] @@ -1459,18 +1709,25 @@ pub enum WithdrawError { InvalidMemo(String), #[display(fmt = "No such coin {}", coin)] NoSuchCoin { coin: String }, + #[from_trait(WithTimeout::timeout)] #[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 = "RPC 'task' is awaiting '{}' user action", expected)] + UnexpectedUserAction { expected: String }, + #[from_trait(WithHwRpcError::hw_rpc_error)] + HwError(HwRpcError), + #[cfg(target_arch = "wasm32")] + BroadcastExpected(String), #[display(fmt = "Transport error: {}", _0)] Transport(String), + #[from_trait(WithInternal::internal)] + #[from_stringify("NumConversError", "UnexpectedDerivationMethod", "PrivKeyPolicyNotAllowed")] #[display(fmt = "Internal error: {}", _0)] InternalError(String), } @@ -1481,8 +1738,8 @@ impl HttpStatusCode for WithdrawError { WithdrawError::NoSuchCoin { .. } => StatusCode::NOT_FOUND, WithdrawError::Timeout(_) => StatusCode::REQUEST_TIMEOUT, WithdrawError::CoinDoesntSupportInitWithdraw { .. } - | WithdrawError::UnexpectedUserAction { .. } | WithdrawError::NotSufficientBalance { .. } + | WithdrawError::NotSufficientPlatformBalanceForFee { .. } | WithdrawError::ZeroBalanceToWithdrawMax | WithdrawError::AmountTooLow { .. } | WithdrawError::InvalidAddress(_) @@ -1490,21 +1747,16 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::InvalidMemo(_) | 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, + | WithdrawError::UnknownAccount { .. } + | WithdrawError::UnexpectedUserAction { .. } => StatusCode::BAD_REQUEST, + WithdrawError::HwError(_) => StatusCode::GONE, + #[cfg(target_arch = "wasm32")] + WithdrawError::BroadcastExpected(_) => StatusCode::BAD_REQUEST, + WithdrawError::Transport(_) | WithdrawError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } -impl From for WithdrawError { - fn from(e: NumConversError) -> Self { WithdrawError::InternalError(e.to_string()) } -} - impl From for WithdrawError { fn from(e: BalanceError) -> Self { match e { @@ -1531,12 +1783,8 @@ impl From for WithdrawError { } } -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 From for WithdrawError { + fn from(e: TimeoutError) -> Self { WithdrawError::Timeout(e.duration) } } impl WithdrawError { @@ -1583,11 +1831,12 @@ impl WithdrawError { } } -#[derive(Serialize, Display, Debug, SerializeErrorType)] +#[derive(Serialize, Display, Debug, EnumFromStringify, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum SignatureError { #[display(fmt = "Invalid request: {}", _0)] InvalidRequest(String), + #[from_stringify("CoinFindError", "ethkey::Error", "keys::Error", "PrivKeyPolicyNotAllowed")] #[display(fmt = "Internal error: {}", _0)] InternalError(String), #[display(fmt = "Coin is not found: {}", _0)] @@ -1607,22 +1856,6 @@ impl HttpStatusCode for SignatureError { } } -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 { @@ -1688,7 +1921,9 @@ impl From for VerificationError { /// 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 { +pub trait MmCoin: + SwapOps + TakerSwapMakerCoin + MakerSwapTakerCoin + WatcherOps + MarketCoinOps + 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); // coin statistics that we might want to share with UI; @@ -1703,10 +1938,19 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { coin_conf["wallet_only"].as_bool().unwrap_or(false) } + /// Returns a spawner pinned to the coin. + /// + /// # Note + /// + /// `CoinFutSpawner` doesn't prevent the spawned futures from being aborted. + fn spawner(&self) -> CoinFutSpawner; + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut; fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut; + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut; + /// Maximum number of digits after decimal point used to denominate integer coin units (satoshis, wei, etc.) fn decimals(&self) -> u8; @@ -1719,6 +1963,7 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { fn process_history_loop(&self, ctx: MmArc) -> Box + Send>; /// Path to tx history file + #[cfg(not(target_arch = "wasm32"))] 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: @@ -1729,6 +1974,18 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { .join(format!("{}_{}.json", self.ticker(), my_address)) } + /// Path to tx history migration file + #[cfg(not(target_arch = "wasm32"))] + fn tx_migration_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!("{}_{}_migration", 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> { @@ -1739,6 +1996,14 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { save_history_to_file_impl(self, ctx, history) } + #[cfg(not(target_arch = "wasm32"))] + fn get_tx_history_migration(&self, ctx: &MmArc) -> TxHistoryFut { get_tx_history_migration_impl(self, ctx) } + + #[cfg(not(target_arch = "wasm32"))] + fn update_migration_file(&self, ctx: &MmArc, migration_number: u64) -> TxHistoryFut<()> { + update_migration_file_impl(self, ctx, migration_number) + } + /// Transaction history background sync status fn history_sync_status(&self) -> HistorySyncState; @@ -1753,7 +2018,7 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { ) -> 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; + fn get_receiver_trade_fee(&self, send_amount: BigDecimal, 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. async fn get_fee_to_send_taker_fee( @@ -1777,6 +2042,9 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { /// Get swap contract address if the coin uses it in Atomic Swaps. fn swap_contract_address(&self) -> Option; + /// Get fallback swap contract address if the coin uses it in Atomic Swaps. + fn fallback_swap_contract(&self) -> Option; + /// The minimum number of confirmations at which a transaction is considered mature. fn mature_confirmations(&self) -> Option; @@ -1785,9 +2053,52 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { /// Check if serialized coin protocol info is supported by current version. fn is_coin_protocol_supported(&self, info: &Option>) -> bool; + + /// Abort all coin related futures on coin deactivation. + fn on_disabled(&self) -> Result<(), AbortedError>; + + /// For Handling the removal/deactivation of token on platform coin deactivation. + fn on_token_deactivated(&self, _ticker: &str); } -#[derive(Clone, Debug)] +/// The coin futures spawner. It's used to spawn futures that can be aborted immediately or after a timeout +/// on the the coin deactivation. +/// +/// # Note +/// +/// `CoinFutSpawner` doesn't prevent the spawned futures from being aborted. +#[derive(Clone)] +pub struct CoinFutSpawner { + inner: WeakSpawner, +} + +impl CoinFutSpawner { + pub fn new(system: &AbortableQueue) -> CoinFutSpawner { + CoinFutSpawner { + inner: system.weak_spawner(), + } + } +} + +impl SpawnFuture for CoinFutSpawner { + fn spawn(&self, f: F) + where + F: Future03 + Send + 'static, + { + self.inner.spawn(f) + } +} + +impl SpawnAbortable for CoinFutSpawner { + fn spawn_with_settings(&self, fut: F, settings: AbortSettings) + where + F: Future03 + Send + 'static, + { + self.inner.spawn_with_settings(fut, settings) + } +} + +#[derive(Clone)] #[allow(clippy::large_enum_variant)] pub enum MmCoinEnum { UtxoCoin(UtxoStandardCoin), @@ -1798,6 +2109,8 @@ pub enum MmCoinEnum { ZCoin(ZCoin), Bch(BchCoin), SlpToken(SlpToken), + Tendermint(TendermintCoin), + TendermintToken(TendermintToken), #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] SolanaCoin(SolanaCoin), #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] @@ -1845,6 +2158,14 @@ impl From for MmCoinEnum { fn from(c: SlpToken) -> MmCoinEnum { MmCoinEnum::SlpToken(c) } } +impl From for MmCoinEnum { + fn from(c: TendermintCoin) -> Self { MmCoinEnum::Tendermint(c) } +} + +impl From for MmCoinEnum { + fn from(c: TendermintToken) -> Self { MmCoinEnum::TendermintToken(c) } +} + #[cfg(not(target_arch = "wasm32"))] impl From for MmCoinEnum { fn from(c: LightningCoin) -> MmCoinEnum { MmCoinEnum::LightningCoin(c) } @@ -1866,6 +2187,8 @@ impl Deref for MmCoinEnum { MmCoinEnum::EthCoin(ref c) => c, MmCoinEnum::Bch(ref c) => c, MmCoinEnum::SlpToken(ref c) => c, + MmCoinEnum::Tendermint(ref c) => c, + MmCoinEnum::TendermintToken(ref c) => c, #[cfg(not(target_arch = "wasm32"))] MmCoinEnum::LightningCoin(ref c) => c, #[cfg(not(target_arch = "wasm32"))] @@ -1904,20 +2227,18 @@ pub struct CoinsContext { /// Similar to `LP_coins`. coins: AsyncMutex>, balance_update_handlers: AsyncMutex>>, - withdraw_task_manager: WithdrawTaskManagerShared, + account_balance_task_manager: AccountBalanceTaskManagerShared, create_account_manager: CreateAccountTaskManagerShared, + get_new_address_manager: GetNewAddressTaskManagerShared, + platform_coin_tokens: PaMutex>>, scan_addresses_manager: ScanAddressesTaskManagerShared, + withdraw_task_manager: WithdrawTaskManagerShared, #[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, @@ -1928,52 +2249,126 @@ impl CoinsContext { pub fn from_ctx(ctx: &MmArc) -> Result, String> { Ok(try_s!(from_ctx(&ctx.coins_ctx, move || { Ok(CoinsContext { + platform_coin_tokens: PaMutex::new(HashMap::new()), coins: AsyncMutex::new(HashMap::new()), balance_update_handlers: AsyncMutex::new(vec![]), - withdraw_task_manager: WithdrawTaskManager::new_shared(), + account_balance_task_manager: AccountBalanceTaskManager::new_shared(), create_account_manager: CreateAccountTaskManager::new_shared(), + get_new_address_manager: GetNewAddressTaskManager::new_shared(), scan_addresses_manager: ScanAddressesTaskManager::new_shared(), + withdraw_task_manager: WithdrawTaskManager::new_shared(), #[cfg(target_arch = "wasm32")] - tx_history_db: ConstructibleDb::new_shared(ctx), + tx_history_db: ConstructibleDb::new(ctx).into_shared(), #[cfg(target_arch = "wasm32")] - hd_wallet_db: ConstructibleDb::new_shared(ctx), + hd_wallet_db: ConstructibleDb::new_shared_db(ctx).into_shared(), }) }))) } - pub async fn add_coin(&self, coin: MmCoinEnum) -> Result<(), MmError> { + pub async fn add_token(&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(), + return MmError::err(RegisterCoinError::CoinIsInitializedAlready { + coin: coin.ticker().into(), }); } + let ticker = coin.ticker(); + + let mut platform_coin_tokens = self.platform_coin_tokens.lock(); + // Here, we try to add a token to platform_coin_tokens if the token belongs to a platform coin. + if let Some(platform) = platform_coin_tokens.get_mut(coin.platform_ticker()) { + platform.insert(ticker.to_owned()); + } - coins.insert(coin.ticker().into(), coin); + coins.insert(ticker.into(), coin); Ok(()) } + /// Adds a Layer 2 coin that depends on a standalone platform. + /// The process of adding l2 coins is identical to that of adding tokens. + pub async fn add_l2(&self, coin: MmCoinEnum) -> Result<(), MmError> { + self.add_token(coin).await + } + pub async fn add_platform_with_tokens( &self, platform: MmCoinEnum, tokens: Vec, ) -> Result<(), MmError> { let mut coins = self.coins.lock().await; + let mut platform_coin_tokens = self.platform_coin_tokens.lock(); + if coins.contains_key(platform.ticker()) { return MmError::err(PlatformIsAlreadyActivatedErr { ticker: platform.ticker().into(), }); } - coins.insert(platform.ticker().into(), platform); + let platform_ticker = platform.ticker().to_string(); + coins.insert(platform_ticker.clone(), platform); // Tokens can't be activated without platform coin so we can safely insert them without checking prior existence + let mut token_tickers = Vec::with_capacity(tokens.len()); + // TODO + // Handling for these case: + // USDT was activated via enable RPC + // We try to activate ETH coin and USDT token via enable_eth_with_tokens for token in tokens { + token_tickers.push(token.ticker().to_string()); coins.insert(token.ticker().into(), token); } + + platform_coin_tokens + .entry(platform_ticker) + .or_default() + .extend(token_tickers); Ok(()) } + /// If `ticker` is a platform coin, returns tokens dependent on it. + pub async fn get_dependent_tokens(&self, ticker: &str) -> HashSet { + let coins = self.platform_coin_tokens.lock(); + coins.get(ticker).cloned().unwrap_or_default() + } + + pub async fn remove_coin(&self, coin: MmCoinEnum) { + let ticker = coin.ticker(); + let platform_ticker = coin.platform_ticker(); + let mut coins_storage = self.coins.lock().await; + let mut platform_tokens_storage = self.platform_coin_tokens.lock(); + + // Check if ticker is a platform coin and remove from it platform's token list + if ticker == platform_ticker { + if let Some(tokens_to_remove) = platform_tokens_storage.remove(ticker) { + tokens_to_remove.iter().for_each(|token| { + if let Some(token) = coins_storage.remove(token) { + // Abort all token related futures on token deactivation + token + .on_disabled() + .error_log_with_msg(&format!("Error aborting coin({ticker}) futures")); + } + }); + }; + } else { + if let Some(tokens) = platform_tokens_storage.get_mut(platform_ticker) { + tokens.remove(ticker); + } + if let Some(platform_coin) = coins_storage.get(platform_ticker) { + platform_coin.on_token_deactivated(ticker); + } + }; + + // Remove coin from coin list + coins_storage + .remove(ticker) + .ok_or(format!("{} is disabled already", ticker)) + .error_log(); + + // Abort all coin related futures on coin deactivation + coin.on_disabled() + .error_log_with_msg(&format!("Error aborting coin({ticker}) futures")); + } + #[cfg(target_arch = "wasm32")] async fn tx_history_db(&self) -> TxHistoryResult> { Ok(self.tx_history_db.get_or_initialize().await?) @@ -1983,18 +2378,12 @@ impl CoinsContext { /// This enum is used in coin activation requests. #[derive(Copy, Clone, Debug, Deserialize, Serialize)] pub enum PrivKeyActivationPolicy { - IguanaPrivKey, + ContextPrivKey, 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 } +impl Default for PrivKeyActivationPolicy { + fn default() -> Self { PrivKeyActivationPolicy::ContextPrivKey } } #[derive(Debug)] @@ -2011,59 +2400,69 @@ impl PrivKeyPolicy { } } - pub fn key_pair_or_err(&self) -> Result<&T, MmError> { + pub fn key_pair_or_err(&self) -> Result<&T, MmError> { self.key_pair() - .or_mm_err(|| PrivKeyNotAllowed::HardwareWalletNotSupported) + .or_mm_err(|| PrivKeyPolicyNotAllowed::HardwareWalletNotSupported) } } #[derive(Clone)] -pub enum PrivKeyBuildPolicy<'a> { - IguanaPrivKey(&'a [u8]), +pub enum PrivKeyBuildPolicy { + IguanaPrivKey(IguanaPrivKey), + GlobalHDAccount(GlobalHDAccountArc), Trezor, } -impl<'a> PrivKeyBuildPolicy<'a> { - pub fn iguana_priv_key(crypto_ctx: &'a CryptoCtx) -> Self { - PrivKeyBuildPolicy::IguanaPrivKey(crypto_ctx.iguana_ctx().secp256k1_privkey_bytes()) +impl PrivKeyBuildPolicy { + /// Detects the `PrivKeyBuildPolicy` with which the given `MmArc` is initialized. + pub fn detect_priv_key_policy(ctx: &MmArc) -> MmResult { + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + + match crypto_ctx.key_pair_policy() { + // Use an internal private key as the coin secret. + KeyPairPolicy::Iguana => Ok(PrivKeyBuildPolicy::IguanaPrivKey( + crypto_ctx.mm2_internal_privkey_secret(), + )), + KeyPairPolicy::GlobalHDAccount(global_hd) => Ok(PrivKeyBuildPolicy::GlobalHDAccount(global_hd.clone())), + } } } #[derive(Debug)] pub enum DerivationMethod { - Iguana(Address), + SingleAddress(Address), HDWallet(HDWallet), } impl DerivationMethod { - pub fn iguana(&self) -> Option<&Address> { + pub fn single_addr(&self) -> Option<&Address> { match self { - DerivationMethod::Iguana(my_address) => Some(my_address), + DerivationMethod::SingleAddress(my_address) => Some(my_address), DerivationMethod::HDWallet(_) => None, } } - pub fn iguana_or_err(&self) -> MmResult<&Address, UnexpectedDerivationMethod> { - self.iguana() - .or_mm_err(|| UnexpectedDerivationMethod::IguanaPrivKeyUnavailable) + pub fn single_addr_or_err(&self) -> MmResult<&Address, UnexpectedDerivationMethod> { + self.single_addr() + .or_mm_err(|| UnexpectedDerivationMethod::ExpectedSingleAddress) } pub fn hd_wallet(&self) -> Option<&HDWallet> { match self { - DerivationMethod::Iguana(_) => None, + DerivationMethod::SingleAddress(_) => 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) + .or_mm_err(|| UnexpectedDerivationMethod::ExpectedHDWallet) } /// # Panic /// /// Panic if the address mode is [`DerivationMethod::HDWallet`]. - pub fn unwrap_iguana(&self) -> &Address { self.iguana_or_err().unwrap() } + pub fn unwrap_single_addr(&self) -> &Address { self.single_addr_or_err().unwrap() } } #[async_trait] @@ -2102,11 +2501,13 @@ pub enum CoinProtocol { BCH { slp_prefix: String, }, + TENDERMINT(TendermintProtocolInfo), + TENDERMINTTOKEN(TendermintTokenProtocolInfo), #[cfg(not(target_arch = "wasm32"))] LIGHTNING { platform: String, network: BlockchainNetwork, - confirmations: PlatformCoinConfirmations, + confirmation_targets: PlatformCoinConfirmationTargets, }, #[cfg(not(target_arch = "wasm32"))] SOLANA, @@ -2131,6 +2532,8 @@ pub trait RpcTransportEventHandler { fn on_incoming_response(&self, data: &[u8]); fn on_connected(&self, address: String) -> Result<(), String>; + + fn on_disconnected(&self, address: String) -> Result<(), String>; } impl fmt::Debug for dyn RpcTransportEventHandler + Send + Sync { @@ -2145,6 +2548,8 @@ impl RpcTransportEventHandler for RpcTransportEventHandlerShared { fn on_incoming_response(&self, data: &[u8]) { self.as_ref().on_incoming_response(data) } fn on_connected(&self, address: String) -> Result<(), String> { self.as_ref().on_connected(address) } + + fn on_disconnected(&self, address: String) -> Result<(), String> { self.as_ref().on_disconnected(address) } } impl RpcTransportEventHandler for Vec { @@ -2171,6 +2576,13 @@ impl RpcTransportEventHandler for Vec { } Ok(()) } + + fn on_disconnected(&self, address: String) -> Result<(), String> { + for handler in self { + try_s!(handler.on_disconnected(address.clone())) + } + Ok(()) + } } pub enum RpcClientType { @@ -2216,16 +2628,16 @@ impl RpcTransportEventHandler for CoinTransportMetrics { fn on_outgoing_request(&self, data: &[u8]) { mm_counter!(self.metrics, "rpc_client.traffic.out", data.len() as u64, - "coin" => self.ticker.clone(), "client" => self.client.clone()); + "coin" => self.ticker.to_owned(), "client" => self.client.to_owned()); mm_counter!(self.metrics, "rpc_client.request.count", 1, - "coin" => self.ticker.clone(), "client" => self.client.clone()); + "coin" => self.ticker.to_owned(), "client" => self.client.to_owned()); } fn on_incoming_response(&self, data: &[u8]) { mm_counter!(self.metrics, "rpc_client.traffic.in", data.len() as u64, - "coin" => self.ticker.clone(), "client" => self.client.clone()); + "coin" => self.ticker.to_owned(), "client" => self.client.to_owned()); mm_counter!(self.metrics, "rpc_client.response.count", 1, - "coin" => self.ticker.clone(), "client" => self.client.clone()); + "coin" => self.ticker.to_owned(), "client" => self.client.to_owned()); } fn on_connected(&self, _address: String) -> Result<(), String> { @@ -2233,6 +2645,12 @@ impl RpcTransportEventHandler for CoinTransportMetrics { // Now just return the Ok Ok(()) } + + fn on_disconnected(&self, _address: String) -> Result<(), String> { + // Handle disconnected endpoint if necessary. + // Now just return the Ok + Ok(()) + } } #[async_trait] @@ -2299,10 +2717,9 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result Result { 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() + try_s!(utxo_standard_coin_with_policy(ctx, ticker, &coins_en, ¶ms, priv_key_policy).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() + try_s!(qtum_coin_with_policy(ctx, ticker, &coins_en, ¶ms, priv_key_policy).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, protocol, priv_key_policy).await).into() }, CoinProtocol::QRC20 { platform, @@ -2331,8 +2748,16 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result Result Result return ERR!("TENDERMINT protocol is not supported by lp_coininit"), + CoinProtocol::TENDERMINTTOKEN(_) => return ERR!("TENDERMINTTOKEN protocol is not supported by lp_coininit"), #[cfg(not(target_arch = "wasm32"))] CoinProtocol::ZHTLC { .. } => return ERR!("ZHTLC protocol is not supported by lp_coininit"), #[cfg(not(target_arch = "wasm32"))] @@ -2376,9 +2809,13 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result Result<(), MmError> { - let RegisterCoinParams { ticker, tx_history } = params; + let RegisterCoinParams { ticker } = 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 @@ -2415,26 +2851,22 @@ pub async fn lp_register_coin( }, RawEntryMut::Vacant(ve) => ve.insert(ticker.clone(), coin.clone()), }; - if tx_history { - lp_spawn_tx_history(ctx.clone(), coin).map_to_mm(RegisterCoinError::Internal)?; - } - Ok(()) -} -#[cfg(not(target_arch = "wasm32"))] -fn lp_spawn_tx_history(ctx: MmArc, coin: MmCoinEnum) -> Result<(), String> { - try_s!(std::thread::Builder::new() - .name(format!("tx_history_{}", coin.ticker())) - .spawn(move || coin.process_history_loop(ctx).wait())); + if coin.ticker() == coin.platform_ticker() { + let mut platform_coin_tokens = cctx.platform_coin_tokens.lock(); + platform_coin_tokens + .entry(coin.ticker().to_string()) + .or_insert_with(HashSet::new); + } Ok(()) } -#[cfg(target_arch = "wasm32")] fn lp_spawn_tx_history(ctx: MmArc, coin: MmCoinEnum) -> Result<(), String> { + let spawner = coin.spawner(); let fut = async move { let _res = coin.process_history_loop(ctx).compat().await; }; - common::executor::spawn_local(fut); + spawner.spawn(fut); Ok(()) } @@ -2620,7 +3052,7 @@ pub async fn send_raw_transaction(ctx: MmArc, req: Json) -> Result Result>, String> Ok(try_s!(Response::builder().body(res))) } -pub async fn disable_coin(ctx: &MmArc, ticker: &str) -> Result<(), String> { - let coins_ctx = try_s!(CoinsContext::from_ctx(ctx)); - let mut coins = coins_ctx.coins.lock().await; - match coins.remove(ticker) { - Some(_) => Ok(()), - None => ERR!("{} is disabled already", ticker), - } -} - #[derive(Deserialize)] pub struct ConfirmationsReq { coin: String, @@ -2921,6 +3344,25 @@ pub fn address_by_coin_conf_and_pubkey_str( _ => ERR!("Platform protocol {:?} is not BCH", platform_protocol), } }, + CoinProtocol::TENDERMINT(protocol) => tendermint::account_id_from_pubkey_hex(&protocol.account_prefix, pubkey) + .map(|id| id.to_string()) + .map_err(|e| e.to_string()), + CoinProtocol::TENDERMINTTOKEN(proto) => { + let platform_conf = coin_conf(ctx, &proto.platform); + if platform_conf.is_null() { + return ERR!("platform {} conf is null", proto.platform); + } + // TODO is there any way to make it better without duplicating the prefix in the IBC conf? + let platform_protocol: CoinProtocol = try_s!(json::from_value(platform_conf["protocol"].clone())); + match platform_protocol { + CoinProtocol::TENDERMINT(platform) => { + tendermint::account_id_from_pubkey_hex(&platform.account_prefix, pubkey) + .map(|id| id.to_string()) + .map_err(|e| e.to_string()) + }, + _ => ERR!("Platform protocol {:?} is not TENDERMINT", platform_protocol), + } + }, #[cfg(not(target_arch = "wasm32"))] CoinProtocol::LIGHTNING { .. } => { ERR!("address_by_coin_conf_and_pubkey_str is not implemented for lightning protocol yet!") @@ -2941,7 +3383,7 @@ where { let ctx = ctx.clone(); let ticker = coin.ticker().to_owned(); - let my_address = try_f!(coin.my_address().map_to_mm(TxHistoryError::InternalError)); + let my_address = try_f!(coin.my_address()); let fut = async move { let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); @@ -3015,9 +3457,9 @@ where { let ctx = ctx.clone(); let ticker = coin.ticker().to_owned(); - let my_address = try_f!(coin.my_address().map_to_mm(TxHistoryError::InternalError)); + let my_address = try_f!(coin.my_address()); - history.sort_unstable_by(compare_transactions); + history.sort_unstable_by(compare_transaction_details); let fut = async move { let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); @@ -3028,6 +3470,61 @@ where Box::new(fut.boxed().compat()) } +#[cfg(not(target_arch = "wasm32"))] +fn get_tx_history_migration_impl(coin: &T, ctx: &MmArc) -> TxHistoryFut +where + T: MmCoin + MarketCoinOps + ?Sized, +{ + let migration_path = coin.tx_migration_path(ctx); + + let fut = async move { + let current_migration = match fs::read(&migration_path).await { + Ok(bytes) => { + let mut num_bytes = [0; 8]; + if bytes.len() == 8 { + num_bytes.clone_from_slice(&bytes); + u64::from_le_bytes(num_bytes) + } else { + 0 + } + }, + Err(_) => 0, + }; + + Ok(current_migration) + }; + + Box::new(fut.boxed().compat()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn update_migration_file_impl(coin: &T, ctx: &MmArc, migration_number: u64) -> TxHistoryFut<()> +where + T: MmCoin + MarketCoinOps + ?Sized, +{ + let migration_path = coin.tx_migration_path(ctx); + let tmp_file = format!("{}.tmp", migration_path.display()); + + let fut = async move { + let fs_fut = async { + let mut file = fs::File::create(&tmp_file).await?; + file.write_all(&migration_number.to_le_bytes()).await?; + file.flush().await?; + fs::rename(&tmp_file, migration_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()) +} + #[cfg(not(target_arch = "wasm32"))] fn save_history_to_file_impl(coin: &T, ctx: &MmArc, mut history: Vec) -> TxHistoryFut<()> where @@ -3036,7 +3533,7 @@ where let history_path = coin.tx_history_path(ctx); let tmp_file = format!("{}.tmp", history_path.display()); - history.sort_unstable_by(compare_transactions); + history.sort_unstable_by(compare_transaction_details); let fut = async move { let content = json::to_vec(&history).map_to_mm(|e| TxHistoryError::ErrorSerializing(e.to_string()))?; @@ -3059,10 +3556,28 @@ where Box::new(fut.boxed().compat()) } -fn compare_transactions(a: &TransactionDetails, b: &TransactionDetails) -> Ordering { +pub(crate) fn compare_transaction_details(a: &TransactionDetails, b: &TransactionDetails) -> Ordering { + let a = TxIdHeight::new(a.block_height, a.internal_id.deref()); + let b = TxIdHeight::new(b.block_height, b.internal_id.deref()); + compare_transactions(a, b) +} + +pub(crate) struct TxIdHeight { + block_height: u64, + tx_id: Id, +} + +impl TxIdHeight { + pub(crate) fn new(block_height: u64, tx_id: Id) -> TxIdHeight { TxIdHeight { block_height, tx_id } } +} + +pub(crate) fn compare_transactions(a: TxIdHeight, b: TxIdHeight) -> Ordering +where + Id: Ord, +{ // 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) + a.tx_id.cmp(&b.tx_id) } else if a.block_height == 0 { Ordering::Less } else if b.block_height == 0 { @@ -3071,3 +3586,13 @@ fn compare_transactions(a: &TransactionDetails, b: &TransactionDetails) -> Order b.block_height.cmp(&a.block_height) } } + +/// Use trait in the case, when we have to send requests to rpc client. +#[async_trait] +pub trait RpcCommonOps { + type RpcClient; + type Error; + + /// Returns an alive RPC client or returns an error if no RPC endpoint is currently available. + async fn get_live_client(&self) -> Result; +} diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index c297264ab2..971ab7b19e 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -1,15 +1,21 @@ -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 crate::hd_wallet::{AddressDerivingError, InvalidBip44ChainError}; +use crate::tendermint::{TENDERMINT_ASSET_PROTOCOL_TYPE, TENDERMINT_COIN_PROTOCOL_TYPE}; +use crate::tx_history_storage::{CreateTxHistoryStorageError, FilteringAddresses, GetTxHistoryFilters, + TxHistoryStorageBuilder, WalletId}; +use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; +use crate::{coin_conf, lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HDAccountAddressId, 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 crypto::StandardHDPath; 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 num_traits::ToPrimitive; use rpc::v1::types::{Bytes as BytesJson, ToTxHash}; use std::collections::HashSet; @@ -66,13 +72,21 @@ pub trait TxHistoryStorage: Send + Sync + 'static { internal_id: &BytesJson, ) -> Result, MmError>; + /// Gets the highest block_height from the selected wallet's history + async fn get_highest_block_height(&self, wallet_id: &WalletId) -> Result, MmError>; + /// Returns whether the history contains unconfirmed transactions. - async fn history_contains_unconfirmed_txes(&self, wallet_id: &WalletId) -> Result>; + async fn history_contains_unconfirmed_txes( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result>; /// Gets the unconfirmed transactions from the wallet's history. async fn get_unconfirmed_txes_from_history( &self, wallet_id: &WalletId, + for_addresses: FilteringAddresses, ) -> Result, MmError>; /// Updates transaction in the selected wallet's history @@ -86,7 +100,11 @@ pub trait TxHistoryStorage: Send + Sync + 'static { 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>; + async fn unique_tx_hashes_num_in_history( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result>; /// Adds the given `tx_hex` transaction to the selected wallet's cache. async fn add_tx_to_cache( @@ -203,8 +221,18 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T bytes_for_hash.extend_from_slice(&token_id.0); sha256(&bytes_for_hash).to_vec().into() }, + TransactionType::CustomTendermintMsg { token_id, .. } => { + if let Some(token_id) = 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() + } else { + tx_hash.clone() + } + }, TransactionType::StakingDelegation | TransactionType::RemoveDelegation + | TransactionType::FeeForTokenTx | TransactionType::StandardTransfer => tx_hash.clone(), }; @@ -224,17 +252,34 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T internal_id, kmd_rewards: None, transaction_type: self.transaction_type, + memo: None, } } } -#[derive(Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum MyTxHistoryTarget { + Iguana, + AccountId { account_id: u32 }, + AddressId(HDAccountAddressId), + AddressDerivationPath(StandardHDPath), +} + +impl Default for MyTxHistoryTarget { + fn default() -> Self { MyTxHistoryTarget::Iguana } +} + +#[derive(Clone, Deserialize)] pub struct MyTxHistoryRequestV2 { - coin: String, + pub(crate) coin: String, #[serde(default = "ten")] pub(crate) limit: usize, #[serde(default)] pub(crate) paging_options: PagingOptionsEnum, + #[serde(default)] + pub(crate) target: MyTxHistoryTarget, } #[derive(Serialize)] @@ -247,6 +292,7 @@ pub struct MyTxHistoryDetails { #[derive(Serialize)] pub struct MyTxHistoryResponseV2 { pub(crate) coin: String, + pub(crate) target: MyTxHistoryTarget, pub(crate) current_block: u64, pub(crate) transactions: Vec, pub(crate) sync_status: HistorySyncState, @@ -261,6 +307,7 @@ pub struct MyTxHistoryResponseV2 { #[serde(tag = "error_type", content = "error_data")] pub enum MyTxHistoryErrorV2 { CoinIsNotActive(String), + InvalidTarget(String), StorageIsNotInitialized(String), StorageError(String), RpcError(String), @@ -268,15 +315,21 @@ pub enum MyTxHistoryErrorV2 { Internal(String), } +impl MyTxHistoryErrorV2 { + pub fn with_expected_target(actual: MyTxHistoryTarget, expected: &str) -> MyTxHistoryErrorV2 { + MyTxHistoryErrorV2::InvalidTarget(format!("Expected {:?} target, found: {:?}", expected, actual)) + } +} + impl HttpStatusCode for MyTxHistoryErrorV2 { fn status_code(&self) -> StatusCode { match self { - MyTxHistoryErrorV2::CoinIsNotActive(_) => StatusCode::PRECONDITION_REQUIRED, + MyTxHistoryErrorV2::CoinIsNotActive(_) => StatusCode::NOT_FOUND, MyTxHistoryErrorV2::StorageIsNotInitialized(_) | MyTxHistoryErrorV2::StorageError(_) | MyTxHistoryErrorV2::RpcError(_) | MyTxHistoryErrorV2::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - MyTxHistoryErrorV2::NotSupportedFor(_) => StatusCode::BAD_REQUEST, + MyTxHistoryErrorV2::NotSupportedFor(_) | MyTxHistoryErrorV2::InvalidTarget(_) => StatusCode::BAD_REQUEST, } } } @@ -304,10 +357,28 @@ impl From for MyTxHistoryErrorV2 { fn from(err: UtxoRpcError) -> Self { MyTxHistoryErrorV2::RpcError(err.to_string()) } } +impl From for MyTxHistoryErrorV2 { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::InvalidBip44Chain { .. } => MyTxHistoryErrorV2::InvalidTarget(e.to_string()), + AddressDerivingError::Bip32Error(_) => MyTxHistoryErrorV2::Internal(e.to_string()), + AddressDerivingError::Internal(internal) => MyTxHistoryErrorV2::Internal(internal), + } + } +} + +impl From for MyTxHistoryErrorV2 { + fn from(e: InvalidBip44ChainError) -> Self { MyTxHistoryErrorV2::InvalidTarget(e.to_string()) } +} + +#[async_trait] pub trait CoinWithTxHistoryV2 { fn history_wallet_id(&self) -> WalletId; - fn get_tx_history_filters(&self) -> GetTxHistoryFilters; + async fn get_tx_history_filters( + &self, + target: MyTxHistoryTarget, + ) -> MmResult; } /// According to the [comment](https://github.com/KomodoPlatform/atomicDEX-API/pull/1285#discussion_r888410390), @@ -319,6 +390,10 @@ pub async fn my_tx_history_v2_rpc( 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, + MmCoinEnum::UtxoCoin(utxo) => my_tx_history_v2_impl(ctx, &utxo, request).await, + MmCoinEnum::QtumCoin(qtum) => my_tx_history_v2_impl(ctx, &qtum, request).await, + MmCoinEnum::Tendermint(tendermint) => my_tx_history_v2_impl(ctx, &tendermint, request).await, + MmCoinEnum::TendermintToken(tendermint_token) => my_tx_history_v2_impl(ctx, &tendermint_token, request).await, other => MmError::err(MyTxHistoryErrorV2::NotSupportedFor(other.ticker().to_owned())), } } @@ -345,11 +420,15 @@ where .await .map_to_mm(MyTxHistoryErrorV2::RpcError)?; - let filters = coin.get_tx_history_filters(); + let filters = coin.get_tx_history_filters(request.target.clone()).await?; let history = tx_history_storage .get_history(&wallet_id, filters, request.paging_options.clone(), request.limit) .await?; + let coin_conf = coin_conf(&ctx, coin.ticker()); + let protocol_type = coin_conf["protocol"]["type"].as_str().unwrap_or_default(); + let decimals = coin.decimals(); + let transactions = history .transactions .into_iter() @@ -358,6 +437,59 @@ where if details.coin != request.coin { details.coin = request.coin.clone(); } + + // TODO + // !! temporary solution !! + // for tendermint, tx_history_v2 implementation doesn't include amount parsing logic. + // therefore, re-mapping is required + match protocol_type { + TENDERMINT_COIN_PROTOCOL_TYPE | TENDERMINT_ASSET_PROTOCOL_TYPE => { + // TODO + // see this https://github.com/KomodoPlatform/atomicDEX-API/pull/1526#discussion_r1037001780 + if let Some(TxFeeDetails::Utxo(fee)) = &mut details.fee_details { + let mapped_fee = crate::tendermint::TendermintFeeDetails { + // We make sure this is filled in `tendermint_tx_history_v2` + coin: fee.coin.as_ref().expect("can't be empty").to_owned(), + amount: fee.amount.clone(), + gas_limit: crate::tendermint::GAS_LIMIT_DEFAULT, + // ignored anyway + uamount: 0, + }; + details.fee_details = Some(TxFeeDetails::Tendermint(mapped_fee)); + } + + match &details.transaction_type { + // Amount mappings are by-passed when `TransactionType` is `FeeForTokenTx` + TransactionType::FeeForTokenTx => {}, + _ => { + // In order to use error result instead of panicking, we should do an extra iteration above this map. + // Because all the values are inserted by u64 convertion in tx_history_v2 implementation, using `panic` + // shouldn't harm. + + let u_total_amount = details.total_amount.to_u64().unwrap_or_else(|| { + panic!("Parsing '{}' into u64 should not fail", details.total_amount) + }); + details.total_amount = big_decimal_from_sat_unsigned(u_total_amount, decimals); + + let u_spent_by_me = details.spent_by_me.to_u64().unwrap_or_else(|| { + panic!("Parsing '{}' into u64 should not fail", details.spent_by_me) + }); + details.spent_by_me = big_decimal_from_sat_unsigned(u_spent_by_me, decimals); + + let u_received_by_me = details.received_by_me.to_u64().unwrap_or_else(|| { + panic!("Parsing '{}' into u64 should not fail", details.received_by_me) + }); + details.received_by_me = big_decimal_from_sat_unsigned(u_received_by_me, decimals); + + // Because this can be negative values, no need to read and parse + // this since it's always 0 from tx_history_v2 implementation. + details.my_balance_change = &details.received_by_me - &details.spent_by_me; + }, + } + }, + _ => {}, + }; + let confirmations = if details.block_height == 0 || details.block_height > current_block { 0 } else { @@ -369,6 +501,7 @@ where Ok(MyTxHistoryResponseV2 { coin: request.coin, + target: request.target, current_block, transactions, sync_status: coin.history_sync_status(), @@ -390,3 +523,19 @@ pub async fn z_coin_tx_history_rpc( other => MmError::err(MyTxHistoryErrorV2::NotSupportedFor(other.ticker().to_owned())), } } + +#[cfg(test)] +pub(crate) mod for_tests { + use super::{CoinWithTxHistoryV2, TxHistoryStorage}; + use crate::tx_history_storage::TxHistoryStorageBuilder; + use common::block_on; + use mm2_core::mm_ctx::MmArc; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + + pub 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) + } +} diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 4378cf44f4..cddbfaf6d5 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1,3 +1,4 @@ +use crate::coin_errors::{MyAddressError, ValidatePaymentError}; use crate::eth::{self, u256_to_big_decimal, wei_from_big_decimal, TryToAddress}; use crate::qrc20::rpc_clients::{LogEntry, Qrc20ElectrumOps, Qrc20NativeOps, Qrc20RpcOps, TopicFilter, TxReceipt, ViewContractCallType}; @@ -6,23 +7,32 @@ use crate::utxo::rpc_clients::{ElectrumClient, NativeClient, UnspentInfo, UtxoRp 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, +use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, + UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, + UtxoFieldsWithIguanaSecretBuilder}; +use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_utxo_inputs_signed_by_pub, UtxoTxBuilder}; +use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, AddrFromStrError, 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, PrivKeyNotAllowed, RawTransactionFut, RawTransactionRequest, - SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, FeeApproxStage, + FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, MarketCoinOps, MmCoin, + NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RefundError, RefundResult, + SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, + SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, + SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, + SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, - TransactionFut, TransactionType, UnexpectedDerivationMethod, ValidateAddressResult, ValidatePaymentInput, - VerificationResult, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; + TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, + ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, + WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; -use common::executor::Timer; +use common::executor::{AbortableSystem, AbortedError, Timer}; use common::jsonrpc_client::{JsonRpcClient, JsonRpcRequest, RpcRes}; use common::log::{error, warn}; use common::now_ms; @@ -75,7 +85,7 @@ pub type Qrc20AbiResult = Result>; pub enum Qrc20GenTxError { ErrorGeneratingUtxoTx(GenerateTxError), ErrorSigningTx(UtxoSignWithKeyPairError), - PrivKeyNotAllowed(PrivKeyNotAllowed), + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), UnexpectedDerivationMethod(UnexpectedDerivationMethod), } @@ -87,8 +97,8 @@ 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: PrivKeyPolicyNotAllowed) -> Self { Qrc20GenTxError::PrivKeyPolicyNotAllowed(e) } } impl From for Qrc20GenTxError { @@ -106,7 +116,7 @@ impl Qrc20GenTxError { 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::PrivKeyPolicyNotAllowed(priv_err) => WithdrawError::InternalError(priv_err.to_string()), Qrc20GenTxError::UnexpectedDerivationMethod(addr_err) => WithdrawError::InternalError(addr_err.to_string()), } } @@ -151,7 +161,7 @@ struct Qrc20CoinBuilder<'a> { ticker: &'a str, conf: &'a Json, activation_params: &'a Qrc20ActivationParams, - priv_key: &'a [u8], + priv_key_policy: PrivKeyBuildPolicy, platform: String, token_contract_address: H160, } @@ -162,7 +172,7 @@ impl<'a> Qrc20CoinBuilder<'a> { ticker: &'a str, conf: &'a Json, activation_params: &'a Qrc20ActivationParams, - priv_key: &'a [u8], + priv_key_policy: PrivKeyBuildPolicy, platform: String, token_contract_address: H160, ) -> Qrc20CoinBuilder<'a> { @@ -171,7 +181,7 @@ impl<'a> Qrc20CoinBuilder<'a> { ticker, conf, activation_params, - priv_key, + priv_key_policy, platform, token_contract_address, } @@ -253,18 +263,33 @@ impl<'a> UtxoCoinBuilderCommonOps for Qrc20CoinBuilder<'a> { } } -#[async_trait] -impl<'a> UtxoFieldsWithIguanaPrivKeyBuilder for Qrc20CoinBuilder<'a> {} +impl<'a> UtxoFieldsWithIguanaSecretBuilder for Qrc20CoinBuilder<'a> {} + +impl<'a> UtxoFieldsWithGlobalHDBuilder for Qrc20CoinBuilder<'a> {} + +/// Although, `Qrc20Coin` doesn't support [`PrivKeyBuildPolicy::Trezor`] yet, +/// `UtxoCoinBuilder` trait requires `UtxoFieldsWithHardwareWalletBuilder` to be implemented. +impl<'a> UtxoFieldsWithHardwareWalletBuilder for Qrc20CoinBuilder<'a> {} #[async_trait] -impl<'a> UtxoCoinWithIguanaPrivKeyBuilder for Qrc20CoinBuilder<'a> { +impl<'a> UtxoCoinBuilder for Qrc20CoinBuilder<'a> { type ResultCoin = Qrc20Coin; type Error = UtxoCoinBuildError; - fn priv_key(&self) -> &[u8] { self.priv_key } + fn priv_key_policy(&self) -> PrivKeyBuildPolicy { self.priv_key_policy.clone() } async fn build(self) -> MmResult { - let utxo = self.build_utxo_fields_with_iguana_priv_key(self.priv_key()).await?; + let utxo = match self.priv_key_policy() { + PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => self.build_utxo_fields_with_iguana_secret(priv_key).await?, + PrivKeyBuildPolicy::GlobalHDAccount(global_hd_ctx) => { + self.build_utxo_fields_with_global_hd(global_hd_ctx).await? + }, + PrivKeyBuildPolicy::Trezor => { + let priv_key_err = PrivKeyPolicyNotAllowed::HardwareWalletNotSupported; + return MmError::err(UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err)); + }, + }; + let inner = Qrc20CoinFields { utxo, platform: self.platform, @@ -276,13 +301,13 @@ impl<'a> UtxoCoinWithIguanaPrivKeyBuilder for Qrc20CoinBuilder<'a> { } } -pub async fn qrc20_coin_from_conf_and_params( +pub async fn qrc20_coin_with_policy( ctx: &MmArc, ticker: &str, platform: &str, conf: &Json, params: &Qrc20ActivationParams, - priv_key: &[u8], + priv_key_policy: PrivKeyBuildPolicy, contract_address: H160, ) -> Result { let builder = Qrc20CoinBuilder::new( @@ -290,14 +315,26 @@ pub async fn qrc20_coin_from_conf_and_params( ticker, conf, params, - priv_key, + priv_key_policy, platform.to_owned(), contract_address, ); Ok(try_s!(builder.build().await)) } -#[derive(Debug)] +pub async fn qrc20_coin_with_priv_key( + ctx: &MmArc, + ticker: &str, + platform: &str, + conf: &Json, + params: &Qrc20ActivationParams, + priv_key: IguanaPrivKey, + contract_address: H160, +) -> Result { + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(priv_key); + qrc20_coin_with_policy(ctx, ticker, platform, conf, params, priv_key_policy, contract_address).await +} + pub struct Qrc20CoinFields { pub utxo: UtxoCoinFields, pub platform: String, @@ -306,12 +343,12 @@ pub struct Qrc20CoinFields { pub fallback_swap_contract: Option, } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct Qrc20Coin(Arc); impl Deref for Qrc20Coin { type Target = Qrc20CoinFields; - fn deref(&self) -> &Qrc20CoinFields { &*self.0 } + fn deref(&self) -> &Qrc20CoinFields { &self.0 } } impl AsRef for Qrc20Coin { @@ -418,6 +455,10 @@ impl From for Qrc20AbiError { fn from(e: ethabi::Error) -> Qrc20AbiError { Qrc20AbiError::AbiError(e.to_string()) } } +impl From for ValidatePaymentError { + fn from(e: Qrc20AbiError) -> ValidatePaymentError { ValidatePaymentError::TxDeserializationError(e.to_string()) } +} + impl From for GenerateTxError { fn from(e: Qrc20AbiError) -> Self { GenerateTxError::Internal(e.to_string()) } } @@ -476,7 +517,7 @@ impl Qrc20Coin { &self, contract_outputs: Vec, ) -> Result> { - let my_address = self.utxo.derivation_method.iguana_or_err()?; + let my_address = self.utxo.derivation_method.single_addr_or_err()?; let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; let mut gas_fee = 0; @@ -493,7 +534,7 @@ impl Qrc20Coin { .build() .await?; - let my_address = self.utxo.derivation_method.iguana_or_err()?; + let my_address = self.utxo.derivation_method.single_addr_or_err()?; let key_pair = self.utxo.priv_key_policy.key_pair_or_err()?; let prev_script = ScriptBuilder::build_p2pkh(&my_address.hash); @@ -524,7 +565,8 @@ impl Qrc20Coin { let params = function.encode_input(&[Token::Address(to_addr), Token::Uint(amount)])?; let script_pubkey = - generate_contract_call_script_pubkey(¶ms, gas_limit, gas_price, &self.contract_address)?.to_bytes(); + generate_contract_call_script_pubkey(¶ms, gas_limit, gas_price, self.contract_address.as_bytes())? + .to_bytes(); Ok(ContractCallOutput { value: OUTPUT_QTUM_AMOUNT, @@ -608,8 +650,8 @@ impl GetUtxoListOps for Qrc20Coin { #[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 + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { + utxo_common::get_htlc_spend_fee(self, tx_size, stage).await } fn addresses_from_script(&self, script: &Script) -> Result, String> { @@ -622,7 +664,7 @@ impl UtxoCommonOps for Qrc20Coin { utxo_common::my_public_key(self.as_ref()) } - fn address_from_str(&self, address: &str) -> Result { + fn address_from_str(&self, address: &str) -> MmResult { utxo_common::checked_address_from_str(self, address) } @@ -723,20 +765,13 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - 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 { - 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_tx_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); - let secret_hash = Vec::from(secret_hash); - let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); + fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { + let time_lock = maker_payment_args.time_lock; + let taker_addr = try_tx_fus!(self.contract_address_from_raw_pubkey(maker_payment_args.other_pubkey)); + let id = qrc20_swap_id(time_lock, maker_payment_args.secret_hash); + let value = try_tx_fus!(wei_from_big_decimal(&maker_payment_args.amount, self.utxo.decimals)); + let secret_hash = Vec::from(maker_payment_args.secret_hash); + let swap_contract_address = try_tx_fus!(maker_payment_args.swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -747,20 +782,14 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - 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 { - 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_tx_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); - let secret_hash = Vec::from(secret_hash); - let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); + #[inline] + fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { + let time_lock = taker_payment_args.time_lock; + let maker_addr = try_tx_fus!(self.contract_address_from_raw_pubkey(taker_payment_args.other_pubkey)); + let id = qrc20_swap_id(time_lock, taker_payment_args.secret_hash); + let value = try_tx_fus!(wei_from_big_decimal(&taker_payment_args.amount, self.utxo.decimals)); + let secret_hash = Vec::from(taker_payment_args.secret_hash); + let swap_contract_address = try_tx_fus!(taker_payment_args.swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -771,18 +800,15 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } + #[inline] 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], + maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, ) -> TransactionFut { - 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 payment_tx: UtxoTx = + try_tx_fus!(deserialize(maker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_fus!(maker_spends_payment_args.swap_contract_address.try_to_address()); + let secret = maker_spends_payment_args.secret.to_vec(); let selfi = self.clone(); let fut = async move { @@ -793,18 +819,15 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } + #[inline] 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], + taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, ) -> TransactionFut { - 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_tx_fus!(swap_contract_address.try_to_address()); + let payment_tx: UtxoTx = + try_tx_fus!(deserialize(taker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let secret = taker_spends_payment_args.secret.to_vec(); + let swap_contract_address = try_tx_fus!(taker_spends_payment_args.swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -815,17 +838,11 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - 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 { - 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()); + #[inline] + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + let payment_tx: UtxoTx = + try_tx_fus!(deserialize(taker_refunds_payment_args.payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_fus!(taker_refunds_payment_args.swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -836,17 +853,11 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - 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 { - 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()); + #[inline] + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { + let payment_tx: UtxoTx = + try_tx_fus!(deserialize(maker_refunds_payment_args.payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_fus!(maker_refunds_payment_args.swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -857,25 +868,26 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } + #[inline] fn validate_fee( &self, - fee_tx: &TransactionEnum, - expected_sender: &[u8], - fee_addr: &[u8], - amount: &BigDecimal, - min_block_number: u64, - _uuid: &[u8], + validate_fee_args: ValidateFeeArgs<'_>, ) -> Box + Send> { + let fee_tx = validate_fee_args.fee_tx; + let min_block_number = validate_fee_args.min_block_number; let fee_tx = match fee_tx { TransactionEnum::UtxoTx(tx) => tx, _ => panic!("Unexpected TransactionEnum"), }; let fee_tx_hash = fee_tx.hash().reversed().into(); - if !try_fus!(check_all_inputs_signed_by_pub(fee_tx, expected_sender)) { + if !try_fus!(check_all_utxo_inputs_signed_by_pub( + fee_tx, + validate_fee_args.expected_sender + )) { return Box::new(futures01::future::err(ERRL!("The dex fee was sent from wrong address"))); } - let fee_addr = try_fus!(self.contract_address_from_raw_pubkey(fee_addr)); - let expected_value = try_fus!(wei_from_big_decimal(amount, self.utxo.decimals)); + let fee_addr = try_fus!(self.contract_address_from_raw_pubkey(validate_fee_args.fee_addr)); + let expected_value = try_fus!(wei_from_big_decimal(validate_fee_args.amount, self.utxo.decimals)); let selfi = self.clone(); let fut = async move { @@ -886,10 +898,16 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - 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()); + #[inline] + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + let payment_tx: UtxoTx = try_f!(deserialize(input.payment_tx.as_slice())); + let sender = try_f!(self + .contract_address_from_raw_pubkey(&input.other_pub) + .map_to_mm(ValidatePaymentError::InvalidParameter)); + let swap_contract_address = try_f!(input + .swap_contract_address + .try_to_address() + .map_to_mm(ValidatePaymentError::InvalidParameter)); let selfi = self.clone(); let fut = async move { @@ -907,10 +925,16 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - 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)); + #[inline] + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + let swap_contract_address = try_f!(input + .swap_contract_address + .try_to_address() + .map_to_mm(ValidatePaymentError::InvalidParameter)); + let payment_tx: UtxoTx = try_f!(deserialize(input.payment_tx.as_slice())); + let sender = try_f!(self + .contract_address_from_raw_pubkey(&input.other_pub) + .map_to_mm(ValidatePaymentError::InvalidParameter)); let selfi = self.clone(); let fut = async move { @@ -928,17 +952,14 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } + #[inline] 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], + if_my_payment_sent_args: CheckIfMyPaymentSentArgs<'_>, ) -> 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()); + let search_from_block = if_my_payment_sent_args.search_from_block; + let swap_id = qrc20_swap_id(if_my_payment_sent_args.time_lock, if_my_payment_sent_args.secret_hash); + let swap_contract_address = try_fus!(if_my_payment_sent_args.swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -949,6 +970,7 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } + #[inline] async fn search_for_swap_tx_spend_my( &self, input: SearchForSwapTxSpendInput<'_>, @@ -969,10 +991,24 @@ impl SwapOps for Qrc20Coin { .await } - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + #[inline] + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { + utxo_common::check_all_inputs_signed_by_pub(tx, expected_pub) + } + + #[inline] + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { self.extract_secret_impl(secret_hash, spend_tx) } + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } + fn negotiate_swap_contract_addr( &self, other_side_address: Option<&[u8]>, @@ -982,19 +1018,19 @@ impl SwapOps for Qrc20Coin { if bytes.len() != 20 { return MmError::err(NegotiateSwapContractAddrErr::InvalidOtherAddrLen(bytes.into())); } - let other_addr = H160::from(bytes); + let other_addr = H160::from_slice(bytes); if other_addr == self.swap_contract_address { - return Ok(Some(self.swap_contract_address.to_vec().into())); + return Ok(Some(self.swap_contract_address.0.to_vec().into())); } if Some(other_addr) == self.fallback_swap_contract { - return Ok(self.fallback_swap_contract.map(|addr| addr.to_vec().into())); + return Ok(self.fallback_swap_contract.map(|addr| addr.0.to_vec().into())); } MmError::err(NegotiateSwapContractAddrErr::UnexpectedOtherAddr(bytes.into())) }, None => self .fallback_swap_contract - .map(|addr| Some(addr.to_vec().into())) + .map(|addr| Some(addr.0.to_vec().into())) .ok_or_else(|| MmError::new(NegotiateSwapContractAddrErr::NoOtherAddrAndNoFallback)), } } @@ -1002,12 +1038,125 @@ impl SwapOps for Qrc20Coin { fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) } + + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + utxo_common::derive_htlc_pubkey(self, swap_unique_data) + } + + #[inline] + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + utxo_common::validate_other_pubkey(raw_pubkey) + } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } +} + +#[async_trait] +impl TakerSwapMakerCoin for Qrc20Coin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for Qrc20Coin { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for Qrc20Coin { + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &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_taker_payment_refund_preimage( + &self, + _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + async fn watcher_search_for_swap_tx_spend( + &self, + _input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } } impl MarketCoinOps for Qrc20Coin { fn ticker(&self) -> &str { &self.utxo.conf.ticker } - fn my_address(&self) -> Result { utxo_common::my_address(self) } + fn my_address(&self) -> MmResult { utxo_common::my_address(self) } fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.as_ref())?; @@ -1092,26 +1241,28 @@ impl MarketCoinOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, transaction: &[u8], + _secret_hash: &[u8], wait_until: u64, from_block: u64, _swap_contract_address: &Option, + check_every: f64, ) -> TransactionFut { 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) + .wait_for_tx_spend_impl(tx, wait_until, from_block, check_every) .map_err(TransactionErr::Plain) .await }; Box::new(fut.boxed().compat()) } - fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result { + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { utxo_common::tx_enum_from_bytes(self.as_ref(), bytes) } @@ -1133,6 +1284,8 @@ impl MarketCoinOps for Qrc20Coin { impl MmCoin for Qrc20Coin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo) } + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { Box::new(qrc20_withdraw(self.clone(), req).boxed().compat()) } @@ -1141,6 +1294,10 @@ impl MmCoin for Qrc20Coin { Box::new(utxo_common::get_raw_transaction(&self.utxo, req).boxed().compat()) } + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { + Box::new(utxo_common::get_tx_hex_by_hash(&self.utxo, tx_hash).boxed().compat()) + } + fn decimals(&self) -> u8 { utxo_common::decimals(&self.utxo) } fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { @@ -1222,7 +1379,7 @@ impl MmCoin for Qrc20Coin { }) } - fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, stage: FeeApproxStage) -> TradePreimageFut { let selfi = self.clone(); let fut = async move { // pass the dummy params @@ -1287,6 +1444,10 @@ impl MmCoin for Qrc20Coin { Some(BytesJson::from(self.swap_contract_address.0.as_ref())) } + fn fallback_swap_contract(&self) -> Option { + self.fallback_swap_contract.map(|a| BytesJson::from(a.0.as_ref())) + } + fn mature_confirmations(&self) -> Option { Some(self.utxo.conf.mature_confirmations) } fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } @@ -1294,6 +1455,10 @@ impl MmCoin for Qrc20Coin { fn is_coin_protocol_supported(&self, info: &Option>) -> bool { utxo_common::is_coin_protocol_supported(self, info) } + + fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.as_ref().abortable_system) } + + fn on_token_deactivated(&self, _ticker: &str) {} } pub fn qrc20_swap_id(time_lock: u32, secret_hash: &[u8]) -> Vec { @@ -1376,7 +1541,7 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult .await .mm_err(|gen_tx_error| gen_tx_error.into_withdraw_error(coin.platform.clone(), coin.utxo.decimals))?; - let my_address = coin.utxo.derivation_method.iguana_or_err()?; + let my_address = coin.utxo.derivation_method.single_addr_or_err()?; let received_by_me = if to_addr == *my_address { qrc20_amount.clone() } else { @@ -1385,7 +1550,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_string = coin.my_address().map_to_mm(WithdrawError::InternalError)?; + let my_address_string = coin.my_address()?; let to_address = to_addr.display_address().map_to_mm(WithdrawError::InternalError)?; let fee_details = Qrc20FeeDetails { @@ -1412,6 +1577,7 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult timestamp: now_ms() / 1000, kmd_rewards: None, transaction_type: TransactionType::StandardTransfer, + memo: None, }) } diff --git a/mm2src/coins/qrc20/history.rs b/mm2src/coins/qrc20/history.rs index 967ba82ba4..0e61ce0a79 100644 --- a/mm2src/coins/qrc20/history.rs +++ b/mm2src/coins/qrc20/history.rs @@ -3,8 +3,8 @@ use crate::utxo::{RequestTxHistoryResult, UtxoFeeDetails}; use crate::{CoinsContext, TxFeeDetails, TxHistoryResult}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use common::jsonrpc_client::JsonRpcErrorType; -use common::mm_metrics::MetricsArc; use itertools::Itertools; +use mm2_metrics::MetricsArc; use script_pubkey::{extract_contract_call_from_script, extract_gas_from_script, ExtractGasEnum}; use std::collections::HashMap; use std::io::Cursor; @@ -224,7 +224,7 @@ impl Qrc20Coin { receipt: TxReceipt, miner_fee: BigDecimal, ) -> Result { - let my_address = try_s!(self.utxo.derivation_method.iguana_or_err()); + let my_address = try_s!(self.utxo.derivation_method.single_addr_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!( @@ -352,7 +352,8 @@ impl Qrc20Coin { }, JsonRpcErrorType::InvalidRequest(err) | JsonRpcErrorType::Transport(err) - | JsonRpcErrorType::Parse(_, err) => { + | JsonRpcErrorType::Parse(_, err) + | JsonRpcErrorType::Internal(err) => { return RequestTxHistoryResult::Retry { error: ERRL!("Error {} on blockchain_contract_event_get_history", err), }; @@ -620,7 +621,7 @@ impl TransferHistoryBuilder { .coin .utxo .derivation_method - .iguana_or_err() + .single_addr_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()))? @@ -813,7 +814,7 @@ fn is_transfer_event_log(log: &LogEntry) -> bool { mod tests { use super::*; use common::block_on; - use common::mm_metrics::{MetricType, MetricsJson, MetricsOps}; + use mm2_metrics::{MetricType, MetricsJson, MetricsOps}; use mm2_test_helpers::for_tests::find_metrics_in_json; use qrc20_tests::qrc20_coin_for_test; @@ -839,8 +840,8 @@ mod tests { 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); - ctx.metrics.init().unwrap(); + let (ctx, coin) = qrc20_coin_for_test(priv_key, None); + ctx.metrics.init(); let tx_hash: H256Json = hex::decode("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") .unwrap() @@ -871,8 +872,8 @@ mod tests { 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); - ctx.metrics.init().unwrap(); + let (ctx, coin) = qrc20_coin_for_test(priv_key, None); + ctx.metrics.init(); let tx_hash: H256Json = hex::decode("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") .unwrap() @@ -913,8 +914,8 @@ mod tests { 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); - ctx.metrics.init().unwrap(); + let (ctx, coin) = qrc20_coin_for_test(priv_key, None); + ctx.metrics.init(); let tx_hash: H256Json = hex::decode("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") .unwrap() @@ -955,7 +956,7 @@ mod tests { 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 (ctx, coin) = qrc20_coin_for_test(priv_key, None); let tx_hash: H256Json = hex::decode("35e03bc529528a853ee75dde28f27eec8ed7b152b6af7ab6dfa5d55ea46f25ac") .unwrap() @@ -980,7 +981,7 @@ mod tests { 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 (ctx, coin) = qrc20_coin_for_test(priv_key, None); let tx_hash: H256Json = hex::decode("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") .unwrap() @@ -1005,10 +1006,10 @@ mod tests { 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 (ctx, coin) = qrc20_coin_for_test(priv_key, None); let metrics = MetricsArc::new(); - metrics.init().unwrap(); + metrics.init(); let tx_hash_invalid: H256Json = hex::decode("0000000000000000000000000000000000000000000000000000000000000000") .unwrap() diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 8fa226ee15..12f606cb26 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -3,6 +3,7 @@ use crate::utxo::rpc_clients::UnspentInfo; use crate::TxFeeDetails; use chain::OutPoint; use common::{block_on, DEX_FEE_ADDR_RAW_PUBKEY}; +use crypto::Secp256k1Secret; use itertools::Itertools; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_number::bigdecimal::Zero; @@ -14,8 +15,9 @@ 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; const SWAP_PAYMENT_GAS_FEE: i64 = (QRC20_PAYMENT_GAS_LIMIT * QRC20_GAS_PRICE_DEFAULT) as i64; +const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; -pub fn qrc20_coin_for_test(priv_key: &[u8], fallback_swap: Option<&str>) -> (MmArc, Qrc20Coin) { +pub fn qrc20_coin_for_test(priv_key: [u8; 32], fallback_swap: Option<&str>) -> (MmArc, Qrc20Coin) { let conf = json!({ "coin":"QRC20", "decimals": 8, @@ -33,17 +35,17 @@ pub fn qrc20_coin_for_test(priv_key: &[u8], fallback_swap: Option<&str>) -> (MmA "swap_contract_address": "0xba8b71f3544b93e2f681f996da519a98ace0107a", "fallback_swap_contract": fallback_swap, }); - let contract_address = "0xd362e096e873eb7907e205fadc6175c6fec7bc44".into(); + let contract_address = H160::from_str("0xd362e096e873eb7907e205fadc6175c6fec7bc44").unwrap(); 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( + let coin = block_on(qrc20_coin_with_priv_key( &ctx, "QRC20", "QTUM", &conf, ¶ms, - priv_key, + Secp256k1Secret::from(priv_key), contract_address, )) .unwrap(); @@ -61,13 +63,13 @@ fn test_withdraw_to_p2sh_address_should_fail() { 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 (_, 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(), + hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_iguana().checksum_type, + checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, hrp: coin.as_ref().conf.bech32_hrp.clone(), addr_format: UtxoAddressFormat::Standard, }; @@ -106,7 +108,7 @@ fn test_withdraw_impl_fee_details() { 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 (_ctx, coin) = qrc20_coin_for_test(priv_key, None); let withdraw_req = WithdrawRequest { amount: 10.into(), @@ -144,10 +146,10 @@ fn test_validate_maker_payment() { 24, 181, 194, 193, 18, 152, 142, 168, 71, 73, 70, 244, 9, 101, 92, 168, 243, 61, 132, 48, 25, 39, 103, 92, 29, 17, 11, 29, 113, 235, 48, 70, ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key, None); + let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); assert_eq!( - *coin.utxo.derivation_method.unwrap_iguana(), + *coin.utxo.derivation_method.unwrap_single_addr(), "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf".into() ); @@ -159,6 +161,7 @@ fn test_validate_maker_payment() { let mut input = ValidatePaymentInput { payment_tx, + time_lock_duration: 0, time_lock: 1601367157, other_pub: correct_maker_pub.clone(), secret_hash: vec![1; 20], @@ -172,29 +175,66 @@ fn test_validate_maker_payment() { coin.validate_maker_payment(input.clone()).wait().unwrap(); input.other_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); - let error = coin.validate_maker_payment(input.clone()).wait().unwrap_err(); + let error = coin + .validate_maker_payment(input.clone()) + .wait() + .unwrap_err() + .into_inner(); log!("error: {:?}", error); - assert!( - error.contains("Payment tx was sent from wrong address, expected 0x783cf0be521101942da509846ea476e683aad832") - ); + match error { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Payment tx 0x9e032d4b0090a11dc40fe6c47601499a35d55fbb was sent from wrong address, expected 0x783cf0be521101942da509846ea476e683aad832")), + _ => panic!("Expected `WrongPaymentTx` wrong address, found {:?}", error), + } input.other_pub = correct_maker_pub; input.amount = BigDecimal::from_str("0.3").unwrap(); - let error = coin.validate_maker_payment(input.clone()).wait().unwrap_err(); + let error = coin + .validate_maker_payment(input.clone()) + .wait() + .unwrap_err() + .into_inner(); log!("error: {:?}", error); - assert!(error.contains("Unexpected 'erc20Payment' contract call bytes")); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains("Unexpected 'erc20Payment' contract call bytes")) + }, + _ => panic!( + "Expected `WrongPaymentTx` unexpected contract call bytes, found {:?}", + error + ), + } input.amount = correct_amount; input.secret_hash = vec![2; 20]; - let error = coin.validate_maker_payment(input.clone()).wait().unwrap_err(); + let error = coin + .validate_maker_payment(input.clone()) + .wait() + .unwrap_err() + .into_inner(); log!("error: {:?}", error); - assert!(error.contains("Payment state is not PAYMENT_STATE_SENT, got 0")); + match error { + ValidatePaymentError::UnexpectedPaymentState(err) => { + assert!(err.contains("Payment state is not PAYMENT_STATE_SENT, got 0")) + }, + _ => panic!( + "Expected `UnexpectedPaymentState` state not PAYMENT_STATE_SENT, found {:?}", + error + ), + } input.secret_hash = vec![1; 20]; input.time_lock = 123; - let error = coin.validate_maker_payment(input).wait().unwrap_err(); + let error = coin.validate_maker_payment(input).wait().unwrap_err().into_inner(); log!("error: {:?}", error); - assert!(error.contains("Payment state is not PAYMENT_STATE_SENT, got 0")); + match error { + ValidatePaymentError::UnexpectedPaymentState(err) => { + assert!(err.contains("Payment state is not PAYMENT_STATE_SENT, got 0")) + }, + _ => panic!( + "Expected `UnexpectedPaymentState` state not PAYMENT_STATE_SENT, found {:?}", + error + ), + } } #[test] @@ -204,10 +244,10 @@ fn test_wait_for_confirmations_excepted() { 24, 181, 194, 193, 18, 152, 142, 168, 71, 73, 70, 244, 9, 101, 92, 168, 243, 61, 132, 48, 25, 39, 103, 92, 29, 17, 11, 29, 113, 235, 48, 70, ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key, None); + let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); assert_eq!( - *coin.utxo.derivation_method.unwrap_iguana(), + *coin.utxo.derivation_method.unwrap_single_addr(), "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf".into() ); @@ -251,7 +291,7 @@ fn test_send_taker_fee() { 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 (_ctx, coin) = qrc20_coin_for_test(priv_key, None); let amount = BigDecimal::from_str("0.01").unwrap(); let tx = coin @@ -265,14 +305,14 @@ fn test_send_taker_fee() { log!("Fee tx {:?}", tx_hash); let result = coin - .validate_fee( - &tx, - coin.my_public_key().unwrap(), - &DEX_FEE_ADDR_RAW_PUBKEY, - &amount, - 0, - &[], - ) + .validate_fee(ValidateFeeArgs { + fee_tx: &tx, + expected_sender: coin.my_public_key().unwrap(), + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 0, + uuid: &[], + }) .wait(); assert_eq!(result, Ok(())); } @@ -284,7 +324,7 @@ fn test_validate_fee() { 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 (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // QRC20 transfer tx "f97d3a43dbea0993f1b7a6a299377d4ee164c84935a1eb7d835f70c9429e6a1d" let tx = TransactionEnum::UtxoTx("010000000160fd74b5714172f285db2b36f0b391cd6883e7291441631c8b18f165b0a4635d020000006a47304402205d409e141111adbc4f185ae856997730de935ac30a0d2b1ccb5a6c4903db8171022024fc59bbcfdbba283556d7eeee4832167301dc8e8ad9739b7865f67b9676b226012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000625403a08601012844a9059cbb000000000000000000000000ca1e04745e8ca0c60d8c5881531d51bec470743f00000000000000000000000000000000000000000000000000000000000f424014d362e096e873eb7907e205fadc6175c6fec7bc44c200ada205000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acfe967d5f".into()); @@ -293,13 +333,27 @@ 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(ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &sender_pub, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 0, + uuid: &[], + }) .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(ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &sender_pub, + fee_addr: &fee_addr_dif, + amount: &amount, + min_block_number: 0, + uuid: &[], + }) .wait() .err() .expect("Expected an error"); @@ -307,7 +361,14 @@ fn test_validate_fee() { 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(ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 0, + uuid: &[], + }) .wait() .err() .expect("Expected an error"); @@ -315,7 +376,14 @@ fn test_validate_fee() { 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(ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &sender_pub, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 2000000, + uuid: &[], + }) .wait() .err() .expect("Expected an error"); @@ -324,7 +392,14 @@ fn test_validate_fee() { 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(ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &sender_pub, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount_dif, + min_block_number: 0, + uuid: &[], + }) .wait() .err() .expect("Expected an error"); @@ -335,7 +410,14 @@ fn test_validate_fee() { 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(ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &sender_pub, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 0, + uuid: &[], + }) .wait() .err() .expect("Expected an error"); @@ -350,7 +432,7 @@ fn test_wait_for_tx_spend_malicious() { 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 (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // f94d79f89e9ec785db40bb8bb8dca9bc01b7761429618d4c843bbebbc31836b7 // the transaction has two outputs: @@ -363,7 +445,14 @@ fn test_wait_for_tx_spend_malicious() { let wait_until = (now_ms() / 1000) + 1; let from_block = 696245; let found = coin - .wait_for_tx_spend(&payment_tx, wait_until, from_block, &coin.swap_contract_address()) + .wait_for_htlc_tx_spend( + &payment_tx, + &[], + wait_until, + from_block, + &coin.swap_contract_address(), + TAKER_PAYMENT_SPEND_SEARCH_INTERVAL, + ) .wait() .unwrap(); @@ -382,14 +471,14 @@ fn test_extract_secret() { 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 (_ctx, coin) = qrc20_coin_for_test(priv_key, None); let expected_secret = &[1; 32]; let secret_hash = &*dhash160(expected_secret); // taker spent maker payment - d3f5dab4d54c14b3d7ed8c7f5c8cc7f47ccf45ce589fdc7cd5140a3c1c3df6e1 let tx_hex = hex::decode("01000000033f56ecafafc8602fde083ba868d1192d6649b8433e42e1a2d79ba007ea4f7abb010000006b48304502210093404e90e40d22730013035d31c404c875646dcf2fad9aa298348558b6d65ba60220297d045eac5617c1a3eddb71d4bca9772841afa3c4c9d6c68d8d2d42ee6de3950121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff9cac7fe90d597922a1d92e05306c2215628e7ea6d5b855bfb4289c2944f4c73a030000006b483045022100b987da58c2c0c40ce5b6ef2a59e8124ed4ef7a8b3e60c7fb631139280019bc93022069649bcde6fe4dd5df9462a1fcae40598488d6af8c324cd083f5c08afd9568be0121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff70b9870f2b0c65d220a839acecebf80f5b44c3ca4c982fa2fdc5552c037f5610010000006a473044022071b34dd3ebb72d29ca24f3fa0fc96571c815668d3b185dd45cc46a7222b6843f02206c39c030e618d411d4124f7b3e7ca1dd5436775bd8083a85712d123d933a51300121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff020000000000000000c35403a0860101284ca402ed292b806a1835a1b514ad643f2acdb5c8db6b6a9714accff3275ea0d79a3f23be8fd00000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101010101010101010101010101000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac2c02288d4010000001976a914783cf0be521101942da509846ea476e683aad83288ac0f047f5f").unwrap(); - let secret = coin.extract_secret(secret_hash, &tx_hex).unwrap(); + let secret = block_on(coin.extract_secret(secret_hash, &tx_hex)).unwrap(); assert_eq!(secret, expected_secret); } @@ -401,7 +490,7 @@ fn test_extract_secret_malicious() { 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 (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // f94d79f89e9ec785db40bb8bb8dca9bc01b7761429618d4c843bbebbc31836b7 // the transaction has two outputs: @@ -410,7 +499,7 @@ fn test_extract_secret_malicious() { let spend_tx = hex::decode("01000000022bc8299981ec0cea664cdf9df4f8306396a02e2067d6ac2d3770b34646d2bc2a010000006b483045022100eb13ef2d99ac1cd9984045c2365654b115dd8a7815b7fbf8e2a257f0b93d1592022060d648e73118c843e97f75fafc94e5ff6da70ec8ba36ae255f8c96e2626af6260121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffffd92a0a10ac6d144b36033916f67ae79889f40f35096629a5cd87be1a08f40ee7010000006b48304502210080cdad5c4770dfbeb760e215494c63cc30da843b8505e75e7bf9e8dad18568000220234c0b11c41bfbcdd50046c69059976aedabe17657fe43d809af71e9635678e20121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff030000000000000000c35403a0860101284ca402ed292b8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d000202020202020202020202020202020202020202020202020202020202020202000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac20000000000000000c35403a0860101284ca402ed292b8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101010101010101010101010101000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac2b8ea82d3010000001976a914783cf0be521101942da509846ea476e683aad83288ac735d855f").unwrap(); let expected_secret = &[1; 32]; let secret_hash = &*dhash160(expected_secret); - let actual = coin.extract_secret(secret_hash, &spend_tx); + let actual = block_on(coin.extract_secret(secret_hash, &spend_tx)); assert_eq!(actual, Ok(expected_secret.to_vec())); } @@ -421,7 +510,7 @@ fn test_generate_token_transfer_script_pubkey() { 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 (_ctx, coin) = qrc20_coin_for_test(priv_key, None); let gas_limit = 2_500_000; let gas_price = 40; @@ -470,7 +559,7 @@ fn test_transfer_details_by_hash() { 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 (_ctx, coin) = qrc20_coin_for_test(priv_key, None); let tx_hash_bytes = hex::decode("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb").unwrap(); let tx_hash: H256Json = tx_hash_bytes.as_slice().into(); let tx_hex:BytesJson = hex::decode("0100000001426d27fde82e12e1ce84e73ca41e2a30420f4c94aaa37b30d4c5b8b4f762c042040000006a473044022032665891693ee732571cefaa6d322ec5114c78259f2adbe03a0d7e6b65fbf40d022035c9319ca41e5423e09a8a613ac749a20b8f5ad6ba4ad6bb60e4a020b085d009012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff050000000000000000625403a08601012844095ea7b30000000000000000000000001549128bbfb33b997949b4105b6a6371c998e212000000000000000000000000000000000000000000000000000000000000000014d362e096e873eb7907e205fadc6175c6fec7bc44c20000000000000000625403a08601012844095ea7b30000000000000000000000001549128bbfb33b997949b4105b6a6371c998e21200000000000000000000000000000000000000000000000000000000000927c014d362e096e873eb7907e205fadc6175c6fec7bc44c20000000000000000835403a0860101284c640c565ae300000000000000000000000000000000000000000000000000000000000493e0000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000000000000000000000000000000000000000000000141549128bbfb33b997949b4105b6a6371c998e212c20000000000000000835403a0860101284c640c565ae300000000000000000000000000000000000000000000000000000000000493e0000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000000000000000000000000000000000000000000001141549128bbfb33b997949b4105b6a6371c998e212c231754b04000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acf7cd8b5f").unwrap().into(); @@ -511,6 +600,7 @@ fn test_transfer_details_by_hash() { .into(), kmd_rewards: None, transaction_type: Default::default(), + memo: None, }; assert_eq!(actual, expected); @@ -535,6 +625,7 @@ fn test_transfer_details_by_hash() { .into(), kmd_rewards: None, transaction_type: Default::default(), + memo: None, }; assert_eq!(actual, expected); @@ -559,6 +650,7 @@ fn test_transfer_details_by_hash() { .into(), kmd_rewards: None, transaction_type: Default::default(), + memo: None, }; assert_eq!(actual, expected); @@ -583,6 +675,7 @@ fn test_transfer_details_by_hash() { .into(), kmd_rewards: None, transaction_type: Default::default(), + memo: None, }; assert_eq!(actual, expected); @@ -607,6 +700,7 @@ fn test_transfer_details_by_hash() { .into(), kmd_rewards: None, transaction_type: Default::default(), + memo: None, }; assert_eq!(actual, expected); assert!(it.next().is_none()); @@ -619,7 +713,7 @@ fn test_get_trade_fee() { 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 (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); @@ -646,7 +740,7 @@ fn test_sender_trade_preimage_zero_allowance() { 222, 243, 64, 156, 9, 153, 78, 253, 85, 119, 62, 117, 230, 140, 75, 69, 171, 21, 243, 19, 119, 29, 97, 174, 63, 231, 153, 202, 20, 238, 120, 64, ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key, None); + let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); @@ -682,7 +776,7 @@ fn test_sender_trade_preimage_with_allowance() { 32, 192, 195, 65, 165, 53, 21, 68, 180, 241, 67, 147, 54, 54, 41, 117, 174, 253, 139, 155, 56, 101, 69, 39, 32, 143, 221, 19, 47, 74, 175, 100, ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key, None); + let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); @@ -753,17 +847,17 @@ fn test_get_sender_trade_fee_preimage_for_correct_ticker() { "swap_contract_address": "0xba8b71f3544b93e2f681f996da519a98ace0107a", }); - let contract_address = "0xd362e096e873eb7907e205fadc6175c6fec7bc44".into(); + let contract_address = H160::from_str("0xd362e096e873eb7907e205fadc6175c6fec7bc44").unwrap(); 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( + let coin = block_on(qrc20_coin_with_priv_key( &ctx, "QRC20", "tQTUM", &conf, ¶ms, - &priv_key, + Secp256k1Secret::from(priv_key), contract_address, )) .unwrap(); @@ -790,12 +884,12 @@ fn test_receiver_trade_preimage() { 32, 192, 195, 65, 165, 53, 21, 68, 180, 241, 67, 147, 54, 54, 41, 117, 174, 253, 139, 155, 56, 101, 69, 39, 32, 143, 221, 19, 47, 74, 175, 100, ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key, None); + let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); let actual = coin - .get_receiver_trade_fee(FeeApproxStage::WithoutApprox) + .get_receiver_trade_fee(Default::default(), FeeApproxStage::WithoutApprox) .wait() .expect("!get_receiver_trade_fee"); // only one contract call should be included into the expected trade fee @@ -817,7 +911,7 @@ fn test_taker_fee_tx_fee() { 32, 192, 195, 65, 165, 53, 21, 68, 180, 241, 67, 147, 54, 54, 41, 117, 174, 253, 139, 155, 56, 101, 69, 39, 32, 143, 221, 19, 47, 74, 175, 100, ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key, None); + let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); let expected_balance = CoinBalance { @@ -861,17 +955,17 @@ fn test_coin_from_conf_without_decimals() { "swap_contract_address": "0xba8b71f3544b93e2f681f996da519a98ace0107a", }); // 0459c999c3edf05e73c83f3fbae9f0f020919f91 has 12 decimals instead of standard 8 - let contract_address = "0x0459c999c3edf05e73c83f3fbae9f0f020919f91".into(); + let contract_address = H160::from_str("0x0459c999c3edf05e73c83f3fbae9f0f020919f91").unwrap(); 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( + let coin = block_on(qrc20_coin_with_priv_key( &ctx, "QRC20", "QTUM", &conf, ¶ms, - &priv_key, + Secp256k1Secret::from(priv_key), contract_address, )) .unwrap(); @@ -929,7 +1023,7 @@ fn test_validate_maker_payment_malicious() { 24, 181, 194, 193, 18, 152, 142, 168, 71, 73, 70, 244, 9, 101, 92, 168, 243, 61, 132, 48, 25, 39, 103, 92, 29, 17, 11, 29, 113, 235, 48, 70, ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key, None); + let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // Malicious tx 81540dc6abe59cf1e301a97a7e1c9b66d5f475da916faa3f0ef7ea896c0b3e5a let payment_tx = hex::decode("01000000010144e2b8b5e6da0666faf1db95075653ef49e2acaa8924e1ec595f6b89a6f715050000006a4730440220415adec5e24148db8e9654a6beda4b1af8aded596ab1cd8667af32187853e8f5022007a91d44ee13046194aafc07ca46ec44f770e75b41187acaa4e38e17d4eccb5d012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff030000000000000000625403a08601012844095ea7b300000000000000000000000085a4df739bbb2d247746bea611d5d365204725830000000000000000000000000000000000000000000000000000000005f5e10014d362e096e873eb7907e205fadc6175c6fec7bc44c20000000000000000e35403a0860101284cc49b415b2a0a1a8b4af2762154115ced87e2424b3cb940c0181cc3c850523702f1ec298fef0000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000783cf0be521101942da509846ea476e683aad8324b6b2e5444c2639cc0fb7bcea5afba3f3cdce239000000000000000000000000000000000000000000000000000000000000000000000000000000005fa0fffb1485a4df739bbb2d247746bea611d5d36520472583c208535c01000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acc700a15f").unwrap(); @@ -940,6 +1034,7 @@ fn test_validate_maker_payment_malicious() { let input = ValidatePaymentInput { payment_tx, + time_lock_duration: 0, time_lock: 1604386811, secret_hash, amount, @@ -953,14 +1048,20 @@ fn test_validate_maker_payment_malicious() { .validate_maker_payment(input) .wait() .err() - .expect("'erc20Payment' was called from another swap contract, expected an error"); + .expect("'erc20Payment' was called from another swap contract, expected an error") + .into_inner(); log!("error: {}", error); - assert!(error.contains("Unexpected amount 1000 in 'Transfer' event, expected 100000000")); + match error { + ValidatePaymentError::TxDeserializationError(err) => { + assert!(err.contains("Unexpected amount 1000 in 'Transfer' event, expected 100000000")) + }, + _ => panic!("Expected `TxDeserializationError` unexpected amount, found {:?}", error), + } } #[test] fn test_negotiate_swap_contract_addr_no_fallback() { - let (_, coin) = qrc20_coin_for_test(&[1; 32], None); + let (_, coin) = qrc20_coin_for_test([1; 32], None); let input = None; let error = coin.negotiate_swap_contract_addr(input).unwrap_err().into_inner(); @@ -990,11 +1091,11 @@ fn test_negotiate_swap_contract_addr_has_fallback() { let fallback = "0x8500AFc0bc5214728082163326C2FF0C73f4a871"; let fallback_addr = qtum::contract_addr_from_str(fallback).unwrap(); - let (_, coin) = qrc20_coin_for_test(&[1; 32], Some(fallback)); + let (_, coin) = qrc20_coin_for_test([1; 32], Some(fallback)); let input = None; let result = coin.negotiate_swap_contract_addr(input).unwrap(); - assert_eq!(Some(fallback_addr.to_vec().into()), result); + assert_eq!(Some(fallback_addr.0.to_vec().into()), result); let slice: &[u8] = &[1; 1]; let error = coin.negotiate_swap_contract_addr(Some(slice)).unwrap_err().into_inner(); @@ -1016,7 +1117,7 @@ fn test_negotiate_swap_contract_addr_has_fallback() { let slice: &[u8] = fallback_addr.as_ref(); let result = coin.negotiate_swap_contract_addr(Some(slice)).unwrap(); - assert_eq!(Some(fallback_addr.to_vec().into()), result); + assert_eq!(Some(fallback_addr.0.to_vec().into()), result); } #[test] @@ -1025,7 +1126,7 @@ fn test_send_contract_calls_recoverable_tx() { 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 (_ctx, coin) = qrc20_coin_for_test(priv_key, None); let tx = TransactionEnum::UtxoTx("010000000160fd74b5714172f285db2b36f0b391cd6883e7291441631c8b18f165b0a4635d020000006a47304402205d409e141111adbc4f185ae856997730de935ac30a0d2b1ccb5a6c4903db8171022024fc59bbcfdbba283556d7eeee4832167301dc8e8ad9739b7865f67b9676b226012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000625403a08601012844a9059cbb000000000000000000000000ca1e04745e8ca0c60d8c5881531d51bec470743f00000000000000000000000000000000000000000000000000000000000f424014d362e096e873eb7907e205fadc6175c6fec7bc44c200ada205000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acfe967d5f".into()); diff --git a/mm2src/coins/qrc20/script_pubkey.rs b/mm2src/coins/qrc20/script_pubkey.rs index 2cbb4bba9e..08abad3024 100644 --- a/mm2src/coins/qrc20/script_pubkey.rs +++ b/mm2src/coins/qrc20/script_pubkey.rs @@ -104,7 +104,7 @@ pub fn extract_contract_addr_from_script(script: &Script) -> Result return ERR!("Unexpected instruction's opcode {}", instruction.opcode), } - Ok(instruction.data.ok_or(ERRL!("An empty contract call data"))?.into()) + instruction.data.try_to_address() } /// Serialize the `number` similar to BigEndian but in QRC20 specific format. diff --git a/mm2src/coins/qrc20/swap.rs b/mm2src/coins/qrc20/swap.rs index 098ddd8ab9..39803f4046 100644 --- a/mm2src/coins/qrc20/swap.rs +++ b/mm2src/coins/qrc20/swap.rs @@ -1,5 +1,7 @@ use super::history::TransferHistoryBuilder; use super::*; +use crate::eth::decode_contract_call; +use bitcrypto::ripemd160; use script_pubkey::{extract_contract_addr_from_script, extract_contract_call_from_script, is_contract_call}; /// `erc20Payment` call details consist of values obtained from [`TransactionOutput::script_pubkey`] and [`TxReceipt::logs`]. @@ -38,6 +40,12 @@ impl Qrc20Coin { receiver_addr: H160, swap_contract_address: H160, ) -> Result { + let secret_hash = if secret_hash.len() == 32 { + ripemd160(&secret_hash).to_vec() + } else { + secret_hash + }; + let balance = try_tx_s!(self.my_spendable_balance().compat().await); let balance = try_tx_s!(wei_from_big_decimal(&balance, self.utxo.decimals)); @@ -113,46 +121,54 @@ impl Qrc20Coin { secret_hash: Vec, amount: BigDecimal, expected_swap_contract_address: H160, - ) -> Result<(), String> { + ) -> Result<(), MmError> { let expected_swap_id = qrc20_swap_id(time_lock, &secret_hash); - let status = try_s!( - self.payment_status(&expected_swap_contract_address, expected_swap_id.clone()) - .await - ); + let status = self + .payment_status(&expected_swap_contract_address, expected_swap_id.clone()) + .await?; if status != eth::PAYMENT_STATE_SENT.into() { - return ERR!("Payment state is not PAYMENT_STATE_SENT, got {}", status); + return MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( + "Payment state is not PAYMENT_STATE_SENT, got {}", + status + ))); } let expected_call_bytes = { - let expected_value = try_s!(wei_from_big_decimal(&amount, self.utxo.decimals)); - 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( + let expected_value = wei_from_big_decimal(&amount, self.utxo.decimals)?; + let my_address = self.utxo.derivation_method.single_addr_or_err()?.clone(); + let expected_receiver = qtum::contract_addr_from_utxo_addr(my_address) + .mm_err(|err| ValidatePaymentError::InternalError(err.to_string()))?; + self.erc20_payment_call_bytes( expected_swap_id, expected_value, time_lock, &secret_hash, - expected_receiver - )) + expected_receiver, + )? }; - - let erc20_payment = try_s!(self.erc20_payment_details_from_tx(&payment_tx).await); + let erc20_payment = self + .erc20_payment_details_from_tx(&payment_tx) + .await + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; if erc20_payment.contract_call_bytes != expected_call_bytes { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Unexpected 'erc20Payment' contract call bytes: {:?}", erc20_payment.contract_call_bytes - ); + ))); } if sender != erc20_payment.sender { - return ERR!("Payment tx was sent from wrong address, expected {:?}", sender); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx {:?} was sent from wrong address, expected {:?}", + erc20_payment.sender, sender + ))); } if expected_swap_contract_address != erc20_payment.swap_contract_address { - return ERR!( - "Payment tx was sent to wrong address, expected {:?}", - expected_swap_contract_address - ); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx receiver arg {:?} is invalid, expected {:?}", + erc20_payment.swap_contract_address, expected_swap_contract_address, + ))); } Ok(()) @@ -247,7 +263,7 @@ impl Qrc20Coin { } // Else try to find a 'senderRefund' contract call. - let my_address = try_s!(self.utxo.derivation_method.iguana_or_err()).clone(); + let my_address = try_s!(self.utxo.derivation_method.single_addr_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| { @@ -271,7 +287,7 @@ impl Qrc20Coin { return Ok(None); }; - let my_address = try_s!(self.utxo.derivation_method.iguana_or_err()).clone(); + let my_address = try_s!(self.utxo.derivation_method.single_addr_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 @@ -282,6 +298,12 @@ impl Qrc20Coin { } pub fn extract_secret_impl(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + let secret_hash = if secret_hash.len() == 32 { + ripemd160(secret_hash) + } else { + chain::hash::H160::from(secret_hash) + }; + let spend_tx: UtxoTx = try_s!(deserialize(spend_tx).map_err(|e| ERRL!("{:?}", e))); let spend_tx_hash: H256Json = spend_tx.hash().reversed().into(); for output in spend_tx.outputs { @@ -296,7 +318,7 @@ impl Qrc20Coin { }, }; - let actual_secret_hash = &*dhash160(&secret); + let actual_secret_hash = dhash160(&secret); if actual_secret_hash != secret_hash { warn!( "invalid 'dhash160(secret)' {:?}, expected {:?}", @@ -316,6 +338,7 @@ impl Qrc20Coin { tx: UtxoTx, wait_until: u64, from_block: u64, + check_every: f64, ) -> Result { let Erc20PaymentDetails { swap_id, @@ -339,7 +362,7 @@ impl Qrc20Coin { if now_ms() / 1000 > wait_until { return ERR!("Waited too long until {} for {:?} to be spent ", wait_until, tx); } - Timer::sleep(10.).await; + Timer::sleep(check_every).await; } } @@ -436,7 +459,7 @@ impl Qrc20Coin { let my_address = self .utxo .derivation_method - .iguana_or_err() + .single_addr_or_err() .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; let tokens = self .utxo @@ -466,26 +489,28 @@ impl Qrc20Coin { /// Get payment status by `swap_id`. /// Do not use self swap_contract_address, because it could be updated during restart. - async fn payment_status(&self, swap_contract_address: &H160, swap_id: Vec) -> Result { - let decoded = try_s!( - self.utxo - .rpc_client - .rpc_contract_call(ViewContractCallType::Payments, swap_contract_address, &[ - Token::FixedBytes(swap_id) - ]) - .compat() - .await - ); + async fn payment_status(&self, swap_contract_address: &H160, swap_id: Vec) -> MmResult { + let decoded = self + .utxo + .rpc_client + .rpc_contract_call(ViewContractCallType::Payments, swap_contract_address, &[ + Token::FixedBytes(swap_id), + ]) + .compat() + .await?; if decoded.len() < 3 { - return ERR!( + return MmError::err(UtxoRpcError::InvalidResponse(format!( "Expected at least 3 tokens in \"payments\" call, found {}", decoded.len() - ); + ))); } match decoded[2] { Token::Uint(state) => Ok(state), - _ => ERR!("Payment status must be uint, got {:?}", decoded[2]), + _ => MmError::err(UtxoRpcError::InvalidResponse(format!( + "Payment status must be uint, got {:?}", + decoded[2] + ))), } } @@ -497,7 +522,8 @@ impl Qrc20Coin { let gas_limit = QRC20_GAS_LIMIT_DEFAULT; let gas_price = QRC20_GAS_PRICE_DEFAULT; let script_pubkey = - generate_contract_call_script_pubkey(¶ms, gas_limit, gas_price, &self.contract_address)?.to_bytes(); + generate_contract_call_script_pubkey(¶ms, gas_limit, gas_price, self.contract_address.as_bytes())? + .to_bytes(); Ok(ContractCallOutput { value: OUTPUT_QTUM_AMOUNT, @@ -525,7 +551,7 @@ impl Qrc20Coin { ¶ms, // params of the function gas_limit, gas_price, - swap_contract_address, // address of the contract which function will be called + swap_contract_address.as_bytes(), // address of the contract which function will be called )? .to_bytes(); @@ -580,7 +606,7 @@ impl Qrc20Coin { ¶ms, // params of the function gas_limit, gas_price, - swap_contract_address, // address of the contract which function will be called + swap_contract_address.as_bytes(), // address of the contract which function will be called )? .to_bytes(); @@ -616,7 +642,7 @@ impl Qrc20Coin { ¶ms, // params of the function gas_limit, gas_price, - swap_contract_address, // address of the contract which function will be called + swap_contract_address.as_bytes(), // address of the contract which function will be called )? .to_bytes(); @@ -655,7 +681,7 @@ impl Qrc20Coin { try_s!(check_if_contract_call_completed(&receipt)); let function = try_s!(eth::SWAP_CONTRACT.function("erc20Payment")); - let decoded = try_s!(function.decode_input(&contract_call_bytes)); + let decoded = try_s!(decode_contract_call(function, &contract_call_bytes)); let mut decoded = decoded.into_iter(); @@ -844,7 +870,7 @@ fn transfer_call_details_from_script_pubkey(script_pubkey: &Script) -> Result<(H } let function = try_s!(eth::ERC20_CONTRACT.function("transfer")); - let decoded = try_s!(function.decode_input(&contract_call_bytes)); + let decoded = try_s!(decode_contract_call(function, &contract_call_bytes)); let mut decoded = decoded.into_iter(); let receiver = match decoded.next() { @@ -876,7 +902,7 @@ pub fn receiver_spend_call_details_from_script_pubkey(script_pubkey: &Script) -> } let function = try_s!(eth::SWAP_CONTRACT.function("receiverSpend")); - let decoded = try_s!(function.decode_input(&contract_call_bytes)); + let decoded = try_s!(decode_contract_call(function, &contract_call_bytes)); let mut decoded = decoded.into_iter(); let swap_id = match decoded.next() { @@ -923,6 +949,12 @@ fn find_receiver_spend_with_swap_id_and_secret_hash( expected_swap_id: &[u8], expected_secret_hash: &[u8], ) -> Option { + let expected_secret_hash = if expected_secret_hash.len() == 32 { + ripemd160(expected_secret_hash) + } else { + chain::hash::H160::from(expected_secret_hash) + }; + for (output_idx, output) in tx.outputs.iter().enumerate() { let script_pubkey: Script = output.script_pubkey.clone().into(); let ReceiverSpendDetails { swap_id, secret, .. } = @@ -938,7 +970,7 @@ fn find_receiver_spend_with_swap_id_and_secret_hash( continue; } - let secret_hash = &*dhash160(&secret); + let secret_hash = dhash160(&secret); if secret_hash != expected_secret_hash { warn!( "invalid 'dhash160(secret)' {:?}, expected {:?}", @@ -988,7 +1020,7 @@ fn find_swap_contract_call_with_swap_id( } let function = call_type.as_function(); - let decoded = match function.decode_input(&contract_call_bytes) { + let decoded = match decode_contract_call(function, &contract_call_bytes) { Ok(d) => d, Err(e) => { error!("{}", e); diff --git a/mm2src/coins/rpc_command/get_current_mtp.rs b/mm2src/coins/rpc_command/get_current_mtp.rs new file mode 100644 index 0000000000..24b4f563b9 --- /dev/null +++ b/mm2src/coins/rpc_command/get_current_mtp.rs @@ -0,0 +1,72 @@ +use common::{HttpStatusCode, StatusCode}; +use derive_more::Display; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; + +use crate::{lp_coinfind_or_err, + utxo::{rpc_clients::UtxoRpcError, UtxoCommonOps}, + CoinFindError, MmCoinEnum}; + +pub type GetCurrentMtpRpcResult = Result>; + +#[derive(Deserialize)] +pub struct GetCurrentMtpRequest { + coin: String, +} + +#[derive(Serialize)] +pub struct GetCurrentMtpResponse { + mtp: u32, +} + +#[derive(Serialize, Display, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetCurrentMtpError { + NoSuchCoin(String), + #[display(fmt = "Requested coin: {}; is not supported for this action.", _0)] + NotSupportedCoin(String), + RpcError(String), +} + +impl HttpStatusCode for GetCurrentMtpError { + fn status_code(&self) -> StatusCode { + match self { + GetCurrentMtpError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + GetCurrentMtpError::NotSupportedCoin(_) => StatusCode::BAD_REQUEST, + GetCurrentMtpError::RpcError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for GetCurrentMtpError { + fn from(err: UtxoRpcError) -> Self { Self::RpcError(err.to_string()) } +} + +impl From for GetCurrentMtpError { + fn from(err: CoinFindError) -> Self { Self::NoSuchCoin(err.to_string()) } +} + +pub async fn get_current_mtp_rpc( + ctx: MmArc, + req: GetCurrentMtpRequest, +) -> GetCurrentMtpRpcResult { + match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::UtxoCoin(utxo) => Ok(GetCurrentMtpResponse { + mtp: utxo.get_current_mtp().await?, + }), + MmCoinEnum::QtumCoin(qtum) => Ok(GetCurrentMtpResponse { + mtp: qtum.get_current_mtp().await?, + }), + MmCoinEnum::Qrc20Coin(qrc) => Ok(GetCurrentMtpResponse { + mtp: qrc.get_current_mtp().await?, + }), + #[cfg(not(target_arch = "wasm32"))] + MmCoinEnum::ZCoin(zcoin) => Ok(GetCurrentMtpResponse { + mtp: zcoin.get_current_mtp().await?, + }), + MmCoinEnum::Bch(bch) => Ok(GetCurrentMtpResponse { + mtp: bch.get_current_mtp().await?, + }), + _ => Err(MmError::new(GetCurrentMtpError::NotSupportedCoin(req.coin))), + } +} diff --git a/mm2src/coins/rpc_command/get_enabled_coins.rs b/mm2src/coins/rpc_command/get_enabled_coins.rs new file mode 100644 index 0000000000..9390dd0fe5 --- /dev/null +++ b/mm2src/coins/rpc_command/get_enabled_coins.rs @@ -0,0 +1,48 @@ +use crate::CoinsContext; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmResult; +use mm2_err_handle::prelude::*; + +#[derive(Serialize, Display, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetEnabledCoinsError { + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl HttpStatusCode for GetEnabledCoinsError { + fn status_code(&self) -> StatusCode { + match self { + GetEnabledCoinsError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +#[derive(Deserialize)] +pub struct GetEnabledCoinsRequest; + +#[derive(Serialize)] +pub struct GetEnabledCoinsResponse { + coins: Vec, +} + +#[derive(Serialize)] +pub struct EnabledCoin { + ticker: String, +} + +pub async fn get_enabled_coins( + ctx: MmArc, + _req: GetEnabledCoinsRequest, +) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(GetEnabledCoinsError::Internal)?; + let coins_map = coins_ctx.coins.lock().await; + + let coins = coins_map + .iter() + .map(|(ticker, _coin)| EnabledCoin { ticker: ticker.clone() }) + .collect(); + Ok(GetEnabledCoinsResponse { coins }) +} diff --git a/mm2src/coins/rpc_command/get_new_address.rs b/mm2src/coins/rpc_command/get_new_address.rs new file mode 100644 index 0000000000..54263226e6 --- /dev/null +++ b/mm2src/coins/rpc_command/get_new_address.rs @@ -0,0 +1,517 @@ +use crate::coin_balance::HDAddressBalance; +use crate::hd_confirm_address::{ConfirmAddressStatus, HDConfirmAddress, HDConfirmAddressError, RpcTaskConfirmAddress}; +use crate::hd_wallet::{AddressDerivingError, InvalidBip44ChainError, NewAddressDeriveConfirmError, + NewAddressDerivingError}; +use crate::{lp_coinfind_or_err, BalanceError, CoinFindError, CoinsContext, MmCoinEnum, UnexpectedDerivationMethod}; +use async_trait::async_trait; +use common::{HttpStatusCode, SuccessResponse}; +use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction, HwRpcTaskUserActionRequest}; +use crypto::{from_hw_error, Bip44Chain, HwError, HwRpcError, WithHwRpcError}; +use derive_more::Display; +use enum_from::EnumFromTrait; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, + RpcTaskStatusRequest, RpcTaskUserActionError}; +use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use std::time::Duration; + +pub type GetNewAddressUserAction = HwRpcTaskUserAction; +pub type GetNewAddressAwaitingStatus = HwRpcTaskAwaitingStatus; +pub type GetNewAddressTaskManager = RpcTaskManager; +pub type GetNewAddressTaskManagerShared = RpcTaskManagerShared; +pub type GetNewAddressTaskHandle = RpcTaskHandle; +pub type GetNewAddressRpcTaskStatus = RpcTaskStatus< + GetNewAddressResponse, + GetNewAddressRpcError, + GetNewAddressInProgressStatus, + GetNewAddressAwaitingStatus, +>; + +#[derive(Clone, Debug, Display, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetNewAddressRpcError { + #[display(fmt = "Hardware Wallet context is not initialized")] + HwContextNotInitialized, + #[display(fmt = "No such coin {coin}")] + NoSuchCoin { coin: String }, + #[display(fmt = "RPC 'task' is awaiting '{expected}' user action")] + UnexpectedUserAction { expected: String }, + #[display(fmt = "Coin is expected to be activated with the HD wallet derivation method")] + CoinIsActivatedNotWithHDWallet, + #[display(fmt = "HD account '{account_id}' is not activated")] + 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 = "Addresses limit reached. Max number of addresses: {max_addresses_number}")] + AddressLimitReached { max_addresses_number: u32 }, + #[display(fmt = "Empty addresses limit reached. Gap limit: {gap_limit}")] + EmptyAddressesLimitReached { gap_limit: u32 }, + #[display(fmt = "Electrum/Native RPC invalid response: {_0}")] + RpcInvalidResponse(String), + #[display(fmt = "HD wallet storage error: {_0}")] + WalletStorageError(String), + #[from_trait(WithTimeout::timeout)] + #[display(fmt = "RPC timed out {_0:?}")] + Timeout(Duration), + #[from_trait(WithHwRpcError::hw_rpc_error)] + HwError(HwRpcError), + #[display(fmt = "Transport: {_0}")] + Transport(String), + #[from_trait(WithInternal::internal)] + #[display(fmt = "Internal: {_0}")] + Internal(String), +} + +impl From for GetNewAddressRpcError { + fn from(e: BalanceError) -> Self { + match e { + BalanceError::Transport(transport) => GetNewAddressRpcError::Transport(transport), + BalanceError::InvalidResponse(rpc) => GetNewAddressRpcError::RpcInvalidResponse(rpc), + BalanceError::UnexpectedDerivationMethod(der_path) => GetNewAddressRpcError::from(der_path), + BalanceError::WalletStorageError(internal) | BalanceError::Internal(internal) => { + GetNewAddressRpcError::Internal(internal) + }, + } + } +} + +impl From for GetNewAddressRpcError { + fn from(e: UnexpectedDerivationMethod) -> Self { + match e { + UnexpectedDerivationMethod::ExpectedHDWallet => GetNewAddressRpcError::CoinIsActivatedNotWithHDWallet, + unexpected_error => GetNewAddressRpcError::Internal(unexpected_error.to_string()), + } + } +} + +impl From for GetNewAddressRpcError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => GetNewAddressRpcError::NoSuchCoin { coin }, + } + } +} + +impl From for GetNewAddressRpcError { + fn from(e: InvalidBip44ChainError) -> Self { GetNewAddressRpcError::InvalidBip44Chain { chain: e.chain } } +} + +impl From for GetNewAddressRpcError { + fn from(e: NewAddressDerivingError) -> Self { + match e { + NewAddressDerivingError::AddressLimitReached { max_addresses_number } => { + GetNewAddressRpcError::AddressLimitReached { max_addresses_number } + }, + NewAddressDerivingError::InvalidBip44Chain { chain } => GetNewAddressRpcError::InvalidBip44Chain { chain }, + NewAddressDerivingError::Bip32Error(bip32) => GetNewAddressRpcError::Internal(bip32.to_string()), + NewAddressDerivingError::WalletStorageError(storage) => { + GetNewAddressRpcError::WalletStorageError(storage.to_string()) + }, + NewAddressDerivingError::Internal(internal) => GetNewAddressRpcError::Internal(internal), + } + } +} + +impl From for GetNewAddressRpcError { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::InvalidBip44Chain { chain } => GetNewAddressRpcError::InvalidBip44Chain { chain }, + AddressDerivingError::Bip32Error(bip32) => GetNewAddressRpcError::ErrorDerivingAddress(bip32.to_string()), + AddressDerivingError::Internal(internal) => GetNewAddressRpcError::Internal(internal), + } + } +} + +impl From for GetNewAddressRpcError { + fn from(e: NewAddressDeriveConfirmError) -> Self { + match e { + NewAddressDeriveConfirmError::DeriveError(derive) => GetNewAddressRpcError::from(derive), + NewAddressDeriveConfirmError::ConfirmError(confirm) => GetNewAddressRpcError::from(confirm), + } + } +} + +impl From for GetNewAddressRpcError { + fn from(e: HDConfirmAddressError) -> Self { + match e { + HDConfirmAddressError::HwContextNotInitialized => GetNewAddressRpcError::HwContextNotInitialized, + HDConfirmAddressError::RpcTaskError(rpc) => GetNewAddressRpcError::from(rpc), + HDConfirmAddressError::HardwareWalletError(hw) => GetNewAddressRpcError::from(hw), + HDConfirmAddressError::InvalidAddress { expected, found } => GetNewAddressRpcError::Internal(format!( + "Confirmation address mismatched: expected '{expected}, found '{found}''" + )), + HDConfirmAddressError::Internal(internal) => GetNewAddressRpcError::Internal(internal), + } + } +} + +impl From for GetNewAddressRpcError { + fn from(e: HwError) -> Self { from_hw_error(e) } +} + +impl From for GetNewAddressRpcError { + fn from(e: RpcTaskError) -> Self { + let error = e.to_string(); + match e { + RpcTaskError::Cancelled => GetNewAddressRpcError::Internal("Cancelled".to_owned()), + RpcTaskError::Timeout(timeout) => GetNewAddressRpcError::Timeout(timeout), + RpcTaskError::NoSuchTask(_) | RpcTaskError::UnexpectedTaskStatus { .. } => { + GetNewAddressRpcError::Internal(error) + }, + RpcTaskError::UnexpectedUserAction { expected } => GetNewAddressRpcError::UnexpectedUserAction { expected }, + RpcTaskError::Internal(internal) => GetNewAddressRpcError::Internal(internal), + } + } +} + +impl HttpStatusCode for GetNewAddressRpcError { + fn status_code(&self) -> StatusCode { + match self { + GetNewAddressRpcError::HwContextNotInitialized + | GetNewAddressRpcError::NoSuchCoin { .. } + | GetNewAddressRpcError::UnexpectedUserAction { .. } + | GetNewAddressRpcError::CoinIsActivatedNotWithHDWallet + | GetNewAddressRpcError::UnknownAccount { .. } + | GetNewAddressRpcError::InvalidBip44Chain { .. } + | GetNewAddressRpcError::ErrorDerivingAddress(_) + | GetNewAddressRpcError::AddressLimitReached { .. } + | GetNewAddressRpcError::EmptyAddressesLimitReached { .. } => StatusCode::BAD_REQUEST, + GetNewAddressRpcError::Transport(_) + | GetNewAddressRpcError::RpcInvalidResponse(_) + | GetNewAddressRpcError::WalletStorageError(_) + | GetNewAddressRpcError::HwError(_) + | GetNewAddressRpcError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + GetNewAddressRpcError::Timeout(_) => StatusCode::REQUEST_TIMEOUT, + } + } +} + +#[derive(Deserialize)] +pub struct GetNewAddressRequest { + coin: String, + #[serde(flatten)] + params: GetNewAddressParams, +} + +#[derive(Clone, Deserialize)] +pub struct GetNewAddressParams { + pub(crate) account_id: u32, + pub(crate) chain: Option, + // The max number of empty addresses in a row. + // If there are more or equal to the `gap_limit` last empty addresses in a row, + // we'll not allow to generate new address. + pub(crate) gap_limit: Option, +} + +#[derive(Clone, Debug, Serialize)] +pub struct GetNewAddressResponse { + new_address: HDAddressBalance, +} + +#[derive(Clone, Serialize)] +pub enum GetNewAddressInProgressStatus { + 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, + FollowHwDeviceInstructions, + ConfirmAddress { + expected_address: String, + }, +} + +impl ConfirmAddressStatus for GetNewAddressInProgressStatus { + fn confirm_addr_status(expected_address: String) -> Self { + GetNewAddressInProgressStatus::ConfirmAddress { expected_address } + } +} + +#[async_trait] +pub trait GetNewAddressRpcOps { + /// Generates a new address. + /// TODO remove once GUI integrates `task::get_new_address::init`. + async fn get_new_address_rpc_without_conf( + &self, + params: GetNewAddressParams, + ) -> MmResult; + + /// Generates and asks the user to confirm a new address. + async fn get_new_address_rpc( + &self, + params: GetNewAddressParams, + confirm_address: &ConfirmAddress, + ) -> MmResult + where + ConfirmAddress: HDConfirmAddress; +} + +pub struct InitGetNewAddressTask { + ctx: MmArc, + coin: MmCoinEnum, + req: GetNewAddressRequest, +} + +impl RpcTaskTypes for InitGetNewAddressTask { + type Item = GetNewAddressResponse; + type Error = GetNewAddressRpcError; + type InProgressStatus = GetNewAddressInProgressStatus; + type AwaitingStatus = GetNewAddressAwaitingStatus; + type UserAction = GetNewAddressUserAction; +} + +#[async_trait] +impl RpcTask for InitGetNewAddressTask { + fn initial_status(&self) -> Self::InProgressStatus { GetNewAddressInProgressStatus::Preparing } + + // Do nothing if the task has been cancelled. + async fn cancel(self) {} + + async fn run(&mut self, task_handle: &RpcTaskHandle) -> Result> { + async fn get_new_address_helper( + ctx: &MmArc, + coin: &Coin, + params: GetNewAddressParams, + task_handle: &GetNewAddressTaskHandle, + ) -> MmResult + where + Coin: GetNewAddressRpcOps + Send + Sync, + { + let hw_statuses = HwConnectStatuses { + on_connect: GetNewAddressInProgressStatus::WaitingForTrezorToConnect, + on_connected: GetNewAddressInProgressStatus::Preparing, + on_connection_failed: GetNewAddressInProgressStatus::Finishing, + on_button_request: GetNewAddressInProgressStatus::FollowHwDeviceInstructions, + on_pin_request: GetNewAddressAwaitingStatus::EnterTrezorPin, + on_passphrase_request: GetNewAddressAwaitingStatus::EnterTrezorPassphrase, + on_ready: GetNewAddressInProgressStatus::RequestingAccountBalance, + }; + let confirm_address: RpcTaskConfirmAddress<'_, InitGetNewAddressTask> = + RpcTaskConfirmAddress::new(ctx, task_handle, hw_statuses)?; + coin.get_new_address_rpc(params, &confirm_address).await + } + + match self.coin { + MmCoinEnum::UtxoCoin(ref utxo) => { + get_new_address_helper(&self.ctx, utxo, self.req.params.clone(), task_handle).await + }, + MmCoinEnum::QtumCoin(ref qtum) => { + get_new_address_helper(&self.ctx, qtum, self.req.params.clone(), task_handle).await + }, + _ => MmError::err(GetNewAddressRpcError::CoinIsActivatedNotWithHDWallet), + } + } +} + +/// Generates a new address. +/// TODO remove once GUI integrates `task::get_new_address::init`. +pub async fn get_new_address( + ctx: MmArc, + req: GetNewAddressRequest, +) -> MmResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + match coin { + MmCoinEnum::UtxoCoin(utxo) => utxo.get_new_address_rpc_without_conf(req.params).await, + MmCoinEnum::QtumCoin(qtum) => qtum.get_new_address_rpc_without_conf(req.params).await, + _ => MmError::err(GetNewAddressRpcError::CoinIsActivatedNotWithHDWallet), + } +} + +/// Generates a new address. +/// TODO remove once GUI integrates `task::get_new_address::init`. +pub async fn init_get_new_address( + ctx: MmArc, + req: GetNewAddressRequest, +) -> MmResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(GetNewAddressRpcError::Internal)?; + let spawner = coin.spawner(); + let task = InitGetNewAddressTask { ctx, coin, req }; + let task_id = GetNewAddressTaskManager::spawn_rpc_task(&coins_ctx.get_new_address_manager, &spawner, task)?; + Ok(InitRpcTaskResponse { task_id }) +} + +pub async fn init_get_new_address_status( + ctx: MmArc, + req: RpcTaskStatusRequest, +) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(RpcTaskStatusError::Internal)?; + let mut task_manager = coins_ctx + .get_new_address_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_get_new_address_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 + .get_new_address_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 async fn cancel_get_new_address( + ctx: MmArc, + req: CancelRpcTaskRequest, +) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(CancelRpcTaskError::Internal)?; + let mut task_manager = coins_ctx + .get_new_address_manager + .lock() + .map_to_mm(|e| CancelRpcTaskError::Internal(e.to_string()))?; + task_manager.cancel_task(req.task_id)?; + Ok(SuccessResponse::new()) +} + +pub(crate) mod common_impl { + use super::*; + use crate::coin_balance::{HDAddressBalanceScanner, HDWalletBalanceOps}; + use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; + use crate::{CoinWithDerivationMethod, HDAddress}; + use crypto::RpcDerivationPath; + use std::fmt; + use std::ops::DerefMut; + + /// TODO remove once GUI integrates `task::get_new_address::init`. + pub async fn get_new_address_rpc_without_conf( + coin: &Coin, + params: GetNewAddressParams, + ) -> MmResult + where + Coin: + HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync + Send, + ::Address: fmt::Display, + { + let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; + + let account_id = params.account_id; + let mut hd_account = hd_wallet + .get_account_mut(account_id) + .await + .or_mm_err(|| GetNewAddressRpcError::UnknownAccount { account_id })?; + + let chain = params.chain.unwrap_or_else(|| hd_wallet.default_receiver_chain()); + let gap_limit = params.gap_limit.unwrap_or_else(|| hd_wallet.gap_limit()); + + // Check if we can generate new address. + check_if_can_get_new_address(coin, hd_wallet, &hd_account, chain, gap_limit).await?; + + 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(GetNewAddressResponse { + new_address: HDAddressBalance { + address: address.to_string(), + derivation_path: RpcDerivationPath(derivation_path), + chain, + balance, + }, + }) + } + + pub async fn get_new_address_rpc<'a, Coin, ConfirmAddress>( + coin: &Coin, + params: GetNewAddressParams, + confirm_address: &ConfirmAddress, + ) -> MmResult + where + ConfirmAddress: HDConfirmAddress, + Coin: + HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Send + Sync, + ::Address: fmt::Display, + { + let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; + + let account_id = params.account_id; + let mut hd_account = hd_wallet + .get_account_mut(account_id) + .await + .or_mm_err(|| GetNewAddressRpcError::UnknownAccount { account_id })?; + + let chain = params.chain.unwrap_or_else(|| hd_wallet.default_receiver_chain()); + let gap_limit = params.gap_limit.unwrap_or_else(|| hd_wallet.gap_limit()); + + // Check if we can generate new address. + check_if_can_get_new_address(coin, hd_wallet, &hd_account, chain, gap_limit).await?; + + let HDAddress { + address, + derivation_path, + .. + } = coin + .generate_and_confirm_new_address(hd_wallet, &mut hd_account, chain, confirm_address) + .await?; + + let balance = coin.known_address_balance(&address).await?; + Ok(GetNewAddressResponse { + new_address: HDAddressBalance { + address: address.to_string(), + derivation_path: RpcDerivationPath(derivation_path), + chain, + balance, + }, + }) + } + + async fn check_if_can_get_new_address( + coin: &Coin, + hd_wallet: &Coin::HDWallet, + hd_account: &Coin::HDAccount, + chain: Bip44Chain, + gap_limit: u32, + ) -> MmResult<(), GetNewAddressRpcError> + where + Coin: HDWalletBalanceOps + Sync, + ::Address: fmt::Display, + { + let known_addresses_number = hd_account.known_addresses_number(chain)?; + if known_addresses_number == 0 || gap_limit > known_addresses_number { + return Ok(()); + } + + let max_addresses_number = hd_wallet.address_limit(); + if known_addresses_number >= max_addresses_number { + return MmError::err(GetNewAddressRpcError::AddressLimitReached { max_addresses_number }); + } + + let address_scanner = coin.produce_hd_address_scanner().await?; + + // Address IDs start from 0, so the `last_known_address_id = known_addresses_number - 1`. + // At this point we are sure that `known_addresses_number > 0`. + let last_address_id = known_addresses_number - 1; + + for address_id in (0..=last_address_id).rev() { + let HDAddress { address, .. } = coin.derive_address(hd_account, chain, address_id).await?; + if address_scanner.is_address_used(&address).await? { + return Ok(()); + } + + let empty_addresses_number = last_address_id - address_id + 1; + if empty_addresses_number >= gap_limit { + // We already have `gap_limit` empty addresses. + return MmError::err(GetNewAddressRpcError::EmptyAddressesLimitReached { gap_limit }); + } + } + + Ok(()) + } +} diff --git a/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs index 851d2b42d8..e929692285 100644 --- a/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs +++ b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs @@ -60,7 +60,7 @@ impl From for HDAccountBalanceRpcError { impl From for HDAccountBalanceRpcError { fn from(e: UnexpectedDerivationMethod) -> Self { match e { - UnexpectedDerivationMethod::HDWalletUnavailable => HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet, + UnexpectedDerivationMethod::ExpectedHDWallet => HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet, unexpected_error => HDAccountBalanceRpcError::Internal(unexpected_error.to_string()), } } @@ -72,8 +72,7 @@ impl From for HDAccountBalanceRpcError { 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), + BalanceError::WalletStorageError(e) | BalanceError::Internal(e) => HDAccountBalanceRpcError::Internal(e), } } } @@ -85,9 +84,11 @@ impl From for HDAccountBalanceRpcError { impl From for HDAccountBalanceRpcError { fn from(e: AddressDerivingError) -> Self { match e { + AddressDerivingError::InvalidBip44Chain { chain } => HDAccountBalanceRpcError::InvalidBip44Chain { chain }, AddressDerivingError::Bip32Error(bip32) => { HDAccountBalanceRpcError::ErrorDerivingAddress(bip32.to_string()) }, + AddressDerivingError::Internal(internal) => HDAccountBalanceRpcError::Internal(internal), } } } @@ -95,11 +96,12 @@ impl From for HDAccountBalanceRpcError { impl From for HDAccountBalanceRpcError { fn from(e: RpcTaskError) -> Self { match e { - RpcTaskError::Canceled => HDAccountBalanceRpcError::Internal("Canceled".to_owned()), + RpcTaskError::Cancelled => HDAccountBalanceRpcError::Internal("Cancelled".to_owned()), RpcTaskError::Timeout(timeout) => HDAccountBalanceRpcError::Timeout(timeout), - RpcTaskError::NoSuchTask(_) | RpcTaskError::UnexpectedTaskStatus { .. } => { - HDAccountBalanceRpcError::Internal(e.to_string()) - }, + RpcTaskError::NoSuchTask(_) + // `UnexpectedTaskStatus` and `UnexpectedUserAction` are not expected at the balance request. + | RpcTaskError::UnexpectedTaskStatus { .. } + | RpcTaskError::UnexpectedUserAction { .. } => HDAccountBalanceRpcError::Internal(e.to_string()), RpcTaskError::Internal(internal) => HDAccountBalanceRpcError::Internal(internal), } } diff --git a/mm2src/coins/rpc_command/init_account_balance.rs b/mm2src/coins/rpc_command/init_account_balance.rs new file mode 100644 index 0000000000..3317acea67 --- /dev/null +++ b/mm2src/coins/rpc_command/init_account_balance.rs @@ -0,0 +1,155 @@ +use crate::coin_balance::HDAccountBalance; +use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; +use crate::{lp_coinfind_or_err, CoinsContext, MmCoinEnum}; +use async_trait::async_trait; +use common::{SerdeInfallible, SuccessResponse}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, + RpcTaskStatusRequest}; +use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; + +pub type AccountBalanceUserAction = SerdeInfallible; +pub type AccountBalanceAwaitingStatus = SerdeInfallible; +pub type AccountBalanceTaskManager = RpcTaskManager; +pub type AccountBalanceTaskManagerShared = RpcTaskManagerShared; +pub type InitAccountBalanceTaskHandle = RpcTaskHandle; +pub type AccountBalanceRpcTaskStatus = RpcTaskStatus< + HDAccountBalance, + HDAccountBalanceRpcError, + AccountBalanceInProgressStatus, + AccountBalanceAwaitingStatus, +>; + +#[derive(Clone, Serialize)] +pub enum AccountBalanceInProgressStatus { + RequestingAccountBalance, +} + +#[derive(Deserialize)] +pub struct InitAccountBalanceRequest { + coin: String, + #[serde(flatten)] + params: InitAccountBalanceParams, +} + +#[derive(Clone, Deserialize)] +pub struct InitAccountBalanceParams { + account_index: u32, +} + +#[async_trait] +pub trait InitAccountBalanceRpcOps { + async fn init_account_balance_rpc( + &self, + params: InitAccountBalanceParams, + ) -> MmResult; +} + +pub struct InitAccountBalanceTask { + coin: MmCoinEnum, + req: InitAccountBalanceRequest, +} + +impl RpcTaskTypes for InitAccountBalanceTask { + type Item = HDAccountBalance; + type Error = HDAccountBalanceRpcError; + type InProgressStatus = AccountBalanceInProgressStatus; + type AwaitingStatus = AccountBalanceAwaitingStatus; + type UserAction = AccountBalanceUserAction; +} + +#[async_trait] +impl RpcTask for InitAccountBalanceTask { + fn initial_status(&self) -> Self::InProgressStatus { AccountBalanceInProgressStatus::RequestingAccountBalance } + + // Do nothing if the task has been cancelled. + async fn cancel(self) {} + + async fn run(&mut self, _task_handle: &InitAccountBalanceTaskHandle) -> Result> { + match self.coin { + MmCoinEnum::UtxoCoin(ref utxo) => utxo.init_account_balance_rpc(self.req.params.clone()).await, + MmCoinEnum::QtumCoin(ref qtum) => qtum.init_account_balance_rpc(self.req.params.clone()).await, + _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), + } + } +} + +pub async fn init_account_balance( + ctx: MmArc, + req: InitAccountBalanceRequest, +) -> MmResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let spawner = coin.spawner(); + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(HDAccountBalanceRpcError::Internal)?; + let task = InitAccountBalanceTask { coin, req }; + let task_id = AccountBalanceTaskManager::spawn_rpc_task(&coins_ctx.account_balance_task_manager, &spawner, task)?; + Ok(InitRpcTaskResponse { task_id }) +} + +pub async fn init_account_balance_status( + ctx: MmArc, + req: RpcTaskStatusRequest, +) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(RpcTaskStatusError::Internal)?; + let mut task_manager = coins_ctx + .account_balance_task_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 cancel_account_balance( + ctx: MmArc, + req: CancelRpcTaskRequest, +) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(CancelRpcTaskError::Internal)?; + let mut task_manager = coins_ctx + .account_balance_task_manager + .lock() + .map_to_mm(|e| CancelRpcTaskError::Internal(e.to_string()))?; + task_manager.cancel_task(req.task_id)?; + Ok(SuccessResponse::new()) +} + +pub mod common_impl { + use super::*; + use crate::coin_balance::HDWalletBalanceOps; + use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; + use crate::{CoinBalance, CoinWithDerivationMethod}; + use crypto::RpcDerivationPath; + use std::fmt; + + pub async fn init_account_balance_rpc( + coin: &Coin, + params: InitAccountBalanceParams, + ) -> 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 addresses = coin.all_known_addresses_balances(&hd_account).await?; + let total_balance = addresses + .iter() + .fold(CoinBalance::default(), |total_balance, address_balance| { + total_balance + address_balance.balance.clone() + }); + + Ok(HDAccountBalance { + account_index: account_id, + derivation_path: RpcDerivationPath(hd_account.account_derivation_path()), + total_balance, + addresses, + }) + } +} diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index 89667aa649..82f99587b6 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -1,15 +1,23 @@ 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 crate::hd_pubkey::{HDExtractPubkeyError, HDXPubExtractor, RpcTaskXPubExtractor}; +use crate::hd_wallet::NewAccountCreatingError; +use crate::{lp_coinfind_or_err, BalanceError, CoinBalance, CoinFindError, CoinWithDerivationMethod, CoinsContext, + MmCoinEnum, UnexpectedDerivationMethod}; use async_trait::async_trait; -use common::{true_f, SuccessResponse}; +use common::{true_f, HttpStatusCode, SuccessResponse}; use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction, HwRpcTaskUserActionRequest}; -use crypto::RpcDerivationPath; +use crypto::{from_hw_error, Bip44Chain, HwError, HwRpcError, RpcDerivationPath, WithHwRpcError}; +use derive_more::Display; +use enum_from::EnumFromTrait; +use http::StatusCode; 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}; +use parking_lot::Mutex as PaMutex; +use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, + RpcTaskStatusRequest, RpcTaskUserActionError}; +use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use std::sync::Arc; +use std::time::Duration; pub type CreateAccountUserAction = HwRpcTaskUserAction; pub type CreateAccountAwaitingStatus = HwRpcTaskAwaitingStatus; @@ -17,10 +25,136 @@ pub type CreateAccountTaskManager = RpcTaskManager; pub type CreateAccountTaskManagerShared = RpcTaskManagerShared; pub type CreateAccountTaskHandle = RpcTaskHandle; pub type CreateAccountRpcTaskStatus = - RpcTaskStatus; + RpcTaskStatus; type CreateAccountXPubExtractor<'task> = RpcTaskXPubExtractor<'task, InitCreateAccountTask>; +#[derive(Clone, Debug, Display, EnumFromTrait, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum CreateAccountRpcError { + #[display(fmt = "Hardware Wallet context is not initialized")] + HwContextNotInitialized, + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "RPC 'task' is awaiting '{}' user action", expected)] + UnexpectedUserAction { expected: String }, + #[from_trait(WithTimeout::timeout)] + #[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 = "Coin doesn't support the given BIP44 chain: {:?}", chain)] + InvalidBip44Chain { chain: Bip44Chain }, + #[display(fmt = "Accounts limit reached. Max number of accounts: {}", max_accounts_number)] + AccountLimitReached { max_accounts_number: u32 }, + #[display(fmt = "Electrum/Native RPC invalid response: {}", _0)] + RpcInvalidResponse(String), + #[display(fmt = "HD wallet storage error: {}", _0)] + WalletStorageError(String), + #[from_trait(WithHwRpcError::hw_rpc_error)] + #[display(fmt = "{}", _0)] + HwError(HwRpcError), + #[display(fmt = "Transport: {}", _0)] + Transport(String), + #[from_trait(WithInternal::internal)] + #[display(fmt = "Internal: {}", _0)] + Internal(String), +} + +impl From for CreateAccountRpcError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => CreateAccountRpcError::NoSuchCoin { coin }, + } + } +} + +impl From for CreateAccountRpcError { + fn from(e: UnexpectedDerivationMethod) -> Self { + match e { + UnexpectedDerivationMethod::ExpectedHDWallet => CreateAccountRpcError::CoinIsActivatedNotWithHDWallet, + unexpected_error => CreateAccountRpcError::Internal(unexpected_error.to_string()), + } + } +} + +impl From for CreateAccountRpcError { + fn from(e: NewAccountCreatingError) -> Self { + match e { + NewAccountCreatingError::HwContextNotInitialized => CreateAccountRpcError::HwContextNotInitialized, + NewAccountCreatingError::HDWalletUnavailable => CreateAccountRpcError::CoinIsActivatedNotWithHDWallet, + NewAccountCreatingError::CoinDoesntSupportTrezor => { + CreateAccountRpcError::Internal("Coin must support Trezor at this point".to_string()) + }, + NewAccountCreatingError::RpcTaskError(rpc) => CreateAccountRpcError::from(rpc), + NewAccountCreatingError::HardwareWalletError(hw) => CreateAccountRpcError::from(hw), + NewAccountCreatingError::AccountLimitReached { max_accounts_number } => { + CreateAccountRpcError::AccountLimitReached { max_accounts_number } + }, + NewAccountCreatingError::ErrorSavingAccountToStorage(e) => { + let error = format!("Error uploading HD account info to the storage: {}", e); + CreateAccountRpcError::WalletStorageError(error) + }, + NewAccountCreatingError::Internal(internal) => CreateAccountRpcError::Internal(internal), + } + } +} + +impl From for CreateAccountRpcError { + fn from(e: BalanceError) -> Self { + match e { + BalanceError::Transport(transport) => CreateAccountRpcError::Transport(transport), + BalanceError::InvalidResponse(rpc) => CreateAccountRpcError::RpcInvalidResponse(rpc), + BalanceError::UnexpectedDerivationMethod(der_path) => CreateAccountRpcError::from(der_path), + BalanceError::WalletStorageError(internal) | BalanceError::Internal(internal) => { + CreateAccountRpcError::Internal(internal) + }, + } + } +} + +impl From for CreateAccountRpcError { + fn from(e: HDExtractPubkeyError) -> Self { CreateAccountRpcError::from(NewAccountCreatingError::from(e)) } +} + +impl From for CreateAccountRpcError { + fn from(e: RpcTaskError) -> Self { + let error = e.to_string(); + match e { + RpcTaskError::Cancelled => CreateAccountRpcError::Internal("Cancelled".to_owned()), + RpcTaskError::Timeout(timeout) => CreateAccountRpcError::Timeout(timeout), + RpcTaskError::NoSuchTask(_) | RpcTaskError::UnexpectedTaskStatus { .. } => { + CreateAccountRpcError::Internal(error) + }, + RpcTaskError::UnexpectedUserAction { expected } => CreateAccountRpcError::UnexpectedUserAction { expected }, + RpcTaskError::Internal(internal) => CreateAccountRpcError::Internal(internal), + } + } +} + +impl From for CreateAccountRpcError { + fn from(e: HwError) -> Self { from_hw_error(e) } +} + +impl HttpStatusCode for CreateAccountRpcError { + fn status_code(&self) -> StatusCode { + match self { + CreateAccountRpcError::HwContextNotInitialized + | CreateAccountRpcError::NoSuchCoin { .. } + | CreateAccountRpcError::UnexpectedUserAction { .. } + | CreateAccountRpcError::CoinIsActivatedNotWithHDWallet + | CreateAccountRpcError::InvalidBip44Chain { .. } + | CreateAccountRpcError::AccountLimitReached { .. } => StatusCode::BAD_REQUEST, + CreateAccountRpcError::HwError(_) => StatusCode::GONE, + CreateAccountRpcError::Timeout(_) => StatusCode::REQUEST_TIMEOUT, + CreateAccountRpcError::Transport(_) + | CreateAccountRpcError::RpcInvalidResponse(_) + | CreateAccountRpcError::WalletStorageError(_) + | CreateAccountRpcError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + #[derive(Deserialize)] pub struct CreateNewAccountRequest { coin: String, @@ -28,10 +162,12 @@ pub struct CreateNewAccountRequest { params: CreateNewAccountParams, } -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] pub struct CreateNewAccountParams { #[serde(default = "true_f")] scan: bool, + // The max number of empty addresses in a row. + // If transactions were sent to an address outside the `gap_limit`, they will not be identified. gap_limit: Option, } @@ -43,29 +179,48 @@ pub enum CreateAccountInProgressStatus { /// 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, + FollowHwDeviceInstructions, +} + +#[derive(Default)] +struct StateData { + account_id: Option, +} + +#[derive(Clone, Default)] +pub struct CreateAccountState(Arc>); + +impl CreateAccountState { + pub fn on_account_created(&self, account_id: u32) { self.0.lock().account_id = Some(account_id); } + + pub fn create_account_id(&self) -> Option { self.0.lock().account_id } } #[async_trait] -pub trait InitCreateHDAccountRpcOps { +pub trait InitCreateAccountRpcOps { async fn init_create_account_rpc( &self, params: CreateNewAccountParams, + state: CreateAccountState, xpub_extractor: &XPubExtractor, - ) -> MmResult + ) -> MmResult where - XPubExtractor: HDXPubExtractor + Sync; + XPubExtractor: HDXPubExtractor; + + async fn revert_creating_account(&self, account_id: u32); } pub struct InitCreateAccountTask { ctx: MmArc, coin: MmCoinEnum, req: CreateNewAccountRequest, + /// The state of the account creation. It's used to revert changes if the task has been cancelled. + task_state: CreateAccountState, } impl RpcTaskTypes for InitCreateAccountTask { type Item = HDAccountBalance; - type Error = HDWalletRpcError; + type Error = CreateAccountRpcError; type InProgressStatus = CreateAccountInProgressStatus; type AwaitingStatus = CreateAccountAwaitingStatus; type UserAction = CreateAccountUserAction; @@ -75,36 +230,63 @@ impl RpcTaskTypes for InitCreateAccountTask { impl RpcTask for InitCreateAccountTask { fn initial_status(&self) -> Self::InProgressStatus { CreateAccountInProgressStatus::Preparing } - async fn run(self, task_handle: &CreateAccountTaskHandle) -> Result> { + async fn cancel(self) { + if let Some(account_id) = self.task_state.create_account_id() { + // We created the account already, so need to revert the changes. + match self.coin { + MmCoinEnum::UtxoCoin(utxo) => utxo.revert_creating_account(account_id).await, + MmCoinEnum::QtumCoin(qtum) => qtum.revert_creating_account(account_id).await, + _ => (), + } + }; + } + + async fn run(&mut self, task_handle: &CreateAccountTaskHandle) -> Result> { async fn create_new_account_helper( ctx: &MmArc, - coin: Coin, + coin: &Coin, params: CreateNewAccountParams, + state: CreateAccountState, task_handle: &CreateAccountTaskHandle, - ) -> MmResult + ) -> MmResult where - Coin: InitCreateHDAccountRpcOps + Send + Sync, + Coin: InitCreateAccountRpcOps + 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_button_request: CreateAccountInProgressStatus::FollowHwDeviceInstructions, + on_pin_request: CreateAccountAwaitingStatus::EnterTrezorPin, + on_passphrase_request: CreateAccountAwaitingStatus::EnterTrezorPassphrase, on_ready: CreateAccountInProgressStatus::RequestingAccountBalance, }; let xpub_extractor = CreateAccountXPubExtractor::new(ctx, task_handle, hw_statuses)?; - coin.init_create_account_rpc(params, &xpub_extractor).await + coin.init_create_account_rpc(params, state, &xpub_extractor).await } match self.coin { - MmCoinEnum::UtxoCoin(utxo) => { - create_new_account_helper(&self.ctx, utxo, self.req.params, task_handle).await + MmCoinEnum::UtxoCoin(ref utxo) => { + create_new_account_helper( + &self.ctx, + utxo, + self.req.params.clone(), + self.task_state.clone(), + task_handle, + ) + .await }, - MmCoinEnum::QtumCoin(qtum) => { - create_new_account_helper(&self.ctx, qtum, self.req.params, task_handle).await + MmCoinEnum::QtumCoin(ref qtum) => { + create_new_account_helper( + &self.ctx, + qtum, + self.req.params.clone(), + self.task_state.clone(), + task_handle, + ) + .await }, - _ => MmError::err(HDWalletRpcError::CoinIsActivatedNotWithHDWallet), + _ => MmError::err(CreateAccountRpcError::CoinIsActivatedNotWithHDWallet), } } } @@ -112,11 +294,17 @@ impl RpcTask for InitCreateAccountTask { pub async fn init_create_new_account( ctx: MmArc, req: CreateNewAccountRequest, -) -> MmResult { +) -> 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)?; + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(CreateAccountRpcError::Internal)?; + let spawner = coin.spawner(); + let task = InitCreateAccountTask { + ctx, + coin, + req, + task_state: CreateAccountState::default(), + }; + let task_id = CreateAccountTaskManager::spawn_rpc_task(&coins_ctx.create_account_manager, &spawner, task)?; Ok(InitRpcTaskResponse { task_id }) } @@ -147,34 +335,47 @@ pub async fn init_create_new_account_user_action( Ok(SuccessResponse::new()) } +pub async fn cancel_create_new_account( + ctx: MmArc, + req: CancelRpcTaskRequest, +) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(CancelRpcTaskError::Internal)?; + let mut task_manager = coins_ctx + .create_account_manager + .lock() + .map_to_mm(|e| CancelRpcTaskError::Internal(e.to_string()))?; + task_manager.cancel_task(req.task_id)?; + 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, + state: CreateAccountState, xpub_extractor: &XPubExtractor, - ) -> MmResult + ) -> MmResult where - Coin: HDWalletBalanceOps - + CoinWithDerivationMethod::HDWallet> - + Send - + Sync - + MarketCoinOps, - XPubExtractor: HDXPubExtractor + Sync, + Coin: + HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Send + Sync, + XPubExtractor: HDXPubExtractor, { 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(); + // Change the task state. + state.on_account_created(account_index); + let addresses = if params.scan { let gap_limit = params.gap_limit.unwrap_or_else(|| hd_wallet.gap_limit()); + let address_scanner = coin.produce_hd_address_scanner().await?; coin.scan_for_new_addresses(hd_wallet, &mut new_account, &address_scanner, gap_limit) .await? } else { @@ -194,4 +395,13 @@ pub(crate) mod common_impl { addresses, }) } + + pub async fn revert_creating_account(coin: &Coin, account_id: u32) + where + Coin: HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync, + { + if let Some(hd_wallet) = coin.derivation_method().hd_wallet() { + hd_wallet.remove_account_if_last(account_id).await; + } + } } diff --git a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs index e2400e8b4a..e90d44eecc 100644 --- a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs +++ b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs @@ -2,12 +2,16 @@ 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 common::{SerdeInfallible, SuccessResponse}; 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::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, + RpcTaskStatusRequest}; use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +pub type ScanAddressesUserAction = SerdeInfallible; +pub type ScanAddressesAwaitingStatus = SerdeInfallible; pub type ScanAddressesTaskManager = RpcTaskManager; pub type ScanAddressesTaskManagerShared = RpcTaskManagerShared; pub type ScanAddressesTaskHandle = RpcTaskHandle; @@ -32,9 +36,11 @@ pub struct ScanAddressesRequest { params: ScanAddressesParams, } -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] pub struct ScanAddressesParams { pub account_index: u32, + // The max number of empty addresses in a row. + // If transactions were sent to an address outside the `gap_limit`, they will not be identified. pub gap_limit: Option, } @@ -43,16 +49,6 @@ 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( @@ -79,10 +75,13 @@ impl RpcTask for InitScanAddressesTask { #[inline] fn initial_status(&self) -> Self::InProgressStatus { ScanAddressesInProgressStatus::InProgress } - async fn run(self, _task_handle: &ScanAddressesTaskHandle) -> Result> { + // Do nothing if the task has been cancelled. + async fn cancel(self) {} + + async fn run(&mut 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, + MmCoinEnum::UtxoCoin(ref utxo) => utxo.init_scan_for_new_addresses_rpc(self.req.params.clone()).await, + MmCoinEnum::QtumCoin(ref qtum) => qtum.init_scan_for_new_addresses_rpc(self.req.params.clone()).await, _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), } } @@ -93,9 +92,10 @@ pub async fn init_scan_for_new_addresses( req: ScanAddressesRequest, ) -> MmResult { let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let spawner = coin.spawner(); 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)?; + let task_id = ScanAddressesTaskManager::spawn_rpc_task(&coins_ctx.scan_addresses_manager, &spawner, task)?; Ok(InitRpcTaskResponse { task_id }) } @@ -113,6 +113,19 @@ pub async fn init_scan_for_new_addresses_status( .or_mm_err(|| RpcTaskStatusError::NoSuchTask(req.task_id)) } +pub async fn cancel_scan_for_new_addresses( + ctx: MmArc, + req: CancelRpcTaskRequest, +) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(CancelRpcTaskError::Internal)?; + let mut task_manager = coins_ctx + .scan_addresses_manager + .lock() + .map_to_mm(|e| CancelRpcTaskError::Internal(e.to_string()))?; + task_manager.cancel_task(req.task_id)?; + Ok(SuccessResponse::new()) +} + pub mod common_impl { use super::*; use crate::coin_balance::HDWalletBalanceOps; diff --git a/mm2src/coins/rpc_command/init_withdraw.rs b/mm2src/coins/rpc_command/init_withdraw.rs index dd3d22ca63..c9ba606250 100644 --- a/mm2src/coins/rpc_command/init_withdraw.rs +++ b/mm2src/coins/rpc_command/init_withdraw.rs @@ -5,7 +5,8 @@ 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::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, + RpcTaskStatusRequest, RpcTaskUserActionError}; use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatusAlias, RpcTaskTypes}; pub type WithdrawAwaitingStatus = HwRpcTaskAwaitingStatus; @@ -23,6 +24,7 @@ pub type WithdrawInitResult = Result>; #[async_trait] pub trait CoinWithdrawInit { + #[allow(clippy::result_large_err)] fn init_withdraw( ctx: MmArc, req: WithdrawRequest, @@ -32,13 +34,14 @@ pub trait CoinWithdrawInit { pub async fn init_withdraw(ctx: MmArc, request: WithdrawRequest) -> WithdrawInitResult { let coin = lp_coinfind_or_err(&ctx, &request.coin).await?; + let spawner = coin.spawner(); 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)?; + let task_id = WithdrawTaskManager::spawn_rpc_task(&coins_ctx.withdraw_task_manager, &spawner, task)?; Ok(InitWithdrawResponse { task_id }) } @@ -65,7 +68,7 @@ pub enum WithdrawInProgressStatus { /// 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, + FollowHwDeviceInstructions, WaitingForUserToConfirmSigning, } @@ -82,6 +85,16 @@ pub async fn withdraw_user_action( Ok(SuccessResponse::new()) } +pub async fn cancel_withdraw(ctx: MmArc, req: CancelRpcTaskRequest) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(CancelRpcTaskError::Internal)?; + let mut task_manager = coins_ctx + .withdraw_task_manager + .lock() + .map_to_mm(|e| CancelRpcTaskError::Internal(e.to_string()))?; + task_manager.cancel_task(req.task_id)?; + Ok(SuccessResponse::new()) +} + #[async_trait] pub trait InitWithdrawCoin { async fn init_withdraw( @@ -110,14 +123,17 @@ impl RpcTaskTypes for WithdrawTask { impl RpcTask for WithdrawTask { fn initial_status(&self) -> Self::InProgressStatus { WithdrawInProgressStatus::Preparing } - async fn run(self, task_handle: &WithdrawTaskHandle) -> Result> { + // Do nothing if the task has been cancelled. + async fn cancel(self) {} + + async fn run(&mut self, task_handle: &WithdrawTaskHandle) -> Result> { + let ctx = self.ctx.clone(); + let request = self.request.clone(); 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, + MmCoinEnum::UtxoCoin(ref standard_utxo) => standard_utxo.init_withdraw(ctx, request, task_handle).await, + MmCoinEnum::QtumCoin(ref qtum) => qtum.init_withdraw(ctx, request, task_handle).await, #[cfg(not(target_arch = "wasm32"))] - MmCoinEnum::ZCoin(ref z) => z.init_withdraw(self.ctx, self.request, task_handle).await, + MmCoinEnum::ZCoin(ref z) => z.init_withdraw(ctx, request, task_handle).await, _ => MmError::err(WithdrawError::CoinDoesntSupportInitWithdraw { coin: self.coin.ticker().to_owned(), }), diff --git a/mm2src/coins/rpc_command/lightning/close_channel.rs b/mm2src/coins/rpc_command/lightning/close_channel.rs new file mode 100644 index 0000000000..a3fa05644c --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/close_channel.rs @@ -0,0 +1,83 @@ +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::{async_blocking, HttpStatusCode}; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type CloseChannelResult = Result>; + +#[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 = "No such channel with rpc_channel_id {}", _0)] + NoSuchChannel(u64), + #[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::NoSuchChannel(_) | CloseChannelError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + CloseChannelError::CloseChannelError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for CloseChannelError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => CloseChannelError::NoSuchCoin(coin), + } + } +} + +#[derive(Deserialize)] +pub struct CloseChannelReq { + pub coin: String, + pub rpc_channel_id: u64, + #[serde(default)] + pub force_close: bool, +} + +pub async fn close_channel(ctx: MmArc, req: CloseChannelReq) -> CloseChannelResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(CloseChannelError::UnsupportedCoin(e.ticker().to_string())), + }; + + let channel_details = ln_coin + .get_channel_by_rpc_id(req.rpc_channel_id) + .await + .ok_or(CloseChannelError::NoSuchChannel(req.rpc_channel_id))?; + let channel_id = channel_details.channel_id; + let counterparty_node_id = channel_details.counterparty.node_id; + + if req.force_close { + async_blocking(move || { + ln_coin + .channel_manager + .force_close_broadcasting_latest_txn(&channel_id, &counterparty_node_id) + .map_to_mm(|e| CloseChannelError::CloseChannelError(format!("{:?}", e))) + }) + .await?; + } else { + async_blocking(move || { + ln_coin + .channel_manager + .close_channel(&channel_id, &counterparty_node_id) + .map_to_mm(|e| CloseChannelError::CloseChannelError(format!("{:?}", e))) + }) + .await?; + } + + Ok(format!( + "Initiated closing of channel with rpc_channel_id: {}", + req.rpc_channel_id + )) +} diff --git a/mm2src/coins/rpc_command/lightning/connect_to_node.rs b/mm2src/coins/rpc_command/lightning/connect_to_node.rs new file mode 100644 index 0000000000..79ba0f1889 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/connect_to_node.rs @@ -0,0 +1,93 @@ +use crate::lightning::ln_errors::EnableLightningError; +use crate::lightning::ln_p2p::{connect_to_ln_node, ConnectToNodeRes, ConnectionError}; +use crate::lightning::ln_serialization::NodeAddress; +use crate::lightning::ln_storage::LightningStorage; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use std::collections::hash_map::Entry; + +type ConnectToNodeResult = Result>; + +#[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::NOT_FOUND, + } + } +} + +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()) } +} + +impl From for ConnectToNodeError { + fn from(err: ConnectionError) -> ConnectToNodeError { ConnectToNodeError::ConnectionError(err.to_string()) } +} + +#[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_node(ctx: MmArc, req: ConnectToNodeRequest) -> ConnectToNodeResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(ConnectToNodeError::UnsupportedCoin(e.ticker().to_string())), + }; + + let node_pubkey = req.node_address.pubkey; + let node_addr = req.node_address.addr; + let res = connect_to_ln_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_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()) +} diff --git a/mm2src/coins/rpc_command/lightning/generate_invoice.rs b/mm2src/coins/rpc_command/lightning/generate_invoice.rs new file mode 100644 index 0000000000..94740793eb --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/generate_invoice.rs @@ -0,0 +1,126 @@ +use crate::lightning::ln_db::{LightningDB, PaymentInfo, PaymentType}; +use crate::lightning::ln_p2p::connect_to_ln_node; +use crate::lightning::DEFAULT_INVOICE_EXPIRY; +use crate::{lp_coinfind_or_err, CoinFindError, H256Json, MmCoinEnum}; +use bitcoin_hashes::Hash; +use common::log::LogOnError; +use common::{async_blocking, HttpStatusCode}; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use lightning::ln::PaymentHash; +use lightning_invoice::utils::create_invoice_from_channelmanager; +use lightning_invoice::{Invoice, SignOrCreationError}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type GenerateInvoiceResult = Result>; + +#[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::NOT_FOUND, + 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(Deserialize)] +pub struct GenerateInvoiceRequest { + pub coin: String, + pub amount_in_msat: Option, + pub description: String, + pub expiry: Option, +} + +#[derive(Serialize)] +pub struct GenerateInvoiceResponse { + payment_hash: H256Json, + invoice: Invoice, +} + +/// 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 ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(GenerateInvoiceError::UnsupportedCoin(e.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_ln_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 channel_manager = ln_coin.channel_manager.clone(); + let keys_manager = ln_coin.keys_manager.clone(); + let amount_in_msat = req.amount_in_msat; + let description = req.description.clone(); + let expiry = req.expiry.unwrap_or(DEFAULT_INVOICE_EXPIRY); + let invoice = async_blocking(move || { + create_invoice_from_channelmanager( + &channel_manager, + keys_manager, + network, + amount_in_msat, + description, + expiry, + ) + }) + .await?; + + let payment_hash = invoice.payment_hash().into_inner(); + let payment_info = PaymentInfo::new( + PaymentHash(payment_hash), + PaymentType::InboundPayment, + req.description, + req.amount_in_msat.map(|a| a as i64), + ); + // Note: Although the preimage can be recreated from the keymanager and the invoice secret, the payment info is added to db at invoice generation stage + // to save the description. Although it's not ideal to keep track of invoices before they are paid since they may never be paid, but this is the only way + // to have the invoice description saved in the db. + ln_coin.db.add_payment_to_db(&payment_info).await?; + + Ok(GenerateInvoiceResponse { + payment_hash: payment_hash.into(), + invoice, + }) +} diff --git a/mm2src/coins/rpc_command/lightning/get_channel_details.rs b/mm2src/coins/rpc_command/lightning/get_channel_details.rs new file mode 100644 index 0000000000..f6c18ae633 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/get_channel_details.rs @@ -0,0 +1,81 @@ +use crate::lightning::ln_db::{DBChannelDetails, LightningDB}; +use crate::lightning::ln_serialization::ChannelDetailsForRPC; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::HttpStatusCode; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type GetChannelDetailsResult = Result>; + +#[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(_) | 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(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(DBChannelDetails), +} + +pub async fn get_channel_details( + ctx: MmArc, + req: GetChannelDetailsRequest, +) -> GetChannelDetailsResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(GetChannelDetailsError::UnsupportedCoin(e.ticker().to_string())), + }; + + let channel_details = match ln_coin.get_channel_by_rpc_id(req.rpc_channel_id).await { + Some(details) => GetChannelDetailsResponse::Open(details.into()), + None => GetChannelDetailsResponse::Closed( + ln_coin + .db + .get_channel_from_db(req.rpc_channel_id) + .await? + .ok_or(GetChannelDetailsError::NoSuchChannel(req.rpc_channel_id))?, + ), + }; + + Ok(channel_details) +} diff --git a/mm2src/coins/rpc_command/lightning/get_claimable_balances.rs b/mm2src/coins/rpc_command/lightning/get_claimable_balances.rs new file mode 100644 index 0000000000..5d1b84dad1 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/get_claimable_balances.rs @@ -0,0 +1,67 @@ +use crate::lightning::ln_serialization::ClaimableBalance; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::{async_blocking, HttpStatusCode}; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type ClaimableBalancesResult = Result>; + +#[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::NOT_FOUND, + } + } +} + +impl From for ClaimableBalancesError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => ClaimableBalancesError::NoSuchCoin(coin), + } + } +} + +#[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 ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(ClaimableBalancesError::UnsupportedCoin(e.ticker().to_string())), + }; + let ignored_channels = if req.include_open_channels_balances { + Vec::new() + } else { + ln_coin.list_channels().await + }; + let claimable_balances = async_blocking(move || { + ln_coin + .chain_monitor + .get_claimable_balances(&ignored_channels.iter().collect::>()[..]) + .into_iter() + .map(From::from) + .collect() + }) + .await; + + Ok(claimable_balances) +} diff --git a/mm2src/coins/rpc_command/lightning/get_payment_details.rs b/mm2src/coins/rpc_command/lightning/get_payment_details.rs new file mode 100644 index 0000000000..0e7a4868c2 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/get_payment_details.rs @@ -0,0 +1,76 @@ +use crate::lightning::ln_db::LightningDB; +use crate::lightning::ln_serialization::PaymentInfoForRPC; +use crate::{lp_coinfind_or_err, CoinFindError, H256Json, MmCoinEnum}; +use common::HttpStatusCode; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use lightning::ln::PaymentHash; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type GetPaymentDetailsResult = Result>; + +#[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::NOT_FOUND, + 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(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 ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(GetPaymentDetailsError::UnsupportedCoin(e.ticker().to_string())), + }; + + if let Some(payment_info) = ln_coin.db.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)) +} diff --git a/mm2src/coins/rpc_command/lightning/list_channels.rs b/mm2src/coins/rpc_command/lightning/list_channels.rs new file mode 100644 index 0000000000..a96106a99f --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/list_channels.rs @@ -0,0 +1,130 @@ +use crate::lightning::ln_db::{ClosedChannelsFilter, DBChannelDetails, LightningDB}; +use crate::lightning::ln_serialization::ChannelDetailsForRPC; +use crate::lightning::OpenChannelsFilter; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum}; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type ListChannelsResult = Result>; + +#[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::NOT_FOUND, + 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(Deserialize)] +pub struct ListOpenChannelsRequest { + pub coin: String, + pub filter: Option, + #[serde(default = "ten")] + limit: usize, + #[serde(default)] + paging_options: PagingOptionsEnum, +} + +#[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 ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(ListChannelsError::UnsupportedCoin(e.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 ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(ListChannelsError::UnsupportedCoin(e.ticker().to_string())), + }; + let closed_channels_res = ln_coin + .db + .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, + }) +} diff --git a/mm2src/coins/rpc_command/lightning/list_payments_by_filter.rs b/mm2src/coins/rpc_command/lightning/list_payments_by_filter.rs new file mode 100644 index 0000000000..0a6d0d3308 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/list_payments_by_filter.rs @@ -0,0 +1,88 @@ +use crate::lightning::ln_db::LightningDB; +use crate::lightning::ln_serialization::{PaymentInfoForRPC, PaymentsFilterForRPC}; +use crate::{lp_coinfind_or_err, CoinFindError, H256Json, MmCoinEnum}; +use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum}; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use lightning::ln::PaymentHash; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type ListPaymentsResult = Result>; + +#[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::NOT_FOUND, + 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(Deserialize)] +pub struct ListPaymentsReq { + pub coin: String, + pub filter: Option, + #[serde(default = "ten")] + limit: usize, + #[serde(default)] + paging_options: PagingOptionsEnum, +} + +#[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 ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(ListPaymentsError::UnsupportedCoin(e.ticker().to_string())), + }; + let get_payments_res = ln_coin + .db + .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, + }) +} diff --git a/mm2src/coins/rpc_command/lightning/mod.rs b/mm2src/coins/rpc_command/lightning/mod.rs new file mode 100644 index 0000000000..73aa00bd08 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/mod.rs @@ -0,0 +1,33 @@ +mod close_channel; +mod connect_to_node; +mod generate_invoice; +mod get_channel_details; +mod get_claimable_balances; +mod get_payment_details; +mod list_channels; +mod list_payments_by_filter; +mod open_channel; +mod send_payment; +mod trusted_nodes; +mod update_channel; + +pub mod channels { + pub use super::close_channel::*; + pub use super::get_channel_details::*; + pub use super::get_claimable_balances::*; + pub use super::list_channels::*; + pub use super::open_channel::*; + pub use super::update_channel::*; +} + +pub mod nodes { + pub use super::connect_to_node::*; + pub use super::trusted_nodes::*; +} + +pub mod payments { + pub use super::generate_invoice::*; + pub use super::get_payment_details::*; + pub use super::list_payments_by_filter::*; + pub use super::send_payment::*; +} diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs new file mode 100644 index 0000000000..9d131b1b48 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -0,0 +1,237 @@ +use crate::lightning::ln_conf::{ChannelOptions, OurChannelsConfigs}; +use crate::lightning::ln_db::{DBChannelDetails, LightningDB}; +use crate::lightning::ln_p2p::{connect_to_ln_node, ConnectionError}; +use crate::lightning::ln_serialization::NodeAddress; +use crate::lightning::ln_storage::LightningStorage; +use crate::utxo::utxo_common::UtxoTxBuilder; +use crate::utxo::{sat_from_big_decimal, FeePolicy, GetUtxoListOps, UtxoTxGenerationOps}; +use crate::{lp_coinfind_or_err, BalanceError, CoinFindError, GenerateTxError, MmCoinEnum, NumConversError, + UnexpectedDerivationMethod, UtxoRpcError}; +use chain::TransactionOutput; +use common::log::error; +use common::{async_blocking, HttpStatusCode}; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use keys::AddressHashEnum; +use lightning::util::config::UserConfig; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::BigDecimal; +use script::Builder; + +type OpenChannelResult = Result>; + +#[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), +} + +impl HttpStatusCode for OpenChannelError { + fn status_code(&self) -> StatusCode { + match self { + OpenChannelError::UnsupportedCoin(_) | OpenChannelError::RpcError(_) => StatusCode::BAD_REQUEST, + OpenChannelError::FailureToOpenChannel(_, _) + | OpenChannelError::ConnectToNodeError(_) + | OpenChannelError::InternalError(_) + | OpenChannelError::GenerateTxErr(_) + | OpenChannelError::IOError(_) + | OpenChannelError::DbError(_) + | OpenChannelError::InvalidPath(_) + | OpenChannelError::BalanceError(_) => StatusCode::INTERNAL_SERVER_ERROR, + OpenChannelError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + } + } +} + +impl From for OpenChannelError { + fn from(err: ConnectionError) -> 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(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(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 channel_configs: 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 ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(OpenChannelError::UnsupportedCoin(e.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_ln_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.single_addr_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_according_to(options), + None => conf.channel_options = Some(options), + } + } + if let Some(configs) = req.channel_configs { + match conf.our_channels_configs.as_mut() { + Some(o) => o.update_according_to(configs), + None => conf.our_channels_configs = Some(configs), + } + } + drop_mutability!(conf); + let user_config: UserConfig = conf.into(); + + let rpc_channel_id = ln_coin.db.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 = DBChannelDetails::new( + rpc_channel_id, + temp_channel_id, + node_pubkey, + true, + user_config.channel_handshake_config.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?; + + if let Err(e) = ln_coin.db.add_channel_to_db(&pending_channel_details).await { + error!("Unable to add new outbound channel {} to db: {}", rpc_channel_id, e); + } + + Ok(OpenChannelResponse { + rpc_channel_id, + node_address: req.node_address, + }) +} diff --git a/mm2src/coins/rpc_command/lightning/send_payment.rs b/mm2src/coins/rpc_command/lightning/send_payment.rs new file mode 100644 index 0000000000..3efde180d4 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/send_payment.rs @@ -0,0 +1,113 @@ +use crate::lightning::ln_p2p::connect_to_ln_node; +use crate::lightning::ln_serialization::PublicKeyForRPC; +use crate::lightning::ln_utils::PaymentError; +use crate::{lp_coinfind_or_err, CoinFindError, H256Json, MmCoinEnum}; +use common::log::LogOnError; +use common::HttpStatusCode; +use db_common::sqlite::rusqlite::Error as SqlError; +use http::StatusCode; +use lightning_invoice::Invoice; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type SendPaymentResult = Result>; + +#[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 = "DB error {}", _0)] + DbError(String), +} + +impl HttpStatusCode for SendPaymentError { + fn status_code(&self) -> StatusCode { + match self { + SendPaymentError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + SendPaymentError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + SendPaymentError::PaymentError(_) | SendPaymentError::NoRouteFound(_) | 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()) } +} + +impl From for SendPaymentError { + fn from(err: PaymentError) -> SendPaymentError { SendPaymentError::PaymentError(err.to_string()) } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum Payment { + #[serde(rename = "invoice")] + Invoice { invoice: Invoice }, + #[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 ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(SendPaymentError::UnsupportedCoin(e.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_ln_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, None).await?, + Payment::Keysend { + destination, + amount_in_msat, + expiry, + } => ln_coin.keysend(destination.into(), amount_in_msat, expiry).await?, + }; + + Ok(SendPaymentResponse { + payment_hash: payment_info.payment_hash.0.into(), + }) +} diff --git a/mm2src/coins/rpc_command/lightning/trusted_nodes.rs b/mm2src/coins/rpc_command/lightning/trusted_nodes.rs new file mode 100644 index 0000000000..6f0f2fd36e --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/trusted_nodes.rs @@ -0,0 +1,120 @@ +use crate::lightning::ln_serialization::PublicKeyForRPC; +use crate::lightning::ln_storage::LightningStorage; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type TrustedNodeResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum TrustedNodeError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "I/O error {}", _0)] + IOError(String), +} + +impl HttpStatusCode for TrustedNodeError { + fn status_code(&self) -> StatusCode { + match self { + TrustedNodeError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + TrustedNodeError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + TrustedNodeError::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for TrustedNodeError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => TrustedNodeError::NoSuchCoin(coin), + } + } +} + +impl From for TrustedNodeError { + fn from(err: std::io::Error) -> TrustedNodeError { TrustedNodeError::IOError(err.to_string()) } +} + +#[derive(Deserialize)] +pub struct AddTrustedNodeReq { + pub coin: String, + pub node_id: PublicKeyForRPC, +} + +#[derive(Serialize)] +pub struct AddTrustedNodeResponse { + pub added_node: PublicKeyForRPC, +} + +pub async fn add_trusted_node(ctx: MmArc, req: AddTrustedNodeReq) -> TrustedNodeResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), + }; + + if ln_coin.trusted_nodes.lock().insert(req.node_id.clone().into()) { + ln_coin.persister.save_trusted_nodes(ln_coin.trusted_nodes).await?; + } + + Ok(AddTrustedNodeResponse { + added_node: req.node_id, + }) +} + +#[derive(Deserialize)] +pub struct RemoveTrustedNodeReq { + pub coin: String, + pub node_id: PublicKeyForRPC, +} + +#[derive(Serialize)] +pub struct RemoveTrustedNodeResponse { + pub removed_node: PublicKeyForRPC, +} + +pub async fn remove_trusted_node( + ctx: MmArc, + req: RemoveTrustedNodeReq, +) -> TrustedNodeResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), + }; + + if ln_coin.trusted_nodes.lock().remove(&req.node_id.clone().into()) { + ln_coin.persister.save_trusted_nodes(ln_coin.trusted_nodes).await?; + } + + Ok(RemoveTrustedNodeResponse { + removed_node: req.node_id, + }) +} + +#[derive(Deserialize)] +pub struct ListTrustedNodesReq { + pub coin: String, +} + +#[derive(Serialize)] +pub struct ListTrustedNodesResponse { + trusted_nodes: Vec, +} + +pub async fn list_trusted_nodes(ctx: MmArc, req: ListTrustedNodesReq) -> TrustedNodeResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), + }; + + let trusted_nodes = ln_coin.trusted_nodes.lock().clone(); + + Ok(ListTrustedNodesResponse { + trusted_nodes: trusted_nodes.into_iter().map(PublicKeyForRPC).collect(), + }) +} diff --git a/mm2src/coins/rpc_command/lightning/update_channel.rs b/mm2src/coins/rpc_command/lightning/update_channel.rs new file mode 100644 index 0000000000..a108659ea0 --- /dev/null +++ b/mm2src/coins/rpc_command/lightning/update_channel.rs @@ -0,0 +1,83 @@ +use crate::lightning::ln_conf::ChannelOptions; +use crate::{lp_coinfind_or_err, CoinFindError, MmCoinEnum}; +use common::{async_blocking, HttpStatusCode}; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +type UpdateChannelResult = Result>; + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum UpdateChannelError { + #[display(fmt = "Lightning network is not supported for {}", _0)] + UnsupportedCoin(String), + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display(fmt = "No such channel with rpc_channel_id {}", _0)] + NoSuchChannel(u64), + #[display(fmt = "Failure to channel {}: {}", _0, _1)] + FailureToUpdateChannel(u64, String), +} + +impl HttpStatusCode for UpdateChannelError { + fn status_code(&self) -> StatusCode { + match self { + UpdateChannelError::UnsupportedCoin(_) => StatusCode::BAD_REQUEST, + UpdateChannelError::NoSuchChannel(_) | UpdateChannelError::NoSuchCoin(_) => StatusCode::NOT_FOUND, + UpdateChannelError::FailureToUpdateChannel(_, _) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for UpdateChannelError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => UpdateChannelError::NoSuchCoin(coin), + } + } +} + +#[derive(Deserialize)] +pub struct UpdateChannelReq { + pub coin: String, + pub rpc_channel_id: u64, + pub channel_options: ChannelOptions, +} + +#[derive(Serialize)] +pub struct UpdateChannelResponse { + channel_options: ChannelOptions, +} + +/// Updates configuration for an open channel. +pub async fn update_channel(ctx: MmArc, req: UpdateChannelReq) -> UpdateChannelResult { + let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await? { + MmCoinEnum::LightningCoin(c) => c, + e => return MmError::err(UpdateChannelError::UnsupportedCoin(e.ticker().to_string())), + }; + + let channel_details = ln_coin + .get_channel_by_rpc_id(req.rpc_channel_id) + .await + .ok_or(UpdateChannelError::NoSuchChannel(req.rpc_channel_id))?; + + async_blocking(move || { + let mut channel_options = ln_coin + .conf + .channel_options + .unwrap_or_else(|| req.channel_options.clone()); + if channel_options != req.channel_options { + channel_options.update_according_to(req.channel_options.clone()); + } + drop_mutability!(channel_options); + let channel_ids = &[channel_details.channel_id]; + let counterparty_node_id = channel_details.counterparty.node_id; + ln_coin + .channel_manager + .update_channel_config(&counterparty_node_id, channel_ids, &channel_options.clone().into()) + .map_to_mm(|e| UpdateChannelError::FailureToUpdateChannel(req.rpc_channel_id, format!("{:?}", e)))?; + Ok(UpdateChannelResponse { channel_options }) + }) + .await +} diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs index 7a98d3fc2e..945cea77fe 100644 --- a/mm2src/coins/rpc_command/mod.rs +++ b/mm2src/coins/rpc_command/mod.rs @@ -1,5 +1,10 @@ pub mod account_balance; +pub mod get_current_mtp; +pub mod get_enabled_coins; +pub mod get_new_address; pub mod hd_account_balance_rpc_error; +pub mod init_account_balance; pub mod init_create_account; pub mod init_scan_for_new_addresses; pub mod init_withdraw; +#[cfg(not(target_arch = "wasm32"))] pub mod lightning; diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs index 6bf3c7049c..cd92a1c30f 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -1,15 +1,25 @@ -use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum}; +use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum, WatcherOps}; +use crate::coin_errors::MyAddressError; 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, +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, + MakerSwapTakerCoin, NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, + PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RefundError, + RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, + SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, + SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, + SignatureResult, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, + TransactionDetails, TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use base58::ToBase58; use bincode::{deserialize, serialize}; +use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}; use common::{async_blocking, now_ms}; +use crypto::StandardHDPathToCoin; use derive_more::Display; use futures::{FutureExt, TryFutureExt}; use futures01::Future; @@ -31,17 +41,18 @@ use solana_sdk::{pubkey::Pubkey, 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}; +use std::{convert::TryFrom, fmt::Debug, 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; + +#[cfg(all(test, not(feature = "disable-solana-tests")))] +mod solana_common_tests; +#[cfg(all(test, not(feature = "disable-solana-tests")))] +mod solana_tests; +#[cfg(all(test, not(feature = "disable-solana-tests")))] +mod spl_tests; pub const SOLANA_DEFAULT_DECIMALS: u64 = 9; pub const LAMPORTS_DUMMY_AMOUNT: u64 = 10; @@ -163,19 +174,35 @@ fn generate_keypair_from_slice(priv_key: &[u8]) -> Result 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 priv_key = match priv_key_policy { + PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => priv_key, + PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { + let derivation_path: StandardHDPathToCoin = try_s!(json::from_value(conf["derivation_path"].clone())); + try_s!(global_hd.derive_secp256k1_secret(&derivation_path)) + }, + PrivKeyBuildPolicy::Trezor => return ERR!("{}", PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), + }; + + let key_pair = try_s!(generate_keypair_from_slice(priv_key.as_slice())); let my_address = key_pair.pubkey().to_string(); let spl_tokens_infos = Arc::new(Mutex::new(HashMap::new())); + + // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, + // all spawned futures related to `SolanaCoin` will be aborted as well. + let abortable_system: AbortableQueue = try_s!(ctx.abortable_system.create_subsystem()); + let solana_coin = SolanaCoin(Arc::new(SolanaCoinImpl { my_address, key_pair, @@ -183,6 +210,7 @@ pub async fn solana_coin_from_conf_and_params( client, decimals, spl_tokens_infos, + abortable_system, })); Ok(solana_coin) } @@ -195,17 +223,16 @@ pub struct SolanaCoinImpl { decimals: u8, my_address: String, spl_tokens_infos: Arc>>, + /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation + /// and on [`MmArc::stop`]. + pub abortable_system: AbortableQueue, } -impl Debug for SolanaCoinImpl { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.write_str(&*self.ticker) } -} - -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct SolanaCoin(Arc); impl Deref for SolanaCoin { type Target = SolanaCoinImpl; - fn deref(&self) -> &SolanaCoinImpl { &*self.0 } + fn deref(&self) -> &SolanaCoinImpl { &self.0 } } #[async_trait] @@ -265,11 +292,12 @@ async fn withdraw_base_coin_impl(coin: SolanaCoin, req: WithdrawRequest) -> With internal_id: vec![].into(), kmd_rewards: None, transaction_type: TransactionType::StandardTransfer, + memo: None, }) } async fn withdraw_impl(coin: SolanaCoin, req: WithdrawRequest) -> WithdrawResult { - let validate_address_result = coin.validate_address(&*req.to); + 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()), @@ -315,14 +343,14 @@ impl SolanaCoin { }); } let actual_token_pubkey = - Pubkey::from_str(&*token_accounts[0].pubkey).map_err(|e| BalanceError::Internal(format!("{:?}", e)))?; + 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()))?; + BigDecimal::from_str(&amount.ui_amount_string).map_to_mm(|e| BalanceError::Internal(e.to_string()))?; Ok(CoinBalance { spendable: balance, unspendable: Default::default(), @@ -343,6 +371,9 @@ impl SolanaCoin { self.spl_tokens_infos.lock().unwrap().insert(ticker, info); } + /// WARNING + /// Be very careful using this function since it returns dereferenced clone + /// of value behind the MutexGuard and makes it non-thread-safe. pub fn get_spl_tokens_infos(&self) -> HashMap { let guard = self.spl_tokens_infos.lock().unwrap(); (*guard).clone() @@ -352,7 +383,7 @@ impl SolanaCoin { impl MarketCoinOps for SolanaCoin { fn ticker(&self) -> &str { &self.ticker } - fn my_address(&self) -> Result { Ok(self.my_address.clone()) } + fn my_address(&self) -> MmResult { Ok(self.my_address.clone()) } fn get_public_key(&self) -> Result> { unimplemented!() } @@ -423,17 +454,23 @@ impl MarketCoinOps for SolanaCoin { unimplemented!() } - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, _transaction: &[u8], + _secret_hash: &[u8], _wait_until: u64, _from_block: u64, _swap_contract_address: &Option, + _check_every: f64, ) -> TransactionFut { unimplemented!() } - fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result { unimplemented!() } + fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result> { + MmError::err(TxMarshalingErr::NotSupported( + "tx_enum_from_bytes is not supported for Solana yet.".to_string(), + )) + } fn current_block(&self) -> Box + Send> { let coin = self.clone(); @@ -448,111 +485,47 @@ impl MarketCoinOps for SolanaCoin { 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_maker_payment(&self, _maker_payment_args: SendMakerPaymentArgs) -> 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_taker_payment(&self, _taker_payment_args: SendTakerPaymentArgs) -> 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], + _maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, ) -> 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], + _taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, ) -> 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 { + fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> 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 { + fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> 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> { + fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> Box + Send> { unimplemented!() } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { - unimplemented!() - } + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { - unimplemented!() - } + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { 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], + _if_my_payment_sent_args: CheckIfMyPaymentSentArgs, ) -> Box, Error = String> + Send> { unimplemented!() } @@ -571,7 +544,19 @@ impl SwapOps for SolanaCoin { unimplemented!() } - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } fn negotiate_swap_contract_addr( &self, @@ -580,20 +565,135 @@ impl SwapOps for SolanaCoin { unimplemented!() } + #[inline] fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { todo!() } + + #[inline] + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + } + + fn validate_other_pubkey(&self, _raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + unimplemented!() + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + unimplemented!() + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + unimplemented!() + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + unimplemented!() + } +} + +#[async_trait] +impl TakerSwapMakerCoin for SolanaCoin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for SolanaCoin { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for SolanaCoin { + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &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_taker_payment_refund_preimage( + &self, + _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + async fn watcher_search_for_swap_tx_spend( + &self, + input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } } -#[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 spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + 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 get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { unimplemented!() } + fn decimals(&self) -> u8 { self.decimals } fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { unimplemented!() } @@ -636,18 +736,20 @@ impl MmCoin for SolanaCoin { async fn get_sender_trade_fee( &self, - value: TradePreimageValue, - stage: FeeApproxStage, + _value: TradePreimageValue, + _stage: FeeApproxStage, ) -> TradePreimageResult { unimplemented!() } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { + unimplemented!() + } async fn get_fee_to_send_taker_fee( &self, - dex_fee_amount: BigDecimal, - stage: FeeApproxStage, + _dex_fee_amount: BigDecimal, + _stage: FeeApproxStage, ) -> TradePreimageResult { unimplemented!() } @@ -662,9 +764,15 @@ impl MmCoin for SolanaCoin { fn swap_contract_address(&self) -> Option { unimplemented!() } + fn fallback_swap_contract(&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 } + + fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.abortable_system) } + + fn on_token_deactivated(&self, _ticker: &str) {} } diff --git a/mm2src/coins/solana/solana_common_tests.rs b/mm2src/coins/solana/solana_common_tests.rs index 9f5eebddf7..6a332a462b 100644 --- a/mm2src/coins/solana/solana_common_tests.rs +++ b/mm2src/coins/solana/solana_common_tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solana::spl::{SplToken, SplTokenConf}; +use crate::solana::spl::{SplToken, SplTokenFields}; use bip39::Language; use crypto::privkey::key_pair_from_seed; use ed25519_dalek_bip32::{DerivationPath, ExtendedSecretKey}; @@ -60,10 +60,11 @@ pub fn spl_coin_for_test( token_contract_address: Pubkey, ) -> SplToken { let spl_coin = SplToken { - conf: Arc::new(SplTokenConf { + conf: Arc::new(SplTokenFields { decimals, ticker, token_contract_address, + abortable_system: AbortableQueue::default(), }), platform_coin: solana_coin, }; @@ -85,6 +86,7 @@ pub fn solana_coin_for_test(seed: String, net_type: SolanaNet) -> (MmArc, Solana 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 spawner = AbortableQueue::default(); let solana_coin = SolanaCoin(Arc::new(SolanaCoinImpl { decimals, @@ -93,6 +95,7 @@ pub fn solana_coin_for_test(seed: String, net_type: SolanaNet) -> (MmArc, Solana ticker, client, spl_tokens_infos, + abortable_system: spawner, })); (ctx, solana_coin) } diff --git a/mm2src/coins/solana/solana_decode_tx_helpers.rs b/mm2src/coins/solana/solana_decode_tx_helpers.rs index 1f089de2df..2ac0876809 100644 --- a/mm2src/coins/solana/solana_decode_tx_helpers.rs +++ b/mm2src/coins/solana/solana_decode_tx_helpers.rs @@ -69,6 +69,7 @@ impl SolanaConfirmedTransaction { internal_id: Default::default(), kmd_rewards: None, transaction_type: TransactionType::StandardTransfer, + memo: None, }; transactions.push(tx); } diff --git a/mm2src/coins/solana/solana_tests.rs b/mm2src/coins/solana/solana_tests.rs index 62f49f8173..5004d05dd8 100644 --- a/mm2src/coins/solana/solana_tests.rs +++ b/mm2src/coins/solana/solana_tests.rs @@ -28,6 +28,7 @@ fn solana_keypair_from_secp() { } // Research tests +// TODO remove `ignore` attribute once the test is stable. #[test] #[ignore] #[cfg(not(target_arch = "wasm32"))] @@ -35,13 +36,13 @@ 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(), + "hood vacant left trim hard mushroom device flavor ask better arrest again".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"); + assert_eq!(public_address, "4rmosKwMH7zeaXGbej1PFybZBUyuUNQLf8RfyzCcYvkx"); + assert_eq!(priv_key, "CZtxt17aTfDrJrzwBWdVqcmFwVVptW8EX7RRnth9tT3M"); 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); diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs index 8447d10b28..05b4e76566 100644 --- a/mm2src/coins/solana/spl.rs +++ b/mm2src/coins/solana/spl.rs @@ -1,13 +1,22 @@ -use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum}; +use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum, WatcherOps}; +use crate::coin_errors::MyAddressError; 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 crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, + MakerSwapTakerCoin, NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, + RawTransactionFut, RawTransactionRequest, RefundError, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, + SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, + SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, SignatureResult, SolanaCoin, + TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, + TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, + ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest, + WithdrawResult}; use async_trait::async_trait; use bincode::serialize; +use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}; use common::{async_blocking, now_ms}; use futures::{FutureExt, TryFutureExt}; use futures01::Future; @@ -30,13 +39,18 @@ use std::{convert::TryFrom, #[derive(Debug)] pub enum SplTokenCreationError { InvalidPubkey(String), + Internal(String), } -#[derive(Debug)] -pub struct SplTokenConf { +impl From for SplTokenCreationError { + fn from(e: AbortedError) -> Self { SplTokenCreationError::Internal(e.to_string()) } +} + +pub struct SplTokenFields { pub decimals: u8, pub ticker: String, pub token_contract_address: Pubkey, + pub abortable_system: AbortableQueue, } #[derive(Clone, Debug)] @@ -54,12 +68,12 @@ pub struct SplProtocolConf { #[derive(Clone)] pub struct SplToken { - pub conf: Arc, + pub conf: Arc, pub platform_coin: SolanaCoin, } impl Debug for SplToken { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.write_str(&*self.conf.ticker) } + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.write_str(&self.conf.ticker) } } impl SplToken { @@ -68,13 +82,19 @@ impl SplToken { ticker: String, token_address: String, platform_coin: SolanaCoin, - ) -> Result> { + ) -> MmResult { 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 { + + // Create an abortable system linked to the `platform_coin` so if the platform coin is disabled, + // all spawned futures related to `SplToken` will be aborted as well. + let abortable_system = platform_coin.abortable_system.create_subsystem()?; + + let conf = Arc::new(SplTokenFields { decimals, ticker, token_contract_address, + abortable_system, }); Ok(SplToken { conf, platform_coin }) } @@ -149,11 +169,12 @@ async fn withdraw_spl_token_impl(coin: SplToken, req: WithdrawRequest) -> Withdr internal_id: vec![].into(), kmd_rewards: None, transaction_type: TransactionType::StandardTransfer, + memo: None, }) } async fn withdraw_impl(coin: SplToken, req: WithdrawRequest) -> WithdrawResult { - let validate_address_result = coin.validate_address(&*req.to); + 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()), @@ -193,7 +214,7 @@ impl SplToken { if token_accounts.is_empty() { return MmError::err(AccountError::NotFundedError("account_not_funded".to_string())); } - Ok(Pubkey::from_str(&*token_accounts[0].pubkey)?) + Ok(Pubkey::from_str(&token_accounts[0].pubkey)?) } fn my_balance_impl(&self) -> BalanceFut { @@ -213,7 +234,7 @@ impl SplToken { impl MarketCoinOps for SplToken { fn ticker(&self) -> &str { &self.conf.ticker } - fn my_address(&self) -> Result { Ok(self.platform_coin.my_address.clone()) } + fn my_address(&self) -> MmResult { Ok(self.platform_coin.my_address.clone()) } fn get_public_key(&self) -> Result> { unimplemented!() } @@ -257,17 +278,23 @@ impl MarketCoinOps for SplToken { unimplemented!() } - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, _transaction: &[u8], + _secret_hash: &[u8], _wait_until: u64, _from_block: u64, _swap_contract_address: &Option, + check_every: f64, ) -> TransactionFut { unimplemented!() } - fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result { unimplemented!() } + fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result> { + MmError::err(TxMarshalingErr::NotSupported( + "tx_enum_from_bytes is not supported for Spl yet.".to_string(), + )) + } fn current_block(&self) -> Box + Send> { self.platform_coin.current_block() } @@ -278,111 +305,47 @@ impl MarketCoinOps for SplToken { 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_maker_payment(&self, _maker_payment_args: SendMakerPaymentArgs) -> 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_taker_payment(&self, _taker_payment_args: SendTakerPaymentArgs) -> 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], + _maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, ) -> 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], + _taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, ) -> 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 { + fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> 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 { + fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> 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> { + fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> Box + Send> { unimplemented!() } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { - unimplemented!() - } + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { - unimplemented!() - } + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { 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], + _if_my_payment_sent_args: CheckIfMyPaymentSentArgs, ) -> Box, Error = String> + Send> { unimplemented!() } @@ -401,7 +364,19 @@ impl SwapOps for SplToken { unimplemented!() } - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } fn negotiate_swap_contract_addr( &self, @@ -410,20 +385,135 @@ impl SwapOps for SplToken { unimplemented!() } + #[inline] fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { todo!() } + + #[inline] + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + } + + fn validate_other_pubkey(&self, _raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + unimplemented!() + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + unimplemented!() + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + unimplemented!() + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + unimplemented!() + } +} + +#[async_trait] +impl TakerSwapMakerCoin for SplToken { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for SplToken { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for SplToken { + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &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 create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_taker_payment_refund_preimage( + &self, + _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + async fn watcher_search_for_swap_tx_spend( + &self, + input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } } -#[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 spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.conf.abortable_system) } + 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 get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { unimplemented!() } + fn decimals(&self) -> u8 { self.conf.decimals } fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { unimplemented!() } @@ -445,7 +535,9 @@ impl MmCoin for SplToken { unimplemented!() } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { + unimplemented!() + } async fn get_fee_to_send_taker_fee( &self, @@ -465,9 +557,15 @@ impl MmCoin for SplToken { fn swap_contract_address(&self) -> Option { unimplemented!() } + fn fallback_swap_contract(&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 } + + fn on_disabled(&self) -> Result<(), AbortedError> { self.conf.abortable_system.abort_all() } + + fn on_token_deactivated(&self, _ticker: &str) {} } diff --git a/mm2src/coins/solana/spl_tests.rs b/mm2src/coins/solana/spl_tests.rs index c1c45de5c6..22af85ef5c 100644 --- a/mm2src/coins/solana/spl_tests.rs +++ b/mm2src/coins/solana/spl_tests.rs @@ -89,6 +89,7 @@ fn spl_my_balance() { assert_eq!(res.spendable, BigDecimal::from(0)); } +// Stop ignoring when Solana is released #[test] #[ignore] #[cfg(not(target_arch = "wasm32"))] diff --git a/mm2src/coins/tendermint/iris/htlc.rs b/mm2src/coins/tendermint/iris/htlc.rs new file mode 100644 index 0000000000..226c1794fc --- /dev/null +++ b/mm2src/coins/tendermint/iris/htlc.rs @@ -0,0 +1,176 @@ +// IRIS HTLC implementation in Rust on top of Cosmos SDK(cosmrs) for AtomicDEX. +// +// This module includes HTLC creating & claiming representation structstures +// and their trait implementations. +// +// ** Acquiring testnet assets ** +// +// Since there is no sdk exists for Rust on Iris Network, we should +// either implement some of the Iris Network funcionality on Rust or +// simply use their unit tests. +// +// Because we had limited time for the HTLC implementation, for now +// we can use their unit tests in order to acquire IBC assets. +// For that, clone https://github.com/ozkanonur/irishub-sdk-js repository and check +// dummy.test.ts file(change the asset, amount, target address if needed) +// and then run the following commands: +// - yarn +// - npm run test +// +// If the sender address doesn't have enough nyan tokens to complete unit tests, +// check this page https://www.irisnet.org/docs/get-started/testnet.html#faucet + +use super::htlc_proto::{ClaimHtlcProtoRep, CreateHtlcProtoRep}; + +use crate::tendermint::type_urls::{CLAIM_HTLC_TYPE_URL, CREATE_HTLC_TYPE_URL}; +use cosmrs::{tx::{Msg, MsgProto}, + AccountId, Coin, ErrorReport}; +use std::convert::TryFrom; + +// https://github.com/irisnet/irismod/blob/043e058cd6e17f4f96d32f17bfd20b67debfab0b/proto/htlc/htlc.proto#L36 +pub const HTLC_STATE_OPEN: i32 = 0; +pub const HTLC_STATE_COMPLETED: i32 = 1; +pub const HTLC_STATE_REFUNDED: i32 = 2; + +#[allow(dead_code)] +pub(crate) struct IrisHtlc { + /// Generated HTLC's ID. + pub(crate) id: String, + + /// Message payload to be sent + pub(crate) msg_payload: cosmrs::Any, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct MsgCreateHtlc { + /// Sender's address. + pub(crate) to: AccountId, + + /// Recipient's address. + pub(crate) sender: AccountId, + + /// The claim receiving address on the other chain. + pub(crate) receiver_on_other_chain: String, + + /// The counterparty creator address on the other chain. + pub(crate) sender_on_other_chain: String, + + /// Amount to send. + pub(crate) amount: Vec, + + /// The sha256 hash generated from secret and timestamp. + pub(crate) hash_lock: String, + + /// The number of blocks to wait before the asset may be returned to. + pub(crate) time_lock: u64, + + /// The timestamp in seconds for generating hash lock if provided. + pub(crate) timestamp: u64, + + /// Whether it is an HTLT transaction. + pub(crate) transfer: bool, +} + +impl Msg for MsgCreateHtlc { + type Proto = CreateHtlcProtoRep; +} + +impl TryFrom for MsgCreateHtlc { + type Error = ErrorReport; + + fn try_from(proto: CreateHtlcProtoRep) -> Result { MsgCreateHtlc::try_from(&proto) } +} + +impl TryFrom<&CreateHtlcProtoRep> for MsgCreateHtlc { + type Error = ErrorReport; + + fn try_from(proto: &CreateHtlcProtoRep) -> Result { + Ok(MsgCreateHtlc { + sender: proto.sender.parse()?, + to: proto.to.parse()?, + amount: proto.amount.iter().map(TryFrom::try_from).collect::>()?, + receiver_on_other_chain: proto.receiver_on_other_chain.clone(), + sender_on_other_chain: proto.sender_on_other_chain.clone(), + hash_lock: proto.hash_lock.clone(), + timestamp: proto.timestamp, + time_lock: proto.time_lock, + transfer: proto.transfer, + }) + } +} + +impl From for CreateHtlcProtoRep { + fn from(coin: MsgCreateHtlc) -> CreateHtlcProtoRep { CreateHtlcProtoRep::from(&coin) } +} + +impl From<&MsgCreateHtlc> for CreateHtlcProtoRep { + fn from(msg: &MsgCreateHtlc) -> CreateHtlcProtoRep { + CreateHtlcProtoRep { + sender: msg.sender.to_string(), + to: msg.to.to_string(), + amount: msg.amount.iter().map(Into::into).collect(), + receiver_on_other_chain: msg.receiver_on_other_chain.clone(), + sender_on_other_chain: msg.sender_on_other_chain.clone(), + hash_lock: msg.hash_lock.clone(), + timestamp: msg.timestamp, + time_lock: msg.time_lock, + transfer: msg.transfer, + } + } +} + +impl MsgProto for CreateHtlcProtoRep { + const TYPE_URL: &'static str = CREATE_HTLC_TYPE_URL; +} + +#[derive(Clone)] +pub(crate) struct MsgClaimHtlc { + /// Sender's address. + pub(crate) sender: AccountId, + + /// Generated HTLC ID + pub(crate) id: String, + + /// Secret that has been used for generating hash_lock + pub(crate) secret: String, +} + +impl Msg for MsgClaimHtlc { + type Proto = ClaimHtlcProtoRep; +} + +impl TryFrom for MsgClaimHtlc { + type Error = ErrorReport; + + fn try_from(proto: ClaimHtlcProtoRep) -> Result { MsgClaimHtlc::try_from(&proto) } +} + +impl TryFrom<&ClaimHtlcProtoRep> for MsgClaimHtlc { + type Error = ErrorReport; + + fn try_from(proto: &ClaimHtlcProtoRep) -> Result { + Ok(MsgClaimHtlc { + sender: proto.sender.parse()?, + id: proto.id.clone(), + secret: proto.secret.clone(), + }) + } +} + +impl From for ClaimHtlcProtoRep { + fn from(coin: MsgClaimHtlc) -> ClaimHtlcProtoRep { ClaimHtlcProtoRep::from(&coin) } +} + +impl From<&MsgClaimHtlc> for ClaimHtlcProtoRep { + fn from(msg: &MsgClaimHtlc) -> ClaimHtlcProtoRep { + ClaimHtlcProtoRep { + sender: msg.sender.to_string(), + id: msg.id.clone(), + secret: msg.secret.clone(), + } + } +} + +impl MsgProto for ClaimHtlcProtoRep { + const TYPE_URL: &'static str = CLAIM_HTLC_TYPE_URL; +} diff --git a/mm2src/coins/tendermint/iris/htlc_proto.rs b/mm2src/coins/tendermint/iris/htlc_proto.rs new file mode 100644 index 0000000000..7bf9b5281b --- /dev/null +++ b/mm2src/coins/tendermint/iris/htlc_proto.rs @@ -0,0 +1,81 @@ +#[derive(prost::Message)] +pub(crate) struct CreateHtlcProtoRep { + #[prost(string, tag = "1")] + pub(crate) sender: prost::alloc::string::String, + #[prost(string, tag = "2")] + pub(crate) to: prost::alloc::string::String, + #[prost(string, tag = "3")] + pub(crate) receiver_on_other_chain: prost::alloc::string::String, + #[prost(string, tag = "4")] + pub(crate) sender_on_other_chain: prost::alloc::string::String, + #[prost(message, repeated, tag = "5")] + pub(crate) amount: prost::alloc::vec::Vec, + #[prost(string, tag = "6")] + pub(crate) hash_lock: prost::alloc::string::String, + #[prost(uint64, tag = "7")] + pub(crate) timestamp: u64, + #[prost(uint64, tag = "8")] + pub(crate) time_lock: u64, + #[prost(bool, tag = "9")] + pub(crate) transfer: bool, +} + +#[derive(prost::Message)] +pub(crate) struct ClaimHtlcProtoRep { + #[prost(string, tag = "1")] + pub(crate) sender: prost::alloc::string::String, + #[prost(string, tag = "2")] + pub(crate) id: prost::alloc::string::String, + #[prost(string, tag = "3")] + pub(crate) secret: prost::alloc::string::String, +} + +#[derive(prost::Message)] +pub(crate) struct QueryHtlcRequestProto { + #[prost(string, tag = "1")] + pub(crate) id: prost::alloc::string::String, +} + +#[derive(prost::Enumeration, Debug)] +#[repr(i32)] +pub enum HtlcState { + Open = 0, + Completed = 1, + Refunded = 2, +} + +#[derive(prost::Message)] +pub struct HtlcProto { + #[prost(string, tag = "1")] + pub(crate) id: prost::alloc::string::String, + #[prost(string, tag = "2")] + pub(crate) sender: prost::alloc::string::String, + #[prost(string, tag = "3")] + pub(crate) to: prost::alloc::string::String, + #[prost(string, tag = "4")] + pub(crate) receiver_on_other_chain: prost::alloc::string::String, + #[prost(string, tag = "5")] + pub(crate) sender_on_other_chain: prost::alloc::string::String, + #[prost(message, repeated, tag = "6")] + pub(crate) amount: prost::alloc::vec::Vec, + #[prost(string, tag = "7")] + pub(crate) hash_lock: prost::alloc::string::String, + #[prost(string, tag = "8")] + pub(crate) secret: prost::alloc::string::String, + #[prost(uint64, tag = "9")] + pub(crate) timestamp: u64, + #[prost(uint64, tag = "10")] + pub(crate) expiration_height: u64, + #[prost(enumeration = "HtlcState", tag = "11")] + pub(crate) state: i32, + #[prost(uint64, tag = "12")] + pub(crate) closed_block: u64, + #[prost(bool, tag = "13")] + pub(crate) transfer: bool, +} + +#[derive(prost::Message)] +pub(crate) struct QueryHtlcResponseProto { + #[prost(message, tag = "1")] + pub(crate) htlc: Option, +} diff --git a/mm2src/coins/tendermint/iris/mod.rs b/mm2src/coins/tendermint/iris/mod.rs new file mode 100644 index 0000000000..00c493c504 --- /dev/null +++ b/mm2src/coins/tendermint/iris/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod htlc; +pub(crate) mod htlc_proto; diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs new file mode 100644 index 0000000000..6e904c4567 --- /dev/null +++ b/mm2src/coins/tendermint/mod.rs @@ -0,0 +1,30 @@ +// Module implementing Tendermint (Cosmos) integration +// Useful resources +// https://docs.cosmos.network/ + +mod iris; +mod rpc; +mod tendermint_coin; +mod tendermint_token; +pub mod tendermint_tx_history_v2; + +pub use tendermint_coin::*; +pub use tendermint_token::*; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum CustomTendermintMsgType { + /// Create HTLC as sender + SendHtlcAmount, + /// Claim HTLC as reciever + ClaimHtlcAmount, + /// Claim HTLC for reciever + SignClaimHtlc, +} + +pub(crate) const TENDERMINT_COIN_PROTOCOL_TYPE: &str = "TENDERMINT"; +pub(crate) const TENDERMINT_ASSET_PROTOCOL_TYPE: &str = "TENDERMINTTOKEN"; + +pub(crate) mod type_urls { + pub(crate) const CREATE_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgCreateHTLC"; + pub(crate) const CLAIM_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgClaimHTLC"; +} diff --git a/mm2src/coins/tendermint/rpc/mod.rs b/mm2src/coins/tendermint/rpc/mod.rs new file mode 100644 index 0000000000..bd34834ce9 --- /dev/null +++ b/mm2src/coins/tendermint/rpc/mod.rs @@ -0,0 +1,23 @@ +#[cfg(not(target_arch = "wasm32"))] mod tendermint_native_rpc; +#[cfg(not(target_arch = "wasm32"))] +pub use tendermint_native_rpc::*; + +#[cfg(target_arch = "wasm32")] mod tendermint_wasm_rpc; +#[cfg(target_arch = "wasm32")] pub use tendermint_wasm_rpc::*; + +pub(crate) const TX_SUCCESS_CODE: u32 = 0; + +#[repr(u8)] +pub enum TendermintResultOrder { + Ascending = 1, + Descending, +} + +impl From for Order { + fn from(order: TendermintResultOrder) -> Self { + match order { + TendermintResultOrder::Ascending => Self::Ascending, + TendermintResultOrder::Descending => Self::Descending, + } + } +} diff --git a/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs b/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs new file mode 100644 index 0000000000..dde181b3e3 --- /dev/null +++ b/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs @@ -0,0 +1,497 @@ +use async_trait::async_trait; +use core::convert::{TryFrom, TryInto}; +use core::str::FromStr; +pub use cosmrs::tendermint::abci::Path as AbciPath; +use cosmrs::tendermint::abci::{self, Transaction}; +use cosmrs::tendermint::block::Height; +use cosmrs::tendermint::evidence::Evidence; +use cosmrs::tendermint::Genesis; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt; +use std::time::Duration; +use tendermint_config::net; +use tendermint_rpc::endpoint::validators::DEFAULT_VALIDATORS_PER_PAGE; +use tendermint_rpc::endpoint::*; +pub use tendermint_rpc::endpoint::{abci_query::Request as AbciRequest, health::Request as HealthRequest, + tx_search::Request as TxSearchRequest}; +use tendermint_rpc::Paging; +pub use tendermint_rpc::{query::Query as TendermintQuery, Error, Order, Scheme, SimpleRequest, Url}; +use tokio::time; + +/// Provides lightweight access to the Tendermint RPC. It gives access to all +/// endpoints with the exception of the event subscription-related ones. +/// +/// To access event subscription capabilities, use a client that implements the +/// [`SubscriptionClient`] trait. +/// +/// [`SubscriptionClient`]: trait.SubscriptionClient.html +#[async_trait] +pub trait Client { + /// `/abci_info`: get information about the ABCI application. + async fn abci_info(&self) -> Result { + Ok(self.perform(abci_info::Request).await?.response) + } + + /// `/abci_query`: query the ABCI application + async fn abci_query( + &self, + path: Option, + data: V, + height: Option, + prove: bool, + ) -> Result + where + V: Into> + Send, + { + Ok(self + .perform(abci_query::Request::new(path, data, height, prove)) + .await? + .response) + } + + /// `/block`: get block at a given height. + async fn block(&self, height: H) -> Result + where + H: Into + Send, + { + self.perform(block::Request::new(height.into())).await + } + + /// `/block`: get the latest block. + async fn latest_block(&self) -> Result { self.perform(block::Request::default()).await } + + /// `/block_results`: get ABCI results for a block at a particular height. + async fn block_results(&self, height: H) -> Result + where + H: Into + Send, + { + self.perform(block_results::Request::new(height.into())).await + } + + /// `/block_results`: get ABCI results for the latest block. + async fn latest_block_results(&self) -> Result { + self.perform(block_results::Request::default()).await + } + + /// `/block_search`: search for blocks by BeginBlock and EndBlock events. + async fn block_search( + &self, + query: TendermintQuery, + page: u32, + per_page: u8, + order: Order, + ) -> Result { + self.perform(block_search::Request::new(query, page, per_page, order)) + .await + } + + /// `/blockchain`: get block headers for `min` <= `height` <= `max`. + /// + /// Block headers are returned in descending order (highest first). + /// + /// Returns at most 20 items. + async fn blockchain(&self, min: H, max: H) -> Result + where + H: Into + Send, + { + // TODO(tarcieri): return errors for invalid params before making request? + self.perform(blockchain::Request::new(min.into(), max.into())).await + } + + /// `/broadcast_tx_async`: broadcast a transaction, returning immediately. + async fn broadcast_tx_async(&self, tx: Transaction) -> Result { + self.perform(broadcast::tx_async::Request::new(tx)).await + } + + /// `/broadcast_tx_sync`: broadcast a transaction, returning the response + /// from `CheckTx`. + async fn broadcast_tx_sync(&self, tx: Transaction) -> Result { + self.perform(broadcast::tx_sync::Request::new(tx)).await + } + + /// `/broadcast_tx_commit`: broadcast a transaction, returning the response + /// from `DeliverTx`. + async fn broadcast_tx_commit(&self, tx: Transaction) -> Result { + self.perform(broadcast::tx_commit::Request::new(tx)).await + } + + /// `/commit`: get block commit at a given height. + async fn commit(&self, height: H) -> Result + where + H: Into + Send, + { + self.perform(commit::Request::new(height.into())).await + } + + /// `/consensus_params`: get current consensus parameters at the specified + /// height. + async fn consensus_params(&self, height: H) -> Result + where + H: Into + Send, + { + self.perform(consensus_params::Request::new(Some(height.into()))).await + } + + /// `/consensus_state`: get current consensus state + async fn consensus_state(&self) -> Result { + self.perform(consensus_state::Request::new()).await + } + + // TODO(thane): Simplify once validators endpoint removes pagination. + /// `/validators`: get validators a given height. + async fn validators(&self, height: H, paging: Paging) -> Result + where + H: Into + Send, + { + let height = height.into(); + match paging { + Paging::Default => self.perform(validators::Request::new(Some(height), None, None)).await, + Paging::Specific { page_number, per_page } => { + self.perform(validators::Request::new( + Some(height), + Some(page_number), + Some(per_page), + )) + .await + }, + Paging::All => { + let mut page_num = 1_usize; + let mut validators = Vec::new(); + let per_page = DEFAULT_VALIDATORS_PER_PAGE.into(); + loop { + let response = self + .perform(validators::Request::new( + Some(height), + Some(page_num.into()), + Some(per_page), + )) + .await?; + validators.extend(response.validators); + if validators.len() as i32 == response.total { + return Ok(validators::Response::new( + response.block_height, + validators, + response.total, + )); + } + page_num += 1; + } + }, + } + } + + /// `/consensus_params`: get the latest consensus parameters. + async fn latest_consensus_params(&self) -> Result { + self.perform(consensus_params::Request::new(None)).await + } + + /// `/commit`: get the latest block commit + async fn latest_commit(&self) -> Result { self.perform(commit::Request::default()).await } + + /// `/health`: get node health. + /// + /// Returns empty result (200 OK) on success, no response in case of an error. + async fn health(&self) -> Result<(), Error> { + self.perform(health::Request).await?; + Ok(()) + } + + /// `/genesis`: get genesis file. + async fn genesis(&self) -> Result, Error> + where + AppState: fmt::Debug + Serialize + DeserializeOwned + Send, + { + Ok(self.perform(genesis::Request::default()).await?.genesis) + } + + /// `/net_info`: obtain information about P2P and other network connections. + async fn net_info(&self) -> Result { self.perform(net_info::Request).await } + + /// `/status`: get Tendermint status including node info, pubkey, latest + /// block hash, app hash, block height and time. + async fn status(&self) -> Result { self.perform(status::Request).await } + + /// `/broadcast_evidence`: broadcast an evidence. + async fn broadcast_evidence(&self, e: Evidence) -> Result { + self.perform(evidence::Request::new(e)).await + } + + /// `/tx`: find transaction by hash. + async fn tx(&self, hash: abci::transaction::Hash, prove: bool) -> Result { + self.perform(tx::Request::new(hash, prove)).await + } + + /// `/tx_search`: search for transactions with their results. + async fn tx_search( + &self, + query: TendermintQuery, + prove: bool, + page: u32, + per_page: u8, + order: Order, + ) -> Result { + self.perform(tx_search::Request::new(query, prove, page, per_page, order)) + .await + } + + /// Poll the `/health` endpoint until it returns a successful result or + /// the given `timeout` has elapsed. + async fn wait_until_healthy(&self, timeout: T) -> Result<(), Error> + where + T: Into + Send, + { + let timeout = timeout.into(); + let poll_interval = Duration::from_millis(200); + let mut attempts_remaining = timeout.as_millis() / poll_interval.as_millis(); + + while self.health().await.is_err() { + if attempts_remaining == 0 { + return Err(Error::timeout(timeout)); + } + + attempts_remaining -= 1; + time::sleep(poll_interval).await; + } + + Ok(()) + } + + /// Perform a request against the RPC endpoint + async fn perform(&self, request: R) -> Result + where + R: SimpleRequest; +} + +/// A JSON-RPC/HTTP Tendermint RPC client (implements [`crate::Client`]). +/// +/// Supports both HTTP and HTTPS connections to Tendermint RPC endpoints, and +/// allows for the use of HTTP proxies (see [`HttpClient::new_with_proxy`] for +/// details). +/// +/// Does not provide [`crate::event::Event`] subscription facilities (see +/// [`crate::WebSocketClient`] for a client that does). +/// +/// ## Examples +/// +/// ```rust,ignore +/// use tendermint_rpc::{HttpClient, Client}; +/// +/// #[tokio::main] +/// async fn main() { +/// let client = HttpClient::new("http://127.0.0.1:26657") +/// .unwrap(); +/// +/// let abci_info = client.abci_info() +/// .await +/// .unwrap(); +/// +/// println!("Got ABCI info: {:?}", abci_info); +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct HttpClient { + inner: sealed::HttpClient, +} + +impl HttpClient { + /// Construct a new Tendermint RPC HTTP/S client connecting to the given + /// URL. + pub fn new(url: U) -> Result + where + U: TryInto, + { + let url = url.try_into()?; + Ok(Self { + inner: if url.0.is_secure() { + sealed::HttpClient::new_https(url.try_into()?) + } else { + sealed::HttpClient::new_http(url.try_into()?) + }, + }) + } +} + +#[async_trait] +impl Client for HttpClient { + async fn perform(&self, request: R) -> Result + where + R: SimpleRequest, + { + self.inner.perform(request).await + } +} + +/// A URL limited to use with HTTP clients. +/// +/// Facilitates useful type conversions and inferences. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HttpClientUrl(Url); + +impl TryFrom for HttpClientUrl { + type Error = Error; + + fn try_from(value: Url) -> Result { + match value.scheme() { + Scheme::Http | Scheme::Https => Ok(Self(value)), + _ => Err(Error::invalid_url(value)), + } + } +} + +impl FromStr for HttpClientUrl { + type Err = Error; + + fn from_str(s: &str) -> Result { + let url: Url = s.parse()?; + url.try_into() + } +} + +impl TryFrom<&str> for HttpClientUrl { + type Error = Error; + + fn try_from(value: &str) -> Result { value.parse() } +} + +impl TryFrom for HttpClientUrl { + type Error = Error; + + fn try_from(value: net::Address) -> Result { + match value { + net::Address::Tcp { peer_id: _, host, port } => format!("http://{}:{}", host, port).parse(), + net::Address::Unix { .. } => Err(Error::invalid_network_address()), + } + } +} + +impl From for Url { + fn from(url: HttpClientUrl) -> Self { url.0 } +} + +impl TryFrom for hyper::Uri { + type Error = Error; + + fn try_from(value: HttpClientUrl) -> Result { + value + .0 + .to_string() + .parse() + .map_err(|e: http::uri::InvalidUri| Error::parse(e.to_string())) + } +} + +mod sealed { + use common::log::debug; + use hyper::body::Buf; + use hyper::client::connect::Connect; + use hyper::client::HttpConnector; + use hyper::{header, Uri}; + use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; + use std::io::Read; + use tendermint_rpc::{Error, Response, SimpleRequest}; + + fn https_connector() -> HttpsConnector { + HttpsConnectorBuilder::new() + .with_webpki_roots() + .https_or_http() + .enable_http1() + .enable_http2() + .build() + } + + /// A wrapper for a `hyper`-based client, generic over the connector type. + #[derive(Debug, Clone)] + pub struct HyperClient { + uri: Uri, + inner: hyper::Client, + } + + impl HyperClient { + pub fn new(uri: Uri, inner: hyper::Client) -> Self { Self { uri, inner } } + } + + impl HyperClient + where + C: Connect + Clone + Send + Sync + 'static, + { + pub async fn perform(&self, request: R) -> Result + where + R: SimpleRequest, + { + let request = self.build_request(request)?; + let response = self + .inner + .request(request) + .await + .map_err(|e| Error::client_internal(e.to_string()))?; + let response_body = response_to_string(response).await?; + debug!("Incoming response: {}", response_body); + R::Response::from_string(&response_body) + } + } + + impl HyperClient { + /// Build a request using the given Tendermint RPC request. + pub fn build_request(&self, request: R) -> Result, Error> { + let request_body = request.into_json(); + + let mut request = hyper::Request::builder() + .method("POST") + .uri(&self.uri) + .body(hyper::Body::from(request_body.into_bytes())) + .map_err(|e| Error::client_internal(e.to_string()))?; + + { + let headers = request.headers_mut(); + headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap()); + headers.insert( + header::USER_AGENT, + format!("tendermint.rs/{}", env!("CARGO_PKG_VERSION")).parse().unwrap(), + ); + } + + Ok(request) + } + } + + /// We offer several variations of `hyper`-based client. + /// + /// Here we erase the type signature of the underlying `hyper`-based + /// client, allowing the higher-level HTTP client to operate via HTTP or + /// HTTPS, and with or without a proxy. + #[derive(Debug, Clone)] + pub enum HttpClient { + Http(HyperClient), + Https(HyperClient>), + } + + impl HttpClient { + pub fn new_http(uri: Uri) -> Self { Self::Http(HyperClient::new(uri, hyper::Client::new())) } + + pub fn new_https(uri: Uri) -> Self { + Self::Https(HyperClient::new(uri, hyper::Client::builder().build(https_connector()))) + } + + pub async fn perform(&self, request: R) -> Result + where + R: SimpleRequest, + { + match self { + HttpClient::Http(c) => c.perform(request).await, + HttpClient::Https(c) => c.perform(request).await, + } + } + } + + async fn response_to_string(response: hyper::Response) -> Result { + let mut response_body = String::new(); + hyper::body::aggregate(response.into_body()) + .await + .map_err(|e| Error::client_internal(e.to_string()))? + .reader() + .read_to_string(&mut response_body) + .map_err(Error::io)?; + + Ok(response_body) + } +} diff --git a/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs new file mode 100644 index 0000000000..036815a25f --- /dev/null +++ b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs @@ -0,0 +1,123 @@ +use common::APPLICATION_JSON; +pub use cosmrs::tendermint::abci::Path as AbciPath; +use cosmrs::tendermint::abci::Transaction; +use cosmrs::tendermint::block::Height; +use derive_more::Display; +use http::header::{ACCEPT, CONTENT_TYPE}; +use http::uri::InvalidUri; +use http::{StatusCode, Uri}; +use mm2_net::transport::SlurpError; +use mm2_net::wasm_http::FetchRequest; +use std::str::FromStr; +use tendermint_rpc::endpoint::{abci_info, broadcast}; +pub use tendermint_rpc::endpoint::{abci_query::{AbciQuery, Request as AbciRequest}, + health::Request as HealthRequest, + tx_search::Request as TxSearchRequest}; +use tendermint_rpc::error::Error as TendermintRpcError; +pub use tendermint_rpc::query::Query as TendermintQuery; +use tendermint_rpc::request::SimpleRequest; +pub use tendermint_rpc::Order; +use tendermint_rpc::Response; + +#[derive(Debug, Clone)] +pub struct HttpClient { + uri: String, +} + +#[derive(Debug, Display)] +pub(crate) enum HttpClientInitError { + InvalidUri(InvalidUri), +} + +impl From for HttpClientInitError { + fn from(err: InvalidUri) -> Self { HttpClientInitError::InvalidUri(err) } +} + +#[derive(Debug, Display)] +pub enum PerformError { + TendermintRpc(TendermintRpcError), + Slurp(SlurpError), + #[display(fmt = "Request failed with status code {}, response {}", status_code, response)] + StatusCode { + status_code: StatusCode, + response: String, + }, +} + +impl From for PerformError { + fn from(err: SlurpError) -> Self { PerformError::Slurp(err) } +} + +impl From for PerformError { + fn from(err: TendermintRpcError) -> Self { PerformError::TendermintRpc(err) } +} + +impl HttpClient { + pub(crate) fn new(url: &str) -> Result { + Uri::from_str(url)?; + Ok(HttpClient { uri: url.to_owned() }) + } + + pub(crate) async fn perform(&self, request: R) -> Result + where + R: SimpleRequest, + { + let request_str = request.into_json(); + let (status_code, response_str) = FetchRequest::post(&self.uri) + .cors() + .body_utf8(request_str) + .header(ACCEPT.as_str(), APPLICATION_JSON) + .header(CONTENT_TYPE.as_str(), APPLICATION_JSON) + .request_str() + .await + .map_err(|e| e.into_inner())?; + if !status_code.is_success() { + return Err(PerformError::StatusCode { + status_code, + response: response_str, + }); + } + Ok(R::Response::from_string(response_str)?) + } + + /// `/abci_info`: get information about the ABCI application. + pub async fn abci_info(&self) -> Result { + Ok(self.perform(abci_info::Request).await?.response) + } + + /// `/abci_query`: query the ABCI application + pub async fn abci_query( + &self, + path: Option, + data: V, + height: Option, + prove: bool, + ) -> Result + where + V: Into> + Send, + { + Ok(self + .perform(AbciRequest::new(path, data, height, prove)) + .await? + .response) + } + + /// `/broadcast_tx_commit`: broadcast a transaction, returning the response + /// from `DeliverTx`. + pub async fn broadcast_tx_commit(&self, tx: Transaction) -> Result { + self.perform(broadcast::tx_commit::Request::new(tx)).await + } +} + +mod tests { + use super::*; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test] + async fn test_get_abci_info() { + let client = HttpClient::new("https://rpc.sentry-02.theta-testnet.polypore.xyz").unwrap(); + client.abci_info().await.unwrap(); + } +} diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs new file mode 100644 index 0000000000..cb70b0b8fb --- /dev/null +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -0,0 +1,3109 @@ +use super::iris::htlc::{IrisHtlc, MsgClaimHtlc, MsgCreateHtlc, HTLC_STATE_COMPLETED, HTLC_STATE_OPEN, + HTLC_STATE_REFUNDED}; +use super::iris::htlc_proto::{CreateHtlcProtoRep, QueryHtlcRequestProto, QueryHtlcResponseProto}; +use super::rpc::*; +use crate::coin_errors::{MyAddressError, ValidatePaymentError}; +use crate::utxo::sat_from_big_decimal; +use crate::utxo::utxo_common::big_decimal_from_sat; +use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, + CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, + MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, + PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, + RawTransactionRequest, RawTransactionRes, RefundError, RefundResult, RpcCommonOps, + SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, + SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, + SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, + SignatureError, SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, + TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, + TransactionErr, TransactionFut, TransactionType, TxFeeDetails, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, + WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WithdrawError, WithdrawFut, WithdrawRequest}; +use async_std::prelude::FutureExt as AsyncStdFutureExt; +use async_trait::async_trait; +use bitcrypto::{dhash160, sha256}; +use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; +use common::executor::{AbortedError, Timer}; +use common::log::{debug, warn}; +use common::{get_utc_timestamp, now_ms, Future01CompatExt, DEX_FEE_ADDR_PUBKEY}; +use cosmrs::bank::MsgSend; +use cosmrs::crypto::secp256k1::SigningKey; +use cosmrs::proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest, QueryAccountResponse}; +use cosmrs::proto::cosmos::bank::v1beta1::{MsgSend as MsgSendProto, QueryBalanceRequest, QueryBalanceResponse}; +use cosmrs::proto::cosmos::base::tendermint::v1beta1::{GetBlockByHeightRequest, GetBlockByHeightResponse, + GetLatestBlockRequest, GetLatestBlockResponse}; +use cosmrs::proto::cosmos::base::v1beta1::Coin as CoinProto; +use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, + SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; +use cosmrs::tendermint::block::Height; +use cosmrs::tendermint::chain::Id as ChainId; +use cosmrs::tendermint::PublicKey; +use cosmrs::tx::{self, Fee, Msg, Raw, SignDoc, SignerInfo}; +use cosmrs::{AccountId, Any, Coin, Denom, ErrorReport}; +use crypto::{privkey::key_pair_from_secret, Secp256k1Secret, StandardHDPathToCoin}; +use derive_more::Display; +use futures::lock::Mutex as AsyncMutex; +use futures::{FutureExt, TryFutureExt}; +use futures01::Future; +use hex::FromHexError; +use itertools::Itertools; +use keys::KeyPair; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::MmNumber; +use parking_lot::Mutex as PaMutex; +use primitives::hash::H256; +use prost::{DecodeError, Message}; +use rand::{thread_rng, Rng}; +use rpc::v1::types::Bytes as BytesJson; +use serde_json::{self as json, Value as Json}; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::ops::Deref; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use uuid::Uuid; + +// ABCI Request Paths +const ABCI_GET_LATEST_BLOCK_PATH: &str = "/cosmos.base.tendermint.v1beta1.Service/GetLatestBlock"; +const ABCI_GET_BLOCK_BY_HEIGHT_PATH: &str = "/cosmos.base.tendermint.v1beta1.Service/GetBlockByHeight"; +const ABCI_SIMULATE_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/Simulate"; +const ABCI_QUERY_ACCOUNT_PATH: &str = "/cosmos.auth.v1beta1.Query/Account"; +const ABCI_QUERY_BALANCE_PATH: &str = "/cosmos.bank.v1beta1.Query/Balance"; +const ABCI_GET_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTx"; +const ABCI_QUERY_HTLC_PATH: &str = "/irismod.htlc.Query/HTLC"; +const ABCI_GET_TXS_EVENT_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTxsEvent"; + +pub(crate) const MIN_TX_SATOSHIS: i64 = 1; + +// ABCI Request Defaults +const ABCI_REQUEST_HEIGHT: Option = None; +const ABCI_REQUEST_PROVE: bool = false; + +/// 0.25 is good average gas price on atom and iris +const DEFAULT_GAS_PRICE: f64 = 0.25; +pub(super) const TIMEOUT_HEIGHT_DELTA: u64 = 100; +pub const GAS_LIMIT_DEFAULT: u64 = 100_000; +pub(crate) const TX_DEFAULT_MEMO: &str = ""; + +// https://github.com/irisnet/irismod/blob/5016c1be6fdbcffc319943f33713f4a057622f0a/modules/htlc/types/validation.go#L19-L22 +const MAX_TIME_LOCK: i64 = 34560; +const MIN_TIME_LOCK: i64 = 50; + +#[async_trait] +pub trait TendermintCommons { + fn platform_denom(&self) -> String; + + fn set_history_sync_state(&self, new_state: HistorySyncState); + + async fn get_block_timestamp(&self, block: i64) -> MmResult, TendermintCoinRpcError>; + + async fn all_balances(&self) -> MmResult; + + async fn rpc_client(&self) -> MmResult; +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct TendermintFeeDetails { + pub coin: String, + pub amount: BigDecimal, + #[serde(skip)] + pub uamount: u64, + pub gas_limit: u64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TendermintProtocolInfo { + decimals: u8, + denom: String, + pub account_prefix: String, + chain_id: String, + gas_price: Option, +} + +#[derive(Clone)] +pub struct ActivatedTokenInfo { + pub(crate) decimals: u8, + pub(crate) denom: Denom, +} + +pub struct TendermintConf { + avg_blocktime: u8, + /// 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`. + derivation_path: Option, +} + +impl TendermintConf { + pub fn try_from_json(ticker: &str, conf: &Json) -> MmResult { + let avg_blocktime = conf.get("avg_blocktime").or_mm_err(|| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::AvgBlockTimeMissing, + })?; + + let avg_blocktime = avg_blocktime.as_i64().or_mm_err(|| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::AvgBlockTimeInvalid, + })?; + + let avg_blocktime = u8::try_from(avg_blocktime).map_to_mm(|_| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::AvgBlockTimeInvalid, + })?; + + let derivation_path = json::from_value(conf["derivation_path"].clone()).map_to_mm(|e| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::ErrorDeserializingDerivationPath(e.to_string()), + })?; + + Ok(TendermintConf { + avg_blocktime, + derivation_path, + }) + } +} + +struct TendermintRpcClient(AsyncMutex); + +struct TendermintRpcClientImpl { + rpc_clients: Vec, +} + +#[async_trait] +impl RpcCommonOps for TendermintCoin { + type RpcClient = HttpClient; + type Error = TendermintCoinRpcError; + + async fn get_live_client(&self) -> Result { + let mut client_impl = self.client.0.lock().await; + // try to find first live client + for (i, client) in client_impl.rpc_clients.clone().into_iter().enumerate() { + match client.perform(HealthRequest).timeout(Duration::from_secs(15)).await { + Ok(Ok(_)) => { + // Bring the live client to the front of rpc_clients + client_impl.rpc_clients.rotate_left(i); + return Ok(client); + }, + Ok(Err(rpc_error)) => { + debug!("Could not perform healthcheck on: {:?}. Error: {}", &client, rpc_error); + }, + Err(timeout_error) => { + debug!("Healthcheck timeout exceed on: {:?}. Error: {}", &client, timeout_error); + }, + }; + } + return Err(TendermintCoinRpcError::RpcClientError( + "All the current rpc nodes are unavailable.".to_string(), + )); + } +} + +pub struct TendermintCoinImpl { + ticker: String, + /// As seconds + avg_blocktime: u8, + /// My address + pub account_id: AccountId, + pub(super) account_prefix: String, + priv_key: Vec, + pub(crate) decimals: u8, + pub(super) denom: Denom, + chain_id: ChainId, + gas_price: Option, + pub(super) sequence_lock: AsyncMutex<()>, + pub(crate) tokens_info: PaMutex>, + /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation + /// or on [`MmArc::stop`]. + pub(super) abortable_system: AbortableQueue, + pub(crate) history_sync_state: Mutex, + client: TendermintRpcClient, +} + +#[derive(Clone)] +pub struct TendermintCoin(Arc); + +impl Deref for TendermintCoin { + type Target = TendermintCoinImpl; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +#[derive(Debug)] +pub struct TendermintInitError { + pub ticker: String, + pub kind: TendermintInitErrorKind, +} + +#[derive(Display, Debug)] +pub enum TendermintInitErrorKind { + Internal(String), + InvalidPrivKey(String), + CouldNotGenerateAccountId(String), + EmptyRpcUrls, + RpcClientInitError(String), + InvalidChainId(String), + InvalidDenom(String), + #[display(fmt = "'derivation_path' field is not found in config")] + DerivationPathIsNotSet, + #[display(fmt = "Error deserializing 'derivation_path': {}", _0)] + ErrorDeserializingDerivationPath(String), + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + RpcError(String), + #[display(fmt = "avg_blocktime is missing in coin configuration")] + AvgBlockTimeMissing, + #[display(fmt = "avg_blocktime must be in-between '0' and '255'.")] + AvgBlockTimeInvalid, +} + +#[derive(Display, Debug)] +pub enum TendermintCoinRpcError { + Prost(DecodeError), + InvalidResponse(String), + PerformError(String), + RpcClientError(String), +} + +impl From for TendermintCoinRpcError { + fn from(err: DecodeError) -> Self { TendermintCoinRpcError::Prost(err) } +} + +impl From for WithdrawError { + fn from(err: TendermintCoinRpcError) -> Self { WithdrawError::Transport(err.to_string()) } +} + +impl From for BalanceError { + fn from(err: TendermintCoinRpcError) -> Self { + match err { + TendermintCoinRpcError::InvalidResponse(e) => BalanceError::InvalidResponse(e), + TendermintCoinRpcError::Prost(e) => BalanceError::InvalidResponse(e.to_string()), + TendermintCoinRpcError::PerformError(e) => BalanceError::Transport(e), + TendermintCoinRpcError::RpcClientError(e) => BalanceError::Transport(e), + } + } +} + +impl From for ValidatePaymentError { + fn from(err: TendermintCoinRpcError) -> Self { + match err { + TendermintCoinRpcError::InvalidResponse(e) => ValidatePaymentError::InvalidRpcResponse(e), + TendermintCoinRpcError::Prost(e) => ValidatePaymentError::InvalidRpcResponse(e.to_string()), + TendermintCoinRpcError::PerformError(e) => ValidatePaymentError::Transport(e), + TendermintCoinRpcError::RpcClientError(e) => ValidatePaymentError::Transport(e), + } + } +} + +impl From for TradePreimageError { + fn from(err: TendermintCoinRpcError) -> Self { TradePreimageError::Transport(err.to_string()) } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for TendermintCoinRpcError { + fn from(err: tendermint_rpc::Error) -> Self { TendermintCoinRpcError::PerformError(err.to_string()) } +} + +#[cfg(target_arch = "wasm32")] +impl From for TendermintCoinRpcError { + fn from(err: PerformError) -> Self { TendermintCoinRpcError::PerformError(err.to_string()) } +} + +impl From for RawTransactionError { + fn from(err: TendermintCoinRpcError) -> Self { RawTransactionError::Transport(err.to_string()) } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct CosmosTransaction { + pub data: cosmrs::proto::cosmos::tx::v1beta1::TxRaw, +} + +impl crate::Transaction for CosmosTransaction { + fn tx_hex(&self) -> Vec { self.data.encode_to_vec() } + + fn tx_hash(&self) -> BytesJson { + let bytes = self.data.encode_to_vec(); + let hash = sha256(&bytes); + hash.to_vec().into() + } +} + +fn account_id_from_privkey(priv_key: &[u8], prefix: &str) -> MmResult { + let signing_key = + SigningKey::from_bytes(priv_key).map_to_mm(|e| TendermintInitErrorKind::InvalidPrivKey(e.to_string()))?; + + signing_key + .public_key() + .account_id(prefix) + .map_to_mm(|e| TendermintInitErrorKind::CouldNotGenerateAccountId(e.to_string())) +} + +#[derive(Display, Debug)] +pub enum AccountIdFromPubkeyHexErr { + InvalidHexString(FromHexError), + CouldNotCreateAccountId(ErrorReport), +} + +impl From for AccountIdFromPubkeyHexErr { + fn from(err: FromHexError) -> Self { AccountIdFromPubkeyHexErr::InvalidHexString(err) } +} + +impl From for AccountIdFromPubkeyHexErr { + fn from(err: ErrorReport) -> Self { AccountIdFromPubkeyHexErr::CouldNotCreateAccountId(err) } +} + +pub fn account_id_from_pubkey_hex(prefix: &str, pubkey: &str) -> MmResult { + let pubkey_bytes = hex::decode(pubkey)?; + let pubkey_hash = dhash160(&pubkey_bytes); + Ok(AccountId::new(prefix, pubkey_hash.as_slice())?) +} + +#[derive(Debug, Clone, PartialEq)] +pub struct AllBalancesResult { + pub platform_balance: BigDecimal, + pub tokens_balances: HashMap, +} + +#[derive(Debug, Display)] +enum SearchForSwapTxSpendErr { + Cosmrs(ErrorReport), + Rpc(TendermintCoinRpcError), + TxMessagesEmpty, + ClaimHtlcTxNotFound, + UnexpectedHtlcState(i32), + Proto(DecodeError), +} + +impl From for SearchForSwapTxSpendErr { + fn from(e: ErrorReport) -> Self { SearchForSwapTxSpendErr::Cosmrs(e) } +} + +impl From for SearchForSwapTxSpendErr { + fn from(e: TendermintCoinRpcError) -> Self { SearchForSwapTxSpendErr::Rpc(e) } +} + +impl From for SearchForSwapTxSpendErr { + fn from(e: DecodeError) -> Self { SearchForSwapTxSpendErr::Proto(e) } +} + +#[async_trait] +impl TendermintCommons for TendermintCoin { + fn platform_denom(&self) -> String { self.denom.to_string() } + + fn set_history_sync_state(&self, new_state: HistorySyncState) { + *self.history_sync_state.lock().unwrap() = new_state; + } + + async fn get_block_timestamp(&self, block: i64) -> MmResult, TendermintCoinRpcError> { + let block_response = self.get_block_by_height(block).await?; + let block_header = some_or_return_ok_none!(some_or_return_ok_none!(block_response.block).header); + let timestamp = some_or_return_ok_none!(block_header.time); + + Ok(u64::try_from(timestamp.seconds).ok()) + } + + async fn all_balances(&self) -> MmResult { + let platform_balance_denom = self.balance_for_denom(self.denom.to_string()).await?; + let platform_balance = big_decimal_from_sat_unsigned(platform_balance_denom, self.decimals); + let ibc_assets_info = self.tokens_info.lock().clone(); + + let mut result = AllBalancesResult { + platform_balance, + tokens_balances: HashMap::new(), + }; + for (ticker, info) in ibc_assets_info { + let balance_denom = self.balance_for_denom(info.denom.to_string()).await?; + let balance_decimal = big_decimal_from_sat_unsigned(balance_denom, info.decimals); + result.tokens_balances.insert(ticker, balance_decimal); + } + + Ok(result) + } + + #[inline(always)] + async fn rpc_client(&self) -> MmResult { + self.get_live_client().await.map_to_mm(|e| e) + } +} + +impl TendermintCoin { + pub async fn init( + ctx: &MmArc, + ticker: String, + conf: TendermintConf, + protocol_info: TendermintProtocolInfo, + rpc_urls: Vec, + tx_history: bool, + priv_key_policy: PrivKeyBuildPolicy, + ) -> MmResult { + if rpc_urls.is_empty() { + return MmError::err(TendermintInitError { + ticker, + kind: TendermintInitErrorKind::EmptyRpcUrls, + }); + } + + let priv_key = secret_from_priv_key_policy(&conf, &ticker, priv_key_policy)?; + + let account_id = + account_id_from_privkey(priv_key.as_slice(), &protocol_info.account_prefix).mm_err(|kind| { + TendermintInitError { + ticker: ticker.clone(), + kind, + } + })?; + + let rpc_clients = clients_from_urls(rpc_urls.as_ref()).mm_err(|kind| TendermintInitError { + ticker: ticker.clone(), + kind, + })?; + + let client_impl = TendermintRpcClientImpl { rpc_clients }; + + let chain_id = ChainId::try_from(protocol_info.chain_id).map_to_mm(|e| TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::InvalidChainId(e.to_string()), + })?; + + let denom = Denom::from_str(&protocol_info.denom).map_to_mm(|e| TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::InvalidDenom(e.to_string()), + })?; + + let history_sync_state = if tx_history { + HistorySyncState::NotStarted + } else { + HistorySyncState::NotEnabled + }; + + // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, + // all spawned futures related to `TendermintCoin` will be aborted as well. + let abortable_system = ctx + .abortable_system + .create_subsystem() + .map_to_mm(|e| TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::Internal(e.to_string()), + })?; + + Ok(TendermintCoin(Arc::new(TendermintCoinImpl { + ticker, + account_id, + account_prefix: protocol_info.account_prefix, + priv_key: priv_key.to_vec(), + decimals: protocol_info.decimals, + denom, + chain_id, + gas_price: protocol_info.gas_price, + avg_blocktime: conf.avg_blocktime, + sequence_lock: AsyncMutex::new(()), + tokens_info: PaMutex::new(HashMap::new()), + abortable_system, + history_sync_state: Mutex::new(history_sync_state), + client: TendermintRpcClient(AsyncMutex::new(client_impl)), + }))) + } + + #[inline(always)] + fn gas_price(&self) -> f64 { self.gas_price.unwrap_or(DEFAULT_GAS_PRICE) } + + #[allow(unused)] + async fn get_latest_block(&self) -> MmResult { + let path = AbciPath::from_str(ABCI_GET_LATEST_BLOCK_PATH).expect("valid path"); + + let request = GetLatestBlockRequest {}; + let request = AbciRequest::new( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ); + + let response = self.rpc_client().await?.perform(request).await?; + + Ok(GetLatestBlockResponse::decode(response.response.value.as_slice())?) + } + + #[allow(unused)] + async fn get_block_by_height(&self, height: i64) -> MmResult { + let path = AbciPath::from_str(ABCI_GET_BLOCK_BY_HEIGHT_PATH).expect("valid path"); + + let request = GetBlockByHeightRequest { height }; + let request = AbciRequest::new( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ); + + let response = self.rpc_client().await?.perform(request).await?; + + Ok(GetBlockByHeightResponse::decode(response.response.value.as_slice())?) + } + + // We must simulate the tx on rpc nodes in order to calculate network fee. + // Right now cosmos doesn't expose any of gas price and fee informations directly. + // Therefore, we can call SimulateRequest or CheckTx(doesn't work with using Abci interface) to get used gas or fee itself. + pub(super) fn gen_simulated_tx( + &self, + account_info: BaseAccount, + tx_payload: Any, + timeout_height: u64, + memo: String, + ) -> cosmrs::Result> { + let fee_amount = Coin { + denom: self.denom.clone(), + amount: 0_u64.into(), + }; + + let fee = Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT); + + let signkey = SigningKey::from_bytes(&self.priv_key)?; + let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); + let auth_info = SignerInfo::single_direct(Some(signkey.public_key()), account_info.sequence).auth_info(fee); + let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; + sign_doc.sign(&signkey)?.to_bytes() + } + + /// This is converted from irismod and cosmos-sdk source codes written in golang. + /// Refs: + /// - Main algorithm: https://github.com/irisnet/irismod/blob/main/modules/htlc/types/htlc.go#L157 + /// - Coins string building https://github.com/cosmos/cosmos-sdk/blob/main/types/coin.go#L210-L225 + fn calculate_htlc_id( + &self, + from_address: &AccountId, + to_address: &AccountId, + amount: Vec, + secret_hash: &[u8], + ) -> String { + // Needs to be sorted if cointains multiple coins + // let mut amount = amount; + // amount.sort(); + + let coins_string = amount + .iter() + .map(|t| format!("{}{}", t.amount, t.denom)) + .collect::>() + .join(","); + + let mut htlc_id = vec![]; + htlc_id.extend_from_slice(secret_hash); + htlc_id.extend_from_slice(&from_address.to_bytes()); + htlc_id.extend_from_slice(&to_address.to_bytes()); + htlc_id.extend_from_slice(coins_string.as_bytes()); + sha256(&htlc_id).to_string().to_uppercase() + } + + #[allow(deprecated)] + pub(super) async fn calculate_fee( + &self, + base_denom: Denom, + tx_bytes: Vec, + ) -> MmResult { + let path = AbciPath::from_str(ABCI_SIMULATE_TX_PATH).expect("valid path"); + let request = SimulateRequest { tx_bytes, tx: None }; + let request = AbciRequest::new( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ); + + let raw_response = self.rpc_client().await?.perform(request).await?; + let response = SimulateResponse::decode(raw_response.response.value.as_slice())?; + + let gas = response.gas_info.as_ref().ok_or_else(|| { + TendermintCoinRpcError::InvalidResponse(format!( + "Could not read gas_info. Invalid Response: {:?}", + raw_response + )) + })?; + + let amount = ((gas.gas_used as f64 * 1.5) * self.gas_price()).ceil(); + + let fee_amount = Coin { + denom: base_denom, + amount: (amount as u64).into(), + }; + + Ok(Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT)) + } + + #[allow(deprecated)] + pub(super) async fn calculate_fee_amount_as_u64(&self, tx_bytes: Vec) -> MmResult { + let path = AbciPath::from_str(ABCI_SIMULATE_TX_PATH).expect("valid path"); + let request = SimulateRequest { tx_bytes, tx: None }; + let request = AbciRequest::new( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ); + + let raw_response = self.rpc_client().await?.perform(request).await?; + let response = SimulateResponse::decode(raw_response.response.value.as_slice())?; + + let gas = response.gas_info.as_ref().ok_or_else(|| { + TendermintCoinRpcError::InvalidResponse(format!( + "Could not read gas_info. Invalid Response: {:?}", + raw_response + )) + })?; + + Ok(((gas.gas_used as f64 * 1.5) * self.gas_price()).ceil() as u64) + } + + pub(super) async fn my_account_info(&self) -> MmResult { + let path = AbciPath::from_str(ABCI_QUERY_ACCOUNT_PATH).expect("valid path"); + let request = QueryAccountRequest { + address: self.account_id.to_string(), + }; + let request = AbciRequest::new( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ); + + let response = self.rpc_client().await?.perform(request).await?; + let account_response = QueryAccountResponse::decode(response.response.value.as_slice())?; + let account = account_response + .account + .or_mm_err(|| TendermintCoinRpcError::InvalidResponse("Account is None".into()))?; + Ok(BaseAccount::decode(account.value.as_slice())?) + } + + pub(super) async fn balance_for_denom(&self, denom: String) -> MmResult { + let path = AbciPath::from_str(ABCI_QUERY_BALANCE_PATH).expect("valid path"); + let request = QueryBalanceRequest { + address: self.account_id.to_string(), + denom, + }; + let request = AbciRequest::new( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ); + + let response = self.rpc_client().await?.perform(request).await?; + let response = QueryBalanceResponse::decode(response.response.value.as_slice())?; + response + .balance + .or_mm_err(|| TendermintCoinRpcError::InvalidResponse("balance is None".into()))? + .amount + .parse() + .map_to_mm(|e| TendermintCoinRpcError::InvalidResponse(format!("balance is not u64, err {}", e))) + } + + fn gen_create_htlc_tx( + &self, + denom: Denom, + to: &AccountId, + amount: cosmrs::Decimal, + secret_hash: &[u8], + time_lock: u64, + ) -> MmResult { + let amount = vec![Coin { denom, amount }]; + let timestamp = 0_u64; + let msg_payload = MsgCreateHtlc { + sender: self.account_id.clone(), + to: to.clone(), + receiver_on_other_chain: "".to_string(), + sender_on_other_chain: "".to_string(), + amount: amount.clone(), + hash_lock: hex::encode(secret_hash), + timestamp, + time_lock, + transfer: false, + }; + + let htlc_id = self.calculate_htlc_id(&self.account_id, to, amount, secret_hash); + + Ok(IrisHtlc { + id: htlc_id, + msg_payload: msg_payload + .to_any() + .map_err(|e| MmError::new(TxMarshalingErr::InvalidInput(e.to_string())))?, + }) + } + + fn gen_claim_htlc_tx(&self, htlc_id: String, secret: &[u8]) -> MmResult { + let msg_payload = MsgClaimHtlc { + id: htlc_id.clone(), + sender: self.account_id.clone(), + secret: hex::encode(secret), + }; + + Ok(IrisHtlc { + id: htlc_id, + msg_payload: msg_payload + .to_any() + .map_err(|e| MmError::new(TxMarshalingErr::InvalidInput(e.to_string())))?, + }) + } + + pub(super) fn any_to_signed_raw_tx( + &self, + account_info: BaseAccount, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + ) -> cosmrs::Result { + let signkey = SigningKey::from_bytes(&self.priv_key)?; + let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); + let auth_info = SignerInfo::single_direct(Some(signkey.public_key()), account_info.sequence).auth_info(fee); + let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; + sign_doc.sign(&signkey) + } + + pub fn add_activated_token_info(&self, ticker: String, decimals: u8, denom: Denom) { + self.tokens_info + .lock() + .insert(ticker, ActivatedTokenInfo { decimals, denom }); + } + + fn estimate_blocks_from_duration(&self, duration: u64) -> i64 { + let estimated_time_lock = (duration / self.avg_blocktime as u64) as i64; + + estimated_time_lock.clamp(MIN_TIME_LOCK, MAX_TIME_LOCK) + } + + pub(crate) fn check_if_my_payment_sent_for_denom( + &self, + decimals: u8, + denom: Denom, + other_pub: &[u8], + secret_hash: &[u8], + amount: &BigDecimal, + ) -> Box, Error = String> + Send> { + let amount = try_fus!(sat_from_big_decimal(amount, decimals)); + let amount = vec![Coin { + denom, + amount: amount.into(), + }]; + + let pubkey_hash = dhash160(other_pub); + let to_address = try_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); + + let htlc_id = self.calculate_htlc_id(&self.account_id, &to_address, amount, secret_hash); + + let coin = self.clone(); + let fut = async move { + let htlc_response = try_s!(coin.query_htlc(htlc_id.clone()).await); + let htlc_data = match htlc_response.htlc { + Some(htlc) => htlc, + None => return Ok(None), + }; + + match htlc_data.state { + HTLC_STATE_OPEN | HTLC_STATE_COMPLETED | HTLC_STATE_REFUNDED => {}, + unexpected_state => return Err(format!("Unexpected state for HTLC {}", unexpected_state)), + }; + + let rpc_client = try_s!(coin.rpc_client().await); + let q = format!("create_htlc.id = '{}'", htlc_id); + + let response = try_s!( + // Search single tx + rpc_client + .perform(TxSearchRequest::new( + q, + false, + 1, + 1, + TendermintResultOrder::Descending.into() + )) + .await + ); + + if let Some(tx) = response.txs.first() { + if let cosmrs::tendermint::abci::Code::Err(err_code) = tx.tx_result.code { + return Err(format!( + "Got {} error code. Broadcasted HTLC likely isn't valid.", + err_code + )); + } + + let deserialized_tx = try_s!(cosmrs::Tx::from_bytes(tx.tx.as_bytes())); + let msg = try_s!(deserialized_tx.body.messages.first().ok_or("Tx body couldn't be read.")); + let htlc = try_s!(CreateHtlcProtoRep::decode(msg.value.as_slice())); + + if htlc.hash_lock.to_uppercase() == htlc_data.hash_lock.to_uppercase() { + let htlc = TransactionEnum::CosmosTransaction(CosmosTransaction { + data: try_s!(TxRaw::decode(tx.tx.as_bytes())), + }); + return Ok(Some(htlc)); + } + } + + Ok(None) + }; + + Box::new(fut.boxed().compat()) + } + + pub(super) fn send_htlc_for_denom( + &self, + time_lock_duration: u64, + other_pub: &[u8], + secret_hash: &[u8], + amount: BigDecimal, + denom: Denom, + decimals: u8, + ) -> TransactionFut { + let pubkey_hash = dhash160(other_pub); + let to = try_tx_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); + + let amount_as_u64 = try_tx_fus!(sat_from_big_decimal(&amount, decimals)); + let amount = cosmrs::Decimal::from(amount_as_u64); + + let secret_hash = secret_hash.to_vec(); + let coin = self.clone(); + let fut = async move { + let time_lock = coin.estimate_blocks_from_duration(time_lock_duration); + + let create_htlc_tx = try_tx_s!(coin.gen_create_htlc_tx(denom, &to, amount, &secret_hash, time_lock as u64)); + + let _sequence_lock = coin.sequence_lock.lock().await; + let current_block = try_tx_s!(coin.current_block().compat().await); + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + let account_info = try_tx_s!(coin.my_account_info().await); + + let simulated_tx = try_tx_s!(coin.gen_simulated_tx( + account_info.clone(), + create_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + )); + + let fee = try_tx_s!(coin.calculate_fee(coin.denom.clone(), simulated_tx).await); + + let tx_raw = try_tx_s!(coin.any_to_signed_raw_tx( + account_info.clone(), + create_htlc_tx.msg_payload.clone(), + fee, + timeout_height, + TX_DEFAULT_MEMO.into(), + )); + + let _tx_id = try_tx_s!(coin.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await); + + Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { + data: tx_raw.into(), + })) + }; + + Box::new(fut.boxed().compat()) + } + + pub(super) fn send_taker_fee_for_denom( + &self, + fee_addr: &[u8], + amount: BigDecimal, + denom: Denom, + decimals: u8, + uuid: &[u8], + ) -> TransactionFut { + let memo = try_tx_fus!(Uuid::from_slice(uuid)).to_string(); + let from_address = self.account_id.clone(); + let pubkey_hash = dhash160(fee_addr); + let to_address = try_tx_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); + + let amount_as_u64 = try_tx_fus!(sat_from_big_decimal(&amount, decimals)); + let amount = cosmrs::Decimal::from(amount_as_u64); + + let amount = vec![Coin { denom, amount }]; + + let tx_payload = try_tx_fus!(MsgSend { + from_address, + to_address, + amount, + } + .to_any()); + + let coin = self.clone(); + let fut = async move { + let _sequence_lock = coin.sequence_lock.lock().await; + let account_info = try_tx_s!(coin.my_account_info().await); + + let current_block = try_tx_s!(coin.current_block().compat().await.map_to_mm(WithdrawError::Transport)); + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + + let simulated_tx = try_tx_s!(coin.gen_simulated_tx( + account_info.clone(), + tx_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + )); + + let fee = try_tx_s!(coin.calculate_fee(coin.denom.clone(), simulated_tx).await); + + let tx_raw = try_tx_s!(coin + .any_to_signed_raw_tx(account_info, tx_payload, fee, timeout_height, memo) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))); + + let tx_bytes = try_tx_s!(tx_raw + .to_bytes() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))); + + let _tx_id = try_tx_s!(coin.send_raw_tx_bytes(&tx_bytes).compat().await); + + Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { + data: tx_raw.into(), + })) + }; + + Box::new(fut.boxed().compat()) + } + + #[allow(clippy::too_many_arguments)] + pub(super) fn validate_fee_for_denom( + &self, + fee_tx: &TransactionEnum, + expected_sender: &[u8], + fee_addr: &[u8], + amount: &BigDecimal, + decimals: u8, + uuid: &[u8], + denom: String, + ) -> Box + Send> { + let tx = match fee_tx { + TransactionEnum::CosmosTransaction(tx) => tx.clone(), + invalid_variant => { + return Box::new(futures01::future::err(ERRL!( + "Unexpected tx variant {:?}", + invalid_variant + ))) + }, + }; + + let uuid = try_fus!(Uuid::from_slice(uuid)).to_string(); + let sender_pubkey_hash = dhash160(expected_sender); + let expected_sender_address = + try_fus!(AccountId::new(&self.account_prefix, sender_pubkey_hash.as_slice())).to_string(); + + let dex_fee_addr_pubkey_hash = dhash160(fee_addr); + let expected_dex_fee_address = try_fus!(AccountId::new( + &self.account_prefix, + dex_fee_addr_pubkey_hash.as_slice() + )) + .to_string(); + + let expected_amount = try_fus!(sat_from_big_decimal(amount, decimals)); + let expected_amount = CoinProto { + denom, + amount: expected_amount.to_string(), + }; + + let coin = self.clone(); + let fut = async move { + let tx_body = try_s!(TxBody::decode(tx.data.body_bytes.as_slice())); + if tx_body.messages.len() != 1 { + return ERR!("Tx body must have exactly one message"); + } + + let msg = try_s!(MsgSendProto::decode(tx_body.messages[0].value.as_slice())); + if msg.to_address != expected_dex_fee_address { + return ERR!( + "Dex fee is sent to wrong address: {}, expected {}", + msg.to_address, + expected_dex_fee_address + ); + } + + if msg.amount.len() != 1 { + return ERR!("Msg must have exactly one Coin"); + } + + if msg.amount[0] != expected_amount { + return ERR!("Invalid amount {:?}, expected {:?}", msg.amount[0], expected_amount); + } + + if msg.from_address != expected_sender_address { + return ERR!( + "Invalid sender: {}, expected {}", + msg.from_address, + expected_sender_address + ); + } + + if tx_body.memo != uuid { + return ERR!("Invalid memo: {}, expected {}", msg.from_address, uuid); + } + + let encoded_tx = tx.data.encode_to_vec(); + let hash = hex::encode_upper(sha256(&encoded_tx).as_slice()); + let encoded_from_rpc = try_s!(coin.request_tx(hash).await).encode_to_vec(); + if encoded_tx != encoded_from_rpc { + return ERR!("Transaction from RPC doesn't match the input"); + } + Ok(()) + }; + Box::new(fut.boxed().compat()) + } + + pub(super) fn validate_payment_for_denom( + &self, + input: ValidatePaymentInput, + denom: Denom, + decimals: u8, + ) -> ValidatePaymentFut<()> { + let coin = self.clone(); + let fut = async move { + let tx = cosmrs::Tx::from_bytes(&input.payment_tx) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; + + if tx.body.messages.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Payment tx must have exactly one message".into(), + )); + } + + let create_htlc_msg_proto = CreateHtlcProtoRep::decode(tx.body.messages[0].value.as_slice()) + .map_to_mm(|e| ValidatePaymentError::WrongPaymentTx(e.to_string()))?; + let create_htlc_msg = MsgCreateHtlc::try_from(create_htlc_msg_proto) + .map_to_mm(|e| ValidatePaymentError::WrongPaymentTx(e.to_string()))?; + + let sender_pubkey_hash = dhash160(&input.other_pub); + let sender = AccountId::new(&coin.account_prefix, sender_pubkey_hash.as_slice()) + .map_to_mm(|e| ValidatePaymentError::InvalidParameter(e.to_string()))?; + + let amount = sat_from_big_decimal(&input.amount, decimals)?; + let amount = vec![Coin { + denom, + amount: amount.into(), + }]; + + let time_lock = coin.estimate_blocks_from_duration(input.time_lock_duration); + + let expected_msg = MsgCreateHtlc { + sender: sender.clone(), + to: coin.account_id.clone(), + receiver_on_other_chain: "".into(), + sender_on_other_chain: "".into(), + amount: amount.clone(), + hash_lock: hex::encode(&input.secret_hash), + timestamp: 0, + time_lock: time_lock as u64, + transfer: false, + }; + + if create_htlc_msg != expected_msg { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Incorrect CreateHtlc message {:?}, expected {:?}", + create_htlc_msg, expected_msg + ))); + } + + let hash = hex::encode_upper(sha256(&input.payment_tx).as_slice()); + let tx_from_rpc = coin.request_tx(hash).await?; + if input.payment_tx != tx_from_rpc.encode_to_vec() { + return MmError::err(ValidatePaymentError::InvalidRpcResponse( + "Tx from RPC doesn't match the input".into(), + )); + } + + let htlc_id = coin.calculate_htlc_id(&sender, &coin.account_id, amount, &input.secret_hash); + + let htlc_response = coin.query_htlc(htlc_id.clone()).await?; + let htlc_data = htlc_response + .htlc + .or_mm_err(|| ValidatePaymentError::InvalidRpcResponse(format!("No HTLC data for {}", htlc_id)))?; + + match htlc_data.state { + HTLC_STATE_OPEN => Ok(()), + unexpected_state => MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( + "{}", + unexpected_state + ))), + } + }; + Box::new(fut.boxed().compat()) + } + + pub(super) async fn get_sender_trade_fee_for_denom( + &self, + ticker: String, + denom: Denom, + decimals: u8, + amount: BigDecimal, + ) -> TradePreimageResult { + const TIME_LOCK: u64 = 1750; + let sec: [u8; 32] = thread_rng().gen(); + let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) + .map_err(|e| MmError::new(TradePreimageError::InternalError(e.into_inner().to_string())))?; + + let amount = sat_from_big_decimal(&amount, decimals)?; + + let create_htlc_tx = self + .gen_create_htlc_tx(denom, &to_address, amount.into(), sha256(&sec).as_slice(), TIME_LOCK) + .map_err(|e| { + MmError::new(TradePreimageError::InternalError(format!( + "Could not create HTLC. {:?}", + e.into_inner() + ))) + })?; + + let current_block = self.current_block().compat().await.map_err(|e| { + MmError::new(TradePreimageError::InternalError(format!( + "Could not get current_block. {}", + e + ))) + })?; + + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + let account_info = self.my_account_info().await?; + + let simulated_tx = self + .gen_simulated_tx( + account_info.clone(), + create_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .map_err(|e| { + MmError::new(TradePreimageError::InternalError(format!( + "Tx simulation failed. {:?}", + e + ))) + })?; + + let fee_uamount = self.calculate_fee_amount_as_u64(simulated_tx).await?; + let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, self.decimals); + + Ok(TradeFee { + coin: ticker, + amount: fee_amount.into(), + paid_from_trading_vol: false, + }) + } + + pub(super) async fn get_fee_to_send_taker_fee_for_denom( + &self, + ticker: String, + denom: Denom, + decimals: u8, + dex_fee_amount: BigDecimal, + ) -> TradePreimageResult { + let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) + .map_err(|e| MmError::new(TradePreimageError::InternalError(e.into_inner().to_string())))?; + let amount = sat_from_big_decimal(&dex_fee_amount, decimals)?; + + let current_block = self.current_block().compat().await.map_err(|e| { + MmError::new(TradePreimageError::InternalError(format!( + "Could not get current_block. {}", + e + ))) + })?; + + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + let account_info = self.my_account_info().await?; + + let msg_send = MsgSend { + from_address: self.account_id.clone(), + to_address: to_address.clone(), + amount: vec![Coin { + denom, + amount: amount.into(), + }], + } + .to_any() + .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; + + let simulated_tx = self + .gen_simulated_tx(account_info.clone(), msg_send, timeout_height, TX_DEFAULT_MEMO.into()) + .map_err(|e| { + MmError::new(TradePreimageError::InternalError(format!( + "Tx simulation failed. {:?}", + e + ))) + })?; + + let fee_uamount = self.calculate_fee_amount_as_u64(simulated_tx).await?; + let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, decimals); + + Ok(TradeFee { + coin: ticker, + amount: fee_amount.into(), + paid_from_trading_vol: false, + }) + } + + async fn request_tx(&self, hash: String) -> MmResult { + let path = AbciPath::from_str(ABCI_GET_TX_PATH).expect("valid path"); + let request = GetTxRequest { hash }; + let response = self + .rpc_client() + .await? + .abci_query( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + let response = GetTxResponse::decode(response.value.as_slice())?; + response + .tx + .or_mm_err(|| TendermintCoinRpcError::InvalidResponse(format!("Tx {} does not exist", request.hash))) + } + + /// Returns status code of transaction. + /// If tx doesn't exists on chain, then returns `None`. + async fn get_tx_status_code_or_none( + &self, + hash: String, + ) -> MmResult, TendermintCoinRpcError> { + let path = AbciPath::from_str(ABCI_GET_TX_PATH).expect("valid path"); + let request = GetTxRequest { hash }; + let response = self + .rpc_client() + .await? + .abci_query( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + let tx = GetTxResponse::decode(response.value.as_slice())?; + + if let Some(tx_response) = tx.tx_response { + // non-zero values are error. + match tx_response.code { + TX_SUCCESS_CODE => Ok(Some(cosmrs::tendermint::abci::Code::Ok)), + err_code => Ok(Some(cosmrs::tendermint::abci::Code::Err(err_code))), + } + } else { + Ok(None) + } + } + + pub(crate) async fn query_htlc(&self, id: String) -> MmResult { + let path = AbciPath::from_str(ABCI_QUERY_HTLC_PATH).expect("valid path"); + let request = QueryHtlcRequestProto { id }; + let response = self + .rpc_client() + .await? + .abci_query( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + Ok(QueryHtlcResponseProto::decode(response.value.as_slice())?) + } + + #[inline] + pub(crate) fn is_tx_amount_enough(&self, decimals: u8, amount: &BigDecimal) -> bool { + let min_tx_amount = big_decimal_from_sat(MIN_TX_SATOSHIS, decimals); + amount >= &min_tx_amount + } + + async fn search_for_swap_tx_spend( + &self, + input: SearchForSwapTxSpendInput<'_>, + ) -> MmResult, SearchForSwapTxSpendErr> { + let tx = cosmrs::Tx::from_bytes(input.tx)?; + let first_message = tx + .body + .messages + .first() + .or_mm_err(|| SearchForSwapTxSpendErr::TxMessagesEmpty)?; + let htlc_proto = CreateHtlcProtoRep::decode(first_message.value.as_slice())?; + let htlc = MsgCreateHtlc::try_from(htlc_proto)?; + let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, htlc.amount, input.secret_hash); + + let htlc_response = self.query_htlc(htlc_id.clone()).await?; + let htlc_data = match htlc_response.htlc { + Some(htlc) => htlc, + None => return Ok(None), + }; + + match htlc_data.state { + HTLC_STATE_OPEN => Ok(None), + HTLC_STATE_COMPLETED => { + let events_string = format!("claim_htlc.id='{}'", htlc_id); + let request = GetTxsEventRequest { + events: vec![events_string], + pagination: None, + order_by: TendermintResultOrder::Ascending as i32, + }; + let encoded_request = request.encode_to_vec(); + + let path = AbciPath::from_str(ABCI_GET_TXS_EVENT_PATH).expect("valid path"); + let response = self + .rpc_client() + .await? + .abci_query( + Some(path), + encoded_request.as_slice(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await + .map_to_mm(TendermintCoinRpcError::from)?; + let response = GetTxsEventResponse::decode(response.value.as_slice())?; + match response.txs.first() { + Some(tx) => { + let tx = TransactionEnum::CosmosTransaction(CosmosTransaction { + data: TxRaw { + body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + signatures: tx.signatures.clone(), + }, + }); + Ok(Some(FoundSwapTxSpend::Spent(tx))) + }, + None => MmError::err(SearchForSwapTxSpendErr::ClaimHtlcTxNotFound), + } + }, + HTLC_STATE_REFUNDED => { + // HTLC is refunded automatically without transaction. We have to return dummy tx data + Ok(Some(FoundSwapTxSpend::Refunded(TransactionEnum::CosmosTransaction( + CosmosTransaction { data: TxRaw::default() }, + )))) + }, + unexpected_state => MmError::err(SearchForSwapTxSpendErr::UnexpectedHtlcState(unexpected_state)), + } + } +} + +fn clients_from_urls(rpc_urls: &[String]) -> MmResult, TendermintInitErrorKind> { + if rpc_urls.is_empty() { + return MmError::err(TendermintInitErrorKind::EmptyRpcUrls); + } + let mut clients = Vec::new(); + let mut errors = Vec::new(); + // check that all urls are valid + // keep all invalid urls in one vector to show all of them in error + for url in rpc_urls.iter() { + match HttpClient::new(url.as_str()) { + Ok(client) => clients.push(client), + Err(e) => errors.push(format!("Url {} is invalid, got error {}", url, e)), + } + } + drop_mutability!(clients); + drop_mutability!(errors); + if !errors.is_empty() { + let errors: String = errors.into_iter().join(", "); + return MmError::err(TendermintInitErrorKind::RpcClientInitError(errors)); + } + Ok(clients) +} + +#[async_trait] +#[allow(unused_variables)] +impl MmCoin for TendermintCoin { + fn is_asset_chain(&self) -> bool { false } + + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { + let coin = self.clone(); + let fut = async move { + let to_address = + AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + if to_address.prefix() != coin.account_prefix { + return MmError::err(WithdrawError::InvalidAddress(format!( + "expected {} address prefix", + coin.account_prefix + ))); + } + let balance_denom = coin.balance_for_denom(coin.denom.to_string()).await?; + let balance_dec = big_decimal_from_sat_unsigned(balance_denom, coin.decimals); + + // << BEGIN TX SIMULATION FOR FEE CALCULATION + let (amount_denom, amount_dec, total_amount) = if req.max { + let amount_denom = balance_denom; + ( + amount_denom, + big_decimal_from_sat_unsigned(amount_denom, coin.decimals), + balance_dec.clone(), + ) + } else { + let total = req.amount.clone(); + + ( + sat_from_big_decimal(&req.amount, coin.decimals)?, + req.amount.clone(), + total, + ) + }; + + if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { + return MmError::err(WithdrawError::AmountTooLow { + amount: amount_dec, + threshold: coin.min_tx_amount(), + }); + } + + let received_by_me = if to_address == coin.account_id { + amount_dec + } else { + BigDecimal::default() + }; + + let msg_send = MsgSend { + from_address: coin.account_id.clone(), + to_address: to_address.clone(), + amount: vec![Coin { + denom: coin.denom.clone(), + amount: amount_denom.into(), + }], + } + .to_any() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); + let current_block = coin + .current_block() + .compat() + .await + .map_to_mm(WithdrawError::Transport)?; + + let _sequence_lock = coin.sequence_lock.lock().await; + let account_info = coin.my_account_info().await?; + + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + + let simulated_tx = coin + .gen_simulated_tx(account_info.clone(), msg_send.clone(), timeout_height, memo.clone()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + // >> END TX SIMULATION FOR FEE CALCULATION + + let fee_amount_u64 = coin.calculate_fee_amount_as_u64(simulated_tx).await?; + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); + + let fee_amount = Coin { + denom: coin.denom.clone(), + amount: fee_amount_u64.into(), + }; + + let fee = Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT); + + let (amount_denom, amount_dec, total_amount) = if req.max { + if balance_denom < fee_amount_u64 { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: balance_dec, + required: fee_amount_dec, + }); + } + let amount_denom = balance_denom - fee_amount_u64; + ( + amount_denom, + big_decimal_from_sat_unsigned(amount_denom, coin.decimals), + balance_dec, + ) + } else { + let total = &req.amount + &fee_amount_dec; + if balance_dec < total { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: balance_dec, + required: total, + }); + } + + ( + sat_from_big_decimal(&req.amount, coin.decimals)?, + req.amount.clone(), + total, + ) + }; + + let msg_send = MsgSend { + from_address: coin.account_id.clone(), + to_address, + amount: vec![Coin { + denom: coin.denom.clone(), + amount: amount_denom.into(), + }], + } + .to_any() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let tx_raw = coin + .any_to_signed_raw_tx(account_info, msg_send, fee, timeout_height, memo.clone()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let tx_bytes = tx_raw + .to_bytes() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let hash = sha256(&tx_bytes); + Ok(TransactionDetails { + tx_hash: hex::encode_upper(hash.as_slice()), + tx_hex: tx_bytes.into(), + from: vec![coin.account_id.to_string()], + to: vec![req.to], + my_balance_change: &received_by_me - &total_amount, + spent_by_me: total_amount.clone(), + total_amount, + received_by_me, + block_height: 0, + timestamp: 0, + fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { + coin: coin.ticker.clone(), + amount: fee_amount_dec, + uamount: fee_amount_u64, + gas_limit: GAS_LIMIT_DEFAULT, + })), + coin: coin.ticker.to_string(), + internal_id: hash.to_vec().into(), + kmd_rewards: None, + transaction_type: TransactionType::default(), + memo: Some(memo), + }) + }; + Box::new(fut.boxed().compat()) + } + + fn get_raw_transaction(&self, mut req: RawTransactionRequest) -> RawTransactionFut { + let coin = self.clone(); + let fut = async move { + req.tx_hash.make_ascii_uppercase(); + let tx_from_rpc = coin.request_tx(req.tx_hash).await?; + Ok(RawTransactionRes { + tx_hex: tx_from_rpc.encode_to_vec().into(), + }) + }; + Box::new(fut.boxed().compat()) + } + + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { + let coin = self.clone(); + let hash = hex::encode_upper(H256::from(tx_hash.as_slice())); + let fut = async move { + let tx_from_rpc = coin.request_tx(hash).await?; + Ok(RawTransactionRes { + tx_hex: tx_from_rpc.encode_to_vec().into(), + }) + }; + Box::new(fut.boxed().compat()) + } + + fn decimals(&self) -> u8 { self.decimals } + + fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { + // TODO + Err("Not implemented".into()) + } + + fn validate_address(&self, address: &str) -> ValidateAddressResult { + match AccountId::from_str(address) { + Ok(account) if account.prefix() != self.account_prefix => ValidateAddressResult { + is_valid: false, + reason: Some(format!( + "Expected {} account prefix, got {}", + self.account_prefix, + account.prefix() + )), + }, + Ok(_) => ValidateAddressResult { + is_valid: true, + reason: None, + }, + Err(e) => ValidateAddressResult { + is_valid: false, + reason: Some(e.to_string()), + }, + } + } + + fn process_history_loop(&self, ctx: MmArc) -> Box + Send> { + warn!("process_history_loop is deprecated, tendermint uses tx_history_v2"); + Box::new(futures01::future::err(())) + } + + fn history_sync_status(&self) -> HistorySyncState { self.history_sync_state.lock().unwrap().clone() } + + fn get_trade_fee(&self) -> Box + Send> { + Box::new(futures01::future::err("Not implemented".into())) + } + + async fn get_sender_trade_fee( + &self, + value: TradePreimageValue, + _stage: FeeApproxStage, + ) -> TradePreimageResult { + let amount = match value { + TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => decimal, + }; + self.get_sender_trade_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, amount) + .await + } + + fn get_receiver_trade_fee(&self, send_amount: BigDecimal, stage: FeeApproxStage) -> TradePreimageFut { + let coin = self.clone(); + let fut = async move { + // We can't simulate Claim Htlc without having information about broadcasted htlc tx. + // Since create and claim htlc fees are almost same, we can simply simulate create htlc tx. + coin.get_sender_trade_fee_for_denom(coin.ticker.clone(), coin.denom.clone(), coin.decimals, send_amount) + .await + }; + Box::new(fut.boxed().compat()) + } + + async fn get_fee_to_send_taker_fee( + &self, + dex_fee_amount: BigDecimal, + _stage: FeeApproxStage, + ) -> TradePreimageResult { + self.get_fee_to_send_taker_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, dex_fee_amount) + .await + } + + fn required_confirmations(&self) -> u64 { 0 } + + fn requires_notarization(&self) -> bool { false } + + fn set_required_confirmations(&self, confirmations: u64) { + warn!("set_required_confirmations is not supported for tendermint") + } + + fn set_requires_notarization(&self, requires_nota: bool) { warn!("TendermintCoin doesn't support notarization") } + + fn swap_contract_address(&self) -> Option { None } + + fn fallback_swap_contract(&self) -> Option { None } + + fn mature_confirmations(&self) -> Option { None } + + fn coin_protocol_info(&self) -> Vec { Vec::new() } + + fn is_coin_protocol_supported(&self, info: &Option>) -> bool { true } + + fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.abortable_system) } + + fn on_token_deactivated(&self, _ticker: &str) {} +} + +impl MarketCoinOps for TendermintCoin { + fn ticker(&self) -> &str { &self.ticker } + + fn my_address(&self) -> MmResult { Ok(self.account_id.to_string()) } + + fn get_public_key(&self) -> Result> { + let key = SigningKey::from_bytes(&self.priv_key).expect("privkey validity is checked on coin creation"); + Ok(key.public_key().to_string()) + } + + fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { + // TODO + None + } + + fn sign_message(&self, _message: &str) -> SignatureResult { + // TODO + MmError::err(SignatureError::InternalError("Not implemented".into())) + } + + fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult { + // TODO + MmError::err(VerificationError::InternalError("Not implemented".into())) + } + + fn my_balance(&self) -> BalanceFut { + let coin = self.clone(); + let fut = async move { + let balance_denom = coin.balance_for_denom(coin.denom.to_string()).await?; + Ok(CoinBalance { + spendable: big_decimal_from_sat_unsigned(balance_denom, coin.decimals), + unspendable: BigDecimal::default(), + }) + }; + Box::new(fut.boxed().compat()) + } + + fn base_coin_balance(&self) -> BalanceFut { + Box::new(self.my_balance().map(|coin_balance| coin_balance.spendable)) + } + + fn platform_ticker(&self) -> &str { &self.ticker } + + fn send_raw_tx(&self, tx: &str) -> Box + Send> { + let tx_bytes = try_fus!(hex::decode(tx)); + self.send_raw_tx_bytes(&tx_bytes) + } + + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + // as sanity check + try_fus!(Raw::from_bytes(tx)); + + let coin = self.clone(); + let tx_bytes = tx.to_owned(); + let fut = async move { + let broadcast_res = try_s!( + try_s!(coin.rpc_client().await) + .broadcast_tx_commit(tx_bytes.into()) + .await + ); + if !broadcast_res.check_tx.code.is_ok() { + return ERR!("Tx check failed {:?}", broadcast_res.check_tx); + } + + if !broadcast_res.deliver_tx.code.is_ok() { + return ERR!("Tx deliver failed {:?}", broadcast_res.deliver_tx); + } + Ok(broadcast_res.hash.to_string()) + }; + Box::new(fut.boxed().compat()) + } + + fn wait_for_confirmations( + &self, + tx_bytes: &[u8], + _confirmations: u64, + _requires_nota: bool, + wait_until: u64, + check_every: u64, + ) -> Box + Send> { + // Sanity check + let _: TxRaw = try_fus!(Message::decode(tx_bytes)); + + let tx_hash = hex::encode_upper(sha256(tx_bytes)); + + let coin = self.clone(); + let fut = async move { + loop { + if now_ms() / 1000 > wait_until { + return ERR!( + "Waited too long until {} for payment {} to be received", + wait_until, + tx_hash.clone() + ); + } + + let tx_status_code = try_s!(coin.get_tx_status_code_or_none(tx_hash.clone()).await); + + if let Some(tx_status_code) = tx_status_code { + return match tx_status_code { + cosmrs::tendermint::abci::Code::Ok => Ok(()), + cosmrs::tendermint::abci::Code::Err(err_code) => Err(format!( + "Got error code: '{}' for tx: '{}'. Broadcasted tx isn't valid.", + err_code, tx_hash + )), + }; + }; + + Timer::sleep(check_every as f64).await; + } + }; + + Box::new(fut.boxed().compat()) + } + + fn wait_for_htlc_tx_spend( + &self, + transaction: &[u8], + secret_hash: &[u8], + wait_until: u64, + _from_block: u64, + _swap_contract_address: &Option, + _check_every: f64, + ) -> TransactionFut { + let tx = try_tx_fus!(cosmrs::Tx::from_bytes(transaction)); + let first_message = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); + let htlc_proto = try_tx_fus!(CreateHtlcProtoRep::decode(first_message.value.as_slice())); + let htlc = try_tx_fus!(MsgCreateHtlc::try_from(htlc_proto)); + let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, htlc.amount, secret_hash); + + let events_string = format!("claim_htlc.id='{}'", htlc_id); + let request = GetTxsEventRequest { + events: vec![events_string], + pagination: None, + order_by: TendermintResultOrder::Ascending as i32, + }; + let encoded_request = request.encode_to_vec(); + + let coin = self.clone(); + let path = try_tx_fus!(AbciPath::from_str(ABCI_GET_TXS_EVENT_PATH)); + let fut = async move { + loop { + let response = try_tx_s!( + try_tx_s!(coin.rpc_client().await) + .abci_query( + Some(path.clone()), + encoded_request.as_slice(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE + ) + .await + ); + let response = try_tx_s!(GetTxsEventResponse::decode(response.value.as_slice())); + if let Some(tx) = response.txs.first() { + return Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { + data: TxRaw { + body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + signatures: tx.signatures.clone(), + }, + })); + } + Timer::sleep(5.).await; + if get_utc_timestamp() > wait_until as i64 { + return Err(TransactionErr::Plain("Waited too long".into())); + } + } + }; + + Box::new(fut.boxed().compat()) + } + + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { + let tx_raw: TxRaw = Message::decode(bytes).map_to_mm(|e| TxMarshalingErr::InvalidInput(e.to_string()))?; + Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { data: tx_raw })) + } + + fn current_block(&self) -> Box + Send> { + let coin = self.clone(); + let fut = async move { + let info = try_s!(try_s!(coin.rpc_client().await).abci_info().await); + Ok(info.last_block_height.into()) + }; + Box::new(fut.boxed().compat()) + } + + fn display_priv_key(&self) -> Result { Ok(hex::encode(&self.priv_key)) } + + fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat(MIN_TX_SATOSHIS, self.decimals) } + + fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } +} + +#[async_trait] +#[allow(unused_variables)] +impl SwapOps for TendermintCoin { + fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, uuid: &[u8]) -> TransactionFut { + self.send_taker_fee_for_denom(fee_addr, amount, self.denom.clone(), self.decimals, uuid) + } + + fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { + self.send_htlc_for_denom( + maker_payment_args.time_lock_duration, + maker_payment_args.other_pubkey, + maker_payment_args.secret_hash, + maker_payment_args.amount, + self.denom.clone(), + self.decimals, + ) + } + + fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { + self.send_htlc_for_denom( + taker_payment_args.time_lock_duration, + taker_payment_args.other_pubkey, + taker_payment_args.secret_hash, + taker_payment_args.amount, + self.denom.clone(), + self.decimals, + ) + } + + fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, + ) -> TransactionFut { + let tx = try_tx_fus!(cosmrs::Tx::from_bytes(maker_spends_payment_args.other_payment_tx)); + let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); + let htlc_proto: CreateHtlcProtoRep = try_tx_fus!(prost::Message::decode(msg.value.as_slice())); + let htlc = try_tx_fus!(MsgCreateHtlc::try_from(htlc_proto)); + + let mut amount = htlc.amount.clone(); + amount.sort(); + drop_mutability!(amount); + + let coins_string = amount + .iter() + .map(|t| format!("{}{}", t.amount, t.denom)) + .collect::>() + .join(","); + + let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, amount, maker_spends_payment_args.secret_hash); + + let claim_htlc_tx = try_tx_fus!(self.gen_claim_htlc_tx(htlc_id, maker_spends_payment_args.secret)); + let coin = self.clone(); + + let fut = async move { + let _sequence_lock = coin.sequence_lock.lock().await; + let current_block = try_tx_s!(coin.current_block().compat().await); + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + + let account_info = try_tx_s!(coin.my_account_info().await); + + let simulated_tx = try_tx_s!(coin.gen_simulated_tx( + account_info.clone(), + claim_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + )); + + let fee = try_tx_s!(coin.calculate_fee(coin.denom.clone(), simulated_tx).await); + + let tx_raw = try_tx_s!(coin.any_to_signed_raw_tx( + account_info, + claim_htlc_tx.msg_payload, + fee, + timeout_height, + TX_DEFAULT_MEMO.into(), + )); + + let tx_id = try_tx_s!(coin.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await); + + Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { + data: tx_raw.into(), + })) + }; + + Box::new(fut.boxed().compat()) + } + + fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, + ) -> TransactionFut { + let tx = try_tx_fus!(cosmrs::Tx::from_bytes(taker_spends_payment_args.other_payment_tx)); + let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); + let htlc_proto: CreateHtlcProtoRep = try_tx_fus!(prost::Message::decode(msg.value.as_slice())); + let htlc = try_tx_fus!(MsgCreateHtlc::try_from(htlc_proto)); + + let mut amount = htlc.amount.clone(); + amount.sort(); + drop_mutability!(amount); + + let coins_string = amount + .iter() + .map(|t| format!("{}{}", t.amount, t.denom)) + .collect::>() + .join(","); + + let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, amount, taker_spends_payment_args.secret_hash); + + let claim_htlc_tx = try_tx_fus!(self.gen_claim_htlc_tx(htlc_id, taker_spends_payment_args.secret)); + let coin = self.clone(); + + let fut = async move { + let _sequence_lock = coin.sequence_lock.lock().await; + let current_block = try_tx_s!(coin.current_block().compat().await); + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + + let account_info = try_tx_s!(coin.my_account_info().await); + + let simulated_tx = try_tx_s!(coin.gen_simulated_tx( + account_info.clone(), + claim_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + )); + + let fee = try_tx_s!(coin.calculate_fee(coin.denom.clone(), simulated_tx).await); + + let tx_raw = try_tx_s!(coin.any_to_signed_raw_tx( + account_info, + claim_htlc_tx.msg_payload, + fee, + timeout_height, + TX_DEFAULT_MEMO.into(), + )); + + let tx_id = try_tx_s!(coin.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await); + + Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { + data: tx_raw.into(), + })) + }; + + Box::new(fut.boxed().compat()) + } + + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + Box::new(futures01::future::err(TransactionErr::Plain( + "Doesn't need transaction broadcast to refund IRIS HTLC".into(), + ))) + } + + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { + Box::new(futures01::future::err(TransactionErr::Plain( + "Doesn't need transaction broadcast to refund IRIS HTLC".into(), + ))) + } + + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + self.validate_fee_for_denom( + validate_fee_args.fee_tx, + validate_fee_args.expected_sender, + validate_fee_args.fee_addr, + validate_fee_args.amount, + self.decimals, + validate_fee_args.uuid, + self.denom.to_string(), + ) + } + + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + self.validate_payment_for_denom(input, self.denom.clone(), self.decimals) + } + + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + self.validate_payment_for_denom(input, self.denom.clone(), self.decimals) + } + + fn check_if_my_payment_sent( + &self, + if_my_payment_sent_args: CheckIfMyPaymentSentArgs, + ) -> Box, Error = String> + Send> { + self.check_if_my_payment_sent_for_denom( + self.decimals, + self.denom.clone(), + if_my_payment_sent_args.other_pub, + if_my_payment_sent_args.secret_hash, + if_my_payment_sent_args.amount, + ) + } + + async fn search_for_swap_tx_spend_my( + &self, + input: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + self.search_for_swap_tx_spend(input).await.map_err(|e| e.to_string()) + } + + async fn search_for_swap_tx_spend_other( + &self, + input: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + self.search_for_swap_tx_spend(input).await.map_err(|e| e.to_string()) + } + + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + let tx = try_s!(cosmrs::Tx::from_bytes(spend_tx)); + let msg = try_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); + let htlc_proto: super::iris::htlc_proto::ClaimHtlcProtoRep = + try_s!(prost::Message::decode(msg.value.as_slice())); + let htlc = try_s!(MsgClaimHtlc::try_from(htlc_proto)); + + Ok(try_s!(hex::decode(htlc.secret))) + } + + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + + // Todo + fn is_auto_refundable(&self) -> bool { false } + + // Todo + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } + + fn negotiate_swap_contract_addr( + &self, + other_side_address: Option<&[u8]>, + ) -> Result, MmError> { + Ok(None) + } + + #[inline] + fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { + key_pair_from_secret(&self.priv_key).expect("valid priv key") + } + + #[inline] + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + } + + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + PublicKey::from_raw_secp256k1(raw_pubkey) + .or_mm_err(|| ValidateOtherPubKeyErr::InvalidPubKey(hex::encode(raw_pubkey)))?; + Ok(()) + } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } +} + +#[async_trait] +impl TakerSwapMakerCoin for TendermintCoin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for TendermintCoin { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for TendermintCoin { + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &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_taker_payment_refund_preimage( + &self, + _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + async fn watcher_search_for_swap_tx_spend( + &self, + _input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } +} + +/// Processes the given `priv_key_policy` and returns corresponding `Secp256k1Secret`. +/// This function expects either [`PrivKeyBuildPolicy::IguanaPrivKey`] +/// or [`PrivKeyBuildPolicy::GlobalHDAccount`], otherwise returns `PrivKeyPolicyNotAllowed` error. +pub(crate) fn secret_from_priv_key_policy( + conf: &TendermintConf, + ticker: &str, + priv_key_policy: PrivKeyBuildPolicy, +) -> MmResult { + match priv_key_policy { + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(iguana), + PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { + let derivation_path = conf.derivation_path.as_ref().or_mm_err(|| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::DerivationPathIsNotSet, + })?; + global_hd + .derive_secp256k1_secret(derivation_path) + .mm_err(|e| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::InvalidPrivKey(e.to_string()), + }) + }, + PrivKeyBuildPolicy::Trezor => { + let kind = + TendermintInitErrorKind::PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed::HardwareWalletNotSupported); + MmError::err(TendermintInitError { + ticker: ticker.to_string(), + kind, + }) + }, + } +} + +#[cfg(test)] +pub mod tendermint_coin_tests { + use super::*; + + use common::{block_on, DEX_FEE_ADDR_RAW_PUBKEY}; + use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventResponse}; + use crypto::privkey::key_pair_from_seed; + use rand::{thread_rng, Rng}; + use std::mem::discriminant; + + pub const IRIS_TESTNET_HTLC_PAIR1_SEED: &str = "iris test seed"; + // pub const IRIS_TESTNET_HTLC_PAIR1_PUB_KEY: &str = &[ + // 2, 35, 133, 39, 114, 92, 150, 175, 252, 203, 124, 85, 243, 144, 11, 52, 91, 128, 236, 82, 104, 212, 131, 40, + // 79, 22, 40, 7, 119, 93, 50, 179, 43, + // ]; + // const IRIS_TESTNET_HTLC_PAIR1_ADDRESS: &str = "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2"; + + // const IRIS_TESTNET_HTLC_PAIR2_SEED: &str = "iris test2 seed"; + const IRIS_TESTNET_HTLC_PAIR2_PUB_KEY: &[u8] = &[ + 2, 90, 55, 151, 92, 7, 154, 117, 67, 96, 63, 202, 178, 78, 37, 101, 164, 173, 238, 60, 249, 175, 137, 52, 105, + 14, 16, 50, 130, 250, 64, 37, 17, + ]; + const IRIS_TESTNET_HTLC_PAIR2_ADDRESS: &str = "iaa1erfnkjsmalkwtvj44qnfr2drfzdt4n9ldh0kjv"; + + pub const IRIS_TESTNET_RPC_URL: &str = "http://34.80.202.172:26657"; + + const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; + const AVG_BLOCKTIME: u8 = 5; + + const SUCCEED_TX_HASH_SAMPLES: &[&str] = &[ + // https://nyancat.iobscan.io/#/tx?txHash=A010FC0AA33FC6D597A8635F9D127C0A7B892FAAC72489F4DADD90048CFE9279 + "A010FC0AA33FC6D597A8635F9D127C0A7B892FAAC72489F4DADD90048CFE9279", + // https://nyancat.iobscan.io/#/tx?txHash=54FD77054AE311C484CC2EADD4621428BB23D14A9BAAC128B0E7B47422F86EC8 + "54FD77054AE311C484CC2EADD4621428BB23D14A9BAAC128B0E7B47422F86EC8", + // https://nyancat.iobscan.io/#/tx?txHash=7C00FAE7F70C36A316A4736025B08A6EAA2A0CC7919A2C4FC4CD14D9FFD166F9 + "7C00FAE7F70C36A316A4736025B08A6EAA2A0CC7919A2C4FC4CD14D9FFD166F9", + ]; + + const FAILED_TX_HASH_SAMPLES: &[&str] = &[ + // https://nyancat.iobscan.io/#/tx?txHash=57EE62B2DF7E311C98C24AE2A53EB0FF2C16D289CECE0826CA1FF1108C91B3F9 + "57EE62B2DF7E311C98C24AE2A53EB0FF2C16D289CECE0826CA1FF1108C91B3F9", + // https://nyancat.iobscan.io/#/tx?txHash=F3181D69C580318DFD54282C656AC81113BC600BCFBAAA480E6D8A6469EE8786 + "F3181D69C580318DFD54282C656AC81113BC600BCFBAAA480E6D8A6469EE8786", + // https://nyancat.iobscan.io/#/tx?txHash=FE6F9F395DA94A14FCFC04E0E8C496197077D5F4968DA5528D9064C464ADF522 + "FE6F9F395DA94A14FCFC04E0E8C496197077D5F4968DA5528D9064C464ADF522", + ]; + + fn get_iris_usdc_ibc_protocol() -> TendermintProtocolInfo { + TendermintProtocolInfo { + decimals: 6, + denom: String::from("ibc/5C465997B4F582F602CD64E12031C6A6E18CAF1E6EDC9B5D808822DC0B5F850C"), + account_prefix: String::from("iaa"), + chain_id: String::from("nyancat-9"), + gas_price: None, + } + } + + fn get_iris_protocol() -> TendermintProtocolInfo { + TendermintProtocolInfo { + decimals: 6, + denom: String::from("unyan"), + account_prefix: String::from("iaa"), + chain_id: String::from("nyancat-9"), + gas_price: None, + } + } + + #[test] + fn test_tx_hash_str_from_bytes() { + let tx_hex = "0a97010a8f010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126f0a2d636f736d6f7331737661773061716334353834783832356a753775613033673578747877643061686c3836687a122d636f736d6f7331737661773061716334353834783832356a753775613033673578747877643061686c3836687a1a0f0a057561746f6d120631303030303018d998bf0512670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2102000eef4ab169e7b26a4a16c47420c4176ab702119ba57a8820fb3e53c8e7506212040a020801180312130a0d0a057561746f6d12043130303010a08d061a4093e5aec96f7d311d129f5ec8714b21ad06a75e483ba32afab86354400b2ac8350bfc98731bbb05934bf138282750d71aadbe08ceb6bb195f2b55e1bbfdddaaad"; + let expected_hash = "1C25ED7D17FCC5959409498D5423594666C4E84F15AF7B4AF17DF29B2AF9E7F5"; + + let tx_bytes = hex::decode(tx_hex).unwrap(); + let hash = sha256(&tx_bytes); + assert_eq!(hex::encode_upper(hash.as_slice()), expected_hash); + } + + #[test] + fn test_htlc_create_and_claim() { + let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; + + let protocol_conf = get_iris_usdc_ibc_protocol(); + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + + let coin = block_on(TendermintCoin::init( + &ctx, + "USDC-IBC".to_string(), + conf, + protocol_conf, + rpc_urls, + false, + priv_key_policy, + )) + .unwrap(); + + // << BEGIN HTLC CREATION + let base_denom: Denom = "unyan".parse().unwrap(); + let to: AccountId = IRIS_TESTNET_HTLC_PAIR2_ADDRESS.parse().unwrap(); + const UAMOUNT: u64 = 1; + let amount: cosmrs::Decimal = UAMOUNT.into(); + let amount_dec = big_decimal_from_sat_unsigned(UAMOUNT, coin.decimals); + let sec: [u8; 32] = thread_rng().gen(); + let time_lock = 1000; + + let create_htlc_tx = coin + .gen_create_htlc_tx(coin.denom.clone(), &to, amount, sha256(&sec).as_slice(), time_lock) + .unwrap(); + + let current_block_fut = coin.current_block().compat(); + let current_block = block_on(async { current_block_fut.await.unwrap() }); + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + + let account_info_fut = coin.my_account_info(); + let account_info = block_on(async { account_info_fut.await.unwrap() }); + + let simulated_tx = coin + .gen_simulated_tx( + account_info.clone(), + create_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .unwrap(); + + let fee = block_on(async { coin.calculate_fee(base_denom.clone(), simulated_tx).await.unwrap() }); + + let raw_tx = block_on(async { + coin.any_to_signed_raw_tx( + account_info.clone(), + create_htlc_tx.msg_payload.clone(), + fee, + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .unwrap() + }); + let tx_bytes = raw_tx.to_bytes().unwrap(); + + let send_tx_fut = coin.send_raw_tx_bytes(&tx_bytes).compat(); + block_on(async { + send_tx_fut.await.unwrap(); + }); + // >> END HTLC CREATION + + let htlc_spent = block_on( + coin.check_if_my_payment_sent(CheckIfMyPaymentSentArgs { + time_lock: 0, + other_pub: IRIS_TESTNET_HTLC_PAIR2_PUB_KEY, + secret_hash: sha256(&sec).as_slice(), + search_from_block: current_block, + swap_contract_address: &None, + swap_unique_data: &[], + amount: &amount_dec, + payment_instructions: &None, + }) + .compat(), + ) + .unwrap(); + assert!(htlc_spent.is_some()); + + // << BEGIN HTLC CLAIMING + let claim_htlc_tx = coin.gen_claim_htlc_tx(create_htlc_tx.id, &sec).unwrap(); + + let current_block_fut = coin.current_block().compat(); + let current_block = common::block_on(async { current_block_fut.await.unwrap() }); + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + + let account_info_fut = coin.my_account_info(); + let account_info = block_on(async { account_info_fut.await.unwrap() }); + + let simulated_tx = coin + .gen_simulated_tx( + account_info.clone(), + claim_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .unwrap(); + + let fee = block_on(async { coin.calculate_fee(base_denom.clone(), simulated_tx).await.unwrap() }); + + let raw_tx = coin + .any_to_signed_raw_tx( + account_info, + claim_htlc_tx.msg_payload, + fee, + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .unwrap(); + + let tx_bytes = raw_tx.to_bytes().unwrap(); + let send_tx_fut = coin.send_raw_tx_bytes(&tx_bytes).compat(); + block_on(async { + send_tx_fut.await.unwrap(); + }); + println!("Claim HTLC tx hash {}", hex::encode_upper(sha256(&tx_bytes).as_slice())); + // >> END HTLC CLAIMING + } + + #[test] + fn try_query_claim_htlc_txs_and_get_secret() { + let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; + + let protocol_conf = get_iris_usdc_ibc_protocol(); + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + + let coin = block_on(TendermintCoin::init( + &ctx, + "USDC-IBC".to_string(), + conf, + protocol_conf, + rpc_urls, + false, + priv_key_policy, + )) + .unwrap(); + + let events = "claim_htlc.id='2B925FC83A106CC81590B3DB108AC2AE496FFA912F368FE5E29BC1ED2B754F2C'"; + let request = GetTxsEventRequest { + events: vec![events.into()], + pagination: None, + order_by: TendermintResultOrder::Ascending as i32, + }; + let path = AbciPath::from_str(ABCI_GET_TXS_EVENT_PATH).unwrap(); + let response = block_on(block_on(coin.rpc_client()).unwrap().abci_query( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + )) + .unwrap(); + println!("{:?}", response); + + let response = GetTxsEventResponse::decode(response.value.as_slice()).unwrap(); + let tx = response.txs.first().unwrap(); + println!("{:?}", tx); + + let first_msg = tx.body.as_ref().unwrap().messages.first().unwrap(); + println!("{:?}", first_msg); + + let claim_htlc = + crate::tendermint::iris::htlc_proto::ClaimHtlcProtoRep::decode(first_msg.value.as_slice()).unwrap(); + let expected_secret = [1; 32]; + let actual_secret = hex::decode(claim_htlc.secret).unwrap(); + + assert_eq!(actual_secret, expected_secret); + } + + #[test] + fn wait_for_tx_spend_test() { + let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; + + let protocol_conf = get_iris_usdc_ibc_protocol(); + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + + let coin = block_on(TendermintCoin::init( + &ctx, + "USDC-IBC".to_string(), + conf, + protocol_conf, + rpc_urls, + false, + priv_key_policy, + )) + .unwrap(); + + // https://nyancat.iobscan.io/#/tx?txHash=2DB382CE3D9953E4A94957B475B0E8A98F5B6DDB32D6BF0F6A765D949CF4A727 + let create_tx_hash = "2DB382CE3D9953E4A94957B475B0E8A98F5B6DDB32D6BF0F6A765D949CF4A727"; + + let request = GetTxRequest { + hash: create_tx_hash.into(), + }; + + let path = AbciPath::from_str(ABCI_GET_TX_PATH).unwrap(); + let response = block_on(block_on(coin.rpc_client()).unwrap().abci_query( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + )) + .unwrap(); + println!("{:?}", response); + + let response = GetTxResponse::decode(response.value.as_slice()).unwrap(); + let tx = response.tx.unwrap(); + + println!("{:?}", tx); + + let encoded_tx = tx.encode_to_vec(); + + let secret_hash = hex::decode("0C34C71EBA2A51738699F9F3D6DAFFB15BE576E8ED543203485791B5DA39D10D").unwrap(); + let spend_tx = block_on( + coin.wait_for_htlc_tx_spend( + &encoded_tx, + &secret_hash, + get_utc_timestamp() as u64, + 0, + &None, + TAKER_PAYMENT_SPEND_SEARCH_INTERVAL, + ) + .compat(), + ) + .unwrap(); + + // https://nyancat.iobscan.io/#/tx?txHash=565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137 + let expected_spend_hash = "565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137"; + let hash = spend_tx.tx_hash(); + assert_eq!(hex::encode_upper(&hash.0), expected_spend_hash); + } + + #[test] + fn validate_taker_fee_test() { + let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; + + let protocol_conf = get_iris_protocol(); + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + + let coin = block_on(TendermintCoin::init( + &ctx, + "IRIS-TEST".to_string(), + conf, + protocol_conf, + rpc_urls, + false, + priv_key_policy, + )) + .unwrap(); + + // CreateHtlc tx, validation should fail because first message of dex fee tx must be MsgSend + // https://nyancat.iobscan.io/#/tx?txHash=2DB382CE3D9953E4A94957B475B0E8A98F5B6DDB32D6BF0F6A765D949CF4A727 + let create_htlc_tx_hash = "2DB382CE3D9953E4A94957B475B0E8A98F5B6DDB32D6BF0F6A765D949CF4A727"; + let create_htlc_tx_bytes = block_on(coin.request_tx(create_htlc_tx_hash.into())) + .unwrap() + .encode_to_vec(); + let create_htlc_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { + data: TxRaw::decode(create_htlc_tx_bytes.as_slice()).unwrap(), + }); + + let invalid_amount = 1.into(); + let validate_err = coin + .validate_fee(ValidateFeeArgs { + fee_tx: &create_htlc_tx, + expected_sender: &[], + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, + amount: &invalid_amount, + min_block_number: 0, + uuid: &[1; 16], + }) + .wait() + .unwrap_err(); + println!("{}", validate_err); + assert!(validate_err.contains("failed to decode Protobuf message: MsgSend.amount")); + + // just a random transfer tx not related to AtomicDEX, should fail on recipient address check + // https://nyancat.iobscan.io/#/tx?txHash=65815814E7D74832D87956144C1E84801DC94FE9A509D207A0ABC3F17775E5DF + let random_transfer_tx_hash = "65815814E7D74832D87956144C1E84801DC94FE9A509D207A0ABC3F17775E5DF"; + let random_transfer_tx_bytes = block_on(coin.request_tx(random_transfer_tx_hash.into())) + .unwrap() + .encode_to_vec(); + + let random_transfer_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { + data: TxRaw::decode(random_transfer_tx_bytes.as_slice()).unwrap(), + }); + + let validate_err = coin + .validate_fee(ValidateFeeArgs { + fee_tx: &random_transfer_tx, + expected_sender: &[], + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, + amount: &invalid_amount, + min_block_number: 0, + uuid: &[1; 16], + }) + .wait() + .unwrap_err(); + println!("{}", validate_err); + assert!(validate_err.contains("sent to wrong address")); + + // dex fee tx sent during real swap + // https://nyancat.iobscan.io/#/tx?txHash=8AA6B9591FE1EE93C8B89DE4F2C59B2F5D3473BD9FB5F3CFF6A5442BEDC881D7 + let dex_fee_hash = "8AA6B9591FE1EE93C8B89DE4F2C59B2F5D3473BD9FB5F3CFF6A5442BEDC881D7"; + let dex_fee_tx = block_on(coin.request_tx(dex_fee_hash.into())).unwrap(); + + let pubkey = dex_fee_tx.auth_info.as_ref().unwrap().signer_infos[0] + .public_key + .as_ref() + .unwrap() + .value[2..] + .to_vec(); + let dex_fee_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { + data: TxRaw::decode(dex_fee_tx.encode_to_vec().as_slice()).unwrap(), + }); + + let validate_err = coin + .validate_fee(ValidateFeeArgs { + fee_tx: &dex_fee_tx, + expected_sender: &[], + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, + amount: &invalid_amount, + min_block_number: 0, + uuid: &[1; 16], + }) + .wait() + .unwrap_err(); + println!("{}", validate_err); + assert!(validate_err.contains("Invalid amount")); + + let valid_amount: BigDecimal = "0.0001".parse().unwrap(); + // valid amount but invalid sender + let validate_err = coin + .validate_fee(ValidateFeeArgs { + fee_tx: &dex_fee_tx, + expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, + amount: &valid_amount, + min_block_number: 0, + uuid: &[1; 16], + }) + .wait() + .unwrap_err(); + println!("{}", validate_err); + assert!(validate_err.contains("Invalid sender")); + + // invalid memo + let validate_err = coin + .validate_fee(ValidateFeeArgs { + fee_tx: &dex_fee_tx, + expected_sender: &pubkey, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, + amount: &valid_amount, + min_block_number: 0, + uuid: &[1; 16], + }) + .wait() + .unwrap_err(); + println!("{}", validate_err); + assert!(validate_err.contains("Invalid memo")); + + // https://nyancat.iobscan.io/#/tx?txHash=5939A9D1AF57BB828714E0C4C4D7F2AEE349BB719B0A1F25F8FBCC3BB227C5F9 + let fee_with_memo_hash = "5939A9D1AF57BB828714E0C4C4D7F2AEE349BB719B0A1F25F8FBCC3BB227C5F9"; + let fee_with_memo_tx = block_on(coin.request_tx(fee_with_memo_hash.into())).unwrap(); + + let pubkey = fee_with_memo_tx.auth_info.as_ref().unwrap().signer_infos[0] + .public_key + .as_ref() + .unwrap() + .value[2..] + .to_vec(); + + let fee_with_memo_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { + data: TxRaw::decode(fee_with_memo_tx.encode_to_vec().as_slice()).unwrap(), + }); + + let uuid: Uuid = "cae6011b-9810-4710-b784-1e5dd0b3a0d0".parse().unwrap(); + let amount: BigDecimal = "0.0001".parse().unwrap(); + block_on( + coin.validate_fee_for_denom( + &fee_with_memo_tx, + &pubkey, + &DEX_FEE_ADDR_RAW_PUBKEY, + &amount, + 6, + uuid.as_bytes(), + "nim".into(), + ) + .compat(), + ) + .unwrap(); + } + + #[test] + fn validate_payment_test() { + let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; + + let protocol_conf = get_iris_protocol(); + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + + let coin = block_on(TendermintCoin::init( + &ctx, + "IRIS-TEST".to_string(), + conf, + protocol_conf, + rpc_urls, + false, + priv_key_policy, + )) + .unwrap(); + + // just a random transfer tx not related to AtomicDEX, should fail because the message is not CreateHtlc + // https://nyancat.iobscan.io/#/tx?txHash=65815814E7D74832D87956144C1E84801DC94FE9A509D207A0ABC3F17775E5DF + let random_transfer_tx_hash = "65815814E7D74832D87956144C1E84801DC94FE9A509D207A0ABC3F17775E5DF"; + let random_transfer_tx_bytes = block_on(coin.request_tx(random_transfer_tx_hash.into())) + .unwrap() + .encode_to_vec(); + + let input = ValidatePaymentInput { + payment_tx: random_transfer_tx_bytes, + time_lock_duration: 0, + time_lock: 0, + other_pub: Vec::new(), + secret_hash: Vec::new(), + amount: Default::default(), + swap_contract_address: None, + try_spv_proof_until: 0, + confirmations: 0, + unique_swap_data: Vec::new(), + }; + let validate_err = coin.validate_taker_payment(input).wait().unwrap_err(); + match validate_err.into_inner() { + ValidatePaymentError::WrongPaymentTx(e) => assert!(e.contains("Incorrect CreateHtlc message")), + unexpected => panic!("Unexpected error variant {:?}", unexpected), + }; + + // The HTLC that was already claimed or refunded should not pass the validation + // https://nyancat.iobscan.io/#/tx?txHash=93CF377D470EB27BD6E2C5B95BFEFE99359F95B88C70D785B34D1D2C670201B9 + let claimed_htlc_tx_hash = "93CF377D470EB27BD6E2C5B95BFEFE99359F95B88C70D785B34D1D2C670201B9"; + let claimed_htlc_tx_bytes = block_on(coin.request_tx(claimed_htlc_tx_hash.into())) + .unwrap() + .encode_to_vec(); + + let input = ValidatePaymentInput { + payment_tx: claimed_htlc_tx_bytes, + time_lock_duration: 20000, + time_lock: 1664984893, + other_pub: hex::decode("025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa402511").unwrap(), + secret_hash: hex::decode("441d0237e93677d3458e1e5a2e69f61e3622813521bf048dd56290306acdd134").unwrap(), + amount: "0.01".parse().unwrap(), + swap_contract_address: None, + try_spv_proof_until: 0, + confirmations: 0, + unique_swap_data: Vec::new(), + }; + let validate_err = block_on( + coin.validate_payment_for_denom(input, "nim".parse().unwrap(), 6) + .compat(), + ) + .unwrap_err(); + match validate_err.into_inner() { + ValidatePaymentError::UnexpectedPaymentState(_) => (), + unexpected => panic!("Unexpected error variant {:?}", unexpected), + }; + } + + #[test] + fn test_search_for_swap_tx_spend_spent() { + let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; + + let protocol_conf = get_iris_protocol(); + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + + let coin = block_on(TendermintCoin::init( + &ctx, + "IRIS-TEST".to_string(), + conf, + protocol_conf, + rpc_urls, + false, + priv_key_policy, + )) + .unwrap(); + + // https://nyancat.iobscan.io/#/tx?txHash=2DB382CE3D9953E4A94957B475B0E8A98F5B6DDB32D6BF0F6A765D949CF4A727 + let create_tx_hash = "2DB382CE3D9953E4A94957B475B0E8A98F5B6DDB32D6BF0F6A765D949CF4A727"; + + let request = GetTxRequest { + hash: create_tx_hash.into(), + }; + + let path = AbciPath::from_str(ABCI_GET_TX_PATH).unwrap(); + let response = block_on(block_on(coin.rpc_client()).unwrap().abci_query( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + )) + .unwrap(); + println!("{:?}", response); + + let response = GetTxResponse::decode(response.value.as_slice()).unwrap(); + let tx = response.tx.unwrap(); + + println!("{:?}", tx); + + let encoded_tx = tx.encode_to_vec(); + + let secret_hash = hex::decode("0C34C71EBA2A51738699F9F3D6DAFFB15BE576E8ED543203485791B5DA39D10D").unwrap(); + let input = SearchForSwapTxSpendInput { + time_lock: 0, + other_pub: &[], + secret_hash: &secret_hash, + tx: &encoded_tx, + search_from_block: 0, + swap_contract_address: &None, + swap_unique_data: &[], + }; + + let spend_tx = match block_on(coin.search_for_swap_tx_spend_my(input)).unwrap().unwrap() { + FoundSwapTxSpend::Spent(tx) => tx, + unexpected => panic!("Unexpected search_for_swap_tx_spend_my result {:?}", unexpected), + }; + + // https://nyancat.iobscan.io/#/tx?txHash=565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137 + let expected_spend_hash = "565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137"; + let hash = spend_tx.tx_hash(); + assert_eq!(hex::encode_upper(&hash.0), expected_spend_hash); + } + + #[test] + fn test_search_for_swap_tx_spend_refunded() { + let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; + + let protocol_conf = get_iris_protocol(); + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + + let coin = block_on(TendermintCoin::init( + &ctx, + "IRIS-TEST".to_string(), + conf, + protocol_conf, + rpc_urls, + false, + priv_key_policy, + )) + .unwrap(); + + // https://nyancat.iobscan.io/#/tx?txHash=BD1A76F43E8E2C7A1104EE363D63455CD50C76F2BFE93B703235F0A973061297 + let create_tx_hash = "BD1A76F43E8E2C7A1104EE363D63455CD50C76F2BFE93B703235F0A973061297"; + + let request = GetTxRequest { + hash: create_tx_hash.into(), + }; + + let path = AbciPath::from_str(ABCI_GET_TX_PATH).unwrap(); + let response = block_on(block_on(coin.rpc_client()).unwrap().abci_query( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + )) + .unwrap(); + println!("{:?}", response); + + let response = GetTxResponse::decode(response.value.as_slice()).unwrap(); + let tx = response.tx.unwrap(); + + println!("{:?}", tx); + + let encoded_tx = tx.encode_to_vec(); + + let secret_hash = hex::decode("cb11cacffdfc82060aa4a9a1bb9cc094c4141b170994f7642cd54d7e7af6743e").unwrap(); + let input = SearchForSwapTxSpendInput { + time_lock: 0, + other_pub: &[], + secret_hash: &secret_hash, + tx: &encoded_tx, + search_from_block: 0, + swap_contract_address: &None, + swap_unique_data: &[], + }; + + match block_on(coin.search_for_swap_tx_spend_my(input)).unwrap().unwrap() { + FoundSwapTxSpend::Refunded(tx) => { + let expected = TransactionEnum::CosmosTransaction(CosmosTransaction { data: TxRaw::default() }); + assert_eq!(expected, tx); + }, + unexpected => panic!("Unexpected search_for_swap_tx_spend_my result {:?}", unexpected), + }; + } + + #[test] + fn test_get_tx_status_code_or_none() { + let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; + let protocol_conf = get_iris_usdc_ibc_protocol(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + + let coin = common::block_on(TendermintCoin::init( + &ctx, + "USDC-IBC".to_string(), + conf, + protocol_conf, + rpc_urls, + false, + priv_key_policy, + )) + .unwrap(); + + for succeed_tx_hash in SUCCEED_TX_HASH_SAMPLES { + let status_code = common::block_on(coin.get_tx_status_code_or_none(succeed_tx_hash.to_string())) + .unwrap() + .expect("tx exists"); + + assert_eq!(status_code, cosmrs::tendermint::abci::Code::Ok); + } + + for failed_tx_hash in FAILED_TX_HASH_SAMPLES { + let status_code = common::block_on(coin.get_tx_status_code_or_none(failed_tx_hash.to_string())) + .unwrap() + .expect("tx exists"); + + assert_eq!( + discriminant(&status_code), + discriminant(&cosmrs::tendermint::abci::Code::Err(61)) + ); + } + + // Doesn't exists + let tx_hash = "0000000000000000000000000000000000000000000000000000000000000000".to_string(); + let status_code = common::block_on(coin.get_tx_status_code_or_none(tx_hash)).unwrap(); + assert!(status_code.is_none()); + } + + #[test] + fn test_wait_for_confirmations() { + const CHECK_INTERVAL: u64 = 2; + + let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; + let protocol_conf = get_iris_usdc_ibc_protocol(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + + let coin = common::block_on(TendermintCoin::init( + &ctx, + "USDC-IBC".to_string(), + conf, + protocol_conf, + rpc_urls, + false, + priv_key_policy, + )) + .unwrap(); + + let wait_until = || now_ms() + 45; + + for succeed_tx_hash in SUCCEED_TX_HASH_SAMPLES { + let tx_bytes = block_on(coin.request_tx(succeed_tx_hash.to_string())) + .unwrap() + .encode_to_vec(); + + block_on( + coin.wait_for_confirmations(&tx_bytes, 0, false, wait_until(), CHECK_INTERVAL) + .compat(), + ) + .unwrap(); + } + + for failed_tx_hash in FAILED_TX_HASH_SAMPLES { + let tx_bytes = block_on(coin.request_tx(failed_tx_hash.to_string())) + .unwrap() + .encode_to_vec(); + + block_on( + coin.wait_for_confirmations(&tx_bytes, 0, false, wait_until(), CHECK_INTERVAL) + .compat(), + ) + .unwrap_err(); + } + } +} diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs new file mode 100644 index 0000000000..3a0b6af9f1 --- /dev/null +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -0,0 +1,670 @@ +/// Module containing implementation for Tendermint Tokens. They include native assets + IBC +use super::{TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATOSHIS, TIMEOUT_HEIGHT_DELTA, + TX_DEFAULT_MEMO}; +use crate::utxo::utxo_common::big_decimal_from_sat; +use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFut, BigDecimal, + CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, + MakerSwapTakerCoin, MarketCoinOps, MmCoin, MyAddressError, NegotiateSwapContractAddrErr, + PaymentInstructions, PaymentInstructionsErr, RawTransactionFut, RawTransactionRequest, RefundError, + RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, + SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, + SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, + SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionType, + TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; +use async_trait::async_trait; +use bitcrypto::sha256; +use common::executor::abortable_queue::AbortableQueue; +use common::executor::{AbortableSystem, AbortedError}; +use common::log::warn; +use common::Future01CompatExt; +use cosmrs::{bank::MsgSend, + tx::{Fee, Msg}, + AccountId, Coin, Denom}; +use futures::{FutureExt, TryFutureExt}; +use futures01::Future; +use keys::KeyPair; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::MmNumber; +use rpc::v1::types::Bytes as BytesJson; +use serde_json::Value as Json; +use std::ops::Deref; +use std::str::FromStr; +use std::sync::Arc; + +pub struct TendermintTokenImpl { + pub ticker: String, + pub platform_coin: TendermintCoin, + pub decimals: u8, + pub denom: Denom, + /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation + /// or on [`MmArc::stop`]. + abortable_system: AbortableQueue, +} + +#[derive(Clone)] +pub struct TendermintToken(Arc); + +impl Deref for TendermintToken { + type Target = TendermintTokenImpl; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TendermintTokenProtocolInfo { + pub platform: String, + pub decimals: u8, + pub denom: String, +} + +#[derive(Clone, Deserialize)] +pub struct TendermintTokenActivationParams {} + +pub enum TendermintTokenInitError { + Internal(String), + InvalidDenom(String), + MyAddressError(String), + CouldNotFetchBalance(String), +} + +impl From for TendermintTokenInitError { + fn from(err: MyAddressError) -> Self { TendermintTokenInitError::MyAddressError(err.to_string()) } +} + +impl From for TendermintTokenInitError { + fn from(e: AbortedError) -> Self { TendermintTokenInitError::Internal(e.to_string()) } +} + +impl TendermintToken { + pub fn new( + ticker: String, + platform_coin: TendermintCoin, + decimals: u8, + denom: String, + ) -> MmResult { + let denom = Denom::from_str(&denom).map_to_mm(|e| TendermintTokenInitError::InvalidDenom(e.to_string()))?; + let token_impl = TendermintTokenImpl { + abortable_system: platform_coin.abortable_system.create_subsystem()?, + ticker, + platform_coin, + decimals, + denom, + }; + Ok(TendermintToken(Arc::new(token_impl))) + } +} + +#[async_trait] +#[allow(unused_variables)] +impl SwapOps for TendermintToken { + fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, uuid: &[u8]) -> TransactionFut { + self.platform_coin + .send_taker_fee_for_denom(fee_addr, amount, self.denom.clone(), self.decimals, uuid) + } + + fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { + self.platform_coin.send_htlc_for_denom( + maker_payment_args.time_lock_duration, + maker_payment_args.other_pubkey, + maker_payment_args.secret_hash, + maker_payment_args.amount, + self.denom.clone(), + self.decimals, + ) + } + + fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { + self.platform_coin.send_htlc_for_denom( + taker_payment_args.time_lock_duration, + taker_payment_args.other_pubkey, + taker_payment_args.secret_hash, + taker_payment_args.amount, + self.denom.clone(), + self.decimals, + ) + } + + fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, + ) -> TransactionFut { + self.platform_coin + .send_maker_spends_taker_payment(maker_spends_payment_args) + } + + fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, + ) -> TransactionFut { + self.platform_coin + .send_taker_spends_maker_payment(taker_spends_payment_args) + } + + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + Box::new(futures01::future::err(TransactionErr::Plain( + "Doesn't need transaction broadcast to be refunded".into(), + ))) + } + + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { + Box::new(futures01::future::err(TransactionErr::Plain( + "Doesn't need transaction broadcast to be refunded".into(), + ))) + } + + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + self.platform_coin.validate_fee_for_denom( + validate_fee_args.fee_tx, + validate_fee_args.expected_sender, + validate_fee_args.fee_addr, + validate_fee_args.amount, + self.decimals, + validate_fee_args.uuid, + self.denom.to_string(), + ) + } + + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + self.platform_coin + .validate_payment_for_denom(input, self.denom.clone(), self.decimals) + } + + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + self.platform_coin + .validate_payment_for_denom(input, self.denom.clone(), self.decimals) + } + + fn check_if_my_payment_sent( + &self, + if_my_payment_sent_args: CheckIfMyPaymentSentArgs, + ) -> Box, Error = String> + Send> { + self.platform_coin.check_if_my_payment_sent_for_denom( + self.decimals, + self.denom.clone(), + if_my_payment_sent_args.other_pub, + if_my_payment_sent_args.secret_hash, + if_my_payment_sent_args.amount, + ) + } + + async fn search_for_swap_tx_spend_my( + &self, + input: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + self.platform_coin.search_for_swap_tx_spend_my(input).await + } + + async fn search_for_swap_tx_spend_other( + &self, + input: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + self.platform_coin.search_for_swap_tx_spend_other(input).await + } + + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + self.platform_coin.extract_secret(secret_hash, spend_tx).await + } + + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + + // Todo + fn is_auto_refundable(&self) -> bool { false } + + // Todo + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } + + fn negotiate_swap_contract_addr( + &self, + other_side_address: Option<&[u8]>, + ) -> Result, MmError> { + self.platform_coin.negotiate_swap_contract_addr(other_side_address) + } + + #[inline] + fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { + self.platform_coin.derive_htlc_key_pair(swap_unique_data) + } + + #[inline] + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + } + + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + self.platform_coin.validate_other_pubkey(raw_pubkey) + } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } +} + +#[async_trait] +impl TakerSwapMakerCoin for TendermintToken { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for TendermintToken { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for TendermintToken { + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &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_taker_payment_refund_preimage( + &self, + _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + async fn watcher_search_for_swap_tx_spend( + &self, + _input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } +} + +impl MarketCoinOps for TendermintToken { + fn ticker(&self) -> &str { &self.ticker } + + fn my_address(&self) -> MmResult { self.platform_coin.my_address() } + + fn get_public_key(&self) -> Result> { + self.platform_coin.get_public_key() + } + + fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { self.platform_coin.sign_message_hash(message) } + + fn sign_message(&self, message: &str) -> SignatureResult { self.platform_coin.sign_message(message) } + + fn verify_message(&self, signature: &str, message: &str, address: &str) -> VerificationResult { + self.platform_coin.verify_message(signature, message, address) + } + + fn my_balance(&self) -> BalanceFut { + let coin = self.clone(); + let fut = async move { + let balance_denom = coin.platform_coin.balance_for_denom(coin.denom.to_string()).await?; + Ok(CoinBalance { + spendable: big_decimal_from_sat_unsigned(balance_denom, coin.decimals), + unspendable: BigDecimal::default(), + }) + }; + Box::new(fut.boxed().compat()) + } + + fn base_coin_balance(&self) -> BalanceFut { self.platform_coin.my_spendable_balance() } + + fn platform_ticker(&self) -> &str { self.platform_coin.ticker() } + + fn send_raw_tx(&self, tx: &str) -> Box + Send> { + self.platform_coin.send_raw_tx(tx) + } + + 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> { + self.platform_coin + .wait_for_confirmations(tx, confirmations, requires_nota, wait_until, check_every) + } + + fn wait_for_htlc_tx_spend( + &self, + transaction: &[u8], + secret_hash: &[u8], + wait_until: u64, + from_block: u64, + swap_contract_address: &Option, + check_every: f64, + ) -> TransactionFut { + self.platform_coin.wait_for_htlc_tx_spend( + transaction, + secret_hash, + wait_until, + from_block, + swap_contract_address, + check_every, + ) + } + + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { + self.platform_coin.tx_enum_from_bytes(bytes) + } + + 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 { big_decimal_from_sat(MIN_TX_SATOSHIS, self.decimals) } + + /// !! This function includes dummy implementation for P.O.C work + fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } +} + +#[async_trait] +#[allow(unused_variables)] +impl MmCoin for TendermintToken { + fn is_asset_chain(&self) -> bool { false } + + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { + let platform = self.platform_coin.clone(); + let token = self.clone(); + let fut = async move { + let to_address = + AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + if to_address.prefix() != platform.account_prefix { + return MmError::err(WithdrawError::InvalidAddress(format!( + "expected {} address prefix", + platform.account_prefix + ))); + } + + let base_denom_balance = platform.balance_for_denom(platform.denom.to_string()).await?; + let base_denom_balance_dec = big_decimal_from_sat_unsigned(base_denom_balance, token.decimals()); + + let balance_denom = platform.balance_for_denom(token.denom.to_string()).await?; + let balance_dec = big_decimal_from_sat_unsigned(balance_denom, token.decimals()); + + let (amount_denom, amount_dec, total_amount) = if req.max { + ( + balance_denom, + big_decimal_from_sat_unsigned(balance_denom, token.decimals), + balance_dec, + ) + } else { + if balance_dec < req.amount { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: token.ticker.clone(), + available: balance_dec, + required: req.amount, + }); + } + + ( + sat_from_big_decimal(&req.amount, token.decimals())?, + req.amount.clone(), + req.amount, + ) + }; + + if !platform.is_tx_amount_enough(token.decimals, &amount_dec) { + return MmError::err(WithdrawError::AmountTooLow { + amount: amount_dec, + threshold: token.min_tx_amount(), + }); + } + + let received_by_me = if to_address == platform.account_id { + amount_dec + } else { + BigDecimal::default() + }; + + let msg_send = MsgSend { + from_address: platform.account_id.clone(), + to_address, + amount: vec![Coin { + denom: token.denom.clone(), + amount: amount_denom.into(), + }], + } + .to_any() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); + let current_block = token + .current_block() + .compat() + .await + .map_to_mm(WithdrawError::Transport)?; + + let _sequence_lock = platform.sequence_lock.lock().await; + let account_info = platform.my_account_info().await?; + + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + + let simulated_tx = platform + .gen_simulated_tx(account_info.clone(), msg_send.clone(), timeout_height, memo.clone()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let fee_amount_u64 = platform.calculate_fee_amount_as_u64(simulated_tx).await?; + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); + + if base_denom_balance < fee_amount_u64 { + return MmError::err(WithdrawError::NotSufficientPlatformBalanceForFee { + coin: platform.ticker().to_string(), + available: base_denom_balance_dec, + required: fee_amount_dec, + }); + } + + let fee_amount = Coin { + denom: platform.denom.clone(), + amount: fee_amount_u64.into(), + }; + + let fee = Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT); + + let tx_raw = platform + .any_to_signed_raw_tx(account_info, msg_send, fee, timeout_height, memo.clone()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let tx_bytes = tx_raw + .to_bytes() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let hash = sha256(&tx_bytes); + Ok(TransactionDetails { + tx_hash: hex::encode_upper(hash.as_slice()), + tx_hex: tx_bytes.into(), + from: vec![platform.account_id.to_string()], + to: vec![req.to], + my_balance_change: &received_by_me - &total_amount, + spent_by_me: total_amount.clone(), + total_amount, + received_by_me, + block_height: 0, + timestamp: 0, + fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { + coin: platform.ticker().to_string(), + amount: fee_amount_dec, + uamount: fee_amount_u64, + gas_limit: GAS_LIMIT_DEFAULT, + })), + coin: token.ticker.clone(), + internal_id: hash.to_vec().into(), + kmd_rewards: None, + transaction_type: TransactionType::default(), + memo: Some(memo), + }) + }; + Box::new(fut.boxed().compat()) + } + + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { + self.platform_coin.get_raw_transaction(req) + } + + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { unimplemented!() } + + fn decimals(&self) -> u8 { self.decimals } + + fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { + self.platform_coin.convert_to_address(from, to_address_format) + } + + fn validate_address(&self, address: &str) -> ValidateAddressResult { self.platform_coin.validate_address(address) } + + fn process_history_loop(&self, ctx: MmArc) -> Box + Send> { + warn!("process_history_loop is deprecated, tendermint uses tx_history_v2"); + Box::new(futures01::future::err(())) + } + + fn history_sync_status(&self) -> HistorySyncState { HistorySyncState::NotEnabled } + + fn get_trade_fee(&self) -> Box + Send> { + Box::new(futures01::future::err("Not implemented".into())) + } + + async fn get_sender_trade_fee( + &self, + value: TradePreimageValue, + _stage: FeeApproxStage, + ) -> TradePreimageResult { + let amount = match value { + TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => decimal, + }; + + self.platform_coin + .get_sender_trade_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, amount) + .await + } + + fn get_receiver_trade_fee(&self, send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { + let token = self.clone(); + let fut = async move { + // We can't simulate Claim Htlc without having information about broadcasted htlc tx. + // Since create and claim htlc fees are almost same, we can simply simulate create htlc tx. + token + .platform_coin + .get_sender_trade_fee_for_denom(token.ticker.clone(), token.denom.clone(), token.decimals, send_amount) + .await + }; + Box::new(fut.boxed().compat()) + } + + async fn get_fee_to_send_taker_fee( + &self, + dex_fee_amount: BigDecimal, + _stage: FeeApproxStage, + ) -> TradePreimageResult { + self.platform_coin + .get_fee_to_send_taker_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, dex_fee_amount) + .await + } + + fn required_confirmations(&self) -> u64 { self.platform_coin.required_confirmations() } + + fn requires_notarization(&self) -> bool { self.platform_coin.requires_notarization() } + + fn set_required_confirmations(&self, confirmations: u64) { + warn!("set_required_confirmations is not supported for tendermint") + } + + fn set_requires_notarization(&self, requires_nota: bool) { + self.platform_coin.set_requires_notarization(requires_nota) + } + + fn swap_contract_address(&self) -> Option { self.platform_coin.swap_contract_address() } + + fn fallback_swap_contract(&self) -> Option { self.platform_coin.fallback_swap_contract() } + + fn mature_confirmations(&self) -> Option { None } + + fn coin_protocol_info(&self) -> Vec { self.platform_coin.coin_protocol_info() } + + fn is_coin_protocol_supported(&self, info: &Option>) -> bool { + self.platform_coin.is_coin_protocol_supported(info) + } + + fn on_disabled(&self) -> Result<(), AbortedError> { self.abortable_system.abort_all() } + + fn on_token_deactivated(&self, _ticker: &str) {} +} diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs new file mode 100644 index 0000000000..f833594401 --- /dev/null +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -0,0 +1,900 @@ +use super::{rpc::*, AllBalancesResult, TendermintCoin, TendermintCommons, TendermintToken}; + +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; +use crate::tendermint::{CustomTendermintMsgType, TendermintFeeDetails}; +use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; +use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; +use crate::{HistorySyncState, MarketCoinOps, MmCoin, TransactionDetails, TransactionType, TxFeeDetails}; +use async_trait::async_trait; +use bitcrypto::sha256; +use common::executor::Timer; +use common::log; +use common::state_machine::prelude::*; +use cosmrs::tendermint::abci::Code as TxCode; +use cosmrs::tendermint::abci::Event; +use cosmrs::tx::Fee; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmResult; +use mm2_number::BigDecimal; +use primitives::hash::H256; +use rpc::v1::types::Bytes as BytesJson; +use std::cmp; + +macro_rules! try_or_return_stopped_as_err { + ($exp:expr, $reason: expr, $fmt:literal) => { + match $exp { + Ok(t) => t, + Err(e) => { + return Err(Stopped { + phantom: Default::default(), + stop_reason: $reason(format!("{}: {}", $fmt, e)), + }) + }, + } + }; +} + +macro_rules! try_or_continue { + ($exp:expr, $fmt:literal) => { + match $exp { + Ok(t) => t, + Err(e) => { + log::debug!("{}: {}", $fmt, e); + continue; + }, + } + }; +} + +macro_rules! some_or_continue { + ($exp:expr) => { + match $exp { + Some(t) => t, + None => { + continue; + }, + } + }; +} + +macro_rules! some_or_return { + ($exp:expr) => { + match $exp { + Some(t) => t, + None => { + return; + }, + } + }; +} + +trait CoinCapabilities: TendermintCommons + CoinWithTxHistoryV2 + MmCoin + MarketCoinOps {} +impl CoinCapabilities for TendermintCoin {} + +#[async_trait] +impl CoinWithTxHistoryV2 for TendermintCoin { + fn history_wallet_id(&self) -> WalletId { WalletId::new(self.ticker().into()) } + + async fn get_tx_history_filters( + &self, + _target: MyTxHistoryTarget, + ) -> MmResult { + Ok(GetTxHistoryFilters::for_address(self.account_id.to_string())) + } +} + +#[async_trait] +impl CoinWithTxHistoryV2 for TendermintToken { + fn history_wallet_id(&self) -> WalletId { WalletId::new(self.platform_ticker().into()) } + + async fn get_tx_history_filters( + &self, + _target: MyTxHistoryTarget, + ) -> MmResult { + let denom_hash = sha256(self.denom.to_string().as_bytes()); + let id = H256::from(denom_hash.as_slice()); + + Ok(GetTxHistoryFilters::for_address(self.platform_coin.account_id.to_string()).with_token_id(id.to_string())) + } +} + +struct TendermintTxHistoryCtx { + coin: Coin, + storage: Storage, + balances: AllBalancesResult, + last_received_page: u32, + last_spent_page: u32, +} + +struct TendermintInit { + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl TendermintInit { + fn new() -> Self { + TendermintInit { + phantom: Default::default(), + } + } +} + +#[derive(Debug)] +enum StopReason { + StorageError(String), + RpcClient(String), +} + +struct Stopped { + phantom: std::marker::PhantomData<(Coin, Storage)>, + stop_reason: StopReason, +} + +impl Stopped { + fn storage_error(e: E) -> Self + where + E: std::fmt::Debug, + { + Stopped { + phantom: Default::default(), + stop_reason: StopReason::StorageError(format!("{:?}", e)), + } + } +} + +struct WaitForHistoryUpdateTrigger { + address: String, + last_height_state: u64, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl WaitForHistoryUpdateTrigger { + fn new(address: String, last_height_state: u64) -> Self { + WaitForHistoryUpdateTrigger { + address, + last_height_state, + phantom: Default::default(), + } + } +} + +struct OnIoErrorCooldown { + address: String, + last_block_height: u64, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl OnIoErrorCooldown { + fn new(address: String, last_block_height: u64) -> Self { + OnIoErrorCooldown { + address, + last_block_height, + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for OnIoErrorCooldown {} + +#[async_trait] +impl State for OnIoErrorCooldown +where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, +{ + type Ctx = TendermintTxHistoryCtx; + type Result = (); + + async fn on_changed(mut self: Box, _ctx: &mut Self::Ctx) -> StateResult { + Timer::sleep(30.).await; + + // retry history fetching process from last saved block + return Self::change_state(FetchingTransactionsData::new(self.address, self.last_block_height)); + } +} + +struct FetchingTransactionsData { + /// The list of addresses for those we have requested [`UpdatingUnconfirmedTxes::all_tx_ids_with_height`] TX hashes + /// at the `FetchingTxHashes` state. + address: String, + from_block_height: u64, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl FetchingTransactionsData { + fn new(address: String, from_block_height: u64) -> Self { + FetchingTransactionsData { + address, + phantom: Default::default(), + from_block_height, + } + } +} + +impl TransitionFrom> for Stopped {} +impl TransitionFrom> for FetchingTransactionsData {} +impl TransitionFrom> for FetchingTransactionsData {} +impl TransitionFrom> for OnIoErrorCooldown {} +impl TransitionFrom> for Stopped {} +impl TransitionFrom> for Stopped {} + +impl TransitionFrom> + for FetchingTransactionsData +{ +} + +impl TransitionFrom> + for WaitForHistoryUpdateTrigger +{ +} + +#[async_trait] +impl State for WaitForHistoryUpdateTrigger +where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, +{ + type Ctx = TendermintTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + loop { + Timer::sleep(30.).await; + + let ctx_balances = ctx.balances.clone(); + + let balances = match ctx.coin.all_balances().await { + Ok(balances) => balances, + Err(_) => { + return Self::change_state(OnIoErrorCooldown::new(self.address.clone(), self.last_height_state)); + }, + }; + + if balances != ctx_balances { + // Update balances + ctx.balances = balances; + + return Self::change_state(FetchingTransactionsData::new( + self.address.clone(), + self.last_height_state, + )); + } + } + } +} + +#[async_trait] +impl State for FetchingTransactionsData +where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, +{ + type Ctx = TendermintTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + const TX_PAGE_SIZE: u8 = 50; + + const DEFAULT_TRANSFER_EVENT_COUNT: usize = 1; + const CREATE_HTLC_EVENT: &str = "create_htlc"; + const CLAIM_HTLC_EVENT: &str = "claim_htlc"; + const TRANSFER_EVENT: &str = "transfer"; + const ACCEPTED_EVENTS: &[&str] = &[CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT, TRANSFER_EVENT]; + const RECIPIENT_TAG_KEY: &str = "recipient"; + const SENDER_TAG_KEY: &str = "sender"; + const RECEIVER_TAG_KEY: &str = "receiver"; + const AMOUNT_TAG_KEY: &str = "amount"; + + struct TxAmounts { + total: BigDecimal, + spent_by_me: BigDecimal, + received_by_me: BigDecimal, + } + + fn get_tx_amounts( + transfer_details: &TransferDetails, + is_self_transfer: bool, + sent_by_me: bool, + is_sign_claim_htlc: bool, + fee_details: Option<&TendermintFeeDetails>, + ) -> TxAmounts { + let amount = BigDecimal::from(transfer_details.amount); + + let total = if is_sign_claim_htlc && !is_self_transfer { + BigDecimal::default() + } else { + amount.clone() + }; + + let spent_by_me = + if sent_by_me && !matches!(transfer_details.transfer_event_type, TransferEventType::ClaimHtlc) { + amount.clone() + } else { + BigDecimal::default() + }; + + let received_by_me = if !sent_by_me || is_self_transfer { + amount + } else { + BigDecimal::default() + }; + + let mut tx_amounts = TxAmounts { + total, + spent_by_me, + received_by_me, + }; + + if let Some(fee_details) = fee_details { + tx_amounts.total += BigDecimal::from(fee_details.uamount); + tx_amounts.spent_by_me += BigDecimal::from(fee_details.uamount); + } + + tx_amounts + } + + fn get_fee_details(fee: Fee, coin: &Coin) -> Result + where + Coin: CoinCapabilities, + { + let fee_coin = fee + .amount + .first() + .ok_or_else(|| "fee coin can't be empty".to_string())?; + let fee_uamount: u64 = fee_coin.amount.to_string().parse().map_err(|e| format!("{:?}", e))?; + + Ok(TendermintFeeDetails { + coin: coin.platform_ticker().to_string(), + amount: big_decimal_from_sat_unsigned(fee_uamount, coin.decimals()), + uamount: fee_uamount, + gas_limit: fee.gas_limit.value(), + }) + } + + #[derive(Default, Clone)] + enum TransferEventType { + #[default] + Standard, + CreateHtlc, + ClaimHtlc, + } + + #[derive(Clone)] + struct TransferDetails { + from: String, + to: String, + denom: String, + amount: u64, + transfer_event_type: TransferEventType, + } + + // updates sender and receiver addresses if tx is htlc, and if not leaves as it is. + fn read_real_htlc_addresses(transfer_details: &mut TransferDetails, msg_event: &&Event) { + match msg_event.type_str.as_str() { + CREATE_HTLC_EVENT => { + transfer_details.from = some_or_return!(msg_event + .attributes + .iter() + .find(|tag| tag.key.to_string() == SENDER_TAG_KEY)) + .value + .to_string(); + + transfer_details.to = some_or_return!(msg_event + .attributes + .iter() + .find(|tag| tag.key.to_string() == RECEIVER_TAG_KEY)) + .value + .to_string(); + + transfer_details.transfer_event_type = TransferEventType::CreateHtlc; + }, + CLAIM_HTLC_EVENT => { + transfer_details.from = some_or_return!(msg_event + .attributes + .iter() + .find(|tag| tag.key.to_string() == SENDER_TAG_KEY)) + .value + .to_string(); + + transfer_details.transfer_event_type = TransferEventType::ClaimHtlc; + }, + _ => {}, + } + } + + fn parse_transfer_values_from_events(tx_events: Vec<&Event>) -> Vec { + let mut transfer_details_list: Vec = vec![]; + + for (index, event) in tx_events.iter().enumerate() { + if event.type_str.as_str() == TRANSFER_EVENT { + let amount_with_denoms = some_or_continue!(event + .attributes + .iter() + .find(|tag| tag.key.to_string() == AMOUNT_TAG_KEY)) + .value + .to_string(); + let amount_with_denoms = amount_with_denoms.split(','); + + for amount_with_denom in amount_with_denoms { + let extracted_amount: String = + amount_with_denom.chars().take_while(|c| c.is_numeric()).collect(); + let denom = &amount_with_denom[extracted_amount.len()..]; + let amount = some_or_continue!(extracted_amount.parse().ok()); + + let from = some_or_continue!(event + .attributes + .iter() + .find(|tag| tag.key.to_string() == SENDER_TAG_KEY)) + .value + .to_string(); + + let to = some_or_continue!(event + .attributes + .iter() + .find(|tag| tag.key.to_string() == RECIPIENT_TAG_KEY)) + .value + .to_string(); + + let mut tx_details = TransferDetails { + from, + to, + denom: denom.to_owned(), + amount, + // Default is Standard, can be changed later in read_real_htlc_addresses + transfer_event_type: TransferEventType::default(), + }; + + if index != 0 { + // If previous message is htlc related, that means current transfer + // addresses will be wrong. + if let Some(prev_event) = tx_events.get(index - 1) { + if [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&prev_event.type_str.as_str()) { + read_real_htlc_addresses(&mut tx_details, prev_event); + } + }; + } + + // sum the amounts coins and pairs are same + let mut duplicated_details = transfer_details_list.iter_mut().find(|details| { + details.from == tx_details.from + && details.to == tx_details.to + && details.denom == tx_details.denom + }); + + if let Some(duplicated_details) = &mut duplicated_details { + duplicated_details.amount += tx_details.amount; + } else { + transfer_details_list.push(tx_details); + } + } + } + } + + transfer_details_list + } + + fn get_transfer_details(tx_events: Vec, fee_amount_with_denom: String) -> Vec { + // Filter out irrelevant events + let mut events: Vec<&Event> = tx_events + .iter() + .filter(|event| ACCEPTED_EVENTS.contains(&event.type_str.as_str())) + .collect(); + + events.reverse(); + + if events.len() > DEFAULT_TRANSFER_EVENT_COUNT { + // Retain fee related events + events.retain(|event| { + if event.type_str == TRANSFER_EVENT { + let amount_with_denom = event + .attributes + .iter() + .find(|tag| tag.key.to_string() == AMOUNT_TAG_KEY) + .map(|t| t.value.to_string()); + + amount_with_denom != Some(fee_amount_with_denom.clone()) + } else { + true + } + }); + } + + parse_transfer_values_from_events(events) + } + + fn get_transaction_type( + transfer_event_type: &TransferEventType, + token_id: Option, + is_sign_claim_htlc: bool, + ) -> TransactionType { + match (transfer_event_type, token_id) { + (TransferEventType::CreateHtlc, token_id) => TransactionType::CustomTendermintMsg { + msg_type: CustomTendermintMsgType::SendHtlcAmount, + token_id, + }, + (TransferEventType::ClaimHtlc, token_id) => TransactionType::CustomTendermintMsg { + msg_type: if is_sign_claim_htlc { + CustomTendermintMsgType::SignClaimHtlc + } else { + CustomTendermintMsgType::ClaimHtlcAmount + }, + token_id, + }, + (_, Some(token_id)) => TransactionType::TokenTransfer(token_id), + _ => TransactionType::StandardTransfer, + } + } + + fn get_pair_addresses( + my_address: String, + tx_sent_by_me: bool, + transfer_details: &TransferDetails, + ) -> Option<(Vec, Vec)> { + match transfer_details.transfer_event_type { + TransferEventType::CreateHtlc => { + if tx_sent_by_me { + Some((vec![my_address], vec![])) + } else { + // This shouldn't happen if rpc node properly executes the tx search query. + None + } + }, + TransferEventType::ClaimHtlc => Some((vec![my_address], vec![])), + TransferEventType::Standard => { + Some((vec![transfer_details.from.clone()], vec![transfer_details.to.clone()])) + }, + } + } + + async fn fetch_and_insert_txs( + address: String, + coin: &Coin, + storage: &Storage, + query: String, + from_height: u64, + page: &mut u32, + ) -> Result> + where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, + { + let mut highest_height = from_height; + + let client = try_or_return_stopped_as_err!( + coin.rpc_client().await, + StopReason::RpcClient, + "could not get rpc client" + ); + + loop { + let response = try_or_return_stopped_as_err!( + client + .perform(TxSearchRequest::new( + query.clone(), + false, + *page, + TX_PAGE_SIZE, + TendermintResultOrder::Ascending.into(), + )) + .await, + StopReason::RpcClient, + "tx search rpc call failed" + ); + + let mut tx_details = vec![]; + let current_page_is_full = response.txs.len() == TX_PAGE_SIZE as usize; + for tx in response.txs { + if tx.tx_result.code != TxCode::Ok { + continue; + } + + let timestamp = try_or_return_stopped_as_err!( + coin.get_block_timestamp(i64::from(tx.height)).await, + StopReason::RpcClient, + "could not get block_timestamp over rpc node" + ); + let timestamp = some_or_continue!(timestamp); + + let tx_hash = tx.hash.to_string(); + + highest_height = cmp::max(highest_height, tx.height.into()); + + let deserialized_tx = try_or_continue!( + cosmrs::Tx::from_bytes(tx.tx.as_bytes()), + "Could not deserialize transaction" + ); + + let msg = try_or_continue!( + deserialized_tx.body.messages.first().ok_or("Tx body couldn't be read."), + "Tx body messages is empty" + ) + .value + .as_slice(); + + let fee_data = match deserialized_tx.auth_info.fee.amount.first() { + Some(data) => data, + None => { + log::debug!("Could not read transaction fee for tx '{}', skipping it", &tx_hash); + continue; + }, + }; + + let fee_amount_with_denom = format!("{}{}", fee_data.amount, fee_data.denom); + + let transfer_details_list = get_transfer_details(tx.tx_result.events, fee_amount_with_denom); + + if transfer_details_list.is_empty() { + log::debug!( + "Could not find transfer details in events for tx '{}', skipping it", + &tx_hash + ); + continue; + } + + let fee_details = try_or_continue!( + get_fee_details(deserialized_tx.auth_info.fee, coin), + "get_fee_details failed" + ); + + let mut fee_added = false; + for (index, transfer_details) in transfer_details_list.iter().enumerate() { + let mut internal_id_hash = index.to_le_bytes().to_vec(); + internal_id_hash.extend_from_slice(tx_hash.as_bytes()); + drop_mutability!(internal_id_hash); + + let internal_id = H256::from(internal_id_hash.as_slice()).reversed().to_vec().into(); + + if let Ok(Some(_)) = storage + .get_tx_from_history(&coin.history_wallet_id(), &internal_id) + .await + { + log::debug!("Tx '{}' already exists in tx_history. Skipping it.", &tx_hash); + continue; + } + + let tx_sent_by_me = address == transfer_details.from; + let is_platform_coin_tx = transfer_details.denom == coin.platform_denom(); + let is_self_tx = transfer_details.to == transfer_details.from && tx_sent_by_me; + let is_sign_claim_htlc = tx_sent_by_me + && matches!(transfer_details.transfer_event_type, TransferEventType::ClaimHtlc); + + let (from, to) = + some_or_continue!(get_pair_addresses(address.clone(), tx_sent_by_me, transfer_details)); + + let maybe_add_fees = if !fee_added + // if tx is platform coin tx and sent by me + && is_platform_coin_tx && tx_sent_by_me + { + fee_added = true; + Some(&fee_details) + } else { + None + }; + + let tx_amounts = get_tx_amounts( + transfer_details, + is_self_tx, + tx_sent_by_me, + is_sign_claim_htlc, + maybe_add_fees, + ); + + let token_id: Option = match !is_platform_coin_tx { + true => { + let denom_hash = sha256(transfer_details.denom.clone().as_bytes()); + Some(H256::from(denom_hash.as_slice()).to_vec().into()) + }, + false => None, + }; + + let transaction_type = get_transaction_type( + &transfer_details.transfer_event_type, + token_id.clone(), + is_sign_claim_htlc, + ); + + let details = TransactionDetails { + from, + to, + total_amount: tx_amounts.total, + spent_by_me: tx_amounts.spent_by_me, + received_by_me: tx_amounts.received_by_me, + // This can be 0 since it gets remapped in `coins::my_tx_history_v2` + my_balance_change: BigDecimal::default(), + tx_hash: tx_hash.to_string(), + tx_hex: msg.into(), + fee_details: Some(TxFeeDetails::Tendermint(fee_details.clone())), + block_height: tx.height.into(), + coin: transfer_details.denom.clone(), + internal_id, + timestamp, + kmd_rewards: None, + transaction_type, + memo: Some(deserialized_tx.body.memo.clone()), + }; + tx_details.push(details.clone()); + + // Display fees as extra transactions for asset txs sent by user + if tx_sent_by_me && !fee_added && !is_platform_coin_tx { + let fee_details = fee_details.clone(); + let mut fee_tx_details = details; + fee_tx_details.to = vec![]; + fee_tx_details.total_amount = fee_details.amount.clone(); + fee_tx_details.spent_by_me = fee_details.amount.clone(); + fee_tx_details.received_by_me = BigDecimal::default(); + fee_tx_details.my_balance_change = BigDecimal::default() - &fee_details.amount; + fee_tx_details.coin = coin.platform_ticker().to_string(); + // Non-reversed version of original internal id + fee_tx_details.internal_id = H256::from(internal_id_hash.as_slice()).to_vec().into(); + fee_tx_details.transaction_type = TransactionType::FeeForTokenTx; + + tx_details.push(fee_tx_details); + fee_added = true; + } + } + + log::debug!("Tx '{}' successfully parsed.", tx.hash); + } + + try_or_return_stopped_as_err!( + storage + .add_transactions_to_history(&coin.history_wallet_id(), tx_details) + .await + .map_err(|e| format!("{:?}", e)), + StopReason::StorageError, + "add_transactions_to_history failed" + ); + + if (*page * TX_PAGE_SIZE as u32) >= response.total_count { + // if last page is full, we can start with next page on next iteration + if current_page_is_full { + *page += 1; + } + break Ok(highest_height); + } + *page += 1; + } + } + + let q = format!("coin_spent.spender = '{}'", self.address); + let highest_send_tx_height = match fetch_and_insert_txs( + self.address.clone(), + &ctx.coin, + &ctx.storage, + q, + self.from_block_height, + &mut ctx.last_spent_page, + ) + .await + { + Ok(block) => block, + Err(stopped) => { + if let StopReason::RpcClient(e) = &stopped.stop_reason { + log::error!("Sent tx history process turned into cooldown mode due to rpc error: {e}"); + return Self::change_state(OnIoErrorCooldown::new(self.address.clone(), self.from_block_height)); + } + + return Self::change_state(stopped); + }, + }; + + let q = format!("coin_received.receiver = '{}'", self.address); + let highest_received_tx_height = match fetch_and_insert_txs( + self.address.clone(), + &ctx.coin, + &ctx.storage, + q, + self.from_block_height, + &mut ctx.last_received_page, + ) + .await + { + Ok(block) => block, + Err(stopped) => { + if let StopReason::RpcClient(e) = &stopped.stop_reason { + log::error!("Received tx history process turned into cooldown mode due to rpc error: {e}"); + return Self::change_state(OnIoErrorCooldown::new(self.address.clone(), self.from_block_height)); + } + + return Self::change_state(stopped); + }, + }; + + let last_fetched_block = cmp::max(highest_send_tx_height, highest_received_tx_height); + + log::info!( + "Tx history fetching finished for {}. Last fetched block {}", + ctx.coin.platform_ticker(), + last_fetched_block + ); + + ctx.coin.set_history_sync_state(HistorySyncState::Finished); + Self::change_state(WaitForHistoryUpdateTrigger::new( + self.address.clone(), + last_fetched_block, + )) + } +} + +#[async_trait] +impl State for TendermintInit +where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, +{ + type Ctx = TendermintTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + const INITIAL_SEARCH_HEIGHT: u64 = 0; + + ctx.coin.set_history_sync_state(HistorySyncState::NotStarted); + + if let Err(e) = ctx.storage.init(&ctx.coin.history_wallet_id()).await { + return Self::change_state(Stopped::storage_error(e)); + } + + let search_from = match ctx + .storage + .get_highest_block_height(&ctx.coin.history_wallet_id()) + .await + { + Ok(Some(height)) if height > 0 => height as u64 - 1, + _ => INITIAL_SEARCH_HEIGHT, + }; + + Self::change_state(FetchingTransactionsData::new( + ctx.coin.my_address().expect("my_address can't fail"), + search_from, + )) + } +} + +#[async_trait] +impl LastState for Stopped +where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, +{ + type Ctx = TendermintTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> Self::Result { + log::info!( + "Stopping tx history fetching for {}. Reason: {:?}", + ctx.coin.ticker(), + self.stop_reason + ); + + let new_state_json = json!({ + "message": format!("{:?}", self.stop_reason), + }); + + ctx.coin.set_history_sync_state(HistorySyncState::Error(new_state_json)); + } +} + +pub async fn tendermint_history_loop( + coin: TendermintCoin, + storage: impl TxHistoryStorage, + _ctx: MmArc, + _current_balance: BigDecimal, +) { + let balances = match coin.all_balances().await { + Ok(balances) => balances, + Err(e) => { + log::error!("{}", e); + return; + }, + }; + + let ctx = TendermintTxHistoryCtx { + coin, + storage, + balances, + last_received_page: 1, + last_spent_page: 1, + }; + + let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ctx); + state_machine.run(TendermintInit::new()).await; +} diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index fa8d4d1662..8b59b92cae 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -1,9 +1,19 @@ +#![allow(clippy::all)] + 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 crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, + FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, NegotiateSwapContractAddrErr, PaymentInstructions, + PaymentInstructionsErr, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, + SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, + SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, + SendWatcherRefundsPaymentArgs, SignatureResult, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; +use common::executor::AbortedError; use futures01::Future; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; @@ -29,11 +39,10 @@ impl TestCoin { } #[mockable] -#[allow(clippy::forget_ref, clippy::forget_copy, clippy::cast_ref_to_mut)] impl MarketCoinOps for TestCoin { fn ticker(&self) -> &str { &self.ticker } - fn my_address(&self) -> Result { unimplemented!() } + fn my_address(&self) -> MmResult { unimplemented!() } fn get_public_key(&self) -> Result> { unimplemented!() } @@ -67,17 +76,23 @@ impl MarketCoinOps for TestCoin { unimplemented!() } - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, transaction: &[u8], + secret_hash: &[u8], wait_until: u64, from_block: u64, swap_contract_address: &Option, + check_every: f64, ) -> TransactionFut { unimplemented!() } - fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result { unimplemented!() } + fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result> { + MmError::err(TxMarshalingErr::NotSupported( + "tx_enum_from_bytes is not supported for Test coin yet.".to_string(), + )) + } fn current_block(&self) -> Box + Send> { unimplemented!() } @@ -90,116 +105,46 @@ impl MarketCoinOps for TestCoin { #[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, 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_maker_payment(&self, _maker_payment_args: SendMakerPaymentArgs) -> 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_taker_payment(&self, _taker_payment_args: SendTakerPaymentArgs) -> 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], + _maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, ) -> 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], + _taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, ) -> 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 { + fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> 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 { + fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> 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> { + fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> Box + Send> { unimplemented!() } - fn validate_maker_payment( - &self, - _input: ValidatePaymentInput, - ) -> Box + Send> { - unimplemented!() - } + fn validate_maker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } - fn validate_taker_payment( - &self, - _input: ValidatePaymentInput, - ) -> Box + Send> { - unimplemented!() - } + fn validate_taker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentFut<()> { 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], + _if_my_payment_sent_args: CheckIfMyPaymentSentArgs, ) -> Box, Error = String> + Send> { unimplemented!() } @@ -218,7 +163,15 @@ impl SwapOps for TestCoin { unimplemented!() } - fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { unimplemented!() } fn negotiate_swap_contract_addr( &self, @@ -228,16 +181,132 @@ impl SwapOps for TestCoin { } fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { unimplemented!() } + + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { unimplemented!() } + + fn can_refund_htlc(&self, locktime: u64) -> Box + Send + '_> { + unimplemented!() + } + + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + unimplemented!() + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + unimplemented!() + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + unimplemented!() + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + unimplemented!() + } +} + +#[async_trait] +impl TakerSwapMakerCoin for TestCoin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for TestCoin { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +#[mockable] +impl WatcherOps for TestCoin { + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &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_taker_payment_refund_preimage( + &self, + _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + async fn watcher_search_for_swap_tx_spend( + &self, + input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + 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 spawner(&self) -> CoinFutSpawner { unimplemented!() } + fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { unimplemented!() } + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { unimplemented!() } fn decimals(&self) -> u8 { unimplemented!() } @@ -255,18 +324,20 @@ impl MmCoin for TestCoin { async fn get_sender_trade_fee( &self, - value: TradePreimageValue, - stage: FeeApproxStage, + _value: TradePreimageValue, + _stage: FeeApproxStage, ) -> TradePreimageResult { unimplemented!() } - fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { + unimplemented!() + } async fn get_fee_to_send_taker_fee( &self, - dex_fee_amount: BigDecimal, - stage: FeeApproxStage, + _dex_fee_amount: BigDecimal, + _stage: FeeApproxStage, ) -> TradePreimageResult { unimplemented!() } @@ -281,9 +352,15 @@ impl MmCoin for TestCoin { fn swap_contract_address(&self) -> Option { unimplemented!() } + fn fallback_swap_contract(&self) -> Option { unimplemented!() } + fn mature_confirmations(&self) -> Option { unimplemented!() } fn coin_protocol_info(&self) -> Vec { Vec::new() } fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } + + fn on_disabled(&self) -> Result<(), AbortedError> { Ok(()) } + + fn on_token_deactivated(&self, _ticker: &str) { () } } diff --git a/mm2src/coins/tx_history_storage/mod.rs b/mm2src/coins/tx_history_storage/mod.rs index 9fca3dcc3d..1f0ca4f8d8 100644 --- a/mm2src/coins/tx_history_storage/mod.rs +++ b/mm2src/coins/tx_history_storage/mod.rs @@ -23,7 +23,16 @@ mod tx_history_v2_tests; #[inline] pub fn token_id_from_tx_type(tx_type: &TransactionType) -> String { match tx_type { - TransactionType::TokenTransfer(token_id) => format!("{:02x}", token_id), + TransactionType::TokenTransfer(token_id) => { + format!("{:02x}", token_id) + }, + TransactionType::CustomTendermintMsg { token_id, .. } => { + if let Some(token_id) = token_id { + format!("{:02x}", token_id) + } else { + String::new() + } + }, _ => String::new(), } } @@ -52,8 +61,7 @@ impl<'a> TxHistoryStorageBuilder<'a> { } /// Whether transaction is unconfirmed or confirmed. -/// Serializes to either `0u8` or `1u8` correspondingly. -#[repr(u8)] +/// Serializes to either `0` or `1` correspondingly. #[derive(Clone, Copy, Debug)] pub enum ConfirmationStatus { Unconfirmed = 0, @@ -107,7 +115,7 @@ impl WalletId { #[inline] pub fn new(ticker: String) -> WalletId { WalletId { - ticker, + ticker: ticker.replace('-', "_"), hd_wallet_rmd160: None, } } @@ -122,33 +130,38 @@ impl WalletId { } } -#[derive(Debug, Default)] +#[derive(Clone, Debug)] pub struct GetTxHistoryFilters { token_id: Option, - for_addresses: Option, + for_addresses: FilteringAddresses, } 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 + pub fn for_address(address: String) -> GetTxHistoryFilters { + GetTxHistoryFilters { + token_id: None, + for_addresses: std::iter::once(address).collect(), + } } #[inline] - pub fn set_for_addresses>(&mut self, addresses: I) { - self.for_addresses = Some(addresses.into_iter().collect()); + pub fn for_addresses>(addresses: I) -> GetTxHistoryFilters { + GetTxHistoryFilters { + token_id: None, + for_addresses: addresses.into_iter().collect(), + } } #[inline] - pub fn with_for_addresses>(mut self, addresses: I) -> GetTxHistoryFilters { - self.set_for_addresses(addresses); + pub fn with_token_id(mut self, token_id: String) -> GetTxHistoryFilters { + self.set_token_id(token_id); self } + #[inline] + pub fn set_token_id(&mut self, token_id: String) { self.token_id = Some(token_id); } + /// 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() } 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 index a0bcf1c915..b418bba556 100644 --- a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs @@ -4,7 +4,7 @@ use crate::tx_history_storage::{token_id_from_tx_type, ConfirmationStatus, Creat use crate::TransactionDetails; use async_trait::async_trait; use common::{async_blocking, PagingOptionsEnum}; -use db_common::sql_query::SqlQuery; +use db_common::sql_build::*; 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}; @@ -155,6 +155,18 @@ fn select_tx_by_internal_id_sql(wallet_id: &WalletId) -> Result Result> { + let table_name = tx_history_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!( + "SELECT block_height FROM {} ORDER BY block_height DESC LIMIT 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)?; @@ -172,88 +184,141 @@ fn update_tx_in_table_by_internal_id_sql(wallet_id: &WalletId) -> Result Result> { +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 confirmation_status = {};", - table_name, - ConfirmationStatus::Unconfirmed.to_sql_param() - ); + let sql = format!("SELECT COUNT(id) FROM {} WHERE tx_hash = ?1;", table_name); Ok(sql) } -fn get_unconfirmed_transactions_sql(wallet_id: &WalletId) -> Result> { - let table_name = tx_history_table(wallet_id); +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 details_json FROM {} WHERE confirmation_status = {};", - table_name, - ConfirmationStatus::Unconfirmed.to_sql_param() - ); + let sql = format!("SELECT tx_hex FROM {} WHERE tx_hash = ?1 LIMIT 1;", table_name); 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)?; +/// Creates `SqlQuery` builder to query transactions from `tx_history` table +/// joining `tx_addresses` table and specifying from/to `for_addresses` addresses. +fn tx_history_with_addresses_builder_preimage<'a>( + connection: &'a Connection, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, +) -> Result, MmError> { + let mut sql_builder = SqlQuery::select_from_alias(connection, &tx_history_table(wallet_id), "tx_history")?; - let sql = format!("SELECT COUNT(id) FROM {} WHERE tx_hash = ?1;", table_name); + // Query transactions that were sent from/to `for_addresses` addresses. + let tx_address_table_name = tx_address_table(wallet_id); - Ok(sql) + 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")?; + + Ok(sql_builder) } -fn unique_tx_hashes_num_sql(wallet_id: &WalletId) -> Result> { - let table_name = tx_history_table(wallet_id); - validate_table_name(&table_name)?; +fn count_unique_tx_hashes_preimage<'a>( + connection: &'a Connection, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, +) -> Result, MmError> { + /// The alias is needed so that the external query can access the results of the subquery. + /// Example: + /// SUBQUERY: `SELECT h.tx_hash AS __TX_HASH_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(DISTINCT __TX_HASH_ALIAS) FROM ();` + /// Here we can't use `h.tx_hash` in the external query because it doesn't know about the `tx_history h` table. + /// So we need to give the `h.tx_hash` an alias like `__TX_HASH_ALIAS`. + const TX_HASH_ALIAS: &str = "__TX_HASH_ALIAS"; - let sql = format!("SELECT COUNT(DISTINCT tx_hash) FROM {};", table_name); + let subquery = { + let mut sql_builder = tx_history_with_addresses_builder_preimage(connection, wallet_id, for_addresses)?; - Ok(sql) + // Query `tx_hash` field and give it the `__TX_HASH_ALIAS` alias. + sql_builder.field_alias("tx_history.tx_hash", TX_HASH_ALIAS)?; + + drop_mutability!(sql_builder); + sql_builder.subquery() + }; + + let mut external_query = SqlQuery::select_from_subquery(subquery)?; + external_query.count_distinct(TX_HASH_ALIAS)?; + Ok(external_query) } -fn get_tx_hex_from_cache_sql(wallet_id: &WalletId) -> Result> { - let table_name = tx_cache_table(wallet_id); - validate_table_name(&table_name)?; +fn history_contains_unconfirmed_txes_preimage<'a>( + connection: &'a Connection, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, +) -> Result, MmError> { + /// The alias is needed so that the external query can access the results of the subquery. + /// Example: + /// SUBQUERY: `SELECT h.id AS __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(__ID_ALIAS) FROM ();` + /// Here we can't use `h.id` in the external query because it doesn't know about the `tx_history h` table. + /// So we need to give the `h.id` an alias like `__ID_ALIAS`. + const ID_ALIAS: &str = "__ID_ALIAS"; - let sql = format!("SELECT tx_hex FROM {} WHERE tx_hash = ?1 LIMIT 1;", table_name); + let subquery = { + let mut sql_builder = tx_history_with_addresses_builder_preimage(connection, wallet_id, for_addresses)?; - Ok(sql) + // Query `tx_hash` field and give it the `__ID_ALIAS` alias. + sql_builder + .field_alias("tx_history.id", ID_ALIAS)? + .and_where_eq("confirmation_status", ConfirmationStatus::Unconfirmed.to_sql_param())?; + + drop_mutability!(sql_builder); + sql_builder.subquery() + }; + + let mut external_query = SqlQuery::select_from_subquery(subquery)?; + external_query.count(ID_ALIAS)?; + Ok(external_query) } -/// 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>( +fn get_unconfirmed_txes_builder_preimage<'a>( connection: &'a Connection, wallet_id: &WalletId, - token_id: String, - for_addresses: Option, + for_addresses: FilteringAddresses, ) -> Result, MmError> { - let mut sql_builder = SqlQuery::select_from_alias(connection, &tx_history_table(wallet_id), "tx_history")?; + let mut sql_builder = tx_history_with_addresses_builder_preimage(connection, wallet_id, for_addresses)?; - // 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 + .field("details_json")? + .and_where_eq("confirmation_status", ConfirmationStatus::Unconfirmed.to_sql_param())?; - sql_builder - .join_alias(&tx_address_table_name, "tx_address")? - .on_join_eq("tx_history.internal_id", "tx_address.internal_id")?; + drop_mutability!(sql_builder); + Ok(sql_builder) +} - sql_builder - .and_where_in_params("tx_address.address", for_addresses)? - .group_by("tx_history.internal_id")?; - } +/// Creates an `SqlQuery` instance with the required `WHERE`, `ORDER`, `GROUP_BY` constraints. +/// +/// # Note +/// +/// 1) You can refer to the [`tx_history_table(wallet_id)`] table by the `tx_history` alias. +/// 2) The selected transactions will be ordered the same way as `compare_transaction_details` is implemented. +fn get_history_builder_preimage<'a>( + connection: &'a Connection, + wallet_id: &WalletId, + token_id: String, + for_addresses: FilteringAddresses, +) -> Result, MmError> { + let mut sql_builder = tx_history_with_addresses_builder_preimage(connection, wallet_id, for_addresses)?; + // Set other query conditions. sql_builder .and_where_eq_param("tx_history.token_id", token_id)? + // The following statements repeat the `compare_transaction_details` implementation: .order_asc("tx_history.confirmation_status")? .order_desc("tx_history.block_height")? - .order_asc("tx_history.id")?; + .order_asc("tx_history.internal_id")?; Ok(sql_builder) } @@ -287,10 +352,14 @@ fn tx_details_from_row(row: &Row<'_>) -> Result { json::from_str(&json_string).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e))) } +fn block_height_from_row(row: &Row<'_>) -> Result { row.get(0) } + impl TxHistoryStorageError for SqlError {} impl ConfirmationStatus { - fn to_sql_param(self) -> String { (self as u8).to_string() } + fn to_sql_param_str(self) -> String { (self as u8).to_string() } + + fn to_sql_param(self) -> i64 { self as i64 } } impl WalletId { @@ -393,7 +462,7 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { tx_hash, internal_id.clone(), tx.block_height.to_string(), - confirmation_status.to_sql_param(), + confirmation_status.to_sql_param_str(), token_id, tx_json, ]; @@ -457,13 +526,32 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { .await } - async fn history_contains_unconfirmed_txes(&self, wallet_id: &WalletId) -> Result> { - let sql = contains_unconfirmed_transactions_sql(wallet_id)?; + async fn get_highest_block_height(&self, wallet_id: &WalletId) -> Result, MmError> { + let sql = select_highest_block_height_sql(wallet_id)?; + let selfi = self.clone(); + + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + query_single_row(&conn, &sql, NO_PARAMS, block_height_from_row).map_to_mm(SqlError::from) + }) + .await + } + + async fn history_contains_unconfirmed_txes( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result> { + let wallet_id = wallet_id.clone(); 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))?; + let sql_query = history_contains_unconfirmed_txes_preimage(&conn, &wallet_id, for_addresses)?; + + let count_unconfirmed: u32 = sql_query + .query_single_row(|row| row.get(0))? + .or_mm_err(|| SqlError::QueryReturnedNoRows)?; Ok(count_unconfirmed > 0) }) .await @@ -472,15 +560,16 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { async fn get_unconfirmed_txes_from_history( &self, wallet_id: &WalletId, + for_addresses: FilteringAddresses, ) -> Result, MmError> { - let sql = get_unconfirmed_transactions_sql(wallet_id)?; + let wallet_id = wallet_id.clone(); 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::>()?; + + let sql_query = get_unconfirmed_txes_builder_preimage(&conn, &wallet_id, for_addresses)?; + let result = sql_query.query(tx_details_from_row)?; Ok(result) }) .await @@ -500,7 +589,7 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { let params = [ block_height, - confirmation_status.to_sql_param(), + confirmation_status.to_sql_param_str(), json_details, internal_id, ]; @@ -526,12 +615,21 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { .await } - async fn unique_tx_hashes_num_in_history(&self, wallet_id: &WalletId) -> Result> { - let sql = unique_tx_hashes_num_sql(wallet_id)?; + async fn unique_tx_hashes_num_in_history( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result> { let selfi = self.clone(); + let wallet_id = wallet_id.clone(); + async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let count: u32 = conn.query_row(&sql, NO_PARAMS, |row| row.get(0))?; + + let sql_query = count_unique_tx_hashes_preimage(&conn, &wallet_id, for_addresses)?; + let count: u32 = sql_query + .query_single_row(|row| row.get(0))? + .or_mm_err(|| SqlError::QueryReturnedNoRows)?; Ok(count as usize) }) .await @@ -570,7 +668,7 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { } let tx_hex = maybe_tx_hex?; let tx_bytes = - hex::decode(&tx_hex).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e)))?; + hex::decode(tx_hex).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e)))?; Ok(Some(tx_bytes.into())) }) .await @@ -583,9 +681,9 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { paging: PagingOptionsEnum, limit: usize, ) -> Result> { - // Check if [`GetTxHistoryFilters::for_addresses`] is specified and empty. + // Check if [`GetTxHistoryFilters::for_addresses`] is 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()) { + if filters.for_addresses.is_empty() { return Ok(GetHistoryResult { transactions: Vec::new(), skipped: 0, diff --git a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs index 8adefb6a74..69e9bb4eee 100644 --- a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs +++ b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs @@ -1,10 +1,14 @@ +//! Consider using very dirty [Rust script](https://pastebin.ubuntu.com/p/9r2mDmGGHT/) +//! to print all transactions from `../for_tests/tBCH_tx_history_fixtures.json` ordered. + use crate::my_tx_history_v2::{GetHistoryResult, TxHistoryStorage}; -use crate::tx_history_storage::{GetTxHistoryFilters, TxHistoryStorageBuilder, WalletId}; +use crate::tx_history_storage::{FilteringAddresses, 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::iter::FromIterator; use std::num::NonZeroUsize; const BCH_TX_HISTORY_STR: &str = include_str!("../for_tests/tBCH_tx_history_fixtures.json"); @@ -35,20 +39,6 @@ fn assert_get_history_result(actual: GetHistoryResult, expected_ids: Vec( - 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"); @@ -60,12 +50,20 @@ async fn test_add_transactions_impl() { let tx1 = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); let transactions = [tx1.clone(), tx1.clone()]; + let filters = GetTxHistoryFilters::for_address("bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66".to_string()); + let paging_options = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); + let limit = u32::MAX as usize; + // 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; + let actual_txs = storage + .get_history(&wallet_id, filters.clone(), paging_options.clone(), limit) + .await + .unwrap() + .transactions; assert!(actual_txs.is_empty()); let tx2 = get_bch_tx_details("c07836722bbdfa2404d8fe0ea56700d02e2012cb9dc100ccaf1138f334a759ce"); @@ -74,7 +72,11 @@ async fn test_add_transactions_impl() { .add_transactions_to_history(&wallet_id, transactions.clone()) .await .unwrap(); - let actual_txs = get_coin_history(&storage, &wallet_id).await; + let actual_txs = storage + .get_history(&wallet_id, filters, paging_options, limit) + .await + .unwrap() + .transactions; assert_eq!(actual_txs, transactions); } @@ -187,26 +189,84 @@ async fn test_contains_and_get_unconfirmed_transaction_impl() { storage.init(&wallet_id).await.unwrap(); - let mut tx_details = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); - tx_details.block_height = 0; + let mut tx1 = get_bch_tx_details("afa7785fdb0e49e649aa9b6467fa183c8185c398095baac2c11df50175a7f92b"); + tx1.block_height = 0; + let mut tx2 = get_bch_tx_details("06f38595a2d5d23df8a81a0d744ac3a70c3e46a01efa64a4be862b9d582167b0"); + tx2.block_height = 0; + let mut tx3 = get_bch_tx_details("0fcc9cf22ea2332c73cf6cb4cf89b764d1b936a1ef4d92a087e760378fe6b96e"); + tx3.block_height = 0; + storage - .add_transactions_to_history(&wallet_id, [tx_details.clone()]) + .add_transactions_to_history(&wallet_id, [tx1.clone(), tx2.clone(), tx3.clone()]) .await .unwrap(); - let contains_unconfirmed = storage.history_contains_unconfirmed_txes(&wallet_id).await.unwrap(); + let for_first_address = + FilteringAddresses::from_iter(["bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x".to_string()]); + + let contains_unconfirmed = storage + .history_contains_unconfirmed_txes(&wallet_id, for_first_address.clone()) + .await + .unwrap(); assert!(contains_unconfirmed); - let unconfirmed_transactions = storage.get_unconfirmed_txes_from_history(&wallet_id).await.unwrap(); + let unconfirmed_transactions = storage + .get_unconfirmed_txes_from_history(&wallet_id, for_first_address.clone()) + .await + .unwrap(); + // There only 2 unconfirmed transactions for `bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x` address. + assert_eq!(unconfirmed_transactions.len(), 2); + + tx1.block_height = 12345; + storage.update_tx_in_history(&wallet_id, &tx1).await.unwrap(); + + let unconfirmed_transactions = storage + .get_unconfirmed_txes_from_history(&wallet_id, for_first_address) + .await + .unwrap(); + // Now there is 1 unconfirmed transaction for `bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x` address. assert_eq!(unconfirmed_transactions.len(), 1); - tx_details.block_height = 12345; - storage.update_tx_in_history(&wallet_id, &tx_details).await.unwrap(); + let for_all_addresses = FilteringAddresses::from_iter([ + "bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x".to_string(), + "bchtest:qp5fphvvj3pvrrv2awhm7dyu8xjueydapg3ju9kwmm".to_string(), + ]); + let unconfirmed_transactions = storage + .get_unconfirmed_txes_from_history(&wallet_id, for_all_addresses) + .await + .unwrap(); + // 1 unconfirmed transaction for `bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x` + // and 1 unconfirmed transaction for `bchtest:qp5fphvvj3pvrrv2awhm7dyu8xjueydapg3ju9kwmm`. + assert_eq!(unconfirmed_transactions.len(), 2); + + tx3.block_height = 54321; + storage.update_tx_in_history(&wallet_id, &tx3).await.unwrap(); - let contains_unconfirmed = storage.history_contains_unconfirmed_txes(&wallet_id).await.unwrap(); + let for_second_address = + FilteringAddresses::from_iter(["bchtest:qp5fphvvj3pvrrv2awhm7dyu8xjueydapg3ju9kwmm".to_string()]); + let contains_unconfirmed = storage + .history_contains_unconfirmed_txes(&wallet_id, for_second_address.clone()) + .await + .unwrap(); assert!(!contains_unconfirmed); - let unconfirmed_transactions = storage.get_unconfirmed_txes_from_history(&wallet_id).await.unwrap(); + let unconfirmed_transactions = storage + .get_unconfirmed_txes_from_history(&wallet_id, for_second_address) + .await + .unwrap(); + assert!(unconfirmed_transactions.is_empty()); + + let for_unknown_address = FilteringAddresses::from_iter(["bchtest:unknown_address".to_string()]); + let contains_unconfirmed = storage + .history_contains_unconfirmed_txes(&wallet_id, for_unknown_address.clone()) + .await + .unwrap(); + assert!(!contains_unconfirmed); + + let unconfirmed_transactions = storage + .get_unconfirmed_txes_from_history(&wallet_id, for_unknown_address) + .await + .unwrap(); assert!(unconfirmed_transactions.is_empty()); } @@ -264,8 +324,28 @@ async fn test_unique_tx_hashes_num_impl() { .await .unwrap(); - let tx_hashes_num = storage.unique_tx_hashes_num_in_history(&wallet_id).await.unwrap(); + let for_addresses = + FilteringAddresses::from_iter(["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66".to_string()]); + let tx_hashes_num = storage + .unique_tx_hashes_num_in_history(&wallet_id, for_addresses) + .await + .unwrap(); assert_eq!(2, tx_hashes_num); + + let for_addresses = + FilteringAddresses::from_iter(["bchtest:qz2nkwgfla42y60ctk35cye2jfpygs8p3c87hd35es".to_string()]); + let tx_hashes_num = storage + .unique_tx_hashes_num_in_history(&wallet_id, for_addresses) + .await + .unwrap(); + assert_eq!(1, tx_hashes_num); + + let for_addresses = FilteringAddresses::from_iter(["bchtest:unknown_address".to_string()]); + let tx_hashes_num = storage + .unique_tx_hashes_num_in_history(&wallet_id, for_addresses) + .await + .unwrap(); + assert_eq!(0, tx_hashes_num); } async fn test_add_and_get_tx_from_cache_impl() { @@ -351,7 +431,7 @@ async fn test_get_history_page_number_impl() { .await .unwrap(); - let filters = GetTxHistoryFilters::new(); + let filters = GetTxHistoryFilters::for_address("bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66".to_string()); let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); let limit = 4; @@ -365,21 +445,19 @@ async fn test_get_history_page_number_impl() { ]; assert_get_history_result(result, expected_internal_ids, 0, 123); - let filters = GetTxHistoryFilters::new() + let filters = GetTxHistoryFilters::for_address("slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8".to_string()) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(2).unwrap()); - let limit = 5; + let limit = 3; let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); let expected_internal_ids: Vec = vec![ - "433b641bc89e1b59c22717918583c60ec98421805c8e85b064691705d9aeb970".into(), - "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989".into(), + "babe9bd0dc1495dff0920da14a76311b744daadc9d01314f8bd4e2438c6b183b".into(), "1c1e68357cf5a6dacb53881f13aa5d2048fe0d0fab24b76c9ec48f53884bed97".into(), - "c4304b5ef4f1b88ed4939534a8ca9eca79f592939233174ae08002e8454e3f06".into(), - "b0035434a1e7be5af2ed991ee2a21a90b271c5852a684a0b7d315c5a770d1b1c".into(), + "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989".into(), ]; - assert_get_history_result(result, expected_internal_ids, 5, 121); + assert_get_history_result(result, expected_internal_ids, 3, 119); } async fn test_get_history_from_id_impl() { @@ -395,7 +473,7 @@ async fn test_get_history_from_id_impl() { .await .unwrap(); - let filters = GetTxHistoryFilters::new(); + let filters = GetTxHistoryFilters::for_address("bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66".to_string()); let paging = PagingOptionsEnum::FromId("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into()); let limit = 3; @@ -408,7 +486,7 @@ async fn test_get_history_from_id_impl() { ]; assert_get_history_result(result, expected_internal_ids, 1, 123); - let filters = GetTxHistoryFilters::new() + let filters = GetTxHistoryFilters::for_address("slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8".to_string()) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::FromId("433b641bc89e1b59c22717918583c60ec98421805c8e85b064691705d9aeb970".into()); let limit = 4; @@ -416,12 +494,12 @@ async fn test_get_history_from_id_impl() { let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); let expected_internal_ids: Vec = vec![ - "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989".into(), + "babe9bd0dc1495dff0920da14a76311b744daadc9d01314f8bd4e2438c6b183b".into(), "1c1e68357cf5a6dacb53881f13aa5d2048fe0d0fab24b76c9ec48f53884bed97".into(), - "c4304b5ef4f1b88ed4939534a8ca9eca79f592939233174ae08002e8454e3f06".into(), + "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989".into(), "b0035434a1e7be5af2ed991ee2a21a90b271c5852a684a0b7d315c5a770d1b1c".into(), ]; - assert_get_history_result(result, expected_internal_ids, 6, 121); + assert_get_history_result(result, expected_internal_ids, 3, 119); } async fn test_get_history_for_addresses_impl() { @@ -441,8 +519,7 @@ async fn test_get_history_for_addresses_impl() { "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), ]; - let filters = GetTxHistoryFilters::new() - .with_for_addresses(for_addresses) + let filters = GetTxHistoryFilters::for_addresses(for_addresses) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); let limit = 5; @@ -462,8 +539,7 @@ async fn test_get_history_for_addresses_impl() { "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), ]; - let filters = GetTxHistoryFilters::new() - .with_for_addresses(for_addresses) + let filters = GetTxHistoryFilters::for_addresses(for_addresses) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::FromId("e46fa0836be0534f7799b2ef5b538551ea25b6f430b7e015a95731efb7a0cd4f".into()); let limit = 4; @@ -482,8 +558,7 @@ async fn test_get_history_for_addresses_impl() { "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), ]; - let filters = GetTxHistoryFilters::new() - .with_for_addresses(for_addresses) + let filters = GetTxHistoryFilters::for_addresses(for_addresses) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::FromId("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into()); let limit = 2; @@ -498,8 +573,7 @@ async fn test_get_history_for_addresses_impl() { "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), ]; - let filters = GetTxHistoryFilters::new() - .with_for_addresses(for_addresses) + let filters = GetTxHistoryFilters::for_addresses(for_addresses) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(2).unwrap()); let limit = 4; @@ -508,7 +582,7 @@ async fn test_get_history_for_addresses_impl() { assert_get_history_result(result, Vec::new(), 4, 4); } -#[cfg(test)] +#[cfg(all(test, not(target_arch = "wasm32")))] mod native_tests { use super::wallet_id_for_test; use crate::my_tx_history_v2::TxHistoryStorage; 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 index a07bdaf6fc..12255a6a10 100644 --- a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs @@ -3,7 +3,7 @@ use crate::tx_history_storage::wasm::tx_history_db::{TxHistoryDb, TxHistoryDbLoc 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 crate::{compare_transaction_details, CoinsContext, TransactionDetails}; use async_trait::async_trait; use common::PagingOptionsEnum; use itertools::Itertools; @@ -118,24 +118,28 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { 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)?; + async fn get_highest_block_height(&self, _wallet_id: &WalletId) -> Result, MmError> { + // TODO + Ok(None) + } - let count_unconfirmed = table.count_by_multi_index(index_keys).await?; - Ok(count_unconfirmed > 0) + /// Since we need to filter the transactions by the given `for_addresses`, + /// we can't use [`DbTable::count_by_multi_index`]. + /// TODO consider one of the solutions described at [`IndexedDbTxHistoryStorage::get_history`]. + async fn history_contains_unconfirmed_txes( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result> { + let txs = self.get_unconfirmed_txes_from_history(wallet_id, for_addresses).await?; + Ok(!txs.is_empty()) } /// Gets the unconfirmed transactions from the history async fn get_unconfirmed_txes_from_history( &self, wallet_id: &WalletId, + for_addresses: FilteringAddresses, ) -> MmResult, Self::Error> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; @@ -146,13 +150,13 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { .with_value(wallet_id.hd_wallet_rmd160_or_exclude())? .with_value(ConfirmationStatus::Unconfirmed)?; - table + let transactions = table .get_items_by_multi_index(index_keys) .await? .into_iter() - .map(|(_item_id, item)| tx_details_from_item(item)) - // Collect `WasmTxHistoryResult>`. - .collect() + .map(|(_item_id, item)| item); + + Self::take_according_to_filtering_addresses(transactions, &for_addresses) } /// Updates transaction in the selected coin's history @@ -183,9 +187,12 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { 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> { + /// TODO consider refactoring this method to avoid fetching all transactions. + async fn unique_tx_hashes_num_in_history( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; let table = db_transaction.table::().await?; @@ -196,12 +203,15 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { // `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 + let transactions = table .get_items_by_multi_index(index_keys) .await? .into_iter() - .unique_by(|(_item_id, tx)| tx.tx_hash.clone()) - .count()) + .map(|(_item_id, tx)| tx) + .unique_by(|tx| tx.tx_hash.clone()); + + let filtered_transactions = Self::take_according_to_filtering_addresses(transactions, &for_addresses)?; + Ok(filtered_transactions.len()) } async fn add_tx_to_cache( @@ -260,9 +270,9 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { paging: PagingOptionsEnum, limit: usize, ) -> MmResult { - // Check if [`GetTxHistoryFilters::for_addresses`] is specified and empty. + // Check if [`GetTxHistoryFilters::for_addresses`] is 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()) { + if filters.for_addresses.is_empty() { return Ok(GetHistoryResult { transactions: Vec::new(), skipped: 0, @@ -285,7 +295,7 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { .into_iter() .map(|(_item_id, tx)| tx); - let transactions = Self::take_according_to_filtering_addresses(transactions, &filters.for_addresses); + let transactions = Self::take_according_to_filtering_addresses(transactions, &filters.for_addresses)?; Self::take_according_to_paging_opts(transactions, paging, limit) } } @@ -293,28 +303,31 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { impl IndexedDbTxHistoryStorage { fn take_according_to_filtering_addresses( txs: I, - for_addresses: &Option, - ) -> Vec + for_addresses: &FilteringAddresses, + ) -> WasmTxHistoryResult> 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(), - } + txs.filter(|tx| { + tx.from_addresses.has_intersection(for_addresses) || tx.to_addresses.has_intersection(for_addresses) + }) + .map(tx_details_from_item) + .collect() } pub(super) fn take_according_to_paging_opts( - txs: Vec, + mut txs: Vec, paging: PagingOptionsEnum, limit: usize, ) -> WasmTxHistoryResult { let total_count = txs.len(); + // This is super inefficient to fetch the whole transaction history, sort it on the client side. + // It's required to implement `DESC` order for `IdbCursor` in order to sort the transactions + // the same way as `compare_transaction_details` does. + // But it's difficult to implement, and I think it can be postponed for a while. + txs.sort_by(compare_transaction_details); + let skip = match paging { // `page_number` is ignored if from_uuid is set PagingOptionsEnum::FromId(from_internal_id) => { @@ -336,15 +349,8 @@ impl IndexedDbTxHistoryStorage { 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, + transactions: txs.into_iter().skip(skip).take(limit).collect(), skipped: skip, total: total_count, }) diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 6e30d5468f..f3519f9807 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -22,8 +22,7 @@ // pub mod bch; -pub mod bch_and_slp_tx_history; -mod bchd_grpc; +pub(crate) mod bchd_grpc; #[allow(clippy::all)] #[rustfmt::skip] #[path = "utxo/pb.rs"] @@ -31,51 +30,58 @@ mod bchd_pb; pub mod qtum; pub mod rpc_clients; pub mod slp; +pub mod spv; pub mod utxo_block_header_storage; pub mod utxo_builder; pub mod utxo_common; pub mod utxo_standard; +pub mod utxo_tx_history_v2; pub mod utxo_withdraw; use async_trait::async_trait; +#[cfg(not(target_arch = "wasm32"))] use bitcoin::network::constants::Network as BitcoinNetwork; pub use bitcrypto::{dhash160, sha256, ChecksumType}; pub use chain::Transaction as UtxoTx; use chain::{OutPoint, TransactionOutput, TxHashAlgo}; +use common::executor::abortable_queue::AbortableQueue; #[cfg(not(target_arch = "wasm32"))] use common::first_char_to_upper; use common::jsonrpc_client::JsonRpcError; -use common::mm_metrics::MetricsArc; +use common::log::LogOnError; use common::now_ms; -use crypto::trezor::utxo::TrezorUtxoCoin; -use crypto::{Bip32DerPathOps, Bip32Error, Bip44Chain, Bip44DerPathError, Bip44PathToAccount, Bip44PathToCoin, - ChildNumber, DerivationPath, Secp256k1ExtendedPublicKey}; +use crypto::{Bip32DerPathOps, Bip32Error, Bip44Chain, ChildNumber, DerivationPath, Secp256k1ExtendedPublicKey, + StandardHDPathError, StandardHDPathToAccount, StandardHDPathToCoin}; use derive_more::Display; #[cfg(not(target_arch = "wasm32"))] use dirs::home_dir; -use futures::channel::mpsc; +use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender, UnboundedSender}; use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures01::Future; use keys::bytes::Bytes; pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, KeyPair, Private, Public, Secret, Type as ScriptType}; +#[cfg(not(target_arch = "wasm32"))] use lightning_invoice::Currency as LightningCurrency; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; #[cfg(test)] use mocktopus::macros::*; use num_traits::ToPrimitive; -use primitives::hash::{H256, H264}; +use primitives::hash::{H160, 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 serialization::{serialize, serialize_with_flags, Error as SerError, SERIALIZE_TRANSACTION_WITNESS}; +use spv_validation::conf::SPVConf; use spv_validation::helpers_validation::SPVError; +use spv_validation::storage::BlockHeaderStorageError; use std::array::TryFromSliceError; use std::collections::{HashMap, HashSet}; use std::convert::TryInto; use std::hash::Hash; -use std::num::NonZeroU64; +use std::num::{NonZeroU64, TryFromIntError}; use std::ops::Deref; #[cfg(not(target_arch = "wasm32"))] use std::path::{Path, PathBuf}; @@ -90,26 +96,20 @@ 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 super::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BalanceResult, CoinBalance, CoinFutSpawner, + CoinsContext, DerivationMethod, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, KmdRewardsDetails, + MarketCoinOps, MmCoin, NumConversError, NumConversResult, PrivKeyActivationPolicy, PrivKeyPolicy, + PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RawTransactionResult, + RpcTransportEventHandler, RpcTransportEventHandlerShared, TradeFee, TradePreimageError, TradePreimageFut, + TradePreimageResult, Transaction, TransactionDetails, TransactionEnum, TransactionErr, + UnexpectedDerivationMethod, VerificationError, WithdrawError, WithdrawRequest}; +use crate::coin_balance::{EnableCoinScanPolicy, EnabledCoinBalanceParams, HDAddressBalanceScanner}; +use crate::hd_wallet::{HDAccountOps, HDAccountsMutex, HDAddress, HDAddressId, 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"))] -pub mod utxo_sql_block_header_storage; #[cfg(any(test, target_arch = "wasm32"))] pub mod utxo_common_tests; @@ -135,6 +135,7 @@ pub type GenerateTxResult = Result<(TransactionInputSigner, AdditionalTxData), M pub type HistoryUtxoTxMap = HashMap; pub type MatureUnspentMap = HashMap; pub type RecentlySpentOutPointsGuard<'a> = AsyncMutexGuard<'a, RecentlySpentOutPoints>; +pub type UtxoHDAddress = HDAddress; #[cfg(windows)] #[cfg(not(target_arch = "wasm32"))] @@ -225,8 +226,8 @@ impl From for TxProviderError { } } -impl From for HDWalletStorageError { - fn from(e: Bip44DerPathError) -> Self { HDWalletStorageError::ErrorDeserializing(e.to_string()) } +impl From for HDWalletStorageError { + fn from(e: StandardHDPathError) -> Self { HDWalletStorageError::ErrorDeserializing(e.to_string()) } } impl From for HDWalletStorageError { @@ -381,23 +382,20 @@ impl RecentlySpentOutPoints { pub fn replace_spent_outputs_with_cache(&self, mut outputs: HashSet) -> HashSet { let mut replacement_unspents = HashSet::new(); - outputs = outputs - .into_iter() - .filter(|unspent| { - let outs = self.input_to_output_map.get(&unspent.clone().into()); - match outs { - Some(outs) => { - for out in outs.iter() { - if !replacement_unspents.contains(out) { - replacement_unspents.insert(out.clone()); - } + outputs.retain(|unspent| { + let outs = self.input_to_output_map.get(&unspent.clone().into()); + match outs { + Some(outs) => { + for out in outs.iter() { + if !replacement_unspents.contains(out) { + replacement_unspents.insert(out.clone()); } - false - }, - None => true, - } - }) - .collect(); + } + false + }, + None => true, + } + }); if replacement_unspents.is_empty() { return outputs; } @@ -416,6 +414,7 @@ pub enum BlockchainNetwork { Regtest, } +#[cfg(not(target_arch = "wasm32"))] impl From for BitcoinNetwork { fn from(network: BlockchainNetwork) -> Self { match network { @@ -426,6 +425,7 @@ impl From for BitcoinNetwork { } } +#[cfg(not(target_arch = "wasm32"))] impl From for LightningCurrency { fn from(network: BlockchainNetwork) -> Self { match network { @@ -436,6 +436,54 @@ impl From for LightningCurrency { } } +pub enum UtxoSyncStatus { + SyncingBlockHeaders { + current_scanned_block: u64, + last_block: u64, + }, + TemporaryError(String), + PermanentError(String), + Finished { + block_number: u64, + }, +} + +#[derive(Clone)] +pub struct UtxoSyncStatusLoopHandle(AsyncSender); + +impl UtxoSyncStatusLoopHandle { + pub fn new(sync_status_notifier: AsyncSender) -> Self { + UtxoSyncStatusLoopHandle(sync_status_notifier) + } + + pub fn notify_blocks_headers_sync_status(&mut self, current_scanned_block: u64, last_block: u64) { + self.0 + .try_send(UtxoSyncStatus::SyncingBlockHeaders { + current_scanned_block, + last_block, + }) + .debug_log_with_msg("No one seems interested in UtxoSyncStatus"); + } + + pub fn notify_on_temp_error(&mut self, error: impl ToString) { + self.0 + .try_send(UtxoSyncStatus::TemporaryError(error.to_string())) + .debug_log_with_msg("No one seems interested in UtxoSyncStatus"); + } + + pub fn notify_on_permanent_error(&mut self, error: impl ToString) { + self.0 + .try_send(UtxoSyncStatus::PermanentError(error.to_string())) + .debug_log_with_msg("No one seems interested in UtxoSyncStatus"); + } + + pub fn notify_sync_finished(&mut self, block_number: u64) { + self.0 + .try_send(UtxoSyncStatus::Finished { block_number }) + .debug_log_with_msg("No one seems interested in UtxoSyncStatus"); + } +} + #[derive(Debug)] pub struct UtxoCoinConf { pub ticker: String, @@ -503,12 +551,19 @@ pub struct UtxoCoinConf { /// 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, + pub trezor_coin: Option, + /// Whether to verify swaps and lightning transactions using spv or not. When enabled, block headers will be retrieved, verified according + /// to [`SPVConf::validation_params`] and stored in the DB. Can be false if the coin's RPC server is trusted. + pub spv_conf: Option, + /// 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: Option, + /// The average time in seconds needed to mine a new block for this coin. + pub avg_blocktime: Option, } -#[derive(Debug)] pub struct UtxoCoinFields { /// UTXO coin config pub conf: UtxoCoinConf, @@ -530,7 +585,6 @@ pub struct UtxoCoinFields { pub history_sync_state: Mutex, /// 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 @@ -539,6 +593,15 @@ pub struct UtxoCoinFields { /// 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, + /// The notifier/sender of the block headers synchronization status, + /// initialized only for non-native mode if spv is enabled for the coin. + pub block_headers_status_notifier: Option, + /// The watcher/receiver of the block headers synchronization status, + /// initialized only for non-native mode if spv is enabled for the coin. + pub block_headers_status_watcher: Option>>, + /// This abortable system is used to spawn coin's related futures that should be aborted on coin deactivation + /// and on [`MmArc::stop`]. + pub abortable_system: AbortableQueue, } #[derive(Debug, Display)] @@ -567,26 +630,62 @@ impl From for WithdrawError { } #[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum GetTxError { + Rpc(UtxoRpcError), + TxDeserialization(SerError), +} + +impl From for GetTxError { + fn from(err: UtxoRpcError) -> GetTxError { GetTxError::Rpc(err) } +} + +impl From for GetTxError { + fn from(err: SerError) -> GetTxError { GetTxError::TxDeserialization(err) } +} + +#[derive(Debug, Display)] pub enum GetTxHeightError { - HeightNotFound, + HeightNotFound(String), + StorageError(BlockHeaderStorageError), + ConversionError(TryFromIntError), } impl From for SPVError { fn from(e: GetTxHeightError) -> Self { match e { - GetTxHeightError::HeightNotFound => SPVError::InvalidHeight, + GetTxHeightError::HeightNotFound(e) => SPVError::InvalidHeight(e), + GetTxHeightError::StorageError(e) => SPVError::HeaderStorageError(e), + GetTxHeightError::ConversionError(e) => SPVError::Internal(e.to_string()), } } } -#[derive(Debug)] +impl From for GetTxHeightError { + fn from(e: UtxoRpcError) -> Self { GetTxHeightError::HeightNotFound(e.to_string()) } +} + +impl From for GetTxHeightError { + fn from(e: BlockHeaderStorageError) -> Self { GetTxHeightError::StorageError(e) } +} + +impl From for GetTxHeightError { + fn from(err: TryFromIntError) -> GetTxHeightError { GetTxHeightError::ConversionError(err) } +} + +#[derive(Debug, Display)] pub enum GetBlockHeaderError { + #[display(fmt = "Block header storage error: {}", _0)] StorageError(BlockHeaderStorageError), + #[display(fmt = "RPC error: {}", _0)] RpcError(JsonRpcError), + #[display(fmt = "Serialization error: {}", _0)] SerializationError(serialization::Error), + #[display(fmt = "Invalid response: {}", _0)] InvalidResponse(String), + #[display(fmt = "Error validating headers: {}", _0)] SPVError(SPVError), - NativeNotSupported(String), + #[display(fmt = "Internal error: {}", _0)] Internal(String), } @@ -604,10 +703,6 @@ impl From for GetBlockHeaderError { } } -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) } } @@ -616,6 +711,55 @@ impl From for GetBlockHeaderError { fn from(err: BlockHeaderStorageError) -> Self { GetBlockHeaderError::StorageError(err) } } +impl From for SPVError { + fn from(e: GetBlockHeaderError) -> Self { SPVError::UnableToGetHeader(e.to_string()) } +} + +#[derive(Debug, Display)] +pub enum GetConfirmedTxError { + HeightNotFound(GetTxHeightError), + UnableToGetHeader(GetBlockHeaderError), + RpcError(JsonRpcError), + SerializationError(serialization::Error), + SPVError(SPVError), +} + +impl From for GetConfirmedTxError { + fn from(err: GetTxHeightError) -> Self { GetConfirmedTxError::HeightNotFound(err) } +} + +impl From for GetConfirmedTxError { + fn from(err: GetBlockHeaderError) -> Self { GetConfirmedTxError::UnableToGetHeader(err) } +} + +impl From for GetConfirmedTxError { + fn from(err: JsonRpcError) -> Self { GetConfirmedTxError::RpcError(err) } +} + +impl From for GetConfirmedTxError { + fn from(err: serialization::Error) -> Self { GetConfirmedTxError::SerializationError(err) } +} + +#[derive(Debug, Display)] +pub enum AddrFromStrError { + #[display(fmt = "{}", _0)] + Unsupported(UnsupportedAddr), + #[display(fmt = "Cannot determine format: {:?}", _0)] + CannotDetermineFormat(Vec), +} + +impl From for AddrFromStrError { + fn from(e: UnsupportedAddr) -> Self { AddrFromStrError::Unsupported(e) } +} + +impl From for VerificationError { + fn from(e: AddrFromStrError) -> Self { VerificationError::AddressDecodingError(e.to_string()) } +} + +impl From for WithdrawError { + fn from(e: AddrFromStrError) -> Self { WithdrawError::InvalidAddress(e.to_string()) } +} + impl UtxoCoinFields { pub fn transaction_preimage(&self) -> TransactionInputSigner { let lock_time = if self.conf.ticker == "KMD" { @@ -702,6 +846,7 @@ pub enum UtxoAddressScanner { } #[async_trait] +#[cfg_attr(test, mockable)] impl HDAddressBalanceScanner for UtxoAddressScanner { type Address = Address; @@ -788,7 +933,7 @@ impl MatureUnspentList { pub trait UtxoCommonOps: AsRef + UtxoTxGenerationOps + UtxoTxBroadcastOps + Clone + Send + Sync + 'static { - async fn get_htlc_spend_fee(&self, tx_size: u64) -> UtxoRpcResult; + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult; fn addresses_from_script(&self, script: &Script) -> Result, String>; @@ -805,7 +950,7 @@ pub trait UtxoCommonOps: /// Try to parse address from string using specified on asset enable format, /// and if it failed inform user that he used a wrong format. - fn address_from_str(&self, address: &str) -> Result; + fn address_from_str(&self, address: &str) -> MmResult; async fn get_current_mtp(&self) -> UtxoRpcResult; @@ -953,11 +1098,11 @@ pub trait UtxoStandardOps { ) -> UtxoRpcResult<()>; } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct UtxoArc(Arc); impl Deref for UtxoArc { type Target = UtxoCoinFields; - fn deref(&self) -> &UtxoCoinFields { &*self.0 } + fn deref(&self) -> &UtxoCoinFields { &self.0 } } impl From for UtxoArc { @@ -980,7 +1125,7 @@ impl UtxoArc { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct UtxoWeak(Weak); impl From> for UtxoWeak { @@ -1159,10 +1304,15 @@ pub fn coin_daemon_data_dir(name: &str, is_asset_chain: bool) -> PathBuf { data_dir } +enum ElectrumProtoVerifierEvent { + Connected(String), + Disconnected(String), +} + /// Electrum protocol version verifier. /// The structure is used to handle the `on_connected` event and notify `electrum_version_loop`. struct ElectrumProtoVerifier { - on_connect_tx: mpsc::UnboundedSender, + on_event_tx: UnboundedSender, } impl ElectrumProtoVerifier { @@ -1177,7 +1327,16 @@ impl RpcTransportEventHandler for ElectrumProtoVerifier { fn on_incoming_response(&self, _data: &[u8]) {} fn on_connected(&self, address: String) -> Result<(), String> { - try_s!(self.on_connect_tx.unbounded_send(address)); + try_s!(self + .on_event_tx + .unbounded_send(ElectrumProtoVerifierEvent::Connected(address))); + Ok(()) + } + + fn on_disconnected(&self, address: String) -> Result<(), String> { + try_s!(self + .on_event_tx + .unbounded_send(ElectrumProtoVerifierEvent::Disconnected(address))); Ok(()) } } @@ -1191,14 +1350,6 @@ pub struct UtxoMergeParams { pub max_merge_at_once: usize, } -#[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, -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct UtxoActivationParams { pub mode: UtxoRpcMode, @@ -1208,10 +1359,12 @@ pub struct UtxoActivationParams { pub required_confirmations: Option, pub requires_notarization: Option, pub address_format: Option, + // The max number of empty addresses in a row. + // If transactions were sent to an address outside the `gap_limit`, they will not be identified. pub gap_limit: Option, + #[serde(flatten)] + pub enable_params: EnabledCoinBalanceParams, #[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 @@ -1229,6 +1382,7 @@ pub enum UtxoFromLegacyReqErr { InvalidAddressFormat(json::Error), InvalidCheckUtxoMaturity(json::Error), InvalidScanPolicy(json::Error), + InvalidMinAddressesNumber(json::Error), InvalidPrivKeyPolicy(json::Error), } @@ -1258,9 +1412,15 @@ impl UtxoActivationParams { let scan_policy = json::from_value::>(req["scan_policy"].clone()) .map_to_mm(UtxoFromLegacyReqErr::InvalidScanPolicy)? .unwrap_or_default(); + let min_addresses_number = json::from_value(req["min_addresses_number"].clone()) + .map_to_mm(UtxoFromLegacyReqErr::InvalidMinAddressesNumber)?; + let enable_params = EnabledCoinBalanceParams { + scan_policy, + min_addresses_number, + }; let priv_key_policy = json::from_value::>(req["priv_key_policy"].clone()) .map_to_mm(UtxoFromLegacyReqErr::InvalidPrivKeyPolicy)? - .unwrap_or(PrivKeyActivationPolicy::IguanaPrivKey); + .unwrap_or(PrivKeyActivationPolicy::ContextPrivKey); Ok(UtxoActivationParams { mode, @@ -1270,7 +1430,7 @@ impl UtxoActivationParams { requires_notarization, address_format, gap_limit: None, - scan_policy, + enable_params, priv_key_policy, check_utxo_maturity, }) @@ -1284,6 +1444,11 @@ pub enum UtxoRpcMode { Electrum { servers: Vec }, } +impl UtxoRpcMode { + #[inline] + pub fn is_native(&self) -> bool { matches!(*self, UtxoRpcMode::Native) } +} + #[derive(Debug)] pub struct ElectrumBuilderArgs { pub spawn_ping: bool, @@ -1303,15 +1468,18 @@ impl Default for ElectrumBuilderArgs { #[derive(Debug)] pub struct UtxoHDWallet { + pub hd_wallet_rmd160: H160, 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, + pub derivation_path: StandardHDPathToCoin, /// User accounts. pub accounts: HDAccountsMutex, + // The max number of empty addresses in a row. + // If transactions were sent to an address outside the `gap_limit`, they will not be identified. pub gap_limit: u32, } @@ -1325,19 +1493,37 @@ impl HDWalletOps for UtxoHDWallet { fn get_accounts_mutex(&self) -> &HDAccountsMutex { &self.accounts } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default)] +pub struct HDAddressesCache { + cache: Arc>>, +} + +impl HDAddressesCache { + pub fn with_capacity(capacity: usize) -> HDAddressesCache { + HDAddressesCache { + cache: Arc::new(AsyncMutex::new(HashMap::with_capacity(capacity))), + } + } + + pub async fn lock(&self) -> AsyncMutexGuard<'_, HashMap> { self.cache.lock().await } +} + +#[derive(Clone, Debug)] 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, + pub account_derivation_path: StandardHDPathToAccount, /// 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, + /// The cache of derived addresses. + /// This is used at [`HDWalletCoinOps::derive_address`]. + pub derived_addresses: HDAddressesCache, } impl HDAccountOps for UtxoHDAccount { @@ -1355,7 +1541,7 @@ impl HDAccountOps for UtxoHDAccount { impl UtxoHDAccount { pub fn try_from_storage_item( - wallet_der_path: &Bip44PathToCoin, + wallet_der_path: &StandardHDPathToCoin, account_info: &HDAccountStorageItem, ) -> HDWalletStorageResult { const ACCOUNT_CHILD_HARDENED: bool = true; @@ -1363,14 +1549,17 @@ impl UtxoHDAccount { 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)?; + .map_to_mm(StandardHDPathError::from)?; let extended_pubkey = Secp256k1ExtendedPublicKey::from_str(&account_info.account_xpub)?; + let capacity = + account_info.external_addresses_number + account_info.internal_addresses_number + DEFAULT_GAP_LIMIT; 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, + derived_addresses: HDAddressesCache::with_capacity(capacity as usize), }) } @@ -1505,7 +1694,7 @@ pub async fn kmd_rewards_info(coin: &T) -> Result( where T: UtxoCommonOps + GetUtxoListOps, { - let my_address = try_tx_s!(coin.as_ref().derivation_method.iguana_or_err()); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_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 } @@ -1589,7 +1778,7 @@ async fn generate_and_send_tx( where T: AsRef + UtxoTxGenerationOps + UtxoTxBroadcastOps, { - let my_address = try_tx_s!(coin.as_ref().derivation_method.iguana_or_err()); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()); let key_pair = try_tx_s!(coin.as_ref().priv_key_policy.key_pair_or_err()); let mut builder = UtxoTxBuilder::new(coin) @@ -1659,8 +1848,8 @@ pub fn address_by_conf_and_pubkey_str( requires_notarization: None, address_format: None, gap_limit: None, - scan_policy: EnableCoinScanPolicy::default(), - priv_key_policy: PrivKeyActivationPolicy::IguanaPrivKey, + enable_params: EnabledCoinBalanceParams::default(), + priv_key_policy: PrivKeyActivationPolicy::ContextPrivKey, check_utxo_maturity: None, }; let conf_builder = UtxoConfBuilder::new(conf, ¶ms, coin); diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 1cbaea6a5e..e04d345877 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -1,23 +1,34 @@ use super::*; -use crate::my_tx_history_v2::{CoinWithTxHistoryV2, TxDetailsBuilder, TxHistoryStorage, TxHistoryStorageError}; +use crate::coin_errors::MyAddressError; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxDetailsBuilder, + TxHistoryStorage}; 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::slp::{parse_slp_script, 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 crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, + UtxoTxHistoryOps}; +use crate::{BlockHeightAndTime, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinProtocol, + CoinWithDerivationMethod, IguanaPrivKey, MakerSwapTakerCoin, NegotiateSwapContractAddrErr, + PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, + RefundError, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, + SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, + SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, + SendWatcherRefundsPaymentArgs, SignatureResult, SwapOps, TakerSwapMakerCoin, TradePreimageValue, + TransactionFut, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherOps, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; +use common::executor::{AbortableSystem, AbortedError}; 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_metrics::MetricsArc; use mm2_number::MmNumber; use serde_json::{self as json, Value as Json}; use serialization::{deserialize, CoinVariant}; @@ -58,7 +69,7 @@ impl BchActivationRequest { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct BchCoin { utxo_arc: UtxoArc, slp_addr_prefix: CashAddrPrefix, @@ -140,40 +151,6 @@ 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 } @@ -329,7 +306,7 @@ impl BchCoin { let my_address = self .as_ref() .derivation_method - .iguana_or_err() + .single_addr_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) = ( @@ -348,7 +325,7 @@ impl BchCoin { let my_address = self .as_ref() .derivation_method - .iguana_or_err() + .single_addr_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) = ( @@ -369,7 +346,7 @@ impl BchCoin { } pub fn get_my_slp_address(&self) -> Result { - let my_address = try_s!(self.as_ref().derivation_method.iguana_or_err()); + let my_address = try_s!(self.as_ref().derivation_method.single_addr_or_err()); let slp_address = my_address.to_cashaddress( &self.slp_prefix().to_string(), self.as_ref().conf.pub_addr_prefix, @@ -378,36 +355,21 @@ impl BchCoin { 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?; + params: UtxoTxDetailsParams<'_, T>, + ) -> MmResult, UtxoTxDetailsError> { + let tx = self.tx_from_storage_or_rpc(params.hash, params.storage).await?; let bch_tx_details = self - .bch_tx_details(tx_hash, &tx, block_height_and_time, storage) + .bch_tx_details( + params.hash, + &tx, + params.block_height_and_time, + params.storage, + params.my_addresses, + ) .await?; let maybe_op_return: Script = tx.outputs[0].script_pubkey.clone().into(); if !(maybe_op_return.is_pay_to_public_key_hash() @@ -419,9 +381,10 @@ impl BchCoin { .slp_tx_details( &tx, slp_details.transaction, - block_height_and_time, + params.block_height_and_time, bch_tx_details.fee_details.clone(), - storage, + params.storage, + params.my_addresses, ) .await?; return Ok(vec![bch_tx_details, slp_tx_details]); @@ -437,10 +400,9 @@ impl BchCoin { 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); + my_addresses: &HashSet
, + ) -> MmResult { + let mut tx_builder = TxDetailsBuilder::new(self.ticker().to_owned(), tx, height_and_time, my_addresses.clone()); for output in &tx.outputs { let addresses = match self.addresses_from_script(&output.script_pubkey.clone().into()) { Ok(a) => a, @@ -457,7 +419,7 @@ impl BchCoin { self.ticker(), tx_hash, ); - return MmError::err(GetTxDetailsError::AddressesFromScriptError(msg)); + return MmError::err(UtxoTxDetailsError::TxAddressDeserializationError(msg)); } let amount = big_decimal_from_sat_unsigned(output.value, self.decimals()); @@ -475,14 +437,14 @@ impl BchCoin { 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)?; + .map_to_mm(UtxoTxDetailsError::TxAddressDeserializationError)?; 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)); + return MmError::err(UtxoTxDetailsError::TxAddressDeserializationError(msg)); } let prev_value = prev_tx.outputs[index as usize].value; @@ -506,13 +468,16 @@ impl BchCoin { &self, token_id: H256, storage: &T, - ) -> Result>> { + ) -> MmResult { 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)), + _ => { + let error = format!("SLP token ID '{}' is not a genesis TX", token_id); + MmError::err(UtxoTxDetailsError::InvalidTransaction(error)) + }, } } @@ -521,7 +486,7 @@ impl BchCoin { utxo_tx: &UtxoTx, slp_tx: SlpTransaction, storage: &T, - ) -> Result, MmError>> { + ) -> MmResult, UtxoTxDetailsError> { let slp_amounts = match slp_tx { SlpTransaction::Send { token_id, amounts } => { let genesis_params = self.get_slp_genesis_params(token_id, storage).await?; @@ -554,22 +519,29 @@ impl BchCoin { Some(output) => { let addresses = self .addresses_from_script(&output.script_pubkey.clone().into()) - .map_to_mm(GetTxDetailsError::AddressesFromScriptError)?; + .map_to_mm(UtxoTxDetailsError::TxAddressDeserializationError)?; 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)); + return MmError::err(UtxoTxDetailsError::TxAddressDeserializationError(msg)); } let slp_address = self .slp_address(&addresses[0]) - .map_to_mm(GetTxDetailsError::ToSlpAddressError)?; + .map_to_mm(UtxoTxDetailsError::InvalidTransaction)?; result.insert(output_index, (slp_address, amount)); }, - None => return MmError::err(GetTxDetailsError::InvalidSlpTransaction(utxo_tx.hash().reversed())), + None => { + let error = format!( + "Unexpected '{}' output index at {} TX", + output_index, + utxo_tx.hash().reversed() + ); + return MmError::err(UtxoTxDetailsError::InvalidTransaction(error)); + }, } } Ok(result) @@ -582,20 +554,21 @@ impl BchCoin { height_and_time: Option, tx_fee: Option, storage: &Storage, - ) -> Result>> { + my_addresses: &HashSet
, + ) -> MmResult { 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 slp_addresses: Vec<_> = my_addresses + .iter() + .map(|addr| self.slp_address(addr)) + .collect::>() + .map_to_mm(UtxoTxDetailsError::Internal)?; let mut slp_tx_details_builder = - TxDetailsBuilder::new(self.ticker().to_owned(), tx, height_and_time, addresses); + TxDetailsBuilder::new(self.ticker().to_owned(), tx, height_and_time, slp_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); @@ -622,7 +595,7 @@ impl BchCoin { Ok(slp_tx_details_builder.build()) } - pub async fn get_block_timestamp(&self, height: u64) -> Result> { + pub async fn get_block_timestamp(&self, height: u64) -> Result> { self.as_ref().rpc_client.get_block_timestamp(height).await } } @@ -631,13 +604,13 @@ impl AsRef for BchCoin { fn as_ref(&self) -> &UtxoCoinFields { &self.utxo_arc } } -pub async fn bch_coin_from_conf_and_params( +pub async fn bch_coin_with_policy( ctx: &MmArc, ticker: &str, conf: &Json, params: BchActivationRequest, slp_addr_prefix: CashAddrPrefix, - priv_key: &[u8], + priv_key_policy: PrivKeyBuildPolicy, ) -> Result { if params.bchd_urls.is_empty() && !params.allow_slp_unsafe_conf { return Err("Using empty bchd_urls is unsafe for SLP users!".into()); @@ -654,7 +627,6 @@ pub async fn bch_coin_from_conf_and_params( } }; - 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() @@ -663,6 +635,18 @@ pub async fn bch_coin_from_conf_and_params( Ok(coin) } +pub async fn bch_coin_with_priv_key( + ctx: &MmArc, + ticker: &str, + conf: &Json, + params: BchActivationRequest, + slp_addr_prefix: CashAddrPrefix, + priv_key: IguanaPrivKey, +) -> Result { + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(priv_key); + bch_coin_with_policy(ctx, ticker, conf, params, slp_addr_prefix, priv_key_policy).await +} + #[derive(Debug)] pub enum BchActivationError { CoinInitError(String), @@ -746,8 +730,8 @@ impl GetUtxoListOps for BchCoin { #[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 + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { + utxo_common::get_htlc_spend_fee(self, tx_size, stage).await } fn addresses_from_script(&self, script: &Script) -> Result, String> { @@ -760,7 +744,7 @@ impl UtxoCommonOps for BchCoin { utxo_common::my_public_key(self.as_ref()) } - fn address_from_str(&self, address: &str) -> Result { + fn address_from_str(&self, address: &str) -> MmResult { utxo_common::checked_address_from_str(self, address) } @@ -845,134 +829,93 @@ impl UtxoCommonOps for BchCoin { #[async_trait] impl SwapOps for BchCoin { + #[inline] 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 { + #[inline] + fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { utxo_common::send_maker_payment( self.clone(), - time_lock, - taker_pub, - secret_hash, - amount, - swap_unique_data, + maker_payment_args.time_lock, + maker_payment_args.other_pubkey, + maker_payment_args.secret_hash, + maker_payment_args.amount, + maker_payment_args.secret_hash, ) } - 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 { + #[inline] + fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { utxo_common::send_taker_payment( self.clone(), - time_lock, - maker_pub, - secret_hash, - amount, - swap_unique_data, + taker_payment_args.time_lock, + taker_payment_args.other_pubkey, + taker_payment_args.secret_hash, + taker_payment_args.amount, + taker_payment_args.swap_unique_data, ) } + #[inline] 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], + maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, ) -> TransactionFut { utxo_common::send_maker_spends_taker_payment( self.clone(), - taker_payment_tx, - time_lock, - taker_pub, - secret, - swap_unique_data, + maker_spends_payment_args.other_payment_tx, + maker_spends_payment_args.time_lock, + maker_spends_payment_args.other_pubkey, + maker_spends_payment_args.secret, + maker_spends_payment_args.secret_hash, + maker_spends_payment_args.swap_unique_data, ) } + #[inline] 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], + taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, ) -> TransactionFut { utxo_common::send_taker_spends_maker_payment( self.clone(), - maker_payment_tx, - time_lock, - maker_pub, - secret, - swap_unique_data, + taker_spends_payment_args.other_payment_tx, + taker_spends_payment_args.time_lock, + taker_spends_payment_args.other_pubkey, + taker_spends_payment_args.secret, + taker_spends_payment_args.secret_hash, + taker_spends_payment_args.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 { + #[inline] + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { utxo_common::send_taker_refunds_payment( self.clone(), - taker_tx, - time_lock, - maker_pub, - secret_hash, - swap_unique_data, + taker_refunds_payment_args.payment_tx, + taker_refunds_payment_args.time_lock, + taker_refunds_payment_args.other_pubkey, + taker_refunds_payment_args.secret_hash, + taker_refunds_payment_args.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 { + #[inline] + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { utxo_common::send_maker_refunds_payment( self.clone(), - maker_tx, - time_lock, - taker_pub, - secret_hash, - swap_unique_data, + maker_refunds_payment_args.payment_tx, + maker_refunds_payment_args.time_lock, + maker_refunds_payment_args.other_pubkey, + maker_refunds_payment_args.secret_hash, + maker_refunds_payment_args.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 { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + let tx = match validate_fee_args.fee_tx { TransactionEnum::UtxoTx(tx) => tx.clone(), _ => panic!(), }; @@ -980,33 +923,38 @@ impl SwapOps for BchCoin { self.clone(), tx, utxo_common::DEFAULT_FEE_VOUT, - expected_sender, - amount, - min_block_number, - fee_addr, + validate_fee_args.expected_sender, + validate_fee_args.amount, + validate_fee_args.min_block_number, + validate_fee_args.fee_addr, ) } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + #[inline] + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { utxo_common::validate_maker_payment(self, input) } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + #[inline] + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { utxo_common::validate_taker_payment(self, input) } + #[inline] 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], + if_my_payment_sent_args: CheckIfMyPaymentSentArgs, ) -> Box, Error = String> + Send> { - utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash, swap_unique_data) + utxo_common::check_if_my_payment_sent( + self.clone(), + if_my_payment_sent_args.time_lock, + if_my_payment_sent_args.other_pub, + if_my_payment_sent_args.secret_hash, + if_my_payment_sent_args.swap_unique_data, + ) } + #[inline] async fn search_for_swap_tx_spend_my( &self, input: SearchForSwapTxSpendInput<'_>, @@ -1014,6 +962,7 @@ impl SwapOps for BchCoin { utxo_common::search_for_swap_tx_spend_my(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } + #[inline] async fn search_for_swap_tx_spend_other( &self, input: SearchForSwapTxSpendInput<'_>, @@ -1021,10 +970,25 @@ impl SwapOps for BchCoin { 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> { + #[inline] + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { + utxo_common::check_all_inputs_signed_by_pub(tx, expected_pub) + } + + #[inline] + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } + + #[inline] fn can_refund_htlc(&self, locktime: u64) -> Box + Send + '_> { Box::new( utxo_common::can_refund_htlc(self, locktime) @@ -1034,6 +998,7 @@ impl SwapOps for BchCoin { ) } + #[inline] fn negotiate_swap_contract_addr( &self, _other_side_address: Option<&[u8]>, @@ -1044,16 +1009,151 @@ impl SwapOps for BchCoin { fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) } + + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + utxo_common::derive_htlc_pubkey(self, swap_unique_data) + } + + #[inline] + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + utxo_common::validate_other_pubkey(raw_pubkey) + } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { true } +} + +#[async_trait] +impl TakerSwapMakerCoin for BchCoin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for BchCoin { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } } fn total_unspent_value<'a>(unspents: impl IntoIterator) -> u64 { unspents.into_iter().fold(0, |cur, unspent| cur + unspent.value) } +#[async_trait] +impl WatcherOps for BchCoin { + #[inline] + fn create_maker_payment_spend_preimage( + &self, + maker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::create_maker_payment_spend_preimage( + self, + maker_payment_tx, + time_lock, + maker_pub, + secret_hash, + swap_unique_data, + ) + } + + #[inline] + fn send_maker_payment_spend_preimage(&self, input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + utxo_common::send_maker_payment_spend_preimage(self, input) + } + + #[inline] + fn create_taker_payment_refund_preimage( + &self, + taker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + _swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::create_taker_payment_refund_preimage( + self, + taker_payment_tx, + time_lock, + maker_pub, + secret_hash, + swap_unique_data, + ) + } + + #[inline] + fn send_taker_payment_refund_preimage( + &self, + watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + utxo_common::send_taker_payment_refund_preimage(self, watcher_refunds_payment_args) + } + + #[inline] + fn watcher_validate_taker_fee(&self, input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + utxo_common::watcher_validate_taker_fee(self, input, utxo_common::DEFAULT_FEE_VOUT) + } + + #[inline] + fn watcher_validate_taker_payment(&self, input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + utxo_common::watcher_validate_taker_payment(self, input) + } + + async fn watcher_search_for_swap_tx_spend( + &self, + input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + utxo_common::watcher_search_for_swap_tx_spend(self, input, utxo_common::DEFAULT_SWAP_VOUT).await + } +} + impl MarketCoinOps for BchCoin { fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } - fn my_address(&self) -> Result { utxo_common::my_address(self) } + fn my_address(&self) -> MmResult { utxo_common::my_address(self) } fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; @@ -1075,7 +1175,7 @@ impl MarketCoinOps for BchCoin { 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 my_address = coin.as_ref().derivation_method.single_addr_or_err()?; let bch_unspents = coin.bch_unspents_for_display(my_address).await?; Ok(bch_unspents.platform_balance(coin.as_ref().decimals)) }; @@ -1114,12 +1214,14 @@ impl MarketCoinOps for BchCoin { ) } - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, transaction: &[u8], + _secret_hash: &[u8], wait_until: u64, from_block: u64, _swap_contract_address: &Option, + check_every: f64, ) -> TransactionFut { utxo_common::wait_for_output_spend( &self.utxo_arc, @@ -1127,10 +1229,11 @@ impl MarketCoinOps for BchCoin { utxo_common::DEFAULT_SWAP_VOUT, from_block, wait_until, + check_every, ) } - fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result { + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { utxo_common::tx_enum_from_bytes(self.as_ref(), bytes) } @@ -1145,37 +1248,24 @@ impl MarketCoinOps for BchCoin { 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 spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) } + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { + Box::new( + utxo_common::get_tx_hex_by_hash(&self.utxo_arc, tx_hash) + .boxed() + .compat(), + ) + } + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { Box::new(utxo_common::withdraw(self.clone(), req).boxed().compat()) } @@ -1188,13 +1278,9 @@ impl MmCoin for BchCoin { 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 process_history_loop(&self, _ctx: MmArc) -> Box + Send> { + warn!("'process_history_loop' is not implemented for BchCoin! Consider using 'my_tx_history_v2'"); + Box::new(futures01::future::err(())) } fn history_sync_status(&self) -> HistorySyncState { utxo_common::history_sync_status(&self.utxo_arc) } @@ -1211,7 +1297,7 @@ impl MmCoin for BchCoin { utxo_common::get_sender_trade_fee(self, value, stage).await } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { utxo_common::get_receiver_trade_fee(self.clone()) } @@ -1237,6 +1323,8 @@ impl MmCoin for BchCoin { fn swap_contract_address(&self) -> Option { utxo_common::swap_contract_address() } + fn fallback_swap_contract(&self) -> Option { utxo_common::fallback_swap_contract() } + fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } @@ -1244,21 +1332,108 @@ impl MmCoin for BchCoin { fn is_coin_protocol_supported(&self, info: &Option>) -> bool { utxo_common::is_coin_protocol_supported(self, info) } + + fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.as_ref().abortable_system) } + + fn on_token_deactivated(&self, ticker: &str) { + if let Ok(tokens) = self.slp_tokens_infos.lock().as_deref_mut() { + tokens.remove(ticker); + }; + } } +impl CoinWithDerivationMethod for BchCoin { + type Address = Address; + type HDWallet = UtxoHDWallet; + + fn derivation_method(&self) -> &DerivationMethod { + utxo_common::derivation_method(self.as_ref()) + } +} + +#[async_trait] 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() } + /// TODO consider using `utxo_common::utxo_tx_history_common::get_tx_history_filters` + /// when `BchCoin` implements `CoinWithDerivationMethod`. + async fn get_tx_history_filters( + &self, + target: MyTxHistoryTarget, + ) -> MmResult { + match target { + MyTxHistoryTarget::Iguana => (), + target => { + let error = format!("Expected 'Iguana' target, found {target:?}"); + return MmError::err(MyTxHistoryErrorV2::InvalidTarget(error)); + }, + } + let my_address = self.my_address()?; + Ok(GetTxHistoryFilters::for_address(my_address)) + } +} + +#[async_trait] +impl UtxoTxHistoryOps for BchCoin { + async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { + let my_address = self.as_ref().derivation_method.single_addr_or_err()?; + Ok(std::iter::once(my_address.clone()).collect()) + } + + async fn tx_details_by_hash( + &self, + params: UtxoTxDetailsParams<'_, Storage>, + ) -> MmResult, UtxoTxDetailsError> + where + Storage: TxHistoryStorage, + { + Ok(self.transaction_details_with_token_transfers(params).await?) + } + + async fn tx_from_storage_or_rpc( + &self, + tx_hash: &H256Json, + storage: &Storage, + ) -> MmResult { + utxo_common::utxo_tx_history_v2_common::tx_from_storage_or_rpc(self, tx_hash, storage).await + } + + async fn request_tx_history( + &self, + metrics: MetricsArc, + for_addresses: &HashSet
, + ) -> RequestTxHistoryResult { + utxo_common::utxo_tx_history_v2_common::request_tx_history(self, metrics, for_addresses).await + } + + async fn get_block_timestamp(&self, height: u64) -> MmResult { + self.get_block_timestamp(height).await + } + + async fn my_addresses_balances(&self) -> BalanceResult> { + let my_address = self + .my_address() + .map_err(|err| BalanceError::Internal(err.to_string()))?; + let my_balance = self.my_balance().compat().await?; + Ok(std::iter::once((my_address, my_balance.into_total())).collect()) + } + + fn address_from_str(&self, address: &str) -> MmResult { + utxo_common::checked_address_from_str(self, address) + } + + fn set_history_sync_state(&self, new_state: HistorySyncState) { + *self.as_ref().history_sync_state.lock().unwrap() = new_state; + } } // testnet #[cfg(test)] -pub fn tbch_coin_for_test() -> BchCoin { +pub fn tbch_coin_for_test() -> (MmArc, BchCoin) { use common::block_on; use crypto::privkey::key_pair_from_seed; use mm2_core::mm_ctx::MmCtxBuilder; + use mm2_test_helpers::for_tests::BCHD_TESTNET_URLS; let ctx = MmCtxBuilder::default().into_mm_arc(); let keypair = key_pair_from_seed("BCH SLP test").unwrap(); @@ -1269,20 +1444,21 @@ pub fn tbch_coin_for_test() -> BchCoin { "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"], + "bchd_urls": BCHD_TESTNET_URLS, "allow_slp_unsafe_conf": false, }); let params = BchActivationRequest::from_legacy_req(&req).unwrap(); - block_on(bch_coin_from_conf_and_params( + let coin = block_on(bch_coin_with_priv_key( &ctx, "BCH", &conf, params, CashAddrPrefix::SlpTest, - &*keypair.private().secret, + keypair.private().secret, )) - .unwrap() + .unwrap(); + (ctx, coin) } // mainnet @@ -1306,13 +1482,13 @@ pub fn bch_coin_for_test() -> BchCoin { }); let params = BchActivationRequest::from_legacy_req(&req).unwrap(); - block_on(bch_coin_from_conf_and_params( + block_on(bch_coin_with_priv_key( &ctx, "BCH", &conf, params, CashAddrPrefix::SimpleLedger, - &*keypair.private().secret, + keypair.private().secret, )) .unwrap() } @@ -1320,21 +1496,13 @@ pub fn bch_coin_for_test() -> BchCoin { #[cfg(test)] mod bch_tests { use super::*; - use crate::tx_history_storage::TxHistoryStorageBuilder; + use crate::my_tx_history_v2::for_tests::init_storage_for; 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 (_ctx, coin) = tbch_coin_for_test(); let token_id = "bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".into(); let (_ctx, storage) = init_storage_for(&coin); @@ -1345,13 +1513,14 @@ mod bch_tests { #[test] fn test_plain_bch_tx_details() { - let coin = tbch_coin_for_test(); + let (_ctx, 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 my_addresses = block_on(coin.my_addresses()).unwrap(); + let details = block_on(coin.bch_tx_details(&hash, &tx, None, &storage, &my_addresses)).unwrap(); let expected_total: BigDecimal = "0.11407782".parse().unwrap(); assert_eq!(expected_total, details.total_amount); @@ -1387,7 +1556,7 @@ mod bch_tests { #[test] fn test_slp_tx_details() { - let coin = tbch_coin_for_test(); + let (_ctx, coin) = tbch_coin_for_test(); let (_ctx, storage) = init_storage_for(&coin); let hash = "a8dcc3c6776e93e7bd21fb81551e853447c55e2d8ac141b418583bc8095ce390".into(); @@ -1395,7 +1564,9 @@ mod bch_tests { 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 my_addresses = block_on(coin.my_addresses()).unwrap(); + let slp_tx_details = + block_on(coin.slp_tx_details(&tx, slp_details.transaction, None, None, &storage, &my_addresses)).unwrap(); let expected_total: BigDecimal = "6.2974".parse().unwrap(); assert_eq!(expected_total, slp_tx_details.total_amount); @@ -1427,7 +1598,7 @@ mod bch_tests { #[test] fn test_sign_message() { - let coin = tbch_coin_for_test(); + let (_ctx, coin) = tbch_coin_for_test(); let signature = coin.sign_message("test").unwrap(); assert_eq!( signature, @@ -1438,7 +1609,7 @@ mod bch_tests { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_verify_message() { - let coin = tbch_coin_for_test(); + let (_ctx, coin) = tbch_coin_for_test(); let is_valid = coin .verify_message( "ILuePKMsycXwJiNDOT7Zb7TfIlUW7Iq+5ylKd15AK72vGVYXbnf7Gj9Lk9MFV+6Ub955j7MiAkp0wQjvuIoRPPA=", diff --git a/mm2src/coins/utxo/bch_and_slp_tx_history.rs b/mm2src/coins/utxo/bch_and_slp_tx_history.rs deleted file mode 100644 index 5da1212e9d..0000000000 --- a/mm2src/coins/utxo/bch_and_slp_tx_history.rs +++ /dev/null @@ -1,406 +0,0 @@ -/// 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 index f0a632cef6..70e3594bb9 100644 --- a/mm2src/coins/utxo/bchd_grpc.rs +++ b/mm2src/coins/utxo/bchd_grpc.rs @@ -1,7 +1,7 @@ /// https://bchd.cash/ /// https://bchd.fountainhead.cash/ use super::bchd_pb::*; -use crate::utxo::slp::SlpUnspent; +use crate::{coin_errors::ValidatePaymentError, utxo::slp::SlpUnspent}; use chain::OutPoint; use derive_more::Display; use futures::future::join_all; @@ -70,6 +70,7 @@ pub enum ValidateSlpUtxosErrKind { UnexpectedUtxoInResponse { outpoint: OutPoint, }, + InvalidSlpTxData(String), } #[derive(Debug, Display)] @@ -79,11 +80,29 @@ pub struct ValidateSlpUtxosErr { kind: ValidateSlpUtxosErrKind, } +impl From for ValidatePaymentError { + fn from(err: ValidateSlpUtxosErr) -> Self { + match err.kind { + ValidateSlpUtxosErrKind::MultiReqErr(_) => Self::Transport(err.to_string()), + ValidateSlpUtxosErrKind::InvalidSlpTxData(_) => Self::WrongPaymentTx(err.to_string()), + _ => Self::InvalidRpcResponse(err.to_string()), + } + } +} + impl From for ValidateSlpUtxosErr { fn from(err: GrpcWebMultiUrlReqErr) -> Self { - ValidateSlpUtxosErr { - to_url: err.to_url.clone(), - kind: ValidateSlpUtxosErrKind::MultiReqErr(err), + match err.err { + // For some reason, BCHD responds with empty payload (Which is mapped to PostGrpcWebErr::PayloadTooShort error) in cases where an invalid UTXO is used with grpc-web. + // while it does provide meaningful error response when use with gRPC HTTP2 TLS. + PostGrpcWebErr::PayloadTooShort(e) => ValidateSlpUtxosErr { + to_url: err.to_url.clone(), + kind: ValidateSlpUtxosErrKind::InvalidSlpTxData(e), + }, + _ => ValidateSlpUtxosErr { + to_url: err.to_url.clone(), + kind: ValidateSlpUtxosErrKind::MultiReqErr(err), + }, } } } @@ -225,6 +244,7 @@ mod bchd_grpc_tests { use super::*; use crate::utxo::rpc_clients::UnspentInfo; use common::block_on; + use mm2_test_helpers::for_tests::BCHD_TESTNET_URLS; #[test] fn test_validate_slp_utxos_valid() { @@ -255,9 +275,8 @@ mod bchd_grpc_tests { }, ]; - 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(); + block_on(validate_slp_utxos(BCHD_TESTNET_URLS, &slp_utxos, &token_id)).unwrap(); } #[test] @@ -300,11 +319,10 @@ mod bchd_grpc_tests { }, ]; - 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(); + let err = block_on(validate_slp_utxos(BCHD_TESTNET_URLS, &slp_utxos, &token_id)).unwrap_err(); match err.into_inner().kind { - ValidateSlpUtxosErrKind::MultiReqErr { .. } => (), + ValidateSlpUtxosErrKind::InvalidSlpTxData(_) => (), err @ _ => panic!("Unexpected error {:?}", err), } } @@ -336,9 +354,8 @@ mod bchd_grpc_tests { 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(); + let err = block_on(validate_slp_utxos(BCHD_TESTNET_URLS, &slp_utxos, &token_id)).unwrap_err(); match err.into_inner().kind { ValidateSlpUtxosErrKind::UnexpectedValidityResultType { for_unspent, @@ -381,10 +398,9 @@ mod bchd_grpc_tests { }, ]; - 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(); + let err = block_on(validate_slp_utxos(BCHD_TESTNET_URLS, &slp_utxos, &invalid_token_id)).unwrap_err(); match err.into_inner().kind { ValidateSlpUtxosErrKind::UnexpectedTokenId { expected, actual } => { assert_eq!(invalid_token_id, expected); @@ -396,18 +412,16 @@ mod bchd_grpc_tests { #[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(); + block_on(check_slp_transaction(BCHD_TESTNET_URLS, 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(); + let err = block_on(check_slp_transaction(BCHD_TESTNET_URLS, tx)).unwrap_err(); match err.into_inner().kind { CheckSlpTransactionErrKind::InvalidTransaction { reason, .. } => { println!("{}", reason); @@ -420,15 +434,15 @@ mod bchd_grpc_tests { #[cfg(target_arch = "wasm32")] mod wasm_tests { use super::*; + use mm2_test_helpers::for_tests::BCHD_TESTNET_URLS; 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(); + check_slp_transaction(BCHD_TESTNET_URLS, tx).await.unwrap(); } } diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index cd9ba229af..ab3f336ceb 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -1,29 +1,46 @@ use super::*; -use crate::coin_balance::{self, EnableCoinBalanceError, HDAccountBalance, HDAddressBalance, HDWalletBalance, - HDWalletBalanceOps}; +use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, + HDWalletBalance, HDWalletBalanceOps}; +use crate::coin_errors::MyAddressError; +use crate::hd_confirm_address::HDConfirmAddress; use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{self, AccountUpdatingError, AddressDerivingError, GetNewHDAddressParams, - GetNewHDAddressResponse, HDAccountMut, HDWalletRpcError, HDWalletRpcOps, - NewAccountCreatingError}; +use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, NewAccountCreatingError, + NewAddressDeriveConfirmError}; use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; +use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, + GetNewAddressRpcOps}; 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_account_balance::{self, InitAccountBalanceParams, InitAccountBalanceRpcOps}; +use crate::rpc_command::init_create_account::{self, CreateAccountRpcError, CreateAccountState, CreateNewAccountParams, + InitCreateAccountRpcOps}; 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 crypto::trezor::utxo::TrezorUtxoCoin; +use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; +use crate::utxo::utxo_builder::{BlockHeaderUtxoArcOps, MergeUtxoArcOps, UtxoCoinBuildError, UtxoCoinBuilder, + UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, + UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; +use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, + UtxoTxHistoryOps}; +use crate::{eth, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, DelegationError, + DelegationFut, GetWithdrawSenderAddress, IguanaPrivKey, MakerSwapTakerCoin, NegotiateSwapContractAddrErr, + PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, RefundError, RefundResult, + SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, + SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, + SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, + SignatureResult, StakingInfosFut, SwapOps, TakerSwapMakerCoin, TradePreimageValue, TransactionFut, + TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawSenderAddress}; +use common::executor::{AbortableSystem, AbortedError}; use crypto::Bip44Chain; use ethereum_types::H160; use futures::{FutureExt, TryFutureExt}; use keys::AddressHashEnum; +use mm2_metrics::MetricsArc; use mm2_number::MmNumber; use serde::Serialize; use serialization::CoinVariant; @@ -76,6 +93,7 @@ pub trait QtumDelegationOps { fn remove_delegation(&self) -> DelegationFut; + #[allow(clippy::result_large_err)] fn generate_pod(&self, addr_hash: AddressHashEnum) -> Result>; } @@ -145,7 +163,7 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { } fn my_addr_as_contract_addr(&self) -> MmResult { - let my_address = self.as_ref().derivation_method.iguana_or_err()?.clone(); + let my_address = self.as_ref().derivation_method.single_addr_or_err()?.clone(); contract_addr_from_utxo_addr(my_address).mm_err(Qrc20AddressError::from) } @@ -187,7 +205,7 @@ pub struct QtumCoinBuilder<'a> { ticker: &'a str, conf: &'a Json, activation_params: &'a UtxoActivationParams, - priv_key_policy: PrivKeyBuildPolicy<'a>, + priv_key_policy: PrivKeyBuildPolicy, } #[async_trait] @@ -203,7 +221,9 @@ impl<'a> UtxoCoinBuilderCommonOps for QtumCoinBuilder<'a> { fn check_utxo_maturity(&self) -> bool { self.activation_params().check_utxo_maturity.unwrap_or(true) } } -impl<'a> UtxoFieldsWithIguanaPrivKeyBuilder for QtumCoinBuilder<'a> {} +impl<'a> UtxoFieldsWithIguanaSecretBuilder for QtumCoinBuilder<'a> {} + +impl<'a> UtxoFieldsWithGlobalHDBuilder for QtumCoinBuilder<'a> {} impl<'a> UtxoFieldsWithHardwareWalletBuilder for QtumCoinBuilder<'a> {} @@ -212,28 +232,28 @@ impl<'a> UtxoCoinBuilder for QtumCoinBuilder<'a> { type ResultCoin = QtumCoin; type Error = UtxoCoinBuildError; - fn priv_key_policy(&self) -> PrivKeyBuildPolicy<'_> { self.priv_key_policy.clone() } + 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) + self.spawn_merge_utxo_loop_if_required(&utxo_arc, QtumCoin::from); + Ok(QtumCoin::from(utxo_arc)) } } impl<'a> MergeUtxoArcOps for QtumCoinBuilder<'a> {} +impl<'a> BlockHeaderUtxoArcOps 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>, + priv_key_policy: PrivKeyBuildPolicy, ) -> Self { QtumCoinBuilder { ctx, @@ -245,7 +265,7 @@ impl<'a> QtumCoinBuilder<'a> { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct QtumCoin { utxo_arc: UtxoArc, } @@ -262,14 +282,13 @@ impl From for UtxoArc { fn from(coin: QtumCoin) -> Self { coin.utxo_arc } } -pub async fn qtum_coin_with_priv_key( +pub async fn qtum_coin_with_policy( ctx: &MmArc, ticker: &str, conf: &Json, activation_params: &UtxoActivationParams, - priv_key: &[u8], + priv_key_policy: PrivKeyBuildPolicy, ) -> Result { - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(priv_key); let coin = try_s!( QtumCoinBuilder::new(ctx, ticker, conf, activation_params, priv_key_policy) .build() @@ -278,6 +297,17 @@ pub async fn qtum_coin_with_priv_key( Ok(coin) } +pub async fn qtum_coin_with_priv_key( + ctx: &MmArc, + ticker: &str, + conf: &Json, + activation_params: &UtxoActivationParams, + priv_key: IguanaPrivKey, +) -> Result { + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(priv_key); + qtum_coin_with_policy(ctx, ticker, conf, activation_params, priv_key_policy).await +} + impl QtumBasedCoin for QtumCoin {} #[derive(Clone, Debug, Deserialize)] @@ -371,8 +401,8 @@ impl GetUtxoMapOps for QtumCoin { #[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 + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { + utxo_common::get_htlc_spend_fee(self, tx_size, stage).await } fn addresses_from_script(&self, script: &Script) -> Result, String> { @@ -385,7 +415,7 @@ impl UtxoCommonOps for QtumCoin { utxo_common::my_public_key(self.as_ref()) } - fn address_from_str(&self, address: &str) -> Result { + fn address_from_str(&self, address: &str) -> MmResult { utxo_common::checked_address_from_str(self, address) } @@ -496,134 +526,93 @@ impl UtxoStandardOps for QtumCoin { #[async_trait] impl SwapOps for QtumCoin { + #[inline] 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 { + #[inline] + fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { utxo_common::send_maker_payment( self.clone(), - time_lock, - taker_pub, - secret_hash, - amount, - swap_unique_data, + maker_payment_args.time_lock, + maker_payment_args.other_pubkey, + maker_payment_args.secret_hash, + maker_payment_args.amount, + maker_payment_args.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 { + #[inline] + fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { utxo_common::send_taker_payment( self.clone(), - time_lock, - maker_pub, - secret_hash, - amount, - swap_unique_data, + taker_payment_args.time_lock, + taker_payment_args.other_pubkey, + taker_payment_args.secret_hash, + taker_payment_args.amount, + taker_payment_args.swap_unique_data, ) } + #[inline] fn send_maker_spends_taker_payment( &self, - taker_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - secret: &[u8], - _swap_contract_address: &Option, - swap_unique_data: &[u8], + maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, ) -> TransactionFut { utxo_common::send_maker_spends_taker_payment( self.clone(), - taker_tx, - time_lock, - taker_pub, - secret, - swap_unique_data, + maker_spends_payment_args.other_payment_tx, + maker_spends_payment_args.time_lock, + maker_spends_payment_args.other_pubkey, + maker_spends_payment_args.secret, + maker_spends_payment_args.secret_hash, + maker_spends_payment_args.swap_unique_data, ) } + #[inline] fn send_taker_spends_maker_payment( &self, - maker_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - secret: &[u8], - _swap_contract_address: &Option, - swap_unique_data: &[u8], + taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, ) -> TransactionFut { utxo_common::send_taker_spends_maker_payment( self.clone(), - maker_tx, - time_lock, - maker_pub, - secret, - swap_unique_data, + taker_spends_payment_args.other_payment_tx, + taker_spends_payment_args.time_lock, + taker_spends_payment_args.other_pubkey, + taker_spends_payment_args.secret, + taker_spends_payment_args.secret_hash, + taker_spends_payment_args.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 { + #[inline] + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { utxo_common::send_taker_refunds_payment( self.clone(), - taker_tx, - time_lock, - maker_pub, - secret_hash, - swap_unique_data, + taker_refunds_payment_args.payment_tx, + taker_refunds_payment_args.time_lock, + taker_refunds_payment_args.other_pubkey, + taker_refunds_payment_args.secret_hash, + taker_refunds_payment_args.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 { + #[inline] + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { utxo_common::send_maker_refunds_payment( self.clone(), - maker_tx, - time_lock, - taker_pub, - secret_hash, - swap_unique_data, + maker_refunds_payment_args.payment_tx, + maker_refunds_payment_args.time_lock, + maker_refunds_payment_args.other_pubkey, + maker_refunds_payment_args.secret_hash, + maker_refunds_payment_args.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 { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + let tx = match validate_fee_args.fee_tx { TransactionEnum::UtxoTx(tx) => tx.clone(), _ => panic!(), }; @@ -631,33 +620,38 @@ impl SwapOps for QtumCoin { self.clone(), tx, utxo_common::DEFAULT_FEE_VOUT, - expected_sender, - amount, - min_block_number, - fee_addr, + validate_fee_args.expected_sender, + validate_fee_args.amount, + validate_fee_args.min_block_number, + validate_fee_args.fee_addr, ) } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + #[inline] + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { utxo_common::validate_maker_payment(self, input) } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + #[inline] + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { utxo_common::validate_taker_payment(self, input) } + #[inline] 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], + if_my_payment_sent_args: CheckIfMyPaymentSentArgs, ) -> Box, Error = String> + Send> { - utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash, swap_unique_data) + utxo_common::check_if_my_payment_sent( + self.clone(), + if_my_payment_sent_args.time_lock, + if_my_payment_sent_args.other_pub, + if_my_payment_sent_args.secret_hash, + if_my_payment_sent_args.swap_unique_data, + ) } + #[inline] async fn search_for_swap_tx_spend_my( &self, input: SearchForSwapTxSpendInput<'_>, @@ -665,6 +659,7 @@ impl SwapOps for QtumCoin { utxo_common::search_for_swap_tx_spend_my(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } + #[inline] async fn search_for_swap_tx_spend_other( &self, input: SearchForSwapTxSpendInput<'_>, @@ -672,10 +667,25 @@ impl SwapOps for QtumCoin { 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> { + #[inline] + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { + utxo_common::check_all_inputs_signed_by_pub(tx, expected_pub) + } + + #[inline] + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } + + #[inline] fn can_refund_htlc(&self, locktime: u64) -> Box + Send + '_> { Box::new( utxo_common::can_refund_htlc(self, locktime) @@ -685,6 +695,7 @@ impl SwapOps for QtumCoin { ) } + #[inline] fn negotiate_swap_contract_addr( &self, _other_side_address: Option<&[u8]>, @@ -695,12 +706,147 @@ impl SwapOps for QtumCoin { fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) } + + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + utxo_common::derive_htlc_pubkey(self, swap_unique_data) + } + + #[inline] + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + utxo_common::validate_other_pubkey(raw_pubkey) + } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { true } +} + +#[async_trait] +impl TakerSwapMakerCoin for QtumCoin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for QtumCoin { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for QtumCoin { + #[inline] + fn create_maker_payment_spend_preimage( + &self, + maker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::create_maker_payment_spend_preimage( + self, + maker_payment_tx, + time_lock, + maker_pub, + secret_hash, + swap_unique_data, + ) + } + + #[inline] + fn send_maker_payment_spend_preimage(&self, input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + utxo_common::send_maker_payment_spend_preimage(self, input) + } + + #[inline] + fn create_taker_payment_refund_preimage( + &self, + taker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + _swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::create_taker_payment_refund_preimage( + self, + taker_payment_tx, + time_lock, + maker_pub, + secret_hash, + swap_unique_data, + ) + } + + #[inline] + fn send_taker_payment_refund_preimage( + &self, + watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + utxo_common::send_taker_payment_refund_preimage(self, watcher_refunds_payment_args) + } + + #[inline] + fn watcher_validate_taker_fee(&self, input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + utxo_common::watcher_validate_taker_fee(self, input, utxo_common::DEFAULT_FEE_VOUT) + } + + #[inline] + fn watcher_validate_taker_payment(&self, input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + utxo_common::watcher_validate_taker_payment(self, input) + } + + async fn watcher_search_for_swap_tx_spend( + &self, + input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + utxo_common::watcher_search_for_swap_tx_spend(self, input, utxo_common::DEFAULT_SWAP_VOUT).await + } } impl MarketCoinOps for QtumCoin { fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } - fn my_address(&self) -> Result { utxo_common::my_address(self) } + fn my_address(&self) -> MmResult { utxo_common::my_address(self) } fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; @@ -753,12 +899,14 @@ impl MarketCoinOps for QtumCoin { ) } - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, transaction: &[u8], + _secret_hash: &[u8], wait_until: u64, from_block: u64, _swap_contract_address: &Option, + check_every: f64, ) -> TransactionFut { utxo_common::wait_for_output_spend( &self.utxo_arc, @@ -766,10 +914,11 @@ impl MarketCoinOps for QtumCoin { utxo_common::DEFAULT_SWAP_VOUT, from_block, wait_until, + check_every, ) } - fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result { + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { utxo_common::tx_enum_from_bytes(self.as_ref(), bytes) } @@ -788,10 +937,20 @@ impl MarketCoinOps for QtumCoin { impl MmCoin for QtumCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) } + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { + Box::new( + utxo_common::get_tx_hex_by_hash(&self.utxo_arc, tx_hash) + .boxed() + .compat(), + ) + } + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { Box::new(utxo_common::withdraw(self.clone(), req).boxed().compat()) } @@ -828,7 +987,7 @@ impl MmCoin for QtumCoin { utxo_common::get_sender_trade_fee(self, value, stage).await } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { utxo_common::get_receiver_trade_fee(self.clone()) } @@ -854,6 +1013,8 @@ impl MmCoin for QtumCoin { fn swap_contract_address(&self) -> Option { utxo_common::swap_contract_address() } + fn fallback_swap_contract(&self) -> Option { utxo_common::fallback_swap_contract() } + fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } @@ -861,6 +1022,10 @@ impl MmCoin for QtumCoin { fn is_coin_protocol_supported(&self, info: &Option>) -> bool { utxo_common::is_coin_protocol_supported(self, info) } + + fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.as_ref().abortable_system) } + + fn on_token_deactivated(&self, _ticker: &str) {} } #[async_trait] @@ -891,10 +1056,11 @@ impl InitWithdrawCoin for QtumCoin { impl UtxoSignerOps for QtumCoin { type TxGetter = UtxoRpcClientEnum; - fn trezor_coin(&self) -> UtxoSignTxResult { + fn trezor_coin(&self) -> UtxoSignTxResult { self.utxo_arc .conf .trezor_coin + .clone() .or_mm_err(|| UtxoSignTxError::CoinNotSupportedWithTrezor { coin: self.utxo_arc.conf.ticker.clone(), }) @@ -926,7 +1092,7 @@ impl ExtractExtendedPubkey for QtumCoin { derivation_path: DerivationPath, ) -> MmResult where - XPubExtractor: HDXPubExtractor + Sync, + XPubExtractor: HDXPubExtractor, { utxo_common::extract_extended_pubkey(&self.utxo_arc.conf, xpub_extractor, derivation_path).await } @@ -939,13 +1105,28 @@ impl HDWalletCoinOps for QtumCoin { type HDWallet = UtxoHDWallet; type HDAccount = UtxoHDAccount; - fn derive_address( + async fn derive_addresses( &self, hd_account: &Self::HDAccount, + address_ids: Ids, + ) -> AddressDerivingResult>> + where + Ids: Iterator + Send, + { + utxo_common::derive_addresses(self, hd_account, address_ids).await + } + + async fn generate_and_confirm_new_address( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut Self::HDAccount, chain: Bip44Chain, - address_id: u32, - ) -> MmResult, AddressDerivingError> { - utxo_common::derive_address(self, hd_account, chain, address_id) + confirm_address: &ConfirmAddress, + ) -> MmResult, NewAddressDeriveConfirmError> + where + ConfirmAddress: HDConfirmAddress, + { + utxo_common::generate_and_confirm_new_address(self, hd_wallet, hd_account, chain, confirm_address).await } async fn create_new_account<'a, XPubExtractor>( @@ -954,7 +1135,7 @@ impl HDWalletCoinOps for QtumCoin { xpub_extractor: &XPubExtractor, ) -> MmResult, NewAccountCreatingError> where - XPubExtractor: HDXPubExtractor + Sync, + XPubExtractor: HDXPubExtractor, { utxo_common::create_new_account(self, hd_wallet, xpub_extractor).await } @@ -982,12 +1163,12 @@ impl HDWalletBalanceOps for QtumCoin { &self, hd_wallet: &Self::HDWallet, xpub_extractor: &XPubExtractor, - scan_policy: EnableCoinScanPolicy, + params: EnabledCoinBalanceParams, ) -> MmResult where - XPubExtractor: HDXPubExtractor + Sync, + XPubExtractor: HDXPubExtractor, { - coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, scan_policy).await + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params).await } async fn scan_for_new_addresses( @@ -1023,12 +1204,23 @@ impl HDWalletCoinWithStorageOps for QtumCoin { } #[async_trait] -impl HDWalletRpcOps for QtumCoin { - async fn get_new_address_rpc( +impl GetNewAddressRpcOps for QtumCoin { + async fn get_new_address_rpc_without_conf( &self, - params: GetNewHDAddressParams, - ) -> MmResult { - hd_wallet::common_impl::get_new_address_rpc(self, params).await + params: GetNewAddressParams, + ) -> MmResult { + get_new_address::common_impl::get_new_address_rpc_without_conf(self, params).await + } + + async fn get_new_address_rpc( + &self, + params: GetNewAddressParams, + confirm_address: &ConfirmAddress, + ) -> MmResult + where + ConfirmAddress: HDConfirmAddress, + { + get_new_address::common_impl::get_new_address_rpc(self, params, confirm_address).await } } @@ -1042,6 +1234,16 @@ impl AccountBalanceRpcOps for QtumCoin { } } +#[async_trait] +impl InitAccountBalanceRpcOps for QtumCoin { + async fn init_account_balance_rpc( + &self, + params: InitAccountBalanceParams, + ) -> MmResult { + init_account_balance::common_impl::init_account_balance_rpc(self, params).await + } +} + #[async_trait] impl InitScanAddressesRpcOps for QtumCoin { async fn init_scan_for_new_addresses_rpc( @@ -1053,16 +1255,82 @@ impl InitScanAddressesRpcOps for QtumCoin { } #[async_trait] -impl InitCreateHDAccountRpcOps for QtumCoin { +impl InitCreateAccountRpcOps for QtumCoin { async fn init_create_account_rpc( &self, params: CreateNewAccountParams, + state: CreateAccountState, xpub_extractor: &XPubExtractor, - ) -> MmResult + ) -> MmResult where - XPubExtractor: HDXPubExtractor + Sync, + XPubExtractor: HDXPubExtractor, { - init_create_account::common_impl::init_create_new_account_rpc(self, params, xpub_extractor).await + init_create_account::common_impl::init_create_new_account_rpc(self, params, state, xpub_extractor).await + } + + async fn revert_creating_account(&self, account_id: u32) { + init_create_account::common_impl::revert_creating_account(self, account_id).await + } +} + +#[async_trait] +impl CoinWithTxHistoryV2 for QtumCoin { + fn history_wallet_id(&self) -> WalletId { utxo_common::utxo_tx_history_v2_common::history_wallet_id(self.as_ref()) } + + async fn get_tx_history_filters( + &self, + target: MyTxHistoryTarget, + ) -> MmResult { + utxo_common::utxo_tx_history_v2_common::get_tx_history_filters(self, target).await + } +} + +#[async_trait] +impl UtxoTxHistoryOps for QtumCoin { + async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { + utxo_common::utxo_tx_history_v2_common::my_addresses(self).await + } + + async fn tx_details_by_hash( + &self, + params: UtxoTxDetailsParams<'_, Storage>, + ) -> MmResult, UtxoTxDetailsError> + where + Storage: TxHistoryStorage, + { + utxo_common::utxo_tx_history_v2_common::tx_details_by_hash(self, params).await + } + + async fn tx_from_storage_or_rpc( + &self, + tx_hash: &H256Json, + storage: &Storage, + ) -> MmResult { + utxo_common::utxo_tx_history_v2_common::tx_from_storage_or_rpc(self, tx_hash, storage).await + } + + async fn request_tx_history( + &self, + metrics: MetricsArc, + for_addresses: &HashSet
, + ) -> RequestTxHistoryResult { + utxo_common::utxo_tx_history_v2_common::request_tx_history(self, metrics, for_addresses).await + } + + async fn get_block_timestamp(&self, height: u64) -> MmResult { + self.as_ref().rpc_client.get_block_timestamp(height).await + } + + async fn my_addresses_balances(&self) -> BalanceResult> { + utxo_common::utxo_tx_history_v2_common::my_addresses_balances(self).await + } + + fn address_from_str(&self, address: &str) -> MmResult { + utxo_common::checked_address_from_str(self, address) + } + + fn set_history_sync_state(&self, new_state: HistorySyncState) { + *self.as_ref().history_sync_state.lock().unwrap() = new_state; } } diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index 4a532a3716..c7c172dbd2 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -6,7 +6,7 @@ use crate::utxo::qtum::{QtumBasedCoin, QtumCoin, QtumDelegationOps, QtumDelegati 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::utxo::{PrivKeyPolicyNotAllowed, UTXO_LOCK}; use crate::{DelegationError, DelegationFut, DelegationResult, MarketCoinOps, StakingInfos, StakingInfosError, StakingInfosFut, StakingInfosResult, TransactionDetails, TransactionType}; use bitcrypto::dhash256; @@ -78,8 +78,8 @@ 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 From for QtumStakingAbiError { + fn from(e: PrivKeyPolicyNotAllowed) -> Self { QtumStakingAbiError::Internal(e.to_string()) } } impl QtumDelegationOps for QtumCoin { @@ -123,7 +123,7 @@ impl QtumCoin { } 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)?; + let my_address = self.my_address()?; self.generate_delegation_transaction( outputs, my_address, @@ -156,10 +156,7 @@ impl QtumCoin { .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 last_tx_add = some_or_return_ok_none!(add_delegation_history.last()); let res = &client .blockchain_transaction_get_receipt(&last_tx_add.tx_hash) .compat() @@ -201,7 +198,7 @@ impl QtumCoin { async fn get_delegation_infos_impl(&self) -> StakingInfosResult { let coin = self.as_ref(); - let my_address = coin.derivation_method.iguana_or_err()?; + let my_address = coin.derivation_method.single_addr_or_err()?; let staker = self.am_i_currently_staking().await?; let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; @@ -252,7 +249,7 @@ impl QtumCoin { )?; let outputs = vec![delegation_output]; - let my_address = self.my_address().map_to_mm(DelegationError::InternalError)?; + let my_address = self.my_address()?; self.generate_delegation_transaction( outputs, my_address, @@ -272,7 +269,7 @@ impl QtumCoin { 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 my_address = utxo.derivation_method.single_addr_or_err()?; let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; let mut gas_fee = 0; @@ -317,7 +314,7 @@ impl QtumCoin { 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 my_address_string = self.my_address()?; 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(); @@ -340,15 +337,20 @@ impl QtumCoin { internal_id: vec![].into(), kmd_rewards: None, transaction_type, + memo: None, }) } 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(); + let script_pubkey = generate_contract_call_script_pubkey( + ¶ms, + gas_limit, + gas_price, + QTUM_DELEGATE_CONTRACT_ADDRESS.as_bytes(), + )? + .to_bytes(); Ok(ContractCallOutput { value: OUTPUT_QTUM_AMOUNT, script_pubkey, @@ -357,6 +359,7 @@ impl QtumCoin { }) } + #[allow(clippy::result_large_err)] fn add_delegation_output( &self, to_addr: H160, @@ -373,9 +376,13 @@ impl QtumCoin { Token::Bytes(pod.into()), ])?; - let script_pubkey = - generate_contract_call_script_pubkey(¶ms, gas_limit, gas_price, &QTUM_DELEGATE_CONTRACT_ADDRESS)? - .to_bytes(); + let script_pubkey = generate_contract_call_script_pubkey( + ¶ms, + gas_limit, + gas_price, + QTUM_DELEGATE_CONTRACT_ADDRESS.as_bytes(), + )? + .to_bytes(); Ok(ContractCallOutput { value: OUTPUT_QTUM_AMOUNT, script_pubkey, diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index dac1d3712f..2029702ee5 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -1,26 +1,29 @@ #![cfg_attr(target_arch = "wasm32", allow(unused_macros))] #![cfg_attr(target_arch = "wasm32", allow(dead_code))] -use crate::utxo::{output_script, sat_from_big_decimal}; +use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; +use crate::utxo::{output_script, sat_from_big_decimal, GetBlockHeaderError, GetConfirmedTxError, GetTxError, + GetTxHeightError}; 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_futures::{select_ok_sequential, timeout::FutureTimerExt}; use common::custom_iter::{CollectInto, TryIntoGroupMap}; -use common::executor::{spawn, Timer}; +use common::executor::{abortable_queue, abortable_queue::AbortableQueue, AbortableSystem, SpawnFuture, Timer}; use common::jsonrpc_client::{JsonRpcBatchClient, JsonRpcBatchResponse, JsonRpcClient, JsonRpcError, JsonRpcErrorType, JsonRpcId, JsonRpcMultiClient, JsonRpcRemoteAddr, JsonRpcRequest, JsonRpcRequestEnum, JsonRpcResponse, JsonRpcResponseEnum, JsonRpcResponseFut, RpcRes}; +use common::log::LogOnError; use common::log::{error, info, warn}; use common::{median, now_float, now_ms, OrdRange}; use derive_more::Display; use futures::channel::oneshot as async_oneshot; use futures::compat::{Future01CompatExt, Stream01CompatExt}; -use futures::future::{select as select_func, FutureExt, TryFutureExt}; +use futures::future::{FutureExt, TryFutureExt}; use futures::lock::Mutex as AsyncMutex; use futures::{select, StreamExt}; use futures01::future::select_ok; -use futures01::sync::{mpsc, oneshot}; +use futures01::sync::mpsc; use futures01::{Future, Sink, Stream}; use http::Uri; use itertools::Itertools; @@ -31,11 +34,14 @@ 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::{coin_variant_by_ticker, deserialize, serialize, serialize_with_flags, CoinVariant, CompactInteger, - Reader, SERIALIZE_TRANSACTION_WITNESS}; +use serialization::{deserialize, serialize, serialize_with_flags, CoinVariant, CompactInteger, Reader, + SERIALIZE_TRANSACTION_WITNESS}; use sha2::{Digest, Sha256}; +use spv_validation::helpers_validation::SPVError; +use spv_validation::storage::BlockHeaderStorageOps; use std::collections::hash_map::Entry; use std::collections::HashMap; +use std::convert::TryInto; use std::fmt; use std::io; use std::net::{SocketAddr, ToSocketAddrs}; @@ -63,11 +69,16 @@ cfg_native! { use webpki_roots::TLS_SERVER_ROOTS; } +pub const NO_TX_ERROR_CODE: &str = "'code': -5"; +const RESPONSE_TOO_LARGE_CODE: i16 = -32600; +const TX_NOT_FOUND_RETRIES: u8 = 10; + pub type AddressesByLabelResult = HashMap; pub type JsonRpcPendingRequestsShared = Arc>; pub type JsonRpcPendingRequests = HashMap>; pub type UnspentMap = HashMap>; +type ElectrumTxHistory = Vec; type ElectrumScriptHash = String; type ScriptHashUnspents = Vec; @@ -139,6 +150,7 @@ impl UtxoRpcClientEnum { check_every: u64, ) -> Box + Send> { let selfi = self.clone(); + let mut tx_not_found_retries = TX_NOT_FOUND_RETRIES; let fut = async move { loop { if now_ms() / 1000 > wait_until { @@ -167,11 +179,29 @@ impl UtxoRpcClientEnum { } }, Err(e) => { + if e.get_inner().is_tx_not_found_error() { + if tx_not_found_retries == 0 { + return ERR!( + "Tx {} was not found on chain after {} tries, error: {}", + tx_hash, + TX_NOT_FOUND_RETRIES, + e, + ); + } + error!( + "Tx {} not found on chain, error: {}, retrying in {} seconds. Retries left: {}", + tx_hash, e, check_every, tx_not_found_retries + ); + tx_not_found_retries -= 1; + Timer::sleep(check_every as f64).await; + continue; + }; + 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); + error!("Error {} getting block number, retrying in {} seconds", e, check_every); Timer::sleep(check_every as f64).await; continue; }, @@ -182,8 +212,8 @@ impl UtxoRpcClientEnum { } } error!( - "Error {:?} getting the transaction {:?}, retrying in 10 seconds", - e, tx_hash + "Error {:?} getting the transaction {:?}, retrying in {} seconds", + e, tx_hash, check_every ) }, } @@ -258,7 +288,9 @@ pub enum UtxoRpcError { impl From for UtxoRpcError { fn from(e: JsonRpcError) -> Self { match e.error { - JsonRpcErrorType::InvalidRequest(_) => UtxoRpcError::Internal(e.to_string()), + JsonRpcErrorType::InvalidRequest(_) | JsonRpcErrorType::Internal(_) => { + UtxoRpcError::Internal(e.to_string()) + }, JsonRpcErrorType::Transport(_) => UtxoRpcError::Transport(e), JsonRpcErrorType::Parse(_, _) | JsonRpcErrorType::Response(_, _) => UtxoRpcError::ResponseParseError(e), } @@ -273,6 +305,30 @@ impl From for UtxoRpcError { fn from(e: NumConversError) -> Self { UtxoRpcError::Internal(e.to_string()) } } +impl UtxoRpcError { + pub fn is_tx_not_found_error(&self) -> bool { + if let UtxoRpcError::ResponseParseError(ref json_err) = self { + if let JsonRpcErrorType::Response(_, json) = &json_err.error { + return json["error"]["code"] == -5 // native compatible + || json["message"].as_str().unwrap_or_default().contains(NO_TX_ERROR_CODE); + // electrum compatible; + } + }; + false + } + + pub fn is_response_too_large(&self) -> bool { + if let UtxoRpcError::ResponseParseError(ref json_err) = self { + if let JsonRpcErrorType::Response(_, json) = &json_err.error { + return json["code"] == RESPONSE_TOO_LARGE_CODE; + } + }; + false + } + + pub fn is_network_error(&self) -> bool { matches!(self, UtxoRpcError::Transport(_)) } +} + /// Common operations that both types of UTXO clients have but implement them differently #[async_trait] pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { @@ -337,7 +393,25 @@ pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { ) -> UtxoRpcFut; /// Returns block time in seconds since epoch (Jan 1 1970 GMT). - async fn get_block_timestamp(&self, height: u64) -> Result>; + async fn get_block_timestamp(&self, height: u64) -> Result>; + + /// Returns verbose transaction by the given `txid` if it's on-chain or None if it's not. + async fn get_tx_if_onchain(&self, tx_hash: &H256Json) -> Result, MmError> { + match self + .get_transaction_bytes(tx_hash) + .compat() + .await + .map_err(|e| e.into_inner()) + { + Ok(bytes) => Ok(Some(deserialize(bytes.as_slice())?)), + Err(err) => { + if err.is_tx_not_found_error() { + return Ok(None); + } + Err(err.into()) + }, + } + } } #[derive(Clone, Deserialize, Debug)] @@ -483,14 +557,14 @@ pub enum EstimateFeeMethod { SmartFee, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum BlockNonce { String(String), U64(u64), } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct VerboseBlock { /// Block hash pub hash: H256Json, @@ -578,7 +652,7 @@ impl Default for NativeClientImpl { pub struct NativeClient(pub Arc); impl Deref for NativeClient { type Target = NativeClientImpl; - fn deref(&self) -> &NativeClientImpl { &*self.0 } + fn deref(&self) -> &NativeClientImpl { &self.0 } } /// The trait provides methods to generate the JsonRpcClient instance info such as name of coin. @@ -603,8 +677,8 @@ impl JsonRpcClient for NativeClientImpl { #[cfg(target_arch = "wasm32")] fn transport(&self, _request: JsonRpcRequestEnum) -> JsonRpcResponseFut { - Box::new(futures01::future::err(ERRL!( - "'NativeClientImpl' must be used in native mode only" + Box::new(futures01::future::err(JsonRpcErrorType::Internal( + "'NativeClientImpl' must be used in native mode only".to_string(), ))) } @@ -612,37 +686,37 @@ impl JsonRpcClient for NativeClientImpl { fn transport(&self, request: JsonRpcRequestEnum) -> JsonRpcResponseFut { use mm2_net::transport::slurp_req; - let request_body = try_fus!(json::to_string(&request)); + let request_body = + try_f!(json::to_string(&request).map_err(|e| JsonRpcErrorType::InvalidRequest(e.to_string()))); // 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()); let uri = self.uri.clone(); - let http_request = try_fus!(Request::builder() + let http_request = try_f!(Request::builder() .method("POST") .header(AUTHORIZATION, self.auth.clone()) .uri(uri.clone()) - .body(Vec::from(request_body))); + .body(Vec::from(request_body)) + .map_err(|e| JsonRpcErrorType::InvalidRequest(e.to_string()))); let event_handles = self.event_handlers.clone(); Box::new(slurp_req(http_request).boxed().compat().then( - move |result| -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String> { - let res = try_s!(result); + move |result| -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), JsonRpcErrorType> { + let res = result.map_err(|e| e.into_inner())?; // 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); - let body = try_s!(std::str::from_utf8(&res.2)); + let body = + std::str::from_utf8(&res.2).map_err(|e| JsonRpcErrorType::parse_error(&uri, e.to_string()))?; if res.0 != StatusCode::OK { - return ERR!( - "Rpc request {:?} failed with HTTP status code {}, response body: {}", - request, - res.0, - body - ); + let res_value = serde_json::from_slice(&res.2) + .map_err(|e| JsonRpcErrorType::parse_error(&uri, e.to_string()))?; + return Err(JsonRpcErrorType::Response(uri.into(), res_value)); } - let response = try_s!(json::from_str(body)); + let response = json::from_str(body).map_err(|e| JsonRpcErrorType::parse_error(&uri, e.to_string()))?; Ok((uri.into(), response)) }, )) @@ -873,7 +947,7 @@ impl UtxoRpcClientOps for NativeClient { Box::new(fut.boxed().compat()) } - async fn get_block_timestamp(&self, height: u64) -> Result> { + async fn get_block_timestamp(&self, height: u64) -> Result> { let block = self.get_block_by_height(height).await?; Ok(block.time as u64) } @@ -1096,7 +1170,7 @@ impl NativeClientImpl { /// https://developer.bitcoin.org/reference/rpc/getblockheader.html pub fn get_block_header_bytes(&self, block_hash: H256Json) -> RpcRes { - let verbose = 0; + let verbose = false; rpc_func!(self, "getblockheader", block_hash, verbose) } } @@ -1228,6 +1302,14 @@ pub struct TxMerkleBranch { pub pos: usize, } +#[derive(Clone)] +pub struct ConfirmedTransactionInfo { + pub tx: UtxoTx, + pub header: BlockHeader, + pub index: u64, + pub height: u64, +} + #[derive(Debug, PartialEq)] pub struct BestBlock { pub height: u64, @@ -1373,10 +1455,12 @@ fn addr_to_socket_addr(input: &str) -> Result { } /// Attempts to process the request (parse url, etc), build up the config and create new electrum connection +/// The function takes `abortable_system` that will be used to spawn Electrum's related futures. #[cfg(not(target_arch = "wasm32"))] pub fn spawn_electrum( req: &ElectrumRpcRequest, event_handlers: Vec, + abortable_system: AbortableQueue, ) -> Result { let config = match req.protocol { ElectrumProtocol::TCP => ElectrumConfig::TCP, @@ -1399,14 +1483,21 @@ pub fn spawn_electrum( }, }; - Ok(electrum_connect(req.url.clone(), config, event_handlers)) + Ok(electrum_connect( + req.url.clone(), + config, + event_handlers, + abortable_system, + )) } /// Attempts to process the request (parse url, etc), build up the config and create new electrum connection +/// The function takes `abortable_system` that will be used to spawn Electrum's related futures. #[cfg(target_arch = "wasm32")] pub fn spawn_electrum( req: &ElectrumRpcRequest, event_handlers: Vec, + abortable_system: AbortableQueue, ) -> Result { let mut url = req.url.clone(); let uri: Uri = try_s!(req.url.parse()); @@ -1434,10 +1525,9 @@ pub fn spawn_electrum( }, }; - Ok(electrum_connect(url, config, event_handlers)) + Ok(electrum_connect(url, config, event_handlers, abortable_system)) } -#[derive(Debug)] /// Represents the active Electrum connection to selected address pub struct ElectrumConnection { /// The client connected to this SocketAddr @@ -1447,28 +1537,25 @@ pub struct ElectrumConnection { 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: JsonRpcPendingRequestsShared, /// Selected protocol version. The value is initialized after the server.version RPC call. protocol_version: AsyncMutex>, + /// This spawner is used to spawn Electrum's related futures that should be aborted on coin deactivation. + /// and on [`MmArc::stop`]. + /// This field is not used directly, but it holds all abort handles of futures spawned at `electrum_connect`. + /// + /// Please also note that this abortable system is a subsystem of [`ElectrumClientImpl::abortable_system`]. + /// For more info see [`ElectrumClientImpl::add_server`]. + _abortable_system: AbortableQueue, } impl ElectrumConnection { async fn is_connected(&self) -> bool { self.tx.lock().await.is_some() } async fn set_protocol_version(&self, version: f32) { self.protocol_version.lock().await.replace(version); } -} -impl Drop for ElectrumConnection { - fn drop(&mut self) { - if let Some(shutdown_tx) = self.shutdown_tx.take() { - if shutdown_tx.send(()).is_err() { - warn!("electrum_connection_drop] Warning, shutdown_tx already closed"); - } - } - } + async fn reset_protocol_version(&self) { *self.protocol_version.lock().await = None; } } #[derive(Debug)] @@ -1539,52 +1626,61 @@ pub struct ElectrumClientImpl { protocol_version: OrdRange, get_balance_concurrent_map: ConcurrentRequestMap, list_unspent_concurrent_map: ConcurrentRequestMap>, + block_headers_storage: BlockHeaderStorage, + /// This spawner is used to spawn Electrum's related futures that should be aborted on coin deactivation, + /// and on [`MmArc::stop`]. + /// + /// Please also note that this abortable system is a subsystem of [`UtxoCoinFields::abortable_system`]. + abortable_system: AbortableQueue, + negotiate_version: bool, } async fn electrum_request_multi( client: ElectrumClient, request: JsonRpcRequestEnum, -) -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String> { +) -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), JsonRpcErrorType> { let mut futures = vec![]; let connections = client.connections.lock().await; for (i, connection) in connections.iter().enumerate() { + if client.negotiate_version && connection.protocol_version.lock().await.is_none() { + continue; + } + let connection_addr = connection.addr.clone(); - match &*connection.tx.lock().await { - Some(tx) => { - let fut = electrum_request( - request.clone(), - tx.clone(), - connection.responses.clone(), - ELECTRUM_TIMEOUT / (connections.len() - i) as u64, - ) - .map(|response| (JsonRpcRemoteAddr(connection_addr), response)); - futures.push(fut) - }, - None => (), + let json = json::to_string(&request).map_err(|e| JsonRpcErrorType::InvalidRequest(e.to_string()))?; + if let Some(tx) = &*connection.tx.lock().await { + let fut = electrum_request( + json, + request.rpc_id(), + tx.clone(), + connection.responses.clone(), + ELECTRUM_TIMEOUT / (connections.len() - i) as u64, + ) + .map(|response| (JsonRpcRemoteAddr(connection_addr), response)); + futures.push(fut) } } drop(connections); + if futures.is_empty() { - return ERR!("All electrums are currently disconnected"); + return Err(JsonRpcErrorType::Transport( + "All electrums are currently disconnected".to_string(), + )); } - match request { - JsonRpcRequestEnum::Single(single) if single.method == "server.ping" => { + if let JsonRpcRequestEnum::Single(single) = &request { + 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; - }, - _ => (), + return select_ok(futures).map(|(result, _)| result).compat().await; + } } let (res, no_of_failed_requests) = select_ok_sequential(futures) .compat() .await - .map_err(|e| ERRL!("{:?}", e))?; + .map_err(|e| JsonRpcErrorType::Transport(format!("{:?}", e)))?; client.rotate_servers(no_of_failed_requests).await; + Ok(res) } @@ -1592,35 +1688,41 @@ async fn electrum_request_to( client: ElectrumClient, request: JsonRpcRequestEnum, to_addr: String, -) -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String> { +) -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), JsonRpcErrorType> { let (tx, responses) = { let connections = client.connections.lock().await; let connection = connections .iter() .find(|c| c.addr == to_addr) - .ok_or(ERRL!("Unknown destination address {}", to_addr))?; + .ok_or_else(|| JsonRpcErrorType::Internal(format!("Unknown destination address {}", to_addr)))?; let responses = connection.responses.clone(); let tx = { match &*connection.tx.lock().await { Some(tx) => tx.clone(), - None => return ERR!("Connection {} is not established yet", to_addr), + None => { + return Err(JsonRpcErrorType::Transport(format!( + "Connection {} is not established yet", + to_addr + ))) + }, } }; (tx, responses) }; - - let response = try_s!( - electrum_request(request.clone(), tx, responses, ELECTRUM_TIMEOUT) - .compat() - .await - ); + let json = json::to_string(&request).map_err(|err| JsonRpcErrorType::InvalidRequest(err.to_string()))?; + let response = electrum_request(json, request.rpc_id(), tx, responses, ELECTRUM_TIMEOUT) + .compat() + .await?; Ok((JsonRpcRemoteAddr(to_addr.to_owned()), response)) } impl ElectrumClientImpl { + pub fn spawner(&self) -> abortable_queue::WeakSpawner { self.abortable_system.weak_spawner() } + /// Create an Electrum connection and spawn a green thread actor to handle it. pub async fn add_server(&self, req: &ElectrumRpcRequest) -> Result<(), String> { - let connection = try_s!(spawn_electrum(req, self.event_handlers.clone())); + let subsystem = try_s!(self.abortable_system.create_subsystem()); + let connection = try_s!(spawn_electrum(req, self.event_handlers.clone(), subsystem)); self.connections.lock().await.push(connection); Ok(()) } @@ -1677,15 +1779,29 @@ impl ElectrumClientImpl { Ok(()) } + /// Reset the protocol version for the specified server. + pub async fn reset_protocol_version(&self, server_addr: &str) -> Result<(), String> { + let connections = self.connections.lock().await; + let con = connections + .iter() + .find(|con| con.addr == server_addr) + .ok_or(ERRL!("Unknown electrum address {}", server_addr))?; + con.reset_protocol_version().await; + Ok(()) + } + /// Get available protocol versions. pub fn protocol_version(&self) -> &OrdRange { &self.protocol_version } + + /// Get block headers storage. + pub fn block_headers_storage(&self) -> &BlockHeaderStorage { &self.block_headers_storage } } #[derive(Clone, Debug)] pub struct ElectrumClient(pub Arc); impl Deref for ElectrumClient { type Target = ElectrumClientImpl; - fn deref(&self) -> &ElectrumClientImpl { &*self.0 } + fn deref(&self) -> &ElectrumClientImpl { &self.0 } } const BLOCKCHAIN_HEADERS_SUB_ID: &str = "blockchain.headers.subscribe"; @@ -1777,10 +1893,22 @@ impl ElectrumClient { } /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-history - pub fn scripthash_get_history(&self, hash: &str) -> RpcRes> { + pub fn scripthash_get_history(&self, hash: &str) -> RpcRes { rpc_func!(self, "blockchain.scripthash.get_history", hash) } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-history + /// Requests history of the `hashes` in a batch and returns them in the same order they were requested. + pub fn scripthash_get_history_batch(&self, hashes: I) -> RpcRes> + where + I: IntoIterator, + { + let requests = hashes + .into_iter() + .map(|hash| rpc_req!(self, "blockchain.scripthash.get_history", hash)); + self.batch_rpc(requests) + } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-gethistory pub fn scripthash_get_balance(&self, hash: &str) -> RpcRes { let arc = self.clone(); @@ -1837,19 +1965,121 @@ impl ElectrumClient { rpc_func!(self, "blockchain.block.headers", start_height, count) } - pub fn retrieve_last_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) + } + + // get_tx_height_from_rpc is costly since it loops through history after requesting the whole history of the script pubkey + // This method should always be used if the block headers are saved to the DB + async fn get_tx_height_from_storage(&self, tx: &UtxoTx) -> Result> { + let tx_hash = tx.hash().reversed(); + let blockhash = self.get_verbose_transaction(&tx_hash.into()).compat().await?.blockhash; + Ok(self + .block_headers_storage() + .get_block_height_by_hash(blockhash.into()) + .await? + .ok_or_else(|| { + GetTxHeightError::HeightNotFound(format!( + "Transaction block header is not found in storage for {}", + self.0.coin_ticker + )) + })? + .try_into()?) + } + + // get_tx_height_from_storage is always preferred to be used instead of this, but if there is no headers in storage (storing headers is not enabled) + // this function can be used instead + async fn get_tx_height_from_rpc(&self, tx: &UtxoTx) -> Result { + for output in tx.outputs.clone() { + let script_pubkey_str = hex::encode(electrum_script_hash(&output.script_pubkey)); + if let Ok(history) = self.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); + } + } + } + Err(GetTxHeightError::HeightNotFound(format!( + "Couldn't find height through electrum for {}", + self.coin_ticker + ))) + } + + async fn block_header_from_storage(&self, height: u64) -> Result> { + self.block_headers_storage() + .get_block_header(height) + .await? + .ok_or_else(|| { + GetBlockHeaderError::Internal(format!("Header not found in storage for {}", self.coin_ticker)).into() + }) + } + + async fn block_header_from_storage_or_rpc(&self, height: u64) -> Result> { + match self.block_header_from_storage(height).await { + Ok(h) => Ok(h), + Err(_) => Ok(deserialize( + self.blockchain_block_header(height).compat().await?.as_slice(), + )?), + } + } + + pub async fn get_confirmed_tx_info_from_rpc( &self, - blocks_limit_to_check: NonZeroU64, - block_height: u64, - ) -> UtxoRpcFut<(HashMap, Vec)> { + tx: &UtxoTx, + ) -> Result { + let height = self.get_tx_height_from_rpc(tx).await?; + + let merkle_branch = self + .blockchain_transaction_get_merkle(tx.hash().reversed().into(), height) + .compat() + .await?; + + let header = deserialize(self.blockchain_block_header(height).compat().await?.as_slice())?; + + Ok(ConfirmedTransactionInfo { + tx: tx.clone(), + header, + index: merkle_branch.pos as u64, + height, + }) + } + + pub async fn get_merkle_and_validated_header( + &self, + tx: &UtxoTx, + ) -> Result<(TxMerkleBranch, BlockHeader, u64), MmError> { + let height = self.get_tx_height_from_storage(tx).await?; + + let merkle_branch = self + .blockchain_transaction_get_merkle(tx.hash().reversed().into(), height) + .compat() + .await + .map_to_mm(|err| SPVError::UnableToGetMerkle { + coin: self.coin_ticker.clone(), + err: err.to_string(), + })?; + + let header = self.block_header_from_storage(height).await?; + + Ok((merkle_branch, header, height)) + } +} + +#[cfg_attr(test, mockable)] +impl ElectrumClient { + pub fn retrieve_headers(&self, from: u64, to: 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) + if from == 0 || to < from { + return Box::new(futures01::future::err( + UtxoRpcError::Internal("Invalid values for from/to parameters".to_string()).into(), + )); + } + let count: NonZeroU64 = match (to - from + 1).try_into() { + Ok(c) => c, + Err(e) => return Box::new(futures01::future::err(UtxoRpcError::Internal(e.to_string()).into())), }; Box::new( self.blockchain_block_headers(from, count) @@ -1862,8 +2092,9 @@ impl ElectrumClient { 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); + drop_mutability!(serialized); + let mut reader = + Reader::new_with_coin_variant(serialized.as_slice(), coin_name.as_str().into()); let maybe_block_headers = reader.read_list::(); let block_headers = match maybe_block_headers { Ok(headers) => headers, @@ -1881,11 +2112,6 @@ impl ElectrumClient { }), ) } - - /// 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 @@ -2107,17 +2333,20 @@ 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) + async fn get_block_timestamp(&self, height: u64) -> Result> { + Ok(self.block_header_from_storage_or_rpc(height).await?.time as u64) } } #[cfg_attr(test, mockable)] impl ElectrumClientImpl { - pub fn new(coin_ticker: String, event_handlers: Vec) -> ElectrumClientImpl { + pub fn new( + coin_ticker: String, + event_handlers: Vec, + block_headers_storage: BlockHeaderStorage, + abortable_system: AbortableQueue, + negotiate_version: bool, + ) -> ElectrumClientImpl { let protocol_version = OrdRange::new(1.2, 1.4).unwrap(); ElectrumClientImpl { coin_ticker, @@ -2127,6 +2356,9 @@ impl ElectrumClientImpl { protocol_version, get_balance_concurrent_map: ConcurrentRequestMap::new(), list_unspent_concurrent_map: ConcurrentRequestMap::new(), + block_headers_storage, + abortable_system, + negotiate_version, } } @@ -2135,10 +2367,18 @@ impl ElectrumClientImpl { coin_ticker: String, event_handlers: Vec, protocol_version: OrdRange, + block_headers_storage: BlockHeaderStorage, + abortable_system: AbortableQueue, ) -> ElectrumClientImpl { ElectrumClientImpl { protocol_version, - ..ElectrumClientImpl::new(coin_ticker, event_handlers) + ..ElectrumClientImpl::new( + coin_ticker, + event_handlers, + block_headers_storage, + abortable_system, + false, + ) } } } @@ -2333,12 +2573,13 @@ lazy_static! { } #[cfg(not(target_arch = "wasm32"))] -async fn connect_loop( +async fn connect_loop( config: ElectrumConfig, addr: String, responses: JsonRpcPendingRequestsShared, connection_tx: Arc>>>>, event_handlers: Vec, + _spawner: Spawner, ) -> Result<(), ()> { let delay = Arc::new(AtomicU64::new(0)); @@ -2436,6 +2677,7 @@ async fn connect_loop( macro_rules! reset_tx_and_continue { () => { info!("{} connection dropped", addr); + event_handlers.on_disconnected(addr.clone()).error_log(); *connection_tx.lock().await = None; increase_delay(&delay); continue; @@ -2451,12 +2693,13 @@ async fn connect_loop( } #[cfg(target_arch = "wasm32")] -async fn connect_loop( +async fn connect_loop( _config: ElectrumConfig, addr: String, responses: JsonRpcPendingRequestsShared, connection_tx: Arc>>>>, event_handlers: Vec, + spawner: Spawner, ) -> Result<(), ()> { use std::sync::atomic::AtomicUsize; @@ -2474,7 +2717,8 @@ async fn connect_loop( } let conn_idx = CONN_IDX.fetch_add(1, AtomicOrdering::Relaxed); - let (mut transport_tx, mut transport_rx) = try_loop!(ws_transport(conn_idx, &addr).await, addr, delay); + let (mut transport_tx, mut transport_rx) = + try_loop!(ws_transport(conn_idx, &addr, &spawner).await, addr, delay); info!("Electrum client connected to {}", addr); try_loop!(event_handlers.on_connected(addr.clone()), addr, delay); @@ -2540,6 +2784,7 @@ async fn connect_loop( () => { info!("{} connection dropped", addr); *connection_tx.lock().await = None; + event_handlers.on_disconnected(addr.clone()).error_log(); increase_delay(&delay); continue; }; @@ -2554,66 +2799,69 @@ async fn connect_loop( } /// Builds up the electrum connection, spawns endless loop that attempts to reconnect to the server -/// in case of connection errors +/// in case of connection errors. +/// The function takes `abortable_system` that will be used to spawn Electrum's related futures. fn electrum_connect( addr: String, config: ElectrumConfig, event_handlers: Vec, + abortable_system: AbortableQueue, ) -> ElectrumConnection { - let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); let responses = Arc::new(AsyncMutex::new(JsonRpcPendingRequests::default())); let tx = Arc::new(AsyncMutex::new(None)); - let connect_loop = connect_loop( + let spawner = abortable_system.weak_spawner(); + let fut = connect_loop( config.clone(), addr.clone(), responses.clone(), tx.clone(), event_handlers, - ); + spawner.clone(), + ) + .then(|_| futures::future::ready(())); - let connect_loop = select_func(connect_loop.boxed(), shutdown_rx.compat()); - spawn(connect_loop.map(|_| ())); + spawner.spawn(fut); ElectrumConnection { addr, config, tx, - shutdown_tx: Some(shutdown_tx), responses, protocol_version: AsyncMutex::new(None), + _abortable_system: abortable_system, } } +/// # Important +/// `electrum_request` should always return [`JsonRpcErrorType::Transport`] error. fn electrum_request( - request: JsonRpcRequestEnum, + mut req_json: String, + rpc_id: JsonRpcId, tx: mpsc::Sender>, 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"))] { // Electrum request and responses must end with \n // https://electrumx.readthedocs.io/en/latest/protocol-basics.html#message-stream - json.push('\n'); + req_json.push('\n'); } - let (req_tx, resp_rx) = async_oneshot::channel(); - responses.lock().await.insert(request.rpc_id(), req_tx); - try_s!(tx.send(json.into_bytes()).compat().await); - let resps = try_s!(resp_rx.await); + responses.lock().await.insert(rpc_id, req_tx); + tx.send(req_json.into_bytes()) + .compat() + .await + .map_err(|err| JsonRpcErrorType::Transport(err.to_string()))?; + let resps = resp_rx.await.map_err(|e| JsonRpcErrorType::Transport(e.to_string()))?; Ok(resps) }; let send_fut = send_fut .boxed() .timeout(Duration::from_secs(timeout)) .compat() - .then(|res| match res { - Ok(response) => response, - Err(timeout_error) => ERR!("{}", timeout_error), - }) - .map_err(|e| ERRL!("{}", e)); + .then(move |res| res.map_err(|err| JsonRpcErrorType::Transport(err.to_string()))?); Box::new(send_fut) } diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 9ed15d4843..1f7cf01839 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -3,7 +3,8 @@ //! 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::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut}; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::bch::BchCoin; use crate::utxo::bchd_grpc::{check_slp_transaction, validate_slp_utxos, ValidateSlpUtxosErr}; @@ -12,17 +13,24 @@ use crate::utxo::utxo_common::{self, big_decimal_from_sat_unsigned, payment_scri 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, +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, + HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, + NumConversError, PaymentInstructions, PaymentInstructionsErr, PrivKeyPolicyNotAllowed, RawTransactionFut, + RawTransactionRequest, RefundError, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, + SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, + SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, + SendWatcherRefundsPaymentArgs, SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, - TransactionErr, TransactionFut, TxFeeDetails, UnexpectedDerivationMethod, ValidateAddressResult, - ValidatePaymentInput, VerificationError, VerificationResult, WithdrawError, WithdrawFee, WithdrawFut, + TransactionErr, TransactionFut, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentInput, VerificationError, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use bitcrypto::dhash160; use chain::constants::SEQUENCE_FINAL; use chain::{OutPoint, TransactionOutput}; +use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}; use common::log::warn; use common::now_ms; use derive_more::Display; @@ -41,7 +49,7 @@ use rpc::v1::types::{Bytes as BytesJson, ToTxHash, H256 as H256Json}; use script::bytes::Bytes; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; -use serialization::{deserialize, serialize, Deserializable, Error, Reader}; +use serialization::{deserialize, serialize, Deserializable, Error as SerError, Reader}; use serialization_derive::Deserializable; use std::convert::TryInto; use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; @@ -57,12 +65,35 @@ const SLP_SEND: &str = "SEND"; const SLP_MINT: &str = "MINT"; const SLP_GENESIS: &str = "GENESIS"; -#[derive(Debug)] -pub struct SlpTokenConf { +#[derive(Debug, Display)] +#[allow(clippy::large_enum_variant)] +pub enum EnableSlpError { + GetBalanceError(UtxoRpcError), + UnexpectedDerivationMethod(String), + Internal(String), +} + +impl From for EnableSlpError { + fn from(err: MyAddressError) -> Self { + match err { + MyAddressError::UnexpectedDerivationMethod(der) => EnableSlpError::UnexpectedDerivationMethod(der), + MyAddressError::InternalError(internal) => EnableSlpError::Internal(internal), + } + } +} + +impl From for EnableSlpError { + fn from(e: AbortedError) -> Self { EnableSlpError::Internal(e.to_string()) } +} + +pub struct SlpTokenFields { decimals: u8, ticker: String, token_id: H256, required_confirmations: AtomicU64, + /// This abortable system is used to spawn coin's related futures that should be aborted on coin deactivation + /// and on [`MmArc::stop`]. + abortable_system: AbortableQueue, } /// Minimalistic info that is used to be stored outside of the token's context @@ -73,9 +104,9 @@ pub struct SlpTokenInfo { pub decimals: u8, } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct SlpToken { - conf: Arc, + conf: Arc, platform_coin: BchCoin, } @@ -98,37 +129,6 @@ struct SlpTxPreimage { outputs: Vec, } -#[derive(Debug, Display)] -enum ValidateHtlcError { - TxLackOfOutputs, - #[display(fmt = "TxParseError: {:?}", _0)] - TxParseError(Error), - #[display(fmt = "OpReturnParseError: {:?}", _0)] - 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, @@ -153,7 +153,7 @@ pub enum SpendP2SHError { GenerateTxErr(GenerateTxError), Rpc(UtxoRpcError), SignTxErr(UtxoSignWithKeyPairError), - PrivKeyNotAllowed(PrivKeyNotAllowed), + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), UnexpectedDerivationMethod(UnexpectedDerivationMethod), String(String), } @@ -170,8 +170,8 @@ 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: PrivKeyPolicyNotAllowed) -> Self { SpendP2SHError::PrivKeyPolicyNotAllowed(e) } } impl From for SpendP2SHError { @@ -186,7 +186,7 @@ impl From for SpendP2SHError { pub enum SpendHtlcError { TxLackOfOutputs, #[display(fmt = "DeserializationErr: {:?}", _0)] - DeserializationErr(Error), + DeserializationErr(SerError), #[display(fmt = "PubkeyParseError: {:?}", _0)] PubkeyParseErr(keys::Error), InvalidSlpDetails, @@ -206,8 +206,8 @@ impl From for SpendHtlcError { fn from(err: NumConversError) -> SpendHtlcError { SpendHtlcError::NumConversionErr(err) } } -impl From for SpendHtlcError { - fn from(err: Error) -> SpendHtlcError { SpendHtlcError::DeserializationErr(err) } +impl From for SpendHtlcError { + fn from(err: SerError) -> SpendHtlcError { SpendHtlcError::DeserializationErr(err) } } impl From for SpendHtlcError { @@ -303,14 +303,19 @@ impl SlpToken { token_id: H256, platform_coin: BchCoin, required_confirmations: u64, - ) -> SlpToken { - let conf = Arc::new(SlpTokenConf { + ) -> MmResult { + // Create an abortable system linked to `platform_coin` so if the platform coin is disabled, + // all spawned futures related to `SlpToken` will be aborted as well. + let abortable_system = platform_coin.as_ref().abortable_system.create_subsystem()?; + + let conf = Arc::new(SlpTokenFields { decimals, ticker, token_id, required_confirmations: AtomicU64::new(required_confirmations), + abortable_system, }); - SlpToken { conf, platform_coin } + Ok(SlpToken { conf, platform_coin }) } /// Returns the OP_RETURN output for SLP Send transaction @@ -432,11 +437,13 @@ impl SlpToken { .await } - async fn validate_htlc(&self, input: ValidatePaymentInput) -> Result<(), MmError> { - let mut tx: UtxoTx = deserialize(input.payment_tx.as_slice()).map_to_mm(ValidateHtlcError::TxParseError)?; + async fn validate_htlc(&self, input: ValidatePaymentInput) -> Result<(), MmError> { + let mut tx: UtxoTx = deserialize(input.payment_tx.as_slice())?; tx.tx_hash_algo = self.platform_coin.as_ref().tx_hash_algo; if tx.outputs.len() < 2 { - return MmError::err(ValidateHtlcError::TxLackOfOutputs); + return MmError::err(ValidatePaymentError::TxDeserializationError( + "Not enough transaction output".to_string(), + )); } let slp_satoshis = sat_from_big_decimal(&input.amount, self.decimals())?; @@ -459,26 +466,41 @@ impl SlpToken { match slp_tx.transaction { SlpTransaction::Send { token_id, amounts } => { if token_id != self.token_id() { - return MmError::err(ValidateHtlcError::InvalidSlpDetails); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid tx token_id, Expected: {}, found: {}", + token_id, + self.token_id() + ))); } if amounts.is_empty() { - return MmError::err(ValidateHtlcError::InvalidSlpDetails); + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Input amount can't be empty".to_string(), + )); } if amounts[0] != slp_satoshis { - return MmError::err(ValidateHtlcError::InvalidSlpDetails); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid input amount. Expected: {}, found: {}", + slp_satoshis, amounts[0] + ))); } }, - _ => return MmError::err(ValidateHtlcError::InvalidSlpDetails), + _ => { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Invalid Slp tx details".to_string(), + )) + }, } let htlc_keypair = self.derive_htlc_key_pair(&input.unique_swap_data); + let first_pub = &Public::from_slice(&input.other_pub) + .map_to_mm(|err| ValidatePaymentError::InvalidParameter(err.to_string()))?; let validate_fut = utxo_common::validate_payment( self.platform_coin.clone(), tx, SLP_SWAP_VOUT, - &Public::from_slice(&input.other_pub).map_to_mm(ValidateHtlcError::OtherPubInvalid)?, + first_pub, htlc_keypair.public(), &input.secret_hash, self.platform_dust_dec(), @@ -486,13 +508,7 @@ impl SlpToken { now_ms() / 1000 + 60, input.confirmations, ); - - validate_fut - .compat() - .await - .map_to_mm(ValidateHtlcError::ValidatePaymentError)?; - - Ok(()) + validate_fut.compat().await } pub async fn refund_htlc( @@ -519,7 +535,7 @@ impl SlpToken { if token_id != self.token_id() { return MmError::err(SpendHtlcError::InvalidSlpDetails); } - *amounts.get(0).ok_or(SpendHtlcError::InvalidSlpDetails)? + *amounts.first().ok_or(SpendHtlcError::InvalidSlpDetails)? }, _ => return MmError::err(SpendHtlcError::InvalidSlpDetails), }; @@ -556,20 +572,21 @@ impl SlpToken { other_pub: &Public, time_lock: u32, secret: &[u8], + secret_hash: &[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 = payment_script(time_lock, &*dhash160(secret), &other_pub, keypair.public()); + let redeem = payment_script(time_lock, secret_hash, &other_pub, keypair.public()); let slp_amount = match slp_tx.transaction { SlpTransaction::Send { token_id, amounts } => { if token_id != self.token_id() { return MmError::err(SpendHtlcError::InvalidSlpDetails); } - *amounts.get(0).ok_or(SpendHtlcError::InvalidSlpDetails)? + *amounts.first().ok_or(SpendHtlcError::InvalidSlpDetails)? }, _ => return MmError::err(SpendHtlcError::InvalidSlpDetails), }; @@ -815,7 +832,7 @@ impl SlpTransaction { } impl Deserializable for SlpTransaction { - fn deserialize(reader: &mut Reader) -> Result + fn deserialize(reader: &mut Reader) -> Result where Self: Sized, T: std::io::Read, @@ -831,7 +848,7 @@ impl Deserializable for SlpTransaction { } else { let mut url = vec![0; maybe_push_op_code as usize]; reader.read_slice(&mut url)?; - String::from_utf8(url).map_err(|e| Error::Custom(e.to_string()))? + String::from_utf8(url).map_err(|e| SerError::Custom(e.to_string()))? }; let maybe_push_op_code: u8 = reader.read()?; @@ -852,7 +869,7 @@ impl Deserializable for SlpTransaction { }; let bytes: Vec = reader.read_list()?; if bytes.len() != 8 { - return Err(Error::Custom(format!("Expected 8 bytes, got {}", bytes.len()))); + return Err(SerError::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")); @@ -869,7 +886,10 @@ impl Deserializable for SlpTransaction { 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()))); + return Err(SerError::Custom(format!( + "Unexpected token id length {}", + maybe_id.len() + ))); } let maybe_push_op_code: u8 = reader.read()?; @@ -882,7 +902,7 @@ impl Deserializable for SlpTransaction { let bytes: Vec = reader.read_list()?; if bytes.len() != 8 { - return Err(Error::Custom(format!("Expected 8 bytes, got {}", bytes.len()))); + return Err(SerError::Custom(format!("Expected 8 bytes, got {}", bytes.len()))); } let additional_token_quantity = u64::from_be_bytes(bytes.try_into().expect("length is 8 bytes")); @@ -895,7 +915,10 @@ impl Deserializable for SlpTransaction { 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()))); + return Err(SerError::Custom(format!( + "Unexpected token id length {}", + maybe_id.len() + ))); } let token_id = H256::from(maybe_id.as_slice()); @@ -903,21 +926,21 @@ impl Deserializable for SlpTransaction { while !reader.is_finished() { let bytes: Vec = reader.read_list()?; if bytes.len() != 8 { - return Err(Error::Custom(format!("Expected 8 bytes, got {}", bytes.len()))); + return Err(SerError::Custom(format!("Expected 8 bytes, got {}", bytes.len()))); } let amount = u64::from_be_bytes(bytes.try_into().expect("length is 8 bytes")); amounts.push(amount) } if amounts.len() > 19 { - return Err(Error::Custom(format!( + return Err(SerError::Custom(format!( "Expected at most 19 token amounts, got {}", amounts.len() ))); } Ok(SlpTransaction::Send { token_id, amounts }) }, - _ => Err(Error::Custom(format!( + _ => Err(SerError::Custom(format!( "Unsupported transaction type {}", transaction_type ))), @@ -940,11 +963,15 @@ pub enum ParseSlpScriptError { #[display(fmt = "UnexpectedTokenType: {:?}", _0)] UnexpectedTokenType(Vec), #[display(fmt = "DeserializeFailed: {:?}", _0)] - DeserializeFailed(Error), + DeserializeFailed(SerError), +} + +impl From for ParseSlpScriptError { + fn from(err: SerError) -> ParseSlpScriptError { ParseSlpScriptError::DeserializeFailed(err) } } -impl From for ParseSlpScriptError { - fn from(err: Error) -> ParseSlpScriptError { ParseSlpScriptError::DeserializeFailed(err) } +impl From for ValidatePaymentError { + fn from(err: ParseSlpScriptError) -> Self { Self::TxDeserializationError(err.to_string()) } } pub fn parse_slp_script(script: &[u8]) -> Result> { @@ -1053,10 +1080,13 @@ impl UtxoTxGenerationOps for SlpToken { impl MarketCoinOps for SlpToken { fn ticker(&self) -> &str { &self.conf.ticker } - 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 my_address(&self) -> MmResult { + let my_address = self.as_ref().derivation_method.single_addr_or_err()?; + let slp_address = self + .platform_coin + .slp_address(my_address) + .map_to_mm(MyAddressError::InternalError)?; + slp_address.encode().map_to_mm(MyAddressError::InternalError) } fn get_public_key(&self) -> Result> { @@ -1138,12 +1168,14 @@ impl MarketCoinOps for SlpToken { .wait_for_confirmations(tx, confirmations, requires_nota, wait_until, check_every) } - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, transaction: &[u8], + _secret_hash: &[u8], wait_until: u64, from_block: u64, _swap_contract_address: &Option, + check_every: f64, ) -> TransactionFut { utxo_common::wait_for_output_spend( self.platform_coin.as_ref(), @@ -1151,10 +1183,11 @@ impl MarketCoinOps for SlpToken { SLP_SWAP_VOUT, from_block, wait_until, + check_every, ) } - fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result { + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { self.platform_coin.tx_enum_from_bytes(bytes) } @@ -1191,19 +1224,12 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat().map(|tx| tx.into())) } - 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 { - 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); + fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { + let taker_pub = try_tx_fus!(Public::from_slice(maker_payment_args.other_pubkey)); + let amount = try_tx_fus!(sat_from_big_decimal(&maker_payment_args.amount, self.decimals())); + let secret_hash = maker_payment_args.secret_hash.to_owned(); + let maker_htlc_keypair = self.derive_htlc_key_pair(maker_payment_args.swap_unique_data); + let time_lock = maker_payment_args.time_lock; let coin = self.clone(); let fut = async move { @@ -1216,20 +1242,13 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - 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 { - 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(); + fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { + let maker_pub = try_tx_fus!(Public::from_slice(taker_payment_args.other_pubkey)); + let amount = try_tx_fus!(sat_from_big_decimal(&taker_payment_args.amount, self.decimals())); + let secret_hash = taker_payment_args.secret_hash.to_owned(); - let taker_htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); + let taker_htlc_keypair = self.derive_htlc_key_pair(taker_payment_args.swap_unique_data); + let time_lock = taker_payment_args.time_lock; let coin = self.clone(); let fut = async move { @@ -1244,22 +1263,19 @@ impl SwapOps for SlpToken { 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], + maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, ) -> TransactionFut { - let tx = taker_payment_tx.to_owned(); - 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 tx = maker_spends_payment_args.other_payment_tx.to_owned(); + let taker_pub = try_tx_fus!(Public::from_slice(maker_spends_payment_args.other_pubkey)); + let secret = maker_spends_payment_args.secret.to_owned(); + let secret_hash = maker_spends_payment_args.secret_hash.to_owned(); + let htlc_keypair = self.derive_htlc_key_pair(maker_spends_payment_args.swap_unique_data); let coin = self.clone(); + let time_lock = maker_spends_payment_args.time_lock; let fut = async move { let tx = try_tx_s!( - coin.spend_htlc(&tx, &taker_pub, time_lock, &secret, &htlc_keypair) + coin.spend_htlc(&tx, &taker_pub, time_lock, &secret, &secret_hash, &htlc_keypair) .await ); Ok(tx.into()) @@ -1269,22 +1285,19 @@ impl SwapOps for SlpToken { 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], + taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, ) -> TransactionFut { - let tx = maker_payment_tx.to_owned(); - 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 tx = taker_spends_payment_args.other_payment_tx.to_owned(); + let maker_pub = try_tx_fus!(Public::from_slice(taker_spends_payment_args.other_pubkey)); + let secret = taker_spends_payment_args.secret.to_owned(); + let secret_hash = taker_spends_payment_args.secret_hash.to_owned(); + let htlc_keypair = self.derive_htlc_key_pair(taker_spends_payment_args.swap_unique_data); let coin = self.clone(); + let time_lock = taker_spends_payment_args.time_lock; let fut = async move { let tx = try_tx_s!( - coin.spend_htlc(&tx, &maker_pub, time_lock, &secret, &htlc_keypair) + coin.spend_htlc(&tx, &maker_pub, time_lock, &secret, &secret_hash, &htlc_keypair) .await ); Ok(tx.into()) @@ -1292,20 +1305,13 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - 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 { - let tx = taker_payment_tx.to_owned(); - 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); + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + let tx = taker_refunds_payment_args.payment_tx.to_owned(); + let maker_pub = try_tx_fus!(Public::from_slice(taker_refunds_payment_args.other_pubkey)); + let secret_hash = taker_refunds_payment_args.secret_hash.to_owned(); + let htlc_keypair = self.derive_htlc_key_pair(taker_refunds_payment_args.swap_unique_data); let coin = self.clone(); + let time_lock = taker_refunds_payment_args.time_lock; let fut = async move { let tx = try_s!( @@ -1317,20 +1323,13 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat().map_err(TransactionErr::Plain)) } - 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 { - let tx = maker_payment_tx.to_owned(); - 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); + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { + let tx = maker_refunds_payment_args.payment_tx.to_owned(); + let taker_pub = try_tx_fus!(Public::from_slice(maker_refunds_payment_args.other_pubkey)); + let secret_hash = maker_refunds_payment_args.secret_hash.to_owned(); + let htlc_keypair = self.derive_htlc_key_pair(maker_refunds_payment_args.swap_unique_data); let coin = self.clone(); + let time_lock = maker_refunds_payment_args.time_lock; let fut = async move { let tx = try_tx_s!( @@ -1342,23 +1341,16 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - 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 { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + let tx = match validate_fee_args.fee_tx { TransactionEnum::UtxoTx(tx) => tx.clone(), _ => panic!(), }; let coin = self.clone(); - let expected_sender = expected_sender.to_owned(); - let fee_addr = fee_addr.to_owned(); - let amount = amount.to_owned(); + let expected_sender = validate_fee_args.expected_sender.to_owned(); + let fee_addr = validate_fee_args.fee_addr.to_owned(); + let amount = validate_fee_args.amount.to_owned(); + let min_block_number = validate_fee_args.min_block_number; let fut = async move { try_s!( @@ -1370,42 +1362,39 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { let coin = self.clone(); let fut = async move { - try_s!(coin.validate_htlc(input).await); + coin.validate_htlc(input).await?; Ok(()) }; Box::new(fut.boxed().compat()) } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { let coin = self.clone(); let fut = async move { - try_s!(coin.validate_htlc(input).await); + coin.validate_htlc(input).await?; Ok(()) }; Box::new(fut.boxed().compat()) } + #[inline] 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], + if_my_payment_sent_args: CheckIfMyPaymentSentArgs, ) -> Box, Error = String> + Send> { utxo_common::check_if_my_payment_sent( self.platform_coin.clone(), - time_lock, - other_pub, - secret_hash, - swap_unique_data, + if_my_payment_sent_args.time_lock, + if_my_payment_sent_args.other_pub, + if_my_payment_sent_args.secret_hash, + if_my_payment_sent_args.swap_unique_data, ) } + #[inline] async fn search_for_swap_tx_spend_my( &self, input: SearchForSwapTxSpendInput<'_>, @@ -1413,6 +1402,7 @@ impl SwapOps for SlpToken { utxo_common::search_for_swap_tx_spend_my(&self.platform_coin, input, SLP_SWAP_VOUT).await } + #[inline] async fn search_for_swap_tx_spend_other( &self, input: SearchForSwapTxSpendInput<'_>, @@ -1420,10 +1410,24 @@ impl SwapOps for SlpToken { 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> { + fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + + #[inline] + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } + + #[inline] fn negotiate_swap_contract_addr( &self, _other_side_address: Option<&[u8]>, @@ -1434,6 +1438,119 @@ impl SwapOps for SlpToken { 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) } + + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + utxo_common::derive_htlc_pubkey(self, swap_unique_data) + } + + #[inline] + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + utxo_common::validate_other_pubkey(raw_pubkey) + } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } +} + +#[async_trait] +impl TakerSwapMakerCoin for SlpToken { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for SlpToken { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for SlpToken { + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &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_taker_payment_refund_preimage( + &self, + _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + async fn watcher_search_for_swap_tx_spend( + &self, + _input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } } impl From for TradePreimageError { @@ -1471,6 +1588,8 @@ impl From for TxFeeDetails { impl MmCoin for SlpToken { fn is_asset_chain(&self) -> bool { false } + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.conf.abortable_system) } + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new( utxo_common::get_raw_transaction(self.platform_coin.as_ref(), req) @@ -1479,10 +1598,18 @@ impl MmCoin for SlpToken { ) } + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { + Box::new( + utxo_common::get_tx_hex_by_hash(self.platform_coin.as_ref(), tx_hash) + .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 my_address = coin.platform_coin.as_ref().derivation_method.single_addr_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)?; @@ -1560,7 +1687,7 @@ impl MmCoin for SlpToken { 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 my_address_string = coin.my_address()?; let to_address = address.encode().map_to_mm(WithdrawError::InternalError)?; let total_amount = big_decimal_from_sat_unsigned(amount, coin.decimals()); @@ -1588,6 +1715,7 @@ impl MmCoin for SlpToken { coin: coin.ticker().into(), kmd_rewards: None, transaction_type: Default::default(), + memo: None, }; Ok(details) }; @@ -1674,11 +1802,14 @@ impl MmCoin for SlpToken { }) } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { let coin = self.clone(); let fut = async move { - let htlc_fee = coin.platform_coin.get_htlc_spend_fee(SLP_HTLC_SPEND_SIZE).await?; + let htlc_fee = coin + .platform_coin + .get_htlc_spend_fee(SLP_HTLC_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await?; let amount = (big_decimal_from_sat_unsigned(htlc_fee, coin.platform_decimals()) + coin.platform_dust_dec()).into(); Ok(TradeFee { @@ -1734,20 +1865,37 @@ impl MmCoin for SlpToken { warn!("set_requires_notarization has no effect on SLPTOKEN!") } - fn swap_contract_address(&self) -> Option { None } + fn swap_contract_address(&self) -> Option { utxo_common::fallback_swap_contract() } + + fn fallback_swap_contract(&self) -> Option { utxo_common::fallback_swap_contract() } 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 on_disabled(&self) -> Result<(), AbortedError> { self.conf.abortable_system.abort_all() } + + fn on_token_deactivated(&self, _ticker: &str) {} } +#[async_trait] impl CoinWithTxHistoryV2 for SlpToken { fn history_wallet_id(&self) -> WalletId { WalletId::new(self.platform_ticker().to_owned()) } - fn get_tx_history_filters(&self) -> GetTxHistoryFilters { - GetTxHistoryFilters::new().with_token_id(self.token_id().to_string()) + /// TODO consider using `utxo_common::utxo_tx_history_common::get_tx_history_filters` + /// when `SlpToken` implements `CoinWithDerivationMethod`. + async fn get_tx_history_filters( + &self, + target: MyTxHistoryTarget, + ) -> MmResult { + match target { + MyTxHistoryTarget::Iguana => (), + target => return MmError::err(MyTxHistoryErrorV2::with_expected_target(target, "Iguana")), + } + let my_address = self.my_address()?; + Ok(GetTxHistoryFilters::for_address(my_address).with_token_id(self.token_id().to_string())) } } @@ -1931,9 +2079,9 @@ mod slp_tests { #[test] fn test_slp_address() { - let bch = tbch_coin_for_test(); + let (_ctx, 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, 0).unwrap(); let slp_address = fusd.my_address().unwrap(); assert_eq!("slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8", slp_address); @@ -1941,9 +2089,9 @@ mod slp_tests { #[test] fn test_validate_htlc_valid() { - let bch = tbch_coin_for_test(); + let (_ctx, 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, 0).unwrap(); // https://testnet.simpleledger.info/tx/e935160bfb5b45007a0fc6f8fbe8da618f28df6573731f1ffb54d9560abb49b2 let payment_tx = hex::decode("0100000002736cf584f877ec7b6b95974bc461a9cfb9f126655b5d335471683154cc6cf4c5020000006a47304402206be99fe56a98e7a8c2ffe6f2d05c5c1f46a6577064b84d27d45fe0e959f6e77402201c512629313b48cd4df873222aa49046ae9a3a6e34e359d10d4308cb40438fba4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff736cf584f877ec7b6b95974bc461a9cfb9f126655b5d335471683154cc6cf4c5030000006a473044022020d774d045bbe3dce5b04af836f6a5629c6c4ce75b0b5ba8a1da0ae9a4ecc0530220522f86d20c9e4142e40f9a9c8d25db16fde91d4a0ad6f6ff2107e201386131b64121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8080000000000001f3ee80300000000000017a914b0ca1fea17cf522c7e858416093fc6d95e55824087e8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88accf614801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8c83d460").unwrap(); @@ -1963,6 +2111,7 @@ mod slp_tests { let input = ValidatePaymentInput { payment_tx, other_pub, + time_lock_duration: 0, time_lock: lock_time, secret_hash, amount, @@ -1976,11 +2125,11 @@ mod slp_tests { #[test] fn construct_and_send_invalid_slp_htlc_should_fail() { - let bch = tbch_coin_for_test(); + let (_ctx, bch) = tbch_coin_for_test(); let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); - let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch.clone(), 0); + let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch.clone(), 0).unwrap(); - let bch_address = bch.as_ref().derivation_method.unwrap_iguana(); + let bch_address = bch.as_ref().derivation_method.unwrap_single_addr(); let (unspents, recently_spent) = block_on(bch.get_unspent_ordered_list(bch_address)).unwrap(); let secret_hash = hex::decode("5d9e149ad9ccb20e9f931a69b605df2ffde60242").unwrap(); @@ -2061,9 +2210,9 @@ mod slp_tests { #[test] fn test_validate_htlc_invalid_slp_utxo() { - let bch = tbch_coin_for_test(); + let (_ctx, bch) = tbch_coin_for_test(); let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); - let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch.clone(), 0); + let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch.clone(), 0).unwrap(); // https://www.blockchain.com/ru/bch-testnet/tx/6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69 let payment_tx = hex::decode("0100000001ce59a734f33811afcc00c19dcb12202ed00067a50efed80424fabd2b723678c0020000006b483045022100ec1fecff9c60fb7e821b9a412bd8c4ce4a757c68287f9cf9e0f461165492d6530220222f020dd05d65ba35cddd0116c99255612ec90d63019bb1cea45e2cf09a62a94121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff030000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e8e80300000000000017a914953b3909ff6aa269f85da34c132a92424440e18e879decad00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88acd1215c61").unwrap(); @@ -2096,6 +2245,7 @@ mod slp_tests { let input = ValidatePaymentInput { payment_tx, other_pub: other_pub_bytes, + time_lock_duration: 0, time_lock: lock_time, secret_hash, amount, @@ -2106,16 +2256,16 @@ mod slp_tests { }; 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), + ValidatePaymentError::WrongPaymentTx(e) => println!("{:#?}", e), + err @ _ => panic!("Unexpected err {:#?}", err), }; } #[test] fn test_sign_message() { - let bch = tbch_coin_for_test(); + let (_ctx, 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, 0).unwrap(); let signature = fusd.sign_message("test").unwrap(); assert_eq!( signature, @@ -2126,9 +2276,9 @@ mod slp_tests { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_verify_message() { - let bch = tbch_coin_for_test(); + let (_ctx, 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, 0).unwrap(); let is_valid = fusd .verify_message( "ILuePKMsycXwJiNDOT7Zb7TfIlUW7Iq+5ylKd15AK72vGVYXbnf7Gj9Lk9MFV+6Ub955j7MiAkp0wQjvuIoRPPA=", diff --git a/mm2src/coins/utxo/spv.rs b/mm2src/coins/utxo/spv.rs new file mode 100644 index 0000000000..8abb2da216 --- /dev/null +++ b/mm2src/coins/utxo/spv.rs @@ -0,0 +1,66 @@ +use crate::utxo::rpc_clients::{ConfirmedTransactionInfo, ElectrumClient}; +use async_trait::async_trait; +use chain::Transaction as UtxoTx; +use common::log::error; +use keys::hash::H256; +use serialization::serialize_list; +use spv_validation::helpers_validation::SPVError; +use spv_validation::spv_proof::{SPVProof, TRY_SPV_PROOF_INTERVAL}; + +#[async_trait] +pub trait SimplePaymentVerification { + async fn validate_spv_proof( + &self, + tx: &UtxoTx, + try_spv_proof_until: u64, + ) -> Result; +} + +#[async_trait] +impl SimplePaymentVerification for ElectrumClient { + async fn validate_spv_proof( + &self, + tx: &UtxoTx, + try_spv_proof_until: u64, + ) -> Result { + if tx.outputs.is_empty() { + return Err(SPVError::InvalidVout); + } + + let tx_hash = tx.hash().reversed(); + let (merkle_branch, validated_header, height) = + retry_on_err!(async { self.get_merkle_and_validated_header(tx).await }) + .repeat_every_secs(TRY_SPV_PROOF_INTERVAL as f64) + .with_timeout_secs(try_spv_proof_until as f64) + .inspect_err(move |e| { + error!( + "Failed spv proof validation for transaction {tx_hash} with error: {e:?}, retrying in {TRY_SPV_PROOF_INTERVAL} seconds.", + ) + }) + .await + .map_err(|_| SPVError::Timeout)?; + + 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, + intermediate_nodes, + }; + + proof.validate(&validated_header)?; + + Ok(ConfirmedTransactionInfo { + tx: tx.clone(), + header: validated_header, + index: proof.index, + height, + }) + } +} diff --git a/mm2src/coins/utxo/utxo_block_header_storage.rs b/mm2src/coins/utxo/utxo_block_header_storage.rs deleted file mode 100644 index 925a0c80c3..0000000000 --- a/mm2src/coins/utxo/utxo_block_header_storage.rs +++ /dev/null @@ -1,151 +0,0 @@ -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_block_header_storage/mod.rs b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs new file mode 100644 index 0000000000..1932d9afda --- /dev/null +++ b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs @@ -0,0 +1,361 @@ +#[cfg(not(target_arch = "wasm32"))] mod sql_block_header_storage; +#[cfg(not(target_arch = "wasm32"))] +pub use sql_block_header_storage::SqliteBlockHeadersStorage; + +#[cfg(target_arch = "wasm32")] mod wasm; +#[cfg(target_arch = "wasm32")] +pub use wasm::IDBBlockHeadersStorage; + +use async_trait::async_trait; +use chain::BlockHeader; +use mm2_core::mm_ctx::MmArc; +#[cfg(all(test, not(target_arch = "wasm32")))] +use mocktopus::macros::*; +use primitives::hash::H256; +use spv_validation::storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; + +pub struct BlockHeaderStorage { + pub inner: Box, +} + +impl Debug for BlockHeaderStorage { + fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result { Ok(()) } +} + +impl BlockHeaderStorage { + #[cfg(all(not(test), not(target_arch = "wasm32")))] + pub(crate) fn new_from_ctx(ctx: MmArc, ticker: String) -> Result { + let sqlite_connection = ctx.sqlite_connection.ok_or(BlockHeaderStorageError::Internal( + "sqlite_connection is not initialized".to_owned(), + ))?; + Ok(BlockHeaderStorage { + inner: Box::new(SqliteBlockHeadersStorage { + ticker, + conn: sqlite_connection.clone(), + }), + }) + } + + #[cfg(target_arch = "wasm32")] + pub(crate) fn new_from_ctx(ctx: MmArc, ticker: String) -> Result { + Ok(BlockHeaderStorage { + inner: Box::new(IDBBlockHeadersStorage::new(&ctx, ticker)), + }) + } + + #[cfg(all(test, not(target_arch = "wasm32")))] + pub(crate) fn new_from_ctx(ctx: MmArc, ticker: String) -> Result { + use db_common::sqlite::rusqlite::Connection; + use std::sync::{Arc, Mutex}; + + let conn = Arc::new(Mutex::new(Connection::open_in_memory().unwrap())); + let conn = ctx.sqlite_connection.clone_or(conn); + + Ok(BlockHeaderStorage { + inner: Box::new(SqliteBlockHeadersStorage { ticker, conn }), + }) + } + + #[cfg(test)] + pub(crate) fn into_inner(self) -> Box { self.inner } +} + +#[async_trait] +#[cfg_attr(all(test, not(target_arch = "wasm32")), mockable)] +impl BlockHeaderStorageOps for BlockHeaderStorage { + async fn init(&self) -> Result<(), BlockHeaderStorageError> { self.inner.init().await } + + async fn is_initialized_for(&self) -> Result { + self.inner.is_initialized_for().await + } + + async fn add_block_headers_to_storage( + &self, + headers: HashMap, + ) -> Result<(), BlockHeaderStorageError> { + self.inner.add_block_headers_to_storage(headers).await + } + + async fn get_block_header(&self, height: u64) -> Result, BlockHeaderStorageError> { + self.inner.get_block_header(height).await + } + + async fn get_block_header_raw(&self, height: u64) -> Result, BlockHeaderStorageError> { + self.inner.get_block_header_raw(height).await + } + + async fn get_last_block_height(&self) -> Result, BlockHeaderStorageError> { + self.inner.get_last_block_height().await + } + + async fn get_last_block_header_with_non_max_bits( + &self, + max_bits: u32, + ) -> Result, BlockHeaderStorageError> { + self.inner.get_last_block_header_with_non_max_bits(max_bits).await + } + + async fn get_block_height_by_hash(&self, hash: H256) -> Result, BlockHeaderStorageError> { + self.inner.get_block_height_by_hash(hash).await + } + + async fn remove_headers_up_to_height(&self, to_height: u64) -> Result<(), BlockHeaderStorageError> { + self.inner.remove_headers_up_to_height(to_height).await + } + + async fn is_table_empty(&self) -> Result<(), BlockHeaderStorageError> { self.inner.is_table_empty().await } +} + +#[cfg(test)] +mod block_headers_storage_tests { + use super::*; + use chain::BlockHeaderBits; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + + cfg_wasm32! { + use wasm_bindgen_test::*; + use spv_validation::work::MAX_BITS_BTC; + + wasm_bindgen_test_configure!(run_in_browser); + } + + cfg_native! { + use spv_validation::work::MAX_BITS_BTC; + + } + + pub(crate) async fn test_add_block_headers_impl(for_coin: &str) { + let ctx = mm_ctx_with_custom_db(); + let storage = BlockHeaderStorage::new_from_ctx(ctx, for_coin.to_string()) + .unwrap() + .into_inner(); + storage.init().await.unwrap(); + + let mut headers = HashMap::with_capacity(1); + let block_header: BlockHeader = "0000002076d41d3e4b0bfd4c0d3b30aa69fdff3ed35d85829efd04000000000000000000b386498b583390959d9bac72346986e3015e83ac0b54bc7747a11a494ac35c94bb3ce65a53fb45177f7e311c".into(); + headers.insert(520481, block_header); + storage.add_block_headers_to_storage(headers).await.unwrap(); + assert!(!storage.is_table_empty().await.is_ok()); + } + + pub(crate) async fn test_get_block_header_impl(for_coin: &str) { + let ctx = mm_ctx_with_custom_db(); + let storage = BlockHeaderStorage::new_from_ctx(ctx, for_coin.to_string()) + .unwrap() + .into_inner(); + storage.init().await.unwrap(); + + let mut headers = HashMap::with_capacity(1); + let block_header: BlockHeader = "0000002076d41d3e4b0bfd4c0d3b30aa69fdff3ed35d85829efd04000000000000000000b386498b583390959d9bac72346986e3015e83ac0b54bc7747a11a494ac35c94bb3ce65a53fb45177f7e311c".into(); + headers.insert(520481, block_header); + + storage.add_block_headers_to_storage(headers).await.unwrap(); + assert!(!storage.is_table_empty().await.is_ok()); + + let hex = storage.get_block_header_raw(520481).await.unwrap().unwrap(); + assert_eq!(hex, "0000002076d41d3e4b0bfd4c0d3b30aa69fdff3ed35d85829efd04000000000000000000b386498b583390959d9bac72346986e3015e83ac0b54bc7747a11a494ac35c94bb3ce65a53fb45177f7e311c".to_string()); + + let block_header = storage.get_block_header(520481).await.unwrap().unwrap(); + let block_hash: H256 = "0000000000000000002e31d0714a5ab23100945ff87ba2d856cd566a3c9344ec".into(); + assert_eq!(block_header.hash(), block_hash.reversed()); + + let height = storage.get_block_height_by_hash(block_hash).await.unwrap().unwrap(); + assert_eq!(height, 520481); + } + + pub(crate) async fn test_get_last_block_header_with_non_max_bits_impl(for_coin: &str) { + let ctx = mm_ctx_with_custom_db(); + let storage = BlockHeaderStorage::new_from_ctx(ctx, for_coin.to_string()) + .unwrap() + .into_inner(); + storage.init().await.unwrap(); + + let mut headers = HashMap::with_capacity(2); + + // This block has max difficulty + // https://live.blockcypher.com/btc-testnet/block/00000000961a9d117feb57e516e17217207a849bf6cdfce529f31d9a96053530/ + let block_header: BlockHeader = "02000000ea01a61a2d7420a1b23875e40eb5eb4ca18b378902c8e6384514ad0000000000c0c5a1ae80582b3fe319d8543307fa67befc2a734b8eddb84b1780dfdf11fa2b20e71353ffff001d00805fe0".into(); + headers.insert(201595, block_header); + + // https://live.blockcypher.com/btc-testnet/block/0000000000ad144538e6c80289378ba14cebb50ee47538b2a120742d1aa601ea/ + let expected_block_header: BlockHeader = "02000000cbed7fd98f1f06e85c47e13ff956533642056be45e7e6b532d4d768f00000000f2680982f333fcc9afa7f9a5e2a84dc54b7fe10605cd187362980b3aa882e9683be21353ab80011c813e1fc0".into(); + headers.insert(201594, expected_block_header.clone()); + + // This block has max difficulty + // https://live.blockcypher.com/btc-testnet/block/0000000000ad144538e6c80289378ba14cebb50ee47538b2a120742d1aa601ea/ + let block_header: BlockHeader = "020000001f38c8e30b30af912fbd4c3e781506713cfb43e73dff6250348e060000000000afa8f3eede276ccb4c4ee649ad9823fc181632f262848ca330733e7e7e541beb9be51353ffff001d00a63037".into(); + headers.insert(201593, block_header); + + storage.add_block_headers_to_storage(headers).await.unwrap(); + assert!(!storage.is_table_empty().await.is_ok()); + + let actual_block_header = storage + .get_last_block_header_with_non_max_bits(MAX_BITS_BTC) + .await + .unwrap() + .unwrap(); + assert_ne!(actual_block_header.bits, BlockHeaderBits::Compact(MAX_BITS_BTC.into())); + assert_eq!(actual_block_header, expected_block_header); + } + + pub(crate) async fn test_get_last_block_height_impl(for_coin: &str) { + let ctx = mm_ctx_with_custom_db(); + let storage = BlockHeaderStorage::new_from_ctx(ctx, for_coin.to_string()) + .unwrap() + .into_inner(); + storage.init().await.unwrap(); + + let mut headers = HashMap::with_capacity(2); + + // https://live.blockcypher.com/btc-testnet/block/00000000961a9d117feb57e516e17217207a849bf6cdfce529f31d9a96053530/ + let block_header: BlockHeader = "02000000ea01a61a2d7420a1b23875e40eb5eb4ca18b378902c8e6384514ad0000000000c0c5a1ae80582b3fe319d8543307fa67befc2a734b8eddb84b1780dfdf11fa2b20e71353ffff001d00805fe0".into(); + headers.insert(201595, block_header); + + // https://live.blockcypher.com/btc-testnet/block/0000000000ad144538e6c80289378ba14cebb50ee47538b2a120742d1aa601ea/ + let block_header: BlockHeader = "02000000cbed7fd98f1f06e85c47e13ff956533642056be45e7e6b532d4d768f00000000f2680982f333fcc9afa7f9a5e2a84dc54b7fe10605cd187362980b3aa882e9683be21353ab80011c813e1fc0".into(); + headers.insert(201594, block_header); + + // https://live.blockcypher.com/btc-testnet/block/0000000000ad144538e6c80289378ba14cebb50ee47538b2a120742d1aa601ea/ + let block_header: BlockHeader = "020000001f38c8e30b30af912fbd4c3e781506713cfb43e73dff6250348e060000000000afa8f3eede276ccb4c4ee649ad9823fc181632f262848ca330733e7e7e541beb9be51353ffff001d00a63037".into(); + headers.insert(201593, block_header); + + storage.add_block_headers_to_storage(headers).await.unwrap(); + assert!(!storage.is_table_empty().await.is_ok()); + + let last_block_height = storage.get_last_block_height().await.unwrap(); + assert_eq!(last_block_height.unwrap(), 201595); + } + + pub(crate) async fn test_remove_headers_up_to_height_impl(for_coin: &str) { + let ctx = mm_ctx_with_custom_db(); + let storage = BlockHeaderStorage::new_from_ctx(ctx, for_coin.to_string()) + .unwrap() + .into_inner(); + storage.init().await.unwrap(); + + let mut headers = HashMap::with_capacity(2); + + // https://live.blockcypher.com/btc-testnet/block/00000000961a9d117feb57e516e17217207a849bf6cdfce529f31d9a96053530/ + let block_header: BlockHeader = "02000000ea01a61a2d7420a1b23875e40eb5eb4ca18b378902c8e6384514ad0000000000c0c5a1ae80582b3fe319d8543307fa67befc2a734b8eddb84b1780dfdf11fa2b20e71353ffff001d00805fe0".into(); + headers.insert(201595, block_header); + + // https://live.blockcypher.com/btc-testnet/block/0000000000ad144538e6c80289378ba14cebb50ee47538b2a120742d1aa601ea/ + let block_header: BlockHeader = "02000000cbed7fd98f1f06e85c47e13ff956533642056be45e7e6b532d4d768f00000000f2680982f333fcc9afa7f9a5e2a84dc54b7fe10605cd187362980b3aa882e9683be21353ab80011c813e1fc0".into(); + headers.insert(201594, block_header); + + // https://live.blockcypher.com/btc-testnet/block/0000000000ad144538e6c80289378ba14cebb50ee47538b2a120742d1aa601ea/ + let block_header: BlockHeader = "020000001f38c8e30b30af912fbd4c3e781506713cfb43e73dff6250348e060000000000afa8f3eede276ccb4c4ee649ad9823fc181632f262848ca330733e7e7e541beb9be51353ffff001d00a63037".into(); + headers.insert(201593, block_header); + + storage.add_block_headers_to_storage(headers).await.unwrap(); + assert!(!storage.is_table_empty().await.is_ok()); + + // Remove 2 headers from storage. + storage.remove_headers_up_to_height(201594).await.unwrap(); + + // Validate that blockers 201593..201594 are removed from storage. + for h in 201593..201594 { + let block_header = storage.get_block_header(h).await.unwrap(); + assert!(block_header.is_none()); + } + + // Last height should be 201595 + let last_block_height = storage.get_last_block_height().await.unwrap(); + assert_eq!(last_block_height.unwrap(), 201595); + } +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod native_tests { + use super::*; + use crate::utxo::utxo_block_header_storage::block_headers_storage_tests::*; + use common::block_on; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + + #[test] + fn test_init_collection() { + let for_coin = "init_collection"; + let ctx = mm_ctx_with_custom_db(); + let storage = BlockHeaderStorage::new_from_ctx(ctx, for_coin.to_string()) + .unwrap() + .into_inner(); + + let initialized = block_on(storage.is_initialized_for()).unwrap(); + assert!(!initialized); + + block_on(storage.init()).unwrap(); + // repetitive init must not fail + block_on(storage.init()).unwrap(); + + let initialized = block_on(storage.is_initialized_for()).unwrap(); + assert!(initialized); + } + + const FOR_COIN_GET: &str = "get"; + const FOR_COIN_INSERT: &str = "insert"; + #[test] + fn test_add_block_headers() { block_on(test_add_block_headers_impl(FOR_COIN_INSERT)) } + + #[test] + fn test_test_get_block_header() { block_on(test_get_block_header_impl(FOR_COIN_GET)) } + + #[test] + fn test_get_last_block_header_with_non_max_bits() { + block_on(test_get_last_block_header_with_non_max_bits_impl(FOR_COIN_GET)) + } + + #[test] + fn test_get_last_block_height() { block_on(test_get_last_block_height_impl(FOR_COIN_GET)) } + + #[test] + fn test_remove_headers_up_to_height() { block_on(test_remove_headers_up_to_height_impl(FOR_COIN_GET)) } +} + +#[cfg(all(test, target_arch = "wasm32"))] +mod wasm_test { + use super::*; + use crate::utxo::utxo_block_header_storage::block_headers_storage_tests::*; + use common::log::wasm_log::register_wasm_log; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + + wasm_bindgen_test_configure!(run_in_browser); + + const FOR_COIN: &str = "RICK"; + + #[wasm_bindgen_test] + async fn test_storage_init() { + let ctx = mm_ctx_with_custom_db(); + let storage = IDBBlockHeadersStorage::new(&ctx, "RICK".to_string()); + + register_wasm_log(); + + let initialized = storage.is_initialized_for().await.unwrap(); + assert!(initialized); + + // repetitive init must not fail + storage.init().await.unwrap(); + + let initialized = storage.is_initialized_for().await.unwrap(); + assert!(initialized); + } + + #[wasm_bindgen_test] + async fn test_add_block_headers() { test_add_block_headers_impl(FOR_COIN).await } + + #[wasm_bindgen_test] + async fn test_test_get_block_header() { test_get_block_header_impl(FOR_COIN).await } + + #[wasm_bindgen_test] + async fn test_get_last_block_header_with_non_max_bits() { + test_get_last_block_header_with_non_max_bits_impl(FOR_COIN).await + } + + #[wasm_bindgen_test] + async fn test_get_last_block_height() { test_get_last_block_height_impl(FOR_COIN).await } + + #[wasm_bindgen_test] + async fn test_remove_headers_up_to_height() { test_remove_headers_up_to_height_impl(FOR_COIN).await } +} diff --git a/mm2src/coins/utxo/utxo_block_header_storage/sql_block_header_storage.rs b/mm2src/coins/utxo/utxo_block_header_storage/sql_block_header_storage.rs new file mode 100644 index 0000000000..7ae019a57d --- /dev/null +++ b/mm2src/coins/utxo/utxo_block_header_storage/sql_block_header_storage.rs @@ -0,0 +1,348 @@ +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 primitives::hash::H256; +use serialization::Reader; +use spv_validation::storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; +use std::collections::HashMap; +use std::convert::TryInto; +use std::num::TryFromIntError; +use std::sync::{Arc, Mutex}; + +pub(crate) 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 { + coin: 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, + block_bits INTEGER NOT NULL, + block_hash VARCHAR(255) NOT NULL UNIQUE + );", + 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, block_bits, block_hash) VALUES (?1, ?2, ?3, ?4);", + 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) +} + +fn get_last_block_height_sql(for_coin: &str) -> Result { + let table_name = get_table_name_and_validate(for_coin)?; + let sql = format!( + "SELECT block_height FROM {} ORDER BY block_height DESC LIMIT 1;", + table_name + ); + + Ok(sql) +} + +fn get_last_block_header_with_non_max_bits_sql( + for_coin: &str, + max_bits: u32, +) -> Result { + let table_name = get_table_name_and_validate(for_coin)?; + let sql = format!( + "SELECT hex FROM {} WHERE block_bits<>{} ORDER BY block_height DESC LIMIT 1;", + table_name, max_bits + ); + + Ok(sql) +} + +fn get_block_height_by_hash(for_coin: &str) -> Result { + let table_name = get_table_name_and_validate(for_coin)?; + let sql = format!("SELECT block_height FROM {} WHERE block_hash=?1;", table_name); + + Ok(sql) +} + +fn remove_headers_up_to_height_sql(for_coin: &str, to_height: u64) -> Result { + let table_name = get_table_name_and_validate(for_coin)?; + let sql = format!("DELETE FROM {table_name} WHERE block_height <= {to_height};"); + + Ok(sql) +} + +#[derive(Clone, Debug)] +pub struct SqliteBlockHeadersStorage { + pub ticker: String, + pub conn: Arc>, +} + +fn query_single_row( + conn: &Connection, + query: &str, + params: P, + map_fn: F, +) -> Result, BlockHeaderStorageError> +where + P: IntoIterator, + P::Item: ToSql, + F: FnOnce(&Row<'_>) -> Result, +{ + db_common::sqlite::query_single_row(conn, query, params, map_fn).map_err(|e| BlockHeaderStorageError::QueryError { + query: query.to_string(), + reason: e.to_string(), + }) +} + +#[async_trait] +impl BlockHeaderStorageOps for SqliteBlockHeadersStorage { + async fn init(&self) -> Result<(), BlockHeaderStorageError> { + let coin = self.ticker.clone(); + let selfi = self.clone(); + let sql_cache = create_block_header_cache_table_sql(&coin)?; + async_blocking(move || { + let conn = selfi.conn.lock().unwrap(); + conn.execute(&sql_cache, NO_PARAMS).map(|_| ()).map_err(|e| { + BlockHeaderStorageError::InitializationError { + coin, + reason: e.to_string(), + } + })?; + Ok(()) + }) + .await + } + + async fn is_initialized_for(&self) -> Result { + let coin = self.ticker.clone(); + let block_headers_cache_table = get_table_name_and_validate(&coin)?; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.conn.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_block_headers_to_storage( + &self, + headers: HashMap, + ) -> Result<(), BlockHeaderStorageError> { + let coin = self.ticker.clone(); + let selfi = self.clone(); + async_blocking(move || { + let mut conn = selfi.conn.lock().unwrap(); + let sql_transaction = conn + .transaction() + .map_err(|e| BlockHeaderStorageError::AddToStorageError { + coin: coin.clone(), + reason: e.to_string(), + })?; + + for (height, header) in headers { + let height = height as i64; + let hash = header.hash().reversed().to_string(); + let raw_header = hex::encode(header.raw()); + let bits: u32 = header.bits.into(); + let block_cache_params = [ + &height as &dyn ToSql, + &raw_header as &dyn ToSql, + &bits as &dyn ToSql, + &hash as &dyn ToSql, + ]; + sql_transaction + .execute(&insert_block_header_in_cache_sql(&coin.clone())?, block_cache_params) + .map_err(|e| BlockHeaderStorageError::AddToStorageError { + coin: coin.clone(), + reason: e.to_string(), + })?; + } + sql_transaction + .commit() + .map_err(|e| BlockHeaderStorageError::AddToStorageError { + coin: coin.clone(), + reason: e.to_string(), + })?; + Ok(()) + }) + .await + } + + async fn get_block_header(&self, height: u64) -> Result, BlockHeaderStorageError> { + let coin = self.ticker.clone(); + if let Some(header_raw) = self.get_block_header_raw(height).await? { + let serialized = &hex::decode(header_raw).map_err(|e| BlockHeaderStorageError::DecodeError { + coin: coin.clone(), + reason: e.to_string(), + })?; + let mut reader = Reader::new_with_coin_variant(serialized, coin.as_str().into()); + let header: BlockHeader = + reader + .read() + .map_err(|e: serialization::Error| BlockHeaderStorageError::DecodeError { + coin, + reason: e.to_string(), + })?; + return Ok(Some(header)); + } + Ok(None) + } + + async fn get_block_header_raw(&self, height: u64) -> Result, BlockHeaderStorageError> { + let coin = self.ticker.clone(); + let params = [height as i64]; + let sql = get_block_header_by_height(&coin)?; + let selfi = self.clone(); + + async_blocking(move || { + let conn = selfi.conn.lock().unwrap(); + query_single_row(&conn, &sql, params, string_from_row) + }) + .await + .map_err(|e| BlockHeaderStorageError::GetFromStorageError { + coin, + reason: e.to_string(), + }) + } + + async fn get_last_block_height(&self) -> Result, BlockHeaderStorageError> { + let coin = self.ticker.clone(); + let sql = get_last_block_height_sql(&coin)?; + let selfi = self.clone(); + + async_blocking(move || { + let conn = selfi.conn.lock().unwrap(); + query_single_row(&conn, &sql, NO_PARAMS, |row| row.get::<_, i64>(0)) + }) + .await + .map_err(|e| BlockHeaderStorageError::GetFromStorageError { + coin: coin.clone(), + reason: e.to_string(), + })? + .map(|h| h.try_into()) + .transpose() + .map_err(|e: TryFromIntError| BlockHeaderStorageError::DecodeError { + coin, + reason: e.to_string(), + }) + } + + async fn get_last_block_header_with_non_max_bits( + &self, + max_bits: u32, + ) -> Result, BlockHeaderStorageError> { + let coin = self.ticker.clone(); + let sql = get_last_block_header_with_non_max_bits_sql(&coin, max_bits)?; + let selfi = self.clone(); + + let maybe_header_raw = async_blocking(move || { + let conn = selfi.conn.lock().unwrap(); + query_single_row(&conn, &sql, NO_PARAMS, string_from_row) + }) + .await + .map_err(|e| BlockHeaderStorageError::GetFromStorageError { + coin: coin.clone(), + reason: e.to_string(), + })?; + + if let Some(header_raw) = maybe_header_raw { + let header = BlockHeader::try_from_string_with_coin_variant(header_raw, coin.as_str().into()).map_err( + |e: serialization::Error| BlockHeaderStorageError::DecodeError { + coin, + reason: e.to_string(), + }, + )?; + return Ok(Some(header)); + } + Ok(None) + } + + async fn get_block_height_by_hash(&self, hash: H256) -> Result, BlockHeaderStorageError> { + let coin = self.ticker.clone(); + let params = [hash.to_string()]; + let sql = get_block_height_by_hash(&coin)?; + let selfi = self.clone(); + + async_blocking(move || { + let conn = selfi.conn.lock().unwrap(); + query_single_row(&conn, &sql, params, |row| row.get(0)) + }) + .await + .map_err(|e| BlockHeaderStorageError::GetFromStorageError { + coin, + reason: e.to_string(), + }) + } + + async fn remove_headers_up_to_height(&self, to_height: u64) -> Result<(), BlockHeaderStorageError> { + let coin = self.ticker.clone(); + let selfi = self.clone(); + let sql = remove_headers_up_to_height_sql(&coin, to_height)?; + + async_blocking(move || { + let conn = selfi.conn.lock().unwrap(); + conn.execute(&sql, NO_PARAMS) + .map_err(|e| BlockHeaderStorageError::UnableToDeleteHeaders { + coin: coin.clone(), + to_height, + reason: e.to_string(), + })?; + Ok(()) + }) + .await + } + + async fn is_table_empty(&self) -> Result<(), BlockHeaderStorageError> { + let table_name = get_table_name_and_validate(&self.ticker).unwrap(); + let sql = format!("SELECT COUNT(block_height) FROM {table_name};"); + let conn = self.conn.lock().unwrap(); + let rows_count: u32 = conn.query_row(&sql, NO_PARAMS, |row| row.get(0)).unwrap(); + if rows_count == 0 { + return Ok(()); + }; + + Err(BlockHeaderStorageError::table_err( + &self.ticker, + "Table is not empty".to_string(), + )) + } +} + +#[cfg(test)] +impl SqliteBlockHeadersStorage { + pub fn in_memory(ticker: String) -> Self { + SqliteBlockHeadersStorage { + ticker, + conn: Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + } + } +} diff --git a/mm2src/coins/utxo/utxo_block_header_storage/wasm.rs b/mm2src/coins/utxo/utxo_block_header_storage/wasm.rs new file mode 100644 index 0000000000..f6d54b5114 --- /dev/null +++ b/mm2src/coins/utxo/utxo_block_header_storage/wasm.rs @@ -0,0 +1,7 @@ +mod block_header_table; +mod indexeddb_block_header_storage; + +#[cfg(target_arch = "wasm32")] +pub use block_header_table::BlockHeaderStorageTable; +#[cfg(target_arch = "wasm32")] +pub use indexeddb_block_header_storage::IDBBlockHeadersStorage; diff --git a/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs b/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs new file mode 100644 index 0000000000..79121043d6 --- /dev/null +++ b/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs @@ -0,0 +1,32 @@ +use mm2_db::indexed_db::{DbUpgrader, OnUpgradeResult, TableSignature}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BlockHeaderStorageTable { + pub height: u64, + pub bits: u32, + pub hash: String, + pub raw_header: String, + pub ticker: String, +} + +impl BlockHeaderStorageTable { + pub const HEIGHT_TICKER_INDEX: &str = "block_height_ticker_index"; + pub const HASH_TICKER_INDEX: &str = "block_hash_ticker_index"; +} + +impl TableSignature for BlockHeaderStorageTable { + fn table_name() -> &'static str { "block_header_storage_cache_table" } + + 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(Self::HEIGHT_TICKER_INDEX, &["height", "ticker"], true)?; + table.create_multi_index(Self::HASH_TICKER_INDEX, &["hash", "ticker"], true)?; + table.create_index("ticker", false)?; + }, + _ => (), + } + Ok(()) + } +} diff --git a/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs b/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs new file mode 100644 index 0000000000..c5a02dd151 --- /dev/null +++ b/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs @@ -0,0 +1,345 @@ +use super::BlockHeaderStorageTable; + +use async_trait::async_trait; +use chain::BlockHeader; +use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::cursor_prelude::{CollectCursor, WithOnly}; +use mm2_db::indexed_db::{ConstructibleDb, DbIdentifier, DbInstance, DbLocked, IndexedDb, IndexedDbBuilder, + InitDbResult, MultiIndex, SharedDb}; +use mm2_err_handle::prelude::*; +use primitives::hash::H256; +use serialization::Reader; +use spv_validation::storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; +use std::collections::HashMap; + +const DB_NAME: &str = "block_headers_cache"; +const DB_VERSION: u32 = 1; + +pub type IDBBlockHeadersStorageRes = MmResult; +pub type IDBBlockHeadersInnerLocked<'a> = DbLocked<'a, IDBBlockHeadersInner>; + +pub struct IDBBlockHeadersInner { + pub inner: IndexedDb, +} + +#[async_trait] +impl DbInstance for IDBBlockHeadersInner { + 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(Self { inner }) + } +} + +impl IDBBlockHeadersInner { + pub fn get_inner(&self) -> &IndexedDb { &self.inner } +} + +pub struct IDBBlockHeadersStorage { + pub db: SharedDb, + pub ticker: String, +} + +impl IDBBlockHeadersStorage { + pub fn new(ctx: &MmArc, ticker: String) -> Self { + Self { + db: ConstructibleDb::new(ctx).into_shared(), + ticker, + } + } + + async fn lock_db(&self) -> IDBBlockHeadersStorageRes> { + self.db + .get_or_initialize() + .await + .mm_err(|err| BlockHeaderStorageError::init_err(&self.ticker, err.to_string())) + } +} + +#[async_trait] +impl BlockHeaderStorageOps for IDBBlockHeadersStorage { + async fn init(&self) -> Result<(), BlockHeaderStorageError> { Ok(()) } + + async fn is_initialized_for(&self) -> Result { Ok(true) } + + async fn add_block_headers_to_storage( + &self, + headers: HashMap, + ) -> Result<(), BlockHeaderStorageError> { + let ticker = self.ticker.clone(); + let locked_db = self + .lock_db() + .await + .map_err(|err| BlockHeaderStorageError::add_err(&ticker, err.to_string()))?; + let db_transaction = locked_db + .get_inner() + .transaction() + .await + .map_err(|err| BlockHeaderStorageError::add_err(&ticker, err.to_string()))?; + let block_headers_db = db_transaction + .table::() + .await + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + + for (height, header) in headers { + let hash = header.hash().reversed().to_string(); + let raw_header = hex::encode(header.raw()); + let bits: u32 = header.bits.into(); + let headers_to_store = BlockHeaderStorageTable { + ticker: ticker.clone(), + height, + bits, + hash, + raw_header, + }; + let index_keys = MultiIndex::new(BlockHeaderStorageTable::HEIGHT_TICKER_INDEX) + .with_value(&height) + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + .with_value(&ticker) + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + + block_headers_db + .replace_item_by_unique_multi_index(index_keys, &headers_to_store) + .await + .map_err(|err| BlockHeaderStorageError::add_err(&ticker, err.to_string()))?; + } + Ok(()) + } + + async fn get_block_header(&self, height: u64) -> Result, BlockHeaderStorageError> { + let ticker = self.ticker.clone(); + if let Some(raw_header) = self.get_block_header_raw(height).await? { + let serialized = &hex::decode(raw_header).map_err(|e| BlockHeaderStorageError::DecodeError { + coin: ticker.clone(), + reason: e.to_string(), + })?; + let mut reader = Reader::new_with_coin_variant(serialized, ticker.as_str().into()); + let header: BlockHeader = + reader + .read() + .map_err(|e: serialization::Error| BlockHeaderStorageError::DecodeError { + coin: ticker, + reason: e.to_string(), + })?; + + return Ok(Some(header)); + }; + + Ok(None) + } + + async fn get_block_header_raw(&self, height: u64) -> Result, BlockHeaderStorageError> { + let ticker = self.ticker.clone(); + let locked_db = self + .lock_db() + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + let db_transaction = locked_db + .get_inner() + .transaction() + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + let block_headers_db = db_transaction + .table::() + .await + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + let index_keys = MultiIndex::new(BlockHeaderStorageTable::HEIGHT_TICKER_INDEX) + .with_value(&height) + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + .with_value(&ticker) + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + + Ok(block_headers_db + .get_item_by_unique_multi_index(index_keys) + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .map(|raw| raw.1.raw_header)) + } + + async fn get_last_block_height(&self) -> Result, BlockHeaderStorageError> { + let ticker = self.ticker.clone(); + let locked_db = self + .lock_db() + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + let db_transaction = locked_db + .get_inner() + .transaction() + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + let block_headers_db = db_transaction + .table::() + .await + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + + // Todo: use open_cursor with direction to optimze this process. + let res = block_headers_db + .open_cursor("ticker") + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .only("ticker", ticker.clone()) + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .collect() + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .into_iter() + .map(|(_item_id, item)| item.height) + .collect::>(); + + Ok(res.into_iter().max()) + } + + async fn get_last_block_header_with_non_max_bits( + &self, + max_bits: u32, + ) -> Result, BlockHeaderStorageError> { + let ticker = self.ticker.clone(); + let locked_db = self + .lock_db() + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + let db_transaction = locked_db + .get_inner() + .transaction() + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + let block_headers_db = db_transaction + .table::() + .await + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + + // Todo: use open_cursor with direction to optimze this process. + let res = block_headers_db + .open_cursor("ticker") + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .only("ticker", ticker.clone()) + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .collect() + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .into_iter() + .map(|(_item_id, item)| item) + .collect::>(); + let res = res + .into_iter() + .filter_map(|e| if e.bits != max_bits { Some(e) } else { None }) + .collect::>(); + + for header in res { + let serialized = &hex::decode(header.raw_header).map_err(|e| BlockHeaderStorageError::DecodeError { + coin: ticker.clone(), + reason: e.to_string(), + })?; + let mut reader = Reader::new_with_coin_variant(serialized, ticker.as_str().into()); + let header: BlockHeader = + reader + .read() + .map_err(|e: serialization::Error| BlockHeaderStorageError::DecodeError { + coin: ticker, + reason: e.to_string(), + })?; + + return Ok(Some(header)); + } + + Ok(None) + } + + async fn get_block_height_by_hash(&self, hash: H256) -> Result, BlockHeaderStorageError> { + let ticker = self.ticker.clone(); + let locked_db = self + .lock_db() + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + let db_transaction = locked_db + .get_inner() + .transaction() + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + let block_headers_db = db_transaction + .table::() + .await + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + let index_keys = MultiIndex::new(BlockHeaderStorageTable::HASH_TICKER_INDEX) + .with_value(&hash.to_string()) + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + .with_value(&ticker) + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + + Ok(block_headers_db + .get_item_by_unique_multi_index(index_keys) + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .map(|raw| raw.1.height as i64)) + } + + async fn remove_headers_up_to_height(&self, to_height: u64) -> Result<(), BlockHeaderStorageError> { + let ticker = self.ticker.clone(); + let locked_db = self + .lock_db() + .await + .map_err(|err| BlockHeaderStorageError::delete_err(&ticker, err.to_string(), to_height))?; + let db_transaction = locked_db + .get_inner() + .transaction() + .await + .map_err(|err| BlockHeaderStorageError::delete_err(&ticker, err.to_string(), to_height))?; + let block_headers_db = db_transaction + .table::() + .await + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + + for height in 0..=to_height { + let index_keys = MultiIndex::new(BlockHeaderStorageTable::HEIGHT_TICKER_INDEX) + .with_value(&height) + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + .with_value(&ticker) + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + + block_headers_db + .delete_item_by_unique_multi_index(index_keys) + .await + .map_err(|err| BlockHeaderStorageError::delete_err(&ticker, err.to_string(), to_height))?; + } + + Ok(()) + } + + async fn is_table_empty(&self) -> Result<(), BlockHeaderStorageError> { + let ticker = self.ticker.clone(); + let locked_db = self + .lock_db() + .await + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + let db_transaction = locked_db + .get_inner() + .transaction() + .await + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + let block_headers_db = db_transaction + .table::() + .await + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + + let items = block_headers_db + .get_items("ticker", ticker.clone()) + .await + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + + if !items.is_empty() { + return Err(BlockHeaderStorageError::table_err( + &ticker, + "Table is not empty".to_string(), + )); + }; + + Ok(()) + } +} diff --git a/mm2src/coins/utxo/utxo_builder/mod.rs b/mm2src/coins/utxo/utxo_builder/mod.rs index 9c1cf135d0..88be32e275 100644 --- a/mm2src/coins/utxo/utxo_builder/mod.rs +++ b/mm2src/coins/utxo/utxo_builder/mod.rs @@ -2,8 +2,11 @@ mod utxo_arc_builder; mod utxo_coin_builder; mod utxo_conf_builder; -pub use utxo_arc_builder::{MergeUtxoArcOps, UtxoArcBuilder}; +pub use utxo_arc_builder::{BlockHeaderUtxoArcOps, MergeUtxoArcOps, UtxoArcBuilder}; pub use utxo_coin_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, - UtxoCoinWithIguanaPrivKeyBuilder, UtxoFieldsWithHardwareWalletBuilder, - UtxoFieldsWithIguanaPrivKeyBuilder}; + UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, + UtxoFieldsWithIguanaSecretBuilder}; pub use utxo_conf_builder::{UtxoConfBuilder, UtxoConfError, UtxoConfResult}; + +#[cfg(test)] +pub(crate) use utxo_arc_builder::{block_header_utxo_loop, BlockHeaderUtxoLoopExtraArgs}; diff --git a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs index d4964b12c4..ecd204d30f 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs @@ -1,16 +1,30 @@ +use crate::utxo::rpc_clients::{ElectrumClient, UtxoRpcClientEnum}; 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}; + UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, + UtxoFieldsWithIguanaSecretBuilder}; +use crate::utxo::{generate_and_send_tx, FeePolicy, GetUtxoListOps, UtxoArc, UtxoCommonOps, UtxoSyncStatusLoopHandle, + UtxoWeak}; +use crate::{DerivationMethod, PrivKeyBuildPolicy, UtxoActivationParams}; use async_trait::async_trait; -use common::executor::spawn; -use common::log::info; -use futures::future::{abortable, AbortHandle}; +use chain::TransactionOutput; +use common::executor::{AbortSettings, SpawnAbortable, Timer}; +use common::log::{error, info, warn}; +use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +#[cfg(test)] use mocktopus::macros::*; +use script::Builder; use serde_json::Value as Json; +use serialization::Reader; +use spv_validation::conf::SPVConf; +use spv_validation::helpers_validation::validate_headers; +use spv_validation::storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; +use std::collections::HashMap; +use std::num::NonZeroU64; + +const FETCH_BLOCK_HEADERS_ATTEMPTS: u64 = 3; +const CHUNK_SIZE_REDUCER_VALUE: u64 = 100; pub struct UtxoArcBuilder<'a, F, T> where @@ -20,7 +34,7 @@ where ticker: &'a str, conf: &'a Json, activation_params: &'a UtxoActivationParams, - priv_key_policy: PrivKeyBuildPolicy<'a>, + priv_key_policy: PrivKeyBuildPolicy, constructor: F, } @@ -33,7 +47,7 @@ where ticker: &'a str, conf: &'a Json, activation_params: &'a UtxoActivationParams, - priv_key_policy: PrivKeyBuildPolicy<'a>, + priv_key_policy: PrivKeyBuildPolicy, constructor: F, ) -> UtxoArcBuilder<'a, F, T> { UtxoArcBuilder { @@ -61,7 +75,12 @@ where fn ticker(&self) -> &str { self.ticker } } -impl<'a, F, T> UtxoFieldsWithIguanaPrivKeyBuilder for UtxoArcBuilder<'a, F, T> where +impl<'a, F, T> UtxoFieldsWithIguanaSecretBuilder for UtxoArcBuilder<'a, F, T> where + F: Fn(UtxoArc) -> T + Send + Sync + 'static +{ +} + +impl<'a, F, T> UtxoFieldsWithGlobalHDBuilder for UtxoArcBuilder<'a, F, T> where F: Fn(UtxoArc) -> T + Send + Sync + 'static { } @@ -80,22 +99,31 @@ where type ResultCoin = T; type Error = UtxoCoinBuildError; - fn priv_key_policy(&self) -> PrivKeyBuildPolicy<'_> { self.priv_key_policy.clone() } + fn priv_key_policy(&self) -> PrivKeyBuildPolicy { self.priv_key_policy.clone() } async fn build(self) -> MmResult { let utxo = self.build_utxo_fields().await?; + let sync_status_loop_handle = utxo.block_headers_status_notifier.clone(); + let spv_conf = utxo.conf.spv_conf.clone(); 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); + self.spawn_merge_utxo_loop_if_required(&utxo_arc, self.constructor.clone()); + + let result_coin = (self.constructor)(utxo_arc.clone()); + + if let (Some(spv_conf), Some(sync_handle)) = (spv_conf, sync_status_loop_handle) { + spv_conf.validate(self.ticker).map_to_mm(UtxoCoinBuildError::SPVError)?; + + let block_count = result_coin + .as_ref() + .rpc_client + .get_block_count() + .compat() + .await + .map_err(|err| UtxoCoinBuildError::CantGetBlockCount(err.to_string()))?; + self.spawn_block_header_utxo_loop(&utxo_arc, self.constructor.clone(), sync_handle, block_count, spv_conf); } + Ok(result_coin) } } @@ -114,50 +142,326 @@ where { } +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) => constructor(arc), + None => break, + }; + + let my_address = match coin.as_ref().derivation_method { + DerivationMethod::SingleAddress(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.get_unspent_ordered_list(my_address).await { + Ok((unspents, recently_spent)) => (unspents, recently_spent), + Err(e) => { + error!("Error {} on get_unspent_ordered_list of coin {}", e, ticker); + continue; + }, + }; + if unspents.len() >= merge_at { + 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(&my_address.hash).to_bytes(); + let output = TransactionOutput { value, script_pubkey }; + let merge_tx_fut = generate_and_send_tx( + &coin, + unspents, + None, + FeePolicy::DeductFromOutput(0), + recently_spent, + vec![output], + ); + match merge_tx_fut.await { + Ok(tx) => info!( + "UTXO merge successful for coin {}, tx_hash {:?}", + ticker, + tx.hash().reversed() + ), + Err(e) => error!("Error {:?} on UTXO merge attempt for coin {}", e, ticker), + } + } + } +} + pub trait MergeUtxoArcOps: UtxoCoinBuilderCommonOps { - fn spawn_merge_utxo_loop_if_required(&self, weak: UtxoWeak, constructor: F) + fn spawn_merge_utxo_loop_if_required(&self, utxo_arc: &UtxoArc, 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); + let merge_params = match self.activation_params().utxo_merge_params { + Some(ref merge_params) => merge_params, + None => return, + }; + + let ticker = self.ticker(); + info!("Starting UTXO merge loop for coin {ticker}"); + + let utxo_weak = utxo_arc.downgrade(); + let fut = merge_utxo_loop( + utxo_weak, + merge_params.merge_at, + merge_params.check_every, + merge_params.max_merge_at_once, + constructor, + ); + + let settings = AbortSettings::info_on_abort(format!("spawn_merge_utxo_loop_if_required stopped for {ticker}")); + utxo_arc + .abortable_system + .weak_spawner() + .spawn_with_settings(fut, settings); + } +} + +pub(crate) struct BlockHeaderUtxoLoopExtraArgs { + pub(crate) chunk_size: u64, + pub(crate) error_sleep: f64, + pub(crate) success_sleep: f64, +} + +#[cfg_attr(test, mockable)] +impl Default for BlockHeaderUtxoLoopExtraArgs { + fn default() -> Self { + Self { + chunk_size: 2016, + error_sleep: 10., + success_sleep: 60., + } + } +} + +pub(crate) async fn block_header_utxo_loop( + weak: UtxoWeak, + constructor: impl Fn(UtxoArc) -> T, + mut sync_status_loop_handle: UtxoSyncStatusLoopHandle, + mut block_count: u64, + spv_conf: SPVConf, +) { + let args = BlockHeaderUtxoLoopExtraArgs::default(); + let mut chunk_size = args.chunk_size; + while let Some(arc) = weak.upgrade() { + let coin = constructor(arc); + let ticker = coin.as_ref().conf.ticker.as_str(); + let client = match &coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(_) => break, + UtxoRpcClientEnum::Electrum(client) => client, + }; + + let storage = client.block_headers_storage(); + let from_block_height = match storage.get_last_block_height().await { + Ok(Some(height)) => height, + Ok(None) => { + if let Err(err) = validate_and_store_starting_header(client, ticker, storage, &spv_conf).await { + sync_status_loop_handle.notify_on_permanent_error(err); + break; + } + spv_conf.starting_block_header.height + }, + Err(err) => { + error!("Error {err:?} on getting the height of the last stored {ticker} header in DB!",); + sync_status_loop_handle.notify_on_temp_error(err); + Timer::sleep(args.error_sleep).await; + continue; + }, + }; + + let mut to_block_height = from_block_height + chunk_size; + if to_block_height > block_count { + block_count = match coin.as_ref().rpc_client.get_block_count().compat().await { + Ok(h) => h, + Err(err) => { + error!("Error {err:} on getting the height of the latest {ticker} block from rpc!"); + sync_status_loop_handle.notify_on_temp_error(err); + Timer::sleep(args.error_sleep).await; + continue; + }, + }; + + // More than `chunk_size` blocks could have appeared since the last `get_block_count` RPC. + // So reset `to_block_height` if only `from_block_height + chunk_size > actual_block_count`. + if to_block_height > block_count { + to_block_height = block_count; + } + } + drop_mutability!(to_block_height); + + // Todo: Add code for the case if a chain reorganization happens + if from_block_height == block_count { + sync_status_loop_handle.notify_sync_finished(to_block_height); + Timer::sleep(args.success_sleep).await; + continue; + } + + // Check if there should be a limit on the number of headers stored in storage. + if let Some(max_stored_block_headers) = spv_conf.max_stored_block_headers { + if let Err(err) = + remove_excessive_headers_from_storage(storage, to_block_height, max_stored_block_headers).await + { + sync_status_loop_handle.notify_on_temp_error(err); + Timer::sleep(args.error_sleep).await; + }; } + + sync_status_loop_handle.notify_blocks_headers_sync_status(from_block_height + 1, to_block_height); + + let mut fetch_blocker_headers_attempts = FETCH_BLOCK_HEADERS_ATTEMPTS; + let (block_registry, block_headers) = match client + .retrieve_headers(from_block_height + 1, to_block_height) + .compat() + .await + { + Ok(res) => res, + Err(error) => { + let err_inner = error.get_inner(); + if err_inner.is_network_error() { + log!("Network Error: Will try fetching {ticker} block headers again after 10 secs"); + sync_status_loop_handle.notify_on_temp_error(error); + Timer::sleep(args.error_sleep).await; + continue; + }; + + // If electrum returns response too large error, we will reduce the requested headers by CHUNK_SIZE_REDUCER_VALUE in every loop until we arrive at a reasonable value. + if err_inner.is_response_too_large() && chunk_size > CHUNK_SIZE_REDUCER_VALUE { + chunk_size -= CHUNK_SIZE_REDUCER_VALUE; + continue; + } + + if fetch_blocker_headers_attempts > 0 { + fetch_blocker_headers_attempts -= 1; + error!("Error {error:?} on retrieving latest {ticker} headers from rpc! {fetch_blocker_headers_attempts} attempts left"); + // Todo: remove this electrum server and use another in this case since the headers from this server can't be retrieved + sync_status_loop_handle.notify_on_temp_error(error); + Timer::sleep(args.error_sleep).await; + continue; + }; + + error!( + "Error {error:?} on retrieving latest {ticker} headers from rpc after {FETCH_BLOCK_HEADERS_ATTEMPTS} attempts" + ); + // Todo: remove this electrum server and use another in this case since the headers from this server can't be retrieved + sync_status_loop_handle.notify_on_permanent_error(error); + break; + }, + }; + + // Validate retrieved block headers. + if let Err(err) = validate_headers(ticker, from_block_height, block_headers, storage, &spv_conf).await { + error!("Error {err:?} on validating the latest headers for {ticker}!"); + // Todo: remove this electrum server and use another in this case since the headers from this server are invalid + sync_status_loop_handle.notify_on_permanent_error(err); + break; + }; + + let sleep = args.success_sleep; + ok_or_continue_after_sleep!(storage.add_block_headers_to_storage(block_registry).await, sleep); } } +#[derive(Display)] +enum StartingHeaderValidationError { + #[display(fmt = "Can't decode/deserialize from storage for {} - reason: {}", coin, reason)] + DecodeErr { + coin: String, + reason: String, + }, + RpcError(String), + StorageError(String), + #[display(fmt = "Error validating starting header for {} - reason: {}", coin, reason)] + ValidationError { + coin: String, + reason: String, + }, +} + +async fn validate_and_store_starting_header( + client: &ElectrumClient, + ticker: &str, + storage: &BlockHeaderStorage, + spv_conf: &SPVConf, +) -> MmResult<(), StartingHeaderValidationError> { + let height = spv_conf.starting_block_header.height; + let header_bytes = client + .blockchain_block_header(height) + .compat() + .await + .map_to_mm(|err| StartingHeaderValidationError::RpcError(err.to_string()))?; + + let mut reader = Reader::new_with_coin_variant(&header_bytes, ticker.into()); + let header = reader + .read() + .map_to_mm(|err| StartingHeaderValidationError::DecodeErr { + coin: ticker.to_string(), + reason: err.to_string(), + })?; + + spv_conf + .validate_rpc_starting_header(height, &header) + .map_to_mm(|err| StartingHeaderValidationError::ValidationError { + coin: ticker.to_string(), + reason: err.to_string(), + })?; + + storage + .add_block_headers_to_storage(HashMap::from([(height, header)])) + .await + .map_to_mm(|err| StartingHeaderValidationError::StorageError(err.to_string())) +} + +async fn remove_excessive_headers_from_storage( + storage: &BlockHeaderStorage, + last_height_to_be_added: u64, + max_allowed_headers: NonZeroU64, +) -> Result<(), BlockHeaderStorageError> { + let max_allowed_headers = max_allowed_headers.get(); + if last_height_to_be_added > max_allowed_headers { + return storage + .remove_headers_up_to_height(last_height_to_be_added - max_allowed_headers) + .await; + } + + Ok(()) +} + pub trait BlockHeaderUtxoArcOps: UtxoCoinBuilderCommonOps { - fn spawn_block_header_utxo_loop_if_required( + fn spawn_block_header_utxo_loop( &self, - weak: UtxoWeak, - maybe_storage: &Option, + utxo_arc: &UtxoArc, constructor: F, - ) -> Option - where + sync_status_loop_handle: UtxoSyncStatusLoopHandle, + block_count: u64, + spv_conf: SPVConf, + ) 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 + let ticker = self.ticker(); + info!("Starting UTXO block header loop for coin {ticker}"); + + let utxo_weak = utxo_arc.downgrade(); + let fut = block_header_utxo_loop(utxo_weak, constructor, sync_status_loop_handle, block_count, spv_conf); + + let settings = AbortSettings::info_on_abort(format!("spawn_block_header_utxo_loop stopped for {ticker}")); + utxo_arc + .abortable_system + .weak_spawner() + .spawn_with_settings(fut, settings); } } diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index bb35108ccb..af9ba59f8a 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -3,21 +3,24 @@ 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 crate::utxo::utxo_block_header_storage::BlockHeaderStorage; +use crate::utxo::utxo_builder::utxo_conf_builder::{UtxoConfBuilder, UtxoConfError}; +use crate::utxo::{output_script, utxo_common, ElectrumBuilderArgs, ElectrumProtoVerifier, ElectrumProtoVerifierEvent, + RecentlySpentOutPoints, TxFee, UtxoCoinConf, UtxoCoinFields, UtxoHDAccount, UtxoHDWallet, + UtxoRpcMode, UtxoSyncStatus, UtxoSyncStatusLoopHandle, DEFAULT_GAP_LIMIT, UTXO_DUST_AMOUNT}; +use crate::{BlockchainNetwork, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, + PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, UtxoActivationParams}; use async_trait::async_trait; use chain::TxHashAlgo; -use common::executor::{spawn, Timer}; -use common::log::{error, info}; +use common::custom_futures::repeatable::{Ready, Retry}; +use common::executor::{abortable_queue::AbortableQueue, AbortSettings, AbortableSystem, AbortedError, SpawnAbortable, + Timer}; +use common::log::{error, info, LogOnError}; use common::small_rng; -use crypto::{Bip32DerPathError, Bip44DerPathError, Bip44PathToCoin, CryptoCtx, CryptoInitError, HwWalletType}; +use crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, Secp256k1Secret, + StandardHDPathError, StandardHDPathToCoin}; use derive_more::Display; -use futures::channel::mpsc; +use futures::channel::mpsc::{channel, unbounded, Receiver as AsyncReceiver, UnboundedReceiver}; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use futures::StreamExt; @@ -26,9 +29,12 @@ pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, Key Type as ScriptType}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use primitives::hash::H256; +use primitives::hash::H160; use rand::seq::SliceRandom; use serde_json::{self as json, Value as Json}; +use spv_validation::conf::SPVConf; +use spv_validation::helpers_validation::SPVError; +use spv_validation::storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; use std::sync::{Arc, Mutex, Weak}; cfg_native! { @@ -63,8 +69,8 @@ pub enum UtxoCoinBuildError { ElectrumProtocolVersionCheckError(String), #[display(fmt = "Can not detect the user home directory")] CantDetectUserHome, - #[display(fmt = "Unexpected derivation method: {}", _0)] - UnexpectedDerivationMethod(String), + #[display(fmt = "Private key policy is not allowed: {}", _0)] + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), #[display(fmt = "Hardware Wallet context is not initialized")] HwContextNotInitialized, HDWalletStorageError(HDWalletStorageError), @@ -72,108 +78,155 @@ pub enum UtxoCoinBuildError { fmt = "Coin doesn't support Trezor hardware wallet. Please consider adding the 'trezor_coin' field to the coins config" )] CoinDoesntSupportTrezor, + BlockHeaderStorageError(BlockHeaderStorageError), + #[display(fmt = "Error {} on getting the height of the latest block from rpc!", _0)] + CantGetBlockCount(String), #[display(fmt = "Internal error: {}", _0)] Internal(String), + #[display(fmt = "SPV params verificaiton failed. Error: {_0}")] + SPVError(SPVError), } impl From for UtxoCoinBuildError { fn from(e: UtxoConfError) -> Self { UtxoCoinBuildError::ConfError(e) } } -impl From for UtxoCoinBuildError { +impl From for UtxoCoinBuildError { /// `CryptoCtx` is expected to be initialized already. - fn from(crypto_err: CryptoInitError) -> Self { UtxoCoinBuildError::Internal(crypto_err.to_string()) } + fn from(crypto_err: CryptoCtxError) -> Self { UtxoCoinBuildError::Internal(crypto_err.to_string()) } } impl From for UtxoCoinBuildError { - fn from(e: Bip32DerPathError) -> Self { UtxoCoinBuildError::Internal(Bip44DerPathError::from(e).to_string()) } + fn from(e: Bip32DerPathError) -> Self { UtxoCoinBuildError::Internal(StandardHDPathError::from(e).to_string()) } } impl From for UtxoCoinBuildError { fn from(e: HDWalletStorageError) -> Self { UtxoCoinBuildError::HDWalletStorageError(e) } } +impl From for UtxoCoinBuildError { + fn from(e: BlockHeaderStorageError) -> Self { UtxoCoinBuildError::BlockHeaderStorageError(e) } +} + +impl From for UtxoCoinBuildError { + fn from(e: AbortedError) -> Self { UtxoCoinBuildError::Internal(e.to_string()) } +} + #[async_trait] -pub trait UtxoCoinBuilder: UtxoFieldsWithIguanaPrivKeyBuilder + UtxoFieldsWithHardwareWalletBuilder { +pub trait UtxoCoinBuilder: + UtxoFieldsWithIguanaSecretBuilder + UtxoFieldsWithGlobalHDBuilder + UtxoFieldsWithHardwareWalletBuilder +{ type ResultCoin; type Error: NotMmError; - fn priv_key_policy(&self) -> PrivKeyBuildPolicy<'_>; + 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::IguanaPrivKey(priv_key) => self.build_utxo_fields_with_iguana_secret(priv_key).await, + PrivKeyBuildPolicy::GlobalHDAccount(global_hd_ctx) => { + self.build_utxo_fields_with_global_hd(global_hd_ctx).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; +pub trait UtxoFieldsWithIguanaSecretBuilder: UtxoCoinBuilderCommonOps { + async fn build_utxo_fields_with_iguana_secret( + &self, + priv_key: IguanaPrivKey, + ) -> UtxoCoinBuildResult { + let conf = UtxoConfBuilder::new(self.conf(), self.activation_params(), self.ticker()).build()?; + build_utxo_coin_fields_with_conf_and_secret(self, conf, priv_key).await + } } #[async_trait] -pub trait UtxoFieldsWithIguanaPrivKeyBuilder: UtxoCoinBuilderCommonOps { - async fn build_utxo_fields_with_iguana_priv_key(&self, priv_key: &[u8]) -> UtxoCoinBuildResult { +pub trait UtxoFieldsWithGlobalHDBuilder: UtxoCoinBuilderCommonOps { + async fn build_utxo_fields_with_global_hd( + &self, + global_hd_ctx: GlobalHDAccountArc, + ) -> UtxoCoinBuildResult { let conf = UtxoConfBuilder::new(self.conf(), self.activation_params(), self.ticker()).build()?; - 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 derivation_path = conf + .derivation_path + .as_ref() + .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet)?; + let secret = global_hd_ctx + .derive_secp256k1_secret(derivation_path) + .mm_err(|e| UtxoCoinBuildError::Internal(e.to_string()))?; + build_utxo_coin_fields_with_conf_and_secret(self, conf, secret).await + } +} - 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()?; +async fn build_utxo_coin_fields_with_conf_and_secret( + builder: &Builder, + conf: UtxoCoinConf, + secret: Secp256k1Secret, +) -> UtxoCoinBuildResult +where + Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, +{ + let private = Private { + prefix: conf.wif_prefix, + secret, + 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 = builder.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 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) - } + let my_script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let derivation_method = DerivationMethod::SingleAddress(my_address); + let priv_key_policy = PrivKeyPolicy::KeyPair(key_pair); + + // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, + // all spawned futures related to this `UTXO` coin will be aborted as well. + let abortable_system: AbortableQueue = builder.ctx().abortable_system.create_subsystem()?; + + let rpc_client = builder.rpc_client(abortable_system.create_subsystem()?).await?; + let tx_fee = builder.tx_fee(&rpc_client).await?; + let decimals = builder.decimals(&rpc_client).await?; + let dust_amount = builder.dust_amount(); + + let initial_history_state = builder.initial_history_state(); + let tx_hash_algo = builder.tx_hash_algo(); + let check_utxo_maturity = builder.check_utxo_maturity(); + let tx_cache = builder.tx_cache(); + let (block_headers_status_notifier, block_headers_status_watcher) = + builder.block_header_status_channel(&conf.spv_conf); + + let coin = UtxoCoinFields { + conf, + decimals, + dust_amount, + rpc_client, + priv_key_policy, + derivation_method, + history_sync_state: Mutex::new(initial_history_state), + tx_cache, + recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), + tx_fee, + tx_hash_algo, + check_utxo_maturity, + block_headers_status_notifier, + block_headers_status_watcher, + abortable_system, + }; + Ok(coin) } #[async_trait] @@ -185,7 +238,7 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { if !self.supports_trezor(&conf) { return MmError::err(UtxoCoinBuildError::CoinDoesntSupportTrezor); } - self.check_if_trezor_is_initialized()?; + let hd_wallet_rmd160 = self.trezor_wallet_rmd160()?; // For now, use a default script pubkey. // TODO change the type of `recently_spent_outpoints` to `AsyncMutex>` @@ -193,7 +246,10 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)); let address_format = self.address_format()?; - let derivation_path = self.derivation_path()?; + let derivation_path = conf + .derivation_path + .clone() + .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet)?; let hd_wallet_storage = HDWalletCoinStorage::init(self.ctx(), ticker).await?; @@ -202,6 +258,7 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { .await?; let gap_limit = self.gap_limit(); let hd_wallet = UtxoHDWallet { + hd_wallet_rmd160, hd_wallet_storage, address_format, derivation_path, @@ -209,7 +266,11 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { gap_limit, }; - let rpc_client = self.rpc_client().await?; + // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, + // all spawned futures related to this `UTXO` coin will be aborted as well. + let abortable_system: AbortableQueue = self.ctx().abortable_system.create_subsystem()?; + + let rpc_client = self.rpc_client(abortable_system.create_subsystem()?).await?; let tx_fee = self.tx_fee(&rpc_client).await?; let decimals = self.decimals(&rpc_client).await?; let dust_amount = self.dust_amount(); @@ -218,7 +279,8 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { 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 (block_headers_status_notifier, block_headers_status_watcher) = + self.block_header_status_channel(&conf.spv_conf); let coin = UtxoCoinFields { conf, @@ -228,12 +290,14 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { 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, + block_headers_status_notifier, + block_headers_status_watcher, + abortable_system, }; Ok(coin) } @@ -241,25 +305,27 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { async fn load_hd_wallet_accounts( &self, hd_wallet_storage: &HDWalletCoinStorage, - derivation_path: &Bip44PathToCoin, + derivation_path: &StandardHDPathToCoin, ) -> 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 trezor_wallet_rmd160(&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(hw_ctx.rmd160()), + } + } + fn check_if_trezor_is_initialized(&self) -> UtxoCoinBuildResult<()> { let crypto_ctx = CryptoCtx::from_ctx(self.ctx())?; let hw_ctx = crypto_ctx @@ -281,15 +347,6 @@ pub trait UtxoCoinBuilderCommonOps { 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()) @@ -385,7 +442,7 @@ pub trait UtxoCoinBuilderCommonOps { } } - async fn rpc_client(&self) -> UtxoCoinBuildResult { + async fn rpc_client(&self, abortable_system: AbortableQueue) -> UtxoCoinBuildResult { match self.activation_params().mode.clone() { UtxoRpcMode::Native => { #[cfg(target_arch = "wasm32")] @@ -399,18 +456,23 @@ pub trait UtxoCoinBuilderCommonOps { } }, UtxoRpcMode::Electrum { servers } => { - let electrum = self.electrum_client(ElectrumBuilderArgs::default(), servers).await?; + let electrum = self + .electrum_client(abortable_system, ElectrumBuilderArgs::default(), servers) + .await?; Ok(UtxoRpcClientEnum::Electrum(electrum)) }, } } + /// The method takes `abortable_system` that will be used to spawn Electrum's related futures. + /// It can be pinned to the coin's abortable system via [`AbortableSystem::create_subsystem`], but not required. async fn electrum_client( &self, + abortable_system: AbortableQueue, args: ElectrumBuilderArgs, mut servers: Vec, ) -> UtxoCoinBuildResult { - let (on_connect_tx, on_connect_rx) = mpsc::unbounded(); + let (on_event_tx, on_event_rx) = unbounded(); let ticker = self.ticker().to_owned(); let ctx = self.ctx(); let mut event_handlers = vec![]; @@ -421,12 +483,26 @@ pub trait UtxoCoinBuilderCommonOps { } if args.negotiate_version { - event_handlers.push(ElectrumProtoVerifier { on_connect_tx }.into_shared()); + event_handlers.push(ElectrumProtoVerifier { on_event_tx }.into_shared()); + } + + let storage_ticker = self.ticker().replace('-', "_"); + let block_headers_storage = BlockHeaderStorage::new_from_ctx(self.ctx().clone(), storage_ticker) + .map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; + if !block_headers_storage.is_initialized_for().await? { + block_headers_storage.init().await?; } let mut rng = small_rng(); servers.as_mut_slice().shuffle(&mut rng); - let client = ElectrumClientImpl::new(ticker, event_handlers); + + let client = ElectrumClientImpl::new( + ticker, + event_handlers, + block_headers_storage, + abortable_system, + args.negotiate_version, + ); for server in servers.iter() { match client.add_server(server).await { Ok(_) => (), @@ -449,10 +525,11 @@ pub trait UtxoCoinBuilderCommonOps { let client = Arc::new(client); + let spawner = client.spawner(); 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); + spawn_electrum_version_loop(&spawner, weak_client, on_event_rx, client_name); wait_for_protocol_version_checked(&client) .await @@ -461,7 +538,7 @@ pub trait UtxoCoinBuilderCommonOps { if args.spawn_ping { let weak_client = Arc::downgrade(&client); - spawn_electrum_ping_loop(weak_client, servers); + spawn_electrum_ping_loop(&spawner, weak_client, servers); } Ok(ElectrumClient(client)) @@ -572,6 +649,24 @@ pub trait UtxoCoinBuilderCommonOps { #[cfg(not(target_arch = "wasm32"))] fn tx_cache_path(&self) -> PathBuf { self.ctx().dbdir().join("TX_CACHE") } + + fn block_header_status_channel( + &self, + spv_conf: &Option, + ) -> ( + Option, + Option>>, + ) { + if spv_conf.is_some() && !self.activation_params().mode.is_native() { + let (sync_status_notifier, sync_watcher) = channel(1); + return ( + Some(UtxoSyncStatusLoopHandle::new(sync_status_notifier)), + Some(AsyncMutex::new(sync_watcher)), + ); + }; + + (None, None) + } } /// Attempts to parse native daemon conf file and return rpcport, rpcuser and rpcpassword @@ -593,7 +688,7 @@ fn read_native_mode_conf( .or_else(|| conf.general_section().get(property)) } - let conf: Ini = match Ini::load_from_file(&filename) { + let conf: Ini = match Ini::load_from_file(filename) { Ok(ini) => ini, Err(err) => { return ERR!( @@ -622,41 +717,54 @@ fn read_native_mode_conf( /// 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 { +fn spawn_electrum_ping_loop( + spawner: &Spawner, + weak_client: Weak, + servers: Vec, +) { + let msg_on_stopped = format!("Electrum servers {servers:?} ping loop stopped"); + let fut = 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 } - }); + }; + + let settings = AbortSettings::info_on_any_stop(msg_on_stopped); + spawner.spawn_with_settings(fut, settings); } /// 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( +fn spawn_electrum_version_loop( + spawner: &Spawner, weak_client: Weak, - mut on_connect_rx: mpsc::UnboundedReceiver, + mut on_event_rx: 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, - )); + let fut = async move { + while let Some(event) = on_event_rx.next().await { + match event { + ElectrumProtoVerifierEvent::Connected(electrum_addr) => { + check_electrum_server_version(weak_client.clone(), client_name.clone(), electrum_addr).await + }, + ElectrumProtoVerifierEvent::Disconnected(electrum_addr) => { + if let Some(client) = weak_client.upgrade() { + client.reset_protocol_version(&electrum_addr).await.error_log(); + } + }, + } } - - info!("Electrum server.version loop stopped"); - }); + }; + let settings = AbortSettings::info_on_any_stop("Electrum server.version loop stopped".to_string()); + spawner.spawn_with_settings(fut, settings); } async fn check_electrum_server_version( @@ -720,27 +828,24 @@ async fn check_electrum_server_version( /// 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."); - } - + repeatable!(async { if client.count_connections().await == 0 { // All of the connections were removed because of server.version checking - return ERR!( + return Ready(ERR!( "There are no Electrums with the required protocol version {:?}", client.protocol_version() - ); + )); } if client.is_protocol_version_checked().await { - break; + return Ready(Ok(())); } - - Timer::sleep(0.5).await; - attempts += 1; - } - - Ok(()) + Retry(()) + }) + .repeat_every_secs(0.5) + .attempts(10) + .await + .map_err(|_exceed| ERRL!("Failed protocol version verifying of at least 1 of Electrums in 5 seconds.")) + // Flatten `Result< Result<(), String>, String >` + .flatten() } diff --git a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs index 3cc89efe7b..d19bded6cf 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs @@ -3,14 +3,14 @@ use crate::utxo::{parse_hex_encoded_u32, UtxoCoinConf, DEFAULT_DYNAMIC_FEE_VOLAT MATURE_CONFIRMATIONS_DEFAULT}; use crate::UtxoActivationParams; use bitcrypto::ChecksumType; -use crypto::trezor::utxo::TrezorUtxoCoin; -use crypto::{Bip32Error, ChildNumber}; +use crypto::{Bip32Error, StandardHDPathToCoin}; 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 spv_validation::conf::SPVConf; use std::num::NonZeroU64; use std::sync::atomic::AtomicBool; @@ -24,23 +24,13 @@ pub enum UtxoConfError { 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), + #[display(fmt = "Error deserializing 'spv_conf': {}", _0)] + ErrorDeserializingSPVConf(String), InvalidConsensusBranchId(String), InvalidVersionGroupId(String), InvalidAddressFormat(String), - InvalidBlockHeaderParams(String), InvalidDecimals(String), } @@ -98,7 +88,9 @@ impl<'a> UtxoConfBuilder<'a> { 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(); + let derivation_path = self.derivation_path()?; + let avg_blocktime = self.avg_blocktime(); + let spv_conf = self.spv_conf()?; Ok(UtxoCoinConf { ticker: self.ticker.to_owned(), @@ -130,7 +122,9 @@ impl<'a> UtxoConfBuilder<'a> { mature_confirmations, estimate_fee_blocks, trezor_coin, - enable_spv_proof, + spv_conf, + derivation_path, + avg_blocktime, }) } @@ -284,9 +278,17 @@ impl<'a> UtxoConfBuilder<'a> { 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 trezor_coin(&self) -> Option { self.conf["trezor_coin"].as_str().map(|coin| coin.to_string()) } + + fn spv_conf(&self) -> UtxoConfResult> { + json::from_value(self.conf["spv_conf"].clone()) + .map_to_mm(|e| UtxoConfError::ErrorDeserializingSPVConf(e.to_string())) + } + + fn derivation_path(&self) -> UtxoConfResult> { + json::from_value(self.conf["derivation_path"].clone()) + .map_to_mm(|e| UtxoConfError::ErrorDeserializingDerivationPath(e.to_string())) } - fn enable_spv_proof(&self) -> bool { self.conf["enable_spv_proof"].as_bool().unwrap_or(false) } + fn avg_blocktime(&self) -> Option { self.conf["avg_blocktime"].as_u64() } } diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index a67c9136c4..19db7c9f32 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1,30 +1,34 @@ -use super::rpc_clients::TxMerkleBranch; use super::*; use crate::coin_balance::{AddressBalanceStatus, HDAddressBalance, HDWalletBalanceOps}; +use crate::coin_errors::{MyAddressError, ValidatePaymentError}; +use crate::hd_confirm_address::HDConfirmAddress; use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountMut, HDAccountsMap, - NewAccountCreatingError}; +use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, HDAccountsMap, + NewAccountCreatingError, NewAddressDeriveConfirmError, NewAddressDerivingError}; 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::spv::SimplePaymentVerification; 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; +use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, GetWithdrawSenderAddress, HDAccountAddressId, + RawTransactionError, RawTransactionRequest, RawTransactionRes, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendWatcherRefundsPaymentArgs, SignatureError, SignatureResult, + SwapOps, TradePreimageValue, TransactionFut, TxFeeDetails, TxMarshalingErr, ValidateAddressResult, + ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFrom, + WithdrawResult, WithdrawSenderAddress, EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, + INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; pub use bitcrypto::{dhash160, sha256, ChecksumType}; +use bitcrypto::{dhash256, ripemd160}; use chain::constants::SEQUENCE_FINAL; -use chain::{BlockHeader, OutPoint, RawBlockHeader, TransactionOutput}; +use chain::{OutPoint, TransactionOutput}; use common::executor::Timer; use common::jsonrpc_client::JsonRpcErrorType; -use common::log::{debug, error, info, warn}; -use common::mm_metrics::MetricsArc; +use common::log::{error, warn}; use common::{now_ms, one_hundred, ten_f64}; -use crypto::{Bip32DerPathOps, Bip44Chain, Bip44DerPathError, Bip44DerivationPath, RpcDerivationPath}; +use crypto::{Bip32DerPathOps, Bip44Chain, RpcDerivationPath, StandardHDPath, StandardHDPathError}; use futures::compat::Future01CompatExt; use futures::future::{FutureExt, TryFutureExt}; use futures01::future::Either; @@ -40,26 +44,24 @@ use rpc::v1::types::{Bytes as BytesJson, ToTxHash, TransactionInputEnum, H256 as use script::{Builder, Opcode, Script, ScriptAddress, TransactionInputSigner, UnsignedTransactionInput}; use secp256k1::{PublicKey, Signature}; use serde_json::{self as json}; -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 serialization::{deserialize, serialize, serialize_with_flags, CoinVariant, CompactInteger, Serializable, Stream, + SERIALIZE_TRANSACTION_WITNESS}; use std::cmp::Ordering; use std::collections::hash_map::{Entry, HashMap}; use std::str::FromStr; 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; +pub mod utxo_tx_history_v2_common; + pub const DEFAULT_FEE_VOUT: usize = 0; pub const DEFAULT_SWAP_TX_SPEND_SIZE: u64 = 305; pub const DEFAULT_SWAP_VOUT: usize = 0; +pub const DEFAULT_SWAP_VIN: 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) => { @@ -102,14 +104,22 @@ pub async fn get_tx_fee(coin: &UtxoCoinFields) -> UtxoRpcResult { } } -pub fn derive_address( +fn derive_address_with_cache( 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); + hd_addresses_cache: &mut HashMap, + hd_address_id: HDAddressId, +) -> AddressDerivingResult +where + T: UtxoCommonOps, +{ + // Check if the given HD address has been derived already. + if let Some(hd_address) = hd_addresses_cache.get(&hd_address_id) { + return Ok(hd_address.clone()); + } + + let change_child = hd_address_id.chain.to_child_number(); + let address_id_child = ChildNumber::from(hd_address_id.address_id); let derived_pubkey = hd_account .extended_pubkey @@ -121,11 +131,113 @@ pub fn derive_address( 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 { + + let hd_address = HDAddress { address, pubkey, derivation_path, - }) + }; + + // Cache the derived `hd_address`. + hd_addresses_cache.insert(hd_address_id, hd_address.clone()); + Ok(hd_address) +} + +/// [`HDWalletCoinOps::derive_addresses`] native implementation. +/// +/// # Important +/// +/// The [`HDAddressesCache::cache`] mutex is locked once for the entire duration of this function. +#[cfg(not(target_arch = "wasm32"))] +pub async fn derive_addresses( + coin: &T, + hd_account: &UtxoHDAccount, + address_ids: Ids, +) -> AddressDerivingResult> +where + T: UtxoCommonOps, + Ids: Iterator, +{ + let mut hd_addresses_cache = hd_account.derived_addresses.lock().await; + address_ids + .map(|hd_address_id| derive_address_with_cache(coin, hd_account, &mut hd_addresses_cache, hd_address_id)) + .collect() +} + +/// [`HDWalletCoinOps::derive_addresses`] WASM implementation. +/// +/// # Important +/// +/// This function locks [`HDAddressesCache::cache`] mutex at each iteration. +/// +/// # Performance +/// +/// Locking the [`HDAddressesCache::cache`] mutex at each iteration may significantly degrade performance. +/// But this is required at least for now due the facts that: +/// 1) mm2 runs in the same thread as `KomodoPlatform/air_dex` runs; +/// 2) [`ExtendedPublicKey::derive_child`] is a synchronous operation, and it takes a long time. +/// So we need to periodically invoke Javascript runtime to handle UI events and other asynchronous tasks. +#[cfg(target_arch = "wasm32")] +pub async fn derive_addresses( + coin: &T, + hd_account: &UtxoHDAccount, + address_ids: Ids, +) -> AddressDerivingResult> +where + T: UtxoCommonOps, + Ids: Iterator, +{ + let mut result = Vec::new(); + for hd_address_id in address_ids { + let mut hd_addresses_cache = hd_account.derived_addresses.lock().await; + + let hd_address = derive_address_with_cache(coin, hd_account, &mut hd_addresses_cache, hd_address_id)?; + result.push(hd_address); + } + + Ok(result) +} + +pub async fn generate_and_confirm_new_address( + coin: &Coin, + hd_wallet: &Coin::HDWallet, + hd_account: &mut Coin::HDAccount, + chain: Bip44Chain, + confirm_address: &ConfirmAddress, +) -> MmResult, NewAddressDeriveConfirmError> +where + Coin: HDWalletCoinWithStorageOps
+ + AsRef + + Sync, + ConfirmAddress: HDConfirmAddress, +{ + use crate::hd_wallet::inner_impl; + + let inner_impl::NewAddress { + address, + new_known_addresses_number, + } = inner_impl::generate_new_address_immutable(coin, hd_wallet, hd_account, chain).await?; + + let trezor_coin = coin.as_ref().conf.trezor_coin.clone().or_mm_err(|| { + let ticker = &coin.as_ref().conf.ticker; + let error = format!("'{ticker}' coin must contain the 'trezor_coin' field in the coins config"); + NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::Internal(error)) + })?; + let expected_address = address.address.to_string(); + // Ask the user to confirm if the given `expected_address` is the same as on the HW display. + confirm_address + .confirm_utxo_address(trezor_coin, address.derivation_path.clone(), expected_address) + .await?; + + let actual_known_addresses_number = hd_account.known_addresses_number(chain)?; + // Check if the actual `known_addresses_number` hasn't been changed while we waited for the user confirmation. + // If the actual value is greater than the new one, we don't need to update. + if actual_known_addresses_number < new_known_addresses_number { + coin.set_known_addresses_number(hd_wallet, hd_account, chain, new_known_addresses_number) + .await?; + } + + Ok(address) } pub async fn create_new_account<'a, Coin, XPubExtractor>( @@ -137,7 +249,7 @@ where Coin: ExtractExtendedPubkey + HDWalletCoinWithStorageOps + Sync, - XPubExtractor: HDXPubExtractor + Sync, + XPubExtractor: HDXPubExtractor, { const INIT_ACCOUNT_ID: u32 = 0; let new_account_id = hd_wallet @@ -149,17 +261,16 @@ where .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, - }); + let max_accounts_number = hd_wallet.account_limit(); + if new_account_id >= max_accounts_number { + return MmError::err(NewAccountCreatingError::AccountLimitReached { max_accounts_number }); } 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()))?; - let account_derivation_path: Bip44PathToAccount = hd_wallet.derivation_path.derive(account_child)?; + let account_derivation_path: StandardHDPathToAccount = hd_wallet.derivation_path.derive(account_child)?; let account_pubkey = coin .extract_extended_pubkey(xpub_extractor, account_derivation_path.to_derivation_path()) .await?; @@ -171,6 +282,7 @@ where // We don't know how many addresses are used by the user at this moment. external_addresses_number: 0, internal_addresses_number: 0, + derived_addresses: HDAddressesCache::default(), }; let accounts = hd_wallet.accounts.lock().await; @@ -203,10 +315,9 @@ pub async fn set_known_addresses_number( where T: HDWalletCoinWithStorageOps + Sync, { - if new_known_addresses_number >= ChildNumber::HARDENED_FLAG { - return MmError::err(AccountUpdatingError::AddressLimitReached { - max_addresses_number: ChildNumber::HARDENED_FLAG, - }); + let max_addresses_number = hd_wallet.address_limit(); + if new_known_addresses_number >= max_addresses_number { + return MmError::err(AccountUpdatingError::AddressLimitReached { max_addresses_number }); } match chain { Bip44Chain::External => { @@ -288,29 +399,37 @@ where .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 max_addresses_number = hd_wallet.address_limit(); + while checking_address_id < max_addresses_number && 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)?; + } = coin.derive_address(hd_account, chain, checking_address_id).await?; 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(), - }); - } + // First, derive all empty addresses and put it into `balances` with default balance. + let address_ids = (last_non_empty_address_id..checking_address_id) + .into_iter() + .map(|address_id| HDAddressId { chain, address_id }); + let empty_addresses = + coin.derive_addresses(hd_account, address_ids) + .await? + .into_iter() + .map(|empty_address| HDAddressBalance { + address: empty_address.address.to_string(), + derivation_path: RpcDerivationPath(empty_address.derivation_path), + chain, + balance: CoinBalance::default(), + }); + balances.extend(empty_addresses); + + // Then push this non-empty address. balances.push(HDAddressBalance { address: checking_address.to_string(), derivation_path: RpcDerivationPath(checking_address_der_path), @@ -367,7 +486,7 @@ where pub async fn load_hd_accounts_from_storage( hd_wallet_storage: &HDWalletCoinStorage, - derivation_path: &Bip44PathToCoin, + derivation_path: &StandardHDPathToCoin, ) -> HDWalletStorageResult> { let accounts = hd_wallet_storage.load_all_accounts().await?; let res: HDWalletStorageResult> = accounts @@ -459,17 +578,25 @@ where { let trezor_coin = conf .trezor_coin + .clone() .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) + Secp256k1ExtendedPublicKey::from_str(&xpub).map_to_mm(|e| HDExtractPubkeyError::InvalidXpub(e.to_string())) } /// returns the fee required to be paid for HTLC spend transaction -pub async fn get_htlc_spend_fee(coin: &T, tx_size: u64) -> UtxoRpcResult { +pub async fn get_htlc_spend_fee( + coin: &T, + tx_size: u64, + stage: &FeeApproxStage, +) -> 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 * tx_size) / KILO_BYTE, + ActualTxFee::Dynamic(fee_per_kb) => { + let fee_per_kb = increase_dynamic_fee_by_stage(&coin, fee_per_kb, stage); + (fee_per_kb * tx_size) / KILO_BYTE + }, // return satoshis here as swap spend transaction size is always less than 1 kb ActualTxFee::FixedPerKb(satoshis) => { let tx_size_kb = if tx_size % KILO_BYTE == 0 { @@ -538,44 +665,49 @@ where coin.my_spendable_balance() } -pub fn address_from_str_unchecked(coin: &UtxoCoinFields, address: &str) -> Result { - if let Ok(legacy) = Address::from_str(address) { - return Ok(legacy); - } +pub fn address_from_str_unchecked(coin: &UtxoCoinFields, address: &str) -> MmResult { + let mut errors = Vec::with_capacity(3); + + match Address::from_str(address) { + Ok(legacy) => return Ok(legacy), + Err(e) => errors.push(e.to_string()), + }; - if let Ok(segwit) = Address::from_segwitaddress( + match Address::from_segwitaddress( address, coin.conf.checksum_type, coin.conf.pub_addr_prefix, coin.conf.pub_t_addr_prefix, ) { - return Ok(segwit); + Ok(segwit) => return Ok(segwit), + Err(e) => errors.push(e), } - if let Ok(cashaddress) = Address::from_cashaddress( + match Address::from_cashaddress( address, coin.conf.checksum_type, coin.conf.pub_addr_prefix, coin.conf.p2sh_addr_prefix, coin.conf.pub_t_addr_prefix, ) { - return Ok(cashaddress); + Ok(cashaddress) => return Ok(cashaddress), + Err(e) => errors.push(e), } - return ERR!("Invalid address: {}", address); + MmError::err(AddrFromStrError::CannotDetermineFormat(errors)) } 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), + PrivKeyPolicy::Trezor => MmError::err(UnexpectedDerivationMethod::ExpectedSingleAddress), } } -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)); +pub fn checked_address_from_str(coin: &T, address: &str) -> MmResult { + let addr = address_from_str_unchecked(coin.as_ref(), address)?; + check_withdraw_address_supported(coin, &addr)?; Ok(addr) } @@ -635,7 +767,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { UtxoTxBuilder { tx: coin.as_ref().transaction_preimage(), coin, - from: coin.as_ref().derivation_method.iguana().cloned(), + from: coin.as_ref().derivation_method.single_addr().cloned(), available_inputs: vec![], fee_policy: FeePolicy::SendExact, fee: None, @@ -965,6 +1097,9 @@ pub struct P2SHSpendingTxInput<'a> { } pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxInput<'_>) -> Result { + if input.prev_transaction.outputs.is_empty() { + return ERR!("Transaction doesn't have any output"); + } 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) @@ -1137,26 +1272,42 @@ pub fn send_maker_spends_taker_payment( time_lock: u32, taker_pub: &[u8], secret: &[u8], + secret_hash: &[u8], swap_unique_data: &[u8], ) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_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; + drop_mutability!(prev_transaction); + if prev_transaction.outputs.is_empty() { + return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); + } let key_pair = coin.derive_htlc_key_pair(swap_unique_data); let script_data = Builder::default() .push_data(secret) .push_opcode(Opcode::OP_0) .into_script(); + let redeem_script = payment_script( time_lock, - &*dhash160(secret), + secret_hash, &try_tx_fus!(Public::from_slice(taker_pub)), key_pair.public(), ) .into(); let fut = async move { - let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= prev_transaction.outputs[0].value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + prev_transaction.outputs[0].value + ); + } let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { value: prev_transaction.outputs[0].value - fee, @@ -1182,17 +1333,188 @@ pub fn send_maker_spends_taker_payment( Box::new(fut.boxed().compat()) } +pub fn send_maker_payment_spend_preimage( + coin: &T, + input: SendMakerPaymentSpendPreimageInput, +) -> TransactionFut { + let mut transaction: UtxoTx = try_tx_fus!(deserialize(input.preimage).map_err(|e| ERRL!("{:?}", e))); + if transaction.inputs.is_empty() { + return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any input")); + } + let script = Script::from(transaction.inputs[DEFAULT_SWAP_VIN].script_sig.clone()); + let mut instructions = script.iter(); + + let instruction_1 = try_tx_fus!(try_tx_fus!(instructions.next().ok_or("Instruction not found"))); + let instruction_2 = try_tx_fus!(try_tx_fus!(instructions.next().ok_or("Instruction not found"))); + + let script_sig = try_tx_fus!(instruction_1 + .data + .ok_or("No script signature in the taker spends maker payment preimage")); + let redeem_script = try_tx_fus!(instruction_2 + .data + .ok_or("No redeem script in the taker spends maker payment preimage")); + let script_data = Builder::default() + .push_data(input.secret) + .push_opcode(Opcode::OP_0) + .into_script(); + + let mut resulting_script = Builder::default().push_data(script_sig).into_bytes(); + 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); + + transaction.inputs[DEFAULT_SWAP_VIN].script_sig = resulting_script; + + let coin = coin.clone(); + let fut = async move { + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); + try_tx_s!(tx_fut.await, transaction); + + Ok(transaction.into()) + }; + + Box::new(fut.boxed().compat()) +} + +pub fn create_maker_payment_spend_preimage( + coin: &T, + maker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + swap_unique_data: &[u8], +) -> TransactionFut { + let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_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; + drop_mutability!(prev_transaction); + if prev_transaction.outputs.is_empty() { + return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); + } + + let key_pair = coin.derive_htlc_key_pair(swap_unique_data); + + let script_data = Builder::default().into_script(); + let redeem_script = payment_script( + time_lock, + secret_hash, + &try_tx_fus!(Public::from_slice(maker_pub)), + key_pair.public(), + ) + .into(); + let coin = coin.clone(); + let fut = async move { + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WatcherPreimage) + .await + ); + + if fee >= prev_transaction.outputs[0].value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + prev_transaction.outputs[0].value + ); + } + let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let output = TransactionOutput { + value: prev_transaction.outputs[0].value - fee, + script_pubkey, + }; + + 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); + + Ok(transaction.into()) + }; + Box::new(fut.boxed().compat()) +} + +pub fn create_taker_payment_refund_preimage( + coin: &T, + taker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + swap_unique_data: &[u8], +) -> TransactionFut { + let coin = coin.clone(); + let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_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; + drop_mutability!(prev_transaction); + if prev_transaction.outputs.is_empty() { + return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); + } + + 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, + key_pair.public(), + &try_tx_fus!(Public::from_slice(maker_pub)), + ) + .into(); + let fut = async move { + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WatcherPreimage) + .await + ); + if fee >= prev_transaction.outputs[0].value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + prev_transaction.outputs[0].value + ); + } + let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let output = TransactionOutput { + value: prev_transaction.outputs[0].value - fee, + script_pubkey, + }; + + 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); + + Ok(transaction.into()) + }; + Box::new(fut.boxed().compat()) +} + pub fn send_taker_spends_maker_payment( coin: T, maker_payment_tx: &[u8], time_lock: u32, maker_pub: &[u8], secret: &[u8], + secret_hash: &[u8], swap_unique_data: &[u8], ) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_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; + drop_mutability!(prev_transaction); + if prev_transaction.outputs.is_empty() { + return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); + } let key_pair = coin.derive_htlc_key_pair(swap_unique_data); @@ -1202,13 +1524,23 @@ pub fn send_taker_spends_maker_payment( .into_script(); let redeem_script = payment_script( time_lock, - &*dhash160(secret), + secret_hash, &try_tx_fus!(Public::from_slice(maker_pub)), key_pair.public(), ) .into(); let fut = async move { - let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= prev_transaction.outputs[0].value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + prev_transaction.outputs[0].value + ); + } let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { value: prev_transaction.outputs[0].value - fee, @@ -1242,10 +1574,14 @@ pub fn send_taker_refunds_payment( secret_hash: &[u8], swap_unique_data: &[u8], ) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_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; + drop_mutability!(prev_transaction); + if prev_transaction.outputs.is_empty() { + return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); + } let key_pair = coin.derive_htlc_key_pair(swap_unique_data); let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); @@ -1257,7 +1593,17 @@ pub fn send_taker_refunds_payment( ) .into(); let fut = async move { - let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= prev_transaction.outputs[0].value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + prev_transaction.outputs[0].value + ); + } let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { value: prev_transaction.outputs[0].value - fee, @@ -1283,6 +1629,25 @@ pub fn send_taker_refunds_payment( Box::new(fut.boxed().compat()) } +pub fn send_taker_payment_refund_preimage( + coin: &T, + watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, +) -> TransactionFut { + let coin = coin.clone(); + let transaction: UtxoTx = try_tx_fus!( + deserialize(watcher_refunds_payment_args.payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e))) + ); + + let fut = async move { + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); + try_tx_s!(tx_fut.await, transaction); + + Ok(transaction.into()) + }; + + Box::new(fut.boxed().compat()) +} + pub fn send_maker_refunds_payment( coin: T, maker_payment_tx: &[u8], @@ -1291,9 +1656,13 @@ pub fn send_maker_refunds_payment( secret_hash: &[u8], swap_unique_data: &[u8], ) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_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; + drop_mutability!(prev_transaction); + if prev_transaction.outputs.is_empty() { + return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); + } let key_pair = coin.derive_htlc_key_pair(swap_unique_data); let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); @@ -1305,7 +1674,17 @@ pub fn send_maker_refunds_payment( ) .into(); let fut = async move { - let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= prev_transaction.outputs[0].value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + prev_transaction.outputs[0].value + ); + } let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { value: prev_transaction.outputs[0].value - fee, @@ -1399,13 +1778,21 @@ where } } -pub fn check_all_inputs_signed_by_pub(tx: &UtxoTx, expected_pub: &[u8]) -> Result { +pub fn check_all_inputs_signed_by_pub(tx: &[u8], expected_pub: &[u8]) -> Result> { + let tx: UtxoTx = deserialize(tx)?; + check_all_utxo_inputs_signed_by_pub(&tx, expected_pub) +} + +pub fn check_all_utxo_inputs_signed_by_pub( + tx: &UtxoTx, + expected_pub: &[u8], +) -> Result> { for input in &tx.inputs { let pubkey = if input.has_witness() { - try_s!(pubkey_from_witness_script(&input.script_witness)) + pubkey_from_witness_script(&input.script_witness).map_to_mm(ValidatePaymentError::TxDeserializationError)? } else { let script: Script = input.script_sig.clone().into(); - try_s!(pubkey_from_script_sig(&script)) + pubkey_from_script_sig(&script).map_to_mm(ValidatePaymentError::TxDeserializationError)? }; if *pubkey != expected_pub { return Ok(false); @@ -1415,6 +1802,100 @@ pub fn check_all_inputs_signed_by_pub(tx: &UtxoTx, expected_pub: &[u8]) -> Resul Ok(true) } +pub fn watcher_validate_taker_fee( + coin: &T, + input: WatcherValidateTakerFeeInput, + output_index: usize, +) -> ValidatePaymentFut<()> { + let coin = coin.clone(); + let sender_pubkey = input.sender_pubkey; + let taker_fee_hash = input.taker_fee_hash; + let min_block_number = input.min_block_number; + let lock_duration = input.lock_duration; + let fee_addr = input.fee_addr.to_vec(); + + let fut = async move { + let mut attempts = 0; + loop { + let tx_from_rpc = match coin + .as_ref() + .rpc_client + .get_verbose_transaction(&H256Json::from(taker_fee_hash.as_slice())) + .compat() + .await + { + Ok(t) => t, + Err(e) => { + if attempts > 2 { + return MmError::err(ValidatePaymentError::from(e.into_inner())); + }; + attempts += 1; + error!("Error getting tx {:?} from rpc: {:?}", taker_fee_hash, e); + Timer::sleep(10.).await; + continue; + }, + }; + + let taker_fee_tx: UtxoTx = deserialize(tx_from_rpc.hex.0.as_slice())?; + let inputs_signed_by_pub = check_all_utxo_inputs_signed_by_pub(&taker_fee_tx, &sender_pubkey)?; + if !inputs_signed_by_pub { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Taker fee does not belong to the verified public key", + INVALID_SENDER_ERR_LOG + ))); + } + + let tx_confirmed_before_block = is_tx_confirmed_before_block(&coin, &tx_from_rpc, min_block_number) + .await + .map_to_mm(ValidatePaymentError::InternalError)?; + if tx_confirmed_before_block { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Fee tx {:?} confirmed before min_block {}", + EARLY_CONFIRMATION_ERR_LOG, taker_fee_tx, min_block_number + ))); + } + + if (now_ms() / 1000) as u32 - taker_fee_tx.lock_time > lock_duration as u32 { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Taker fee {:?} is too old", + OLD_TRANSACTION_ERR_LOG, taker_fee_tx + ))); + } + + let address = address_from_raw_pubkey( + &fee_addr, + 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.addr_format().clone(), + ) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + + match taker_fee_tx.outputs.get(output_index) { + Some(out) => { + let expected_script_pubkey = Builder::build_p2pkh(&address.hash).to_bytes(); + if out.script_pubkey != expected_script_pubkey { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", + INVALID_RECEIVER_ERR_LOG, out.script_pubkey, expected_script_pubkey + ))); + } + }, + None => { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided dex fee tx {:?} does not have output {}", + taker_fee_tx, output_index + ))) + }, + } + + return Ok(()); + } + }; + Box::new(fut.boxed().compat()) +} + pub fn validate_fee( coin: T, tx: UtxoTx, @@ -1434,7 +1915,7 @@ pub fn validate_fee( coin.addr_format().clone(), )); - if !try_fus!(check_all_inputs_signed_by_pub(&tx, sender_pubkey)) { + if !try_fus!(check_all_utxo_inputs_signed_by_pub(&tx, sender_pubkey)) { return Box::new(futures01::future::err(ERRL!("The dex fee was sent from wrong address"))); } let fut = async move { @@ -1494,16 +1975,19 @@ pub fn validate_fee( pub fn validate_maker_payment( coin: &T, input: ValidatePaymentInput, -) -> Box + Send> { - let mut tx: UtxoTx = try_fus!(deserialize(input.payment_tx.as_slice()).map_err(|e| ERRL!("{:?}", e))); +) -> ValidatePaymentFut<()> { + let mut tx: UtxoTx = try_f!(deserialize(input.payment_tx.as_slice())); tx.tx_hash_algo = coin.as_ref().tx_hash_algo; let htlc_keypair = coin.derive_htlc_key_pair(&input.unique_swap_data); + let other_pub = + &try_f!(Public::from_slice(&input.other_pub) + .map_to_mm(|err| ValidatePaymentError::InvalidParameter(err.to_string()))); validate_payment( coin.clone(), tx, DEFAULT_SWAP_VOUT, - &try_fus!(Public::from_slice(&input.other_pub)), + other_pub, htlc_keypair.public(), &input.secret_hash, input.amount, @@ -1513,19 +1997,99 @@ pub fn validate_maker_payment( ) } +pub fn watcher_validate_taker_payment( + coin: &T, + input: WatcherValidatePaymentInput, +) -> ValidatePaymentFut<()> { + let taker_payment_tx: UtxoTx = try_f!(deserialize(input.payment_tx.as_slice())); + let taker_payment_refund_preimage: UtxoTx = try_f!(deserialize(input.taker_payment_refund_preimage.as_slice())); + let taker_pub = &try_f!( + Public::from_slice(&input.taker_pub).map_err(|err| ValidatePaymentError::InvalidParameter(err.to_string())) + ); + let maker_pub = &try_f!( + Public::from_slice(&input.maker_pub).map_err(|err| ValidatePaymentError::InvalidParameter(err.to_string())) + ); + let expected_redeem = payment_script(input.time_lock, &input.secret_hash, taker_pub, maker_pub); + let coin = coin.clone(); + + let fut = async move { + let inputs_signed_by_pub = check_all_utxo_inputs_signed_by_pub(&taker_payment_tx, &input.taker_pub)?; + if !inputs_signed_by_pub { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Taker payment does not belong to the verified public key".to_string(), + )); + } + + let taker_payment_locking_script = match taker_payment_tx.outputs.get(DEFAULT_SWAP_VOUT) { + Some(output) => output.script_pubkey.clone(), + None => { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Payment tx has no outputs".to_string(), + )) + }, + }; + + if taker_payment_locking_script != Builder::build_p2sh(&dhash160(&expected_redeem).into()).to_bytes() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx locking script {:?} doesn't match expected", + taker_payment_locking_script + ))); + } + + let script_sig = match taker_payment_refund_preimage.inputs.get(DEFAULT_SWAP_VIN) { + Some(input) => Script::from(input.script_sig.clone()), + None => { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Taker payment refund tx has no inputs".to_string(), + )) + }, + }; + + let instruction = script_sig + .iter() + .last() + .or_mm_err(|| ValidatePaymentError::WrongPaymentTx(String::from("Instruction not found")))? + .map_to_mm(|err| ValidatePaymentError::WrongPaymentTx(err.to_string()))?; + + let redeem_script = instruction.data.or_mm_err(|| { + ValidatePaymentError::WrongPaymentTx(String::from("No redeem script in the taker payment refund preimage")) + })?; + + if expected_redeem.as_slice() != redeem_script { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Taker payment tx locking script doesn't match with taker payment refund redeem script".to_string(), + )); + } + + if let UtxoRpcClientEnum::Electrum(client) = &coin.as_ref().rpc_client { + if coin.as_ref().conf.spv_conf.is_some() && input.confirmations != 0 { + client + .validate_spv_proof(&taker_payment_tx, input.try_spv_proof_until) + .await?; + } + } + Ok(()) + }; + Box::new(fut.boxed().compat()) +} + pub fn validate_taker_payment( coin: &T, input: ValidatePaymentInput, -) -> Box + Send> { - let mut tx: UtxoTx = try_fus!(deserialize(input.payment_tx.as_slice()).map_err(|e| ERRL!("{:?}", e))); +) -> ValidatePaymentFut<()> { + let mut tx: UtxoTx = try_f!(deserialize(input.payment_tx.as_slice())); tx.tx_hash_algo = coin.as_ref().tx_hash_algo; let htlc_keypair = coin.derive_htlc_key_pair(&input.unique_swap_data); + let other_pub = + &try_f!(Public::from_slice(&input.other_pub) + .map_to_mm(|err| ValidatePaymentError::InvalidParameter(err.to_string()))); + validate_payment( coin.clone(), tx, DEFAULT_SWAP_VOUT, - &try_fus!(Public::from_slice(&input.other_pub)), + other_pub, htlc_keypair.public(), &input.secret_hash, input.amount, @@ -1596,6 +2160,24 @@ pub fn check_if_my_payment_sent( Box::new(fut.boxed().compat()) } +pub async fn watcher_search_for_swap_tx_spend + SwapOps>( + coin: &T, + input: WatcherSearchForSwapTxSpendInput<'_>, + output_index: usize, +) -> Result, String> { + search_for_swap_output_spend( + coin.as_ref(), + input.time_lock, + &try_s!(Public::from_slice(input.taker_pub)), + &try_s!(Public::from_slice(input.maker_pub)), + input.secret_hash, + input.tx, + output_index, + input.search_from_block, + ) + .await +} + pub async fn search_for_swap_tx_spend_my + SwapOps>( coin: &T, input: SearchForSwapTxSpendInput<'_>, @@ -1668,11 +2250,16 @@ pub fn extract_secret(secret_hash: &[u8], spend_tx: &[u8]) -> Result, St }, }; - let actual_secret_hash = &*dhash160(&secret); - if actual_secret_hash != secret_hash { + let expected_secret_hash = if secret_hash.len() == 32 { + ripemd160(secret_hash) + } else { + H160::from(secret_hash) + }; + let actual_secret_hash = dhash160(&secret); + if actual_secret_hash != expected_secret_hash { warn!( - "Invalid 'dhash160(secret)' {:?}, expected {:?}", - actual_secret_hash, secret_hash + "Invalid secret hash {:?}, expected {:?}", + actual_secret_hash, expected_secret_hash ); continue; } @@ -1681,10 +2268,14 @@ 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) -> MmResult { 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"), + DerivationMethod::SingleAddress(ref my_address) => { + my_address.display_address().map_to_mm(MyAddressError::InternalError) + }, + DerivationMethod::HDWallet(_) => MmError::err(MyAddressError::UnexpectedDerivationMethod( + "'my_address' is deprecated for HD wallets".to_string(), + )), } } @@ -1718,7 +2309,7 @@ pub fn verify_message( 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)?; + let received_address = checked_address_from_str(coin, address)?; Ok(AddressHashEnum::from(recovered_pubkey.address_hash()) == received_address.hash) } @@ -1729,7 +2320,7 @@ where let my_address = try_f!(coin .as_ref() .derivation_method - .iguana_or_err() + .single_addr_or_err() .mm_err(BalanceError::from)) .clone(); let fut = async move { address_balance(&coin, &my_address).await }; @@ -1786,6 +2377,7 @@ pub fn wait_for_output_spend( output_index: usize, from_block: u64, wait_until: u64, + check_every: f64, ) -> TransactionFut { let mut tx: UtxoTx = try_tx_fus!(deserialize(tx_bytes).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.tx_hash_algo; @@ -1820,14 +2412,24 @@ pub fn wait_for_output_spend( output_index, ); } - Timer::sleep(10.).await; + Timer::sleep(check_every).await; } }; Box::new(fut.boxed().compat()) } -pub fn tx_enum_from_bytes(coin: &UtxoCoinFields, bytes: &[u8]) -> Result { - let mut transaction: UtxoTx = try_s!(deserialize(bytes).map_err(|err| format!("{:?}", err))); +pub fn tx_enum_from_bytes(coin: &UtxoCoinFields, bytes: &[u8]) -> Result> { + let mut transaction: UtxoTx = deserialize(bytes).map_to_mm(|e| TxMarshalingErr::InvalidInput(e.to_string()))?; + + let serialized_length = transaction.tx_hex().len(); + if bytes.len() != serialized_length { + return MmError::err(TxMarshalingErr::CrossCheckFailed(format!( + "Expected '{}' lenght of the serialized transaction, found '{}'", + bytes.len(), + serialized_length + ))); + } + transaction.tx_hash_algo = coin.tx_hash_algo; Ok(transaction.into()) } @@ -1868,6 +2470,16 @@ pub async fn get_raw_transaction(coin: &UtxoCoinFields, req: RawTransactionReque Ok(RawTransactionRes { tx_hex: hex }) } +pub async fn get_tx_hex_by_hash(coin: &UtxoCoinFields, tx_hash: Vec) -> RawTransactionResult { + let hex = coin + .rpc_client + .get_transaction_bytes(&H256Json::from(tx_hash.as_slice())) + .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: UtxoCommonOps + GetUtxoListOps + MarketCoinOps, @@ -1901,11 +2513,12 @@ where + UtxoCommonOps, { match coin.derivation_method() { - DerivationMethod::Iguana(my_address) => get_withdraw_iguana_sender(coin, req, my_address), + DerivationMethod::SingleAddress(my_address) => get_withdraw_iguana_sender(coin, req, my_address), DerivationMethod::HDWallet(hd_wallet) => get_withdraw_hd_sender(coin, req, hd_wallet).await, } } +#[allow(clippy::result_large_err)] pub fn get_withdraw_iguana_sender( coin: &T, req: &WithdrawRequest, @@ -1931,17 +2544,17 @@ pub async fn get_withdraw_hd_sender( hd_wallet: &T::HDWallet, ) -> MmResult, WithdrawError> where - T: HDWalletCoinOps
, + T: HDWalletCoinOps
+ Sync, { - let HDAddressId { + let HDAccountAddressId { 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) + let derivation_path = StandardHDPath::from_str(&derivation_path) + .map_to_mm(StandardHDPathError::from) .mm_err(|e| WithdrawError::UnexpectedFromAddress(e.to_string()))?; let coin_type = derivation_path.coin_type(); let expected_coin_type = hd_wallet.coin_type(); @@ -1952,7 +2565,7 @@ where ); return MmError::err(WithdrawError::UnexpectedFromAddress(error)); } - HDAddressId::from(derivation_path) + HDAccountAddressId::from(derivation_path) }, }; @@ -1960,7 +2573,7 @@ where .get_account(account_id) .await .or_mm_err(|| WithdrawError::UnknownAccount { account_id })?; - let hd_address = coin.derive_address(&hd_account, chain, address_id)?; + let hd_address = coin.derive_address(&hd_account, chain, address_id).await?; let is_address_activated = hd_account .is_address_activated(chain, address_id) @@ -2009,7 +2622,7 @@ pub fn validate_address(coin: &T, address: &str) -> ValidateAd Err(e) => { return ValidateAddressResult { is_valid: false, - reason: Some(e), + reason: Some(e.to_string()), } }, }; @@ -2033,11 +2646,85 @@ pub fn validate_address(coin: &T, address: &str) -> ValidateAd } } +// Quick fix for null valued coin fields in fee details of old tx history entries +#[cfg(not(target_arch = "wasm32"))] +async fn tx_history_migration_1(coin: &T, ctx: &MmArc) +where + T: UtxoStandardOps + UtxoCommonOps + MmCoin + MarketCoinOps, +{ + const MIGRATION_NUMBER: u64 = 1; + let history = match coin.load_history_from_file(ctx).compat().await { + Ok(history) => history, + Err(e) => { + log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "Error {} on 'load_history_from_file', stop the history loop", e + ); + return; + }, + }; + + let mut updated = false; + let to_write: Vec = history + .into_iter() + .filter_map(|mut tx| match tx.fee_details { + Some(TxFeeDetails::Utxo(ref mut fee_details)) => { + if fee_details.coin.is_none() { + fee_details.coin = Some(String::from(&tx.coin)); + updated = true; + } + Some(tx) + }, + Some(_) => None, + None => Some(tx), + }) + .collect(); + + if updated { + 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'", e + ); + return; + }; + } + if let Err(e) = coin.update_migration_file(ctx, MIGRATION_NUMBER).compat().await { + log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "Error {} on 'update_migration_file'", e + ); + }; +} + +#[cfg(not(target_arch = "wasm32"))] +async fn migrate_tx_history(coin: &T, ctx: &MmArc) +where + T: UtxoStandardOps + UtxoCommonOps + MmCoin + MarketCoinOps, +{ + let current_migration = coin.get_tx_history_migration(ctx).compat().await.unwrap_or(0); + if current_migration < 1 { + tx_history_migration_1(coin, ctx).await; + } +} + #[allow(clippy::cognitive_complexity)] pub async fn process_history_loop(coin: T, ctx: MmArc) where T: UtxoStandardOps + UtxoCommonOps + MmCoin + MarketCoinOps, { + #[cfg(not(target_arch = "wasm32"))] + migrate_tx_history(&coin, &ctx).await; + let mut my_balance: Option = None; let history = match coin.load_history_from_file(&ctx).compat().await { Ok(history) => history, @@ -2145,7 +2832,7 @@ where 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(); + let to_write: Vec = history_map.values().cloned().collect(); if let Err(e) = coin.save_history_to_file(&ctx, to_write).compat().await { log_tag!( ctx, @@ -2222,7 +2909,7 @@ where }, } if updated { - let to_write: Vec = history_map.iter().map(|(_, value)| value.clone()).collect(); + let to_write: Vec = history_map.values().cloned().collect(); if let Err(e) = coin.save_history_to_file(&ctx, to_write).compat().await { log_tag!( ctx, @@ -2309,7 +2996,7 @@ where .collect() }, UtxoRpcClientEnum::Electrum(client) => { - let my_address = match coin.as_ref().derivation_method.iguana_or_err() { + let my_address = match coin.as_ref().derivation_method.single_addr_or_err() { Ok(my_address) => my_address, Err(e) => return RequestTxHistoryResult::CriticalError(e.to_string()), }; @@ -2323,8 +3010,9 @@ where Ok(value) => value, Err(e) => match &e.error { JsonRpcErrorType::InvalidRequest(e) + | JsonRpcErrorType::Parse(_, e) | JsonRpcErrorType::Transport(e) - | JsonRpcErrorType::Parse(_, e) => { + | JsonRpcErrorType::Internal(e) => { return RequestTxHistoryResult::Retry { error: ERRL!("Error {} on scripthash_get_history", e), }; @@ -2371,7 +3059,7 @@ pub async fn tx_details_by_hash( 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()); + let my_address = try_s!(coin.as_ref().derivation_method.single_addr_or_err()); input_transactions.insert(hash, HistoryUtxoTx { tx: tx.clone(), @@ -2499,6 +3187,7 @@ pub async fn tx_details_by_hash( timestamp: verbose_tx.time.into(), kmd_rewards, transaction_type: Default::default(), + memo: None, }) } @@ -2566,7 +3255,7 @@ where })); } - let my_address = &coin.my_address().map_to_mm(UtxoRpcError::Internal)?; + let my_address = &coin.my_address()?; let claimed_by_me = tx_details.from.iter().all(|from| from == my_address) && tx_details.to.contains(my_address); tx_details.kmd_rewards = Some(KmdRewardsDetails { @@ -2657,7 +3346,7 @@ where 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()?; + let my_address = coin.as_ref().derivation_method.single_addr_or_err()?; match tx_fee { // if it's a dynamic fee, we should generate a swap transaction to get an actual trade fee @@ -2752,7 +3441,7 @@ where // `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) + 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 @@ -2768,7 +3457,7 @@ where /// 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 { let fut = async move { - let amount_sat = get_htlc_spend_fee(&coin, DEFAULT_SWAP_TX_SPEND_SIZE).await?; + let amount_sat = get_htlc_spend_fee(&coin, DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox).await?; let amount = big_decimal_from_sat_unsigned(amount_sat, coin.as_ref().decimals).into(); Ok(TradeFee { coin: coin.as_ref().conf.ticker.clone(), @@ -3031,6 +3720,10 @@ pub async fn get_verbose_transactions_from_cache_or_rpc( #[inline] pub fn swap_contract_address() -> Option { None } +/// Fallback swap contract address is not used by standard UTXO coins. +#[inline] +pub fn fallback_swap_contract() -> Option { None } + /// Convert satoshis to BigDecimal amount of coin units #[inline] pub fn big_decimal_from_sat(satoshis: i64, decimals: u8) -> BigDecimal { @@ -3078,130 +3771,6 @@ pub fn address_from_pubkey( } } -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)] #[cfg_attr(test, mockable)] pub fn validate_payment( @@ -3215,70 +3784,53 @@ pub fn validate_payment( time_lock: u32, try_spv_proof_until: u64, confirmations: u64, -) -> Box + Send> { - let amount = try_fus!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); +) -> ValidatePaymentFut<()> { + let amount = try_f!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); let expected_redeem = payment_script(time_lock, priv_bn_hash, first_pub0, second_pub0); let fut = async move { - let mut attempts = 0; - loop { - let tx_from_rpc = match coin - .as_ref() - .rpc_client - .get_transaction_bytes(&tx.hash().reversed().into()) - .compat() - .await - { - Ok(t) => t, - Err(e) => { - if attempts > 2 { - return ERR!( - "Got error {:?} after 3 attempts of getting tx {:?} from RPC", - e, - tx.tx_hash() - ); - }; - attempts += 1; - error!("Error getting tx {:?} from rpc: {:?}", tx.tx_hash(), e); - Timer::sleep(10.).await; - continue; - }, - }; - if serialize(&tx).take() != tx_from_rpc.0 - && serialize_with_flags(&tx, SERIALIZE_TRANSACTION_WITNESS).take() != tx_from_rpc.0 - { - return ERR!( - "Provided payment tx {:?} doesn't match tx data from rpc {:?}", - tx, - tx_from_rpc - ); - } + let tx_hash = tx.tx_hash(); - let expected_output = TransactionOutput { - value: amount, - script_pubkey: Builder::build_p2sh(&dhash160(&expected_redeem).into()).into(), - }; + let tx_from_rpc = retry_on_err!(coin + .as_ref() + .rpc_client + .get_transaction_bytes(&tx.hash().reversed().into()) + .compat()) + .repeat_every_secs(10.) + .attempts(4) + .inspect_err(move |e| error!("Error getting tx {tx_hash:?} from rpc: {e:?}")) + .await + .map_err(|repeat_err| repeat_err.into_error().map(ValidatePaymentError::from))?; - let actual_output = tx.outputs.get(output_index); - if actual_output != Some(&expected_output) { - return ERR!( - "Provided payment tx output doesn't match expected {:?} {:?}", - actual_output, - expected_output - ); - } + if serialize(&tx).take() != tx_from_rpc.0 + && serialize_with_flags(&tx, SERIALIZE_TRANSACTION_WITNESS).take() != tx_from_rpc.0 + { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided payment tx {:?} doesn't match tx data from rpc {:?}", + tx, tx_from_rpc + ))); + } - if !coin.as_ref().conf.enable_spv_proof { - return Ok(()); - } + let expected_output = TransactionOutput { + value: amount, + script_pubkey: Builder::build_p2sh(&dhash160(&expected_redeem).into()).into(), + }; - return match confirmations { - 0 => Ok(()), - _ => validate_spv_proof(coin, tx, try_spv_proof_until) - .await - .map_err(|e| format!("{:?}", e)), - }; + let actual_output = tx.outputs.get(output_index); + if actual_output != Some(&expected_output) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided payment tx output doesn't match expected {:?} {:?}", + actual_output, expected_output + ))); } + + if let UtxoRpcClientEnum::Electrum(client) = &coin.as_ref().rpc_client { + if coin.as_ref().conf.spv_conf.is_some() && confirmations != 0 { + client.validate_spv_proof(&tx, try_spv_proof_until).await?; + } + } + + Ok(()) }; Box::new(fut.boxed().compat()) } @@ -3296,6 +3848,10 @@ async fn search_for_swap_output_spend( ) -> Result, String> { let mut tx: UtxoTx = try_s!(deserialize(tx).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.tx_hash_algo; + drop_mutability!(tx); + if tx.outputs.is_empty() { + return ERR!("Transaction doesn't have any output"); + } let script = payment_script(time_lock, secret_hash, first_pub, second_pub); let expected_script_pubkey = Builder::build_p2sh(&dhash160(&script).into()).to_bytes(); if tx.outputs[0].script_pubkey != expected_script_pubkey { @@ -3321,7 +3877,7 @@ async fn search_for_swap_output_spend( 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(); + let script: Script = tx.inputs[DEFAULT_SWAP_VIN].script_sig.clone().into(); if let Some(Ok(ref i)) = script.iter().nth(2) { if i.opcode == Opcode::OP_0 { return Ok(Some(FoundSwapTxSpend::Spent(tx.into()))); @@ -3406,8 +3962,7 @@ where } pub fn payment_script(time_lock: u32, secret_hash: &[u8], pub_0: &Public, pub_1: &Public) -> Script { - let builder = Builder::default(); - builder + let mut builder = Builder::default() .push_opcode(Opcode::OP_IF) .push_bytes(&time_lock.to_le_bytes()) .push_opcode(Opcode::OP_CHECKLOCKTIMEVERIFY) @@ -3418,8 +3973,15 @@ pub fn payment_script(time_lock: u32, secret_hash: &[u8], pub_0: &Public, pub_1: .push_opcode(Opcode::OP_SIZE) .push_bytes(&[32]) .push_opcode(Opcode::OP_EQUALVERIFY) - .push_opcode(Opcode::OP_HASH160) - .push_bytes(secret_hash) + .push_opcode(Opcode::OP_HASH160); + + if secret_hash.len() == 32 { + builder = builder.push_bytes(ripemd160(secret_hash).as_slice()); + } else { + builder = builder.push_bytes(secret_hash); + } + + builder .push_opcode(Opcode::OP_EQUALVERIFY) .push_bytes(pub_1) .push_opcode(Opcode::OP_CHECKSIG) @@ -3543,6 +4105,8 @@ where FeeApproxStage::WithoutApprox => return dynamic_fee, // Take into account that the dynamic fee may increase during the swap by [`UtxoCoinFields::tx_fee_volatility_percent`]. FeeApproxStage::StartSwap => base_percent, + // Take into account that the dynamic fee may increase until the watcher can spend it [`UtxoCoinFields::tx_fee_volatility_percent`]. + FeeApproxStage::WatcherPreimage => base_percent, //This needs discussion // Take into account that the dynamic fee may increase at each of the following stages up to [`UtxoCoinFields::tx_fee_volatility_percent`]: // - until a swap is started; // - during the swap. @@ -3561,187 +4125,6 @@ fn increase_by_percent(num: u64, percent: f64) -> u64 { num + (percent.round() as u64) } -pub async fn valid_block_header_from_storage( - coin: &T, - height: u64, - storage: &BlockHeaderStorage, - client: &ElectrumClient, -) -> Result> -where - 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) => 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.get_unspent_ordered_list(my_address).await { - Ok((unspents, recently_spent)) => (unspents, recently_spent), - Err(e) => { - error!("Error {} on get_unspent_ordered_list of coin {}", e, ticker); - continue; - }, - }; - if unspents.len() >= merge_at { - 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(&my_address.hash).to_bytes(); - let output = TransactionOutput { value, script_pubkey }; - let merge_tx_fut = generate_and_send_tx( - &coin, - unspents, - None, - FeePolicy::DeductFromOutput(0), - recently_spent, - vec![output], - ); - match merge_tx_fut.await { - Ok(tx) => info!( - "UTXO merge successful for coin {}, tx_hash {:?}", - ticker, - tx.hash().reversed() - ), - Err(e) => error!("Error {:?} on UTXO merge attempt for coin {}", e, ticker), - } - } - } -} - pub async fn can_refund_htlc(coin: &T, locktime: u64) -> Result> where T: UtxoCommonOps, @@ -3749,7 +4132,7 @@ where let now = now_ms() / 1000; if now < locktime { let to_wait = locktime - now + 1; - return Ok(CanRefundHtlc::HaveToWait(to_wait.max(3600))); + return Ok(CanRefundHtlc::HaveToWait(to_wait.min(3600))); } let mtp = coin.get_current_mtp().await?; @@ -3759,7 +4142,7 @@ where Ok(CanRefundHtlc::CanRefundNow) } else { let to_wait = (locktime - mtp + 1) as u64; - Ok(CanRefundHtlc::HaveToWait(to_wait.max(3600))) + Ok(CanRefundHtlc::HaveToWait(to_wait.min(3600))) } } @@ -3777,7 +4160,7 @@ where pub fn addr_format(coin: &dyn AsRef) -> &UtxoAddressFormat { match coin.as_ref().derivation_method { - DerivationMethod::Iguana(ref my_address) => &my_address.addr_format, + DerivationMethod::SingleAddress(ref my_address) => &my_address.addr_format, DerivationMethod::HDWallet(UtxoHDWallet { ref address_format, .. }) => address_format, } } @@ -3789,7 +4172,7 @@ pub fn addr_format_for_standard_scripts(coin: &dyn AsRef) -> Utx } } -fn check_withdraw_address_supported(coin: &T, addr: &Address) -> Result<(), MmError> +fn check_withdraw_address_supported(coin: &T, addr: &Address) -> MmResult<(), UnsupportedAddr> where T: UtxoCommonOps, { @@ -3847,6 +4230,7 @@ where .mm_err(From::from) } +#[inline] pub fn derive_htlc_key_pair(coin: &UtxoCoinFields, _swap_unique_data: &[u8]) -> KeyPair { match coin.priv_key_policy { PrivKeyPolicy::KeyPair(k) => k, @@ -3854,6 +4238,18 @@ pub fn derive_htlc_key_pair(coin: &UtxoCoinFields, _swap_unique_data: &[u8]) -> } } +#[inline] +pub fn derive_htlc_pubkey(coin: &dyn SwapOps, swap_unique_data: &[u8]) -> Vec { + coin.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() +} + +pub fn validate_other_pubkey(raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + if let Err(err) = Public::from_slice(raw_pubkey) { + return MmError::err(ValidateOtherPubKeyErr::InvalidPubKey(err.to_string())); + }; + Ok(()) +} + /// Sorts and deduplicates the given `unspents` in ascending order. fn sort_dedup_unspents(unspents: I) -> Vec where diff --git a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs new file mode 100644 index 0000000000..8e6c8dcdd3 --- /dev/null +++ b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs @@ -0,0 +1,417 @@ +use crate::coin_balance::CoinBalanceReportOps; +use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, DisplayAddress, MyTxHistoryErrorV2, MyTxHistoryTarget, + TxDetailsBuilder, TxHistoryStorage}; +use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; +use crate::utxo::rpc_clients::{electrum_script_hash, ElectrumClient, NativeClient, UtxoRpcClientEnum}; +use crate::utxo::utxo_common::{big_decimal_from_sat, HISTORY_TOO_LARGE_ERROR}; +use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, + UtxoTxHistoryOps}; +use crate::utxo::{output_script, RequestTxHistoryResult, UtxoCoinFields, UtxoCommonOps, UtxoHDAccount}; +use crate::{big_decimal_from_sat_unsigned, compare_transactions, BalanceResult, CoinWithDerivationMethod, + DerivationMethod, HDAccountAddressId, MarketCoinOps, TransactionDetails, TxFeeDetails, TxIdHeight, + UtxoFeeDetails, UtxoTx}; +use common::jsonrpc_client::JsonRpcErrorType; +use crypto::Bip44Chain; +use futures::compat::Future01CompatExt; +use itertools::Itertools; +use keys::{Address, Type as ScriptType}; +use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; +use rpc::v1::types::{TransactionInputEnum, H256 as H256Json}; +use serialization::deserialize; +use std::collections::{HashMap, HashSet}; +use std::convert::TryFrom; +use std::iter; + +/// [`CoinWithTxHistoryV2::history_wallet_id`] implementation. +pub fn history_wallet_id(coin: &UtxoCoinFields) -> WalletId { WalletId::new(coin.conf.ticker.clone()) } + +/// [`CoinWithTxHistoryV2::get_tx_history_filters`] implementation. +/// Returns `GetTxHistoryFilters` according to the derivation method. +pub async fn get_tx_history_filters( + coin: &Coin, + target: MyTxHistoryTarget, +) -> MmResult +where + Coin: CoinWithDerivationMethod::HDWallet> + + HDWalletCoinOps + + MarketCoinOps + + Sync, + ::Address: DisplayAddress, +{ + match (coin.derivation_method(), target) { + (DerivationMethod::SingleAddress(_), MyTxHistoryTarget::Iguana) => { + let my_address = coin.my_address()?; + Ok(GetTxHistoryFilters::for_address(my_address)) + }, + (DerivationMethod::SingleAddress(_), target) => { + MmError::err(MyTxHistoryErrorV2::with_expected_target(target, "Iguana")) + }, + (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AccountId { account_id }) => { + get_tx_history_filters_for_hd_account(coin, hd_wallet, account_id).await + }, + (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AddressId(hd_address_id)) => { + get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await + }, + (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AddressDerivationPath(derivation_path)) => { + let hd_address_id = HDAccountAddressId::from(derivation_path); + get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await + }, + (DerivationMethod::HDWallet(_), target) => MmError::err(MyTxHistoryErrorV2::with_expected_target( + target, + "an HD account/address", + )), + } +} + +/// `get_tx_history_filters` function's helper. +async fn get_tx_history_filters_for_hd_account( + coin: &Coin, + hd_wallet: &Coin::HDWallet, + account_id: u32, +) -> MmResult +where + Coin: HDWalletCoinOps + Sync, + Coin::Address: DisplayAddress, +{ + let hd_account = hd_wallet + .get_account(account_id) + .await + .or_mm_err(|| MyTxHistoryErrorV2::InvalidTarget(format!("No such account_id={account_id}")))?; + + let external_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::External).await?; + let internal_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::Internal).await?; + + let addresses_iter = external_addresses + .into_iter() + .chain(internal_addresses) + .map(|hd_address| DisplayAddress::display_address(&hd_address.address)); + Ok(GetTxHistoryFilters::for_addresses(addresses_iter)) +} + +/// `get_tx_history_filters` function's helper. +async fn get_tx_history_filters_for_hd_address( + coin: &Coin, + hd_wallet: &Coin::HDWallet, + hd_address_id: HDAccountAddressId, +) -> MmResult +where + Coin: HDWalletCoinOps + Sync, + Coin::Address: DisplayAddress, +{ + let hd_account = hd_wallet + .get_account(hd_address_id.account_id) + .await + .or_mm_err(|| MyTxHistoryErrorV2::InvalidTarget(format!("No such account_id={}", hd_address_id.account_id)))?; + + let is_address_activated = hd_account.is_address_activated(hd_address_id.chain, hd_address_id.address_id)?; + if !is_address_activated { + let error = format!( + "'{:?}:{}' address is not activated", + hd_address_id.chain, hd_address_id.address_id + ); + return MmError::err(MyTxHistoryErrorV2::InvalidTarget(error)); + } + + let hd_address = coin + .derive_address(&hd_account, hd_address_id.chain, hd_address_id.address_id) + .await?; + Ok(GetTxHistoryFilters::for_address(hd_address.address.display_address())) +} + +/// [`UtxoTxHistoryOps::my_addresses`] implementation. +pub async fn my_addresses(coin: &Coin) -> MmResult, UtxoMyAddressesHistoryError> +where + Coin: HDWalletCoinOps
+ UtxoCommonOps, +{ + const ADDRESSES_CAPACITY: usize = 60; + + match coin.as_ref().derivation_method { + DerivationMethod::SingleAddress(ref my_address) => Ok(iter::once(my_address.clone()).collect()), + DerivationMethod::HDWallet(ref hd_wallet) => { + let hd_accounts = hd_wallet.get_accounts().await; + + let mut all_addresses = HashSet::with_capacity(ADDRESSES_CAPACITY); + for (_, hd_account) in hd_accounts { + let external_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::External).await?; + let internal_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::Internal).await?; + + let addresses_it = external_addresses + .into_iter() + .chain(internal_addresses) + .map(|hd_address| hd_address.address); + all_addresses.extend(addresses_it); + } + + Ok(all_addresses) + }, + } +} + +/// [`UtxoTxHistoryOps::tx_details_by_hash`] implementation. +pub async fn tx_details_by_hash( + coin: &Coin, + params: UtxoTxDetailsParams<'_, Storage>, +) -> MmResult, UtxoTxDetailsError> +where + Coin: UtxoTxHistoryOps + UtxoCommonOps + MarketCoinOps, + Storage: TxHistoryStorage, +{ + let ticker = coin.ticker(); + let decimals = coin.as_ref().decimals; + + let verbose_tx = coin + .as_ref() + .rpc_client + .get_verbose_transaction(params.hash) + .compat() + .await?; + let tx: UtxoTx = deserialize(verbose_tx.hex.as_slice())?; + + let mut tx_builder = TxDetailsBuilder::new( + ticker.to_string(), + &tx, + params.block_height_and_time, + params.my_addresses.clone(), + ); + + let mut input_amount = 0; + let mut output_amount = 0; + + for input in tx.inputs.iter() { + // input transaction is zero if the tx is the coinbase transaction + if input.previous_output.hash.is_zero() { + continue; + } + + let prev_tx_hash: H256Json = input.previous_output.hash.reversed().into(); + + let prev_tx = coin.tx_from_storage_or_rpc(&prev_tx_hash, params.storage).await?; + + let prev_output_index = input.previous_output.index as usize; + let prev_tx_value = prev_tx.outputs[prev_output_index].value; + let prev_script = prev_tx.outputs[prev_output_index].script_pubkey.clone().into(); + + input_amount += prev_tx_value; + let amount = big_decimal_from_sat_unsigned(prev_tx_value, decimals); + + let from: Vec
= coin + .addresses_from_script(&prev_script) + .map_to_mm(UtxoTxDetailsError::TxAddressDeserializationError)?; + for address in from { + tx_builder.transferred_from(address, &amount); + } + } + + for output in tx.outputs.iter() { + let output_script = output.script_pubkey.clone().into(); + let to = coin + .addresses_from_script(&output_script) + .map_to_mm(UtxoTxDetailsError::TxAddressDeserializationError)?; + if to.is_empty() { + continue; + } + + output_amount += output.value; + let amount = big_decimal_from_sat_unsigned(output.value, decimals); + for address in to { + tx_builder.transferred_to(address, &amount); + } + } + + let fee = if input_amount == 0 { + let fee = verbose_tx.vin.iter().fold(0., |cur, input| { + let fee = match input { + TransactionInputEnum::Lelantus(lelantus) => lelantus.n_fees, + _ => 0., + }; + cur + fee + }); + BigDecimal::try_from(fee)? + } else { + let fee = input_amount as i64 - output_amount as i64; + big_decimal_from_sat(fee, decimals) + }; + + let fee_details = UtxoFeeDetails { + coin: Some(ticker.to_string()), + amount: fee, + }; + + tx_builder.set_tx_fee(Some(TxFeeDetails::from(fee_details))); + Ok(vec![tx_builder.build()]) +} + +/// [`UtxoTxHistoryOps::tx_from_storage_or_rpc`] implementation. +pub async fn tx_from_storage_or_rpc( + coin: &Coin, + tx_hash: &H256Json, + storage: &Storage, +) -> MmResult +where + Coin: CoinWithTxHistoryV2 + UtxoCommonOps, + Storage: TxHistoryStorage, +{ + let tx_hash_str = format!("{:02x}", tx_hash); + let wallet_id = coin.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 = coin.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) +} + +/// [`UtxoTxHistoryOps::my_addresses_balances`] implementation. +/// Requests balances of all activated addresses. +pub async fn my_addresses_balances(coin: &Coin) -> BalanceResult> +where + Coin: CoinBalanceReportOps, +{ + let coin_balance = coin.coin_balance_report().await?; + Ok(coin_balance.to_addresses_total_balances()) +} + +/// [`UtxoTxHistoryOps::request_tx_history`] implementation. +/// Requests transaction history according to `UtxoRpcClientEnum`. +pub async fn request_tx_history( + coin: &Coin, + metrics: MetricsArc, + for_addresses: &HashSet
, +) -> RequestTxHistoryResult +where + Coin: UtxoCommonOps + MarketCoinOps, +{ + let ticker = coin.ticker(); + match coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(ref native) => { + request_tx_history_with_native(ticker, native, metrics, for_addresses).await + }, + UtxoRpcClientEnum::Electrum(ref electrum) => { + request_tx_history_with_electrum(ticker, electrum, metrics, for_addresses).await + }, + } +} + +/// `request_tx_history_with_der_method` function's helper. +async fn request_tx_history_with_native( + ticker: &str, + native: &NativeClient, + metrics: MetricsArc, + for_addresses: &HashSet
, +) -> RequestTxHistoryResult { + let my_addresses: HashSet = for_addresses.iter().map(DisplayAddress::display_address).collect(); + + let mut from = 0; + let mut all_transactions = vec![]; + loop { + mm_counter!(metrics, "tx.history.request.count", 1, + "coin" => ticker, "client" => "native", "method" => "listtransactions"); + + let transactions = match native.list_transactions(100, from).compat().await { + Ok(value) => value, + Err(e) => { + return RequestTxHistoryResult::Retry { + error: ERRL!("Error {} on list transactions", e), + }; + }, + }; + + mm_counter!(metrics, "tx.history.response.count", 1, + "coin" => ticker, "client" => "native", "method" => "listtransactions"); + + if transactions.is_empty() { + break; + } + from += 100; + all_transactions.extend(transactions); + } + + mm_counter!(metrics, "tx.history.response.total_length", all_transactions.len() as u64, + "coin" => ticker, "client" => "native", "method" => "listtransactions"); + + let all_transactions = all_transactions + .into_iter() + .filter_map(|item| { + if my_addresses.contains(&item.address) { + Some((item.txid, item.blockindex)) + } else { + None + } + }) + .collect(); + + RequestTxHistoryResult::Ok(all_transactions) +} + +/// `request_tx_history_with_der_method` function's helper. +async fn request_tx_history_with_electrum( + ticker: &str, + electrum: &ElectrumClient, + metrics: MetricsArc, + for_addresses: &HashSet
, +) -> RequestTxHistoryResult { + fn addr_to_script_hash(addr: &Address) -> String { + let script = output_script(addr, ScriptType::P2PKH); + let script_hash = electrum_script_hash(&script); + hex::encode(script_hash) + } + + let script_hashes_count = for_addresses.len() as u64; + let script_hashes = for_addresses.iter().map(addr_to_script_hash); + + mm_counter!(metrics, "tx.history.request.count", script_hashes_count, + "coin" => ticker, "client" => "electrum", "method" => "blockchain.scripthash.get_history"); + + let hashes_history = match electrum.scripthash_get_history_batch(script_hashes).compat().await { + Ok(hashes_history) => hashes_history, + Err(e) => match &e.error { + JsonRpcErrorType::InvalidRequest(e) + | JsonRpcErrorType::Transport(e) + | JsonRpcErrorType::Parse(_, e) + | JsonRpcErrorType::Internal(e) => { + return RequestTxHistoryResult::Retry { + error: ERRL!("Error {} on scripthash_get_history", e), + }; + }, + JsonRpcErrorType::Response(_addr, err) => { + if HISTORY_TOO_LARGE_ERROR.eq(err) { + return RequestTxHistoryResult::HistoryTooLarge; + } else { + return RequestTxHistoryResult::Retry { + error: ERRL!("Error {:?} on scripthash_get_history", e), + }; + } + }, + }, + }; + + let ordered_history: Vec<_> = hashes_history + .into_iter() + .flatten() + .map(|item| { + let height = if item.height < 0 { 0 } else { item.height as u64 }; + (item.tx_hash, height) + }) + // We need to order transactions by their height and TX hash. + .sorted_by(|(tx_hash_left, height_left), (tx_hash_right, height_right)| { + let left = TxIdHeight::new(*height_left, tx_hash_left); + let right = TxIdHeight::new(*height_right, tx_hash_right); + compare_transactions(left, right) + }) + .collect(); + + mm_counter!(metrics, "tx.history.response.count", script_hashes_count, + "coin" => ticker, "client" => "electrum", "method" => "blockchain.scripthash.get_history"); + + mm_counter!(metrics, "tx.history.response.total_length", ordered_history.len() as u64, + "coin" => ticker, "client" => "electrum", "method" => "blockchain.scripthash.get_history"); + + RequestTxHistoryResult::Ok(ordered_history) +} diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index 94613eb5c8..eef16a64ba 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -1,9 +1,198 @@ use super::*; +use crate::hd_wallet::HDAccountsMap; +use crate::my_tx_history_v2::{my_tx_history_v2_impl, CoinWithTxHistoryV2, MyTxHistoryDetails, MyTxHistoryRequestV2, + MyTxHistoryResponseV2, MyTxHistoryTarget}; +use crate::tx_history_storage::TxHistoryStorageBuilder; use crate::utxo::rpc_clients::{ElectrumClient, UtxoRpcClientOps}; +use crate::utxo::tx_cache::dummy_tx_cache::DummyVerboseCache; +use crate::utxo::tx_cache::UtxoVerboseCacheOps; +use crate::utxo::utxo_tx_history_v2::{utxo_history_loop, UtxoTxHistoryOps}; +use crate::{compare_transaction_details, UtxoStandardCoin}; +use common::custom_futures::repeatable::{Ready, Retry}; +use common::executor::{spawn, Timer}; use common::jsonrpc_client::JsonRpcErrorType; +use common::PagingOptionsEnum; +use crypto::privkey::key_pair_from_seed; +use itertools::Itertools; +use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; use std::convert::TryFrom; +use std::num::NonZeroUsize; +use std::time::Duration; -pub async fn test_electrum_display_balances(rpc_client: &ElectrumClient) { +pub(super) const TEST_COIN_NAME: &'static str = "RICK"; +// Made-up hrp for rick to test p2wpkh script +pub(super) const TEST_COIN_HRP: &'static str = "rck"; +pub(super) const TEST_COIN_DECIMALS: u8 = 8; + +const MORTY_HD_TX_HISTORY_STR: &str = include_str!("../for_tests/MORTY_HD_tx_history_fixtures.json"); + +lazy_static! { + static ref MORTY_HD_TX_HISTORY: Vec = parse_tx_history(MORTY_HD_TX_HISTORY_STR); + static ref MORTY_HD_TX_HISTORY_MAP: HashMap = + parse_tx_history_map(MORTY_HD_TX_HISTORY_STR); +} + +fn parse_tx_history(history_str: &'static str) -> Vec { json::from_str(history_str).unwrap() } + +fn parse_tx_history_map(history_str: &'static str) -> HashMap { + parse_tx_history(history_str) + .into_iter() + .map(|tx| (format!("{:02x}", tx.internal_id), tx)) + .collect() +} + +pub(super) fn utxo_coin_fields_for_test( + rpc_client: UtxoRpcClientEnum, + force_seed: Option<&str>, + is_segwit_coin: bool, +) -> UtxoCoinFields { + let checksum_type = ChecksumType::DSHA256; + let default_seed = "spice describe gravity federal blast come thank unfair canal monkey style afraid"; + let seed = match force_seed { + Some(s) => s.into(), + None => match std::env::var("BOB_PASSPHRASE") { + Ok(p) => { + if p.is_empty() { + default_seed.into() + } else { + p + } + }, + Err(_) => default_seed.into(), + }, + }; + let key_pair = key_pair_from_seed(&seed).unwrap(); + let my_address = Address { + prefix: 60, + hash: key_pair.public().address_hash().into(), + t_addr_prefix: 0, + checksum_type, + 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::SingleAddress(my_address); + + let bech32_hrp = if is_segwit_coin { + Some(TEST_COIN_HRP.to_string()) + } else { + None + }; + + UtxoCoinFields { + conf: UtxoCoinConf { + is_pos: false, + requires_notarization: false.into(), + overwintered: true, + segwit: true, + tx_version: 4, + default_address_format: UtxoAddressFormat::Standard, + asset_chain: true, + p2sh_addr_prefix: 85, + 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, + tx_fee_volatility_percent: DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT, + version_group_id: 0x892f2085, + consensus_branch_id: 0x76b809bb, + zcash: true, + checksum_type, + fork_id: 0, + signature_version: SignatureVersion::Base, + required_confirmations: 1.into(), + force_min_relay_fee: false, + mtp_block_count: NonZeroU64::new(11).unwrap(), + estimate_fee_mode: None, + mature_confirmations: MATURE_CONFIRMATIONS_DEFAULT, + estimate_fee_blocks: 1, + trezor_coin: None, + spv_conf: None, + derivation_path: None, + avg_blocktime: None, + }, + decimals: TEST_COIN_DECIMALS, + dust_amount: UTXO_DUST_AMOUNT, + tx_fee: TxFee::FixedPerKb(1000), + rpc_client, + priv_key_policy, + derivation_method, + history_sync_state: Mutex::new(HistorySyncState::NotEnabled), + tx_cache: DummyVerboseCache::default().into_shared(), + recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), + tx_hash_algo: TxHashAlgo::DSHA256, + check_utxo_maturity: false, + block_headers_status_notifier: None, + block_headers_status_watcher: None, + abortable_system: AbortableQueue::default(), + } +} + +pub(super) fn utxo_coin_from_fields(coin: UtxoCoinFields) -> UtxoStandardCoin { + let arc: UtxoArc = coin.into(); + arc.into() +} + +pub(super) async fn wait_for_tx_history_finished( + ctx: &MmArc, + coin: &Coin, + target: MyTxHistoryTarget, + expected_txs: usize, + timeout_s: u64, +) -> MyTxHistoryResponseV2 +where + Coin: CoinWithTxHistoryV2 + MmCoin, +{ + let req = MyTxHistoryRequestV2 { + coin: coin.ticker().to_owned(), + limit: u32::MAX as usize, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()), + target, + }; + + // Let the storage to be initialized for the given coin. + Timer::sleep(1.).await; + + repeatable!(async { + let response = my_tx_history_v2_impl(ctx.clone(), coin, req.clone()).await.unwrap(); + if response.transactions.len() >= expected_txs { + return Ready(response); + } + Retry(()) + }) + .repeat_every(Duration::from_secs(3)) + .with_timeout_ms(timeout_s * 1000) + .await + .unwrap() +} + +pub(super) fn get_morty_hd_transactions_ordered(tx_hashes: &[&str]) -> Vec { + tx_hashes + .iter() + .map(|tx_hash| { + MORTY_HD_TX_HISTORY_MAP + .get(*tx_hash) + .expect(&format!("No such {tx_hash:?} TX in the file")) + .clone() + }) + .sorted_by(compare_transaction_details) + .collect() +} + +pub(super) async fn test_electrum_display_balances(rpc_client: &ElectrumClient) { let addresses = vec![ "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), "RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), @@ -48,3 +237,79 @@ pub async fn test_electrum_display_balances(rpc_client: &ElectrumClient) { ekind => panic!("Unexpected `JsonRpcErrorType`: {:?}", ekind), } } + +/// TODO move this test to `mm2_tests.rs` +/// when [Trezor Daemon Emulator](https://github.com/trezor/trezord-go#emulator-support) is integrated. +pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { + let ctx = mm_ctx_with_custom_db(); + + let hd_account_for_test = UtxoHDAccount { + account_id: 0, + extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), + account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), + external_addresses_number: 11, + internal_addresses_number: 3, + derived_addresses: HDAddressesCache::default(), + }; + let mut hd_accounts = HDAccountsMap::new(); + hd_accounts.insert(0, hd_account_for_test); + + let mut fields = utxo_coin_fields_for_test(rpc_client.into(), None, false); + fields.conf.ticker = "MORTY".to_string(); + fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { + hd_wallet_rmd160: "6d9d2b554d768232320587df75c4338ecc8bf37d".into(), + hd_wallet_storage: HDWalletCoinStorage::default(), + address_format: UtxoAddressFormat::Standard, + derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + gap_limit: 20, + }); + + let coin = utxo_coin_from_fields(fields); + + let current_balances = coin.my_addresses_balances().await.unwrap(); + + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + spawn(utxo_history_loop( + coin.clone(), + storage, + ctx.metrics.clone(), + current_balances, + )); + + let target = MyTxHistoryTarget::AccountId { account_id: 0 }; + let tx_history = wait_for_tx_history_finished(&ctx, &coin, target, 4, 30).await; + + let actual: Vec<_> = tx_history.transactions.into_iter().map(|tx| tx.details).collect(); + let expected = get_morty_hd_transactions_ordered(&[ + "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", + "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", + "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", + "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", + ]); + assert_eq!(actual, expected); + + // Activate new `RQstQeTUEZLh6c3YWJDkeVTTQoZUsfvNCr` address. + match coin.as_ref().derivation_method { + DerivationMethod::HDWallet(ref hd_wallet) => { + let mut accounts = hd_wallet.accounts.lock().await; + accounts.get_mut(&0).unwrap().internal_addresses_number += 1 + }, + _ => unimplemented!(), + } + + // Wait for the TX history loop to fetch Transactions of the activated address. + let target = MyTxHistoryTarget::AccountId { account_id: 0 }; + let tx_history = wait_for_tx_history_finished(&ctx, &coin, target, 5, 60).await; + + let actual: Vec<_> = tx_history.transactions.into_iter().map(|tx| tx.details).collect(); + let expected = get_morty_hd_transactions_ordered(&[ + // New transaction: + "6ca27dd058b939c98a33625b9f68eaeebca5a3058aec062647ca6fd7634bb339", + "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", + "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", + "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", + "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", + ]); + assert_eq!(actual, expected); +} diff --git a/mm2src/coins/utxo/utxo_indexedb_block_header_storage.rs b/mm2src/coins/utxo/utxo_indexedb_block_header_storage.rs deleted file mode 100644 index d00d4f261f..0000000000 --- a/mm2src/coins/utxo/utxo_indexedb_block_header_storage.rs +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index d11b794e54..0000000000 --- a/mm2src/coins/utxo/utxo_sql_block_header_storage.rs +++ /dev/null @@ -1,324 +0,0 @@ -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 b48f9bfe59..34746570f7 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -1,31 +1,46 @@ use super::*; -use crate::coin_balance::{self, EnableCoinBalanceError, HDAccountBalance, HDAddressBalance, HDWalletBalance, - HDWalletBalanceOps}; +use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, + HDWalletBalance, HDWalletBalanceOps}; +use crate::coin_errors::MyAddressError; +use crate::hd_confirm_address::HDConfirmAddress; use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{self, AccountUpdatingError, AddressDerivingError, GetNewHDAddressParams, - GetNewHDAddressResponse, HDAccountMut, HDWalletRpcError, HDWalletRpcOps, - NewAccountCreatingError}; +use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, NewAccountCreatingError, + NewAddressDeriveConfirmError}; use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; +use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, + GetNewAddressRpcOps}; 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_account_balance::{self, InitAccountBalanceParams, InitAccountBalanceRpcOps}; +use crate::rpc_command::init_create_account::{self, CreateAccountRpcError, CreateAccountState, CreateNewAccountParams, + InitCreateAccountRpcOps}; use crate::rpc_command::init_scan_for_new_addresses::{self, InitScanAddressesRpcOps, ScanAddressesParams, ScanAddressesResponse}; use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; +use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; 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 crypto::trezor::utxo::TrezorUtxoCoin; +use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, + UtxoTxHistoryOps}; +use crate::{CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, GetWithdrawSenderAddress, + IguanaPrivKey, MakerSwapTakerCoin, NegotiateSwapContractAddrErr, PaymentInstructions, + PaymentInstructionsErr, PrivKeyBuildPolicy, RefundError, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, + SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, + SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, SignatureResult, SwapOps, + TakerSwapMakerCoin, TradePreimageValue, TransactionFut, TxMarshalingErr, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, + ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherOps, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, + WithdrawSenderAddress}; +use common::executor::{AbortableSystem, AbortedError}; use crypto::Bip44Chain; use futures::{FutureExt, TryFutureExt}; +use mm2_metrics::MetricsArc; use mm2_number::MmNumber; -use serialization::coin_variant_by_ticker; use utxo_signer::UtxoSignerOps; -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct UtxoStandardCoin { utxo_arc: UtxoArc, } @@ -42,14 +57,13 @@ impl From for UtxoArc { fn from(coin: UtxoStandardCoin) -> Self { coin.utxo_arc } } -pub async fn utxo_standard_coin_with_priv_key( +pub async fn utxo_standard_coin_with_policy( ctx: &MmArc, ticker: &str, conf: &Json, activation_params: &UtxoActivationParams, - priv_key: &[u8], + priv_key_policy: PrivKeyBuildPolicy, ) -> Result { - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(priv_key); let coin = try_s!( UtxoArcBuilder::new( ctx, @@ -65,6 +79,17 @@ pub async fn utxo_standard_coin_with_priv_key( Ok(coin) } +pub async fn utxo_standard_coin_with_priv_key( + ctx: &MmArc, + ticker: &str, + conf: &Json, + activation_params: &UtxoActivationParams, + priv_key: IguanaPrivKey, +) -> Result { + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(priv_key); + utxo_standard_coin_with_policy(ctx, ticker, conf, activation_params, priv_key_policy).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)] @@ -144,8 +169,8 @@ impl GetUtxoMapOps for UtxoStandardCoin { #[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 + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { + utxo_common::get_htlc_spend_fee(self, tx_size, stage).await } fn addresses_from_script(&self, script: &Script) -> Result, String> { @@ -158,13 +183,12 @@ impl UtxoCommonOps for UtxoStandardCoin { utxo_common::my_public_key(self.as_ref()) } - fn address_from_str(&self, address: &str) -> Result { + fn address_from_str(&self, address: &str) -> MmResult { utxo_common::checked_address_from_str(self, address) } async fn get_current_mtp(&self) -> UtxoRpcResult { - let coin_variant = coin_variant_by_ticker(self.ticker()); - utxo_common::get_current_mtp(&self.utxo_arc, coin_variant).await + utxo_common::get_current_mtp(&self.utxo_arc, self.ticker().into()).await } fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { @@ -266,134 +290,93 @@ impl UtxoStandardOps for UtxoStandardCoin { #[async_trait] impl SwapOps for UtxoStandardCoin { + #[inline] 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 { + #[inline] + fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { utxo_common::send_maker_payment( self.clone(), - time_lock, - taker_pub, - secret_hash, - amount, - swap_unique_data, + maker_payment_args.time_lock, + maker_payment_args.other_pubkey, + maker_payment_args.secret_hash, + maker_payment_args.amount, + maker_payment_args.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 { + #[inline] + fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { utxo_common::send_taker_payment( self.clone(), - time_lock, - maker_pub, - secret_hash, - amount, - swap_unique_data, + taker_payment_args.time_lock, + taker_payment_args.other_pubkey, + taker_payment_args.secret_hash, + taker_payment_args.amount, + taker_payment_args.swap_unique_data, ) } + #[inline] 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], + maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, ) -> TransactionFut { utxo_common::send_maker_spends_taker_payment( self.clone(), - taker_payment_tx, - time_lock, - taker_pub, - secret, - swap_unique_data, + maker_spends_payment_args.other_payment_tx, + maker_spends_payment_args.time_lock, + maker_spends_payment_args.other_pubkey, + maker_spends_payment_args.secret, + maker_spends_payment_args.secret_hash, + maker_spends_payment_args.swap_unique_data, ) } + #[inline] fn send_taker_spends_maker_payment( &self, - maker_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - secret: &[u8], - _swap_contract_address: &Option, - swap_unique_data: &[u8], + taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, ) -> TransactionFut { utxo_common::send_taker_spends_maker_payment( self.clone(), - maker_tx, - time_lock, - maker_pub, - secret, - swap_unique_data, + taker_spends_payment_args.other_payment_tx, + taker_spends_payment_args.time_lock, + taker_spends_payment_args.other_pubkey, + taker_spends_payment_args.secret, + taker_spends_payment_args.secret_hash, + taker_spends_payment_args.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 { + #[inline] + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { utxo_common::send_taker_refunds_payment( self.clone(), - taker_tx, - time_lock, - maker_pub, - secret_hash, - swap_unique_data, + taker_refunds_payment_args.payment_tx, + taker_refunds_payment_args.time_lock, + taker_refunds_payment_args.other_pubkey, + taker_refunds_payment_args.secret_hash, + taker_refunds_payment_args.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 { + #[inline] + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { utxo_common::send_maker_refunds_payment( self.clone(), - maker_tx, - time_lock, - taker_pub, - secret_hash, - swap_unique_data, + maker_refunds_payment_args.payment_tx, + maker_refunds_payment_args.time_lock, + maker_refunds_payment_args.other_pubkey, + maker_refunds_payment_args.secret_hash, + maker_refunds_payment_args.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 { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + let tx = match validate_fee_args.fee_tx { TransactionEnum::UtxoTx(tx) => tx.clone(), _ => panic!(), }; @@ -401,33 +384,38 @@ impl SwapOps for UtxoStandardCoin { self.clone(), tx, utxo_common::DEFAULT_FEE_VOUT, - expected_sender, - amount, - min_block_number, - fee_addr, + validate_fee_args.expected_sender, + validate_fee_args.amount, + validate_fee_args.min_block_number, + validate_fee_args.fee_addr, ) } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + #[inline] + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { utxo_common::validate_maker_payment(self, input) } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + #[inline] + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { utxo_common::validate_taker_payment(self, input) } + #[inline] 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], + if_my_payment_sent_args: CheckIfMyPaymentSentArgs, ) -> Box, Error = String> + Send> { - utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash, swap_unique_data) + utxo_common::check_if_my_payment_sent( + self.clone(), + if_my_payment_sent_args.time_lock, + if_my_payment_sent_args.other_pub, + if_my_payment_sent_args.secret_hash, + if_my_payment_sent_args.swap_unique_data, + ) } + #[inline] async fn search_for_swap_tx_spend_my( &self, input: SearchForSwapTxSpendInput<'_>, @@ -435,6 +423,7 @@ impl SwapOps for UtxoStandardCoin { utxo_common::search_for_swap_tx_spend_my(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } + #[inline] async fn search_for_swap_tx_spend_other( &self, input: SearchForSwapTxSpendInput<'_>, @@ -442,10 +431,17 @@ impl SwapOps for UtxoStandardCoin { 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> { + #[inline] + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { + utxo_common::check_all_inputs_signed_by_pub(tx, expected_pub) + } + + #[inline] + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } + #[inline] fn can_refund_htlc(&self, locktime: u64) -> Box + Send + '_> { Box::new( utxo_common::can_refund_htlc(self, locktime) @@ -455,6 +451,15 @@ impl SwapOps for UtxoStandardCoin { ) } + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } + + #[inline] fn negotiate_swap_contract_addr( &self, _other_side_address: Option<&[u8]>, @@ -465,6 +470,142 @@ impl SwapOps for UtxoStandardCoin { fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) } + + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + utxo_common::derive_htlc_pubkey(self, swap_unique_data) + } + + #[inline] + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + utxo_common::validate_other_pubkey(raw_pubkey) + } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn is_supported_by_watchers(&self) -> bool { true } +} + +#[async_trait] +impl TakerSwapMakerCoin for UtxoStandardCoin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for UtxoStandardCoin { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for UtxoStandardCoin { + #[inline] + fn create_taker_payment_refund_preimage( + &self, + taker_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + _swap_contract_address: &Option, + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::create_taker_payment_refund_preimage( + self, + taker_tx, + time_lock, + maker_pub, + secret_hash, + swap_unique_data, + ) + } + + #[inline] + fn create_maker_payment_spend_preimage( + &self, + maker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], + swap_unique_data: &[u8], + ) -> TransactionFut { + utxo_common::create_maker_payment_spend_preimage( + self, + maker_payment_tx, + time_lock, + maker_pub, + secret_hash, + swap_unique_data, + ) + } + + #[inline] + fn send_taker_payment_refund_preimage( + &self, + watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + utxo_common::send_taker_payment_refund_preimage(self, watcher_refunds_payment_args) + } + + #[inline] + fn send_maker_payment_spend_preimage(&self, input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + utxo_common::send_maker_payment_spend_preimage(self, input) + } + + #[inline] + fn watcher_validate_taker_fee(&self, input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + utxo_common::watcher_validate_taker_fee(self, input, utxo_common::DEFAULT_FEE_VOUT) + } + + #[inline] + fn watcher_validate_taker_payment(&self, input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + utxo_common::watcher_validate_taker_payment(self, input) + } + + #[inline] + async fn watcher_search_for_swap_tx_spend( + &self, + input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + utxo_common::watcher_search_for_swap_tx_spend(self, input, utxo_common::DEFAULT_SWAP_VOUT).await + } } impl MarketCoinOps for UtxoStandardCoin { @@ -475,7 +616,7 @@ impl MarketCoinOps for UtxoStandardCoin { Ok(pubkey.to_string()) } - fn my_address(&self) -> Result { utxo_common::my_address(self) } + fn my_address(&self) -> MmResult { utxo_common::my_address(self) } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { utxo_common::sign_message_hash(self.as_ref(), message) @@ -523,12 +664,14 @@ impl MarketCoinOps for UtxoStandardCoin { ) } - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, transaction: &[u8], + _secret_hash: &[u8], wait_until: u64, from_block: u64, _swap_contract_address: &Option, + check_every: f64, ) -> TransactionFut { utxo_common::wait_for_output_spend( &self.utxo_arc, @@ -536,10 +679,11 @@ impl MarketCoinOps for UtxoStandardCoin { utxo_common::DEFAULT_SWAP_VOUT, from_block, wait_until, + check_every, ) } - fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result { + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { utxo_common::tx_enum_from_bytes(self.as_ref(), bytes) } @@ -558,10 +702,20 @@ impl MarketCoinOps for UtxoStandardCoin { impl MmCoin for UtxoStandardCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) } + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { + Box::new( + utxo_common::get_tx_hex_by_hash(&self.utxo_arc, tx_hash) + .boxed() + .compat(), + ) + } + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { Box::new(utxo_common::withdraw(self.clone(), req).boxed().compat()) } @@ -597,7 +751,7 @@ impl MmCoin for UtxoStandardCoin { utxo_common::get_sender_trade_fee(self, value, stage).await } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { utxo_common::get_receiver_trade_fee(self.clone()) } @@ -623,6 +777,8 @@ impl MmCoin for UtxoStandardCoin { fn swap_contract_address(&self) -> Option { utxo_common::swap_contract_address() } + fn fallback_swap_contract(&self) -> Option { utxo_common::fallback_swap_contract() } + fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } @@ -630,6 +786,10 @@ impl MmCoin for UtxoStandardCoin { fn is_coin_protocol_supported(&self, info: &Option>) -> bool { utxo_common::is_coin_protocol_supported(self, info) } + + fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.as_ref().abortable_system) } + + fn on_token_deactivated(&self, _ticker: &str) {} } #[async_trait] @@ -660,10 +820,11 @@ impl InitWithdrawCoin for UtxoStandardCoin { impl UtxoSignerOps for UtxoStandardCoin { type TxGetter = UtxoRpcClientEnum; - fn trezor_coin(&self) -> UtxoSignTxResult { + fn trezor_coin(&self) -> UtxoSignTxResult { self.utxo_arc .conf .trezor_coin + .clone() .or_mm_err(|| UtxoSignTxError::CoinNotSupportedWithTrezor { coin: self.utxo_arc.conf.ticker.clone(), }) @@ -695,7 +856,7 @@ impl ExtractExtendedPubkey for UtxoStandardCoin { derivation_path: DerivationPath, ) -> MmResult where - XPubExtractor: HDXPubExtractor + Sync, + XPubExtractor: HDXPubExtractor, { utxo_common::extract_extended_pubkey(&self.utxo_arc.conf, xpub_extractor, derivation_path).await } @@ -708,13 +869,28 @@ impl HDWalletCoinOps for UtxoStandardCoin { type HDWallet = UtxoHDWallet; type HDAccount = UtxoHDAccount; - fn derive_address( + async fn derive_addresses( &self, hd_account: &Self::HDAccount, + address_ids: Ids, + ) -> AddressDerivingResult>> + where + Ids: Iterator + Send, + { + utxo_common::derive_addresses(self, hd_account, address_ids).await + } + + async fn generate_and_confirm_new_address( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut Self::HDAccount, chain: Bip44Chain, - address_id: u32, - ) -> MmResult, AddressDerivingError> { - utxo_common::derive_address(self, hd_account, chain, address_id) + confirm_address: &ConfirmAddress, + ) -> MmResult, NewAddressDeriveConfirmError> + where + ConfirmAddress: HDConfirmAddress, + { + utxo_common::generate_and_confirm_new_address(self, hd_wallet, hd_account, chain, confirm_address).await } async fn create_new_account<'a, XPubExtractor>( @@ -723,7 +899,7 @@ impl HDWalletCoinOps for UtxoStandardCoin { xpub_extractor: &XPubExtractor, ) -> MmResult, NewAccountCreatingError> where - XPubExtractor: HDXPubExtractor + Sync, + XPubExtractor: HDXPubExtractor, { utxo_common::create_new_account(self, hd_wallet, xpub_extractor).await } @@ -740,12 +916,23 @@ impl HDWalletCoinOps for UtxoStandardCoin { } #[async_trait] -impl HDWalletRpcOps for UtxoStandardCoin { - async fn get_new_address_rpc( +impl GetNewAddressRpcOps for UtxoStandardCoin { + async fn get_new_address_rpc_without_conf( &self, - params: GetNewHDAddressParams, - ) -> MmResult { - hd_wallet::common_impl::get_new_address_rpc(self, params).await + params: GetNewAddressParams, + ) -> MmResult { + get_new_address::common_impl::get_new_address_rpc_without_conf(self, params).await + } + + async fn get_new_address_rpc( + &self, + params: GetNewAddressParams, + confirm_address: &ConfirmAddress, + ) -> MmResult + where + ConfirmAddress: HDConfirmAddress, + { + get_new_address::common_impl::get_new_address_rpc(self, params, confirm_address).await } } @@ -761,12 +948,12 @@ impl HDWalletBalanceOps for UtxoStandardCoin { &self, hd_wallet: &Self::HDWallet, xpub_extractor: &XPubExtractor, - scan_policy: EnableCoinScanPolicy, + params: EnabledCoinBalanceParams, ) -> MmResult where - XPubExtractor: HDXPubExtractor + Sync, + XPubExtractor: HDXPubExtractor, { - coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, scan_policy).await + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params).await } async fn scan_for_new_addresses( @@ -811,6 +998,16 @@ impl AccountBalanceRpcOps for UtxoStandardCoin { } } +#[async_trait] +impl InitAccountBalanceRpcOps for UtxoStandardCoin { + async fn init_account_balance_rpc( + &self, + params: InitAccountBalanceParams, + ) -> MmResult { + init_account_balance::common_impl::init_account_balance_rpc(self, params).await + } +} + #[async_trait] impl InitScanAddressesRpcOps for UtxoStandardCoin { async fn init_scan_for_new_addresses_rpc( @@ -822,15 +1019,81 @@ impl InitScanAddressesRpcOps for UtxoStandardCoin { } #[async_trait] -impl InitCreateHDAccountRpcOps for UtxoStandardCoin { +impl InitCreateAccountRpcOps for UtxoStandardCoin { async fn init_create_account_rpc( &self, params: CreateNewAccountParams, + state: CreateAccountState, xpub_extractor: &XPubExtractor, - ) -> MmResult + ) -> MmResult where - XPubExtractor: HDXPubExtractor + Sync, + XPubExtractor: HDXPubExtractor, { - init_create_account::common_impl::init_create_new_account_rpc(self, params, xpub_extractor).await + init_create_account::common_impl::init_create_new_account_rpc(self, params, state, xpub_extractor).await + } + + async fn revert_creating_account(&self, account_id: u32) { + init_create_account::common_impl::revert_creating_account(self, account_id).await + } +} + +#[async_trait] +impl CoinWithTxHistoryV2 for UtxoStandardCoin { + fn history_wallet_id(&self) -> WalletId { utxo_common::utxo_tx_history_v2_common::history_wallet_id(self.as_ref()) } + + async fn get_tx_history_filters( + &self, + target: MyTxHistoryTarget, + ) -> MmResult { + utxo_common::utxo_tx_history_v2_common::get_tx_history_filters(self, target).await + } +} + +#[async_trait] +impl UtxoTxHistoryOps for UtxoStandardCoin { + async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { + utxo_common::utxo_tx_history_v2_common::my_addresses(self).await + } + + async fn tx_details_by_hash( + &self, + params: UtxoTxDetailsParams<'_, Storage>, + ) -> MmResult, UtxoTxDetailsError> + where + Storage: TxHistoryStorage, + { + utxo_common::utxo_tx_history_v2_common::tx_details_by_hash(self, params).await + } + + async fn tx_from_storage_or_rpc( + &self, + tx_hash: &H256Json, + storage: &Storage, + ) -> MmResult { + utxo_common::utxo_tx_history_v2_common::tx_from_storage_or_rpc(self, tx_hash, storage).await + } + + async fn request_tx_history( + &self, + metrics: MetricsArc, + for_addresses: &HashSet
, + ) -> RequestTxHistoryResult { + utxo_common::utxo_tx_history_v2_common::request_tx_history(self, metrics, for_addresses).await + } + + async fn get_block_timestamp(&self, height: u64) -> MmResult { + self.as_ref().rpc_client.get_block_timestamp(height).await + } + + async fn my_addresses_balances(&self) -> BalanceResult> { + utxo_common::utxo_tx_history_v2_common::my_addresses_balances(self).await + } + + fn address_from_str(&self, address: &str) -> MmResult { + utxo_common::checked_address_from_str(self, address) + } + + fn set_history_sync_state(&self, new_state: HistorySyncState) { + *self.as_ref().history_sync_state.lock().unwrap() = new_state; } } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index e864ac9946..cec80eb40c 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -1,49 +1,60 @@ use super::*; use crate::coin_balance::HDAddressBalance; +use crate::hd_confirm_address::for_tests::MockableConfirmAddress; +use crate::hd_confirm_address::{HDConfirmAddress, HDConfirmAddressError}; use crate::hd_wallet::HDAccountsMap; use crate::hd_wallet_storage::{HDWalletMockStorage, HDWalletStorageInternalOps}; +use crate::my_tx_history_v2::for_tests::init_storage_for; +use crate::my_tx_history_v2::CoinWithTxHistoryV2; use crate::rpc_command::account_balance::{AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; +use crate::rpc_command::get_new_address::{GetNewAddressParams, GetNewAddressRpcError, GetNewAddressRpcOps}; 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}; +#[cfg(not(target_arch = "wasm32"))] +use crate::utxo::rpc_clients::{BlockHashOrHeight, NativeUnspent}; +use crate::utxo::rpc_clients::{ElectrumBalance, ElectrumClient, ElectrumClientImpl, GetAddressInfoRes, + ListSinceBlockRes, NativeClient, NativeClientImpl, NetworkInfo, UtxoRpcClientOps, + ValidateAddressRes, VerboseBlock}; +use crate::utxo::spv::SimplePaymentVerification; +#[cfg(not(target_arch = "wasm32"))] +use crate::utxo::utxo_block_header_storage::{BlockHeaderStorage, SqliteBlockHeadersStorage}; +use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder, UtxoCoinBuilderCommonOps}; use crate::utxo::utxo_common::UtxoTxBuilder; -use crate::utxo::utxo_common_tests; +#[cfg(not(target_arch = "wasm32"))] +use crate::utxo::utxo_common_tests::TEST_COIN_DECIMALS; +use crate::utxo::utxo_common_tests::{self, utxo_coin_fields_for_test, utxo_coin_from_fields, TEST_COIN_NAME}; use crate::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; +use crate::utxo::utxo_tx_history_v2::{UtxoTxDetailsParams, UtxoTxHistoryOps}; #[cfg(not(target_arch = "wasm32"))] use crate::WithdrawFee; -use crate::{CoinBalance, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, StakingInfosDetails, SwapOps, - TradePreimageValue, TxFeeDetails}; -use chain::OutPoint; +use crate::{BlockHeightAndTime, CoinBalance, IguanaPrivKey, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, + SendMakerSpendsTakerPaymentArgs, StakingInfosDetails, SwapOps, TradePreimageValue, TxFeeDetails, + TxMarshalingErr, ValidateFeeArgs}; +use chain::{BlockHeader, BlockHeaderBits, OutPoint}; 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 crypto::{privkey::key_pair_from_seed, Bip44Chain, RpcDerivationPath, Secp256k1Secret}; +#[cfg(not(target_arch = "wasm32"))] +use db_common::sqlite::rusqlite::Connection; +use futures::channel::mpsc::channel; use futures::future::join_all; use futures::TryFutureExt; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_number::bigdecimal::{BigDecimal, Signed}; +use mm2_test_helpers::for_tests::{mm_ctx_with_custom_db, MORTY_ELECTRUM_ADDRS, RICK_ELECTRUM_ADDRS}; use mocktopus::mocking::*; use rpc::v1::types::H256 as H256Json; use serialization::{deserialize, CoinVariant}; -use std::convert::TryFrom; +use spv_validation::conf::{BlockHeaderValidationParams, SPVBlockHeader}; +use spv_validation::storage::BlockHeaderStorageOps; +use spv_validation::work::DifficultyAlgorithm; +#[cfg(not(target_arch = "wasm32"))] 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 -const TEST_COIN_HRP: &'static str = "rck"; -const RICK_ELECTRUM_ADDRS: &[&'static str] = &[ - "electrum1.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum3.cipig.net:10017", -]; -const TEST_COIN_DECIMALS: u8 = 8; +#[cfg(not(target_arch = "wasm32"))] +const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; pub fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let ctx = MmCtxBuilder::default().into_mm_arc(); @@ -53,7 +64,7 @@ pub fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { "servers": servers, }); let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(&[]); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(IguanaPrivKey::default()); let builder = UtxoArcBuilder::new( &ctx, TEST_COIN_NAME, @@ -69,120 +80,65 @@ pub fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { }; let servers = servers.into_iter().map(|s| json::from_value(s).unwrap()).collect(); - block_on(builder.electrum_client(args, servers)).unwrap() + let abortable_system = AbortableQueue::default(); + block_on(builder.electrum_client(abortable_system, args, servers)).unwrap() } /// Returned client won't work by default, requires some mocks to be usable #[cfg(not(target_arch = "wasm32"))] fn native_client_for_test() -> NativeClient { NativeClient(Arc::new(NativeClientImpl::default())) } -fn utxo_coin_fields_for_test( +fn utxo_coin_for_test( rpc_client: UtxoRpcClientEnum, force_seed: Option<&str>, is_segwit_coin: bool, -) -> UtxoCoinFields { - let checksum_type = ChecksumType::DSHA256; - let default_seed = "spice describe gravity federal blast come thank unfair canal monkey style afraid"; - let seed = match force_seed { - Some(s) => s.into(), - None => match std::env::var("BOB_PASSPHRASE") { - Ok(p) => { - if p.is_empty() { - default_seed.into() - } else { - p - } - }, - Err(_) => default_seed.into(), - }, - }; - let key_pair = key_pair_from_seed(&seed).unwrap(); - let my_address = Address { - prefix: 60, - hash: key_pair.public().address_hash().into(), - t_addr_prefix: 0, - checksum_type, - 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 { - None - }; - - UtxoCoinFields { - conf: UtxoCoinConf { - is_pos: false, - requires_notarization: false.into(), - overwintered: true, - segwit: true, - tx_version: 4, - default_address_format: UtxoAddressFormat::Standard, - asset_chain: true, - p2sh_addr_prefix: 85, - 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, - tx_fee_volatility_percent: DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT, - version_group_id: 0x892f2085, - consensus_branch_id: 0x76b809bb, - zcash: true, - checksum_type, - fork_id: 0, - signature_version: SignatureVersion::Base, - required_confirmations: 1.into(), - force_min_relay_fee: false, - mtp_block_count: NonZeroU64::new(11).unwrap(), - estimate_fee_mode: None, - mature_confirmations: MATURE_CONFIRMATIONS_DEFAULT, - estimate_fee_blocks: 1, - trezor_coin: None, - enable_spv_proof: false, - }, - decimals: TEST_COIN_DECIMALS, - dust_amount: UTXO_DUST_AMOUNT, - tx_fee: TxFee::FixedPerKb(1000), - rpc_client, - priv_key_policy, - derivation_method, - history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - 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, - } +) -> UtxoStandardCoin { + utxo_coin_from_fields(utxo_coin_fields_for_test(rpc_client, force_seed, is_segwit_coin)) } -fn utxo_coin_from_fields(coin: UtxoCoinFields) -> UtxoStandardCoin { - let arc: UtxoArc = coin.into(); - arc.into() +/// Returns `TransactionDetails` of the given `tx_hash` via [`UtxoStandardOps::tx_details_by_hash`]. +#[track_caller] +fn get_tx_details_by_hash(coin: &Coin, tx_hash: &str) -> TransactionDetails { + let hash = hex::decode(tx_hash).unwrap(); + let mut input_transactions = HistoryUtxoTxMap::new(); + + block_on(UtxoStandardOps::tx_details_by_hash( + coin, + &hash, + &mut input_transactions, + )) + .unwrap() } -fn utxo_coin_for_test( - rpc_client: UtxoRpcClientEnum, - force_seed: Option<&str>, - is_segwit_coin: bool, -) -> UtxoStandardCoin { - utxo_coin_from_fields(utxo_coin_fields_for_test(rpc_client, force_seed, is_segwit_coin)) +/// Returns `TransactionDetails` of the given `tx_hash` via [`UtxoTxHistoryOps::tx_details_by_hash`]. +fn get_tx_details_by_hash_v2(coin: &Coin, tx_hash: &str, height: u64, timestamp: u64) -> Vec +where + Coin: CoinWithTxHistoryV2 + UtxoTxHistoryOps, +{ + let my_addresses = block_on(coin.my_addresses()).unwrap(); + let (_ctx, storage) = init_storage_for(coin); + let params = UtxoTxDetailsParams { + hash: &hex::decode(tx_hash).unwrap().as_slice().into(), + block_height_and_time: Some(BlockHeightAndTime { height, timestamp }), + storage: &storage, + my_addresses: &my_addresses, + }; + + block_on(UtxoTxHistoryOps::tx_details_by_hash(coin, params)).unwrap() +} + +/// Returns `TransactionDetails` of the given `tx_hash` and checks that +/// [`UtxoTxHistoryOps::tx_details_by_hash`] and [`UtxoStandardOps::tx_details_by_hash`] return the same TX details. +#[track_caller] +fn get_tx_details_eq_for_both_versions(coin: &Coin, tx_hash: &str) -> TransactionDetails +where + Coin: CoinWithTxHistoryV2 + UtxoTxHistoryOps + UtxoStandardOps, +{ + let tx_details_v1 = get_tx_details_by_hash(coin, tx_hash); + let tx_details_v2 = get_tx_details_by_hash_v2(coin, tx_hash, tx_details_v1.block_height, tx_details_v1.timestamp); + + assert_eq!(vec![tx_details_v1.clone()], tx_details_v2); + tx_details_v1 } #[test] @@ -193,7 +149,7 @@ fn test_extract_secret() { let tx_hex = hex::decode("0100000001de7aa8d29524906b2b54ee2e0281f3607f75662cbc9080df81d1047b78e21dbc00000000d7473044022079b6c50820040b1fbbe9251ced32ab334d33830f6f8d0bf0a40c7f1336b67d5b0220142ccf723ddabb34e542ed65c395abc1fbf5b6c3e730396f15d25c49b668a1a401209da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365004c6b6304f62b0e5cb175210270e75970bb20029b3879ec76c4acd320a8d0589e003636264d01a7d566504bfbac6782012088a9142fb610d856c19fd57f2d0cffe8dff689074b3d8a882103f368228456c940ac113e53dad5c104cf209f2f102a409207269383b6ab9b03deac68ffffffff01d0dc9800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac40280e5c").unwrap(); let expected_secret = hex::decode("9da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365").unwrap(); let secret_hash = &*dhash160(&expected_secret); - let secret = coin.extract_secret(secret_hash, &tx_hex).unwrap(); + let secret = block_on(coin.extract_secret(secret_hash, &tx_hex)).unwrap(); assert_eq!(secret, expected_secret); } @@ -203,16 +159,17 @@ fn test_send_maker_spends_taker_payment_recoverable_tx() { let coin = utxo_coin_for_test(client.into(), None, false); let tx_hex = hex::decode("0100000001de7aa8d29524906b2b54ee2e0281f3607f75662cbc9080df81d1047b78e21dbc00000000d7473044022079b6c50820040b1fbbe9251ced32ab334d33830f6f8d0bf0a40c7f1336b67d5b0220142ccf723ddabb34e542ed65c395abc1fbf5b6c3e730396f15d25c49b668a1a401209da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365004c6b6304f62b0e5cb175210270e75970bb20029b3879ec76c4acd320a8d0589e003636264d01a7d566504bfbac6782012088a9142fb610d856c19fd57f2d0cffe8dff689074b3d8a882103f368228456c940ac113e53dad5c104cf209f2f102a409207269383b6ab9b03deac68ffffffff01d0dc9800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac40280e5c").unwrap(); let secret = hex::decode("9da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365").unwrap(); - + let maker_spends_payment_args = SendMakerSpendsTakerPaymentArgs { + other_payment_tx: &tx_hex, + time_lock: 777, + other_pubkey: &coin.my_public_key().unwrap().to_vec(), + secret: &secret, + secret_hash: &*dhash160(&secret), + swap_contract_address: &coin.swap_contract_address(), + swap_unique_data: &[], + }; let tx_err = coin - .send_maker_spends_taker_payment( - &tx_hex, - 777, - &coin.my_public_key().unwrap().to_vec(), - &secret, - &coin.swap_contract_address(), - &[], - ) + .send_maker_spends_taker_payment(maker_spends_payment_args) .wait() .unwrap_err(); @@ -278,7 +235,7 @@ fn test_generate_transaction() { }]; let outputs = vec![TransactionOutput { - script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_iguana().hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_single_addr().hash).to_bytes(), value: 100000, }]; @@ -455,21 +412,44 @@ fn test_wait_for_payment_spend_timeout_native() { let from_block = 1000; assert!(coin - .wait_for_tx_spend(&transaction, wait_until, from_block, &None) + .wait_for_htlc_tx_spend( + &transaction, + &[], + wait_until, + from_block, + &None, + TAKER_PAYMENT_SPEND_SEARCH_INTERVAL + ) .wait() .is_err()); assert!(unsafe { OUTPUT_SPEND_CALLED }); } +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_wait_for_payment_spend_timeout_electrum() { static mut OUTPUT_SPEND_CALLED: bool = false; + ElectrumClient::find_output_spend.mock_safe(|_, _, _, _, _| { unsafe { OUTPUT_SPEND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(None))) }); - let client = ElectrumClientImpl::new(TEST_COIN_NAME.into(), Default::default()); + let block_headers_storage = BlockHeaderStorage { + inner: Box::new(SqliteBlockHeadersStorage { + ticker: TEST_COIN_NAME.into(), + conn: Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), + }), + }; + let abortable_system = AbortableQueue::default(); + + let client = ElectrumClientImpl::new( + TEST_COIN_NAME.into(), + Default::default(), + block_headers_storage, + abortable_system, + true, + ); let client = UtxoRpcClientEnum::Electrum(ElectrumClient(Arc::new(client))); let coin = utxo_coin_for_test(client, None, false); let transaction = hex::decode("01000000000102fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f00000000494830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01eeffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac000247304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee0121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635711000000") @@ -478,7 +458,14 @@ fn test_wait_for_payment_spend_timeout_electrum() { let from_block = 1000; assert!(coin - .wait_for_tx_spend(&transaction, wait_until, from_block, &None) + .wait_for_htlc_tx_spend( + &transaction, + &[], + wait_until, + from_block, + &None, + TAKER_PAYMENT_SPEND_SEARCH_INTERVAL + ) .wait() .is_err()); assert!(unsafe { OUTPUT_SPEND_CALLED }); @@ -949,7 +936,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().derivation_method.unwrap_iguana().hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_single_addr().hash).to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -964,18 +951,24 @@ 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() + let header: BlockHeader = deserialize( + block_on(client.blockchain_block_header(452248).compat()) + .unwrap() + .as_slice(), + ) + .unwrap(); + let mut headers = HashMap::new(); + headers.insert(452248, header); + let storage = client.block_headers_storage(); + block_on(storage.add_block_headers_to_storage(headers)).unwrap(); + + let res = block_on(client.validate_spv_proof(&tx, now_ms() / 1000 + 30)); + res.unwrap(); } #[test] @@ -988,6 +981,10 @@ fn list_since_block_btc_serde() { #[test] // https://github.com/KomodoPlatform/atomicDEX-API/issues/587 fn get_tx_details_coinbase_transaction() { + /// Hash of coinbase transaction + /// https://morty.explorer.dexstats.info/tx/b59b093ed97c1798f2a88ee3375a0c11d0822b6e4468478777f899891abd34a5 + const TX_HASH: &str = "b59b093ed97c1798f2a88ee3375a0c11d0822b6e4468478777f899891abd34a5"; + let client = electrum_client_for_test(&[ "electrum1.cipig.net:10018", "electrum2.cipig.net:10018", @@ -999,16 +996,8 @@ fn get_tx_details_coinbase_transaction() { false, ); - let fut = async move { - // hash of coinbase transaction https://morty.explorer.dexstats.info/tx/b59b093ed97c1798f2a88ee3375a0c11d0822b6e4468478777f899891abd34a5 - let hash = hex::decode("b59b093ed97c1798f2a88ee3375a0c11d0822b6e4468478777f899891abd34a5").unwrap(); - - let mut input_transactions = HistoryUtxoTxMap::new(); - let tx_details = coin.tx_details_by_hash(&hash, &mut input_transactions).await.unwrap(); - assert!(tx_details.from.is_empty()); - }; - - block_on(fut); + let tx_details = get_tx_details_eq_for_both_versions(&coin, TX_HASH); + assert!(tx_details.from.is_empty()); } #[test] @@ -1350,6 +1339,8 @@ fn test_get_median_time_past_from_native_does_not_have_median_in_get_block() { #[test] fn test_cashaddresses_in_tx_details_by_hash() { + const TX_HASH: &str = "0f2f6e0c8f440c641895023782783426c3aca1acc78d7c0db7751995e8aa5751"; + let conf = json!({ "coin": "BCH", "pubtype": 0, @@ -1371,28 +1362,20 @@ 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_with_priv_key( - &ctx, "BCH", &conf, ¶ms, &[1u8; 32], - )) - .unwrap(); - - let hash = hex::decode("0f2f6e0c8f440c641895023782783426c3aca1acc78d7c0db7751995e8aa5751").unwrap(); - 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); + let priv_key = Secp256k1Secret::from([1; 32]); + let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, "BCH", &conf, ¶ms, priv_key)).unwrap(); - assert!(tx_details - .from - .iter() - .any(|addr| addr == "bchtest:qze8g4gx3z428jjcxzpycpxl7ke7d947gca2a7n2la")); - assert!(tx_details - .to - .iter() - .any(|addr| addr == "bchtest:qr39na5d25wdeecgw3euh9fkd4ygvd4pnsury96597")); - }; + let tx_details = get_tx_details_eq_for_both_versions(&coin, TX_HASH); + log!("{:?}", tx_details); - block_on(fut); + assert!(tx_details + .from + .iter() + .any(|addr| addr == "bchtest:qze8g4gx3z428jjcxzpycpxl7ke7d947gca2a7n2la")); + assert!(tx_details + .to + .iter() + .any(|addr| addr == "bchtest:qr39na5d25wdeecgw3euh9fkd4ygvd4pnsury96597")); } #[test] @@ -1418,17 +1401,20 @@ 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_with_priv_key( - &ctx, "BCH", &conf, ¶ms, &[1u8; 32], - )) - .unwrap(); + let priv_key = Secp256k1Secret::from([1; 32]); + let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, "BCH", &conf, ¶ms, priv_key)).unwrap(); // other error on parse - let error = coin - .address_from_str("bitcoincash:000000000000000000000000000000000000000000") + let error = UtxoCommonOps::address_from_str(&coin, "bitcoincash:000000000000000000000000000000000000000000") .err() .unwrap(); - assert!(error.contains("Invalid address: bitcoincash:000000000000000000000000000000000000000000")); + match error.into_inner() { + AddrFromStrError::CannotDetermineFormat(_) => (), + other => panic!( + "Expected 'AddrFromStrError::CannotDetermineFormat' error, found: {}", + other + ), + } } #[test] @@ -1453,23 +1439,36 @@ 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_with_priv_key( - &ctx, "BCH", &conf, ¶ms, &[1u8; 32], - )) - .unwrap(); + let priv_key = Secp256k1Secret::from([1; 32]); + let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, "BCH", &conf, ¶ms, priv_key)).unwrap(); - let error = coin - .address_from_str("bitcoincash:qzxqqt9lh4feptf0mplnk58gnajfepzwcq9f2rxk55") + let error = UtxoCommonOps::address_from_str(&coin, "bitcoincash:qzxqqt9lh4feptf0mplnk58gnajfepzwcq9f2rxk55") .err() .unwrap(); - assert!(error.contains("Legacy address format activated for BCH, but CashAddress format used instead")); + match error.into_inner() { + AddrFromStrError::Unsupported(UnsupportedAddr::FormatMismatch { + ticker, + activated_format, + used_format, + }) => { + assert_eq!(ticker, "BCH"); + assert_eq!(activated_format, "Legacy"); + assert_eq!(used_format, "CashAddress"); + }, + other => panic!("Expected 'UnsupportedAddr::FormatMismatch' error, found: {}", other), + } // other error on parse - let error = coin - .address_from_str("0000000000000000000000000000000000") + let error = UtxoCommonOps::address_from_str(&coin, "0000000000000000000000000000000000") .err() .unwrap(); - assert!(error.contains("Invalid address: 0000000000000000000000000000000000")); + match error.into_inner() { + AddrFromStrError::CannotDetermineFormat(_) => (), + other => panic!( + "Expected 'AddrFromStrError::CannotDetermineFormat' error, found: {}", + other + ), + } } #[test] @@ -1481,13 +1480,17 @@ fn test_network_info_negative_time_offset() { #[test] fn test_unavailable_electrum_proto_version() { - ElectrumClientImpl::new.mock_safe(|coin_ticker, event_handlers| { - MockResult::Return(ElectrumClientImpl::with_protocol_version( - coin_ticker, - event_handlers, - OrdRange::new(1.8, 1.9).unwrap(), - )) - }); + ElectrumClientImpl::new.mock_safe( + |coin_ticker, event_handlers, block_headers_storage, abortable_system, _| { + MockResult::Return(ElectrumClientImpl::with_protocol_version( + coin_ticker, + event_handlers, + OrdRange::new(1.8, 1.9).unwrap(), + block_headers_storage, + abortable_system, + )) + }, + ); let conf = json!({"coin":"RICK","asset":"RICK","rpcport":8923}); let req = json!({ @@ -1497,11 +1500,10 @@ fn test_unavailable_electrum_proto_version() { let ctx = MmCtxBuilder::new().into_mm_arc(); 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(); + let priv_key = Secp256k1Secret::from([1; 32]); + let error = block_on(utxo_standard_coin_with_priv_key(&ctx, "RICK", &conf, ¶ms, priv_key)) + .err() + .unwrap(); log!("Error: {}", error); assert!(error.contains("There are no Electrums with the required protocol version")); } @@ -1525,13 +1527,13 @@ fn test_spam_rick() { "RICK", &conf, ¶ms, - &*key_pair.private().secret, + key_pair.private().secret, )) .unwrap(); let output = TransactionOutput { value: 1000000, - script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_iguana().hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_single_addr().hash).to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -1573,10 +1575,8 @@ fn test_one_unavailable_electrum_proto_version() { 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, "BTC", &conf, ¶ms, &[1u8; 32], - )) - .unwrap(); + let priv_key = Secp256k1Secret::from([1; 32]); + let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, "BTC", &conf, ¶ms, priv_key)).unwrap(); block_on(async { Timer::sleep(0.5).await }); @@ -1585,10 +1585,10 @@ fn test_one_unavailable_electrum_proto_version() { #[test] fn test_qtum_generate_pod() { - let priv_key = [ + let priv_key = Secp256k1Secret::from([ 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","rpcport":13889,"pubtype":120,"p2shtype":110}); let req = json!({ "method": "electrum", @@ -1598,7 +1598,7 @@ fn test_qtum_generate_pod() { 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 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(); @@ -1621,7 +1621,7 @@ fn test_qtum_add_delegation() { "tQTUM", &conf, ¶ms, - keypair.private().secret.as_slice(), + keypair.private().secret, )) .unwrap(); let address = Address::from_str("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE").unwrap(); @@ -1660,7 +1660,7 @@ fn test_qtum_add_delegation_on_already_delegating() { "tQTUM", &conf, ¶ms, - keypair.private().secret.as_slice(), + keypair.private().secret, )) .unwrap(); let address = Address::from_str("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE").unwrap(); @@ -1691,7 +1691,7 @@ fn test_qtum_get_delegation_infos() { "tQTUM", &conf, ¶ms, - keypair.private().secret.as_slice(), + keypair.private().secret, )) .unwrap(); let staking_infos = coin.get_delegation_infos().wait().unwrap(); @@ -1721,7 +1721,7 @@ fn test_qtum_remove_delegation() { "tQTUM", &conf, ¶ms, - keypair.private().secret.as_slice(), + keypair.private().secret, )) .unwrap(); let res = coin.remove_delegation().wait(); @@ -1774,13 +1774,13 @@ fn test_qtum_my_balance() { let ctx = MmCtxBuilder::new().into_mm_arc(); - let priv_key = [ + let priv_key = Secp256k1Secret::from([ 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 params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = block_on(qtum_coin_with_priv_key(&ctx, "tQTUM", &conf, ¶ms, &priv_key)).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); @@ -1810,13 +1810,13 @@ fn test_qtum_my_balance_with_check_utxo_maturity_false() { let ctx = MmCtxBuilder::new().into_mm_arc(); - let priv_key = [ + let priv_key = Secp256k1Secret::from([ 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 params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = block_on(qtum_coin_with_priv_key(&ctx, "tQTUM", &conf, ¶ms, &priv_key)).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(DISPLAY_BALANCE); @@ -2527,17 +2527,15 @@ fn test_validate_fee_wrong_sender() { let tx_bytes = hex::decode("0400008085202f890199cc492c24cc617731d13cff0ef22e7b0c277a64e7368a615b46214424a1c894020000006a473044022071edae37cf518e98db3f7637b9073a7a980b957b0c7b871415dbb4898ec3ebdc022031b402a6b98e64ffdf752266449ca979a9f70144dba77ed7a6a25bfab11648f6012103ad6f89abc2e5beaa8a3ac28e22170659b3209fe2ddf439681b4b8f31508c36faffffffff0202290200000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ac8a96e70b000000001976a914d55f0df6cb82630ad21a4e6049522a6f2b6c9d4588ac8afb2c60000000000000000000000000000000").unwrap(); let taker_fee_tx = coin.tx_enum_from_bytes(&tx_bytes).unwrap(); let amount: BigDecimal = "0.0014157".parse().unwrap(); - let validate_err = coin - .validate_fee( - &taker_fee_tx, - &*DEX_FEE_ADDR_RAW_PUBKEY, - &*DEX_FEE_ADDR_RAW_PUBKEY, - &amount, - 0, - &[], - ) - .wait() - .unwrap_err(); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &taker_fee_tx, + expected_sender: &*DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 0, + uuid: &[], + }; + let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); assert!(validate_err.contains("was sent from wrong address")); } @@ -2554,17 +2552,15 @@ fn test_validate_fee_min_block() { let taker_fee_tx = coin.tx_enum_from_bytes(&tx_bytes).unwrap(); 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, - &[], - ) - .wait() - .unwrap_err(); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &taker_fee_tx, + expected_sender: &sender_pub, + fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 810329, + uuid: &[], + }; + let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); assert!(validate_err.contains("confirmed before min_block")); } @@ -2582,9 +2578,15 @@ 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, &[]) - .wait() - .unwrap(); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &taker_fee_tx, + expected_sender: &sender_pub, + fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + amount: &amount, + min_block_number: 0, + uuid: &[], + }; + coin.validate_fee(validate_fee_args).wait().unwrap(); } #[test] @@ -2651,10 +2653,11 @@ fn firo_lelantus_tx_details() { "electrumx03.firo.org:50001", ]); let coin = utxo_coin_for_test(electrum.into(), None, false); - let mut map = HashMap::new(); - let tx_hash = hex::decode("ad812911f5cba3eab7c193b6cd7020ea02fb5c25634ae64959c3171a6bd5a74d").unwrap(); - let tx_details = block_on(coin.tx_details_by_hash(&tx_hash, &mut map)).unwrap(); + let tx_details = get_tx_details_eq_for_both_versions( + &coin, + "ad812911f5cba3eab7c193b6cd7020ea02fb5c25634ae64959c3171a6bd5a74d", + ); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -2662,8 +2665,10 @@ fn firo_lelantus_tx_details() { }); assert_eq!(Some(expected_fee), tx_details.fee_details); - let tx_hash = hex::decode("06ed4b75010edcf404a315be70903473f44050c978bc37fbcee90e0b49114ba8").unwrap(); - let tx_details = block_on(coin.tx_details_by_hash(&tx_hash, &mut map)).unwrap(); + let tx_details = get_tx_details_eq_for_both_versions( + &coin, + "06ed4b75010edcf404a315be70903473f44050c978bc37fbcee90e0b49114ba8", + ); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -2700,8 +2705,9 @@ fn test_generate_tx_doge_fee() { let ctx = MmCtxBuilder::default().into_mm_arc(); let params = UtxoActivationParams::from_legacy_req(&request).unwrap(); + let priv_key = Secp256k1Secret::from([1; 32]); let doge = block_on(utxo_standard_coin_with_priv_key( - &ctx, "DOGE", &config, ¶ms, &[1; 32], + &ctx, "DOGE", &config, ¶ms, priv_key, )) .unwrap(); @@ -2882,12 +2888,13 @@ 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.derivation_method = DerivationMethod::Iguana(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); let coin = utxo_coin_from_fields(fields); - let mut input_transactions = HistoryUtxoTxMap::new(); - let hash = hex::decode("535ffa3387d3fca14f4a4d373daf7edf00e463982755afce89bc8c48d8168024").unwrap(); - let tx_details = block_on(coin.tx_details_by_hash(&hash, &mut input_transactions)).expect("!tx_details_by_hash"); + let tx_details = get_tx_details_eq_for_both_versions( + &coin, + "535ffa3387d3fca14f4a4d373daf7edf00e463982755afce89bc8c48d8168024", + ); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some("KMD".into()), @@ -2909,6 +2916,8 @@ fn test_tx_details_kmd_rewards() { #[ignore] #[cfg(not(target_arch = "wasm32"))] fn test_tx_details_kmd_rewards_claimed_by_other() { + const TX_HASH: &str = "f09e8894959e74c1e727ffa5a753a30bf2dc6d5d677cc1f24b7ee5bb64e32c7d"; + let electrum = electrum_client_for_test(&[ "electrum1.cipig.net:10001", "electrum2.cipig.net:10001", @@ -2916,12 +2925,10 @@ 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.derivation_method = DerivationMethod::Iguana(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); let coin = utxo_coin_from_fields(fields); - let mut input_transactions = HistoryUtxoTxMap::new(); - let hash = hex::decode("f09e8894959e74c1e727ffa5a753a30bf2dc6d5d677cc1f24b7ee5bb64e32c7d").unwrap(); - let tx_details = block_on(coin.tx_details_by_hash(&hash, &mut input_transactions)).expect("!tx_details_by_hash"); + let tx_details = get_tx_details_eq_for_both_versions(&coin, TX_HASH); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some("KMD".into()), @@ -2938,6 +2945,8 @@ fn test_tx_details_kmd_rewards_claimed_by_other() { #[test] fn test_tx_details_bch_no_rewards() { + const TX_HASH: &str = "eb13d926f15cbb896e0bcc7a1a77a4ec63504e57a1524c13a7a9b80f43ecb05c"; + let electrum = electrum_client_for_test(&[ "electroncash.de:50003", "tbch.loping.net:60001", @@ -2947,10 +2956,7 @@ fn test_tx_details_bch_no_rewards() { ]); let coin = utxo_coin_for_test(electrum.into(), None, false); - let mut input_transactions = HistoryUtxoTxMap::new(); - let hash = hex::decode("eb13d926f15cbb896e0bcc7a1a77a4ec63504e57a1524c13a7a9b80f43ecb05c").unwrap(); - let tx_details = block_on(coin.tx_details_by_hash(&hash, &mut input_transactions)).expect("!tx_details_by_hash"); - + let tx_details = get_tx_details_eq_for_both_versions(&coin, TX_HASH); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), amount: BigDecimal::from_str("0.00000452").unwrap(), @@ -2971,7 +2977,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.derivation_method = DerivationMethod::Iguana(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); let coin = utxo_coin_from_fields(fields); let mut input_transactions = HistoryUtxoTxMap::default(); @@ -3003,7 +3009,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.derivation_method = DerivationMethod::Iguana(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); let coin = utxo_coin_from_fields(fields); let mut input_transactions = HistoryUtxoTxMap::default(); @@ -3066,9 +3072,9 @@ 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().derivation_method.unwrap_iguana().hash.clone(), + hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), t_addr_prefix: coin.as_ref().conf.pub_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_iguana().checksum_type, + checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, hrp: coin.as_ref().conf.bech32_hrp.clone(), addr_format: UtxoAddressFormat::Standard, }; @@ -3114,9 +3120,9 @@ 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().derivation_method.unwrap_iguana().hash.clone(), + hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_iguana().checksum_type, + checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, hrp: coin.as_ref().conf.bech32_hrp.clone(), addr_format: UtxoAddressFormat::Standard, }; @@ -3162,9 +3168,9 @@ 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().derivation_method.unwrap_iguana().hash.clone(), + hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), t_addr_prefix: coin.as_ref().conf.pub_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_iguana().checksum_type, + checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, hrp: coin.as_ref().conf.bech32_hrp.clone(), addr_format: UtxoAddressFormat::Segwit, }; @@ -3214,10 +3220,8 @@ fn test_utxo_standard_with_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 priv_key = Secp256k1Secret::from([1; 32]); + let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, "RICK", &conf, ¶ms, priv_key)).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`]. @@ -3256,10 +3260,8 @@ fn test_utxo_standard_without_check_utxo_maturity() { 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 priv_key = Secp256k1Secret::from([1; 32]); + let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, "RICK", &conf, ¶ms, priv_key)).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`]. @@ -3293,7 +3295,8 @@ fn test_qtum_without_check_utxo_maturity() { 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 priv_key = Secp256k1Secret::from([1; 32]); + let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).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`]. @@ -3305,10 +3308,10 @@ fn test_qtum_without_check_utxo_maturity() { #[test] #[ignore] fn test_split_qtum() { - let priv_key = [ + let priv_key = Secp256k1Secret::from([ 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", @@ -3336,8 +3339,8 @@ fn test_split_qtum() { }); 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 coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).unwrap(); + let p2pkh_address = coin.as_ref().derivation_method.unwrap_single_addr(); 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"); @@ -3407,7 +3410,8 @@ fn test_qtum_with_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 priv_key = Secp256k1Secret::from([1; 32]); + let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).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`]. @@ -3478,21 +3482,24 @@ fn test_account_balance_rpc() { 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(), + account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), external_addresses_number: 7, internal_addresses_number: 3, + derived_addresses: HDAddressesCache::default(), }); 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(), + account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/1'").unwrap(), external_addresses_number: 0, internal_addresses_number: 1, + derived_addresses: HDAddressesCache::default(), }); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), hd_wallet_storage: HDWalletCoinStorage::default(), address_format: UtxoAddressFormat::Standard, - derivation_path: Bip44PathToCoin::from_str("m/44'/141'").unwrap(), + derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), accounts: HDAccountsMutex::new(hd_accounts), gap_limit: 3, }); @@ -3697,30 +3704,39 @@ fn test_scan_for_new_addresses() { }, ); - let mut checking_addresses: HashMap> = HashMap::new(); - let mut non_empty_addresses: Vec = Vec::new(); + // The list of addresses that were checked using [`UtxoAddressScanner::is_address_used`]. + static mut CHECKED_ADDRESSES: Vec = Vec::new(); + + // The map of addresses for those [`NativeClient::display_balance`] called. + let mut display_balances: HashMap = HashMap::new(); + // The expected list of the addresses that were checked using [`UtxoAddressScanner::is_address_used`]. + let mut expected_checked_addresses: Vec = Vec::new(); + // The list of addresses with a non-empty transaction history. + let mut non_empty_addresses: HashSet = HashSet::new(); + // The map of results by the addresses. 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); + if let Some(balance) = $balance { + display_balances.insert($address.to_string(), balance); + non_empty_addresses.insert($address.to_string()); + } + expected_checked_addresses.push($address.to_string()); 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))), + balance: CoinBalance::new(BigDecimal::from($balance.unwrap_or(0i32))), }); - 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) - }; + ($_der_path:literal, $address:literal) => {{ + let address = $address.to_string(); + expected_checked_addresses.push(address); + }}; } macro_rules! get_balances { @@ -3736,7 +3752,8 @@ fn test_scan_for_new_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). + unused_address!("m/44'/141'/0'/0/6", "RSvKMMegKGP5e2EanH7fnD4yNsxdJvLAmL"); + unused_address!("m/44'/141'/0'/0/7", "RX76e9G7H4Xy6cYrtr1qGghxytAmWpv375"); // 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)); @@ -3744,7 +3761,8 @@ fn test_scan_for_new_addresses() { 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). + unused_address!("m/44'/141'/0'/1/6", "RSkY6twW8knTcn6wGACUAG9crJHcuQ2kEH"); + unused_address!("m/44'/141'/0'/1/7", "RGRybU5awT9Chn9FeKZd8CEBREq5vNFDKJ"); // 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)); @@ -3754,59 +3772,61 @@ fn test_scan_for_new_addresses() { 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). + unused_address!("m/44'/141'/1'/0/7", "RYJ6vbhxFre5yChCMiJJFNTTBhAQbKM9AY"); + unused_address!("m/44'/141'/1'/0/8", "RWaND65Cucwc2Cs1djBUQ2z1rrxTopEjoG"); // 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). + unused_address!("m/44'/141'/1'/0/4", "RS4UZtkwZ8eYaTL1xodXgFNryJoTbPJYE5"); + unused_address!("m/44'/141'/1'/0/5", "RDzcAqivNqUCJA4auetoVE4hcmH2p4L1fB"); // 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 + let balance = display_balances .remove(&address) - .expect(&format!("Unexpected address: {}", address)) - .expect(&format!( - "'{}' address is empty. 'NativeClient::display_balance' must not be called for this address", - address - )); + .expect(&format!("Unexpected 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))) + UtxoAddressScanner::is_address_used.mock_safe(move |_, address| { + let address = address.to_string(); + unsafe { + CHECKED_ADDRESSES.push(address.clone()); + } + let is_used = non_empty_addresses.remove(&address); + MockResult::Return(Box::pin(futures::future::ok(is_used))) }); + // This mock is required just not to fail on [`UtxoAddressScanner::init`]. + NativeClient::list_all_transactions + .mock_safe(move |_, _| MockResult::Return(Box::new(futures01::future::ok(Vec::new())))); + 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(), + account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), external_addresses_number: 3, internal_addresses_number: 1, + derived_addresses: HDAddressesCache::default(), }); 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(), + account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/1'").unwrap(), external_addresses_number: 0, internal_addresses_number: 2, + derived_addresses: HDAddressesCache::default(), }); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), hd_wallet_storage: HDWalletCoinStorage::default(), address_format: UtxoAddressFormat::Standard, - derivation_path: Bip44PathToCoin::from_str("m/44'/141'").unwrap(), + derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), accounts: HDAccountsMutex::new(hd_accounts), gap_limit: 3, }); @@ -3871,6 +3891,204 @@ fn test_scan_for_new_addresses() { assert_eq!(accounts[&0].internal_addresses_number, 4); assert_eq!(accounts[&1].external_addresses_number, 5); assert_eq!(accounts[&1].internal_addresses_number, 2); + assert_eq!(unsafe { &CHECKED_ADDRESSES }, &expected_checked_addresses); +} + +#[test] +fn test_get_new_address() { + static mut EXPECTED_CHECKED_ADDRESSES: Vec = Vec::new(); + static mut CHECKED_ADDRESSES: Vec = Vec::new(); + static mut NON_EMPTY_ADDRESSES: Option> = None; + + macro_rules! expected_checked_addresses { + ($($_der_path:literal, $addr:literal);*) => { + unsafe { + CHECKED_ADDRESSES.clear(); + EXPECTED_CHECKED_ADDRESSES = vec![$($addr.to_string()),*]; + } + }; + } + + macro_rules! non_empty_addresses { + ($($_der_path:literal, $addr:literal);*) => { + unsafe { + NON_EMPTY_ADDRESSES = Some(vec![$($addr.to_string()),*].into_iter().collect()); + } + }; + } + + HDWalletMockStorage::update_external_addresses_number + .mock_safe(|_, _, _account_id, _new_val| MockResult::Return(Box::pin(futures::future::ok(())))); + HDWalletMockStorage::update_internal_addresses_number + .mock_safe(|_, _, _account_id, _new_val| MockResult::Return(Box::pin(futures::future::ok(())))); + + // This mock is required just not to fail on [`UtxoStandardCoin::known_address_balance`]. + NativeClient::display_balance + .mock_safe(move |_, _, _| MockResult::Return(Box::new(futures01::future::ok(BigDecimal::from(0))))); + + UtxoAddressScanner::is_address_used.mock_safe(move |_, address| { + let address = address.to_string(); + unsafe { + CHECKED_ADDRESSES.push(address.clone()); + let is_used = NON_EMPTY_ADDRESSES.as_mut().unwrap().remove(&address); + MockResult::Return(Box::pin(futures::future::ok(is_used))) + } + }); + + MockableConfirmAddress::confirm_utxo_address + .mock_safe(move |_, _, _, _| MockResult::Return(Box::pin(futures::future::ok(())))); + + // This mock is required just not to fail on [`UtxoAddressScanner::init`]. + NativeClient::list_all_transactions + .mock_safe(move |_, _| MockResult::Return(Box::new(futures01::future::ok(Vec::new())))); + + 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(); + let hd_account_for_test = UtxoHDAccount { + account_id: 0, + extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), + account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), + external_addresses_number: 4, + internal_addresses_number: 0, + derived_addresses: HDAddressesCache::default(), + }; + // Put multiple the same accounts for tests, + // since every successful `get_new_address_rpc` changes the state of the account. + hd_accounts.insert(0, hd_account_for_test.clone()); + hd_accounts.insert(1, hd_account_for_test.clone()); + hd_accounts.insert(2, hd_account_for_test); + + fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), + hd_wallet_storage: HDWalletCoinStorage::default(), + address_format: UtxoAddressFormat::Standard, + derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + gap_limit: 2, + }); + fields.conf.trezor_coin = Some("Komodo".to_string()); + let coin = utxo_coin_from_fields(fields); + + // ======= + + let confirm_address = MockableConfirmAddress::default(); + + expected_checked_addresses!["m/44'/141'/0'/0/3", "RU1gRFXWXNx7uPRAEJ7wdZAW1RZ4TE6Vv1"]; + non_empty_addresses!["m/44'/141'/0'/0/3", "RU1gRFXWXNx7uPRAEJ7wdZAW1RZ4TE6Vv1"]; + let params = GetNewAddressParams { + account_id: 0, + chain: Some(Bip44Chain::External), + gap_limit: None, // Will be used 2 from `UtxoHDWallet` by default. + }; + block_on(coin.get_new_address_rpc(params, &confirm_address)).unwrap(); + unsafe { assert_eq!(CHECKED_ADDRESSES, EXPECTED_CHECKED_ADDRESSES) }; + + // `m/44'/141'/1'/0/3` is empty, so `m/44'/141'/1'/0/2` will be checked. + + expected_checked_addresses!["m/44'/141'/1'/0/3", "RU1gRFXWXNx7uPRAEJ7wdZAW1RZ4TE6Vv1"]; + non_empty_addresses!["m/44'/141'/1'/0/2", "RSSZjtgfnLzvqF4cZQJJEpN5gvK3pWmd3h"]; + let params = GetNewAddressParams { + account_id: 1, + chain: Some(Bip44Chain::External), + gap_limit: Some(1), + }; + let err = block_on(coin.get_new_address_rpc(params, &confirm_address)) + .expect_err("get_new_address_rpc should have failed with 'EmptyAddressesLimitReached' error"); + let expected = GetNewAddressRpcError::EmptyAddressesLimitReached { gap_limit: 1 }; + assert_eq!(err.into_inner(), expected); + unsafe { assert_eq!(CHECKED_ADDRESSES, EXPECTED_CHECKED_ADDRESSES) }; + + // `m/44'/141'/1'/0/3` is empty, but `m/44'/141'/1'/0/2` is not. + + expected_checked_addresses![ + "m/44'/141'/1'/0/3", "RU1gRFXWXNx7uPRAEJ7wdZAW1RZ4TE6Vv1"; + "m/44'/141'/1'/0/2", "RSSZjtgfnLzvqF4cZQJJEpN5gvK3pWmd3h" + ]; + non_empty_addresses!["m/44'/141'/1'/0/2", "RSSZjtgfnLzvqF4cZQJJEpN5gvK3pWmd3h"]; + let params = GetNewAddressParams { + account_id: 1, + chain: Some(Bip44Chain::External), + gap_limit: Some(2), + }; + block_on(coin.get_new_address_rpc(params, &confirm_address)).unwrap(); + unsafe { assert_eq!(CHECKED_ADDRESSES, EXPECTED_CHECKED_ADDRESSES) }; + + // `m/44'/141'/2'/0/3` and `m/44'/141'/2'/0/2` are empty. + + expected_checked_addresses![ + "m/44'/141'/2'/0/3", "RU1gRFXWXNx7uPRAEJ7wdZAW1RZ4TE6Vv1"; + "m/44'/141'/2'/0/2", "RSSZjtgfnLzvqF4cZQJJEpN5gvK3pWmd3h" + ]; + non_empty_addresses![]; + let params = GetNewAddressParams { + account_id: 2, + chain: Some(Bip44Chain::External), + gap_limit: Some(2), + }; + let err = block_on(coin.get_new_address_rpc(params, &confirm_address)) + .expect_err("get_new_address_rpc should have failed with 'EmptyAddressesLimitReached' error"); + let expected = GetNewAddressRpcError::EmptyAddressesLimitReached { gap_limit: 2 }; + assert_eq!(err.into_inner(), expected); + unsafe { assert_eq!(CHECKED_ADDRESSES, EXPECTED_CHECKED_ADDRESSES) }; + + // `gap_limit=0` means don't allow to generate new address if the last one is empty yet. + + expected_checked_addresses!["m/44'/141'/2'/0/3", "RU1gRFXWXNx7uPRAEJ7wdZAW1RZ4TE6Vv1"]; + non_empty_addresses![]; + let params = GetNewAddressParams { + account_id: 2, + chain: Some(Bip44Chain::External), + gap_limit: Some(0), + }; + let err = block_on(coin.get_new_address_rpc(params, &confirm_address)) + .expect_err("!get_new_address_rpc should have failed with 'EmptyAddressesLimitReached' error"); + let expected = GetNewAddressRpcError::EmptyAddressesLimitReached { gap_limit: 0 }; + assert_eq!(err.into_inner(), expected); + unsafe { assert_eq!(CHECKED_ADDRESSES, EXPECTED_CHECKED_ADDRESSES) }; + + // `(gap_limit=5) > (known_addresses_number=4)`, there should not be any network request. + + expected_checked_addresses![]; + non_empty_addresses![]; + let params = GetNewAddressParams { + account_id: 2, + chain: Some(Bip44Chain::External), + gap_limit: Some(5), + }; + block_on(coin.get_new_address_rpc(params, &confirm_address)).unwrap(); + unsafe { assert_eq!(CHECKED_ADDRESSES, EXPECTED_CHECKED_ADDRESSES) }; + + // `known_addresses_number=0`, always allow. + + expected_checked_addresses![]; + non_empty_addresses![]; + let params = GetNewAddressParams { + account_id: 0, + chain: Some(Bip44Chain::Internal), + gap_limit: Some(0), + }; + block_on(coin.get_new_address_rpc(params, &confirm_address)).unwrap(); + unsafe { assert_eq!(CHECKED_ADDRESSES, EXPECTED_CHECKED_ADDRESSES) }; + + // Check if `get_new_address_rpc` fails on the `HDAddressConfirm::confirm_utxo_address` error. + + MockableConfirmAddress::confirm_utxo_address.mock_safe(move |_, _, _, _| { + MockResult::Return(Box::pin(futures::future::ready(MmError::err( + HDConfirmAddressError::HwContextNotInitialized, + )))) + }); + + expected_checked_addresses![]; + non_empty_addresses![]; + let params = GetNewAddressParams { + account_id: 0, + chain: Some(Bip44Chain::Internal), + gap_limit: Some(2), + }; + let err = block_on(coin.get_new_address_rpc(params, &confirm_address)) + .expect_err("!get_new_address_rpc should have failed with 'HwContextNotInitialized' error"); + assert_eq!(err.into_inner(), GetNewAddressRpcError::HwContextNotInitialized); } /// https://github.com/KomodoPlatform/atomicDEX-API/issues/1196 @@ -3893,6 +4111,29 @@ fn test_electrum_display_balances() { block_on(utxo_common_tests::test_electrum_display_balances(&rpc_client)); } +#[test] +fn test_for_non_existent_tx_hex_utxo_electrum() { + // This test shouldn't wait till timeout! + let timeout = (now_ms() / 1000) + 120; + 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, + ); + // bad transaction hex + let tx = hex::decode("0400008085202f8902bf17bf7d1daace52e08f732a6b8771743ca4b1cb765a187e72fd091a0aabfd52000000006a47304402203eaaa3c4da101240f80f9c5e9de716a22b1ec6d66080de6a0cca32011cd77223022040d9082b6242d6acf9a1a8e658779e1c655d708379862f235e8ba7b8ca4e69c6012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffffff023ca13c0e9e085dd13f481f193e8a3e8fd609020936e98b5587342d994f4d020000006b483045022100c0ba56adb8de923975052312467347d83238bd8d480ce66e8b709a7997373994022048507bcac921fdb2302fa5224ce86e41b7efc1a2e20ae63aa738dfa99b7be826012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0300e1f5050000000017a9141ee6d4c38a3c078eab87ad1a5e4b00f21259b10d87000000000000000016611400000000000000000000000000000000000000001b94d736000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac2d08e35e000000000000000000000000000000").unwrap(); + let actual = coin + .wait_for_confirmations(&tx, 1, false, timeout, 1) + .wait() + .err() + .unwrap(); + assert!(actual.contains( + "Tx d342ff9da528a2e262bddf2b6f9a27d1beb7aeb03f0fc8d9eac2987266447e44 was not found on chain after 10 tries" + )); +} + +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_native_display_balances() { let unspents = vec![ @@ -4013,3 +4254,244 @@ fn test_sign_verify_message_segwit() { .unwrap(); assert!(is_valid); } + +#[test] +fn test_tx_enum_from_bytes() { + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + let coin = utxo_coin_for_test(client.into(), None, false); + + let tx_hex = hex::decode("01000000017b1eabe0209b1fe794124575ef807057c77ada2138ae4fa8d6c4de0398a14f3f00000000494830450221008949f0cb400094ad2b5eb399d59d01c14d73d8fe6e96df1a7150deb388ab8935022079656090d7f6bac4c9a94e0aad311a4268e082a725f8aeae0573fb12ff866a5f01ffffffff01f0ca052a010000001976a914cbc20a7664f2f69e5355aa427045bc15e7c6c77288ac00000000").unwrap(); + coin.tx_enum_from_bytes(&tx_hex).unwrap(); + + let tx_hex = hex::decode("0100000002440f1a2929eb08c350cc8d2385c77c40411560c3b43b65efb5b06f997fc67672020000006b483045022100f82e88af256d2487afe0c30a166c9ecf6b7013e764e1407317c712d47f7731bd0220358a4d7987bfde2271599b5c4376d26f9ce9f1df2e04f5de8f89593352607110012103c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3edfffffffffb9c2fd7a19b55a4ffbda2ce5065d988a4f4efcf1ae567b4ddb6d97529c8fb0c000000006b483045022100dd75291db32dc859657a5eead13b85c340b4d508e57d2450ebfad76484f254130220727fcd65dda046ea62b449ab217da264dbf7c7ca7e63b39c8835973a152752c1012103c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3edffffffff03102700000000000017a9148d0ad41545dea44e914c419d33d422148c35a274870000000000000000166a149c0a919d4e9a23f0234df916a7dd21f9e2fdaa8f931d0000000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88acbd8ff160").unwrap(); + coin.tx_enum_from_bytes(&tx_hex).unwrap(); + + let tx_hex = hex::decode("0200000000010192a4497268107d7999e9551be733f5e0eab479be7d995a061a7bbdc43ef0e5ed0000000000feffffff02cd857a00000000001600145cb39bfcd68d520e29cadc990bceb5cd1562c507a0860100000000001600149a85cc05e9a722575feb770a217c73fd6145cf01024730440220030e0fb58889ab939c701f12d950f00b64836a1a33ec0d6697fd3053d469d244022053e33d72ef53b37b86eea8dfebbafffb0f919ef952dcb6ea6058b81576d8dc86012102225de6aed071dc29d0ca10b9f64a4b502e33e55b3c0759eedd8e333834c6a7d07a1f2000").unwrap(); + coin.tx_enum_from_bytes(&tx_hex).unwrap(); + + let err = coin.tx_enum_from_bytes(&vec![0; 1000000]).unwrap_err().into_inner(); + assert_eq!( + discriminant(&err), + discriminant(&TxMarshalingErr::CrossCheckFailed(String::new())) + ); +} + +#[test] +fn test_hd_utxo_tx_history() { + let client = electrum_client_for_test(MORTY_ELECTRUM_ADDRS); + block_on(utxo_common_tests::test_hd_utxo_tx_history_impl(client)); +} + +#[test] +fn test_utxo_validate_valid_and_invalid_pubkey() { + 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 priv_key = Secp256k1Secret::from([1; 32]); + let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, "RICK", &conf, ¶ms, priv_key)).unwrap(); + // Test expected to pass at this point as we're using a valid pubkey to validate against a valid pubkey + assert!(coin + .validate_other_pubkey(&[ + 3, 23, 183, 225, 206, 31, 159, 148, 195, 42, 67, 115, 146, 41, 248, 140, 11, 3, 51, 41, 111, 180, 110, 143, + 114, 134, 88, 73, 198, 174, 52, 184, 78 + ]) + .is_ok()); + // Test expected to fail at this point as we're using a valid pubkey to validate against an invalid pubkeys + assert!(coin.validate_other_pubkey(&[1u8; 20]).is_err()); + assert!(coin.validate_other_pubkey(&[1u8; 8]).is_err()); +} + +#[test] +fn test_block_header_utxo_loop() { + use crate::utxo::utxo_builder::{block_header_utxo_loop, BlockHeaderUtxoLoopExtraArgs}; + use futures::future::{Either, FutureExt}; + use keys::hash::H256 as H256Json; + + static mut CURRENT_BLOCK_COUNT: u64 = 13; + + ElectrumClient::get_block_count + .mock_safe(move |_| MockResult::Return(Box::new(futures01::future::ok(unsafe { CURRENT_BLOCK_COUNT })))); + let expected_steps: Arc>> = Arc::new(Mutex::new(Vec::with_capacity(14))); + + ElectrumClient::retrieve_headers.mock_safe({ + let expected_steps = expected_steps.clone(); + move |this, from, to| { + let (expected_from, expected_to) = expected_steps.lock().unwrap().remove(0); + assert_eq!(from, expected_from); + assert_eq!(to, expected_to); + MockResult::Continue((this, from, to)) + } + }); + + BlockHeaderUtxoLoopExtraArgs::default.mock_safe(move || { + MockResult::Return(BlockHeaderUtxoLoopExtraArgs { + chunk_size: 4, + error_sleep: 1., + success_sleep: 0.8, + }) + }); + + let ctx = mm_ctx_with_custom_db(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(IguanaPrivKey::from(H256Json::from([1u8; 32]))); + let servers: Vec<_> = RICK_ELECTRUM_ADDRS + .iter() + .map(|server| json!({ "url": server })) + .collect(); + let req = json!({ "method": "electrum", "servers": servers }); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let conf = json!({"coin":"RICK", "asset":"RICK", "rpcport":8923}); + let builder = UtxoArcBuilder::new(&ctx, "RICK", &conf, ¶ms, priv_key_policy, UtxoStandardCoin::from); + let arc: UtxoArc = block_on(builder.build_utxo_fields()).unwrap().into(); + let client = match &arc.rpc_client { + UtxoRpcClientEnum::Electrum(electrum) => electrum.clone(), + UtxoRpcClientEnum::Native(_) => unreachable!(), + }; + + let (sync_status_notifier, _) = channel::(1); + let loop_handle = UtxoSyncStatusLoopHandle::new(sync_status_notifier); + + let spv_conf = json::from_value(json!({ + "starting_block_header": { + "height": 1, + "hash": "0c714ba4f8d5f2d5c014a08c4e21a5387156e23bcc819c0f9bc536437586cdf5", + "time": 1564482125, + "bits": 537857807 + }, + "max_stored_block_headers": 15 + })); + + let loop_fut = async move { + block_header_utxo_loop( + arc.downgrade(), + UtxoStandardCoin::from, + loop_handle, + unsafe { CURRENT_BLOCK_COUNT }, + spv_conf.unwrap(), + ) + .await + }; + + let test_fut = async move { + *expected_steps.lock().unwrap() = vec![(2, 5), (6, 9), (10, 13), (14, 14)]; + unsafe { CURRENT_BLOCK_COUNT = 14 } + Timer::sleep(3.).await; + let get_headers_count = client + .block_headers_storage() + .get_last_block_height() + .await + .unwrap() + .unwrap(); + assert_eq!(get_headers_count, 14); + assert!(expected_steps.lock().unwrap().is_empty()); + + *expected_steps.lock().unwrap() = vec![(15, 18)]; + unsafe { CURRENT_BLOCK_COUNT = 18 } + Timer::sleep(2.).await; + let get_headers_count = client + .block_headers_storage() + .get_last_block_height() + .await + .unwrap() + .unwrap(); + assert_eq!(get_headers_count, 18); + assert!(expected_steps.lock().unwrap().is_empty()); + + *expected_steps.lock().unwrap() = vec![(19, 19)]; + unsafe { CURRENT_BLOCK_COUNT = 19 } + Timer::sleep(2.).await; + let get_headers_count = client + .block_headers_storage() + .get_last_block_height() + .await + .unwrap() + .unwrap(); + assert_eq!(get_headers_count, 19); + assert!(expected_steps.lock().unwrap().is_empty()); + + // Validate max_stored_block_headers + // Since max_stored_block_headers = 15, headers from 2 - 4 shouldn't be in + // storage anymore. + for i in 2..=19 { + let header = client.block_headers_storage().get_block_header(i).await.unwrap(); + if i >= 5 { + assert!(header.is_some()); + break; + } + + assert_eq!(header, None); + } + Timer::sleep(2.).await; + }; + + if let Either::Left(_) = block_on(futures::future::select(loop_fut.boxed(), test_fut.boxed())) { + panic!("Loop shouldn't stop") + }; +} + +#[test] +fn test_spv_conf_with_verification() { + let verification_params = BlockHeaderValidationParams { + difficulty_check: false, + constant_difficulty: false, + difficulty_algorithm: Some(DifficultyAlgorithm::BitcoinMainnet), + }; + + // Block header hash for BLOCK HEIGHT 4032 + let hash = "00000000ca4b69045a03d7b20624def97a5366418648d5005e82fd3b345d20d0".into(); + // test for good retarget_block_header_height + let mut spv_conf = SPVConf { + starting_block_header: SPVBlockHeader { + height: 4032, + hash, + time: 1234466190, + bits: BlockHeaderBits::Compact(486604799.into()), + }, + max_stored_block_headers: None, + validation_params: Some(verification_params.clone()), + }; + assert!(spv_conf.validate("BTC").is_ok()); + + // test for bad retarget_block_header_height + // Block header hash for BLOCK HEIGHT 4032 + let hash = "0000000045c689dc49dee778a9fbca7b5bc48fceca9f05cde5fc8d667f00e7d2".into(); + spv_conf.starting_block_header = SPVBlockHeader { + height: 4037, + hash, + time: 1234470475, + bits: BlockHeaderBits::Compact(486604799.into()), + }; + let validate = spv_conf.validate("BTC").err().unwrap(); + if let SPVError::WrongRetargetHeight { coin, expected_height } = validate { + assert_eq!(coin, "BTC"); + assert_eq!(expected_height, 4032); + } + + // test for bad max_stored_block_headers + // Block header hash for BLOCK HEIGHT 4032 + let hash = "00000000ca4b69045a03d7b20624def97a5366418648d5005e82fd3b345d20d0".into(); + spv_conf = SPVConf { + starting_block_header: SPVBlockHeader { + height: 4032, + hash, + time: 1234466190, + bits: BlockHeaderBits::Compact(486604799.into()), + }, + max_stored_block_headers: NonZeroU64::new(2000), + validation_params: Some(verification_params), + }; + let validate = spv_conf.validate("BTC").err().unwrap(); + assert!(validate + .to_string() + .contains("max_stored_block_headers 2000 must be greater than retargeting interval")); +} diff --git a/mm2src/coins/utxo/utxo_tx_history_v2.rs b/mm2src/coins/utxo/utxo_tx_history_v2.rs new file mode 100644 index 0000000000..f822a285b5 --- /dev/null +++ b/mm2src/coins/utxo/utxo_tx_history_v2.rs @@ -0,0 +1,726 @@ +use super::RequestTxHistoryResult; +use crate::hd_wallet::AddressDerivingError; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, DisplayAddress, TxHistoryStorage, TxHistoryStorageError}; +use crate::tx_history_storage::FilteringAddresses; +use crate::utxo::bch::BchCoin; +use crate::utxo::slp::ParseSlpScriptError; +use crate::utxo::{utxo_common, AddrFromStrError, GetBlockHeaderError}; +use crate::{BalanceError, BalanceResult, BlockHeightAndTime, HistorySyncState, MarketCoinOps, NumConversError, + ParseBigDecimalError, TransactionDetails, UnexpectedDerivationMethod, UtxoRpcError, UtxoTx}; +use async_trait::async_trait; +use common::executor::Timer; +use common::log::{error, info}; +use common::state_machine::prelude::*; +use derive_more::Display; +use keys::Address; +use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; +use rpc::v1::types::H256 as H256Json; +use std::collections::{hash_map::Entry, HashMap, HashSet}; +use std::iter::FromIterator; +use std::str::FromStr; + +macro_rules! try_or_stop_unknown { + ($exp:expr, $fmt:literal) => { + match $exp { + Ok(t) => t, + Err(e) => return Self::change_state(Stopped::unknown(format!("{}: {}", $fmt, e))), + } + }; +} + +#[derive(Debug, Display)] +pub enum UtxoMyAddressesHistoryError { + AddressDerivingError(AddressDerivingError), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), +} + +impl From for UtxoMyAddressesHistoryError { + fn from(e: AddressDerivingError) -> Self { UtxoMyAddressesHistoryError::AddressDerivingError(e) } +} + +impl From for UtxoMyAddressesHistoryError { + fn from(e: UnexpectedDerivationMethod) -> Self { UtxoMyAddressesHistoryError::UnexpectedDerivationMethod(e) } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Display)] +pub enum UtxoTxDetailsError { + #[display(fmt = "Storage error: {}", _0)] + StorageError(String), + #[display(fmt = "Transaction deserialization error: {}", _0)] + TxDeserializationError(serialization::Error), + #[display(fmt = "Invalid transaction: {}", _0)] + InvalidTransaction(String), + #[display(fmt = "TX Address deserialization error: {}", _0)] + TxAddressDeserializationError(String), + #[display(fmt = "{}", _0)] + NumConversionErr(NumConversError), + #[display(fmt = "RPC error: {}", _0)] + RpcError(UtxoRpcError), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for UtxoTxDetailsError { + fn from(e: serialization::Error) -> Self { UtxoTxDetailsError::TxDeserializationError(e) } +} + +impl From for UtxoTxDetailsError { + fn from(e: UtxoRpcError) -> Self { UtxoTxDetailsError::RpcError(e) } +} + +impl From for UtxoTxDetailsError { + fn from(e: NumConversError) -> Self { UtxoTxDetailsError::NumConversionErr(e) } +} + +impl From for UtxoTxDetailsError { + fn from(e: ParseBigDecimalError) -> Self { UtxoTxDetailsError::from(NumConversError::from(e)) } +} + +impl From for UtxoTxDetailsError { + fn from(err: ParseSlpScriptError) -> Self { + UtxoTxDetailsError::InvalidTransaction(format!("Error parsing SLP script: {err}")) + } +} + +impl From for UtxoTxDetailsError +where + StorageErr: TxHistoryStorageError, +{ + fn from(e: StorageErr) -> Self { UtxoTxDetailsError::StorageError(format!("{:?}", e)) } +} + +pub struct UtxoTxDetailsParams<'a, Storage> { + pub hash: &'a H256Json, + pub block_height_and_time: Option, + pub storage: &'a Storage, + pub my_addresses: &'a HashSet
, +} + +#[async_trait] +pub trait UtxoTxHistoryOps: CoinWithTxHistoryV2 + MarketCoinOps + Send + Sync + 'static { + /// Returns addresses for those we need to request Transaction history. + async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError>; + + /// Returns Transaction details by hash using the coin RPC if required. + async fn tx_details_by_hash( + &self, + params: UtxoTxDetailsParams<'_, T>, + ) -> MmResult, UtxoTxDetailsError> + where + T: TxHistoryStorage; + + /// Loads transaction from `storage` or requests it using coin RPC. + async fn tx_from_storage_or_rpc( + &self, + tx_hash: &H256Json, + storage: &Storage, + ) -> MmResult; + + /// Requests transaction history. + async fn request_tx_history(&self, metrics: MetricsArc, for_addresses: &HashSet
) + -> RequestTxHistoryResult; + + /// Requests timestamp of the given block. + + async fn get_block_timestamp(&self, height: u64) -> MmResult; + + /// Requests balances of all activated coin's addresses. + async fn my_addresses_balances(&self) -> BalanceResult>; + + fn address_from_str(&self, address: &str) -> MmResult; + + /// Sets the history sync state. + fn set_history_sync_state(&self, new_state: HistorySyncState); +} + +struct UtxoTxHistoryCtx { + coin: Coin, + storage: Storage, + metrics: MetricsArc, + /// Last requested balances of the activated coin's addresses. + /// TODO add a `CoinBalanceState` structure and replace [`HashMap`] everywhere. + balances: HashMap, +} + +impl UtxoTxHistoryCtx +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + /// Requests balances for every activated address, updates the balances in [`UtxoTxHistoryCtx::balances`] + /// and returns the addresses whose balance has changed. + /// + /// # Note + /// + /// [`UtxoTxHistoryCtx::balances`] is changed if we successfully handled all balances **only**. + async fn updated_addresses(&mut self) -> BalanceResult> { + let current_balances = self.coin.my_addresses_balances().await?; + + // Create a copy of the CTX balances state. + // We must not to save any change of `ctx.balances` if an error occurs while processing `current_balances` collection. + let mut ctx_balances = self.balances.clone(); + + let mut updated_addresses = HashSet::with_capacity(ctx_balances.len()); + for (address, current_balance) in current_balances { + let updated_address = match ctx_balances.entry(address.clone()) { + // Do nothing if the balance hasn't been changed. + Entry::Occupied(entry) if *entry.get() == current_balance => continue, + Entry::Occupied(mut entry) => { + entry.insert(current_balance); + address + }, + Entry::Vacant(entry) => { + entry.insert(current_balance); + address + }, + }; + + // Currently, it's easier to convert `Address` from stringified address + // than to refactor `CoinBalanceReport` by replacing stringified addresses with a type parameter. + // Such refactoring will lead to huge code changes, complex and nested trait bounds. + // I personally think that it's overhead since, a least for now, + // we need to parse `CoinBalanceReport` within the transaction history only. + match self.coin.address_from_str(&updated_address) { + Ok(addr) => updated_addresses.insert(addr), + Err(e) => { + let (kind, trace) = e.split(); + let error = + format!("Error on converting address from 'UtxoTxHistoryOps::my_addresses_balances': {kind}"); + return MmError::err_with_trace(BalanceError::Internal(error), trace); + }, + }; + } + + // Save the changes in the context. + self.balances = ctx_balances; + + Ok(updated_addresses) + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct Init { + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl Init { + fn new() -> Self { + Init { + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for Stopped {} + +#[async_trait] +impl State for Init +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + ctx.coin.set_history_sync_state(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::for_all_addresses()) + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct FetchingTxHashes { + fetch_for_addresses: Option>, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl FetchingTxHashes { + fn for_all_addresses() -> Self { + FetchingTxHashes { + fetch_for_addresses: None, + phantom: Default::default(), + } + } + + fn for_addresses(fetch_for_addresses: HashSet
) -> Self { + FetchingTxHashes { + fetch_for_addresses: Some(fetch_for_addresses), + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for FetchingTxHashes {} +impl TransitionFrom> for FetchingTxHashes {} +impl TransitionFrom> for FetchingTxHashes {} + +#[async_trait] +impl State for FetchingTxHashes +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> 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 fetch_for_addresses = match self.fetch_for_addresses { + Some(for_addresses) => for_addresses, + // `fetch_for_addresses` hasn't been specified. Fetch TX hashses for all addresses. + None => try_or_stop_unknown!(ctx.coin.my_addresses().await, "Error on getting my addresses"), + }; + + let maybe_tx_ids = ctx + .coin + .request_tx_history(ctx.metrics.clone(), &fetch_for_addresses) + .await; + match maybe_tx_ids { + RequestTxHistoryResult::Ok(all_tx_ids_with_height) => { + let filtering_addresses = + FilteringAddresses::from_iter(fetch_for_addresses.iter().map(DisplayAddress::display_address)); + + let in_storage = match ctx + .storage + .unique_tx_hashes_num_in_history(&wallet_id, filtering_addresses) + .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; + let new_state_json = json!({ "transactions_left": txes_left }); + ctx.coin + .set_history_sync_state(HistorySyncState::InProgress(new_state_json)); + } + + Self::change_state(UpdatingUnconfirmedTxes::new( + fetch_for_addresses, + 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(fetch_for_addresses)) + }, + RequestTxHistoryResult::CriticalError(e) => { + error!( + "Critical error {} on requesting tx history for {}", + e, + ctx.coin.ticker() + ); + Self::change_state(Stopped::unknown(e)) + }, + } + } +} + +/// An I/O cooldown before `FetchingTxHashes` state. +/// States have to be generic over storage type because `UtxoTxHistoryCtx` is generic over it. +struct OnIoErrorCooldown { + /// The list of addresses of those we need to fetch TX hashes at the upcoming `FetchingTxHashses` state. + fetch_for_addresses: HashSet
, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl OnIoErrorCooldown { + fn new(fetch_for_addresses: HashSet
) -> Self { + OnIoErrorCooldown { + fetch_for_addresses, + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for OnIoErrorCooldown {} +impl TransitionFrom> for OnIoErrorCooldown {} +impl TransitionFrom> for OnIoErrorCooldown {} + +#[async_trait] +impl State for OnIoErrorCooldown +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(mut self: Box, ctx: &mut Self::Ctx) -> StateResult { + loop { + Timer::sleep(30.).await; + + // We need to check whose balance has changed in these 30 seconds. + let updated_addresses = match ctx.updated_addresses().await { + Ok(updated) => updated, + Err(e) => { + error!("Error {e:?} on balance fetching for the coin {}", ctx.coin.ticker()); + continue; + }, + }; + + // We still need to fetch TX hashes for [`OnIoErrorCooldown::fetch_for_addresses`], + // but now we also need to fetch TX hashes for new `updated_addresses`. + // Merge these two containers. + self.fetch_for_addresses.extend(updated_addresses); + + return Self::change_state(FetchingTxHashes::for_addresses(self.fetch_for_addresses)); + } + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct WaitForHistoryUpdateTrigger { + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl WaitForHistoryUpdateTrigger { + fn new() -> Self { + WaitForHistoryUpdateTrigger { + phantom: Default::default(), + } + } +} + +impl TransitionFrom> + for WaitForHistoryUpdateTrigger +{ +} + +#[async_trait] +impl State for WaitForHistoryUpdateTrigger +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + let wallet_id = ctx.coin.history_wallet_id(); + loop { + Timer::sleep(30.).await; + + let my_addresses = try_or_stop_unknown!(ctx.coin.my_addresses().await, "Error on getting my addresses"); + let for_addresses = to_filtering_addresses(&my_addresses); + + match ctx + .storage + .history_contains_unconfirmed_txes(&wallet_id, for_addresses) + .await + { + // Fetch TX hashses for all addresses. + Ok(true) => return Self::change_state(FetchingTxHashes::for_addresses(my_addresses)), + Ok(false) => (), + Err(e) => return Self::change_state(Stopped::storage_error(e)), + } + + let updated_addresses = match ctx.updated_addresses().await { + Ok(updated) => updated, + Err(e) => { + error!("Error {e:?} on balance fetching for the coin {}", ctx.coin.ticker()); + continue; + }, + }; + + if !updated_addresses.is_empty() { + // Fetch TX hashes for those addresses whose balance has changed only. + return Self::change_state(FetchingTxHashes::for_addresses(updated_addresses)); + } + } + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct UpdatingUnconfirmedTxes { + /// The list of addresses for those we have requested [`UpdatingUnconfirmedTxes::all_tx_ids_with_height`] TX hashses + /// at the `FetchingTxHashes` state. + requested_for_addresses: HashSet
, + all_tx_ids_with_height: Vec<(H256Json, u64)>, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl UpdatingUnconfirmedTxes { + fn new(requested_for_addresses: HashSet
, all_tx_ids_with_height: Vec<(H256Json, u64)>) -> Self { + UpdatingUnconfirmedTxes { + requested_for_addresses, + all_tx_ids_with_height, + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for UpdatingUnconfirmedTxes {} + +#[async_trait] +impl State for UpdatingUnconfirmedTxes +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + let wallet_id = ctx.coin.history_wallet_id(); + + let for_addresses = to_filtering_addresses(&self.requested_for_addresses); + let unconfirmed = match ctx + .storage + .get_unconfirmed_txes_from_history(&wallet_id, for_addresses) + .await + { + Ok(unconfirmed) => unconfirmed, + Err(e) => return Self::change_state(Stopped::storage_error(e)), + }; + + 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(self.requested_for_addresses)), + }; + 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.requested_for_addresses, + self.all_tx_ids_with_height, + )) + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct FetchingTransactionsData { + /// The list of addresses for those we have requested [`UpdatingUnconfirmedTxes::all_tx_ids_with_height`] TX hashses + /// at the `FetchingTxHashes` state. + requested_for_addresses: HashSet
, + all_tx_ids_with_height: Vec<(H256Json, u64)>, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl TransitionFrom> for FetchingTransactionsData {} + +impl FetchingTransactionsData { + fn new(requested_for_addresses: HashSet
, all_tx_ids_with_height: Vec<(H256Json, u64)>) -> Self { + FetchingTransactionsData { + requested_for_addresses, + all_tx_ids_with_height, + phantom: Default::default(), + } + } +} + +#[async_trait] +impl State for FetchingTransactionsData +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + let ticker = ctx.coin.ticker(); + let wallet_id = ctx.coin.history_wallet_id(); + + let my_addresses = try_or_stop_unknown!(ctx.coin.my_addresses().await, "Error on getting my addresses"); + + 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(self.requested_for_addresses)), + }; + Some(BlockHeightAndTime { height, timestamp }) + } else { + None + }; + let params = UtxoTxDetailsParams { + hash: &tx_hash, + block_height_and_time, + storage: &ctx.storage, + my_addresses: &my_addresses, + }; + let tx_details = match ctx.coin.tx_details_by_hash(params).await { + Ok(tx) => tx, + Err(e) => { + error!("Error on getting {ticker} tx details for hash {tx_hash:02x}: {e}"); + return Self::change_state(OnIoErrorCooldown::new(self.requested_for_addresses)); + }, + }; + + 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 {ticker}"); + ctx.coin.set_history_sync_state(HistorySyncState::Finished); + Self::change_state(WaitForHistoryUpdateTrigger::new()) + } +} + +#[derive(Debug)] +enum StopReason { + HistoryTooLarge, + StorageError(String), + UnknownError(String), +} + +struct Stopped { + phantom: std::marker::PhantomData<(Coin, Storage)>, + stop_reason: StopReason, +} + +impl Stopped { + fn history_too_large() -> Self { + Stopped { + phantom: Default::default(), + stop_reason: StopReason::HistoryTooLarge, + } + } + + fn storage_error(e: E) -> Self + where + E: std::fmt::Debug, + { + Stopped { + phantom: Default::default(), + stop_reason: StopReason::StorageError(format!("{:?}", 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 +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + 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.set_history_sync_state(HistorySyncState::Error(new_state_json)); + } +} + +pub async fn bch_and_slp_history_loop( + coin: BchCoin, + storage: impl TxHistoryStorage, + metrics: MetricsArc, + current_balance: BigDecimal, +) { + let my_address = match coin.my_address() { + Ok(my_address) => my_address, + Err(e) => { + error!("{}", e); + return; + }, + }; + let mut balances = HashMap::new(); + balances.insert(my_address, current_balance); + drop_mutability!(balances); + + let ctx = UtxoTxHistoryCtx { + coin, + storage, + metrics, + balances, + }; + let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ctx); + state_machine.run(Init::new()).await; +} + +pub async fn utxo_history_loop( + coin: Coin, + storage: Storage, + metrics: MetricsArc, + current_balances: HashMap, +) where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + let ctx = UtxoTxHistoryCtx { + coin, + storage, + metrics, + balances: current_balances, + }; + let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ctx); + state_machine.run(Init::new()).await; +} + +fn to_filtering_addresses(addresses: &HashSet
) -> FilteringAddresses { + FilteringAddresses::from_iter(addresses.iter().map(DisplayAddress::display_address)) +} diff --git a/mm2src/coins/utxo/utxo_wasm_tests.rs b/mm2src/coins/utxo/utxo_wasm_tests.rs index 4eaaf01d79..0d3dd0041d 100644 --- a/mm2src/coins/utxo/utxo_wasm_tests.rs +++ b/mm2src/coins/utxo/utxo_wasm_tests.rs @@ -1,8 +1,10 @@ -use super::rpc_clients::{ElectrumClient, ElectrumClientImpl, ElectrumProtocol}; +use super::rpc_clients::{ElectrumClient, UtxoRpcClientOps}; +use super::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilderCommonOps}; +use super::utxo_standard::UtxoStandardCoin; use super::*; -use crate::utxo::rpc_clients::UtxoRpcClientOps; use crate::utxo::utxo_common_tests; -use common::executor::Timer; +use crate::{IguanaPrivKey, PrivKeyBuildPolicy}; +use mm2_core::mm_ctx::MmCtxBuilder; use serialization::deserialize; use wasm_bindgen_test::*; @@ -11,29 +13,34 @@ wasm_bindgen_test_configure!(run_in_browser); const TEST_COIN_NAME: &'static str = "RICK"; pub async fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { - let client = ElectrumClientImpl::new(TEST_COIN_NAME.into(), Default::default()); - for server in servers { - client - .add_server(&ElectrumRpcRequest { - url: server.to_string(), - protocol: ElectrumProtocol::WSS, - disable_cert_verification: false, - }) - .await - .expect("!add_server"); - } - - let mut attempts = 0; - while !client.is_connected().await { - if attempts >= 10 { - panic!("Failed to connect to at least 1 of {:?} in 5 seconds.", servers); - } - - Timer::sleep(0.5).await; - attempts += 1; - } - - ElectrumClient(Arc::new(client)) + let ctx = MmCtxBuilder::default().into_mm_arc(); + let servers: Vec<_> = servers + .iter() + .map(|server| json!({ "url": server, "protocol": "WSS" })) + .collect(); + let req = json!({ + "method": "electrum", + "servers": servers, + }); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(IguanaPrivKey::default()); + 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, + }; + + let servers = servers.into_iter().map(|s| json::from_value(s).unwrap()).collect(); + let abortable_system = AbortableQueue::default(); + builder.electrum_client(abortable_system, args, servers).await.unwrap() } #[wasm_bindgen_test] @@ -59,3 +66,9 @@ 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; } + +#[wasm_bindgen_test] +async fn test_hd_utxo_tx_history() { + let rpc_client = electrum_client_for_test(&["electrum1.cipig.net:30018", "electrum2.cipig.net:30018"]).await; + utxo_common_tests::test_hd_utxo_tx_history_impl(rpc_client).await; +} diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index b54fd7a5fd..4f825a6c14 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -1,4 +1,4 @@ -use crate::rpc_command::init_withdraw::{WithdrawAwaitingStatus, WithdrawInProgressStatus, WithdrawTaskHandle}; +use crate::rpc_command::init_withdraw::{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}; @@ -8,10 +8,8 @@ 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 crypto::{from_hw_error, CryptoCtx, CryptoCtxError, DerivationPath, HwError, HwProcessingError, HwRpcError}; use keys::{Public as PublicKey, Type as ScriptType}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -20,14 +18,10 @@ 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::sign_params::{OutputDestination, 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 { @@ -58,44 +52,37 @@ impl From> for WithdrawError { } 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), - } - } + fn from(e: HwError) -> Self { from_hw_error(e) } } impl From for WithdrawError { - fn from(e: TrezorError) -> Self { WithdrawError::HardwareWalletInternal(e.to_string()) } + fn from(e: TrezorError) -> Self { + match e { + TrezorError::DeviceDisconnected => WithdrawError::HwError(HwRpcError::NoTrezorDeviceAvailable), + other => WithdrawError::InternalError(other.to_string()), + } + } } -impl From for WithdrawError { - fn from(e: CryptoInitError) -> Self { WithdrawError::InternalError(e.to_string()) } +impl From for WithdrawError { + fn from(e: CryptoCtxError) -> 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::Cancelled => WithdrawError::InternalError("Cancelled".to_owned()), RpcTaskError::Timeout(timeout) => WithdrawError::Timeout(timeout), RpcTaskError::NoSuchTask(_) | RpcTaskError::UnexpectedTaskStatus { .. } => { WithdrawError::InternalError(error) }, + RpcTaskError::UnexpectedUserAction { expected } => WithdrawError::UnexpectedUserAction { expected }, 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 @@ -119,8 +106,10 @@ where fn prev_script(&self) -> Script { Builder::build_p2pkh(&self.sender_address().hash) } + #[allow(clippy::result_large_err)] fn on_generating_transaction(&self) -> Result<(), MmError>; + #[allow(clippy::result_large_err)] fn on_finishing(&self) -> Result<(), MmError>; async fn sign_tx(&self, unsigned_tx: TransactionInputSigner) -> Result>; @@ -132,9 +121,7 @@ where 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 to = coin.address_from_str(&req.to)?; 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; @@ -225,6 +212,7 @@ where timestamp: now_ms() / 1000, kmd_rewards: data.kmd_rewards, transaction_type: Default::default(), + memo: None, }) } } @@ -291,8 +279,9 @@ where 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(), + destination_address: OutputDestination::plain(self.req.to.clone()), })); match unsigned_tx.outputs.len() { // There is no change output. @@ -300,7 +289,7 @@ where // There is a change output. 2 => { sign_params.add_outputs_infos(once(SendingOutputInfo { - destination_address: self.from_address_string.clone(), + destination_address: OutputDestination::change(self.from_derivation_path.clone()), })); }, unexpected => { @@ -315,11 +304,16 @@ where .with_prev_script(Builder::build_p2pkh(&self.from_address.hash)); let sign_params = sign_params.build()?; + let crypto_ctx = CryptoCtx::from_ctx(&self.ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| WithdrawError::HwError(HwRpcError::NoTrezorDeviceAvailable))?; + 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) + let trezor_session = hw_ctx.trezor().await?; + SignPolicy::WithTrezor(trezor_session) }, }; @@ -366,32 +360,6 @@ impl<'a, Coin> InitUtxoWithdraw<'a, Coin> { 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 { @@ -434,9 +402,10 @@ impl StandardUtxoWithdraw where Coin: AsRef + MarketCoinOps, { + #[allow(clippy::result_large_err)] 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)?; + let my_address = coin.as_ref().derivation_method.single_addr_or_err()?.clone(); + let my_address_string = coin.my_address()?; Ok(StandardUtxoWithdraw { coin, req, diff --git a/mm2src/coins/utxo_signer/Cargo.toml b/mm2src/coins/utxo_signer/Cargo.toml index db17cd348d..7e44ad011e 100644 --- a/mm2src/coins/utxo_signer/Cargo.toml +++ b/mm2src/coins/utxo_signer/Cargo.toml @@ -3,7 +3,8 @@ 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 +[lib] +doctest = false [dependencies] async-trait = "0.1" diff --git a/mm2src/coins/utxo_signer/src/lib.rs b/mm2src/coins/utxo_signer/src/lib.rs index ce6d716d68..9285bf8587 100644 --- a/mm2src/coins/utxo_signer/src/lib.rs +++ b/mm2src/coins/utxo_signer/src/lib.rs @@ -1,8 +1,6 @@ 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 crypto::trezor::{TrezorError, TrezorSession}; use derive_more::Display; use keys::bytes::Bytes; use keys::KeyPair; @@ -112,7 +110,7 @@ pub trait TxProvider { } pub enum SignPolicy<'a> { - WithTrezor(TrezorClient), + WithTrezor(TrezorSession<'a>), WithKeyPair(&'a KeyPair), } @@ -120,7 +118,7 @@ pub enum SignPolicy<'a> { pub trait UtxoSignerOps { type TxGetter: TxProvider + Send + Sync; - fn trezor_coin(&self) -> UtxoSignTxResult; + fn trezor_coin(&self) -> UtxoSignTxResult; fn fork_id(&self) -> u32; diff --git a/mm2src/coins/utxo_signer/src/sign_params.rs b/mm2src/coins/utxo_signer/src/sign_params.rs index 545b951c67..dab3ad17a4 100644 --- a/mm2src/coins/utxo_signer/src/sign_params.rs +++ b/mm2src/coins/utxo_signer/src/sign_params.rs @@ -25,9 +25,23 @@ pub enum SpendingInputInfo { // P2SH {} } +/// Either plain destination address or derivation path of a change address. +pub enum OutputDestination { + Plain { address: String }, + Change { derivation_path: DerivationPath }, +} + +impl OutputDestination { + pub fn plain(address: String) -> OutputDestination { OutputDestination::Plain { address } } + + pub fn change(derivation_path: DerivationPath) -> OutputDestination { + OutputDestination::Change { derivation_path } + } +} + /// An additional info of a sending output. pub struct SendingOutputInfo { - pub destination_address: String, + pub destination_address: OutputDestination, } impl SendingOutputInfo { @@ -161,8 +175,8 @@ impl UtxoSignTxParams { /// Please see [`UtxoSignTxParamsBuilder::build`]. pub fn outputs(&self) -> impl Iterator { assert_eq!( - self.unsigned_tx.inputs.len(), - self.inputs_infos.len(), + self.unsigned_tx.outputs.len(), + self.outputs_infos.len(), "'unsigned_tx.outputs' and 'outputs_infos' must be checked" ); self.unsigned_tx.outputs.iter().zip(self.outputs_infos.iter()) diff --git a/mm2src/coins/utxo_signer/src/with_trezor.rs b/mm2src/coins/utxo_signer/src/with_trezor.rs index c6cc9b1dfe..5cbe8709ae 100644 --- a/mm2src/coins/utxo_signer/src/with_trezor.rs +++ b/mm2src/coins/utxo_signer/src/with_trezor.rs @@ -1,39 +1,38 @@ use crate::sign_common::{complete_tx, p2pkh_spend_with_signature}; -use crate::sign_params::{SendingOutputInfo, SpendingInputInfo, UtxoSignTxParams}; +use crate::sign_params::{OutputDestination, SendingOutputInfo, SpendingInputInfo, UtxoSignTxParams}; use crate::{TxProvider, UtxoSignTxError, UtxoSignTxResult}; use chain::{Transaction as UtxoTx, TransactionOutput}; use common::log::debug; -use crypto::trezor::utxo::{PrevTx, PrevTxInput, PrevTxOutput, TrezorInputScriptType, TrezorUtxoCoin, TxOutput, - TxSignResult, UnsignedTxInput, UnsignedUtxoTx}; -use crypto::trezor::TrezorClient; +use crypto::trezor::utxo::{PrevTx, PrevTxInput, PrevTxOutput, TrezorInputScriptType, TxOutput, TxSignResult, + UnsignedTxInput, UnsignedUtxoTx}; +use crypto::trezor::TrezorSession; use keys::bytes::Bytes; use mm2_err_handle::prelude::*; use rpc::v1::types::H256 as H256Json; use script::{SignatureVersion, UnsignedTransactionInput}; use serialization::deserialize; -pub struct TrezorTxSigner { - pub trezor: TrezorClient, +pub struct TrezorTxSigner<'a, TxP> { + pub trezor: TrezorSession<'a>, pub tx_provider: TxP, - pub trezor_coin: TrezorUtxoCoin, + pub trezor_coin: String, pub params: UtxoSignTxParams, pub fork_id: u32, pub branch_id: u32, } -impl TrezorTxSigner { - pub async fn sign_tx(self) -> UtxoSignTxResult { +impl<'a, TxP: TxProvider + Send + Sync> TrezorTxSigner<'a, TxP> { + pub async fn sign_tx(mut self) -> UtxoSignTxResult { if let SignatureVersion::WitnessV0 = self.params.signature_version { return MmError::err(UtxoSignTxError::TrezorDoesntSupportP2WPKH); } let trezor_unsigned_tx = self.get_trezor_unsigned_tx().await?; - let mut session = self.trezor.session().await?; let TxSignResult { signatures, serialized_tx, - } = session.sign_utxo_tx(trezor_unsigned_tx).await?; + } = self.trezor.sign_utxo_tx(trezor_unsigned_tx).await?; debug!("Transaction signed by Trezor: {}", hex::encode(serialized_tx)); if signatures.len() != self.params.inputs_count() { return MmError::err(UtxoSignTxError::InvalidSignaturesNumber { @@ -69,19 +68,25 @@ impl TrezorTxSigner { .collect(); Ok(UnsignedUtxoTx { - coin: self.trezor_coin, + coin: self.trezor_coin.clone(), inputs, outputs, version: self.params.unsigned_tx.version as u32, lock_time: self.params.unsigned_tx.lock_time, + expiry: self.expiry_if_required(self.params.unsigned_tx.expiry_height), version_group_id: self.version_group_id(), branch_id: self.branch_id(), }) } fn get_trezor_output(&self, tx_output: &TransactionOutput, output_info: &SendingOutputInfo) -> TxOutput { + let (address, address_derivation_path) = match output_info.destination_address { + OutputDestination::Plain { ref address } => (Some(address.clone()), None), + OutputDestination::Change { ref derivation_path } => (None, Some(derivation_path.clone())), + }; TxOutput { - address: output_info.destination_address.clone(), + address, + address_derivation_path, amount: tx_output.value, script_type: output_info.trezor_output_script_type(), } @@ -144,6 +149,7 @@ impl TrezorTxSigner { outputs: prev_tx_outputs, version: prev_utxo.version as u32, lock_time: prev_utxo.lock_time, + expiry: self.expiry_if_required(prev_utxo.expiry_height), version_group_id: self.version_group_id(), branch_id: self.branch_id(), extra_data: self.extra_data(), @@ -168,6 +174,16 @@ impl TrezorTxSigner { } } + /// `expiry` must be set for Decred and Zcash coins *only*. + /// This fixes : https://github.com/KomodoPlatform/atomicDEX-API/issues/1626 + fn expiry_if_required(&self, tx_expiry: u32) -> Option { + if self.is_overwinter_compatible() { + Some(tx_expiry) + } else { + None + } + } + /// Temporary use `0000000000000000000000` extra data for Zcash coins *only*. /// https://github.com/trezor/connect/issues/610#issuecomment-646022404 fn extra_data(&self) -> Vec { @@ -179,5 +195,8 @@ impl TrezorTxSigner { } /// https://github.com/trezor/trezor-utxo-lib/blob/trezor/src/transaction.js#L405 - fn is_overwinter_compatible(&self) -> bool { self.params.unsigned_tx.version > 3 } + fn is_overwinter_compatible(&self) -> bool { self.is_zcash_type() && self.params.unsigned_tx.version > 3 } + + /// https://github.com/trezor/trezor-utxo-lib/blob/trezor/src/coins.js#L55 + fn is_zcash_type(&self) -> bool { matches!(self.trezor_coin.as_str(), "Komodo" | "Zcash" | "Zcash Testnet") } } diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 8038cf6ae7..72c86f38a1 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1,29 +1,41 @@ +use crate::coin_errors::MyAddressError; use crate::my_tx_history_v2::{MyTxHistoryErrorV2, MyTxHistoryRequestV2, MyTxHistoryResponseV2}; use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawInProgressStatus, WithdrawTaskHandle}; use crate::utxo::rpc_clients::{ElectrumRpcRequest, UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; -use crate::utxo::utxo_builder::{UtxoCoinBuilderCommonOps, UtxoCoinWithIguanaPrivKeyBuilder, - UtxoFieldsWithIguanaPrivKeyBuilder}; +use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, + UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, + UtxoFieldsWithIguanaSecretBuilder}; use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat, big_decimal_from_sat_unsigned, payment_script}; -use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxData, Address, BroadcastTxErr, - FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, +use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxData, AddrFromStrError, Address, + BroadcastTxErr, FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, UtxoActivationParams, UtxoAddressFormat, UtxoArc, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; -use crate::{BalanceError, BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, - MmCoin, NegotiateSwapContractAddrErr, NumConversError, PrivKeyActivationPolicy, RawTransactionFut, - RawTransactionRequest, SearchForSwapTxSpendInput, SignatureError, SignatureResult, SwapOps, TradeFee, +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, FeeApproxStage, + FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, + NegotiateSwapContractAddrErr, NumConversError, PaymentInstructions, PaymentInstructionsErr, + PrivKeyActivationPolicy, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, + RawTransactionRequest, RefundError, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, + SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, + SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, + SendWatcherRefundsPaymentArgs, SignatureError, SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, - TransactionFut, TxFeeDetails, UnexpectedDerivationMethod, ValidateAddressResult, ValidatePaymentInput, - VerificationError, VerificationResult, WithdrawFut, WithdrawRequest}; + TransactionFut, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, + ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WatcherOps, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, + WithdrawRequest}; use crate::{Transaction, WithdrawError}; use async_trait::async_trait; -use bitcrypto::{dhash160, dhash256}; +use bitcrypto::dhash256; use chain::constants::SEQUENCE_FINAL; use chain::{Transaction as UtxoTx, TransactionOutput}; -use common::{async_blocking, calc_total_pages, log, PagingOptionsEnum}; +use common::executor::{AbortableSystem, AbortedError}; +use common::{async_blocking, calc_total_pages, log, one_thousand_u32, sha256_digest, PagingOptionsEnum}; use crypto::privkey::{key_pair_from_secret, secp_privkey_from_hash}; +use crypto::{Bip32DerPathOps, GlobalHDAccountArc, StandardHDPathToCoin}; use db_common::sqlite::offset_by_id; use db_common::sqlite::rusqlite::{Error as SqlError, Row, NO_PARAMS}; use db_common::sqlite::sql_builder::{name, SqlBuilder, SqlName}; @@ -31,21 +43,21 @@ use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; use futures01::Future; -use http::Uri; use keys::hash::H256; use keys::{KeyPair, Message, Public}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; +use parking_lot::Mutex; use primitives::bytes::Bytes; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; use serialization::CoinVariant; use std::collections::{HashMap, HashSet}; +use std::iter; use std::path::PathBuf; -use std::str::FromStr; use std::sync::Arc; use zcash_client_backend::data_api::WalletRead; use zcash_client_backend::encoding::{decode_payment_address, encode_extended_spending_key, encode_payment_address}; @@ -61,8 +73,10 @@ use zcash_primitives::sapling::note_encryption::try_sapling_output_recovery; use zcash_primitives::transaction::builder::Builder as ZTxBuilder; use zcash_primitives::transaction::components::{Amount, TxOut}; use zcash_primitives::transaction::Transaction as ZTransaction; +use zcash_primitives::zip32::ChildIndex as Zip32Child; use zcash_primitives::{consensus, constants::mainnet as z_mainnet_constants, sapling::PaymentAddress, zip32::ExtendedFullViewingKey, zip32::ExtendedSpendingKey}; +use zcash_proofs::default_params_folder; use zcash_proofs::prover::LocalTxProver; mod z_htlc; @@ -70,9 +84,10 @@ use z_htlc::{z_p2sh_spend, z_send_dex_fee, z_send_htlc}; mod z_rpc; pub use z_rpc::SyncStatus; -use z_rpc::{init_light_client, SaplingSyncConnector, SaplingSyncGuard, WalletDbShared}; +use z_rpc::{init_light_client, init_native_client, SaplingSyncConnector, SaplingSyncGuard, WalletDbShared}; mod z_coin_errors; +use crate::z_coin::z_rpc::{create_wallet_db, BlockDb}; pub use z_coin_errors::*; #[cfg(all(test, feature = "zhtlc-native-tests"))] @@ -101,6 +116,10 @@ const DEX_FEE_OVK: OutgoingViewingKey = OutgoingViewingKey([7; 32]); const DEX_FEE_Z_ADDR: &str = "zs1rp6426e9r6jkq2nsanl66tkd34enewrmr0uvj0zelhkcwmsy0uvxz2fhm9eu9rl3ukxvgzy2v9f"; const TRANSACTIONS_TABLE: &str = "transactions"; const BLOCKS_TABLE: &str = "blocks"; +const SAPLING_SPEND_NAME: &str = "sapling-spend.params"; +const SAPLING_OUTPUT_NAME: &str = "sapling-output.params"; +const SAPLING_SPEND_EXPECTED_HASH: &str = "8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"; +const SAPLING_OUTPUT_EXPECTED_HASH: &str = "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ZcoinConsensusParams { @@ -131,6 +150,8 @@ pub struct CheckPointBlockInfo { pub struct ZcoinProtocolInfo { consensus_params: ZcoinConsensusParams, check_point_block: Option, + // `z_derivation_path` can be the same or different from [`UtxoCoinFields::derivation_path`]. + z_derivation_path: Option, } impl Parameters for ZcoinConsensusParams { @@ -169,16 +190,6 @@ pub struct ZCoinFields { sync_state_connector: AsyncMutex, } -impl std::fmt::Debug for ZCoinFields { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "ZCoinFields {{ my_z_addr: {:?}, my_z_addr_encoded: {} }}", - self.my_z_addr, self.my_z_addr_encoded - ) - } -} - impl Transaction for ZTransaction { fn tx_hex(&self) -> Vec { let mut hex = Vec::with_capacity(1024); @@ -193,7 +204,7 @@ impl Transaction for ZTransaction { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct ZCoin { utxo_arc: UtxoArc, z_fields: Arc, @@ -671,6 +682,7 @@ impl ZCoin { Ok(MyTxHistoryResponseV2 { coin: self.ticker().into(), + target: request.target, current_block, transactions, // Zcoin is activated only after the state is synced @@ -698,11 +710,16 @@ pub enum ZcoinRpcMode { }, } -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] pub struct ZcoinActivationParams { pub mode: ZcoinRpcMode, pub required_confirmations: Option, pub requires_notarization: Option, + pub zcash_params_path: Option, + #[serde(default = "one_thousand_u32")] + pub scan_blocks_per_iteration: u32, + #[serde(default)] + pub scan_interval_ms: u64, } pub async fn z_coin_from_conf_and_params( @@ -711,11 +728,40 @@ pub async fn z_coin_from_conf_and_params( conf: &Json, params: &ZcoinActivationParams, protocol_info: ZcoinProtocolInfo, - secp_priv_key: &[u8], + priv_key_policy: PrivKeyBuildPolicy, ) -> Result> { - let z_key = ExtendedSpendingKey::master(secp_priv_key); - let db_dir = ctx.dbdir(); - z_coin_from_conf_and_params_with_z_key(ctx, ticker, conf, params, secp_priv_key, db_dir, z_key, protocol_info).await + let db_dir_path = ctx.dbdir(); + let z_spending_key = None; + let builder = ZCoinBuilder::new( + ctx, + ticker, + conf, + params, + priv_key_policy, + db_dir_path, + z_spending_key, + protocol_info, + ); + builder.build().await +} + +fn verify_checksum_zcash_params(spend_path: &PathBuf, output_path: &PathBuf) -> Result { + let spend_hash = sha256_digest(spend_path)?; + let out_hash = sha256_digest(output_path)?; + Ok(spend_hash == SAPLING_SPEND_EXPECTED_HASH && out_hash == SAPLING_OUTPUT_EXPECTED_HASH) +} + +fn get_spend_output_paths(params_dir: PathBuf) -> Result<(PathBuf, PathBuf), ZCoinBuildError> { + if !params_dir.exists() { + return Err(ZCoinBuildError::ZCashParamsNotFound); + }; + let spend_path = params_dir.join(SAPLING_SPEND_NAME); + let output_path = params_dir.join(SAPLING_OUTPUT_NAME); + + if !(spend_path.exists() && output_path.exists()) { + return Err(ZCoinBuildError::ZCashParamsNotFound); + } + Ok((spend_path, output_path)) } pub struct ZCoinBuilder<'a> { @@ -724,9 +770,10 @@ pub struct ZCoinBuilder<'a> { conf: &'a Json, z_coin_params: &'a ZcoinActivationParams, utxo_params: UtxoActivationParams, - secp_priv_key: &'a [u8], + priv_key_policy: PrivKeyBuildPolicy, db_dir_path: PathBuf, - z_spending_key: ExtendedSpendingKey, + /// `Some` if `ZCoin` should be initialized with a forced spending key. + z_spending_key: Option, protocol_info: ZcoinProtocolInfo, } @@ -740,22 +787,31 @@ impl<'a> UtxoCoinBuilderCommonOps for ZCoinBuilder<'a> { fn ticker(&self) -> &str { self.ticker } } -#[async_trait] -impl<'a> UtxoFieldsWithIguanaPrivKeyBuilder for ZCoinBuilder<'a> {} +impl<'a> UtxoFieldsWithIguanaSecretBuilder for ZCoinBuilder<'a> {} + +impl<'a> UtxoFieldsWithGlobalHDBuilder for ZCoinBuilder<'a> {} + +/// Although, `ZCoin` doesn't support [`PrivKeyBuildPolicy::Trezor`] yet, +/// `UtxoCoinBuilder` trait requires `UtxoFieldsWithHardwareWalletBuilder` to be implemented. +impl<'a> UtxoFieldsWithHardwareWalletBuilder for ZCoinBuilder<'a> {} #[async_trait] -impl<'a> UtxoCoinWithIguanaPrivKeyBuilder for ZCoinBuilder<'a> { +impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { type ResultCoin = ZCoin; type Error = ZCoinBuildError; - fn priv_key(&self) -> &[u8] { self.secp_priv_key } + fn priv_key_policy(&self) -> PrivKeyBuildPolicy { self.priv_key_policy.clone() } async fn build(self) -> MmResult { - let utxo = self.build_utxo_fields_with_iguana_priv_key(self.priv_key()).await?; + let utxo = self.build_utxo_fields().await?; let utxo_arc = UtxoArc::new(utxo); - let (_, my_z_addr) = self - .z_spending_key + let z_spending_key = match self.z_spending_key { + Some(ref z_spending_key) => z_spending_key.clone(), + None => extended_spending_key_from_protocol_info_and_policy(&self.protocol_info, &self.priv_key_policy)?, + }; + + let (_, my_z_addr) = z_spending_key .default_address() .map_err(|_| MmError::new(ZCoinBuildError::GetAddressError))?; @@ -766,39 +822,66 @@ impl<'a> UtxoCoinWithIguanaPrivKeyBuilder for ZCoinBuilder<'a> { .expect("DEX_FEE_Z_ADDR is a valid z-address") .expect("DEX_FEE_Z_ADDR is a valid z-address"); - let z_tx_prover = async_blocking(LocalTxProver::with_default_location) - .await - .or_mm_err(|| ZCoinBuildError::ZCashParamsNotFound)?; + let params_dir = match &self.z_coin_params.zcash_params_path { + None => default_params_folder().or_mm_err(|| ZCoinBuildError::ZCashParamsNotFound)?, + Some(file_path) => PathBuf::from(file_path), + }; + + let z_tx_prover = async_blocking(move || { + let (spend_path, output_path) = get_spend_output_paths(params_dir)?; + let verification_successful = verify_checksum_zcash_params(&spend_path, &output_path)?; + if verification_successful { + Ok(LocalTxProver::new(&spend_path, &output_path)) + } else { + MmError::err(ZCoinBuildError::SaplingParamsInvalidChecksum) + } + }) + .await?; let my_z_addr_encoded = encode_payment_address( self.protocol_info.consensus_params.hrp_sapling_payment_address(), &my_z_addr, ); - let evk = ExtendedFullViewingKey::from(&self.z_spending_key); + let evk = ExtendedFullViewingKey::from(&z_spending_key); + let cache_db_path = self.db_dir_path.join(format!("{}_cache.db", self.ticker)); + let wallet_db_path = self.db_dir_path.join(format!("{}_wallet.db", self.ticker)); + let blocks_db = + async_blocking(|| BlockDb::for_path(cache_db_path).map_to_mm(ZcoinClientInitError::BlocksDbInitFailure)) + .await?; + let wallet_db = create_wallet_db( + wallet_db_path, + self.protocol_info.consensus_params.clone(), + self.protocol_info.check_point_block.clone(), + evk, + ) + .await?; + let wallet_db = Arc::new(Mutex::new(wallet_db)); let (sync_state_connector, light_wallet_db) = match &self.z_coin_params.mode { ZcoinRpcMode::Native => { - return MmError::err(ZCoinBuildError::NativeModeIsNotSupportedYet); + let native_client = self.native_client()?; + init_native_client( + self.ticker.into(), + native_client, + blocks_db, + wallet_db, + self.protocol_info.consensus_params.clone(), + self.z_coin_params.scan_blocks_per_iteration, + self.z_coin_params.scan_interval_ms, + ) + .await? }, ZcoinRpcMode::Light { light_wallet_d_servers, .. } => { - let cache_db_path = self.db_dir_path.join(format!("{}_light_cache.db", self.ticker)); - let wallet_db_path = self.db_dir_path.join(format!("{}_light_wallet.db", self.ticker)); - // TODO multi lightwalletd servers support will be added on the next iteration - let uri = Uri::from_str( - light_wallet_d_servers - .first() - .or_mm_err(|| ZCoinBuildError::EmptyLightwalletdUris)?, - )?; - init_light_client( - uri, - cache_db_path, - wallet_db_path, + self.ticker.into(), + light_wallet_d_servers.clone(), + blocks_db, + wallet_db, self.protocol_info.consensus_params.clone(), - self.protocol_info.check_point_block, - evk, + self.z_coin_params.scan_blocks_per_iteration, + self.z_coin_params.scan_interval_ms, ) .await? }, @@ -808,8 +891,8 @@ impl<'a> UtxoCoinWithIguanaPrivKeyBuilder for ZCoinBuilder<'a> { dex_fee_addr, my_z_addr, my_z_addr_encoded, - evk: ExtendedFullViewingKey::from(&self.z_spending_key), - z_spending_key: self.z_spending_key, + evk: ExtendedFullViewingKey::from(&z_spending_key), + z_spending_key, z_tx_prover: Arc::new(z_tx_prover), light_wallet_db, consensus_params: self.protocol_info.consensus_params, @@ -832,9 +915,9 @@ impl<'a> ZCoinBuilder<'a> { ticker: &'a str, conf: &'a Json, z_coin_params: &'a ZcoinActivationParams, - secp_priv_key: &'a [u8], + priv_key_policy: PrivKeyBuildPolicy, db_dir_path: PathBuf, - z_spending_key: ExtendedSpendingKey, + z_spending_key: Option, protocol_info: ZcoinProtocolInfo, ) -> ZCoinBuilder<'a> { let utxo_mode = match &z_coin_params.mode { @@ -851,8 +934,8 @@ impl<'a> ZCoinBuilder<'a> { requires_notarization: z_coin_params.requires_notarization, address_format: None, gap_limit: None, - scan_policy: Default::default(), - priv_key_policy: PrivKeyActivationPolicy::IguanaPrivKey, + enable_params: Default::default(), + priv_key_policy: PrivKeyActivationPolicy::ContextPrivKey, check_utxo_maturity: None, }; ZCoinBuilder { @@ -861,7 +944,7 @@ impl<'a> ZCoinBuilder<'a> { conf, z_coin_params, utxo_params, - secp_priv_key, + priv_key_policy, db_dir_path, z_spending_key, protocol_info, @@ -869,13 +952,15 @@ impl<'a> ZCoinBuilder<'a> { } } +/// Initialize `ZCoin` with a forced `z_spending_key`. +#[cfg(all(test, feature = "zhtlc-native-tests"))] #[allow(clippy::too_many_arguments)] async fn z_coin_from_conf_and_params_with_z_key( ctx: &MmArc, ticker: &str, conf: &Json, params: &ZcoinActivationParams, - secp_priv_key: &[u8], + priv_key_policy: PrivKeyBuildPolicy, db_dir_path: PathBuf, z_spending_key: ExtendedSpendingKey, protocol_info: ZcoinProtocolInfo, @@ -885,9 +970,9 @@ async fn z_coin_from_conf_and_params_with_z_key( ticker, conf, params, - secp_priv_key, + priv_key_policy, db_dir_path, - z_spending_key, + Some(z_spending_key), protocol_info, ); builder.build().await @@ -896,7 +981,7 @@ async fn z_coin_from_conf_and_params_with_z_key( impl MarketCoinOps for ZCoin { fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } - fn my_address(&self) -> Result { Ok(self.z_fields.my_z_addr_encoded.clone()) } + fn my_address(&self) -> MmResult { Ok(self.z_fields.my_z_addr_encoded.clone()) } fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.as_ref())?; @@ -975,12 +1060,14 @@ impl MarketCoinOps for ZCoin { utxo_common::wait_for_confirmations(self.as_ref(), tx, confirmations, requires_nota, wait_until, check_every) } - fn wait_for_tx_spend( + fn wait_for_htlc_tx_spend( &self, transaction: &[u8], + _secret_hash: &[u8], wait_until: u64, from_block: u64, _swap_contract_address: &Option, + check_every: f64, ) -> TransactionFut { utxo_common::wait_for_output_spend( self.as_ref(), @@ -988,11 +1075,14 @@ impl MarketCoinOps for ZCoin { utxo_common::DEFAULT_SWAP_VOUT, from_block, wait_until, + check_every, ) } - fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result { - ZTransaction::read(bytes).map(|tx| tx.into()).map_err(|e| e.to_string()) + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { + ZTransaction::read(bytes) + .map(TransactionEnum::from) + .map_to_mm(|e| TxMarshalingErr::InvalidInput(e.to_string())) } fn current_block(&self) -> Box + Send> { @@ -1025,19 +1115,13 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - 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 { + fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs<'_>) -> TransactionFut { let selfi = self.clone(); - let maker_key_pair = self.derive_htlc_key_pair(swap_unique_data); - let taker_pub = try_tx_fus!(Public::from_slice(taker_pub)); - let secret_hash = secret_hash.to_vec(); + let maker_key_pair = self.derive_htlc_key_pair(maker_payment_args.swap_unique_data); + let taker_pub = try_tx_fus!(Public::from_slice(maker_payment_args.other_pubkey)); + let secret_hash = maker_payment_args.secret_hash.to_vec(); + let time_lock = maker_payment_args.time_lock; + let amount = maker_payment_args.amount; let fut = async move { let utxo_tx = try_tx_s!( z_send_htlc( @@ -1055,19 +1139,13 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - 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 { + fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs<'_>) -> TransactionFut { let selfi = self.clone(); - let taker_keypair = self.derive_htlc_key_pair(swap_unique_data); - let maker_pub = try_tx_fus!(Public::from_slice(maker_pub)); - let secret_hash = secret_hash.to_vec(); + let taker_keypair = self.derive_htlc_key_pair(taker_payment_args.swap_unique_data); + let maker_pub = try_tx_fus!(Public::from_slice(taker_payment_args.other_pubkey)); + let secret_hash = taker_payment_args.secret_hash.to_vec(); + let time_lock = taker_payment_args.time_lock; + let amount = taker_payment_args.amount; let fut = async move { let utxo_tx = try_tx_s!( z_send_htlc( @@ -1087,23 +1165,19 @@ impl SwapOps for ZCoin { 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], + maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs<'_>, ) -> TransactionFut { - let tx = try_tx_fus!(ZTransaction::read(taker_payment_tx)); - let key_pair = self.derive_htlc_key_pair(swap_unique_data); + let tx = try_tx_fus!(ZTransaction::read(maker_spends_payment_args.other_payment_tx)); + let key_pair = self.derive_htlc_key_pair(maker_spends_payment_args.swap_unique_data); + let time_lock = maker_spends_payment_args.time_lock; let redeem_script = payment_script( time_lock, - &*dhash160(secret), - &try_tx_fus!(Public::from_slice(taker_pub)), + maker_spends_payment_args.secret_hash, + &try_tx_fus!(Public::from_slice(maker_spends_payment_args.other_pubkey)), key_pair.public(), ); let script_data = ScriptBuilder::default() - .push_data(secret) + .push_data(maker_spends_payment_args.secret) .push_opcode(Opcode::OP_0) .into_script(); let selfi = self.clone(); @@ -1125,23 +1199,19 @@ impl SwapOps for ZCoin { 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], + taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs<'_>, ) -> TransactionFut { - let tx = try_tx_fus!(ZTransaction::read(maker_payment_tx)); - let key_pair = self.derive_htlc_key_pair(swap_unique_data); + let tx = try_tx_fus!(ZTransaction::read(taker_spends_payment_args.other_payment_tx)); + let key_pair = self.derive_htlc_key_pair(taker_spends_payment_args.swap_unique_data); + let time_lock = taker_spends_payment_args.time_lock; let redeem_script = payment_script( time_lock, - &*dhash160(secret), - &try_tx_fus!(Public::from_slice(maker_pub)), + taker_spends_payment_args.secret_hash, + &try_tx_fus!(Public::from_slice(taker_spends_payment_args.other_pubkey)), key_pair.public(), ); let script_data = ScriptBuilder::default() - .push_data(secret) + .push_data(taker_spends_payment_args.secret) .push_opcode(Opcode::OP_0) .into_script(); let selfi = self.clone(); @@ -1163,20 +1233,16 @@ impl SwapOps for ZCoin { 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], + taker_refunds_payment_args: SendTakerRefundsPaymentArgs<'_>, ) -> TransactionFut { - let tx = try_tx_fus!(ZTransaction::read(taker_payment_tx)); - let key_pair = self.derive_htlc_key_pair(swap_unique_data); + let tx = try_tx_fus!(ZTransaction::read(taker_refunds_payment_args.payment_tx)); + let key_pair = self.derive_htlc_key_pair(taker_refunds_payment_args.swap_unique_data); + let time_lock = taker_refunds_payment_args.time_lock; let redeem_script = payment_script( time_lock, - secret_hash, + taker_refunds_payment_args.secret_hash, key_pair.public(), - &try_tx_fus!(Public::from_slice(maker_pub)), + &try_tx_fus!(Public::from_slice(taker_refunds_payment_args.other_pubkey)), ); let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_1).into_script(); let selfi = self.clone(); @@ -1198,20 +1264,16 @@ impl SwapOps for ZCoin { 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], + maker_refunds_payment_args: SendMakerRefundsPaymentArgs<'_>, ) -> TransactionFut { - let tx = try_tx_fus!(ZTransaction::read(maker_payment_tx)); - let key_pair = self.derive_htlc_key_pair(swap_unique_data); + let tx = try_tx_fus!(ZTransaction::read(maker_refunds_payment_args.payment_tx)); + let key_pair = self.derive_htlc_key_pair(maker_refunds_payment_args.swap_unique_data); + let time_lock = maker_refunds_payment_args.time_lock; let redeem_script = payment_script( time_lock, - secret_hash, + maker_refunds_payment_args.secret_hash, key_pair.public(), - &try_tx_fus!(Public::from_slice(taker_pub)), + &try_tx_fus!(Public::from_slice(maker_refunds_payment_args.other_pubkey)), ); let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_1).into_script(); let selfi = self.clone(); @@ -1233,19 +1295,15 @@ impl SwapOps for ZCoin { fn validate_fee( &self, - fee_tx: &TransactionEnum, - _expected_sender: &[u8], - _fee_addr: &[u8], - amount: &BigDecimal, - min_block_number: u64, - uuid: &[u8], + validate_fee_args: ValidateFeeArgs<'_>, ) -> Box + Send> { - let z_tx = match fee_tx { + let z_tx = match validate_fee_args.fee_tx { TransactionEnum::ZTransaction(t) => t.clone(), - _ => panic!("Unexpected tx {:?}", fee_tx), + _ => panic!("Unexpected tx {:?}", validate_fee_args.fee_tx), }; - let amount_sat = try_fus!(sat_from_big_decimal(amount, self.utxo_arc.decimals)); - let expected_memo = MemoBytes::from_bytes(uuid).expect("Uuid length < 512"); + let amount_sat = try_fus!(sat_from_big_decimal(validate_fee_args.amount, self.utxo_arc.decimals)); + let expected_memo = MemoBytes::from_bytes(validate_fee_args.uuid).expect("Uuid length < 512"); + let min_block_number = validate_fee_args.min_block_number; let coin = self.clone(); let fut = async move { @@ -1316,26 +1374,31 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + #[inline] + fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { utxo_common::validate_maker_payment(self, input) } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> Box + Send> { + #[inline] + fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { utxo_common::validate_taker_payment(self, input) } + #[inline] 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], + if_my_payment_sent_args: CheckIfMyPaymentSentArgs<'_>, ) -> Box, Error = String> + Send> { - utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash, swap_unique_data) + utxo_common::check_if_my_payment_sent( + self.clone(), + if_my_payment_sent_args.time_lock, + if_my_payment_sent_args.other_pub, + if_my_payment_sent_args.secret_hash, + if_my_payment_sent_args.swap_unique_data, + ) } + #[inline] async fn search_for_swap_tx_spend_my( &self, input: SearchForSwapTxSpendInput<'_>, @@ -1343,6 +1406,7 @@ impl SwapOps for ZCoin { utxo_common::search_for_swap_tx_spend_my(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } + #[inline] async fn search_for_swap_tx_spend_other( &self, input: SearchForSwapTxSpendInput<'_>, @@ -1350,10 +1414,24 @@ impl SwapOps for ZCoin { 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> { + fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + + #[inline] + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } + + #[inline] fn negotiate_swap_contract_addr( &self, _other_side_address: Option<&[u8]>, @@ -1368,12 +1446,128 @@ impl SwapOps for ZCoin { let key = secp_privkey_from_hash(dhash256(&signature)); key_pair_from_secret(key.as_slice()).expect("valid privkey") } + + #[inline] + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + } + + #[inline] + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + utxo_common::validate_other_pubkey(raw_pubkey) + } + + async fn maker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _maker_lock_duration: u64, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + async fn taker_payment_instructions( + &self, + _secret_hash: &[u8], + _amount: &BigDecimal, + _expires_in: u64, + ) -> Result>, MmError> { + Ok(None) + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + _maker_lock_duration: u64, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _secret_hash: &[u8], + _amount: BigDecimal, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) + } +} + +#[async_trait] +impl TakerSwapMakerCoin for ZCoin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for ZCoin { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for ZCoin { + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &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_taker_payment_refund_preimage( + &self, + _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + ) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + async fn watcher_search_for_swap_tx_spend( + &self, + _input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } } #[async_trait] impl MmCoin for ZCoin { fn is_asset_chain(&self) -> bool { self.utxo_arc.conf.asset_chain } + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn withdraw(&self, _req: WithdrawRequest) -> WithdrawFut { Box::new(futures01::future::err(MmError::new(WithdrawError::InternalError( "Zcoin doesn't support legacy withdraw".into(), @@ -1384,6 +1578,14 @@ impl MmCoin for ZCoin { Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) } + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { + Box::new( + utxo_common::get_tx_hex_by_hash(&self.utxo_arc, tx_hash) + .boxed() + .compat(), + ) + } + fn decimals(&self) -> u8 { self.utxo_arc.decimals } fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { @@ -1430,7 +1632,7 @@ impl MmCoin for ZCoin { }) } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { utxo_common::get_receiver_trade_fee(self.clone()) } @@ -1460,6 +1662,8 @@ impl MmCoin for ZCoin { fn swap_contract_address(&self) -> Option { utxo_common::swap_contract_address() } + fn fallback_swap_contract(&self) -> Option { utxo_common::fallback_swap_contract() } + fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } @@ -1467,6 +1671,10 @@ impl MmCoin for ZCoin { fn is_coin_protocol_supported(&self, info: &Option>) -> bool { utxo_common::is_coin_protocol_supported(self, info) } + + fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.as_ref().abortable_system) } + + fn on_token_deactivated(&self, _ticker: &str) {} } #[async_trait] @@ -1520,8 +1728,8 @@ impl GetUtxoListOps for ZCoin { #[async_trait] impl UtxoCommonOps for ZCoin { - async fn get_htlc_spend_fee(&self, tx_size: u64) -> UtxoRpcResult { - utxo_common::get_htlc_spend_fee(self, tx_size).await + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { + utxo_common::get_htlc_spend_fee(self, tx_size, stage).await } fn addresses_from_script(&self, script: &Script) -> Result, String> { @@ -1534,7 +1742,7 @@ impl UtxoCommonOps for ZCoin { utxo_common::my_public_key(self.as_ref()) } - fn address_from_str(&self, address: &str) -> Result { + fn address_from_str(&self, address: &str) -> MmResult { utxo_common::checked_address_from_str(self, address) } @@ -1650,7 +1858,7 @@ impl InitWithdrawCoin for ZCoin { task_handle.update_in_progress_status(WithdrawInProgressStatus::GeneratingTransaction)?; let satoshi = sat_from_big_decimal(&amount, self.decimals())?; - let memo = req.memo.map(|memo| interpret_memo_string(&memo)).transpose()?; + let memo = req.memo.as_deref().map(interpret_memo_string).transpose()?; let z_output = ZOutput { to_addr, amount: Amount::from_u64(satoshi) @@ -1689,12 +1897,14 @@ impl InitWithdrawCoin for ZCoin { internal_id: tx_hash.into(), kmd_rewards: None, transaction_type: Default::default(), + memo: req.memo, }) } } /// Interpret a string or hex-encoded memo, and return a Memo object. /// Inspired by https://github.com/adityapk00/zecwallet-light-cli/blob/v1.7.20/lib/src/lightwallet/utils.rs#L23 +#[allow(clippy::result_large_err)] pub fn interpret_memo_string(memo_str: &str) -> MmResult { // If the string starts with an "0x", and contains only hex chars ([a-f0-9]+) then // interpret it as a hex. @@ -1710,6 +1920,50 @@ pub fn interpret_memo_string(memo_str: &str) -> MmResult MmResult { + match priv_key_policy { + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(ExtendedSpendingKey::master(iguana.as_slice())), + PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { + extended_spending_key_from_global_hd_account(protocol_info, global_hd) + }, + PrivKeyBuildPolicy::Trezor => { + let priv_key_err = PrivKeyPolicyNotAllowed::HardwareWalletNotSupported; + MmError::err(ZCoinBuildError::UtxoBuilderError( + UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err), + )) + }, + } +} + +fn extended_spending_key_from_global_hd_account( + protocol_info: &ZcoinProtocolInfo, + global_hd: &GlobalHDAccountArc, +) -> MmResult { + let path_to_coin = protocol_info + .z_derivation_path + .clone() + .or_mm_err(|| ZCoinBuildError::ZDerivationPathNotSet)?; + + let path_to_account = path_to_coin + .to_derivation_path() + .into_iter() + // Map `bip32::ChildNumber` to `zip32::Zip32Child`. + .map(|child| Zip32Child::from_index(child.0)) + // Push the hardened `account` index, so the derivation path looks like: + // `m/purpose'/coin'/account'`. + .chain(iter::once(Zip32Child::Hardened(global_hd.account_id()))); + + let mut spending_key = ExtendedSpendingKey::master(global_hd.root_seed_bytes()); + for zip32_child in path_to_account { + spending_key = spending_key.derive_child(zip32_child); + } + + Ok(spending_key) +} + #[test] fn derive_z_key_from_mm_seed() { use crypto::privkey::key_pair_from_seed; diff --git a/mm2src/coins/z_coin/z_coin_errors.rs b/mm2src/coins/z_coin/z_coin_errors.rs index add11fc1ba..8cb6da1359 100644 --- a/mm2src/coins/z_coin/z_coin_errors.rs +++ b/mm2src/coins/z_coin/z_coin_errors.rs @@ -2,7 +2,8 @@ use crate::my_tx_history_v2::MyTxHistoryErrorV2; use crate::utxo::rpc_clients::UtxoRpcError; use crate::utxo::utxo_builder::UtxoCoinBuildError; use crate::WithdrawError; -use crate::{NumConversError, PrivKeyNotAllowed}; +use crate::{NumConversError, PrivKeyPolicyNotAllowed}; +use common::jsonrpc_client::JsonRpcError; use db_common::sqlite::rusqlite::Error as SqliteError; use db_common::sqlite::rusqlite::Error as SqlError; use derive_more::Display; @@ -19,6 +20,10 @@ pub enum UpdateBlocksCacheErr { GrpcError(tonic::Status), BlocksDbError(SqliteError), ZcashSqliteError(ZcashClientError), + UtxoRpcError(UtxoRpcError), + InternalError(String), + JsonRpcError(JsonRpcError), + GetLiveLightClientError(String), } impl From for UpdateBlocksCacheErr { @@ -33,18 +38,34 @@ impl From for UpdateBlocksCacheErr { fn from(err: ZcashClientError) -> Self { UpdateBlocksCacheErr::ZcashSqliteError(err) } } +impl From for UpdateBlocksCacheErr { + fn from(err: UtxoRpcError) -> Self { UpdateBlocksCacheErr::UtxoRpcError(err) } +} + +impl From for UpdateBlocksCacheErr { + fn from(err: JsonRpcError) -> Self { UpdateBlocksCacheErr::JsonRpcError(err) } +} + #[derive(Debug, Display)] #[non_exhaustive] -pub enum ZcoinLightClientInitError { - TlsConfigFailure(tonic::transport::Error), - ConnectionFailure(tonic::transport::Error), +pub enum ZcoinClientInitError { BlocksDbInitFailure(SqliteError), WalletDbInitFailure(SqliteError), ZcashSqliteError(ZcashClientError), + EmptyLightwalletdUris, + #[display(fmt = "Fail to init clients while iterating lightwalletd urls {:?}", _0)] + UrlIterFailure(Vec), } -impl From for ZcoinLightClientInitError { - fn from(err: ZcashClientError) -> Self { ZcoinLightClientInitError::ZcashSqliteError(err) } +impl From for ZcoinClientInitError { + fn from(err: ZcashClientError) -> Self { ZcoinClientInitError::ZcashSqliteError(err) } +} + +#[derive(Debug, Display)] +pub enum UrlIterError { + InvalidUri(InvalidUri), + TlsConfigFailure(tonic::transport::Error), + ConnectionFailure(tonic::transport::Error), } #[derive(Debug, Display)] @@ -140,11 +161,11 @@ pub enum SendOutputsErr { NumConversion(NumConversError), Rpc(UtxoRpcError), TxNotMined(String), - PrivKeyNotAllowed(PrivKeyNotAllowed), + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), } -impl From for SendOutputsErr { - fn from(err: PrivKeyNotAllowed) -> Self { SendOutputsErr::PrivKeyNotAllowed(err) } +impl From for SendOutputsErr { + fn from(err: PrivKeyPolicyNotAllowed) -> Self { SendOutputsErr::PrivKeyPolicyNotAllowed(err) } } impl From for SendOutputsErr { @@ -182,11 +203,10 @@ pub enum ZCoinBuildError { path: String, }, Io(std::io::Error), - EmptyLightwalletdUris, - NativeModeIsNotSupportedYet, - InvalidLightwalletdUri(InvalidUri), - LightClientInitErr(ZcoinLightClientInitError), + RpcClientInitErr(ZcoinClientInitError), ZCashParamsNotFound, + ZDerivationPathNotSet, + SaplingParamsInvalidChecksum, } impl From for ZCoinBuildError { @@ -205,12 +225,8 @@ impl From for ZCoinBuildError { fn from(err: std::io::Error) -> ZCoinBuildError { ZCoinBuildError::Io(err) } } -impl From for ZCoinBuildError { - fn from(err: InvalidUri) -> Self { ZCoinBuildError::InvalidLightwalletdUri(err) } -} - -impl From for ZCoinBuildError { - fn from(err: ZcoinLightClientInitError) -> Self { ZCoinBuildError::LightClientInitErr(err) } +impl From for ZCoinBuildError { + fn from(err: ZcoinClientInitError) -> Self { ZCoinBuildError::RpcClientInitErr(err) } } pub(super) enum SqlTxHistoryError { diff --git a/mm2src/coins/z_coin/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs index 51957d8218..fca156d0f7 100644 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ b/mm2src/coins/z_coin/z_coin_native_tests.rs @@ -43,13 +43,21 @@ fn zombie_coin_send_and_refund_maker_payment() { &secret_hash, "0.01".parse().unwrap(), &None, + &None, ) .wait() .unwrap(); println!("swap tx {}", hex::encode(&tx.tx_hash().0)); let refund_tx = coin - .send_maker_refunds_payment(&tx.tx_hex(), lock_time, &*taker_pub, &secret_hash, &priv_key, &None) + .send_maker_refunds_payment( + &tx.tx_hex().unwrap(), + lock_time, + &*taker_pub, + &secret_hash, + &priv_key, + &None, + ) .wait() .unwrap(); println!("refund tx {}", hex::encode(&refund_tx.tx_hash().0)); @@ -93,6 +101,7 @@ fn zombie_coin_send_and_spend_maker_payment() { &*secret_hash, "0.01".parse().unwrap(), &None, + &None, ) .wait() .unwrap(); diff --git a/mm2src/coins/z_coin/z_htlc.rs b/mm2src/coins/z_coin/z_htlc.rs index ee286b90ab..4914c0002d 100644 --- a/mm2src/coins/z_coin/z_htlc.rs +++ b/mm2src/coins/z_coin/z_htlc.rs @@ -10,7 +10,7 @@ use crate::utxo::rpc_clients::{UtxoRpcClientEnum, UtxoRpcError}; use crate::utxo::utxo_common::payment_script; use crate::utxo::{sat_from_big_decimal, UtxoAddressFormat}; use crate::z_coin::{SendOutputsErr, ZOutput, DEX_FEE_OVK}; -use crate::{NumConversError, PrivKeyNotAllowed, TransactionEnum}; +use crate::{NumConversError, PrivKeyPolicyNotAllowed, TransactionEnum}; use bitcrypto::dhash160; use common::async_blocking; use derive_more::Display; @@ -96,7 +96,7 @@ pub async fn z_send_dex_fee( #[allow(clippy::large_enum_variant, clippy::upper_case_acronyms)] pub enum ZP2SHSpendError { ZTxBuilderError(ZTxBuilderError), - PrivKeyNotAllowed(PrivKeyNotAllowed), + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), Rpc(UtxoRpcError), #[display(fmt = "{:?} {}", _0, _1)] TxRecoverable(TransactionEnum, String), @@ -107,8 +107,8 @@ impl From for ZP2SHSpendError { fn from(tx_builder: ZTxBuilderError) -> ZP2SHSpendError { ZP2SHSpendError::ZTxBuilderError(tx_builder) } } -impl From for ZP2SHSpendError { - fn from(err: PrivKeyNotAllowed) -> Self { ZP2SHSpendError::PrivKeyNotAllowed(err) } +impl From for ZP2SHSpendError { + fn from(err: PrivKeyPolicyNotAllowed) -> Self { ZP2SHSpendError::PrivKeyPolicyNotAllowed(err) } } impl From for ZP2SHSpendError { diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index 94f0e79901..66a5cfff1b 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -1,21 +1,26 @@ use super::{z_coin_errors::*, CheckPointBlockInfo, ZcoinConsensusParams}; -use crate::utxo::utxo_common; -use common::executor::Timer; +use crate::utxo::rpc_clients::{NativeClient, UtxoRpcClientOps, NO_TX_ERROR_CODE}; +use async_trait::async_trait; +use common::executor::{spawn_abortable, AbortOnDropHandle, Timer}; use common::log::{debug, error, info, LogOnError}; -use common::{async_blocking, spawn_abortable, AbortOnDropHandle}; +use common::{async_blocking, Future01CompatExt}; use db_common::sqlite::rusqlite::{params, Connection, Error as SqliteError, NO_PARAMS}; use db_common::sqlite::{query_single_row, run_optimization_pragmas}; use futures::channel::mpsc::{channel, Receiver as AsyncReceiver, Sender as AsyncSender}; use futures::channel::oneshot::{channel as oneshot_channel, Sender as OneshotSender}; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures::StreamExt; +use group::GroupEncoding; use http::Uri; use mm2_err_handle::prelude::*; use parking_lot::Mutex; use prost::Message; use protobuf::Message as ProtobufMessage; use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::str::FromStr; use std::sync::Arc; +use std::time::Duration; use tokio::task::block_in_place; use tonic::transport::{Channel, ClientTlsConfig}; use zcash_client_backend::data_api::chain::{scan_cached_blocks, validate_chain}; @@ -33,8 +38,12 @@ use zcash_primitives::zip32::ExtendedFullViewingKey; mod z_coin_grpc { tonic::include_proto!("cash.z.wallet.sdk.rpc"); } +use crate::{RpcCommonOps, ZTransaction}; +use rpc::v1::types::H256 as H256Json; use z_coin_grpc::compact_tx_streamer_client::CompactTxStreamerClient; -use z_coin_grpc::{BlockId, BlockRange, ChainSpec, TxFilter}; +use z_coin_grpc::{BlockId, BlockRange, ChainSpec, CompactBlock as TonicCompactBlock, + CompactOutput as TonicCompactOutput, CompactSpend as TonicCompactSpend, CompactTx as TonicCompactTx, + TxFilter}; pub type WalletDbShared = Arc>>; @@ -43,6 +52,225 @@ struct CompactBlockRow { data: Vec, } +pub type OnCompactBlockFn<'a> = dyn FnMut(TonicCompactBlock) -> Result<(), MmError> + Send + 'a; + +#[async_trait] +pub trait ZRpcOps { + async fn get_block_height(&mut self) -> Result>; + + async fn scan_blocks( + &mut self, + start_block: u64, + last_block: u64, + on_block: &mut OnCompactBlockFn, + ) -> Result<(), MmError>; + + async fn check_tx_existence(&mut self, tx_id: TxId) -> bool; +} + +struct LightRpcClient { + rpc_clients: AsyncMutex>>, +} + +#[async_trait] +impl RpcCommonOps for LightRpcClient { + type RpcClient = CompactTxStreamerClient; + type Error = MmError; + + async fn get_live_client(&self) -> Result { + let mut clients = self.rpc_clients.lock().await; + for (i, mut client) in clients.clone().into_iter().enumerate() { + let request = tonic::Request::new(ChainSpec {}); + // use get_latest_block method as a health check + if client.get_latest_block(request).await.is_ok() { + clients.rotate_left(i); + return Ok(client); + } + } + return Err(MmError::new(UpdateBlocksCacheErr::GetLiveLightClientError( + "All the current light clients are unavailable.".to_string(), + ))); + } +} + +#[async_trait] +impl ZRpcOps for LightRpcClient { + async fn get_block_height(&mut self) -> Result> { + let request = tonic::Request::new(ChainSpec {}); + let block = self + .get_live_client() + .await? + .get_latest_block(request) + .await + .map_to_mm(UpdateBlocksCacheErr::GrpcError)? + // return the message + .into_inner(); + Ok(block.height) + } + + async fn scan_blocks( + &mut self, + start_block: u64, + last_block: u64, + on_block: &mut OnCompactBlockFn, + ) -> Result<(), MmError> { + let request = tonic::Request::new(BlockRange { + start: Some(BlockId { + height: start_block, + hash: Vec::new(), + }), + end: Some(BlockId { + height: last_block, + hash: Vec::new(), + }), + }); + let mut response = self + .get_live_client() + .await? + .get_block_range(request) + .await + .map_to_mm(UpdateBlocksCacheErr::GrpcError)? + .into_inner(); + // without Pin method get_mut is not found in current scope + while let Some(block) = Pin::new(&mut response).get_mut().message().await? { + debug!("Got block {:?}", block); + on_block(block)?; + } + Ok(()) + } + + async fn check_tx_existence(&mut self, tx_id: TxId) -> bool { + let mut attempts = 0; + loop { + if let Ok(mut client) = self.get_live_client().await { + let request = tonic::Request::new(TxFilter { + block: None, + index: 0, + hash: tx_id.0.into(), + }); + match client.get_transaction(request).await { + Ok(_) => break, + Err(e) => { + error!("Error on getting tx {}", tx_id); + if e.message().contains(NO_TX_ERROR_CODE) { + if attempts >= 3 { + return false; + } + attempts += 1; + } + Timer::sleep(30.).await; + }, + } + } + } + true + } +} + +#[async_trait] +impl ZRpcOps for NativeClient { + async fn get_block_height(&mut self) -> Result> { + Ok(self.get_block_count().compat().await?) + } + + async fn scan_blocks( + &mut self, + start_block: u64, + last_block: u64, + on_block: &mut OnCompactBlockFn, + ) -> Result<(), MmError> { + for height in start_block..=last_block { + let block = self.get_block_by_height(height).await?; + debug!("Got block {:?}", block); + let mut compact_txs = Vec::new(); + // By default, CompactBlocks only contain CompactTxs for transactions that contain Sapling spends or outputs. + // Create and push compact_tx during iteration. + for (tx_id, hash_tx) in block.tx.iter().enumerate() { + let tx_bytes = self.get_transaction_bytes(hash_tx).compat().await?; + let tx = ZTransaction::read(tx_bytes.as_slice()).unwrap(); + let mut spends = Vec::new(); + let mut outputs = Vec::new(); + if !tx.shielded_spends.is_empty() || !tx.shielded_outputs.is_empty() { + // Create and push spends with outs for compact_tx during iterations. + for spend in &tx.shielded_spends { + let compact_spend = TonicCompactSpend { + nf: spend.nullifier.to_vec(), + }; + spends.push(compact_spend); + } + for out in &tx.shielded_outputs { + let compact_out = TonicCompactOutput { + cmu: out.cmu.to_bytes().to_vec(), + epk: out.ephemeral_key.to_bytes().to_vec(), + // https://zips.z.cash/zip-0307#output-compression + // The first 52 bytes of the ciphertext contain the contents and opening of the note commitment, + // which is all of the data needed to spend the note and to verify that the note is spendable. + ciphertext: out.enc_ciphertext[0..52].to_vec(), + }; + outputs.push(compact_out); + } + // Shadowing mut variables as immutable. No longer need to update them. + drop_mutability!(spends); + drop_mutability!(outputs); + let mut hash_tx_vec = hash_tx.0.to_vec(); + hash_tx_vec.reverse(); + + let compact_tx = TonicCompactTx { + index: tx_id as u64, + hash: hash_tx_vec, + fee: 0, + spends, + outputs, + }; + compact_txs.push(compact_tx); + } + } + let mut hash = block.hash.0.to_vec(); + hash.reverse(); + // Set 0 in vector in the case of genesis block. + let mut prev_hash = block.previousblockhash.unwrap_or_default().0.to_vec(); + prev_hash.reverse(); + // Shadowing mut variables as immutable. + drop_mutability!(hash); + drop_mutability!(prev_hash); + drop_mutability!(compact_txs); + + let compact_block = TonicCompactBlock { + proto_version: 0, + height, + hash, + prev_hash, + time: block.time, + // (hash, prevHash, and time) OR (full header) + header: Vec::new(), + vtx: compact_txs, + }; + on_block(compact_block)?; + } + Ok(()) + } + + async fn check_tx_existence(&mut self, tx_id: TxId) -> bool { + let mut attempts = 0; + loop { + match self.get_raw_transaction_bytes(&H256Json::from(tx_id.0)).compat().await { + Ok(_) => break, + Err(e) => { + error!("Error on getting tx {}", tx_id); + if e.to_string().contains(NO_TX_ERROR_CODE) { + if attempts >= 3 { + return false; + } + attempts += 1; + } + Timer::sleep(30.).await; + }, + } + } + true + } +} + /// A wrapper for the SQLite connection to the block cache database. pub struct BlockDb(Connection); @@ -108,7 +336,7 @@ impl BlockDb { Ok(query_single_row( &self.0, "SELECT height FROM compactblocks ORDER BY height DESC LIMIT 1", - db_common::sqlite::rusqlite::NO_PARAMS, + NO_PARAMS, |row| row.get(0), )? .unwrap_or(0)) @@ -143,25 +371,18 @@ impl BlockSource for BlockDb { } } -pub(super) async fn init_light_client( - lightwalletd_url: Uri, - cache_db_path: PathBuf, +pub async fn create_wallet_db( wallet_db_path: PathBuf, consensus_params: ZcoinConsensusParams, check_point_block: Option, evk: ExtendedFullViewingKey, -) -> Result<(AsyncMutex, WalletDbShared), MmError> { - let blocks_db = - async_blocking(|| BlockDb::for_path(cache_db_path).map_to_mm(ZcoinLightClientInitError::BlocksDbInitFailure)) - .await?; - - let wallet_db = async_blocking({ - let consensus_params = consensus_params.clone(); - move || -> Result<_, MmError> { +) -> Result, MmError> { + async_blocking({ + move || -> Result, MmError> { let db = WalletDb::for_path(wallet_db_path, consensus_params) - .map_to_mm(ZcoinLightClientInitError::WalletDbInitFailure)?; - run_optimization_pragmas(db.sql_conn()).map_to_mm(ZcoinLightClientInitError::WalletDbInitFailure)?; - init_wallet_db(&db).map_to_mm(ZcoinLightClientInitError::WalletDbInitFailure)?; + .map_to_mm(ZcoinClientInitError::WalletDbInitFailure)?; + run_optimization_pragmas(db.sql_conn()).map_to_mm(ZcoinClientInitError::WalletDbInitFailure)?; + init_wallet_db(&db).map_to_mm(ZcoinClientInitError::WalletDbInitFailure)?; if db.get_extended_full_viewing_keys()?.is_empty() { init_accounts_table(&db, &[evk])?; if let Some(check_point) = check_point_block { @@ -177,31 +398,105 @@ pub(super) async fn init_light_client( Ok(db) } }) - .await?; + .await +} - let tonic_channel = Channel::builder(lightwalletd_url) - .tls_config(ClientTlsConfig::new()) - .map_to_mm(ZcoinLightClientInitError::TlsConfigFailure)? - .connect() - .await - .map_to_mm(ZcoinLightClientInitError::ConnectionFailure)?; - let grpc_client = CompactTxStreamerClient::new(tonic_channel); +pub(super) async fn init_light_client( + coin: String, + lightwalletd_urls: Vec, + blocks_db: BlockDb, + wallet_db: WalletDbShared, + consensus_params: ZcoinConsensusParams, + scan_blocks_per_iteration: u32, + scan_interval_ms: u64, +) -> Result<(AsyncMutex, WalletDbShared), MmError> { + let (sync_status_notifier, sync_watcher) = channel(1); + let (on_tx_gen_notifier, on_tx_gen_watcher) = channel(1); + let mut rpc_clients = Vec::new(); + let mut errors = Vec::new(); + if lightwalletd_urls.is_empty() { + return MmError::err(ZcoinClientInitError::EmptyLightwalletdUris); + } + for url in lightwalletd_urls { + let uri = match Uri::from_str(&url) { + Ok(uri) => uri, + Err(err) => { + errors.push(UrlIterError::InvalidUri(err)); + continue; + }, + }; + let endpoint = match Channel::builder(uri).tls_config(ClientTlsConfig::new()) { + Ok(endpoint) => endpoint, + Err(err) => { + errors.push(UrlIterError::TlsConfigFailure(err)); + continue; + }, + }; + let tonic_channel = match endpoint.connect().await { + Ok(tonic_channel) => tonic_channel, + Err(err) => { + errors.push(UrlIterError::ConnectionFailure(err)); + continue; + }, + }; + rpc_clients.push(CompactTxStreamerClient::new(tonic_channel)); + } + drop_mutability!(errors); + drop_mutability!(rpc_clients); + // check if rpc_clients is empty, then for loop wasn't successful + if rpc_clients.is_empty() { + return MmError::err(ZcoinClientInitError::UrlIterFailure(errors)); + } + + let sync_handle = SaplingSyncLoopHandle { + coin, + current_block: BlockHeight::from_u32(0), + blocks_db, + wallet_db: wallet_db.clone(), + consensus_params, + sync_status_notifier, + on_tx_gen_watcher, + watch_for_tx: None, + scan_blocks_per_iteration, + scan_interval_ms, + }; + let light_rpc_clients = LightRpcClient { + rpc_clients: AsyncMutex::new(rpc_clients), + }; + let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(light_rpc_clients))); + + Ok(( + SaplingSyncConnector::new_mutex_wrapped(sync_watcher, on_tx_gen_notifier, abort_handle), + wallet_db, + )) +} + +pub(super) async fn init_native_client( + coin: String, + native_client: NativeClient, + blocks_db: BlockDb, + wallet_db: WalletDbShared, + consensus_params: ZcoinConsensusParams, + scan_blocks_per_iteration: u32, + scan_interval_ms: u64, +) -> Result<(AsyncMutex, WalletDbShared), MmError> { let (sync_status_notifier, sync_watcher) = channel(1); let (on_tx_gen_notifier, on_tx_gen_watcher) = channel(1); - let wallet_db = Arc::new(Mutex::new(wallet_db)); let sync_handle = SaplingSyncLoopHandle { + coin, current_block: BlockHeight::from_u32(0), - grpc_client, blocks_db, wallet_db: wallet_db.clone(), consensus_params, sync_status_notifier, on_tx_gen_watcher, watch_for_tx: None, + scan_blocks_per_iteration, + scan_interval_ms, }; - let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle)); + let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(native_client))); Ok(( SaplingSyncConnector::new_mutex_wrapped(sync_watcher, on_tx_gen_notifier, abort_handle), @@ -218,14 +513,14 @@ fn is_tx_imported(conn: &Connection, tx_id: TxId) -> bool { } pub struct SaplingSyncRespawnGuard { - pub(super) sync_handle: Option, + pub(super) sync_handle: Option<(SaplingSyncLoopHandle, Box)>, pub(super) abort_handle: Arc>, } impl Drop for SaplingSyncRespawnGuard { fn drop(&mut self) { - if let Some(handle) = self.sync_handle.take() { - *self.abort_handle.lock() = spawn_abortable(light_wallet_db_sync_loop(handle)); + if let Some((handle, rpc)) = self.sync_handle.take() { + *self.abort_handle.lock() = spawn_abortable(light_wallet_db_sync_loop(handle, rpc)); } } } @@ -233,12 +528,14 @@ impl Drop for SaplingSyncRespawnGuard { impl SaplingSyncRespawnGuard { pub(super) fn watch_for_tx(&mut self, tx_id: TxId) { if let Some(ref mut handle) = self.sync_handle { - handle.watch_for_tx = Some(tx_id); + handle.0.watch_for_tx = Some(tx_id); } } #[inline] - pub(super) fn current_block(&self) -> BlockHeight { self.sync_handle.as_ref().expect("always Some").current_block } + pub(super) fn current_block(&self) -> BlockHeight { + self.sync_handle.as_ref().expect("always Some").0.current_block + } } pub enum SyncStatus { @@ -257,8 +554,8 @@ pub enum SyncStatus { } pub struct SaplingSyncLoopHandle { + coin: String, current_block: BlockHeight, - grpc_client: CompactTxStreamerClient, blocks_db: BlockDb, wallet_db: WalletDbShared, consensus_params: ZcoinConsensusParams, @@ -266,8 +563,10 @@ pub struct SaplingSyncLoopHandle { sync_status_notifier: AsyncSender, /// If new tx is required to be generated, we stop the sync and respawn it after tx is sent /// This watcher waits for such notification - on_tx_gen_watcher: AsyncReceiver>, + on_tx_gen_watcher: AsyncReceiver)>>, watch_for_tx: Option, + scan_blocks_per_iteration: u32, + scan_interval_ms: u64, } impl SaplingSyncLoopHandle { @@ -303,12 +602,13 @@ impl SaplingSyncLoopHandle { .debug_log_with_msg("No one seems interested in SyncStatus"); } - async fn update_blocks_cache(&mut self) -> Result<(), MmError> { - let request = tonic::Request::new(ChainSpec {}); - let current_blockchain_block = self.grpc_client.get_latest_block(request).await?; + async fn update_blocks_cache( + &mut self, + rpc: &mut (dyn ZRpcOps + Send), + ) -> Result<(), MmError> { + let current_block = rpc.get_block_height().await?; let current_block_in_db = block_in_place(|| self.blocks_db.get_latest_block())?; let extrema = block_in_place(|| self.wallet_db.lock().block_height_extrema())?; - let mut from_block = self .consensus_params .sapling_activation_height @@ -317,30 +617,14 @@ impl SaplingSyncLoopHandle { if let Some((_, max_in_wallet)) = extrema { from_block = from_block.max(max_in_wallet.into()); } - - let current_block: u64 = current_blockchain_block.into_inner().height; - if current_block >= from_block { - let request = tonic::Request::new(BlockRange { - start: Some(BlockId { - height: from_block, - hash: Vec::new(), - }), - end: Some(BlockId { - height: current_block, - hash: Vec::new(), - }), - }); - - let mut response = self.grpc_client.get_block_range(request).await?; - - while let Some(block) = response.get_mut().message().await? { - debug!("Got block {:?}", block); + rpc.scan_blocks(from_block, current_block, &mut |block: TonicCompactBlock| { block_in_place(|| self.blocks_db.insert_block(block.height as u32, block.encode_to_vec()))?; self.notify_blocks_cache_status(block.height, current_block); - } + Ok(()) + }) + .await?; } - self.current_block = BlockHeight::from_u32(current_block as u32); Ok(()) } @@ -384,35 +668,24 @@ impl SaplingSyncLoopHandle { }, None => self.notify_building_wallet_db(0, current_block.into()), } - scan_cached_blocks(&self.consensus_params, &self.blocks_db, &mut wallet_ops, Some(1000))?; + + scan_cached_blocks( + &self.consensus_params, + &self.blocks_db, + &mut wallet_ops, + Some(self.scan_blocks_per_iteration), + )?; + if self.scan_interval_ms > 0 { + std::thread::sleep(Duration::from_millis(self.scan_interval_ms)); + } } Ok(()) } - async fn check_watch_for_tx_existence(&mut self) { + async fn check_watch_for_tx_existence(&mut self, rpc: &mut (dyn ZRpcOps + Send)) { if let Some(tx_id) = self.watch_for_tx { - let mut attempts = 0; - loop { - let filter = TxFilter { - block: None, - index: 0, - hash: tx_id.0.into(), - }; - let request = tonic::Request::new(filter); - match self.grpc_client.get_transaction(request).await { - Ok(_) => break, - Err(e) => { - error!("Error on getting tx {}", tx_id); - if e.message().contains(utxo_common::NO_TX_ERROR_CODE) { - if attempts >= 3 { - self.watch_for_tx = None; - return; - } - attempts += 1; - } - Timer::sleep(30.).await; - }, - } + if !rpc.check_tx_existence(tx_id).await { + self.watch_for_tx = None; } } } @@ -439,10 +712,14 @@ impl SaplingSyncLoopHandle { /// 6. Once the transaction is generated and sent, `SaplingSyncRespawnGuard::watch_for_tx` is called to update `SaplingSyncLoopHandle` state. /// 7. Once the loop is respawned, it will check that broadcast tx is imported (or not available anymore) before stopping in favor of /// next wait_for_gen_tx_blockchain_sync call. -async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle) { +async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle, mut client: Box) { + info!( + "(Re)starting light_wallet_db_sync_loop for {}, blocks per iteration {}, interval in ms {}", + sync_handle.coin, sync_handle.scan_blocks_per_iteration, sync_handle.scan_interval_ms + ); // this loop is spawned as standalone task so it's safe to use block_in_place here loop { - if let Err(e) = sync_handle.update_blocks_cache().await { + if let Err(e) = sync_handle.update_blocks_cache(client.as_mut()).await { error!("Error {} on blocks cache update", e); sync_handle.notify_on_error(e.to_string()); Timer::sleep(10.).await; @@ -458,7 +735,7 @@ async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle) { sync_handle.notify_sync_finished(); - sync_handle.check_watch_for_tx_existence().await; + sync_handle.check_watch_for_tx_existence(client.as_mut()).await; if let Some(tx_id) = sync_handle.watch_for_tx { if !block_in_place(|| is_tx_imported(sync_handle.wallet_db.lock().sql_conn(), tx_id)) { @@ -470,10 +747,11 @@ async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle) { } if let Ok(Some(sender)) = sync_handle.on_tx_gen_watcher.try_next() { - match sender.send(sync_handle) { + match sender.send((sync_handle, client)) { Ok(_) => break, - Err(handle_from_channel) => { + Err((handle_from_channel, rpc_from_channel)) => { sync_handle = handle_from_channel; + client = rpc_from_channel; }, } } @@ -483,7 +761,7 @@ async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle) { } type SyncWatcher = AsyncReceiver; -type NewTxNotifier = AsyncSender>; +type NewTxNotifier = AsyncSender)>>; pub(super) struct SaplingSyncConnector { sync_watcher: SyncWatcher, @@ -519,8 +797,8 @@ impl SaplingSyncConnector { .map_to_mm(|_| BlockchainScanStopped {})?; receiver .await - .map(|handle| SaplingSyncRespawnGuard { - sync_handle: Some(handle), + .map(|(handle, rpc)| SaplingSyncRespawnGuard { + sync_handle: Some((handle, rpc)), abort_handle: self.abort_handle.clone(), }) .map_to_mm(|_| BlockchainScanStopped {}) diff --git a/mm2src/coins_activation/Cargo.toml b/mm2src/coins_activation/Cargo.toml index e95895aa99..7506494822 100644 --- a/mm2src/coins_activation/Cargo.toml +++ b/mm2src/coins_activation/Cargo.toml @@ -3,23 +3,35 @@ name = "coins_activation" version = "0.1.0" edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +doctest = false [dependencies] async-trait = "0.1" coins = { path = "../coins" } common = { path = "../common" } -mm2_core = { path = "../mm2_core" } -mm2_err_handle = { path = "../mm2_err_handle" } -mm2_number = { path = "../mm2_number" } crypto = { path = "../crypto" } derive_more = "0.99" +ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } hex = "0.4.2" +mm2_core = { path = "../mm2_core" } +mm2_err_handle = { path = "../mm2_err_handle" } +mm2_metrics = { path = "../mm2_metrics" } +mm2_number = { path = "../mm2_number" } +parking_lot = { version = "0.12.0", features = ["nightly"] } rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } serde = "1.0" serde_derive = "1.0" -serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } \ No newline at end of file +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +mm2_metamask = { path = "../mm2_metamask" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +lightning = "0.0.110" +lightning-background-processor = "0.0.110" +lightning-invoice = { version = "0.18.0", features = ["serde"] } diff --git a/mm2src/coins_activation/src/bch_with_tokens_activation.rs b/mm2src/coins_activation/src/bch_with_tokens_activation.rs index e0f3102b47..97fca2acca 100644 --- a/mm2src/coins_activation/src/bch_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/bch_with_tokens_activation.rs @@ -3,17 +3,16 @@ use crate::prelude::*; use crate::slp_token_activation::SlpActivationRequest; use async_trait::async_trait; use coins::my_tx_history_v2::TxHistoryStorage; -use coins::utxo::bch::{bch_coin_from_conf_and_params, BchActivationRequest, BchCoin, CashAddrPrefix}; -use coins::utxo::bch_and_slp_tx_history::bch_and_slp_history_loop; +use coins::utxo::bch::{bch_coin_with_policy, BchActivationRequest, BchCoin, CashAddrPrefix}; use coins::utxo::rpc_clients::UtxoRpcError; -use coins::utxo::slp::{SlpProtocolConf, SlpToken}; +use coins::utxo::slp::{EnableSlpError, SlpProtocolConf, SlpToken}; +use coins::utxo::utxo_tx_history_v2::bch_and_slp_history_loop; use coins::utxo::UtxoCommonOps; -use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, PrivKeyNotAllowed, UnexpectedDerivationMethod}; -use common::executor::spawn; -use common::log::info; -use common::mm_metrics::MetricsArc; +use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, + UnexpectedDerivationMethod}; +use common::executor::{AbortSettings, SpawnAbortable}; use common::Future01CompatExt; -use futures::future::{abortable, AbortHandle}; +use crypto::CryptoCtxError; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; @@ -22,6 +21,19 @@ use serde_json::Value as Json; use std::collections::HashMap; use std::str::FromStr; +impl From for InitTokensAsMmCoinsError { + fn from(e: EnableSlpError) -> Self { + match e { + EnableSlpError::GetBalanceError(balance_err) => { + InitTokensAsMmCoinsError::CouldNotFetchBalance(balance_err.to_string()) + }, + EnableSlpError::UnexpectedDerivationMethod(internal) | EnableSlpError::Internal(internal) => { + InitTokensAsMmCoinsError::Internal(internal) + }, + } + } +} + pub struct SlpTokenInitializer { platform_coin: BchCoin, } @@ -35,7 +47,7 @@ impl TokenInitializer for SlpTokenInitializer { type Token = SlpToken; type TokenActivationRequest = SlpActivationRequest; type TokenProtocol = SlpProtocolConf; - type InitTokensError = std::convert::Infallible; + type InitTokensError = EnableSlpError; fn tokens_requests_from_platform_request( platform_params: &BchWithTokensActivationRequest, @@ -46,7 +58,7 @@ impl TokenInitializer for SlpTokenInitializer { async fn enable_tokens( &self, activation_params: Vec>, - ) -> Result, MmError> { + ) -> Result, MmError> { let tokens = activation_params .into_iter() .map(|params| { @@ -66,7 +78,7 @@ impl TokenInitializer for SlpTokenInitializer { required_confirmations, ) }) - .collect(); + .collect::>()?; Ok(tokens) } @@ -90,8 +102,8 @@ impl From for EnablePlatformCoinWithTokensError { prefix, ticker, error )) }, - BchWithTokensActivationError::PrivKeyNotAllowed(e) => { - EnablePlatformCoinWithTokensError::PrivKeyNotAllowed(e) + BchWithTokensActivationError::PrivKeyPolicyNotAllowed(e) => { + EnablePlatformCoinWithTokensError::PrivKeyPolicyNotAllowed(e) }, BchWithTokensActivationError::UnexpectedDerivationMethod(e) => { EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(e) @@ -161,7 +173,7 @@ pub enum BchWithTokensActivationError { prefix: String, error: String, }, - PrivKeyNotAllowed(String), + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), UnexpectedDerivationMethod(String), Transport(String), Internal(String), @@ -177,8 +189,12 @@ impl From for BchWithTokensActivationError { } } -impl From for BchWithTokensActivationError { - fn from(e: PrivKeyNotAllowed) -> Self { BchWithTokensActivationError::PrivKeyNotAllowed(e.to_string()) } +impl From for BchWithTokensActivationError { + fn from(e: PrivKeyPolicyNotAllowed) -> Self { BchWithTokensActivationError::PrivKeyPolicyNotAllowed(e) } +} + +impl From for BchWithTokensActivationError { + fn from(e: CryptoCtxError) -> Self { BchWithTokensActivationError::Internal(e.to_string()) } } #[async_trait] @@ -194,8 +210,9 @@ impl PlatformWithTokensActivationOps for BchCoin { platform_conf: Json, activation_request: Self::ActivationRequest, protocol_conf: Self::PlatformProtocolInfo, - priv_key: &[u8], ) -> Result> { + let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(&ctx)?; + let slp_prefix = CashAddrPrefix::from_str(&protocol_conf.slp_prefix).map_to_mm(|error| { BchWithTokensActivationError::InvalidSlpPrefix { ticker: ticker.clone(), @@ -204,13 +221,13 @@ impl PlatformWithTokensActivationOps for BchCoin { } })?; - let platform_coin = bch_coin_from_conf_and_params( + let platform_coin = bch_coin_with_policy( &ctx, &ticker, &platform_conf, activation_request.platform_request, slp_prefix, - priv_key, + priv_key_policy, ) .await .map_to_mm(|error| BchWithTokensActivationError::PlatformCoinCreationError { ticker, error })?; @@ -228,7 +245,7 @@ impl PlatformWithTokensActivationOps for BchCoin { async fn get_activation_result( &self, ) -> Result> { - let my_address = self.as_ref().derivation_method.iguana_or_err()?; + let my_address = self.as_ref().derivation_method.single_addr_or_err()?; let my_slp_address = self .get_my_slp_address() .map_to_mm(BchWithTokensActivationError::Internal)? @@ -270,22 +287,13 @@ impl PlatformWithTokensActivationOps for BchCoin { fn start_history_background_fetching( &self, - metrics: MetricsArc, + ctx: MmArc, storage: impl TxHistoryStorage + Send + 'static, initial_balance: BigDecimal, - ) -> AbortHandle { - let ticker = self.ticker().to_owned(); - let (fut, abort_handle) = abortable(bch_and_slp_history_loop( - self.clone(), - storage, - metrics, - initial_balance, - )); - spawn(async move { - if let Err(e) = fut.await { - info!("bch_and_slp_history_loop stopped for {}, reason {}", ticker, e); - } - }); - abort_handle + ) { + let fut = bch_and_slp_history_loop(self.clone(), storage, ctx.metrics.clone(), initial_balance); + + let settings = AbortSettings::info_on_abort(format!("bch_and_slp_history_loop stopped for {}", self.ticker())); + self.spawner().spawn_with_settings(fut, settings); } } diff --git a/mm2src/coins_activation/src/context.rs b/mm2src/coins_activation/src/context.rs index 8af083b2e2..a86869e7ab 100644 --- a/mm2src/coins_activation/src/context.rs +++ b/mm2src/coins_activation/src/context.rs @@ -1,3 +1,5 @@ +#[cfg(not(target_arch = "wasm32"))] +use crate::lightning_activation::LightningTaskManagerShared; use crate::utxo_activation::{QtumTaskManagerShared, UtxoStandardTaskManagerShared}; #[cfg(not(target_arch = "wasm32"))] use crate::z_coin_activation::ZcoinTaskManagerShared; @@ -10,6 +12,8 @@ pub struct CoinsActivationContext { pub(crate) init_qtum_task_manager: QtumTaskManagerShared, #[cfg(not(target_arch = "wasm32"))] pub(crate) init_z_coin_task_manager: ZcoinTaskManagerShared, + #[cfg(not(target_arch = "wasm32"))] + pub(crate) init_lightning_task_manager: LightningTaskManagerShared, } impl CoinsActivationContext { @@ -21,6 +25,8 @@ impl CoinsActivationContext { init_qtum_task_manager: RpcTaskManager::new_shared(), #[cfg(not(target_arch = "wasm32"))] init_z_coin_task_manager: RpcTaskManager::new_shared(), + #[cfg(not(target_arch = "wasm32"))] + init_lightning_task_manager: RpcTaskManager::new_shared(), }) }) } diff --git a/mm2src/coins_activation/src/erc20_token_activation.rs b/mm2src/coins_activation/src/erc20_token_activation.rs new file mode 100644 index 0000000000..9a5dff22d9 --- /dev/null +++ b/mm2src/coins_activation/src/erc20_token_activation.rs @@ -0,0 +1,106 @@ +use crate::{prelude::{TryFromCoinProtocol, TryPlatformCoinFromMmCoinEnum}, + token::{EnableTokenError, TokenActivationOps, TokenProtocolParams}}; +use async_trait::async_trait; +use coins::{eth::{v2_activation::{Erc20Protocol, Erc20TokenActivationError, Erc20TokenActivationRequest}, + valid_addr_from_str, EthCoin}, + CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum}; +use common::Future01CompatExt; +use mm2_err_handle::prelude::MmError; +use serde::Serialize; +use std::collections::HashMap; + +#[derive(Debug, Serialize)] +pub struct Erc20InitResult { + balances: HashMap, + platform_coin: String, + token_contract_address: String, + required_confirmations: u64, +} + +impl From for EnableTokenError { + fn from(err: Erc20TokenActivationError) -> Self { + match err { + Erc20TokenActivationError::InternalError(e) => EnableTokenError::Internal(e), + Erc20TokenActivationError::CouldNotFetchBalance(e) => EnableTokenError::Transport(e), + } + } +} + +impl TryPlatformCoinFromMmCoinEnum for EthCoin { + fn try_from_mm_coin(coin: MmCoinEnum) -> Option + where + Self: Sized, + { + match coin { + MmCoinEnum::EthCoin(coin) => Some(coin), + _ => None, + } + } +} + +impl TryFromCoinProtocol for Erc20Protocol { + fn try_from_coin_protocol(proto: CoinProtocol) -> Result> + where + Self: Sized, + { + match proto { + CoinProtocol::ERC20 { + platform, + contract_address, + } => { + let token_addr = valid_addr_from_str(&contract_address).map_err(|_| CoinProtocol::ERC20 { + platform: platform.clone(), + contract_address, + })?; + + Ok(Erc20Protocol { platform, token_addr }) + }, + proto => MmError::err(proto), + } + } +} + +impl TokenProtocolParams for Erc20Protocol { + fn platform_coin_ticker(&self) -> &str { &self.platform } +} + +#[async_trait] +impl TokenActivationOps for EthCoin { + type ActivationParams = Erc20TokenActivationRequest; + type ProtocolInfo = Erc20Protocol; + type ActivationResult = Erc20InitResult; + type ActivationError = Erc20TokenActivationError; + + async fn enable_token( + ticker: String, + platform_coin: Self::PlatformCoin, + activation_params: Self::ActivationParams, + protocol_conf: Self::ProtocolInfo, + ) -> Result<(Self, Self::ActivationResult), MmError> { + let token = platform_coin + .initialize_erc20_token(activation_params, protocol_conf, ticker) + .await?; + + let address = token.my_address()?; + let token_contract_address = token + .erc20_token_address() + .ok_or_else(|| Erc20TokenActivationError::InternalError("Token contract address is missing".to_string()))?; + + let balance = token + .my_balance() + .compat() + .await + .map_err(|e| Erc20TokenActivationError::CouldNotFetchBalance(e.to_string()))?; + + let balances = HashMap::from([(address, balance)]); + + let init_result = Erc20InitResult { + balances, + platform_coin: token.platform_ticker().to_owned(), + required_confirmations: token.required_confirmations(), + token_contract_address: format!("{:#02x}", token_contract_address), + }; + + Ok((token, init_result)) + } +} diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs new file mode 100644 index 0000000000..7b5d099ba5 --- /dev/null +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -0,0 +1,269 @@ +use crate::{platform_coin_with_tokens::{EnablePlatformCoinWithTokensError, GetPlatformBalance, + InitTokensAsMmCoinsError, PlatformWithTokensActivationOps, RegisterTokenInfo, + TokenActivationParams, TokenActivationRequest, TokenAsMmCoinInitializer, + TokenInitializer, TokenOf}, + prelude::*}; +use async_trait::async_trait; +use coins::eth::v2_activation::EthPrivKeyActivationPolicy; +use coins::eth::EthPrivKeyBuildPolicy; +use coins::{eth::{v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationError, + Erc20TokenActivationRequest, EthActivationV2Error, EthActivationV2Request}, + Erc20TokenInfo, EthCoin, EthCoinType}, + my_tx_history_v2::TxHistoryStorage, + CoinBalance, CoinProtocol, MarketCoinOps, MmCoin}; +use common::Future01CompatExt; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +#[cfg(target_arch = "wasm32")] +use mm2_metamask::MetamaskRpcError; +use mm2_number::BigDecimal; +use serde::{Deserialize, Serialize}; +use serde_json::Value as Json; +use std::collections::HashMap; + +impl From for EnablePlatformCoinWithTokensError { + fn from(err: EthActivationV2Error) -> Self { + match err { + EthActivationV2Error::InvalidPayload(e) + | EthActivationV2Error::InvalidSwapContractAddr(e) + | EthActivationV2Error::InvalidFallbackSwapContract(e) => { + EnablePlatformCoinWithTokensError::InvalidPayload(e) + }, + #[cfg(target_arch = "wasm32")] + EthActivationV2Error::ExpectedRpcChainId => { + EnablePlatformCoinWithTokensError::InvalidPayload(err.to_string()) + }, + EthActivationV2Error::ActivationFailed { ticker, error } => { + EnablePlatformCoinWithTokensError::PlatformCoinCreationError { ticker, error } + }, + EthActivationV2Error::AtLeastOneNodeRequired => EnablePlatformCoinWithTokensError::AtLeastOneNodeRequired( + "Enable request for ETH coin must have at least 1 node".to_string(), + ), + EthActivationV2Error::CouldNotFetchBalance(e) | EthActivationV2Error::UnreachableNodes(e) => { + EnablePlatformCoinWithTokensError::Transport(e) + }, + EthActivationV2Error::DerivationPathIsNotSet => EnablePlatformCoinWithTokensError::InvalidPayload( + "'derivation_path' field is not found in config".to_string(), + ), + EthActivationV2Error::ErrorDeserializingDerivationPath(e) => { + EnablePlatformCoinWithTokensError::InvalidPayload(e) + }, + EthActivationV2Error::PrivKeyPolicyNotAllowed(e) => { + EnablePlatformCoinWithTokensError::PrivKeyPolicyNotAllowed(e) + }, + #[cfg(target_arch = "wasm32")] + EthActivationV2Error::MetamaskError(metamask) => { + EnablePlatformCoinWithTokensError::Transport(metamask.to_string()) + }, + EthActivationV2Error::InternalError(e) => EnablePlatformCoinWithTokensError::Internal(e), + } + } +} + +impl TryFromCoinProtocol for EthCoinType { + fn try_from_coin_protocol(proto: CoinProtocol) -> Result> + where + Self: Sized, + { + match proto { + CoinProtocol::ETH => Ok(EthCoinType::Eth), + protocol => MmError::err(protocol), + } + } +} + +pub struct Erc20Initializer { + platform_coin: EthCoin, +} + +impl From for InitTokensAsMmCoinsError { + fn from(error: Erc20TokenActivationError) -> Self { + match error { + Erc20TokenActivationError::InternalError(e) => InitTokensAsMmCoinsError::Internal(e), + Erc20TokenActivationError::CouldNotFetchBalance(e) => InitTokensAsMmCoinsError::CouldNotFetchBalance(e), + } + } +} + +#[async_trait] +impl TokenInitializer for Erc20Initializer { + type Token = EthCoin; + type TokenActivationRequest = Erc20TokenActivationRequest; + type TokenProtocol = Erc20Protocol; + type InitTokensError = Erc20TokenActivationError; + + fn tokens_requests_from_platform_request( + platform_params: &EthWithTokensActivationRequest, + ) -> Vec> { + platform_params.erc20_tokens_requests.clone() + } + + async fn enable_tokens( + &self, + activation_params: Vec>, + ) -> Result, MmError> { + let mut tokens = vec![]; + for param in activation_params { + let token: EthCoin = self + .platform_coin + .initialize_erc20_token(param.activation_request, param.protocol, param.ticker) + .await?; + tokens.push(token); + } + + Ok(tokens) + } + + fn platform_coin(&self) -> &EthCoin { &self.platform_coin } +} + +#[derive(Clone, Deserialize)] +pub struct EthWithTokensActivationRequest { + #[serde(flatten)] + platform_request: EthActivationV2Request, + erc20_tokens_requests: Vec>, +} + +impl TxHistory for EthWithTokensActivationRequest { + fn tx_history(&self) -> bool { false } +} + +impl TokenOf for EthCoin { + type PlatformCoin = EthCoin; +} + +impl RegisterTokenInfo for EthCoin { + fn register_token_info(&self, token: &EthCoin) { + self.add_erc_token_info(token.ticker().to_string(), Erc20TokenInfo { + token_address: token.erc20_token_address().unwrap(), + decimals: token.decimals(), + }); + } +} + +#[derive(Serialize)] +pub struct EthWithTokensActivationResult { + current_block: u64, + eth_addresses_infos: HashMap>, + erc20_addresses_infos: HashMap>, +} + +impl GetPlatformBalance for EthWithTokensActivationResult { + fn get_platform_balance(&self) -> BigDecimal { + self.eth_addresses_infos + .iter() + .fold(BigDecimal::from(0), |total, (_, addr_info)| { + &total + &addr_info.balances.get_total() + }) + } +} + +impl CurrentBlock for EthWithTokensActivationResult { + fn current_block(&self) -> u64 { self.current_block } +} + +#[async_trait] +impl PlatformWithTokensActivationOps for EthCoin { + type ActivationRequest = EthWithTokensActivationRequest; + type PlatformProtocolInfo = EthCoinType; + type ActivationResult = EthWithTokensActivationResult; + type ActivationError = EthActivationV2Error; + + async fn enable_platform_coin( + ctx: MmArc, + ticker: String, + platform_conf: Json, + activation_request: Self::ActivationRequest, + _protocol: Self::PlatformProtocolInfo, + ) -> Result> { + let priv_key_policy = eth_priv_key_build_policy(&ctx, &activation_request.platform_request.priv_key_policy)?; + + let platform_coin = eth_coin_from_conf_and_request_v2( + &ctx, + &ticker, + &platform_conf, + activation_request.platform_request, + priv_key_policy, + ) + .await?; + + Ok(platform_coin) + } + + fn token_initializers( + &self, + ) -> Vec>> { + vec![Box::new(Erc20Initializer { + platform_coin: self.clone(), + })] + } + + async fn get_activation_result(&self) -> Result> { + let my_address = self.my_address()?; + let pubkey = self.get_public_key()?; + + let current_block = self + .current_block() + .compat() + .await + .map_err(EthActivationV2Error::InternalError)?; + + let eth_balance = self + .my_balance() + .compat() + .await + .map_err(|e| EthActivationV2Error::CouldNotFetchBalance(e.to_string()))?; + let token_balances = self + .get_tokens_balance_list() + .await + .map_err(|e| EthActivationV2Error::CouldNotFetchBalance(e.to_string()))?; + + let mut result = EthWithTokensActivationResult { + current_block, + eth_addresses_infos: HashMap::new(), + erc20_addresses_infos: HashMap::new(), + }; + + result + .eth_addresses_infos + .insert(my_address.to_string(), CoinAddressInfo { + derivation_method: DerivationMethod::Iguana, + pubkey: pubkey.clone(), + balances: eth_balance, + }); + + result + .erc20_addresses_infos + .insert(my_address.to_string(), CoinAddressInfo { + derivation_method: DerivationMethod::Iguana, + pubkey, + balances: token_balances, + }); + + Ok(result) + } + + fn start_history_background_fetching( + &self, + _ctx: MmArc, + _storage: impl TxHistoryStorage + Send + 'static, + _initial_balance: BigDecimal, + ) { + } +} + +fn eth_priv_key_build_policy( + ctx: &MmArc, + activation_policy: &EthPrivKeyActivationPolicy, +) -> MmResult { + match activation_policy { + EthPrivKeyActivationPolicy::ContextPrivKey => Ok(EthPrivKeyBuildPolicy::detect_priv_key_policy(ctx)?), + #[cfg(target_arch = "wasm32")] + EthPrivKeyActivationPolicy::Metamask => { + let metamask_ctx = crypto::CryptoCtx::from_ctx(ctx)? + .metamask_ctx() + .or_mm_err(|| EthActivationV2Error::MetamaskError(MetamaskRpcError::MetamaskCtxNotInitialized))?; + Ok(EthPrivKeyBuildPolicy::Metamask(metamask_ctx)) + }, + } +} diff --git a/mm2src/coins_activation/src/l2.rs b/mm2src/coins_activation/src/l2.rs deleted file mode 100644 index 69f5e9a0c6..0000000000 --- a/mm2src/coins_activation/src/l2.rs +++ /dev/null @@ -1,153 +0,0 @@ -/// Contains L2 activation traits and their implementations for various coins -/// -use crate::prelude::*; -use async_trait::async_trait; -use coins::{lp_coinfind, lp_coinfind_or_err, CoinProtocol, CoinsContext, MmCoinEnum}; -use common::{HttpStatusCode, StatusCode}; -use derive_more::Display; -use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::*; -use ser_error_derive::SerializeErrorType; -use serde_derive::{Deserialize, Serialize}; -use serde_json::Value as Json; - -pub trait L2ProtocolParams { - fn platform_coin_ticker(&self) -> &str; -} - -#[async_trait] -pub trait L2ActivationOps: Into { - type PlatformCoin: TryPlatformCoinFromMmCoinEnum; - type ActivationParams; - type ProtocolInfo: L2ProtocolParams + TryFromCoinProtocol; - type ValidatedParams; - type CoinConf; - type ActivationResult; - type ActivationError: NotMmError; - - fn coin_conf_from_json(json: Json) -> Result>; - - fn validate_platform_configuration( - platform_coin: &Self::PlatformCoin, - ) -> Result<(), MmError>; - - fn validate_activation_params( - activation_params: Self::ActivationParams, - ) -> Result>; - - async fn enable_l2( - ctx: &MmArc, - platform_coin: Self::PlatformCoin, - validated_params: Self::ValidatedParams, - protocol_conf: Self::ProtocolInfo, - coin_conf: Self::CoinConf, - ) -> Result<(Self, Self::ActivationResult), MmError>; -} - -#[derive(Debug, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum EnableL2Error { - #[display(fmt = "Layer 2 {} is already activated", _0)] - L2IsAlreadyActivated(String), - #[display(fmt = "Layer 2 {} config is not found", _0)] - L2ConfigIsNotFound(String), - #[display(fmt = "Layer 2 {} protocol parsing failed: {}", ticker, error)] - L2ProtocolParseError { - ticker: String, - error: String, - }, - #[display(fmt = "Unexpected layer 2 protocol {:?} for {}", protocol, ticker)] - UnexpectedL2Protocol { - ticker: String, - protocol: CoinProtocol, - }, - #[display(fmt = "Platform coin {} is not activated", _0)] - PlatformCoinIsNotActivated(String), - #[display(fmt = "{} is not a platform coin for layer 2 {}", platform_coin_ticker, l2_ticker)] - UnsupportedPlatformCoin { - platform_coin_ticker: String, - l2_ticker: String, - }, - #[display(fmt = "Layer 2 configuration parsing failed: {}", _0)] - L2ConfigParseError(String), - Transport(String), - Internal(String), -} - -impl From for EnableL2Error { - fn from(err: CoinConfWithProtocolError) -> Self { - match err { - CoinConfWithProtocolError::ConfigIsNotFound(ticker) => EnableL2Error::L2ConfigIsNotFound(ticker), - CoinConfWithProtocolError::CoinProtocolParseError { ticker, err } => EnableL2Error::L2ProtocolParseError { - ticker, - error: err.to_string(), - }, - CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { - EnableL2Error::UnexpectedL2Protocol { ticker, protocol } - }, - } - } -} - -#[derive(Debug, Deserialize)] -pub struct EnableL2Request { - ticker: String, - activation_params: T, -} - -pub async fn enable_l2( - ctx: MmArc, - req: EnableL2Request, -) -> Result> -where - L2: L2ActivationOps, - EnableL2Error: From, - (L2::ActivationError, EnableL2Error): NotEqual, -{ - if let Ok(Some(_)) = lp_coinfind(&ctx, &req.ticker).await { - return MmError::err(EnableL2Error::L2IsAlreadyActivated(req.ticker)); - } - - let (coin_conf_json, l2_protocol): (Json, L2::ProtocolInfo) = coin_conf_with_protocol(&ctx, &req.ticker)?; - let coin_conf = L2::coin_conf_from_json(coin_conf_json)?; - - let platform_coin = lp_coinfind_or_err(&ctx, l2_protocol.platform_coin_ticker()) - .await - .mm_err(|_| EnableL2Error::PlatformCoinIsNotActivated(req.ticker.clone()))?; - - let platform_coin = - L2::PlatformCoin::try_from_mm_coin(platform_coin).or_mm_err(|| EnableL2Error::UnsupportedPlatformCoin { - platform_coin_ticker: l2_protocol.platform_coin_ticker().into(), - l2_ticker: req.ticker.clone(), - })?; - - L2::validate_platform_configuration(&platform_coin)?; - - let validated_params = L2::validate_activation_params(req.activation_params)?; - - let (l2, activation_result) = L2::enable_l2(&ctx, platform_coin, validated_params, l2_protocol, coin_conf).await?; - - let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); - coins_ctx - .add_coin(l2.into()) - .await - .mm_err(|e| EnableL2Error::L2IsAlreadyActivated(e.ticker))?; - - Ok(activation_result) -} - -impl HttpStatusCode for EnableL2Error { - fn status_code(&self) -> StatusCode { - match self { - EnableL2Error::L2IsAlreadyActivated(_) - | EnableL2Error::PlatformCoinIsNotActivated(_) - | EnableL2Error::L2ConfigIsNotFound { .. } - | EnableL2Error::UnexpectedL2Protocol { .. } => StatusCode::BAD_REQUEST, - EnableL2Error::L2ProtocolParseError { .. } - | EnableL2Error::UnsupportedPlatformCoin { .. } - | EnableL2Error::L2ConfigParseError(_) - | EnableL2Error::Transport(_) - | EnableL2Error::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} diff --git a/mm2src/coins_activation/src/l2/init_l2.rs b/mm2src/coins_activation/src/l2/init_l2.rs new file mode 100644 index 0000000000..00cf6eb8dc --- /dev/null +++ b/mm2src/coins_activation/src/l2/init_l2.rs @@ -0,0 +1,216 @@ +/// Contains L2 activation traits and their implementations for various coins +/// +use crate::context::CoinsActivationContext; +use crate::l2::init_l2_error::{CancelInitL2Error, InitL2StatusError, InitL2UserActionError}; +use crate::l2::InitL2Error; +use crate::prelude::*; +use async_trait::async_trait; +use coins::{lp_coinfind, lp_coinfind_or_err, CoinsContext, MmCoinEnum, RegisterCoinError}; +use common::SuccessResponse; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc_task::rpc_common::{CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusRequest, RpcTaskUserActionRequest}; +use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use serde_derive::Deserialize; +use serde_json::Value as Json; + +pub type InitL2Response = InitRpcTaskResponse; +pub type InitL2StatusRequest = RpcTaskStatusRequest; +pub type InitL2UserActionRequest = RpcTaskUserActionRequest; +pub type InitL2TaskManagerShared = RpcTaskManagerShared>; +pub type InitL2TaskHandle = RpcTaskHandle>; + +#[derive(Debug, Deserialize)] +pub struct InitL2Req { + ticker: String, + activation_params: T, +} + +pub trait L2ProtocolParams { + fn platform_coin_ticker(&self) -> &str; +} + +#[async_trait] +pub trait InitL2ActivationOps: Into + Send + Sync + 'static { + type PlatformCoin: TryPlatformCoinFromMmCoinEnum + Clone + Send + Sync; + type ActivationParams: Clone; + type ProtocolInfo: L2ProtocolParams + TryFromCoinProtocol + Clone + Send + Sync; + type ValidatedParams: Clone + Send + Sync; + type CoinConf: Clone + Send + Sync; + type ActivationResult: serde::Serialize + Clone + Send + Sync; + type ActivationError: From + NotEqual + SerMmErrorType + Clone + Send + Sync; + type InProgressStatus: InitL2InitialStatus + Clone + Send + Sync; + type AwaitingStatus: Clone + Send + Sync; + type UserAction: NotMmError + Send + Sync; + + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &InitL2TaskManagerShared; + + fn coin_conf_from_json(json: Json) -> Result>; + + fn validate_platform_configuration( + platform_coin: &Self::PlatformCoin, + ) -> Result<(), MmError>; + + fn validate_activation_params( + activation_params: Self::ActivationParams, + ) -> Result>; + + async fn init_l2( + ctx: &MmArc, + platform_coin: Self::PlatformCoin, + validated_params: Self::ValidatedParams, + protocol_conf: Self::ProtocolInfo, + coin_conf: Self::CoinConf, + task_handle: &InitL2TaskHandle, + ) -> Result<(Self, Self::ActivationResult), MmError>; +} + +pub async fn init_l2( + ctx: MmArc, + req: InitL2Req, +) -> Result> +where + L2: InitL2ActivationOps, + InitL2Error: From, + (L2::ActivationError, InitL2Error): NotEqual, +{ + let ticker = req.ticker.clone(); + if let Ok(Some(_)) = lp_coinfind(&ctx, &ticker).await { + return MmError::err(InitL2Error::L2IsAlreadyActivated(ticker)); + } + + let (coin_conf_json, protocol_conf): (Json, L2::ProtocolInfo) = coin_conf_with_protocol(&ctx, &ticker)?; + let coin_conf = L2::coin_conf_from_json(coin_conf_json)?; + + let platform_coin = lp_coinfind_or_err(&ctx, protocol_conf.platform_coin_ticker()) + .await + .mm_err(|_| InitL2Error::PlatformCoinIsNotActivated(ticker.clone()))?; + + let platform_coin = + L2::PlatformCoin::try_from_mm_coin(platform_coin).or_mm_err(|| InitL2Error::UnsupportedPlatformCoin { + platform_coin_ticker: protocol_conf.platform_coin_ticker().into(), + l2_ticker: ticker.clone(), + })?; + + L2::validate_platform_configuration(&platform_coin)?; + + let validated_params = L2::validate_activation_params(req.activation_params.clone())?; + + let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(InitL2Error::Internal)?; + let spawner = ctx.spawner(); + let task = InitL2Task:: { + ctx, + ticker, + platform_coin, + validated_params, + protocol_conf, + coin_conf, + }; + let task_manager = L2::rpc_task_manager(&coins_act_ctx); + + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + .mm_err(|e| InitL2Error::Internal(e.to_string()))?; + + Ok(InitL2Response { task_id }) +} + +pub async fn init_l2_status( + ctx: MmArc, + req: InitL2StatusRequest, +) -> MmResult< + RpcTaskStatus, + InitL2StatusError, +> +where + InitL2Error: From, +{ + let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(InitL2StatusError::Internal)?; + let mut task_manager = L2::rpc_task_manager(&coins_act_ctx) + .lock() + .map_to_mm(|poison| InitL2StatusError::Internal(poison.to_string()))?; + task_manager + .task_status(req.task_id, req.forget_if_finished) + .or_mm_err(|| InitL2StatusError::NoSuchTask(req.task_id)) + .map(|rpc_task| rpc_task.map_err(InitL2Error::from)) +} + +pub async fn init_l2_user_action( + ctx: MmArc, + req: InitL2UserActionRequest, +) -> MmResult { + let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(InitL2UserActionError::Internal)?; + let mut task_manager = L2::rpc_task_manager(&coins_act_ctx) + .lock() + .map_to_mm(|poison| InitL2UserActionError::Internal(poison.to_string()))?; + task_manager.on_user_action(req.task_id, req.user_action)?; + Ok(SuccessResponse::new()) +} + +pub async fn cancel_init_l2( + ctx: MmArc, + req: CancelRpcTaskRequest, +) -> MmResult { + let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(CancelInitL2Error::Internal)?; + let mut task_manager = L2::rpc_task_manager(&coins_act_ctx) + .lock() + .map_to_mm(|poison| CancelInitL2Error::Internal(poison.to_string()))?; + task_manager.cancel_task(req.task_id)?; + Ok(SuccessResponse::new()) +} + +pub struct InitL2Task { + ctx: MmArc, + ticker: String, + platform_coin: L2::PlatformCoin, + validated_params: L2::ValidatedParams, + protocol_conf: L2::ProtocolInfo, + coin_conf: L2::CoinConf, +} + +impl RpcTaskTypes for InitL2Task { + type Item = L2::ActivationResult; + type Error = L2::ActivationError; + type InProgressStatus = L2::InProgressStatus; + type AwaitingStatus = L2::AwaitingStatus; + type UserAction = L2::UserAction; +} + +#[async_trait] +impl RpcTask for InitL2Task +where + L2: InitL2ActivationOps, +{ + fn initial_status(&self) -> Self::InProgressStatus { + ::initial_status() + } + + /// Try to disable the coin in case if we managed to register it already. + async fn cancel(self) { + if let Ok(ctx) = CoinsContext::from_ctx(&self.ctx) { + if let Ok(Some(t)) = lp_coinfind(&self.ctx, &self.ticker).await { + ctx.remove_coin(t).await; + }; + }; + } + + async fn run(&mut self, task_handle: &RpcTaskHandle) -> Result> { + let (coin, result) = L2::init_l2( + &self.ctx, + self.platform_coin.clone(), + self.validated_params.clone(), + self.protocol_conf.clone(), + self.coin_conf.clone(), + task_handle, + ) + .await?; + + let c_ctx = CoinsContext::from_ctx(&self.ctx).map_to_mm(RegisterCoinError::Internal)?; + c_ctx.add_l2(coin.into()).await?; + + Ok(result) + } +} + +pub trait InitL2InitialStatus { + fn initial_status() -> Self; +} diff --git a/mm2src/coins_activation/src/l2/init_l2_error.rs b/mm2src/coins_activation/src/l2/init_l2_error.rs new file mode 100644 index 0000000000..d23fd73078 --- /dev/null +++ b/mm2src/coins_activation/src/l2/init_l2_error.rs @@ -0,0 +1,94 @@ +use crate::prelude::CoinConfWithProtocolError; +use coins::CoinProtocol; +use common::{HttpStatusCode, StatusCode}; +use derive_more::Display; +use rpc_task::rpc_common::{CancelRpcTaskError, RpcTaskStatusError, RpcTaskUserActionError}; +use rpc_task::RpcTaskError; +use ser_error_derive::SerializeErrorType; +use serde_derive::Serialize; +use std::time::Duration; + +pub type InitL2StatusError = RpcTaskStatusError; +pub type InitL2UserActionError = RpcTaskUserActionError; +pub type CancelInitL2Error = CancelRpcTaskError; + +#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum InitL2Error { + #[display(fmt = "Layer 2 {} is already activated", _0)] + L2IsAlreadyActivated(String), + #[display(fmt = "Layer 2 {} config is not found", _0)] + L2ConfigIsNotFound(String), + #[display(fmt = "Layer 2 {} protocol parsing failed: {}", ticker, error)] + L2ProtocolParseError { + ticker: String, + error: String, + }, + #[display(fmt = "Unexpected layer 2 protocol {:?} for {}", protocol, ticker)] + UnexpectedL2Protocol { + ticker: String, + protocol: CoinProtocol, + }, + #[display(fmt = "Platform coin {} is not activated", _0)] + PlatformCoinIsNotActivated(String), + #[display(fmt = "{} is not a platform coin for layer 2 {}", platform_coin_ticker, l2_ticker)] + UnsupportedPlatformCoin { + platform_coin_ticker: String, + l2_ticker: String, + }, + #[display(fmt = "Invalid config for platform coin: {}, error: {}", platform_coin_ticker, err)] + InvalidPlatformConfiguration { + platform_coin_ticker: String, + err: String, + }, + #[display(fmt = "Layer 2 configuration parsing failed: {}", _0)] + L2ConfigParseError(String), + #[display(fmt = "Initialization task has timed out {:?}", duration)] + TaskTimedOut { + duration: Duration, + }, + Transport(String), + Internal(String), +} + +impl From for InitL2Error { + fn from(err: CoinConfWithProtocolError) -> Self { + match err { + CoinConfWithProtocolError::ConfigIsNotFound(ticker) => InitL2Error::L2ConfigIsNotFound(ticker), + CoinConfWithProtocolError::CoinProtocolParseError { ticker, err } => InitL2Error::L2ProtocolParseError { + ticker, + error: err.to_string(), + }, + CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { + InitL2Error::UnexpectedL2Protocol { ticker, protocol } + }, + } + } +} + +impl From for InitL2Error { + fn from(rpc_err: RpcTaskError) -> Self { + match rpc_err { + RpcTaskError::Timeout(duration) => InitL2Error::TaskTimedOut { duration }, + internal_error => InitL2Error::Internal(internal_error.to_string()), + } + } +} + +impl HttpStatusCode for InitL2Error { + fn status_code(&self) -> StatusCode { + match self { + InitL2Error::L2IsAlreadyActivated(_) + | InitL2Error::PlatformCoinIsNotActivated(_) + | InitL2Error::L2ConfigIsNotFound { .. } + | InitL2Error::UnexpectedL2Protocol { .. } => StatusCode::BAD_REQUEST, + InitL2Error::TaskTimedOut { .. } => StatusCode::REQUEST_TIMEOUT, + InitL2Error::L2ProtocolParseError { .. } + | InitL2Error::UnsupportedPlatformCoin { .. } + | InitL2Error::InvalidPlatformConfiguration { .. } + | InitL2Error::L2ConfigParseError(_) + | InitL2Error::Transport(_) + | InitL2Error::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} diff --git a/mm2src/coins_activation/src/l2/mod.rs b/mm2src/coins_activation/src/l2/mod.rs new file mode 100644 index 0000000000..b5168fa87e --- /dev/null +++ b/mm2src/coins_activation/src/l2/mod.rs @@ -0,0 +1,6 @@ +mod init_l2; +mod init_l2_error; + +pub use init_l2::{cancel_init_l2, init_l2, init_l2_status, init_l2_user_action, InitL2ActivationOps, + InitL2InitialStatus, InitL2Task, InitL2TaskHandle, InitL2TaskManagerShared, L2ProtocolParams}; +pub use init_l2_error::InitL2Error; diff --git a/mm2src/coins_activation/src/lib.rs b/mm2src/coins_activation/src/lib.rs index 923c835ba1..5f4245af6c 100644 --- a/mm2src/coins_activation/src/lib.rs +++ b/mm2src/coins_activation/src/lib.rs @@ -1,5 +1,7 @@ mod bch_with_tokens_activation; mod context; +mod erc20_token_activation; +mod eth_with_token_activation; mod l2; #[cfg(not(target_arch = "wasm32"))] mod lightning_activation; mod platform_coin_with_tokens; @@ -10,11 +12,14 @@ mod solana_with_tokens_activation; #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] mod spl_token_activation; mod standalone_coin; +mod tendermint_token_activation; +mod tendermint_with_assets_activation; mod token; mod utxo_activation; #[cfg(not(target_arch = "wasm32"))] mod z_coin_activation; -pub use l2::enable_l2; +pub use l2::{cancel_init_l2, init_l2, init_l2_status, init_l2_user_action}; pub use platform_coin_with_tokens::enable_platform_coin_with_tokens; -pub use standalone_coin::{init_standalone_coin, init_standalone_coin_status, init_standalone_coin_user_action}; +pub use standalone_coin::{cancel_init_standalone_coin, init_standalone_coin, init_standalone_coin_status, + init_standalone_coin_user_action}; pub use token::enable_token; diff --git a/mm2src/coins_activation/src/lightning_activation.rs b/mm2src/coins_activation/src/lightning_activation.rs index 5572755d09..b3da8c3f7b 100644 --- a/mm2src/coins_activation/src/lightning_activation.rs +++ b/mm2src/coins_activation/src/lightning_activation.rs @@ -1,22 +1,67 @@ -use crate::l2::{EnableL2Error, L2ActivationOps, L2ProtocolParams}; +use crate::context::CoinsActivationContext; +use crate::l2::{InitL2ActivationOps, InitL2Error, InitL2InitialStatus, InitL2TaskHandle, InitL2TaskManagerShared, + L2ProtocolParams}; use crate::prelude::*; use async_trait::async_trait; +use coins::coin_errors::MyAddressError; use coins::lightning::ln_conf::{LightningCoinConf, LightningProtocolConf}; -use coins::lightning::ln_errors::EnableLightningError; -use coins::lightning::{start_lightning, LightningCoin, LightningParams}; +use coins::lightning::ln_errors::{EnableLightningError, EnableLightningResult}; +use coins::lightning::ln_events::{init_abortable_events, LightningEventHandler}; +use coins::lightning::ln_p2p::{connect_to_ln_nodes_loop, init_peer_manager, ln_node_announcement_loop}; +use coins::lightning::ln_platform::Platform; +use coins::lightning::ln_storage::LightningStorage; +use coins::lightning::ln_utils::{get_open_channels_nodes_addresses, init_channel_manager, init_db, init_keys_manager, + init_persister, PAYMENT_RETRY_ATTEMPTS}; +use coins::lightning::{InvoicePayer, LightningCoin}; use coins::utxo::utxo_standard::UtxoStandardCoin; use coins::utxo::UtxoCommonOps; -use coins::{BalanceError, CoinBalance, CoinProtocol, MarketCoinOps, MmCoinEnum}; +use coins::{BalanceError, CoinBalance, CoinProtocol, MarketCoinOps, MmCoinEnum, RegisterCoinError}; +use common::executor::{SpawnFuture, Timer}; +use crypto::hw_rpc_task::{HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use derive_more::Display; use futures::compat::Future01CompatExt; +use lightning::chain::keysinterface::{KeysInterface, Recipient}; +use lightning::chain::Access; +use lightning::routing::gossip; +use lightning_background_processor::{BackgroundProcessor, GossipSync}; +use lightning_invoice::payment; +use lightning_invoice::utils::DefaultRouter; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use parking_lot::Mutex as PaMutex; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; use serde_json::{self as json, Value as Json}; +use std::sync::Arc; const DEFAULT_LISTENING_PORT: u16 = 9735; +pub type LightningTaskManagerShared = InitL2TaskManagerShared; +pub type LightningRpcTaskHandle = InitL2TaskHandle; +pub type LightningAwaitingStatus = HwRpcTaskAwaitingStatus; +pub type LightningUserAction = HwRpcTaskUserAction; + +#[derive(Clone, PartialEq, Serialize)] +pub enum LightningInProgressStatus { + ActivatingCoin, + GettingFeesFromRPC, + ReadingNetworkGraphFromFile, + InitializingChannelManager, + InitializingPeerManager, + ReadingScorerFromFile, + InitializingBackgroundProcessor, + ReadingChannelsAddressesFromFile, + Finished, + /// This status doesn't require the user to send `UserAction`, + /// but it tells the user that he should confirm/decline an address on his device. + WaitingForTrezorToConnect, + WaitingForUserToConfirmPubkey, +} + +impl InitL2InitialStatus for LightningInProgressStatus { + fn initial_status() -> Self { LightningInProgressStatus::ActivatingCoin } +} + impl TryPlatformCoinFromMmCoinEnum for UtxoStandardCoin { fn try_from_mm_coin(coin: MmCoinEnum) -> Option where @@ -38,11 +83,11 @@ impl TryFromCoinProtocol for LightningProtocolConf { CoinProtocol::LIGHTNING { platform, network, - confirmations, + confirmation_targets, } => Ok(LightningProtocolConf { platform_coin_ticker: platform, network, - confirmations, + confirmation_targets, }), proto => MmError::err(proto), } @@ -67,7 +112,23 @@ pub struct LightningActivationParams { pub backup_path: Option, } -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LightningValidatedParams { + // 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, +} + +#[derive(Clone, Debug, Deserialize, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum LightningValidationErr { #[display(fmt = "Platform coin {} activated in {} mode", _0, _1)] @@ -80,36 +141,59 @@ pub enum LightningValidationErr { InvalidAddress(String), } -#[derive(Debug, Serialize)] -pub struct LightningInitResult { +#[derive(Clone, Debug, Serialize)] +pub struct LightningActivationResult { platform_coin: String, address: String, balance: CoinBalance, } -#[derive(Debug)] +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] pub enum LightningInitError { + CoinIsAlreadyActivated { + ticker: String, + }, InvalidConfiguration(String), + #[display(fmt = "Error while validating {} configuration: {}", platform_coin_ticker, err)] + InvalidPlatformConfiguration { + platform_coin_ticker: String, + err: String, + }, EnableLightningError(EnableLightningError), LightningValidationErr(LightningValidationErr), MyBalanceError(BalanceError), MyAddressError(String), + Internal(String), +} + +impl From for LightningInitError { + fn from(err: MyAddressError) -> Self { Self::MyAddressError(err.to_string()) } } -impl From for EnableL2Error { +impl From for InitL2Error { fn from(err: LightningInitError) -> Self { match err { - LightningInitError::InvalidConfiguration(err) => EnableL2Error::L2ConfigParseError(err), + LightningInitError::CoinIsAlreadyActivated { ticker } => InitL2Error::L2IsAlreadyActivated(ticker), + LightningInitError::InvalidConfiguration(err) => InitL2Error::L2ConfigParseError(err), + LightningInitError::InvalidPlatformConfiguration { + platform_coin_ticker, + err, + } => InitL2Error::InvalidPlatformConfiguration { + platform_coin_ticker, + err, + }, LightningInitError::EnableLightningError(enable_err) => match enable_err { - EnableLightningError::RpcError(rpc_err) => EnableL2Error::Transport(rpc_err), - enable_error => EnableL2Error::Internal(enable_error.to_string()), + EnableLightningError::RpcError(rpc_err) => InitL2Error::Transport(rpc_err), + enable_error => InitL2Error::Internal(enable_error.to_string()), }, - LightningInitError::LightningValidationErr(req_err) => EnableL2Error::Internal(req_err.to_string()), + LightningInitError::LightningValidationErr(req_err) => InitL2Error::Internal(req_err.to_string()), LightningInitError::MyBalanceError(balance_err) => match balance_err { - BalanceError::Transport(e) => EnableL2Error::Transport(e), - balance_error => EnableL2Error::Internal(balance_error.to_string()), + BalanceError::Transport(e) => InitL2Error::Transport(e), + balance_error => InitL2Error::Internal(balance_error.to_string()), }, - LightningInitError::MyAddressError(e) => EnableL2Error::Internal(e), + LightningInitError::MyAddressError(e) => InitL2Error::Internal(e), + LightningInitError::Internal(e) => InitL2Error::Internal(e), } } } @@ -122,15 +206,33 @@ impl From for LightningInitError { fn from(err: LightningValidationErr) -> Self { LightningInitError::LightningValidationErr(err) } } +impl From for LightningInitError { + fn from(reg_err: RegisterCoinError) -> LightningInitError { + match reg_err { + RegisterCoinError::CoinIsInitializedAlready { coin } => { + LightningInitError::CoinIsAlreadyActivated { ticker: coin } + }, + RegisterCoinError::Internal(internal) => LightningInitError::Internal(internal), + } + } +} + #[async_trait] -impl L2ActivationOps for LightningCoin { +impl InitL2ActivationOps for LightningCoin { type PlatformCoin = UtxoStandardCoin; type ActivationParams = LightningActivationParams; type ProtocolInfo = LightningProtocolConf; - type ValidatedParams = LightningParams; + type ValidatedParams = LightningValidatedParams; type CoinConf = LightningCoinConf; - type ActivationResult = LightningInitResult; + type ActivationResult = LightningActivationResult; type ActivationError = LightningInitError; + type InProgressStatus = LightningInProgressStatus; + type AwaitingStatus = LightningAwaitingStatus; + type UserAction = LightningUserAction; + + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &LightningTaskManagerShared { + &activation_ctx.init_lightning_task_manager + } fn coin_conf_from_json(json: Json) -> Result> { json::from_value::(json) @@ -148,6 +250,12 @@ impl L2ActivationOps for LightningCoin { LightningValidationErr::UnsupportedMode("Lightning network".into(), "segwit".into()).into(), ); } + if platform_coin.as_ref().conf.avg_blocktime.is_none() { + return MmError::err(LightningInitError::InvalidPlatformConfiguration { + platform_coin_ticker: platform_coin.ticker().to_string(), + err: "'avg_blocktime' field is not found in platform coin config".into(), + }); + } Ok(()) } @@ -172,7 +280,7 @@ impl L2ActivationOps for LightningCoin { let listening_port = activation_params.listening_port.unwrap_or(DEFAULT_LISTENING_PORT); - Ok(LightningParams { + Ok(LightningValidatedParams { listening_port, node_name, node_color, @@ -181,24 +289,32 @@ impl L2ActivationOps for LightningCoin { }) } - async fn enable_l2( + async fn init_l2( ctx: &MmArc, platform_coin: Self::PlatformCoin, validated_params: Self::ValidatedParams, protocol_conf: Self::ProtocolInfo, coin_conf: Self::CoinConf, + task_handle: &LightningRpcTaskHandle, ) -> Result<(Self, Self::ActivationResult), MmError> { - let lightning_coin = - start_lightning(ctx, platform_coin.clone(), protocol_conf, coin_conf, validated_params).await?; - let address = lightning_coin - .my_address() - .map_to_mm(LightningInitError::MyAddressError)?; + let lightning_coin = start_lightning( + ctx, + platform_coin.clone(), + protocol_conf, + coin_conf, + validated_params, + task_handle, + ) + .await?; + Timer::sleep(10.).await; + + let address = lightning_coin.my_address()?; let balance = lightning_coin .my_balance() .compat() .await .mm_err(LightningInitError::MyBalanceError)?; - let init_result = LightningInitResult { + let init_result = LightningActivationResult { platform_coin: platform_coin.ticker().into(), address, balance, @@ -206,3 +322,172 @@ impl L2ActivationOps for LightningCoin { Ok((lightning_coin, init_result)) } } + +async fn start_lightning( + ctx: &MmArc, + platform_coin: UtxoStandardCoin, + protocol_conf: LightningProtocolConf, + conf: LightningCoinConf, + params: LightningValidatedParams, + task_handle: &LightningRpcTaskHandle, +) -> EnableLightningResult { + // Todo: add support for Hardware wallets for funding transactions and spending spendable outputs (channel closing transactions) + if let coins::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.confirmation_targets, + )?); + task_handle.update_in_progress_status(LightningInProgressStatus::GettingFeesFromRPC)?; + platform.set_latest_fees().await?; + + // Initialize the Logger + let logger = ctx.log.0.clone(); + + // Initialize Persister + let persister = init_persister(ctx, conf.ticker.clone(), params.backup_path).await?; + + // Initialize the KeysManager + let keys_manager = init_keys_manager(&platform)?; + + // Initialize the P2PGossipSync. This is used for providing routes to send payments over + task_handle.update_in_progress_status(LightningInProgressStatus::ReadingNetworkGraphFromFile)?; + let network_graph = Arc::new( + persister + .get_network_graph(protocol_conf.network.into(), logger.clone()) + .await?, + ); + + let gossip_sync = Arc::new(gossip::P2PGossipSync::new( + network_graph.clone(), + None::>, + logger.clone(), + )); + + // Initialize DB + let db = init_db(ctx, conf.ticker.clone()).await?; + + // Initialize the ChannelManager + task_handle.update_in_progress_status(LightningInProgressStatus::InitializingChannelManager)?; + let (chain_monitor, channel_manager) = init_channel_manager( + platform.clone(), + logger.clone(), + persister.clone(), + db.clone(), + keys_manager.clone(), + conf.clone().into(), + ) + .await?; + + // Initialize the PeerManager + task_handle.update_in_progress_status(LightningInProgressStatus::InitializingPeerManager)?; + let peer_manager = init_peer_manager( + ctx.clone(), + &platform, + params.listening_port, + channel_manager.clone(), + gossip_sync.clone(), + keys_manager + .get_node_secret(Recipient::Node) + .map_to_mm(|_| EnableLightningError::UnsupportedMode("'start_lightning'".into(), "local node".into()))?, + logger.clone(), + ) + .await?; + + let trusted_nodes = Arc::new(PaMutex::new(persister.get_trusted_nodes().await?)); + + init_abortable_events(platform.clone(), db.clone()).await?; + + // Initialize the event handler + let event_handler = Arc::new(LightningEventHandler::new( + platform.clone(), + channel_manager.clone(), + keys_manager.clone(), + db.clone(), + trusted_nodes.clone(), + )); + + // Initialize routing Scorer + task_handle.update_in_progress_status(LightningInProgressStatus::ReadingScorerFromFile)?; + // status_notifier + // .try_send(LightningInProgressStatus::ReadingScorerFromFile) + // .debug_log_with_msg("No one seems interested in LightningInProgressStatus"); + let scorer = Arc::new(persister.get_scorer(network_graph.clone(), logger.clone()).await?); + + // Create InvoicePayer + // random_seed_bytes are additional random seed to improve privacy by adding a random CLTV expiry offset to each path's final hop. + // This helps obscure the intended recipient from adversarial intermediate hops. The seed is also used to randomize candidate paths during route selection. + // TODO: random_seed_bytes should be taken in consideration when implementing swaps because they change the payment lock-time. + // https://github.com/lightningdevkit/rust-lightning/issues/158 + // https://github.com/lightningdevkit/rust-lightning/pull/1286 + // https://github.com/lightningdevkit/rust-lightning/pull/1359 + let router_random_seed_bytes = keys_manager.get_secure_random_bytes(); + let router = DefaultRouter::new(network_graph.clone(), logger.clone(), router_random_seed_bytes); + let invoice_payer = Arc::new(InvoicePayer::new( + channel_manager.clone(), + router, + scorer.clone(), + logger.clone(), + event_handler, + // Todo: Add option for choosing payment::Retry::Timeout instead of Attempts in LightningParams + payment::Retry::Attempts(params.payment_retries.unwrap_or(PAYMENT_RETRY_ATTEMPTS)), + )); + + // 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. + // note: background_processor stops automatically when dropped since BackgroundProcessor implements the Drop trait. + task_handle.update_in_progress_status(LightningInProgressStatus::InitializingBackgroundProcessor)?; + let background_processor = Arc::new(BackgroundProcessor::start( + persister.clone(), + invoice_payer.clone(), + chain_monitor.clone(), + channel_manager.clone(), + GossipSync::p2p(gossip_sync), + peer_manager.clone(), + logger.clone(), + Some(scorer.clone()), + )); + + // If channel_nodes_data file exists, read channels nodes data from disk and reconnect to channel nodes/peers if possible. + task_handle.update_in_progress_status(LightningInProgressStatus::ReadingChannelsAddressesFromFile)?; + let open_channels_nodes = Arc::new(PaMutex::new( + get_open_channels_nodes_addresses(persister.clone(), channel_manager.clone()).await?, + )); + + platform.spawner().spawn(connect_to_ln_nodes_loop( + open_channels_nodes.clone(), + peer_manager.clone(), + )); + + // Broadcast Node Announcement + platform.spawner().spawn(ln_node_announcement_loop( + channel_manager.clone(), + params.node_name, + params.node_color, + params.listening_port, + )); + + Ok(LightningCoin { + platform, + conf, + background_processor, + peer_manager, + channel_manager, + chain_monitor, + keys_manager, + invoice_payer, + persister, + db, + open_channels_nodes, + trusted_nodes, + router: Arc::new(DefaultRouter::new(network_graph, logger, router_random_seed_bytes)), + scorer, + }) +} diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index c27a440008..e6e3f1b9f3 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -2,18 +2,16 @@ use crate::prelude::*; use async_trait::async_trait; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tx_history_storage::{CreateTxHistoryStorageError, TxHistoryStorageBuilder}; -use coins::{lp_coinfind, CoinProtocol, CoinsContext, MmCoinEnum}; -use common::mm_metrics::MetricsArc; +use coins::{lp_coinfind, CoinProtocol, CoinsContext, MmCoinEnum, PrivKeyPolicyNotAllowed}; use common::{log, HttpStatusCode, StatusCode}; +use crypto::CryptoCtxError; use derive_more::Display; -use futures::future::AbortHandle; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; use serde_json::Value as Json; -use std::convert::Infallible; #[derive(Clone, Debug, Deserialize)] pub struct TokenActivationRequest { @@ -23,7 +21,7 @@ pub struct TokenActivationRequest { } pub trait TokenOf: Into { - type PlatformCoin: PlatformWithTokensActivationOps + RegisterTokenInfo; + type PlatformCoin: TryPlatformCoinFromMmCoinEnum + PlatformWithTokensActivationOps + RegisterTokenInfo + Clone; } pub struct TokenActivationParams { @@ -65,7 +63,8 @@ pub trait TokenAsMmCoinInitializer: Send + Sync { pub enum InitTokensAsMmCoinsError { TokenConfigIsNotFound(String), - InvalidPubkey(String), + CouldNotFetchBalance(String), + Internal(String), TokenProtocolParseError { ticker: String, error: String }, UnexpectedTokenProtocol { ticker: String, protocol: CoinProtocol }, } @@ -87,14 +86,10 @@ impl From for InitTokensAsMmCoinsError { } } -pub trait RegisterTokenInfo> { +pub trait RegisterTokenInfo { fn register_token_info(&self, token: &T); } -impl From for InitTokensAsMmCoinsError { - fn from(e: Infallible) -> Self { match e {} } -} - #[async_trait] impl TokenAsMmCoinInitializer for T where @@ -149,7 +144,6 @@ pub trait PlatformWithTokensActivationOps: Into { coin_conf: Json, activation_request: Self::ActivationRequest, protocol_conf: Self::PlatformProtocolInfo, - priv_key: &[u8], ) -> Result>; fn token_initializers( @@ -160,10 +154,10 @@ pub trait PlatformWithTokensActivationOps: Into { fn start_history_background_fetching( &self, - metrics: MetricsArc, + ctx: MmArc, storage: impl TxHistoryStorage, initial_balance: BigDecimal, - ) -> AbortHandle; + ); } #[derive(Debug, Deserialize)] @@ -207,10 +201,12 @@ pub enum EnablePlatformCoinWithTokensError { error: String, }, #[display(fmt = "Private key is not allowed: {}", _0)] - PrivKeyNotAllowed(String), + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), #[display(fmt = "Unexpected derivation method: {}", _0)] UnexpectedDerivationMethod(String), Transport(String), + AtLeastOneNodeRequired(String), + InvalidPayload(String), Internal(String), } @@ -245,7 +241,8 @@ impl From for EnablePlatformCoinWithTokensError { InitTokensAsMmCoinsError::UnexpectedTokenProtocol { ticker, protocol } => { EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { ticker, protocol } }, - InitTokensAsMmCoinsError::InvalidPubkey(e) => EnablePlatformCoinWithTokensError::Internal(e), + InitTokensAsMmCoinsError::Internal(e) => EnablePlatformCoinWithTokensError::Internal(e), + InitTokensAsMmCoinsError::CouldNotFetchBalance(e) => EnablePlatformCoinWithTokensError::Transport(e), } } } @@ -258,13 +255,17 @@ impl From for EnablePlatformCoinWithTokensError { } } +impl From for EnablePlatformCoinWithTokensError { + fn from(e: CryptoCtxError) -> Self { EnablePlatformCoinWithTokensError::Internal(e.to_string()) } +} + impl HttpStatusCode for EnablePlatformCoinWithTokensError { fn status_code(&self) -> StatusCode { match self { EnablePlatformCoinWithTokensError::CoinProtocolParseError { .. } | EnablePlatformCoinWithTokensError::TokenProtocolParseError { .. } | EnablePlatformCoinWithTokensError::PlatformCoinCreationError { .. } - | EnablePlatformCoinWithTokensError::PrivKeyNotAllowed(_) + | EnablePlatformCoinWithTokensError::PrivKeyPolicyNotAllowed(_) | EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(_) | EnablePlatformCoinWithTokensError::Transport(_) | EnablePlatformCoinWithTokensError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, @@ -272,6 +273,8 @@ impl HttpStatusCode for EnablePlatformCoinWithTokensError { | EnablePlatformCoinWithTokensError::PlatformConfigIsNotFound(_) | EnablePlatformCoinWithTokensError::TokenConfigIsNotFound(_) | EnablePlatformCoinWithTokensError::UnexpectedPlatformProtocol { .. } + | EnablePlatformCoinWithTokensError::InvalidPayload { .. } + | EnablePlatformCoinWithTokensError::AtLeastOneNodeRequired(_) | EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { .. } => StatusCode::BAD_REQUEST, } } @@ -294,15 +297,12 @@ where let (platform_conf, platform_protocol) = coin_conf_with_protocol(&ctx, &req.ticker)?; - let priv_key = &*ctx.secp256k1_key_pair().private().secret; - let platform_coin = Platform::enable_platform_coin( ctx.clone(), req.ticker.clone(), platform_conf, req.request.clone(), platform_protocol, - priv_key, ) .await?; let mut mm_tokens = Vec::new(); @@ -315,12 +315,11 @@ where log::info!("{} current block {}", req.ticker, activation_result.current_block()); if req.request.tx_history() { - let abort_handler = platform_coin.start_history_background_fetching( - ctx.metrics.clone(), + platform_coin.start_history_background_fetching( + ctx.clone(), TxHistoryStorageBuilder::new(&ctx).build()?, activation_result.get_platform_balance(), ); - ctx.abort_handlers.lock().unwrap().push(abort_handler); } let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); diff --git a/mm2src/coins_activation/src/prelude.rs b/mm2src/coins_activation/src/prelude.rs index 63014af89e..55e1d03aba 100644 --- a/mm2src/coins_activation/src/prelude.rs +++ b/mm2src/coins_activation/src/prelude.rs @@ -4,6 +4,7 @@ use coins::z_coin::ZcoinActivationParams; use coins::{coin_conf, CoinBalance, CoinProtocol, MmCoinEnum}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_number::BigDecimal; use serde_derive::Serialize; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; @@ -25,6 +26,10 @@ impl TxHistory for ZcoinActivationParams { fn tx_history(&self) -> bool { false } } +pub trait GetAddressesBalances { + fn get_addresses_balances(&self) -> HashMap; +} + #[derive(Clone, Debug, Serialize)] #[serde(tag = "type", content = "data")] pub enum DerivationMethod { @@ -51,6 +56,7 @@ pub trait TryPlatformCoinFromMmCoinEnum { } pub trait TryFromCoinProtocol { + #[allow(clippy::result_large_err)] fn try_from_coin_protocol(proto: CoinProtocol) -> Result> where Self: Sized; @@ -63,6 +69,7 @@ pub enum CoinConfWithProtocolError { UnexpectedProtocol { ticker: String, protocol: CoinProtocol }, } +#[allow(clippy::result_large_err)] pub fn coin_conf_with_protocol( ctx: &MmArc, coin: &str, diff --git a/mm2src/coins_activation/src/slp_token_activation.rs b/mm2src/coins_activation/src/slp_token_activation.rs index 55c08c8ac0..85f017b0cc 100644 --- a/mm2src/coins_activation/src/slp_token_activation.rs +++ b/mm2src/coins_activation/src/slp_token_activation.rs @@ -2,8 +2,7 @@ use crate::prelude::*; use crate::token::{EnableTokenError, TokenActivationOps, TokenProtocolParams}; use async_trait::async_trait; use coins::utxo::bch::BchCoin; -use coins::utxo::rpc_clients::UtxoRpcError; -use coins::utxo::slp::{SlpProtocolConf, SlpToken}; +use coins::utxo::slp::{EnableSlpError, SlpProtocolConf, SlpToken}; use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum}; use mm2_err_handle::prelude::*; use rpc::v1::types::H256 as H256Json; @@ -53,11 +52,13 @@ impl TokenProtocolParams for SlpProtocolConf { fn platform_coin_ticker(&self) -> &str { &self.platform_coin_ticker } } -impl From for EnableTokenError { - fn from(err: SlpInitError) -> Self { +impl From for EnableTokenError { + fn from(err: EnableSlpError) -> Self { match err { - SlpInitError::GetBalanceError(rpc_err) => rpc_err.into(), - SlpInitError::MyAddressError(e) => EnableTokenError::Internal(e), + EnableSlpError::GetBalanceError(rpc_err) => rpc_err.into(), + EnableSlpError::UnexpectedDerivationMethod(e) | EnableSlpError::Internal(e) => { + EnableTokenError::Internal(e) + }, } } } @@ -70,20 +71,12 @@ pub struct SlpInitResult { required_confirmations: u64, } -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum SlpInitError { - GetBalanceError(UtxoRpcError), - MyAddressError(String), -} - #[async_trait] impl TokenActivationOps for SlpToken { - type PlatformCoin = BchCoin; type ActivationParams = SlpActivationRequest; type ProtocolInfo = SlpProtocolConf; type ActivationResult = SlpInitResult; - type ActivationError = SlpInitError; + type ActivationError = EnableSlpError; async fn enable_token( ticker: String, @@ -104,9 +97,9 @@ impl TokenActivationOps for SlpToken { protocol_conf.token_id, platform_coin, required_confirmations, - ); - let balance = token.my_coin_balance().await.mm_err(SlpInitError::GetBalanceError)?; - let my_address = token.my_address().map_to_mm(SlpInitError::MyAddressError)?; + )?; + let balance = token.my_coin_balance().await.mm_err(EnableSlpError::GetBalanceError)?; + let my_address = token.my_address()?; let mut balances = HashMap::new(); balances.insert(my_address, balance); let init_result = SlpInitResult { diff --git a/mm2src/coins_activation/src/solana_with_tokens_activation.rs b/mm2src/coins_activation/src/solana_with_tokens_activation.rs index 71fdcf4027..9bdc014691 100644 --- a/mm2src/coins_activation/src/solana_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/solana_with_tokens_activation.rs @@ -6,13 +6,14 @@ use crate::prelude::*; use crate::prelude::{CoinAddressInfo, TokenBalances, TryFromCoinProtocol, TxHistory}; use crate::spl_token_activation::SplActivationRequest; use async_trait::async_trait; +use coins::coin_errors::MyAddressError; use coins::my_tx_history_v2::TxHistoryStorage; +use coins::solana::solana_coin_with_policy; use coins::solana::spl::{SplProtocolConf, SplTokenCreationError}; -use coins::{solana_coin_from_conf_and_params, BalanceError, CoinBalance, CoinProtocol, MarketCoinOps, - SolanaActivationParams, SolanaCoin, SplToken}; -use common::mm_metrics::MetricsArc; +use coins::{BalanceError, CoinBalance, CoinProtocol, MarketCoinOps, PrivKeyBuildPolicy, SolanaActivationParams, + SolanaCoin, SplToken}; use common::Future01CompatExt; -use futures::future::AbortHandle; +use crypto::CryptoCtxError; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; @@ -28,12 +29,17 @@ impl TokenOf for SplToken { type PlatformCoin = SolanaCoin; } +pub struct SplTokenInitializerErr { + ticker: String, + inner: SplTokenCreationError, +} + #[async_trait] impl TokenInitializer for SplTokenInitializer { type Token = SplToken; type TokenActivationRequest = SplActivationRequest; type TokenProtocol = SplProtocolConf; - type InitTokensError = SplTokenCreationError; + type InitTokensError = SplTokenInitializerErr; fn tokens_requests_from_platform_request( platform_params: &SolanaWithTokensActivationRequest, @@ -48,12 +54,14 @@ impl TokenInitializer for SplTokenInitializer { let tokens = activation_params .into_iter() .map(|param| { + let ticker = param.ticker.clone(); SplToken::new( param.protocol.decimals, param.ticker, param.protocol.token_contract_address, self.platform_coin.clone(), ) + .mm_err(|inner| SplTokenInitializerErr { ticker, inner }) }) .collect::, _>>()?; Ok(tokens) @@ -107,6 +115,10 @@ pub enum SolanaWithTokensActivationError { Internal(String), } +impl From for SolanaWithTokensActivationError { + fn from(err: MyAddressError) -> Self { Self::UnableToRetrieveMyAddress(err.to_string()) } +} + impl From for EnablePlatformCoinWithTokensError { fn from(e: SolanaWithTokensActivationError) -> Self { match e { @@ -129,6 +141,10 @@ impl From for SolanaWithTokensActivationError { fn from(e: BalanceError) -> Self { SolanaWithTokensActivationError::GetBalanceError(e) } } +impl From for SolanaWithTokensActivationError { + fn from(e: CryptoCtxError) -> Self { SolanaWithTokensActivationError::Internal(e.to_string()) } +} + pub struct SolanaProtocolInfo {} impl TryFromCoinProtocol for SolanaProtocolInfo { @@ -143,10 +159,14 @@ impl TryFromCoinProtocol for SolanaProtocolInfo { } } -impl From for InitTokensAsMmCoinsError { - fn from(error: SplTokenCreationError) -> Self { - match error { - SplTokenCreationError::InvalidPubkey(e) => InitTokensAsMmCoinsError::InvalidPubkey(e), +impl From for InitTokensAsMmCoinsError { + fn from(err: SplTokenInitializerErr) -> Self { + match err.inner { + SplTokenCreationError::InvalidPubkey(error) => InitTokensAsMmCoinsError::TokenProtocolParseError { + ticker: err.ticker, + error, + }, + SplTokenCreationError::Internal(internal) => InitTokensAsMmCoinsError::Internal(internal), } } } @@ -159,18 +179,22 @@ impl PlatformWithTokensActivationOps for SolanaCoin { type ActivationError = SolanaWithTokensActivationError; async fn enable_platform_coin( - _ctx: MmArc, + ctx: MmArc, ticker: String, platform_conf: Json, activation_request: Self::ActivationRequest, _protocol_conf: Self::PlatformProtocolInfo, - priv_key: &[u8], ) -> Result> { - let platform_coin = - solana_coin_from_conf_and_params(&ticker, &platform_conf, activation_request.platform_request, priv_key) - .await - .map_to_mm(|error| SolanaWithTokensActivationError::PlatformCoinCreationError { ticker, error })?; - Ok(platform_coin) + let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(&ctx)?; + solana_coin_with_policy( + &ctx, + &ticker, + &platform_conf, + activation_request.platform_request, + priv_key_policy, + ) + .await + .map_to_mm(|error| SolanaWithTokensActivationError::PlatformCoinCreationError { ticker, error }) } fn token_initializers( @@ -182,9 +206,7 @@ impl PlatformWithTokensActivationOps for SolanaCoin { } async fn get_activation_result(&self) -> Result> { - let my_address = self - .my_address() - .map_to_mm(Self::ActivationError::UnableToRetrieveMyAddress)?; + let my_address = self.my_address()?; let current_block = self .current_block() .compat() @@ -224,10 +246,9 @@ impl PlatformWithTokensActivationOps for SolanaCoin { fn start_history_background_fetching( &self, - _metrics: MetricsArc, + _ctx: MmArc, _storage: impl TxHistoryStorage + Send + 'static, _initial_balance: BigDecimal, - ) -> AbortHandle { - todo!() + ) { } } diff --git a/mm2src/coins_activation/src/spl_token_activation.rs b/mm2src/coins_activation/src/spl_token_activation.rs index 4f690116b0..69782e11ac 100644 --- a/mm2src/coins_activation/src/spl_token_activation.rs +++ b/mm2src/coins_activation/src/spl_token_activation.rs @@ -1,6 +1,7 @@ use crate::prelude::{TryFromCoinProtocol, TryPlatformCoinFromMmCoinEnum}; use crate::token::{EnableTokenError, TokenActivationOps, TokenProtocolParams}; use async_trait::async_trait; +use coins::coin_errors::MyAddressError; use coins::solana::spl::{SplProtocolConf, SplTokenCreationError}; use coins::{BalanceError, CoinBalance, CoinProtocol, MarketCoinOps, MmCoinEnum, SolanaCoin, SplToken}; use common::Future01CompatExt; @@ -62,6 +63,10 @@ pub enum SplInitError { MyAddressError(String), } +impl From for SplInitError { + fn from(err: MyAddressError) -> Self { Self::MyAddressError(err.to_string()) } +} + impl From for SplInitError { fn from(e: SplTokenCreationError) -> Self { SplInitError::TokenCreationFailed(e) } } @@ -78,7 +83,6 @@ impl From for EnableTokenError { #[async_trait] impl TokenActivationOps for SplToken { - type PlatformCoin = SolanaCoin; type ActivationParams = SplActivationRequest; type ProtocolInfo = SplProtocolConf; type ActivationResult = SplInitResult; @@ -101,7 +105,7 @@ impl TokenActivationOps for SplToken { .compat() .await .map_err(|e| SplInitError::GetBalanceError(e.into_inner()))?; - let my_address = token.my_address().map_to_mm(SplInitError::MyAddressError)?; + let my_address = token.my_address()?; let mut balances = HashMap::new(); balances.insert(my_address, balance); let init_result = SplInitResult { diff --git a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs index e3d0b57d63..46b9638a4c 100644 --- a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs +++ b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs @@ -1,17 +1,23 @@ use crate::context::CoinsActivationContext; use crate::prelude::*; -use crate::standalone_coin::init_standalone_coin_error::{InitStandaloneCoinError, InitStandaloneCoinStatusError, +use crate::standalone_coin::init_standalone_coin_error::{CancelInitStandaloneCoinError, InitStandaloneCoinError, + InitStandaloneCoinStatusError, InitStandaloneCoinUserActionError}; use async_trait::async_trait; -use coins::{lp_coinfind, lp_register_coin, MmCoinEnum, RegisterCoinError, RegisterCoinParams}; +use coins::my_tx_history_v2::TxHistoryStorage; +use coins::tx_history_storage::{CreateTxHistoryStorageError, TxHistoryStorageBuilder}; +use coins::{lp_coinfind, lp_register_coin, CoinsContext, MmCoinEnum, RegisterCoinError, RegisterCoinParams}; use common::{log, SuccessResponse}; use crypto::trezor::trezor_rpc_task::RpcTaskHandle; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use rpc_task::rpc_common::{InitRpcTaskResponse, RpcTaskStatusRequest, RpcTaskUserActionRequest}; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; +use rpc_task::rpc_common::{CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusRequest, RpcTaskUserActionRequest}; use rpc_task::{RpcTask, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; use serde_derive::Deserialize; use serde_json::Value as Json; +use std::collections::HashMap; pub type InitStandaloneCoinResponse = InitRpcTaskResponse; pub type InitStandaloneCoinStatusRequest = RpcTaskStatusRequest; @@ -27,11 +33,12 @@ pub struct InitStandaloneCoinReq { #[async_trait] pub trait InitStandaloneCoinActivationOps: Into + Send + Sync + 'static { - type ActivationRequest: TxHistory + Sync + Send; - type StandaloneProtocol: TryFromCoinProtocol + Send; + type ActivationRequest: TxHistory + Clone + Send + Sync; + type StandaloneProtocol: TryFromCoinProtocol + Clone + Send + Sync; // The following types are related to `RpcTask` management. - type ActivationResult: serde::Serialize + Clone + CurrentBlock + Send + Sync + 'static; + type ActivationResult: serde::Serialize + Clone + CurrentBlock + GetAddressesBalances + Send + Sync + 'static; type ActivationError: From + + From + Into + SerMmErrorType + NotEqual @@ -61,6 +68,13 @@ pub trait InitStandaloneCoinActivationOps: Into + Send + Sync + 'sta task_handle: &InitStandaloneCoinTaskHandle, activation_request: &Self::ActivationRequest, ) -> Result>; + + fn start_history_background_fetching( + &self, + metrics: MetricsArc, + storage: impl TxHistoryStorage, + current_balances: HashMap, + ); } pub async fn init_standalone_coin( @@ -80,6 +94,7 @@ where let (coin_conf, protocol_info) = coin_conf_with_protocol(&ctx, &request.ticker)?; let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(InitStandaloneCoinError::Internal)?; + let spawner = ctx.spawner(); let task = InitStandaloneCoinTask:: { ctx, request, @@ -88,7 +103,7 @@ where }; let task_manager = Standalone::rpc_task_manager(&coins_act_ctx); - let task_id = RpcTaskManager::spawn_rpc_task(task_manager, task) + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) .mm_err(|e| InitStandaloneCoinError::Internal(e.to_string()))?; Ok(InitStandaloneCoinResponse { task_id }) @@ -132,6 +147,18 @@ pub async fn init_standalone_coin_user_action( + ctx: MmArc, + req: CancelRpcTaskRequest, +) -> MmResult { + let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(CancelInitStandaloneCoinError::Internal)?; + let mut task_manager = Standalone::rpc_task_manager(&coins_act_ctx) + .lock() + .map_to_mm(|poison| CancelInitStandaloneCoinError::Internal(poison.to_string()))?; + task_manager.cancel_task(req.task_id)?; + Ok(SuccessResponse::new()) +} + pub struct InitStandaloneCoinTask { ctx: MmArc, request: InitStandaloneCoinReq, @@ -156,14 +183,23 @@ where ::initial_status() } - async fn run(self, task_handle: &RpcTaskHandle) -> Result> { + /// Try to disable the coin in case if we managed to register it already. + async fn cancel(self) { + if let Ok(c_ctx) = CoinsContext::from_ctx(&self.ctx) { + if let Ok(Some(coin)) = lp_coinfind(&self.ctx, &self.request.ticker).await { + c_ctx.remove_coin(coin).await; + }; + }; + } + + async fn run(&mut self, task_handle: &RpcTaskHandle) -> Result> { let ticker = self.request.ticker.clone(); let coin = Standalone::init_standalone_coin( self.ctx.clone(), ticker.clone(), - self.coin_conf, + self.coin_conf.clone(), &self.request.activation_params, - self.protocol_info, + self.protocol_info.clone(), task_handle, ) .await?; @@ -174,8 +210,16 @@ where log::info!("{} current block {}", ticker, result.current_block()); let tx_history = self.request.activation_params.tx_history(); - - lp_register_coin(&self.ctx, coin.into(), RegisterCoinParams { ticker, tx_history }).await?; + if tx_history { + let current_balances = result.get_addresses_balances(); + coin.start_history_background_fetching( + self.ctx.metrics.clone(), + TxHistoryStorageBuilder::new(&self.ctx).build()?, + current_balances, + ); + } + + lp_register_coin(&self.ctx, coin.into(), RegisterCoinParams { ticker }).await?; Ok(result) } diff --git a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs index 0224603cb7..1f0b5db764 100644 --- a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs +++ b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs @@ -1,8 +1,9 @@ use crate::prelude::CoinConfWithProtocolError; use coins::CoinProtocol; use common::{HttpStatusCode, StatusCode}; +use crypto::HwRpcError; use derive_more::Display; -use rpc_task::rpc_common::{RpcTaskStatusError, RpcTaskUserActionError}; +use rpc_task::rpc_common::{CancelRpcTaskError, RpcTaskStatusError, RpcTaskUserActionError}; use rpc_task::{RpcTaskError, TaskId}; use ser_error_derive::SerializeErrorType; use serde_derive::Serialize; @@ -10,40 +11,30 @@ use std::time::Duration; pub type InitStandaloneCoinStatusError = RpcTaskStatusError; pub type InitStandaloneCoinUserActionError = RpcTaskUserActionError; +pub type CancelInitStandaloneCoinError = CancelRpcTaskError; #[derive(Clone, Debug, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum InitStandaloneCoinError { + #[display(fmt = "No such task '{}'", _0)] NoSuchTask(TaskId), #[display(fmt = "Initialization task has timed out {:?}", duration)] - TaskTimedOut { - duration: Duration, - }, - CoinIsAlreadyActivated { - ticker: String, - }, + TaskTimedOut { duration: Duration }, + #[display(fmt = "Coin {} is activated already", ticker)] + CoinIsAlreadyActivated { ticker: String }, #[display(fmt = "Coin {} config is not found", _0)] CoinConfigIsNotFound(String), #[display(fmt = "Coin {} protocol parsing failed: {}", ticker, error)] - CoinProtocolParseError { - ticker: String, - error: String, - }, + CoinProtocolParseError { ticker: String, error: String }, #[display(fmt = "Unexpected platform protocol {:?} for {}", protocol, ticker)] - UnexpectedCoinProtocol { - ticker: String, - protocol: CoinProtocol, - }, + UnexpectedCoinProtocol { ticker: String, protocol: CoinProtocol }, #[display(fmt = "Error on platform coin {} creation: {}", ticker, error)] - CoinCreationError { - ticker: String, - error: String, - }, - #[display(fmt = "Private key is not allowed: {}", _0)] - PrivKeyNotAllowed(String), - #[display(fmt = "Unexpected derivation method: {}", _0)] - UnexpectedDerivationMethod(String), + CoinCreationError { ticker: String, error: String }, + #[display(fmt = "{}", _0)] + HwError(HwRpcError), + #[display(fmt = "Transport error: {}", _0)] Transport(String), + #[display(fmt = "Internal error: {}", _0)] Internal(String), } @@ -82,10 +73,9 @@ impl HttpStatusCode for InitStandaloneCoinError { | InitStandaloneCoinError::CoinConfigIsNotFound { .. } | InitStandaloneCoinError::CoinProtocolParseError { .. } | InitStandaloneCoinError::UnexpectedCoinProtocol { .. } - | InitStandaloneCoinError::CoinCreationError { .. } - | InitStandaloneCoinError::PrivKeyNotAllowed(_) - | InitStandaloneCoinError::UnexpectedDerivationMethod(_) => StatusCode::BAD_REQUEST, + | InitStandaloneCoinError::CoinCreationError { .. } => StatusCode::BAD_REQUEST, InitStandaloneCoinError::TaskTimedOut { .. } => StatusCode::REQUEST_TIMEOUT, + InitStandaloneCoinError::HwError(_) => StatusCode::GONE, InitStandaloneCoinError::Transport(_) | InitStandaloneCoinError::Internal(_) => { StatusCode::INTERNAL_SERVER_ERROR }, diff --git a/mm2src/coins_activation/src/standalone_coin/mod.rs b/mm2src/coins_activation/src/standalone_coin/mod.rs index 44f71c4ac7..6cd26b35bb 100644 --- a/mm2src/coins_activation/src/standalone_coin/mod.rs +++ b/mm2src/coins_activation/src/standalone_coin/mod.rs @@ -1,8 +1,8 @@ mod init_standalone_coin; mod init_standalone_coin_error; -pub use init_standalone_coin::{init_standalone_coin, init_standalone_coin_status, init_standalone_coin_user_action, - InitStandaloneCoinActivationOps, InitStandaloneCoinInitialStatus, - InitStandaloneCoinTask, InitStandaloneCoinTaskHandle, +pub use init_standalone_coin::{cancel_init_standalone_coin, init_standalone_coin, init_standalone_coin_status, + init_standalone_coin_user_action, InitStandaloneCoinActivationOps, + InitStandaloneCoinInitialStatus, InitStandaloneCoinTask, InitStandaloneCoinTaskHandle, InitStandaloneCoinTaskManagerShared}; pub use init_standalone_coin_error::InitStandaloneCoinError; diff --git a/mm2src/coins_activation/src/tendermint_token_activation.rs b/mm2src/coins_activation/src/tendermint_token_activation.rs new file mode 100644 index 0000000000..58cc6b90c1 --- /dev/null +++ b/mm2src/coins_activation/src/tendermint_token_activation.rs @@ -0,0 +1,78 @@ +use crate::{prelude::TryPlatformCoinFromMmCoinEnum, + token::{EnableTokenError, TokenActivationOps, TokenProtocolParams}}; +use async_trait::async_trait; +use coins::{tendermint::{TendermintCoin, TendermintToken, TendermintTokenActivationParams, TendermintTokenInitError, + TendermintTokenProtocolInfo}, + CoinBalance, MarketCoinOps, MmCoinEnum}; +use common::Future01CompatExt; +use mm2_err_handle::prelude::{MapMmError, MmError}; +use serde::Serialize; +use std::collections::HashMap; + +impl From for EnableTokenError { + fn from(err: TendermintTokenInitError) -> Self { + match err { + TendermintTokenInitError::InvalidDenom(e) => EnableTokenError::InvalidConfig(e), + TendermintTokenInitError::MyAddressError(e) | TendermintTokenInitError::Internal(e) => { + EnableTokenError::Internal(e) + }, + TendermintTokenInitError::CouldNotFetchBalance(e) => EnableTokenError::CouldNotFetchBalance(e), + } + } +} + +#[derive(Debug, Serialize)] +pub struct TendermintTokenInitResult { + balances: HashMap, + platform_coin: String, +} + +impl TryPlatformCoinFromMmCoinEnum for TendermintCoin { + fn try_from_mm_coin(coin: MmCoinEnum) -> Option + where + Self: Sized, + { + match coin { + MmCoinEnum::Tendermint(coin) => Some(coin), + _ => None, + } + } +} + +impl TokenProtocolParams for TendermintTokenProtocolInfo { + fn platform_coin_ticker(&self) -> &str { &self.platform } +} + +#[async_trait] +impl TokenActivationOps for TendermintToken { + type ActivationParams = TendermintTokenActivationParams; + type ProtocolInfo = TendermintTokenProtocolInfo; + type ActivationResult = TendermintTokenInitResult; + type ActivationError = TendermintTokenInitError; + + async fn enable_token( + ticker: String, + platform_coin: Self::PlatformCoin, + _activation_params: Self::ActivationParams, + protocol_conf: Self::ProtocolInfo, + ) -> Result<(Self, Self::ActivationResult), MmError> { + let token = TendermintToken::new(ticker, platform_coin, protocol_conf.decimals, protocol_conf.denom)?; + + let balance = token + .my_balance() + .compat() + .await + .mm_err(|e| TendermintTokenInitError::CouldNotFetchBalance(e.to_string()))?; + + let my_address = token.my_address()?; + let mut balances = HashMap::new(); + balances.insert(my_address, balance); + + let init_result = TendermintTokenInitResult { + balances, + platform_coin: token.platform_ticker().into(), + }; + + Ok((token, init_result)) + } +} diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs new file mode 100644 index 0000000000..3c477cfdd3 --- /dev/null +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -0,0 +1,233 @@ +use crate::platform_coin_with_tokens::{EnablePlatformCoinWithTokensError, GetPlatformBalance, + InitTokensAsMmCoinsError, PlatformWithTokensActivationOps, RegisterTokenInfo, + TokenActivationParams, TokenActivationRequest, TokenAsMmCoinInitializer, + TokenInitializer, TokenOf}; +use crate::prelude::*; +use async_trait::async_trait; +use coins::my_tx_history_v2::TxHistoryStorage; +use coins::tendermint::tendermint_tx_history_v2::tendermint_history_loop; +use coins::tendermint::{TendermintCoin, TendermintCommons, TendermintConf, TendermintInitError, + TendermintInitErrorKind, TendermintProtocolInfo, TendermintToken, + TendermintTokenActivationParams, TendermintTokenInitError, TendermintTokenProtocolInfo}; +use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, PrivKeyBuildPolicy}; +use common::executor::{AbortSettings, SpawnAbortable}; +use common::Future01CompatExt; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::BigDecimal; +use serde::{Deserialize, Serialize}; +use serde_json::Value as Json; +use std::collections::HashMap; + +impl TokenOf for TendermintToken { + type PlatformCoin = TendermintCoin; +} + +impl RegisterTokenInfo for TendermintCoin { + fn register_token_info(&self, token: &TendermintToken) { + self.add_activated_token_info(token.ticker.clone(), token.decimals, token.denom.clone()) + } +} + +#[derive(Clone, Deserialize)] +pub struct TendermintActivationParams { + rpc_urls: Vec, + pub tokens_params: Vec>, + #[serde(default)] + tx_history: bool, +} + +impl TxHistory for TendermintActivationParams { + fn tx_history(&self) -> bool { self.tx_history } +} + +struct TendermintTokenInitializer { + platform_coin: TendermintCoin, +} + +struct TendermintTokenInitializerErr { + ticker: String, + inner: TendermintTokenInitError, +} + +#[async_trait] +impl TokenInitializer for TendermintTokenInitializer { + type Token = TendermintToken; + type TokenActivationRequest = TendermintTokenActivationParams; + type TokenProtocol = TendermintTokenProtocolInfo; + type InitTokensError = TendermintTokenInitializerErr; + + fn tokens_requests_from_platform_request( + platform_request: &TendermintActivationParams, + ) -> Vec> { + platform_request.tokens_params.clone() + } + + async fn enable_tokens( + &self, + params: Vec>, + ) -> Result, MmError> { + params + .into_iter() + .map(|param| { + let ticker = param.ticker.clone(); + TendermintToken::new( + param.ticker, + self.platform_coin.clone(), + param.protocol.decimals, + param.protocol.denom, + ) + .mm_err(|inner| TendermintTokenInitializerErr { ticker, inner }) + }) + .collect() + } + + fn platform_coin(&self) -> &::PlatformCoin { &self.platform_coin } +} + +impl TryFromCoinProtocol for TendermintProtocolInfo { + fn try_from_coin_protocol(proto: CoinProtocol) -> Result> { + match proto { + CoinProtocol::TENDERMINT(proto) => Ok(proto), + other => MmError::err(other), + } + } +} + +impl TryFromCoinProtocol for TendermintTokenProtocolInfo { + fn try_from_coin_protocol(proto: CoinProtocol) -> Result> { + match proto { + CoinProtocol::TENDERMINTTOKEN(proto) => Ok(proto), + other => MmError::err(other), + } + } +} + +impl From for InitTokensAsMmCoinsError { + fn from(err: TendermintTokenInitializerErr) -> Self { + match err.inner { + TendermintTokenInitError::InvalidDenom(error) => InitTokensAsMmCoinsError::TokenProtocolParseError { + ticker: err.ticker, + error, + }, + TendermintTokenInitError::MyAddressError(error) | TendermintTokenInitError::Internal(error) => { + InitTokensAsMmCoinsError::Internal(error) + }, + TendermintTokenInitError::CouldNotFetchBalance(error) => { + InitTokensAsMmCoinsError::CouldNotFetchBalance(error) + }, + } + } +} + +#[derive(Serialize)] +pub struct TendermintActivationResult { + ticker: String, + address: String, + current_block: u64, + balance: CoinBalance, + tokens_balances: HashMap, +} + +impl CurrentBlock for TendermintActivationResult { + fn current_block(&self) -> u64 { self.current_block } +} + +impl GetPlatformBalance for TendermintActivationResult { + fn get_platform_balance(&self) -> BigDecimal { self.balance.spendable.clone() } +} + +impl From for EnablePlatformCoinWithTokensError { + fn from(err: TendermintInitError) -> Self { + EnablePlatformCoinWithTokensError::PlatformCoinCreationError { + ticker: err.ticker, + error: err.kind.to_string(), + } + } +} + +#[async_trait] +impl PlatformWithTokensActivationOps for TendermintCoin { + type ActivationRequest = TendermintActivationParams; + type PlatformProtocolInfo = TendermintProtocolInfo; + type ActivationResult = TendermintActivationResult; + type ActivationError = TendermintInitError; + + async fn enable_platform_coin( + ctx: MmArc, + ticker: String, + coin_conf: Json, + activation_request: Self::ActivationRequest, + protocol_conf: Self::PlatformProtocolInfo, + ) -> Result> { + let conf = TendermintConf::try_from_json(&ticker, &coin_conf)?; + + let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(&ctx).mm_err(|e| TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::Internal(e.to_string()), + })?; + + TendermintCoin::init( + &ctx, + ticker, + conf, + protocol_conf, + activation_request.rpc_urls, + activation_request.tx_history, + priv_key_policy, + ) + .await + } + + fn token_initializers( + &self, + ) -> Vec>> { + vec![Box::new(TendermintTokenInitializer { + platform_coin: self.clone(), + })] + } + + async fn get_activation_result(&self) -> Result> { + let current_block = self.current_block().compat().await.map_to_mm(|e| TendermintInitError { + ticker: self.ticker().to_owned(), + kind: TendermintInitErrorKind::RpcError(e), + })?; + + let balances = self.all_balances().await.mm_err(|e| TendermintInitError { + ticker: self.ticker().to_owned(), + kind: TendermintInitErrorKind::RpcError(e.to_string()), + })?; + + Ok(TendermintActivationResult { + address: self.account_id.to_string(), + current_block, + balance: CoinBalance { + spendable: balances.platform_balance, + unspendable: BigDecimal::default(), + }, + tokens_balances: balances + .tokens_balances + .into_iter() + .map(|(ticker, balance)| { + (ticker, CoinBalance { + spendable: balance, + unspendable: BigDecimal::default(), + }) + }) + .collect(), + ticker: self.ticker().to_owned(), + }) + } + + fn start_history_background_fetching( + &self, + ctx: MmArc, + storage: impl TxHistoryStorage, + initial_balance: BigDecimal, + ) { + let fut = tendermint_history_loop(self.clone(), storage, ctx, initial_balance); + + let settings = AbortSettings::info_on_abort(format!("tendermint_history_loop stopped for {}", self.ticker())); + self.spawner().spawn_with_settings(fut, settings); + } +} diff --git a/mm2src/coins_activation/src/token.rs b/mm2src/coins_activation/src/token.rs index 544c1e0b9b..70751b59cc 100644 --- a/mm2src/coins_activation/src/token.rs +++ b/mm2src/coins_activation/src/token.rs @@ -1,9 +1,10 @@ -/// Contains token activation traits and their implementations for various coins -/// +// Contains token activation traits and their implementations for various coins + +use crate::platform_coin_with_tokens::{self, RegisterTokenInfo}; use crate::prelude::*; use async_trait::async_trait; use coins::utxo::rpc_clients::UtxoRpcError; -use coins::{lp_coinfind, lp_coinfind_or_err, BalanceError, CoinProtocol, CoinsContext, MmCoinEnum, +use coins::{lp_coinfind, lp_coinfind_or_err, BalanceError, CoinProtocol, CoinsContext, MmCoinEnum, RegisterCoinError, UnexpectedDerivationMethod}; use common::{HttpStatusCode, StatusCode}; use derive_more::Display; @@ -17,8 +18,7 @@ pub trait TokenProtocolParams { } #[async_trait] -pub trait TokenActivationOps: Into { - type PlatformCoin: TryPlatformCoinFromMmCoinEnum; +pub trait TokenActivationOps: Into + platform_coin_with_tokens::TokenOf { type ActivationParams; type ProtocolInfo: TokenProtocolParams + TryFromCoinProtocol; type ActivationResult; @@ -58,10 +58,21 @@ pub enum EnableTokenError { }, #[display(fmt = "{}", _0)] UnexpectedDerivationMethod(UnexpectedDerivationMethod), + CouldNotFetchBalance(String), + InvalidConfig(String), Transport(String), Internal(String), } +impl From for EnableTokenError { + fn from(err: RegisterCoinError) -> Self { + match err { + RegisterCoinError::CoinIsInitializedAlready { coin } => Self::TokenIsAlreadyActivated(coin), + RegisterCoinError::Internal(err) => Self::Internal(err), + } + } +} + impl From for EnableTokenError { fn from(err: CoinConfWithProtocolError) -> Self { match err { @@ -100,7 +111,7 @@ pub async fn enable_token( req: EnableTokenRequest, ) -> Result> where - Token: TokenActivationOps, + Token: TokenActivationOps + Clone, EnableTokenError: From, (Token::ActivationError, EnableTokenError): NotEqual, { @@ -122,13 +133,12 @@ where })?; let (token, activation_result) = - Token::enable_token(req.ticker, platform_coin, req.activation_params, token_protocol).await?; + Token::enable_token(req.ticker, platform_coin.clone(), req.activation_params, token_protocol).await?; let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); - coins_ctx - .add_coin(token.into()) - .await - .mm_err(|e| EnableTokenError::TokenIsAlreadyActivated(e.ticker))?; + coins_ctx.add_token(token.clone().into()).await?; + + platform_coin.register_token_info(&token); Ok(activation_result) } @@ -156,6 +166,8 @@ impl HttpStatusCode for EnableTokenError { | EnableTokenError::UnsupportedPlatformCoin { .. } | EnableTokenError::UnexpectedDerivationMethod(_) | EnableTokenError::Transport(_) + | EnableTokenError::CouldNotFetchBalance(_) + | EnableTokenError::InvalidConfig(_) | EnableTokenError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, } } diff --git a/mm2src/coins_activation/src/utxo_activation/common_impl.rs b/mm2src/coins_activation/src/utxo_activation/common_impl.rs index 7e216104fd..f33dbd41c5 100644 --- a/mm2src/coins_activation/src/utxo_activation/common_impl.rs +++ b/mm2src/coins_activation/src/utxo_activation/common_impl.rs @@ -5,15 +5,21 @@ use crate::utxo_activation::init_utxo_standard_statuses::{UtxoStandardAwaitingSt use crate::utxo_activation::utxo_standard_activation_result::UtxoStandardActivationResult; use coins::coin_balance::EnableCoinBalanceOps; use coins::hd_pubkey::RpcTaskXPubExtractor; -use coins::utxo::UtxoActivationParams; -use coins::{MarketCoinOps, PrivKeyActivationPolicy, PrivKeyBuildPolicy}; +use coins::my_tx_history_v2::TxHistoryStorage; +use coins::utxo::utxo_tx_history_v2::{utxo_history_loop, UtxoTxHistoryOps}; +use coins::utxo::{UtxoActivationParams, UtxoCoinFields}; +use coins::{CoinFutSpawner, MarketCoinOps, PrivKeyActivationPolicy, PrivKeyBuildPolicy}; +use common::executor::{AbortSettings, SpawnAbortable}; use crypto::hw_rpc_task::HwConnectStatuses; -use crypto::CryptoCtx; +use crypto::CryptoCtxError; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; +use std::collections::HashMap; -pub async fn get_activation_result( +pub(crate) async fn get_activation_result( ctx: &MmArc, coin: &Coin, task_handle: &InitStandaloneCoinTaskHandle, @@ -28,14 +34,12 @@ where > + EnableCoinBalanceOps + MarketCoinOps, { - let current_block = - coin.current_block() - .compat() - .await - .map_to_mm(|error| InitUtxoStandardError::CoinCreationError { - ticker: coin.ticker().to_owned(), - error, - })?; + let ticker = coin.ticker().to_owned(); + let current_block = coin + .current_block() + .compat() + .await + .map_to_mm(InitUtxoStandardError::Transport)?; // Construct an Xpub extractor without checking if the MarketMaker supports HD wallet ops. // [`EnableCoinBalanceOps::enable_coin_balance`] won't just use `xpub_extractor` @@ -43,35 +47,55 @@ where let xpub_extractor = RpcTaskXPubExtractor::new_unchecked(ctx, task_handle, xpub_extractor_rpc_statuses()); task_handle.update_in_progress_status(UtxoStandardInProgressStatus::RequestingWalletBalance)?; let wallet_balance = coin - .enable_coin_balance(&xpub_extractor, activation_params.scan_policy) + .enable_coin_balance(&xpub_extractor, activation_params.enable_params.clone()) .await - .mm_err(|error| InitUtxoStandardError::CoinCreationError { - ticker: coin.ticker().to_owned(), - error: error.to_string(), - })?; + .mm_err(|enable_err| InitUtxoStandardError::from_enable_coin_balance_err(enable_err, ticker.clone()))?; task_handle.update_in_progress_status(UtxoStandardInProgressStatus::ActivatingCoin)?; let result = UtxoStandardActivationResult { + ticker, current_block, wallet_balance, }; Ok(result) } -pub fn xpub_extractor_rpc_statuses() -> HwConnectStatuses { +pub(crate) fn xpub_extractor_rpc_statuses( +) -> HwConnectStatuses { HwConnectStatuses { on_connect: UtxoStandardInProgressStatus::WaitingForTrezorToConnect, on_connected: UtxoStandardInProgressStatus::ActivatingCoin, on_connection_failed: UtxoStandardInProgressStatus::Finishing, - on_button_request: UtxoStandardInProgressStatus::WaitingForUserToConfirmPubkey, - on_pin_request: UtxoStandardAwaitingStatus::WaitForTrezorPin, + on_button_request: UtxoStandardInProgressStatus::FollowHwDeviceInstructions, + on_pin_request: UtxoStandardAwaitingStatus::EnterTrezorPin, + on_passphrase_request: UtxoStandardAwaitingStatus::EnterTrezorPassphrase, on_ready: UtxoStandardInProgressStatus::ActivatingCoin, } } -pub fn priv_key_build_policy(crypto_ctx: &CryptoCtx, activation_policy: PrivKeyActivationPolicy) -> PrivKeyBuildPolicy { +pub(crate) fn priv_key_build_policy( + ctx: &MmArc, + activation_policy: PrivKeyActivationPolicy, +) -> MmResult { match activation_policy { - PrivKeyActivationPolicy::IguanaPrivKey => PrivKeyBuildPolicy::iguana_priv_key(crypto_ctx), - PrivKeyActivationPolicy::Trezor => PrivKeyBuildPolicy::Trezor, + PrivKeyActivationPolicy::ContextPrivKey => PrivKeyBuildPolicy::detect_priv_key_policy(ctx), + PrivKeyActivationPolicy::Trezor => Ok(PrivKeyBuildPolicy::Trezor), } } + +pub(crate) fn start_history_background_fetching( + coin: Coin, + metrics: MetricsArc, + storage: impl TxHistoryStorage, + current_balances: HashMap, +) where + Coin: AsRef + UtxoTxHistoryOps, +{ + let spawner = CoinFutSpawner::new(&coin.as_ref().abortable_system); + + let msg = format!("'utxo_history_loop' has been aborted for {}", coin.ticker()); + let fut = utxo_history_loop(coin, storage, metrics, current_balances); + + let settings = AbortSettings::info_on_abort(msg); + spawner.spawn_with_settings(fut, settings); +} diff --git a/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs index 116f8cf4f5..74e7d12a77 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs @@ -2,24 +2,29 @@ use crate::context::CoinsActivationContext; use crate::prelude::TryFromCoinProtocol; use crate::standalone_coin::{InitStandaloneCoinActivationOps, InitStandaloneCoinTaskHandle, InitStandaloneCoinTaskManagerShared}; -use crate::utxo_activation::common_impl::{get_activation_result, priv_key_build_policy}; +use crate::utxo_activation::common_impl::{get_activation_result, priv_key_build_policy, + start_history_background_fetching}; use crate::utxo_activation::init_utxo_standard_activation_error::InitUtxoStandardError; use crate::utxo_activation::init_utxo_standard_statuses::{UtxoStandardAwaitingStatus, UtxoStandardInProgressStatus, UtxoStandardUserAction}; use crate::utxo_activation::utxo_standard_activation_result::UtxoStandardActivationResult; use async_trait::async_trait; +use coins::my_tx_history_v2::TxHistoryStorage; use coins::utxo::qtum::{QtumCoin, QtumCoinBuilder}; use coins::utxo::utxo_builder::UtxoCoinBuilder; use coins::utxo::UtxoActivationParams; use coins::CoinProtocol; -use crypto::CryptoCtx; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; use serde_json::Value as Json; +use std::collections::HashMap; pub type QtumTaskManagerShared = InitStandaloneCoinTaskManagerShared; pub type QtumRpcTaskHandle = InitStandaloneCoinTaskHandle; +#[derive(Clone)] pub struct QtumProtocolInfo; impl TryFromCoinProtocol for QtumProtocolInfo { @@ -56,8 +61,7 @@ impl InitStandaloneCoinActivationOps for QtumCoin { _protocol_info: Self::StandaloneProtocol, _task_handle: &QtumRpcTaskHandle, ) -> Result> { - let crypto_ctx = CryptoCtx::from_ctx(&ctx)?; - let priv_key_policy = priv_key_build_policy(&crypto_ctx, activation_request.priv_key_policy); + let priv_key_policy = priv_key_build_policy(&ctx, activation_request.priv_key_policy)?; let coin = QtumCoinBuilder::new(&ctx, &ticker, &coin_conf, activation_request, priv_key_policy) .build() @@ -74,4 +78,13 @@ impl InitStandaloneCoinActivationOps for QtumCoin { ) -> MmResult { get_activation_result(&ctx, self, task_handle, activation_request).await } + + fn start_history_background_fetching( + &self, + metrics: MetricsArc, + storage: impl TxHistoryStorage, + current_balances: HashMap, + ) { + start_history_background_fetching(self.clone(), metrics, storage, current_balances) + } } diff --git a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs index 26cbd372c2..342d070469 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs @@ -2,24 +2,30 @@ use crate::context::CoinsActivationContext; use crate::prelude::TryFromCoinProtocol; use crate::standalone_coin::{InitStandaloneCoinActivationOps, InitStandaloneCoinTaskHandle, InitStandaloneCoinTaskManagerShared}; -use crate::utxo_activation::common_impl::{get_activation_result, priv_key_build_policy}; +use crate::utxo_activation::common_impl::{get_activation_result, priv_key_build_policy, + start_history_background_fetching}; use crate::utxo_activation::init_utxo_standard_activation_error::InitUtxoStandardError; use crate::utxo_activation::init_utxo_standard_statuses::{UtxoStandardAwaitingStatus, UtxoStandardInProgressStatus, UtxoStandardUserAction}; use crate::utxo_activation::utxo_standard_activation_result::UtxoStandardActivationResult; use async_trait::async_trait; +use coins::my_tx_history_v2::TxHistoryStorage; use coins::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; use coins::utxo::utxo_standard::UtxoStandardCoin; -use coins::utxo::UtxoActivationParams; +use coins::utxo::{UtxoActivationParams, UtxoSyncStatus}; use coins::CoinProtocol; -use crypto::CryptoCtx; +use futures::StreamExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; use serde_json::Value as Json; +use std::collections::HashMap; pub type UtxoStandardTaskManagerShared = InitStandaloneCoinTaskManagerShared; pub type UtxoStandardRpcTaskHandle = InitStandaloneCoinTaskHandle; +#[derive(Clone)] pub struct UtxoStandardProtocolInfo; impl TryFromCoinProtocol for UtxoStandardProtocolInfo { @@ -54,10 +60,9 @@ impl InitStandaloneCoinActivationOps for UtxoStandardCoin { coin_conf: Json, activation_request: &Self::ActivationRequest, _protocol_info: Self::StandaloneProtocol, - _task_handle: &UtxoStandardRpcTaskHandle, + task_handle: &UtxoStandardRpcTaskHandle, ) -> MmResult { - let crypto_ctx = CryptoCtx::from_ctx(&ctx)?; - let priv_key_policy = priv_key_build_policy(&crypto_ctx, activation_request.priv_key_policy); + let priv_key_policy = priv_key_build_policy(&ctx, activation_request.priv_key_policy)?; let coin = UtxoArcBuilder::new( &ctx, @@ -70,6 +75,39 @@ impl InitStandaloneCoinActivationOps for UtxoStandardCoin { .build() .await .mm_err(|e| InitUtxoStandardError::from_build_err(e, ticker.clone()))?; + + if let Some(sync_watcher_mutex) = &coin.as_ref().block_headers_status_watcher { + let mut sync_watcher = sync_watcher_mutex.lock().await; + loop { + let in_progress_status = + match sync_watcher + .next() + .await + .ok_or(InitUtxoStandardError::CoinCreationError { + ticker: ticker.clone(), + error: "Error waiting for block headers synchronization status!".into(), + })? { + UtxoSyncStatus::SyncingBlockHeaders { + current_scanned_block, + last_block, + } => UtxoStandardInProgressStatus::SyncingBlockHeaders { + current_scanned_block, + last_block, + }, + UtxoSyncStatus::TemporaryError(e) => UtxoStandardInProgressStatus::TemporaryError(e), + UtxoSyncStatus::PermanentError(e) => { + return Err(InitUtxoStandardError::CoinCreationError { + ticker: ticker.clone(), + error: e, + } + .into()) + }, + UtxoSyncStatus::Finished { .. } => break, + }; + task_handle.update_in_progress_status(in_progress_status)?; + } + } + Ok(coin) } @@ -81,4 +119,13 @@ impl InitStandaloneCoinActivationOps for UtxoStandardCoin { ) -> MmResult { get_activation_result(&ctx, self, task_handle, activation_request).await } + + fn start_history_background_fetching( + &self, + metrics: MetricsArc, + storage: impl TxHistoryStorage, + current_balances: HashMap, + ) { + start_history_background_fetching(self.clone(), metrics, storage, current_balances) + } } diff --git a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs index 6e8fac4834..d6a3497640 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs @@ -1,7 +1,10 @@ use crate::standalone_coin::InitStandaloneCoinError; +use coins::coin_balance::EnableCoinBalanceError; +use coins::hd_wallet::{NewAccountCreatingError, NewAddressDerivingError}; +use coins::tx_history_storage::CreateTxHistoryStorageError; use coins::utxo::utxo_builder::UtxoCoinBuildError; -use coins::RegisterCoinError; -use crypto::CryptoInitError; +use coins::{BalanceError, RegisterCoinError}; +use crypto::{CryptoCtxError, HwError, HwRpcError}; use derive_more::Display; use rpc_task::RpcTaskError; use ser_error_derive::SerializeErrorType; @@ -11,18 +14,17 @@ use std::time::Duration; #[derive(Clone, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum InitUtxoStandardError { + #[display(fmt = "{}", _0)] + HwError(HwRpcError), #[display(fmt = "Initialization task has timed out {:?}", duration)] - TaskTimedOut { - duration: Duration, - }, - CoinIsAlreadyActivated { - ticker: String, - }, + TaskTimedOut { duration: Duration }, + #[display(fmt = "Coin {} is activated already", ticker)] + CoinIsAlreadyActivated { ticker: String }, #[display(fmt = "Error on platform coin {} creation: {}", ticker, error)] - CoinCreationError { - ticker: String, - error: String, - }, + CoinCreationError { ticker: String, error: String }, + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] Internal(String), } @@ -35,14 +37,23 @@ impl From for InitUtxoStandardError { } } -impl From for InitUtxoStandardError { +impl From for InitUtxoStandardError { /// `CryptoCtx` is expected to be initialized already. - fn from(crypto_err: CryptoInitError) -> Self { InitUtxoStandardError::Internal(crypto_err.to_string()) } + fn from(crypto_err: CryptoCtxError) -> Self { InitUtxoStandardError::Internal(crypto_err.to_string()) } +} + +impl From for InitUtxoStandardError { + fn from(e: CreateTxHistoryStorageError) -> Self { + match e { + CreateTxHistoryStorageError::Internal(internal) => InitUtxoStandardError::Internal(internal), + } + } } impl From for InitStandaloneCoinError { fn from(e: InitUtxoStandardError) -> Self { match e { + InitUtxoStandardError::HwError(hw) => InitStandaloneCoinError::HwError(hw), InitUtxoStandardError::TaskTimedOut { duration } => InitStandaloneCoinError::TaskTimedOut { duration }, InitUtxoStandardError::CoinIsAlreadyActivated { ticker } => { InitStandaloneCoinError::CoinIsAlreadyActivated { ticker } @@ -50,6 +61,7 @@ impl From for InitStandaloneCoinError { InitUtxoStandardError::CoinCreationError { ticker, error } => { InitStandaloneCoinError::CoinCreationError { ticker, error } }, + InitUtxoStandardError::Transport(transport) => InitStandaloneCoinError::Transport(transport), InitUtxoStandardError::Internal(internal) => InitStandaloneCoinError::Internal(internal), } } @@ -65,6 +77,61 @@ impl InitUtxoStandardError { }, } } + + pub fn from_enable_coin_balance_err(enable_coin_balance_err: EnableCoinBalanceError, ticker: String) -> Self { + match enable_coin_balance_err { + EnableCoinBalanceError::NewAddressDerivingError(addr) => { + Self::from_new_address_deriving_error(addr, ticker) + }, + EnableCoinBalanceError::NewAccountCreatingError(acc) => Self::from_new_account_err(acc, ticker), + EnableCoinBalanceError::BalanceError(balance) => Self::from_balance_err(balance, ticker), + } + } + + fn from_new_address_deriving_error(new_addr_err: NewAddressDerivingError, ticker: String) -> Self { + InitUtxoStandardError::CoinCreationError { + ticker, + error: new_addr_err.to_string(), + } + } + + fn from_new_account_err(new_acc_err: NewAccountCreatingError, ticker: String) -> Self { + match new_acc_err { + NewAccountCreatingError::RpcTaskError(rpc) => Self::from(rpc), + NewAccountCreatingError::HardwareWalletError(hw_err) => Self::from_hw_err(hw_err, ticker), + NewAccountCreatingError::Internal(internal) => InitUtxoStandardError::Internal(internal), + other => InitUtxoStandardError::CoinCreationError { + ticker, + error: other.to_string(), + }, + } + } + + fn from_hw_err(hw_error: HwError, ticker: String) -> Self { + match hw_error { + HwError::NoTrezorDeviceAvailable | HwError::DeviceDisconnected => { + InitUtxoStandardError::HwError(HwRpcError::NoTrezorDeviceAvailable) + }, + HwError::CannotChooseDevice { .. } => InitUtxoStandardError::HwError(HwRpcError::FoundMultipleDevices), + HwError::ConnectionTimedOut { timeout } => InitUtxoStandardError::TaskTimedOut { duration: timeout }, + HwError::FoundUnexpectedDevice => InitUtxoStandardError::HwError(HwRpcError::FoundUnexpectedDevice), + HwError::Failure(error) => InitUtxoStandardError::CoinCreationError { ticker, error }, + other => InitUtxoStandardError::Internal(other.to_string()), + } + } + + fn from_balance_err(balance_err: BalanceError, ticker: String) -> Self { + match balance_err { + BalanceError::Transport(transport) | BalanceError::InvalidResponse(transport) => { + InitUtxoStandardError::Transport(transport) + }, + BalanceError::Internal(internal) => InitUtxoStandardError::Internal(internal), + other => InitUtxoStandardError::CoinCreationError { + ticker, + error: other.to_string(), + }, + } + } } impl From for InitUtxoStandardError { diff --git a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_statuses.rs b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_statuses.rs index 92ab03da56..5bf863815c 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_statuses.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_statuses.rs @@ -8,12 +8,17 @@ pub type UtxoStandardUserAction = HwRpcTaskUserAction; #[derive(Clone, Serialize)] pub enum UtxoStandardInProgressStatus { ActivatingCoin, + SyncingBlockHeaders { + current_scanned_block: u64, + last_block: u64, + }, + TemporaryError(String), RequestingWalletBalance, Finishing, /// This status doesn't require the user to send `UserAction`, /// but it tells the user that he should confirm/decline an address on his device. WaitingForTrezorToConnect, - WaitingForUserToConfirmPubkey, + FollowHwDeviceInstructions, } impl InitStandaloneCoinInitialStatus for UtxoStandardInProgressStatus { diff --git a/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs b/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs index 152a4786d8..aa54ec3698 100644 --- a/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs +++ b/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs @@ -1,13 +1,22 @@ -use crate::prelude::CurrentBlock; -use coins::coin_balance::EnableCoinBalance; +use crate::prelude::{CurrentBlock, GetAddressesBalances}; +use coins::coin_balance::CoinBalanceReport; +use mm2_number::BigDecimal; use serde_derive::Serialize; +use std::collections::HashMap; #[derive(Clone, Serialize)] pub struct UtxoStandardActivationResult { + pub ticker: String, pub current_block: u64, - pub wallet_balance: EnableCoinBalance, + pub wallet_balance: CoinBalanceReport, } impl CurrentBlock for UtxoStandardActivationResult { fn current_block(&self) -> u64 { self.current_block } } + +impl GetAddressesBalances for UtxoStandardActivationResult { + fn get_addresses_balances(&self) -> HashMap { + self.wallet_balance.to_addresses_total_balances() + } +} diff --git a/mm2src/coins_activation/src/z_coin_activation.rs b/mm2src/coins_activation/src/z_coin_activation.rs index c1580d6dab..77b397d887 100644 --- a/mm2src/coins_activation/src/z_coin_activation.rs +++ b/mm2src/coins_activation/src/z_coin_activation.rs @@ -4,20 +4,25 @@ use crate::standalone_coin::{InitStandaloneCoinActivationOps, InitStandaloneCoin InitStandaloneCoinInitialStatus, InitStandaloneCoinTaskHandle, InitStandaloneCoinTaskManagerShared}; use async_trait::async_trait; -use coins::coin_balance::{EnableCoinBalance, IguanaWalletBalance}; +use coins::coin_balance::{CoinBalanceReport, IguanaWalletBalance}; +use coins::my_tx_history_v2::TxHistoryStorage; +use coins::tx_history_storage::CreateTxHistoryStorageError; use coins::z_coin::{z_coin_from_conf_and_params, BlockchainScanStopped, SyncStatus, ZCoin, ZCoinBuildError, ZcoinActivationParams, ZcoinProtocolInfo}; -use coins::{BalanceError, CoinProtocol, MarketCoinOps, RegisterCoinError}; +use coins::{BalanceError, CoinProtocol, MarketCoinOps, PrivKeyBuildPolicy, RegisterCoinError}; use crypto::hw_rpc_task::{HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; -use crypto::CryptoInitError; +use crypto::CryptoCtxError; use derive_more::Display; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; use rpc_task::RpcTaskError; use ser_error_derive::SerializeErrorType; use serde_derive::Serialize; use serde_json::Value as Json; +use std::collections::HashMap; use std::time::Duration; pub type ZcoinTaskManagerShared = InitStandaloneCoinTaskManagerShared; @@ -29,13 +34,19 @@ pub type ZcoinUserAction = HwRpcTaskUserAction; pub struct ZcoinActivationResult { pub ticker: String, pub current_block: u64, - pub wallet_balance: EnableCoinBalance, + pub wallet_balance: CoinBalanceReport, } impl CurrentBlock for ZcoinActivationResult { fn current_block(&self) -> u64 { self.current_block } } +impl GetAddressesBalances for ZcoinActivationResult { + fn get_addresses_balances(&self) -> HashMap { + self.wallet_balance.to_addresses_total_balances() + } +} + #[derive(Clone, Serialize)] #[non_exhaustive] pub enum ZcoinInProgressStatus { @@ -116,14 +127,22 @@ impl From for ZcoinInitError { } } -impl From for ZcoinInitError { - fn from(err: CryptoInitError) -> Self { ZcoinInitError::Internal(err.to_string()) } +impl From for ZcoinInitError { + fn from(err: CryptoCtxError) -> Self { ZcoinInitError::Internal(err.to_string()) } } impl From for ZcoinInitError { fn from(e: BlockchainScanStopped) -> Self { ZcoinInitError::Internal(e.to_string()) } } +impl From for ZcoinInitError { + fn from(e: CreateTxHistoryStorageError) -> Self { + match e { + CreateTxHistoryStorageError::Internal(internal) => ZcoinInitError::Internal(internal), + } + } +} + impl From for InitStandaloneCoinError { fn from(err: ZcoinInitError) -> Self { match err { @@ -145,6 +164,10 @@ impl From for InitStandaloneCoinError { } } +impl From for InitStandaloneCoinError { + fn from(e: CryptoCtxError) -> Self { InitStandaloneCoinError::Internal(e.to_string()) } +} + impl TryFromCoinProtocol for ZcoinProtocolInfo { fn try_from_coin_protocol(proto: CoinProtocol) -> Result> where @@ -179,14 +202,17 @@ impl InitStandaloneCoinActivationOps for ZCoin { protocol_info: ZcoinProtocolInfo, task_handle: &ZcoinRpcTaskHandle, ) -> MmResult { - let secp_privkey = ctx.secp256k1_key_pair().private().secret; + // When `ZCoin` supports Trezor, we'll need to check [`ZcoinActivationParams::priv_key_policy`] + // instead of using [`PrivKeyBuildPolicy::detect_priv_key_policy`]. + let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(&ctx)?; + let coin = z_coin_from_conf_and_params( &ctx, &ticker, &coin_conf, activation_request, protocol_info, - secp_privkey.as_slice(), + priv_key_policy, ) .await .mm_err(|e| ZcoinInitError::from_build_err(e, ticker))?; @@ -233,10 +259,19 @@ impl InitStandaloneCoinActivationOps for ZCoin { Ok(ZcoinActivationResult { ticker: self.ticker().into(), current_block, - wallet_balance: EnableCoinBalance::Iguana(IguanaWalletBalance { + wallet_balance: CoinBalanceReport::Iguana(IguanaWalletBalance { address: self.my_z_address_encoded(), balance, }), }) } + + /// Transaction history is fetching from a wallet database for `ZCoin`. + fn start_history_background_fetching( + &self, + _metrics: MetricsArc, + _storage: impl TxHistoryStorage, + _current_balances: HashMap, + ) { + } } diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index 7018aa5709..2327ebd957 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -16,28 +16,29 @@ track-ctx-pointer = ["shared_ref_counter/enable", "shared_ref_counter/log"] arrayref = "0.3" async-trait = "0.1" backtrace = "0.3" -base64 = "0.10.0" bytes = "1.1" cfg-if = "1.0" crossbeam = "0.7" fnv = "1.0.6" futures01 = { version = "0.1", package = "futures" } futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } +futures-timer = "3.0" hex = "0.4.2" http = "0.2" http-body = "0.1" itertools = "0.10" lazy_static = "1.4" -lightning = { git = "https://github.com/shamardy/rust-lightning", branch = "0.0.106" } log = "0.4.8" parking_lot = { version = "0.12.0", features = ["nightly"] } parking_lot_core = { version = "0.6", features = ["nightly"] } +primitive-types = "0.11.1" rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } serde = "1" serde_derive = "1" -serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } +sha2 = "0.9" shared_ref_counter = { path = "shared_ref_counter", optional = true } uuid = { version = "0.7", features = ["serde", "v4"] } wasm-timer = "0.2.4" @@ -59,18 +60,14 @@ anyhow = "1.0" chrono = "0.4" crossterm = "0.20" gstuff = { version = "0.7", features = ["crossterm", "nightly"] } -hdrhistogram = { version = "7.0", default-features = false, features = ["sync"] } hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } # using webpki-tokio to avoid rejecting valid certificates # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features hyper-rustls = { version = "0.23", default-features = false, features = ["http1", "http2", "webpki-tokio"] } libc = { version = "0.2" } +lightning = "0.0.110" log4rs = { version = "1.0", default-features = false, features = ["console_appender", "pattern_encoder"] } -metrics = { version = "0.12" } -metrics-runtime = { version = "0.13", default-features = false, features = ["metrics-observer-prometheus"] } -metrics-core = { version = "0.5" } -metrics-util = { version = "0.3" } -tokio = { version = "1.7", features = ["io-util", "rt-multi-thread", "net"] } +tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } [target.'cfg(windows)'.dependencies] winapi = "0.3" diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index a6d1e0fd4a..5a8edbd7da 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -11,6 +11,7 @@ //! binary #![allow(uncommon_codepoints)] +#![feature(allocator_api)] #![feature(integer_atomics, panic_info_message)] #![feature(async_closure)] #![feature(hash_raw_entry)] @@ -27,7 +28,7 @@ /// Implements a `From` for `enum` with a variant name matching the name of the type stored. /// -/// This is helpful as a workaround for the lack of datasort refinements. +/// This is helpful as a workaround for the lack of datasort refinements. /// And also as a simpler alternative to `enum_dispatch` and `enum_derive`. /// /// enum Color {Red (Red)} @@ -84,59 +85,79 @@ macro_rules! try_h { }; } +/// Drops mutability of given variable +#[macro_export] +macro_rules! drop_mutability { + ($t: ident) => { + let $t = $t; + }; +} + +/// Reads inner value of `Option`, returns `Ok(None)` otherwise. +#[macro_export] +macro_rules! some_or_return_ok_none { + ($val:expr) => { + match $val { + Some(t) => t, + None => { + return Ok(None); + }, + } + }; +} + #[macro_use] pub mod jsonrpc_client; #[macro_use] -pub mod log; +pub mod fmt; #[macro_use] -pub mod mm_metrics; +pub mod log; pub mod crash_reports; pub mod custom_futures; pub mod custom_iter; +#[path = "executor/mod.rs"] pub mod executor; +pub mod number_type_casting; pub mod seri; #[path = "patterns/state_machine.rs"] pub mod state_machine; pub mod time_cache; -#[cfg(not(target_arch = "wasm32"))] -#[path = "executor/native_executor.rs"] -pub mod executor; - #[cfg(not(target_arch = "wasm32"))] #[path = "wio.rs"] pub mod wio; -#[cfg(target_arch = "wasm32")] -#[path = "executor/wasm_executor.rs"] -pub mod executor; - #[cfg(target_arch = "wasm32")] pub mod wasm; + #[cfg(target_arch = "wasm32")] pub use wasm::*; use backtrace::SymbolName; +use chrono::Utc; pub use futures::compat::Future01CompatExt; -use futures::future::{abortable, AbortHandle, FutureExt}; use futures01::{future, Future}; -use http::header::{HeaderValue, CONTENT_TYPE}; +use http::header::CONTENT_TYPE; use http::Response; use parking_lot::{Mutex as PaMutex, MutexGuard as PaMutexGuard}; use rand::{rngs::SmallRng, SeedableRng}; use serde::{de, ser}; use serde_json::{self as json, Value as Json}; -use std::fmt::{self, Write as FmtWrite}; +use sha2::{Digest, Sha256}; +use std::alloc::Allocator; +use std::fmt::Write as FmtWrite; +use std::fs::File; use std::future::Future as Future03; -use std::io::Write; +use std::io::{BufReader, Read, Write}; use std::iter::Peekable; use std::mem::{forget, zeroed}; use std::num::NonZeroUsize; use std::ops::{Add, Deref, Div, RangeInclusive}; use std::os::raw::c_void; use std::panic::{set_hook, PanicInfo}; +use std::path::PathBuf; use std::ptr::read_volatile; use std::sync::atomic::Ordering; +use std::time::{Duration, SystemTime, SystemTimeError}; use uuid::Uuid; -use crate::executor::spawn; pub use http::StatusCode; pub use serde; @@ -145,7 +166,6 @@ cfg_native! { #[cfg(not(windows))] use findshlibs::{IterationControl, Segment, SharedLibrary, TargetSharedLibrary}; use std::env; - use std::path::PathBuf; use std::sync::Mutex; } @@ -153,6 +173,11 @@ cfg_wasm32! { use std::sync::atomic::AtomicUsize; } +pub const X_GRPC_WEB: &str = "x-grpc-web"; +pub const APPLICATION_JSON: &str = "application/json"; +pub const APPLICATION_GRPC_WEB: &str = "application/grpc-web"; +pub const APPLICATION_GRPC_WEB_PROTO: &str = "application/grpc-web+proto"; + pub const SATOSHIS: u64 = 100_000_000; pub const DEX_FEE_ADDR_PUBKEY: &str = "03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc06"; @@ -164,7 +189,7 @@ lazy_static! { pub auto trait NotSame {} impl !NotSame for (X, X) {} // Makes the error conversion work for structs/enums containing Box -impl NotSame for Box {} +impl NotSame for Box {} /// Converts u64 satoshis to f64 pub fn sat_to_f(sat: u64) -> f64 { sat as f64 / SATOSHIS as f64 } @@ -184,8 +209,8 @@ impl Default for bits256 { } } -impl fmt::Display for bits256 { - fn fmt(&self, fm: &mut fmt::Formatter) -> fmt::Result { +impl std::fmt::Display for bits256 { + fn fmt(&self, fm: &mut std::fmt::Formatter) -> std::fmt::Result { for &ch in self.bytes.iter() { fn hex_from_digit(num: u8) -> char { if num < 10 { @@ -218,7 +243,7 @@ impl<'de> de::Deserialize<'de> for bits256 { struct Bits256Visitor; impl<'de> de::Visitor<'de> for Bits256Visitor { type Value = bits256; - fn expecting(&self, fm: &mut fmt::Formatter) -> fmt::Result { fm.write_str("a byte array") } + fn expecting(&self, fm: &mut std::fmt::Formatter) -> std::fmt::Result { fm.write_str("a byte array") } fn visit_seq(self, mut seq: S) -> Result where S: de::SeqAccess<'de>, @@ -250,8 +275,8 @@ impl<'de> de::Deserialize<'de> for bits256 { } } -impl fmt::Debug for bits256 { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { (self as &dyn fmt::Display).fmt(f) } +impl std::fmt::Debug for bits256 { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { (self as &dyn std::fmt::Display).fmt(f) } } impl From<[u8; 32]> for bits256 { @@ -462,8 +487,8 @@ pub fn set_panic_hook() { let mut trace = String::new(); stack_trace(&mut stack_trace_frame, &mut |l| trace.push_str(l)); - log::info!("{}", info); - log::info!("backtrace\n{}", trace); + log!("{}", info); + log!("backtrace\n{}", trace); let _ = ENTERED.try_with(|e| e.compare_exchange(true, false, Ordering::Relaxed, Ordering::Relaxed)); })) @@ -500,7 +525,7 @@ where { let rf = match Response::builder() .status(status) - .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) + .header(CONTENT_TYPE, APPLICATION_JSON) .body(Vec::from(body)) { Ok(r) => future::ok::>, String>(r), @@ -512,6 +537,11 @@ where Box::new(rf) } +/// An alternative to the `std::convert::Infallible` that implements `Serialize`. +/// Replace it with `!` when it's stable. +#[derive(Clone, Deserialize, Serialize)] +pub enum SerdeInfallible {} + /// An mmrpc 2.0 compatible error variant that is used when the serialization of an RPC response is failed. #[derive(Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] @@ -519,8 +549,8 @@ pub enum SerializationError { InternalError(String), } -impl fmt::Display for SerializationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl std::fmt::Display for SerializationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SerializationError::InternalError(internal) => { write!(f, "Internal error: Couldn't serialize an RPC response: {}", internal) @@ -530,9 +560,7 @@ impl fmt::Display for SerializationError { } impl SerializationError { - pub fn from_error(e: E) -> SerializationError { - SerializationError::InternalError(e.to_string()) - } + pub fn from_error(e: E) -> SerializationError { SerializationError::InternalError(e.to_string()) } } #[derive(Clone, Serialize)] @@ -572,7 +600,7 @@ pub fn rpc_err_response(status: u16, msg: &str) -> HyRes { #[cfg(not(target_arch = "wasm32"))] pub fn var(name: &str) -> Result { - match std::env::var(name) { + match env::var(name) { Ok(v) => Ok(v), Err(_err) => ERR!("No {}", name), } @@ -621,7 +649,6 @@ pub fn now_ms() -> u64 { js_sys::Date::now() as u64 } #[cfg(target_arch = "wasm32")] pub fn now_float() -> f64 { use gstuff::duration_to_float; - use std::time::Duration; duration_to_float(Duration::from_millis(now_ms())) } @@ -750,6 +777,16 @@ pub const fn ten_f64() -> f64 { 10. } pub const fn one_hundred() -> usize { 100 } +pub const fn one_thousand_u32() -> u32 { 1000 } + +pub const fn one_and_half_f64() -> f64 { 1.5 } + +pub const fn three_hundred_f64() -> f64 { 300. } + +pub const fn one_f64() -> f64 { 1. } + +pub const fn sixty_f64() -> f64 { 60. } + pub fn one() -> NonZeroUsize { NonZeroUsize::new(1).unwrap() } #[derive(Debug, Deserialize)] @@ -926,16 +963,30 @@ impl Default for PagingOptionsEnum { fn default() -> Self { PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).expect("1 > 0")) } } -/// The AbortHandle that aborts on drop -pub struct AbortOnDropHandle(AbortHandle); +#[inline(always)] +pub fn get_utc_timestamp() -> i64 { Utc::now().timestamp() } -impl Drop for AbortOnDropHandle { - #[inline(always)] - fn drop(&mut self) { self.0.abort(); } +#[inline(always)] +pub fn get_local_duration_since_epoch() -> Result { + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) } -pub fn spawn_abortable(fut: impl Future03 + Send + 'static) -> AbortOnDropHandle { - let (abortable, handle) = abortable(fut); - spawn(abortable.then(|_| async {})); - AbortOnDropHandle(handle) +/// open file and calculate its sha256 digest as lowercase hex string +pub fn sha256_digest(path: &PathBuf) -> Result { + let input = File::open(path)?; + let mut reader = BufReader::new(input); + + let digest = { + let mut hasher = Sha256::new(); + let mut buffer = [0; 1024]; + loop { + let count = reader.read(&mut buffer)?; + if count == 0 { + break; + } + hasher.update(&buffer[..count]); + } + format!("{:x}", hasher.finalize()) + }; + Ok(digest) } diff --git a/mm2src/common/custom_futures.rs b/mm2src/common/custom_futures.rs deleted file mode 100644 index f6b43bf042..0000000000 --- a/mm2src/common/custom_futures.rs +++ /dev/null @@ -1,307 +0,0 @@ -/// Custom future combinators/implementations - some of standard do not match our requirements. -use crate::executor::Timer; -use crate::now_float; - -use futures::future::{select, Either}; -use futures::lock::Mutex as AsyncMutex; -use futures::task::Poll as Poll03; -use futures::Future as Future03; -use futures01::future::{self, loop_fn, Either as Either01, IntoFuture, Loop}; -use futures01::stream::{Fuse, Stream}; -use futures01::{Async, AsyncSink, Future, Poll, Sink}; - -use std::fmt; -use std::pin::Pin; -use std::task::Context; -use std::time::Duration; - -/// The analogue of join_all combinator running futures `sequentially`. -/// `join_all` runs futures `concurrently` which cause issues with native coins daemons RPC. -/// We need to get raw transactions containing unspent outputs when we build new one in order -/// to get denominated integer amount of UTXO instead of f64 provided by `listunspent` call. -/// Sometimes we might need info about dozens (or even hundreds) transactions at time so we can overflow -/// RPC queue of daemon very fast like this: https://github.com/bitpay/bitcore-node/issues/463#issuecomment-228788871. -/// Thx to https://stackoverflow.com/a/51717254/8707622 -pub fn join_all_sequential( - i: I, -) -> impl Future::Item>, Error = ::Error> -where - I: IntoIterator, - I::Item: IntoFuture, -{ - let iter = i.into_iter(); - loop_fn((vec![], iter), |(mut output, mut iter)| { - let fut = if let Some(next) = iter.next() { - Either01::A(next.into_future().map(Some)) - } else { - Either01::B(future::ok(None)) - }; - - fut.and_then(move |val| { - if let Some(val) = val { - output.push(val); - Ok(Loop::Continue((output, iter))) - } else { - Ok(Loop::Break(output)) - } - }) - }) -} - -/// The analogue of select_ok combinator running futures `sequentially`. -/// The use case of such combinator is Electrum (and maybe not only Electrum) multiple servers support. -/// Electrum client uses shared HashMap to store responses and we can treat the first received response as -/// error while it's really successful. We might change the Electrum support design in the future to avoid -/// such race condition but `select_ok_sequential` might be still useful to reduce the networking overhead. -/// There is no reason actually to send same request to all servers concurrently when it's enough to use just 1. -/// But we do a kind of round-robin if first server fails to respond, etc, and we return error only if all servers attempts failed. -/// When a server responds successfully we return the response and the number of failed attempts in a tuple. -pub fn select_ok_sequential( - i: I, -) -> impl Future::Item, usize), Error = Vec<::Error>> -where - I::Item: IntoFuture, -{ - let futures = i.into_iter(); - loop_fn((vec![], futures), |(mut errors, mut futures)| { - let fut = if let Some(next) = futures.next() { - Either01::A(next.into_future().map(Some)) - } else { - Either01::B(future::ok(None)) - }; - - fut.then(move |val| { - let val = match val { - Ok(val) => val, - Err(e) => { - errors.push(e); - return Ok(Loop::Continue((errors, futures))); - }, - }; - - if let Some(val) = val { - Ok(Loop::Break((val, errors.len()))) - } else { - Err(errors) - } - }) - }) -} - -/// Future for the `Sink::send_all` combinator, which sends a stream of values -/// to a sink and then waits until the sink has fully flushed those values. -/// The difference from standard implementation is this SendAll returns the `Stream` part even in case of errors. -/// It's useful for Electrum connections (based on MPSC channels): -/// If we get connection error standard SendAll will consume the receiver but it can still -/// receive messages from sender bypassing them to new TcpStream created in loop_fn. -#[derive(Debug)] -#[must_use = "futures do nothing unless polled"] -pub struct SendAll { - sink: Option, - stream: Option>, - buffered: Option, -} - -impl SendAll -where - T: Sink, - U: Stream, - T::SinkError: From, -{ - fn sink_mut(&mut self) -> &mut T { - self.sink - .as_mut() - .take() - .expect("Attempted to poll SendAll after completion") - } - - pub fn new(sink: T, stream: U) -> SendAll { - SendAll { - sink: Some(sink), - stream: Some(stream.fuse()), - buffered: None, - } - } - - fn stream_mut(&mut self) -> &mut Fuse { - self.stream - .as_mut() - .take() - .expect("Attempted to poll SendAll after completion") - } - - fn take_stream(&mut self) -> U { - let fuse = self.stream.take().expect("Attempted to poll Forward after completion"); - fuse.into_inner() - } - - fn take_result(&mut self) -> (T, U) { - let sink = self.sink.take().expect("Attempted to poll Forward after completion"); - let fuse = self.stream.take().expect("Attempted to poll Forward after completion"); - (sink, fuse.into_inner()) - } - - fn try_start_send(&mut self, item: U::Item) -> Poll<(), T::SinkError> { - debug_assert!(self.buffered.is_none()); - if let AsyncSink::NotReady(item) = self.sink_mut().start_send(item)? { - self.buffered = Some(item); - return Ok(Async::NotReady); - } - Ok(Async::Ready(())) - } -} - -macro_rules! try_ready_send_all { - ($selff: ident, $e:expr) => { - match $e { - Ok(Async::Ready(t)) => t, - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(e) => return Err(($selff.take_stream(), From::from(e))), - } - }; -} - -impl Future for SendAll -where - T: Sink, - U: Stream, - T::SinkError: From, -{ - type Item = (T, U); - type Error = (U, T::SinkError); - - fn poll(&mut self) -> Poll<(T, U), (U, T::SinkError)> { - // If we've got an item buffered already, we need to write it to the - // sink before we can do anything else - if let Some(item) = self.buffered.take() { - try_ready_send_all!(self, self.try_start_send(item)); - } - - loop { - match self - .stream_mut() - .poll() - .map_err(|e| (self.take_stream(), From::from(e)))? - { - Async::Ready(Some(item)) => try_ready_send_all!(self, self.try_start_send(item)), - Async::Ready(None) => { - try_ready_send_all!(self, self.sink_mut().close()); - return Ok(Async::Ready(self.take_result())); - }, - Async::NotReady => { - try_ready_send_all!(self, self.sink_mut().poll_complete()); - return Ok(Async::NotReady); - }, - } - } - } -} - -pub struct TimedMutexGuard<'a, T>(futures::lock::MutexGuard<'a, T>); -//impl<'a, T> Drop for TimedMutexGuard<'a, T> {fn drop (&mut self) {}} - -/// Like `AsyncMutex` but periodically invokes a callback, -/// allowing the application to implement timeouts, status updates and shutdowns. -pub struct TimedAsyncMutex(AsyncMutex); -impl TimedAsyncMutex { - pub fn new(v: T) -> TimedAsyncMutex { TimedAsyncMutex(AsyncMutex::new(v)) } - - /// Like `AsyncMutex::lock` but invokes the `tick` callback periodically. - /// `tick` returns a time till the next tick, or an error to abort the locking attempt. - /// `tick` parameters are the time when the locking attempt has started and the current time - /// (they are equal on the first invocation of `tick`). - pub async fn lock(&self, mut tick: F) -> Result, E> - where - F: FnMut(f64, f64) -> Result, - { - let start = now_float(); - let mut now = start; - let mut l = self.0.lock(); - let l = loop { - let tick_after = tick(start, now)?; - let t = Timer::till(now + tick_after); - let rc = select(l, t).await; - match rc { - Either::Left((l, _t)) => break l, - Either::Right((_t, lʹ)) => { - now = now_float(); - l = lʹ - }, - } - }; - Ok(TimedMutexGuard(l)) - } -} - -#[derive(Debug)] -pub struct TimeoutError { - pub duration: Duration, -} - -impl fmt::Display for TimeoutError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}s timed out waiting for the future to complete", - self.duration.as_secs_f64() - ) - } -} - -/// Unlike `futures_timer::FutureExt` and Tokio timers, this trait implementation works with any reactor and on WASM arch. -pub trait FutureTimerExt: Future03 + Sized { - /// Finishes with `TimeoutError` if the underlying future isn't ready withing the given timeframe. - fn timeout(self, duration: Duration) -> Timeout { - Timeout { - future: self, - timer: Timer::sleep(duration.as_secs_f64()), - duration, - } - } - - fn timeout_secs(self, secs: f64) -> Timeout { - Timeout { - future: self, - timer: Timer::sleep(secs), - duration: Duration::from_secs_f64(secs), - } - } -} - -impl FutureTimerExt for F {} - -/// Future returned by the `FutureTimerExt::timeout` method. -#[must_use = "futures do nothing unless polled"] -pub struct Timeout { - future: F, - timer: Timer, - duration: Duration, -} - -impl Future03 for Timeout -where - F: Future03 + Unpin, -{ - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll03 { - match Future03::poll(Pin::new(&mut self.future), cx) { - Poll03::Ready(out) => return Poll03::Ready(Ok(out)), - Poll03::Pending => (), - } - match Future03::poll(Pin::new(&mut self.timer), cx) { - Poll03::Ready(()) => Poll03::Ready(Err(TimeoutError { - duration: self.duration, - })), - Poll03::Pending => Poll03::Pending, - } - } -} - -unsafe impl Send for Timeout where F: Send {} - -#[test] -fn test_timeout() { - let _err = crate::block_on(Timer::sleep(0.4).timeout(Duration::from_secs_f64(0.1))).expect_err("Expected timeout"); - let _ok = crate::block_on(Timer::sleep(0.1).timeout(Duration::from_secs_f64(0.2))).expect("Expected future"); -} diff --git a/mm2src/common/custom_futures/mod.rs b/mm2src/common/custom_futures/mod.rs new file mode 100644 index 0000000000..f06583a90b --- /dev/null +++ b/mm2src/common/custom_futures/mod.rs @@ -0,0 +1,79 @@ +/// Custom future combinators/implementations - some of standard do not match our requirements. +use futures01::future::{self, loop_fn, Either as Either01, IntoFuture, Loop}; +use futures01::Future; + +pub mod repeatable; +pub mod timeout; + +/// The analogue of join_all combinator running futures `sequentially`. +/// `join_all` runs futures `concurrently` which cause issues with native coins daemons RPC. +/// We need to get raw transactions containing unspent outputs when we build new one in order +/// to get denominated integer amount of UTXO instead of f64 provided by `listunspent` call. +/// Sometimes we might need info about dozens (or even hundreds) transactions at time so we can overflow +/// RPC queue of daemon very fast like this: https://github.com/bitpay/bitcore-node/issues/463#issuecomment-228788871. +/// Thx to https://stackoverflow.com/a/51717254/8707622 +pub fn join_all_sequential( + i: I, +) -> impl Future::Item>, Error = ::Error> +where + I: IntoIterator, + I::Item: IntoFuture, +{ + let iter = i.into_iter(); + loop_fn((vec![], iter), |(mut output, mut iter)| { + let fut = if let Some(next) = iter.next() { + Either01::A(next.into_future().map(Some)) + } else { + Either01::B(future::ok(None)) + }; + + fut.and_then(move |val| { + if let Some(val) = val { + output.push(val); + Ok(Loop::Continue((output, iter))) + } else { + Ok(Loop::Break(output)) + } + }) + }) +} + +/// The analogue of select_ok combinator running futures `sequentially`. +/// The use case of such combinator is Electrum (and maybe not only Electrum) multiple servers support. +/// Electrum client uses shared HashMap to store responses and we can treat the first received response as +/// error while it's really successful. We might change the Electrum support design in the future to avoid +/// such race condition but `select_ok_sequential` might be still useful to reduce the networking overhead. +/// There is no reason actually to send same request to all servers concurrently when it's enough to use just 1. +/// But we do a kind of round-robin if first server fails to respond, etc, and we return error only if all servers attempts failed. +/// When a server responds successfully we return the response and the number of failed attempts in a tuple. +pub fn select_ok_sequential( + i: I, +) -> impl Future::Item, usize), Error = Vec<::Error>> +where + I::Item: IntoFuture, +{ + let futures = i.into_iter(); + loop_fn((vec![], futures), |(mut errors, mut futures)| { + let fut = if let Some(next) = futures.next() { + Either01::A(next.into_future().map(Some)) + } else { + Either01::B(future::ok(None)) + }; + + fut.then(move |val| { + let val = match val { + Ok(val) => val, + Err(e) => { + errors.push(e); + return Ok(Loop::Continue((errors, futures))); + }, + }; + + if let Some(val) = val { + Ok(Loop::Break((val, errors.len()))) + } else { + Err(errors) + } + }) + }) +} diff --git a/mm2src/common/custom_futures/repeatable.rs b/mm2src/common/custom_futures/repeatable.rs new file mode 100644 index 0000000000..1e31bc5a4d --- /dev/null +++ b/mm2src/common/custom_futures/repeatable.rs @@ -0,0 +1,659 @@ +//! A future that can be repeated if an error occurs or not all conditions are met. +//! +//! # Why `async move` shouldn't be allowed +//! +//! Let's consider the following example: +//! +//! ```rust +//! let mut counter = 0; +//! let res = repeatable!(async move { +//! counter += 1; +//! if counter > 1 { Ready(()) } else { Retry(()) } +//! }) +//! .repeat_every_secs(0.1) +//! .attempts(10) +//! .await; +//! +//! res.expect_err("'counter' will never be greater than 1"); +//! ``` +//! +//! This happens due to the fact that the `counter` variable is not shared between attempts, +//! and every time the future starts with `counter = 0`. + +use crate::executor::Timer; +use crate::now_ms; +use crate::number_type_casting::SafeTypeCastingNumbers; +use futures::FutureExt; +use log::warn; +use std::fmt; +use std::future::Future; +use std::marker::PhantomData; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +pub use Action::{Ready, Retry}; + +/// Wraps the given future into `Repeatable` future. +/// The future should return [`Action`] with any `T` and `E` types. +#[macro_export] +macro_rules! repeatable { + (async { $($t:tt)* }) => { + $crate::custom_futures::repeatable::Repeatable::new(|| Box::pin(async { $($t)* })) + }; + ($fut:expr) => { + $crate::custom_futures::repeatable::Repeatable::new(|| $fut) + }; +} + +/// Wraps the given future into `Repeatable` future. +/// The future should return [`Result`], where +/// * `Ok(T)` => `Action::Ready(T)` +/// * `Err(E)` => `Action::Retry(E)` +#[macro_export] +macro_rules! retry_on_err { + (async { $($t:tt)* }) => { + $crate::custom_futures::repeatable::Repeatable::new(|| { + use $crate::custom_futures::repeatable::RetryOnError; + use futures::FutureExt; + + let fut = async { $($t)* }; + Box::pin(fut.map(Result::retry_on_err)) + }) + }; + ($fut:expr) => { + $crate::custom_futures::repeatable::Repeatable::new(|| { + use $crate::custom_futures::repeatable::RetryOnError; + use futures::FutureExt; + + $fut.map(Result::retry_on_err) + }) + }; +} + +/// Unwraps a result or returns `Action::Retry(E)`. +#[macro_export] +macro_rules! try_or_retry { + ($exp:expr) => {{ + match $exp { + Ok(t) => t, + Err(e) => return $crate::custom_futures::repeatable::Retry(e), + } + }}; +} + +/// Unwraps a result or returns `Action::Ready(E)`. +#[macro_export] +macro_rules! try_or_ready_err { + ($exp:expr) => {{ + match $exp { + Ok(t) => t, + Err(e) => return $crate::custom_futures::repeatable::Ready(Err(e)), + } + }}; +} + +const DEFAULT_REPEAT_EVERY: Duration = Duration::from_secs(1); + +pub trait FactoryTrait: Fn() -> F {} + +impl FactoryTrait for Factory where Factory: Fn() -> F {} + +pub trait RepeatableTrait: Future> + Unpin {} + +impl RepeatableTrait for F where F: Future> + Unpin {} + +pub(crate) trait InspectErrorTrait: 'static + Fn(&E) + Send {} + +impl InspectErrorTrait for F {} + +#[derive(Clone, Debug, PartialEq)] +pub enum RepeatError { + TimeoutExpired { + until_ms: u64, + /// An error occurred during the last attempt. + error: E, + }, + AttemptsExceed { + attempts: usize, + /// An error occurred during the last attempt. + error: E, + }, +} + +impl RepeatError { + pub fn error(&self) -> &E { + match self { + RepeatError::TimeoutExpired { error, .. } | RepeatError::AttemptsExceed { error, .. } => error, + } + } + + pub fn into_error(self) -> E { + match self { + RepeatError::TimeoutExpired { error, .. } | RepeatError::AttemptsExceed { error, .. } => error, + } + } + + fn timeout(until_ms: u64, error: E) -> Self { RepeatError::TimeoutExpired { until_ms, error } } + + fn attempts(attempts: usize, error: E) -> Self { RepeatError::AttemptsExceed { attempts, error } } +} + +impl fmt::Display for RepeatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RepeatError::TimeoutExpired { until_ms, error } => { + write!( + f, + "Waited too long until {until_ms}ms for the future to succeed. Error: {error}", + ) + }, + RepeatError::AttemptsExceed { attempts, error } => { + write!(f, "Error {error} on retrying the future after {attempts} attempts") + }, + } + } +} + +/// The future is ether ready (with a `T` result), or not ready (failed with an intermediate `E` error). +#[derive(Debug)] +pub enum Action { + Ready(T), + Retry(E), +} + +pub trait RetryOnError { + fn retry_on_err(self) -> Action; +} + +impl RetryOnError for Result { + /// Converts `Result` into `Action`: + /// * `Ok(T)` => `Action::Ready(T)`. + /// * `Err(E)` => `Action::Retry(E)`. + #[inline] + fn retry_on_err(self) -> Action { + match self { + Ok(ready) => Action::Ready(ready), + Err(e) => Action::Retry(e), + } + } +} + +/// The result of `repeatable` or `retry_on_err` macros - the first step at the future configuration. +pub struct Repeatable { + factory: Factory, + /// Currently executable future, i.e. an active attempt. + exec_fut: F, + /// A timeout future if we're currently waiting for a timeout. + timeout_fut: Option, + until: RepeatUntil, + repeat_every: Duration, + inspect_err: Option>>, + _phantom: PhantomData<(F, T, E)>, +} + +impl Repeatable +where + Factory: FactoryTrait, + F: RepeatableTrait, +{ + #[inline] + pub fn new(factory: Factory) -> Self { + let exec_fut = factory(); + + Repeatable { + factory, + exec_fut, + timeout_fut: None, + until: RepeatUntil::default(), + repeat_every: DEFAULT_REPEAT_EVERY, + inspect_err: None, + _phantom: PhantomData::default(), + } + } + + /// Specifies an inspect handler that does something with an error on each unsuccessful attempt. + #[inline] + pub fn inspect_err(mut self, inspect: Inspect) -> Self + where + Inspect: 'static + Fn(&E) + Send, + { + self.inspect_err = Some(Box::new(inspect)); + self + } + + #[inline] + pub fn repeat_every(mut self, repeat_every: Duration) -> Self { + self.repeat_every = repeat_every; + self + } + + #[inline] + pub fn repeat_every_ms(self, repeat_every: u64) -> Self { self.repeat_every(Duration::from_millis(repeat_every)) } + + #[inline] + pub fn repeat_every_secs(self, repeat_every: f64) -> Self { + self.repeat_every(Duration::from_secs_f64(repeat_every)) + } + + /// Repeat the future until it's ready. + /// + /// # Warning + /// + /// This may lead to an endless loop if the future is never ready. + #[inline] + pub fn until_ready(mut self) -> Self { + self.until = RepeatUntil::Ready; + self + } + + /// Specifies a total number of attempts to run the future. + /// So there will be up to `total_attempts`. + /// + /// # Panic + /// + /// Panics if `total_attempts` is 0. + #[inline] + pub fn attempts(mut self, total_attempts: usize) -> Self { + assert!(total_attempts > 0, "'total_attempts' cannot be 0"); + + self.until = RepeatUntil::AttemptsExceed(AttemptsState::new(total_attempts)); + self + } + + /// Specifies a deadline in milliseconds before that we may try to repeat the future. + #[inline] + pub fn until_ms(mut self, until_ms: u64) -> Self { + let now = now_ms(); + if now >= until_ms { + warn!("Deadline has already passed: now={now:?} until={until_ms:?}") + } + + self.until = RepeatUntil::TimeoutMsExpired(until_ms); + self + } + + /// Specifies a deadline in seconds before that we may try to repeat the future. + #[inline] + pub fn until_s(self, until_s: u64) -> Self { + let until_ms = until_s * 1000; + self.until_ms(until_ms) + } + + /// Specifies a timeout in milliseconds before that we may try to repeat the future. + /// Note this method name should differ from [`FutureTimerExt::timeout_ms`]. + #[inline] + pub fn with_timeout_ms(self, timeout_ms: u64) -> Self { self.until_ms(now_ms() + timeout_ms) } + + /// Specifies a timeout in seconds before that we may try to repeat the future. + /// Note this method name should differ from [`FutureTimerExt::timeout_secs`]. + #[inline] + pub fn with_timeout_secs(self, timeout_secs: f64) -> Self { + let timeout_ms = (timeout_secs * 1000.) as u64; + self.until_ms(now_ms() + timeout_ms) + } + + /// Checks if the deadline is not going to be reached after the `repeat_every` timeout. + fn check_can_retry_after_timeout(&self, until_ms: u64) -> bool { + let repeat_every_ms: u64 = self.repeat_every.as_millis().into_or_max(); + let will_be_after_timeout = now_ms() + repeat_every_ms; + will_be_after_timeout < until_ms + } +} + +impl Unpin for Repeatable {} + +impl Future for Repeatable +where + Factory: FactoryTrait, + F: RepeatableTrait, +{ + type Output = Result>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + loop { + if poll_timeout(&mut self.timeout_fut, cx).is_pending() { + return Poll::Pending; + } + + match self.exec_fut.poll_unpin(cx) { + Poll::Ready(Ready(ready)) => return Poll::Ready(Ok(ready)), + Poll::Ready(Retry(error)) => { + if let Some(ref inspect) = self.inspect_err { + inspect(&error); + } + + match self.until { + RepeatUntil::TimeoutMsExpired(until_ms) => { + if !self.check_can_retry_after_timeout(until_ms) { + return Poll::Ready(Err(RepeatError::timeout(until_ms, error))); + } + }, + RepeatUntil::AttemptsExceed(ref mut attempts) => { + // Check if we have one more attempt to retry to execute the future. + attempts.current_attempt += 1; + if attempts.current_attempt >= attempts.total_attempts { + return Poll::Ready(Err(RepeatError::attempts(attempts.current_attempt, error))); + } + }, + // Repeat until the future is ready. + RepeatUntil::Ready => (), + } + + // Create a new future attempt. + self.exec_fut = (self.factory)(); + // Reset the timeout future. + self.timeout_fut = Some(Timer::sleep(self.repeat_every.as_secs_f64())); + }, + // We should proceed with this `exec` future attempt later. + Poll::Pending => return Poll::Pending, + } + } + } +} + +struct AttemptsState { + current_attempt: usize, + total_attempts: usize, +} + +impl AttemptsState { + fn new(total_attempts: usize) -> AttemptsState { + AttemptsState { + current_attempt: 0, + total_attempts, + } + } +} + +enum RepeatUntil { + TimeoutMsExpired(u64), + AttemptsExceed(AttemptsState), + Ready, +} + +impl Default for RepeatUntil { + fn default() -> Self { RepeatUntil::AttemptsExceed(AttemptsState::new(1)) } +} + +/// Returns `Poll::Ready(())` if there is no need to wait for the timeout. +fn poll_timeout(timeout_fut: &mut Option, cx: &mut Context<'_>) -> Poll<()> { + let mut timeout = match timeout_fut.take() { + Some(timeout) => timeout, + None => return Poll::Ready(()), + }; + + match timeout.poll_unpin(cx) { + Poll::Ready(_) => Poll::Ready(()), + Poll::Pending => { + *timeout_fut = Some(timeout); + Poll::Pending + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::block_on; + use futures::lock::Mutex as AsyncMutex; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::Arc; + use std::time::Duration; + use std::time::Instant; + + async fn an_operation(counter: &AsyncMutex, finish_if: usize) -> Result { + let mut counter = counter.lock().await; + *counter += 1; + if *counter == finish_if { + Ok(*counter) + } else { + Err("Not ready") + } + } + + #[test] + fn test_attempts_success() { + const ATTEMPTS_TO_FINISH: usize = 3; + + let counter = AsyncMutex::new(0); + + let fut = repeatable!(async { an_operation(&counter, ATTEMPTS_TO_FINISH).await.retry_on_err() }) + .repeat_every(Duration::from_millis(100)) + .attempts(ATTEMPTS_TO_FINISH); + + let actual = block_on(fut); + // If the counter is 3, then there were exactly 3 attempts to finish the future. + assert_eq!(actual, Ok(ATTEMPTS_TO_FINISH)); + } + + #[test] + fn test_attempts_exceed() { + const ATTEMPTS_TO_FINISH: usize = 3; + const ACTUAL_ATTEMPTS: usize = 2; + + let counter = AsyncMutex::new(0); + + let fut = repeatable!(async { an_operation(&counter, ATTEMPTS_TO_FINISH).await.retry_on_err() }) + .repeat_every(Duration::from_millis(100)) + .attempts(ACTUAL_ATTEMPTS); + + let actual = block_on(fut); + assert_eq!( + actual, + Err(RepeatError::AttemptsExceed { + attempts: ACTUAL_ATTEMPTS, + error: "Not ready" + }) + ); + + // If the counter is 2, then there were exactly 2 attempts to finish the future. + let actual_attempts = block_on(counter.lock()); + assert_eq!(*actual_attempts, ACTUAL_ATTEMPTS); + } + + #[test] + fn test_attempts_retry_on_err() { + const ATTEMPTS_TO_FINISH: usize = 3; + + let counter = AsyncMutex::new(0); + + let fut = repeatable!(async { an_operation(&counter, ATTEMPTS_TO_FINISH).await.retry_on_err() }) + .repeat_every(Duration::from_millis(100)) + .attempts(ATTEMPTS_TO_FINISH); + + let actual = block_on(fut); + assert_eq!(actual, Ok(ATTEMPTS_TO_FINISH)); + } + + #[test] + fn test_attempts_retry_on_err_macro() { + const ATTEMPTS_TO_FINISH: usize = 3; + + let counter = AsyncMutex::new(0); + + let fut = retry_on_err!(async { an_operation(&counter, ATTEMPTS_TO_FINISH).await }) + .repeat_every(Duration::from_millis(100)) + .attempts(ATTEMPTS_TO_FINISH); + + let actual = block_on(fut); + assert_eq!(actual, Ok(ATTEMPTS_TO_FINISH)); + } + + #[test] + fn test_attempts_inspect_err() { + const ATTEMPTS_TO_FINISH: usize = 3; + const FAILED_ATTEMPTS: usize = 2; + + let inspect_counter = Arc::new(AtomicUsize::new(0)); + let inspect_counter_c = inspect_counter.clone(); + let counter = AsyncMutex::new(0); + + let fut = repeatable!(async { an_operation(&counter, ATTEMPTS_TO_FINISH).await.retry_on_err() }) + .repeat_every(Duration::from_millis(100)) + .inspect_err(move |_| { + inspect_counter.fetch_add(1, Ordering::Relaxed); + }) + .attempts(ATTEMPTS_TO_FINISH); + + let actual = block_on(fut); + // If the counter is 3, then there were exactly 3 attempts to finish the future. + assert_eq!(actual, Ok(ATTEMPTS_TO_FINISH)); + // There should be 2 errors. + assert_eq!(inspect_counter_c.load(Ordering::Relaxed), FAILED_ATTEMPTS); + } + + #[test] + fn test_until_success() { + const ATTEMPTS_TO_FINISH: usize = 5; + const LOWEST_TIMEOUT: Duration = Duration::from_millis(350); + const HIGHEST_TIMEOUT: Duration = Duration::from_millis(800); + + let counter = AsyncMutex::new(0); + + let fut = repeatable!(async { an_operation(&counter, ATTEMPTS_TO_FINISH).await.retry_on_err() }) + .repeat_every(Duration::from_millis(100)) + .until_ms(now_ms() + HIGHEST_TIMEOUT.as_millis() as u64); + + let before = Instant::now(); + let actual = block_on(fut); + let took = before.elapsed(); + + // If the counter is 3, then there were exactly 3 attempts to finish the future. + assert_eq!(actual, Ok(ATTEMPTS_TO_FINISH)); + + assert!( + LOWEST_TIMEOUT <= took && took <= HIGHEST_TIMEOUT, + "Expected [{:?}, {:?}], but took {:?}", + LOWEST_TIMEOUT, + HIGHEST_TIMEOUT, + took + ); + } + + #[test] + fn test_until_expired() { + const ATTEMPTS_TO_FINISH: usize = 10; + const LOWEST_TIMEOUT: Duration = Duration::from_millis(350); + const HIGHEST_TIMEOUT: Duration = Duration::from_millis(800); + + let counter = AsyncMutex::new(0); + + let until_ms = now_ms() + HIGHEST_TIMEOUT.as_millis() as u64; + + let fut = repeatable!(async { an_operation(&counter, ATTEMPTS_TO_FINISH).await.retry_on_err() }) + .repeat_every(Duration::from_millis(100)) + .until_ms(until_ms); + + let before = Instant::now(); + let actual = block_on(fut); + let took = before.elapsed(); + + // If the counter is 3, then there were exactly 3 attempts to finish the future. + let error = RepeatError::TimeoutExpired { + until_ms, + error: "Not ready", + }; + assert_eq!(actual, Err(error)); + + assert!( + LOWEST_TIMEOUT <= took && took <= HIGHEST_TIMEOUT, + "Expected [{:?}, {:?}], but took {:?}", + LOWEST_TIMEOUT, + HIGHEST_TIMEOUT, + took + ); + } + + #[test] + fn test_until_ms() { + const ATTEMPTS_TO_FINISH: usize = 5; + const LOWEST_TIMEOUT: u64 = 350; + const HIGHEST_TIMEOUT: u64 = 800; + + let counter = AsyncMutex::new(0); + + let fut = repeatable!(async { an_operation(&counter, ATTEMPTS_TO_FINISH).await.retry_on_err() }) + .repeat_every(Duration::from_millis(100)) + .until_ms(now_ms() + HIGHEST_TIMEOUT); + + let before = Instant::now(); + let actual = block_on(fut); + let took = before.elapsed(); + + // If the counter is 3, then there were exactly 3 attempts to finish the future. + assert_eq!(actual, Ok(ATTEMPTS_TO_FINISH)); + + let lowest = Duration::from_millis(LOWEST_TIMEOUT); + let highest = Duration::from_millis(HIGHEST_TIMEOUT); + assert!( + lowest <= took && took <= highest, + "Expected [{:?}, {:?}], but took {:?}", + lowest, + highest, + took + ); + } + + /// `Repeatable` future should be executed the only once + /// if neither [`Repeatable::until`] nor [`Repeatable::attempts`] are specified. + /// + /// The first case within the following: + /// https://github.com/KomodoPlatform/atomicDEX-API/pull/1564#discussion_r1040989842 + #[test] + fn test_without_attempts_and_timeout() { + const ATTEMPTS_TO_FINISH: usize = 5; + + let counter = AsyncMutex::new(0); + + let fut = repeatable!(async { an_operation(&counter, ATTEMPTS_TO_FINISH).await.retry_on_err() }); + + let actual = block_on(fut); + + assert_eq!( + actual, + Err(RepeatError::AttemptsExceed { + attempts: 1, + error: "Not ready" + }) + ); + } + + /// `Repeatable` future should be executed the only once + /// if neither [`Repeatable::until`] nor [`Repeatable::attempts`] are specified. + /// Please note that in this case [`Repeatable::repeat_every`] should have no effect. + /// + /// The first case within the following: + /// https://github.com/KomodoPlatform/atomicDEX-API/pull/1564#discussion_r1040989842 + #[test] + fn test_repeat_every_without_attempts_and_timeout() { + const ATTEMPTS_TO_FINISH: usize = 5; + const LOWEST_TIMEOUT: Duration = Duration::from_micros(0); + const HIGHEST_TIMEOUT: Duration = Duration::from_millis(100); + + let counter = AsyncMutex::new(0); + + let fut = repeatable!(async { an_operation(&counter, ATTEMPTS_TO_FINISH).await.retry_on_err() }) + .repeat_every_secs(10.); + + let before = Instant::now(); + let actual = block_on(fut); + let took = before.elapsed(); + + assert_eq!( + actual, + Err(RepeatError::AttemptsExceed { + attempts: 1, + error: "Not ready" + }) + ); + + assert!( + LOWEST_TIMEOUT <= took && took <= HIGHEST_TIMEOUT, + "Expected [{:?}, {:?}], but took {:?}", + LOWEST_TIMEOUT, + HIGHEST_TIMEOUT, + took + ); + } +} diff --git a/mm2src/common/custom_futures/timeout.rs b/mm2src/common/custom_futures/timeout.rs new file mode 100644 index 0000000000..0c54ed71be --- /dev/null +++ b/mm2src/common/custom_futures/timeout.rs @@ -0,0 +1,86 @@ +use crate::executor::Timer; +use futures::task::Poll as Poll03; +use futures::Future as Future03; +use std::fmt; +use std::pin::Pin; +use std::task::Context; +use std::time::Duration; + +#[derive(Debug)] +pub struct TimeoutError { + pub duration: Duration, +} + +impl fmt::Display for TimeoutError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}s timed out waiting for the future to complete", + self.duration.as_secs_f64() + ) + } +} + +/// Unlike `futures_timer::FutureExt` and Tokio timers, this trait implementation works with any reactor and on WASM arch. +pub trait FutureTimerExt: Future03 + Sized { + /// Finishes with `TimeoutError` if the underlying future isn't ready withing the given timeframe. + fn timeout(self, duration: Duration) -> Timeout { + Timeout { + future: self, + timer: Timer::sleep(duration.as_secs_f64()), + duration, + } + } + + fn timeout_secs(self, secs: f64) -> Timeout { + Timeout { + future: self, + timer: Timer::sleep(secs), + duration: Duration::from_secs_f64(secs), + } + } +} + +impl FutureTimerExt for F {} + +/// Future returned by the `FutureTimerExt::timeout` method. +#[must_use = "futures do nothing unless polled"] +pub struct Timeout { + future: F, + timer: Timer, + duration: Duration, +} + +impl Future03 for Timeout +where + F: Future03 + Unpin, +{ + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll03 { + match Future03::poll(Pin::new(&mut self.future), cx) { + Poll03::Ready(out) => return Poll03::Ready(Ok(out)), + Poll03::Pending => (), + } + match Future03::poll(Pin::new(&mut self.timer), cx) { + Poll03::Ready(()) => Poll03::Ready(Err(TimeoutError { + duration: self.duration, + })), + Poll03::Pending => Poll03::Pending, + } + } +} + +unsafe impl Send for Timeout where F: Send {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_timeout() { + let _err = + crate::block_on(Timer::sleep(0.4).timeout(Duration::from_secs_f64(0.1))).expect_err("Expected timeout"); + let _ok = crate::block_on(Timer::sleep(0.1).timeout(Duration::from_secs_f64(0.2))).expect("Expected future"); + } +} diff --git a/mm2src/common/custom_iter.rs b/mm2src/common/custom_iter.rs index 4a26d4b336..c7e2d353fc 100644 --- a/mm2src/common/custom_iter.rs +++ b/mm2src/common/custom_iter.rs @@ -47,29 +47,6 @@ pub trait TryIntoGroupMap { impl TryIntoGroupMap for T {} -pub trait TryUnzip -where - Self: Iterator> + Sized, -{ - /// An iterator method that unwraps the given `Result<(A, B), Err>` items yielded by the input iterator - /// and collects `(A, B)` tuple pairs into the pair of `(FromA, FromB)` containers until a `E` error is encountered. - fn try_unzip(self) -> Result<(FromA, FromB), E> - where - FromA: Default + Extend, - FromB: Default + Extend, - { - let (mut from_a, mut from_b) = (FromA::default(), FromB::default()); - for res in self { - let (a, b) = res?; - from_a.extend(Some(a)); - from_b.extend(Some(b)); - } - Ok((from_a, from_b)) - } -} - -impl TryUnzip for T where T: Iterator> {} - #[cfg(test)] mod tests { use super::*; @@ -95,18 +72,4 @@ mod tests { .unwrap_err(); assert_eq!(err, "Error"); } - - #[test] - fn test_try_unzip() { - let actual: Result<(Vec<_>, Vec<_>), &'static str> = vec![Ok(("foo", 1)), Ok(("bar", 2)), Ok(("foo", 3))] - .into_iter() - .try_unzip(); - assert_eq!(actual, Ok((vec!["foo", "bar", "foo"], vec![1, 2, 3]))); - - let err = vec![Ok(("foo", 1)), Ok(("bar", 2)), Err("Error"), Ok(("foo", 3))] - .into_iter() - .try_unzip::, Vec<_>>() - .unwrap_err(); - assert_eq!(err, "Error"); - } } diff --git a/mm2src/common/executor.rs b/mm2src/common/executor.rs deleted file mode 100644 index c31f582371..0000000000 --- a/mm2src/common/executor.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::now_float; -use futures::task::{Context, Poll}; -use std::future::Future; -use std::pin::Pin; -use std::sync::{Arc, Mutex}; -use std::task::Waker; -use std::time::Duration; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -extern "C" { - /// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout - fn setTimeout(closure: &Closure, delay_ms: u32) -> i32; - - /// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearTimeout - fn clearTimeout(id: i32); -} - -pub fn spawn(future: impl Future + Send + 'static) { spawn_local(future) } - -pub fn spawn_boxed(future: Box + Send + Unpin + 'static>) { spawn_local(future) } - -pub fn spawn_local(future: impl Future + 'static) { wasm_bindgen_futures::spawn_local(future) } - -/// The timer uses [`setTimeout`] and [`clearTimeout`] for scheduling. -/// See the [example](https://rustwasm.github.io/docs/wasm-bindgen/reference/passing-rust-closures-to-js.html#heap-allocated-closures). -/// -/// According to the [blogpost](https://rustwasm.github.io/2018/10/24/multithreading-rust-and-wasm.html), -/// very few types in [`wasm_bindgen`] are `Send` and `Sync`, and [`wasm_bindgen::closure::Closure`] is not an exception. -/// Although wasm is currently single-threaded, we can implement the `Send` trait for the `Timer`, -/// but it won't be safe when wasm becomes multi-threaded. -#[must_use = "futures do nothing unless polled"] -pub struct Timer { - timeout_id: i32, - _closure: Closure, - state: Arc>, -} - -unsafe impl Send for Timer {} - -impl Timer { - pub fn till(till_utc: f64) -> Timer { - let secs = till_utc - now_float(); - Timer::sleep(secs) - } - - pub fn sleep(secs: f64) -> Timer { - let dur = Duration::from_secs_f64(secs); - let delay_ms = gstuff::duration_to_ms(dur) as u32; - Timer::sleep_ms(delay_ms) - } - - pub fn sleep_ms(delay_ms: u32) -> Timer { - fn on_timeout(state: &Arc>) { - let mut state = match state.lock() { - Ok(s) => s, - Err(e) => { - log::error!("!on_timeout: {}", e); - return; - }, - }; - state.completed = true; - if let Some(waker) = state.waker.take() { - waker.wake(); - } - } - - let state = Arc::new(Mutex::new(TimerState::default())); - let state_c = state.clone(); - // we should hold the closure until the callback function is called - let closure = Closure::new(move || on_timeout(&state_c)); - - let timeout_id = setTimeout(&closure, delay_ms); - Timer { - timeout_id, - _closure: closure, - state, - } - } -} - -/// When the `Timer` is destroyed, cancel its `setTimeout` timer. -impl Drop for Timer { - fn drop(&mut self) { clearTimeout(self.timeout_id) } -} - -impl Future for Timer { - type Output = (); - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let mut state = match self.state.lock() { - Ok(s) => s, - Err(e) => { - log::error!("!Timer::poll: {}", e); - // if the mutex is poisoned, this error will appear every poll iteration - return Poll::Ready(()); - }, - }; - if state.completed { - return Poll::Ready(()); - } - - // NB: We should get a new `Waker` on every `poll` in case the future migrates between executors. - // cf. https://rust-lang.github.io/async-book/02_execution/03_wakeups.html - state.waker = Some(cx.waker().clone()); - Poll::Pending - } -} - -#[derive(Default)] -struct TimerState { - completed: bool, - waker: Option, -} diff --git a/mm2src/common/executor/abort_on_drop.rs b/mm2src/common/executor/abort_on_drop.rs new file mode 100644 index 0000000000..e975f0ba70 --- /dev/null +++ b/mm2src/common/executor/abort_on_drop.rs @@ -0,0 +1,13 @@ +use futures::future::AbortHandle; + +/// The AbortHandle that aborts on drop +pub struct AbortOnDropHandle(AbortHandle); + +impl From for AbortOnDropHandle { + fn from(handle: AbortHandle) -> Self { AbortOnDropHandle(handle) } +} + +impl Drop for AbortOnDropHandle { + #[inline(always)] + fn drop(&mut self) { self.0.abort(); } +} diff --git a/mm2src/common/executor/abortable_system/abortable_queue.rs b/mm2src/common/executor/abortable_system/abortable_queue.rs new file mode 100644 index 0000000000..9e74181e58 --- /dev/null +++ b/mm2src/common/executor/abortable_system/abortable_queue.rs @@ -0,0 +1,354 @@ +use crate::executor::abortable_system::{AbortableSystem, AbortedError, InnerShared, InnerWeak, SystemInner}; +use crate::executor::spawner::{SpawnAbortable, SpawnFuture}; +use crate::executor::{spawn, AbortSettings, Timer}; +use crate::log::{error, LogOnError}; +use futures::channel::oneshot; +use futures::future::{abortable, select, Either}; +use futures::FutureExt; +use std::future::Future as Future03; +use std::sync::Arc; + +const CAPACITY: usize = 1024; + +type FutureId = usize; + +/// This is an `AbortableSystem` that ensures that the spawned futures will be aborted +/// once the `AbortableQueue` instance is dropped. +/// +/// `AbortableQueue` is responsible for storing future handles in `QueueInnerState` +/// and deleting them as soon as they complete. +#[derive(Debug, Default)] +pub struct AbortableQueue { + inner: InnerShared, +} + +impl AbortableQueue { + /// Returns `WeakSpawner` that will not prevent the spawned futures from being aborted. + /// This is the only way to create a `'static` instance pointing to the same `QueueInnerState` + /// that can be passed into spawned futures, since `AbortableQueue` doesn't implement `Clone`. + pub fn weak_spawner(&self) -> WeakSpawner { + WeakSpawner { + inner: Arc::downgrade(&self.inner), + } + } +} + +impl From> for AbortableQueue { + fn from(inner: InnerShared) -> Self { AbortableQueue { inner } } +} + +impl AbortableSystem for AbortableQueue { + type Inner = QueueInnerState; + + /// Aborts all spawned futures and initiates aborting of critical futures + /// after the specified [`AbortSettings::critical_timeout_s`]. + fn abort_all(&self) -> Result<(), AbortedError> { self.inner.lock().abort_all() } + + fn __push_subsystem_abort_tx(&self, subsystem_abort_tx: oneshot::Sender<()>) -> Result<(), AbortedError> { + self.inner.lock().insert_handle(subsystem_abort_tx).map(|_| ()) + } +} + +/// `WeakSpawner` doesn't prevent the spawned futures from being aborted. +/// An instance of `WeakSpawner` can be safely passed into spawned futures. +/// +/// # Important +/// +/// If corresponding `AbortableQueue` instance is dropped, [`WeakSpawner::spawn`] won't +/// actually spawn the future as it's more likely that the program, or part of the program, +/// ends its work, and there is no need to execute tasks that are no longer relevant. +#[derive(Clone)] +pub struct WeakSpawner { + inner: InnerWeak, +} + +impl WeakSpawner { + /// Spawns the `fut` future with the specified abort `settings`. + /// The future won't be executed if `AbortableQueue` is dropped. + fn spawn_with_settings_impl(&self, fut: F, settings: AbortSettings) -> Result<(), AbortedError> + where + F: Future03 + Send + 'static, + { + let (abort_tx, abort_rx) = oneshot::channel(); + let future_id = match self.inner.upgrade() { + Some(inner_arc) => { + let mut inner_guard = inner_arc.lock(); + inner_guard.insert_handle(abort_tx)? + }, + None => return Err(AbortedError), + }; + + let inner_weak = self.inner.clone(); + + let (abortable_fut, abort_handle) = abortable(fut); + + let final_fut = async move { + let critical_timeout_s = settings.critical_timeout_s; + + let wait_till_abort = async move { + // First, wait for the `abort_tx` sender (i.e. corresponding [`QueueInnerState::abort_handlers`] item) is dropped. + abort_rx.await.ok(); + + // If the `critical_timeout_s` is set, give the `fut` future to try + // to complete in `critical_timeout_s` seconds. + if let Some(critical_timeout_s) = critical_timeout_s { + Timer::sleep(critical_timeout_s).await; + } + }; + + match select(abortable_fut.boxed(), wait_till_abort.boxed()).await { + // The future has finished normally. + Either::Left(_) => { + if let Some(on_finish) = settings.on_finish { + log::log!(on_finish.level, "{}", on_finish.msg); + } + + if let Some(queue_inner) = inner_weak.upgrade() { + queue_inner.lock().on_future_finished(future_id); + } + }, + // `abort_tx` has been removed from `QueueInnerState::abort_handlers`, + // *and* the `critical_timeout_s` timeout has expired (if was specified). + Either::Right(_) => { + if let Some(on_abort) = settings.on_abort { + log::log!(on_abort.level, "{}", on_abort.msg); + } + + // Abort the input `fut`. + abort_handle.abort(); + }, + } + }; + + spawn(final_fut); + Ok(()) + } +} + +impl SpawnFuture for WeakSpawner { + /// Records a warning to the log if the corresponding `AbortableQueue` system is aborted already. + #[track_caller] + fn spawn(&self, f: F) + where + F: Future03 + Send + 'static, + { + self.spawn_with_settings_impl(f, AbortSettings::default()).warn_log() + } +} + +impl SpawnAbortable for WeakSpawner { + /// Records a warning to the log if the corresponding `AbortableQueue` system is aborted already. + #[track_caller] + fn spawn_with_settings(&self, fut: F, settings: AbortSettings) + where + F: Future03 + Send + 'static, + { + self.spawn_with_settings_impl(fut, settings).warn_log() + } +} + +/// `QueueInnerState` is the container of the spawned future handles [`oneshot::Sender<()>`]. +/// It holds the future handles, gives every future its *unique* `FutureId` identifier +/// (unique between spawned and alive futures). +/// Once a future is finished, its `FutureId` can be reassign to another future. +/// This is necessary so that this container does not grow indefinitely. +#[derive(Debug)] +pub enum QueueInnerState { + Ready { + abort_handlers: Vec>, + finished_futures: Vec, + }, + Aborted, +} + +impl Default for QueueInnerState { + fn default() -> Self { + QueueInnerState::Ready { + abort_handlers: Vec::with_capacity(CAPACITY), + finished_futures: Vec::with_capacity(CAPACITY), + } + } +} + +impl QueueInnerState { + /// Inserts the given future `handle`. + fn insert_handle(&mut self, handle: oneshot::Sender<()>) -> Result { + let (abort_handlers, finished_futures) = match self { + QueueInnerState::Ready { + abort_handlers, + finished_futures, + } => (abort_handlers, finished_futures), + QueueInnerState::Aborted => return Err(AbortedError), + }; + + match finished_futures.pop() { + // We can reuse the given `finished_id`. + Some(finished_id) if finished_id < abort_handlers.len() => { + abort_handlers[finished_id] = handle; + // The freed future ID. + return Ok(finished_id); + }, + // An invalid `FutureId` has been popped from the `finished_futures` container. + Some(invalid_finished_id) => { + error!("'The finished future ID ({invalid_finished_id}) doesn't belong to any future. Number of futures = {}", abort_handlers.len()); + }, + // There are no finished future IDs. + None => (), + } + + abort_handlers.push(handle); + // Return the last item ID. + Ok(abort_handlers.len() - 1) + } + + /// Releases the `finished_future_id` so it can be reused later on [`QueueInnerState::insert_handle`]. + fn on_future_finished(&mut self, finished_future_id: FutureId) { + if let QueueInnerState::Ready { finished_futures, .. } = self { + finished_futures.push(finished_future_id); + } + } + + #[cfg(test)] + fn count_abort_handlers(&self) -> Result { + match self { + QueueInnerState::Ready { abort_handlers, .. } => Ok(abort_handlers.len()), + QueueInnerState::Aborted => Err(AbortedError), + } + } + + #[cfg(test)] + fn count_finished_futures(&self) -> Result { + match self { + QueueInnerState::Ready { finished_futures, .. } => Ok(finished_futures.len()), + QueueInnerState::Aborted => Err(AbortedError), + } + } +} + +impl SystemInner for QueueInnerState { + fn abort_all(&mut self) -> Result<(), AbortedError> { + if matches!(self, QueueInnerState::Aborted) { + return Err(AbortedError); + } + + *self = QueueInnerState::Aborted; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::block_on; + + fn test_future_finished_impl(settings: AbortSettings) { + let abortable_system = AbortableQueue::default(); + let spawner = abortable_system.weak_spawner(); + + spawner.spawn_with_settings(async {}, settings.clone()); + block_on(Timer::sleep(0.1)); + + { + let inner = abortable_system.inner.lock(); + assert_eq!(inner.count_abort_handlers().unwrap(), 1); + // The future should have finished already. + assert_eq!(inner.count_finished_futures().unwrap(), 1); + } + + let fut1 = async { Timer::sleep(0.3).await }; + let fut2 = async { Timer::sleep(0.7).await }; + spawner.spawn_with_settings(fut1, settings.clone()); + spawner.spawn_with_settings(fut2, settings.clone()); + + { + let inner = abortable_system.inner.lock(); + // `abort_handlers` should be extended once + // because `finished_futures` contained only one freed `FutureId`. + assert_eq!(inner.count_abort_handlers().unwrap(), 2); + // `FutureId` should be used from `finished_futures` container. + assert_eq!(inner.count_finished_futures().unwrap(), 0); + } + + block_on(Timer::sleep(0.5)); + + { + let inner = abortable_system.inner.lock(); + assert_eq!(inner.count_abort_handlers().unwrap(), 2); + assert_eq!(inner.count_finished_futures().unwrap(), 1); + } + + block_on(Timer::sleep(0.4)); + + { + let inner = abortable_system.inner.lock(); + assert_eq!(inner.count_abort_handlers().unwrap(), 2); + assert_eq!(inner.count_finished_futures().unwrap(), 2); + } + } + + #[test] + fn test_critical_future_finished() { + let settings = AbortSettings::default().critical_timout_s(1.); + test_future_finished_impl(settings); + } + + #[test] + fn test_future_finished() { + let settings = AbortSettings::default(); + test_future_finished_impl(settings); + } + + #[test] + fn test_spawn_critical() { + static mut F1_FINISHED: bool = false; + static mut F2_FINISHED: bool = false; + + let abortable_system = AbortableQueue::default(); + let spawner = abortable_system.weak_spawner(); + + let settings = AbortSettings::default().critical_timout_s(0.4); + + let fut1 = async move { + Timer::sleep(0.6).await; + unsafe { F1_FINISHED = true }; + }; + spawner.spawn_with_settings(fut1, settings.clone()); + + let fut2 = async move { + Timer::sleep(0.2).await; + unsafe { F2_FINISHED = true }; + }; + spawner.spawn_with_settings(fut2, settings.clone()); + + abortable_system.abort_all().unwrap(); + + block_on(Timer::sleep(1.2)); + // `fut1` must not complete. + assert!(unsafe { !F1_FINISHED }); + // `fut` must complete. + assert!(unsafe { F2_FINISHED }); + } + + #[test] + fn test_spawn_after_abort() { + static mut F1_FINISHED: bool = false; + + for _ in 0..50 { + let abortable_system = AbortableQueue::default(); + let spawner = abortable_system.weak_spawner(); + + spawner.spawn(futures::future::ready(())); + abortable_system.abort_all().unwrap(); + + // This sleep allows to poll the `select(abortable_fut.boxed(), wait_till_abort.boxed()).await` future. + block_on(Timer::sleep(0.01)); + + spawner.spawn(async move { + unsafe { F1_FINISHED = true }; + }); + } + + // Futures spawned after `AbortableQueue::abort_all` must not complete. + assert!(unsafe { !F1_FINISHED }); + } +} diff --git a/mm2src/common/executor/abortable_system/graceful_shutdown.rs b/mm2src/common/executor/abortable_system/graceful_shutdown.rs new file mode 100644 index 0000000000..3feee076b2 --- /dev/null +++ b/mm2src/common/executor/abortable_system/graceful_shutdown.rs @@ -0,0 +1,76 @@ +use crate::executor::abortable_system::{AbortedError, InnerShared, SystemInner}; +use crate::executor::AbortableSystem; +use futures::channel::oneshot; +use futures::FutureExt; +use std::future::Future; + +/// This is an `AbortableSystem` that initiates listeners for graceful shutdown +/// once the `GracefulShutdownRegistry` instance is dropped. +/// +/// `GracefulShutdownRegistry` can be used in conjunction with the `spawn` method. +/// In some cases, the use of `GracefulShutdownRegistry` and `spawn` is justified. +/// For example, [`hyper::Server::with_graceful_shutdown`]. +#[derive(Default)] +pub struct GracefulShutdownRegistry { + inner: InnerShared, +} + +impl GracefulShutdownRegistry { + /// Registers a graceful shutdown listener and returns a future + /// that acts as a signal for graceful shutdown. + pub fn register_listener(&self) -> Result + Send + Sync + 'static, AbortedError> { + let (tx, rx) = oneshot::channel(); + self.inner.lock().insert_handle(tx)?; + Ok(rx.then(|_| futures::future::ready(()))) + } +} + +impl From> for GracefulShutdownRegistry { + fn from(inner: InnerShared) -> Self { GracefulShutdownRegistry { inner } } +} + +impl AbortableSystem for GracefulShutdownRegistry { + type Inner = ShutdownInnerState; + + fn abort_all(&self) -> Result<(), AbortedError> { self.inner.lock().abort_all() } + + fn __push_subsystem_abort_tx(&self, subsystem_abort_tx: oneshot::Sender<()>) -> Result<(), AbortedError> { + self.inner.lock().insert_handle(subsystem_abort_tx) + } +} + +pub enum ShutdownInnerState { + Ready { abort_handlers: Vec> }, + Aborted, +} + +impl Default for ShutdownInnerState { + fn default() -> Self { + ShutdownInnerState::Ready { + abort_handlers: Vec::new(), + } + } +} + +impl ShutdownInnerState { + fn insert_handle(&mut self, handle: oneshot::Sender<()>) -> Result<(), AbortedError> { + match self { + ShutdownInnerState::Ready { abort_handlers } => { + abort_handlers.push(handle); + Ok(()) + }, + ShutdownInnerState::Aborted => Err(AbortedError), + } + } +} + +impl SystemInner for ShutdownInnerState { + fn abort_all(&mut self) -> Result<(), AbortedError> { + if matches!(self, ShutdownInnerState::Aborted) { + return Err(AbortedError); + } + + *self = ShutdownInnerState::Aborted; + Ok(()) + } +} diff --git a/mm2src/common/executor/abortable_system/mod.rs b/mm2src/common/executor/abortable_system/mod.rs new file mode 100644 index 0000000000..b5399ad6dd --- /dev/null +++ b/mm2src/common/executor/abortable_system/mod.rs @@ -0,0 +1,132 @@ +use crate::executor::spawn; +use crate::log::LogOnError; +use futures::channel::oneshot; +use parking_lot::Mutex as PaMutex; +use std::fmt; +use std::sync::{Arc, Weak}; + +pub mod abortable_queue; +pub mod graceful_shutdown; +pub mod simple_map; + +pub type InnerShared = Arc>; +pub type InnerWeak = Weak>; + +#[derive(Clone, Debug, PartialEq)] +pub struct AbortedError; + +impl fmt::Display for AbortedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Abortable system has been aborted already") } +} + +pub trait AbortableSystem: From> { + type Inner: SystemInner; + + /// Aborts all spawned futures and subsystems if they present. + /// The abortable system is considered not to be + fn abort_all(&self) -> Result<(), AbortedError>; + + /// Creates a new subsystem `S` linked to `Self` the way that + /// if `Self` is aborted, the futures spawned by the subsystem will be aborted as well. + /// For more info, look at the [`tests::test_abort_subsystem`]. + /// + /// + /// But in the same time the subsystem can be aborted independently from `Self` system. + /// For more info, look at the [`tests::test_abort_supersystem`]. + fn create_subsystem(&self) -> Result + where + S: AbortableSystem, + { + let (abort_tx, abort_rx) = oneshot::channel(); + self.__push_subsystem_abort_tx(abort_tx)?; + + let inner_shared = Arc::new(PaMutex::new(S::Inner::default())); + let inner_weak = Arc::downgrade(&inner_shared); + + let abort_fut = async move { + // Once the `abort_rx` is invoked, we need to abort its all futures. + abort_rx.await.ok(); + + if let Some(inner_arc) = inner_weak.upgrade() { + inner_arc.lock().abort_all().warn_log(); + } + }; + + spawn(abort_fut); + Ok(S::from(inner_shared)) + } + + fn __push_subsystem_abort_tx(&self, subsystem_abort_tx: oneshot::Sender<()>) -> Result<(), AbortedError>; +} + +pub trait SystemInner: Default + Send + 'static { + /// Aborts all spawned futures and subsystems if they present. + fn abort_all(&mut self) -> Result<(), AbortedError>; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::block_on; + use crate::executor::{SpawnFuture, Timer}; + use abortable_queue::AbortableQueue; + + #[test] + fn test_abort_subsystem() { + static mut SUPER_FINISHED: bool = false; + static mut SUB_FINISHED: bool = false; + + let super_system = AbortableQueue::default(); + super_system.weak_spawner().spawn(async move { + Timer::sleep(0.5).await; + unsafe { SUPER_FINISHED = true }; + }); + + let sub_system: AbortableQueue = super_system.create_subsystem().unwrap(); + sub_system.weak_spawner().spawn(async move { + Timer::sleep(0.5).await; + unsafe { SUB_FINISHED = true }; + }); + + block_on(Timer::sleep(0.1)); + drop(sub_system); + block_on(Timer::sleep(0.8)); + + // Only the super system should finish as the sub system has been aborted. + unsafe { + assert!(SUPER_FINISHED); + assert!(!SUB_FINISHED); + } + } + + #[test] + fn test_abort_supersystem() { + static mut SUPER_FINISHED: bool = false; + static mut SUB_FINISHED: bool = false; + + let super_system = AbortableQueue::default(); + super_system.weak_spawner().spawn(async move { + Timer::sleep(0.5).await; + unsafe { SUPER_FINISHED = true }; + }); + + let sub_system: AbortableQueue = super_system.create_subsystem().unwrap(); + sub_system.weak_spawner().spawn(async move { + Timer::sleep(0.5).await; + unsafe { SUB_FINISHED = true }; + }); + + block_on(Timer::sleep(0.1)); + drop(super_system); + block_on(Timer::sleep(0.8)); + + // Check if the subsystem can't be aborted twice. + sub_system.abort_all().unwrap_err(); + + // Nothing should finish as the super system has been aborted. + unsafe { + assert!(!SUPER_FINISHED); + assert!(!SUB_FINISHED); + } + } +} diff --git a/mm2src/common/executor/abortable_system/simple_map.rs b/mm2src/common/executor/abortable_system/simple_map.rs new file mode 100644 index 0000000000..c7cd9fc6cd --- /dev/null +++ b/mm2src/common/executor/abortable_system/simple_map.rs @@ -0,0 +1,232 @@ +use crate::executor::abortable_system::{AbortableSystem, AbortedError, InnerShared, SystemInner}; +use crate::executor::{spawn_abortable, AbortOnDropHandle}; +use futures::channel::oneshot; +use futures::future::Future as Future03; +use parking_lot::{Mutex as PaMutex, MutexGuard as PaMutexGuard}; +use std::borrow::Borrow; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::hash::Hash; +use std::sync::Arc; + +/// An alias. +pub trait FutureIdTrait: 'static + Eq + Hash + Send {} + +impl FutureIdTrait for T {} + +/// This is a simple `AbortableSystem` that ensures that the spawned futures will be aborted +/// once the `AbortableMap` instance is dropped. +/// +/// `AbortableSet` is responsible for storing future handles in `SpawnedFuturesMap` *only*, +/// and *not* responsible for deleting them when they complete. +/// +/// `AbortableSet` allows to spawn futures by specified `FutureId`. +#[derive(Default)] +pub struct AbortableSimpleMap { + inner: Arc>>, +} + +impl AbortableSimpleMap { + /// Locks the inner `SimpleMapInner` that can be used to spawn/abort/check if contains future + /// by its `FutureId` identifier. + pub fn lock(&self) -> PaMutexGuard<'_, SimpleMapInnerState> { self.inner.lock() } +} + +impl AbortableSystem for AbortableSimpleMap { + type Inner = SimpleMapInnerState; + + fn abort_all(&self) -> Result<(), AbortedError> { self.inner.lock().abort_all() } + + fn __push_subsystem_abort_tx(&self, subsystem_abort_tx: oneshot::Sender<()>) -> Result<(), AbortedError> { + self.inner.lock().insert_subsystem(subsystem_abort_tx) + } +} + +impl From>> for AbortableSimpleMap { + fn from(inner: InnerShared>) -> Self { AbortableSimpleMap { inner } } +} + +pub enum SimpleMapInnerState { + Ready { + futures: HashMap, + subsystems: Vec>, + }, + Aborted, +} + +impl SimpleMapInnerState { + fn futures_mut(&mut self) -> Result<&mut HashMap, AbortedError> { + match self { + SimpleMapInnerState::Ready { futures, .. } => Ok(futures), + SimpleMapInnerState::Aborted => Err(AbortedError), + } + } +} + +impl Default for SimpleMapInnerState { + fn default() -> Self { + SimpleMapInnerState::Ready { + futures: HashMap::new(), + subsystems: Vec::new(), + } + } +} + +impl SystemInner for SimpleMapInnerState { + fn abort_all(&mut self) -> Result<(), AbortedError> { + if matches!(self, SimpleMapInnerState::Aborted) { + return Err(AbortedError); + } + + *self = SimpleMapInnerState::Aborted; + Ok(()) + } +} + +impl SimpleMapInnerState { + /// Spawns the `fut` future by its `future_id`, + /// or do nothing if there is a spawned future with the same `future_id` already. + /// + /// Returns whether the future has been spawned. + pub fn spawn_or_ignore(&mut self, future_id: FutureId, fut: F) -> Result + where + F: Future03 + Send + 'static, + { + let futures = self.futures_mut()?; + match futures.entry(future_id) { + Entry::Occupied(_) => Ok(false), + Entry::Vacant(entry) => { + let abort_handle = spawn_abortable(fut); + entry.insert(abort_handle); + Ok(true) + }, + } + } + + /// Whether a future with the given `future_id` has been spawned already. + pub fn contains(&self, future_id: &Q) -> Result + where + FutureId: Borrow, + Q: Hash + Eq + ?Sized, + { + match self { + SimpleMapInnerState::Ready { futures, .. } => Ok(futures.contains_key(future_id)), + SimpleMapInnerState::Aborted => Err(AbortedError), + } + } + + /// Aborts a spawned future by the given `future_id` if it's still alive. + pub fn abort_future(&mut self, future_id: &Q) -> Result + where + FutureId: Borrow, + Q: Hash + Eq + ?Sized, + { + Ok(self.futures_mut()?.remove(future_id).is_some()) + } + + fn insert_subsystem(&mut self, subsystem_abort_tx: oneshot::Sender<()>) -> Result<(), AbortedError> { + match self { + SimpleMapInnerState::Ready { subsystems, .. } => { + subsystems.push(subsystem_abort_tx); + Ok(()) + }, + SimpleMapInnerState::Aborted => Err(AbortedError), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::block_on; + use crate::executor::Timer; + + #[test] + fn test_abort_all() { + static mut F1_FINISHED: bool = false; + static mut F2_FINISHED: bool = false; + + let abortable_system = AbortableSimpleMap::default(); + let mut guard = abortable_system.lock(); + + guard + .spawn_or_ignore("F1".to_string(), async move { + Timer::sleep(0.1).await; + unsafe { F1_FINISHED = true }; + }) + .unwrap(); + assert_eq!(guard.contains("F1"), Ok(true)); + assert_eq!(guard.contains("F2"), Ok(false)); + guard + .spawn_or_ignore("F2".to_string(), async move { + Timer::sleep(0.5).await; + unsafe { F2_FINISHED = true }; + }) + .unwrap(); + + drop(guard); + block_on(Timer::sleep(0.3)); + abortable_system.abort_all().unwrap(); + block_on(Timer::sleep(0.4)); + + unsafe { + assert!(F1_FINISHED); + assert!(!F2_FINISHED); + } + } + + #[test] + fn test_abort_future() { + static mut F1_FINISHED: bool = false; + + let abortable_system = AbortableSimpleMap::default(); + let mut guard = abortable_system.lock(); + + guard + .spawn_or_ignore("F1".to_string(), async move { + Timer::sleep(0.2).await; + unsafe { F1_FINISHED = true }; + }) + .unwrap(); + + drop(guard); + block_on(Timer::sleep(0.05)); + + let mut guard = abortable_system.lock(); + guard.abort_future("F1").unwrap(); + assert_eq!(guard.contains("F1"), Ok(false)); + + block_on(Timer::sleep(0.3)); + + unsafe { + assert!(!F1_FINISHED); + } + } + + #[test] + fn test_spawn_twice() { + static mut F1_FINISHED: bool = false; + static mut F1_COPY_FINISHED: bool = false; + + let abortable_system = AbortableSimpleMap::default(); + let mut guard = abortable_system.lock(); + + let fut_1 = async move { + unsafe { F1_FINISHED = true }; + }; + guard.spawn_or_ignore("F1".to_string(), fut_1).unwrap(); + + let fut_2 = async move { + unsafe { F1_COPY_FINISHED = true }; + }; + guard.spawn_or_ignore("F1".to_string(), fut_2).unwrap(); + + drop(guard); + block_on(Timer::sleep(0.1)); + + unsafe { + assert!(F1_FINISHED); + assert!(!F1_COPY_FINISHED); + } + } +} diff --git a/mm2src/common/executor/mod.rs b/mm2src/common/executor/mod.rs new file mode 100644 index 0000000000..c11460a22e --- /dev/null +++ b/mm2src/common/executor/mod.rs @@ -0,0 +1,82 @@ +use futures::future::abortable; +use futures::{Future as Future03, FutureExt}; + +#[cfg(not(target_arch = "wasm32"))] mod native_executor; +#[cfg(not(target_arch = "wasm32"))] +pub use native_executor::{spawn, Timer}; + +mod abortable_system; +pub use abortable_system::{abortable_queue, graceful_shutdown, simple_map, AbortableSystem, AbortedError}; + +mod spawner; +pub use spawner::{BoxFutureSpawner, SpawnAbortable, SpawnFuture}; + +mod abort_on_drop; +pub use abort_on_drop::AbortOnDropHandle; + +#[cfg(target_arch = "wasm32")] mod wasm_executor; +#[cfg(target_arch = "wasm32")] +pub use wasm_executor::{spawn, spawn_local, spawn_local_abortable, Timer}; + +#[derive(Clone, Default)] +pub struct AbortSettings { + on_finish: Option, + on_abort: Option, + critical_timeout_s: Option, +} + +impl AbortSettings { + pub fn info_on_any_stop(msg: String) -> AbortSettings { + let msg = SpawnMsg { + level: log::Level::Info, + msg, + }; + AbortSettings { + on_finish: Some(msg.clone()), + on_abort: Some(msg), + critical_timeout_s: None, + } + } + + pub fn info_on_finish(msg: String) -> AbortSettings { + let msg = SpawnMsg { + level: log::Level::Info, + msg, + }; + AbortSettings { + on_finish: Some(msg), + on_abort: None, + critical_timeout_s: None, + } + } + + pub fn info_on_abort(msg: String) -> AbortSettings { + let msg = SpawnMsg { + level: log::Level::Info, + msg, + }; + AbortSettings { + on_finish: None, + on_abort: Some(msg), + critical_timeout_s: None, + } + } + + pub fn critical_timout_s(mut self, critical_timout_s: f64) -> AbortSettings { + self.critical_timeout_s = Some(critical_timout_s); + self + } +} + +#[derive(Clone)] +struct SpawnMsg { + level: log::Level, + msg: String, +} + +#[must_use] +pub fn spawn_abortable(fut: impl Future03 + Send + 'static) -> AbortOnDropHandle { + let (abortable, handle) = abortable(fut); + spawn(abortable.then(|_| futures::future::ready(()))); + AbortOnDropHandle::from(handle) +} diff --git a/mm2src/common/executor/native_executor.rs b/mm2src/common/executor/native_executor.rs index 7dfe275665..6876355b88 100644 --- a/mm2src/common/executor/native_executor.rs +++ b/mm2src/common/executor/native_executor.rs @@ -1,127 +1,57 @@ use futures::task::Context; use futures::task::Poll as Poll03; use futures::Future as Future03; -use gstuff::now_float; +use futures_timer::Delay; use std::pin::Pin; -use std::thread; use std::time::Duration; +/// # Important +/// +/// The `spawn` function must be used carefully to avoid hanging pointers. +/// Please consider using `AbortableQueue`, `AbortableSimpleMap` or `spawn_abortable` instead. pub fn spawn(future: impl Future03 + Send + 'static) { crate::wio::CORE.0.spawn(future); } -pub fn spawn_boxed(future: Box + Send + Unpin + 'static>) { spawn(future); } - -/// Schedule the given `future` to be executed shortly after the given `utc` time is reached. -pub fn spawn_after(utc: f64, future: impl Future03 + Send + 'static) { - use crossbeam::channel; - use gstuff::Constructible; - use std::collections::BTreeMap; - use std::sync::Once; - - type SheduleChannelItem = (f64, Pin + Send + 'static>>); - static START: Once = Once::new(); - static SCHEDULE: Constructible> = Constructible::new(); - START.call_once(|| { - thread::Builder::new() - .name("spawn_after".into()) - .spawn(move || { - let (tx, rx) = channel::bounded(0); - SCHEDULE.pin(tx).expect("spawn_after] Can't pin the channel"); - type Task = Pin + Send + 'static>>; - let mut tasks: BTreeMap> = BTreeMap::new(); - let mut ready = Vec::with_capacity(4); - loop { - let now = Duration::from_secs_f64(now_float()); - let mut next_stop = Duration::from_secs_f64(0.1); - for (utc, _) in tasks.iter() { - if *utc <= now { - ready.push(*utc) - } else { - next_stop = *utc - now; - break; - } - } - for utc in ready.drain(..) { - let v = match tasks.remove(&utc) { - Some(v) => v, - None => continue, - }; - //log! ("spawn_after] spawning " (v.len()) " tasks at " [utc]); - for f in v { - spawn(f) - } - } - let (utc, f) = match rx.recv_timeout(next_stop) { - Ok(t) => t, - Err(channel::RecvTimeoutError::Disconnected) => break, - Err(channel::RecvTimeoutError::Timeout) => continue, - }; - tasks - .entry(Duration::from_secs_f64(utc)) - .or_insert_with(Vec::new) - .push(f) - } - }) - .expect("Can't spawn a spawn_after thread"); - }); - loop { - match SCHEDULE.as_option() { - None => { - thread::yield_now(); - continue; - }, - Some(tx) => { - tx.send((utc, Box::pin(future))).expect("Can't reach spawn_after"); - break; - }, - } - } -} - -/// A future that completes at a given time. +/// A future that completes at a given time. +#[must_use] pub struct Timer { - till_utc: f64, + delay: Delay, } impl Timer { - pub fn till(till_utc: f64) -> Timer { Timer { till_utc } } pub fn sleep(seconds: f64) -> Timer { Timer { - till_utc: now_float() + seconds, + delay: Delay::new(Duration::from_secs_f64(seconds)), } } - pub fn sleep_ms(ms: u32) -> Timer { - let seconds = gstuff::duration_to_float(Duration::from_millis(ms as u64)); + + pub fn sleep_ms(ms: u64) -> Timer { Timer { - till_utc: now_float() + seconds, + delay: Delay::new(Duration::from_millis(ms)), } } - pub fn till_utc(&self) -> f64 { self.till_utc } } impl Future03 for Timer { type Output = (); - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll03 { - let delta = self.till_utc - now_float(); - if delta <= 0. { - return Poll03::Ready(()); - } - // NB: We should get a new `Waker` on every `poll` in case the future migrates between executors. - // cf. https://rust-lang.github.io/async-book/02_execution/03_wakeups.html - let waker = cx.waker().clone(); - spawn_after(self.till_utc, async move { waker.wake() }); - Poll03::Pending - } + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll03 { Pin::new(&mut self.delay).poll(cx) } } -#[test] -fn test_timer() { - let started = now_float(); - let ti = Timer::sleep(0.2); - let delta = now_float() - started; - assert!(delta < 0.04, "{}", delta); - super::block_on(ti); - let delta = now_float() - started; - println!("time delta is {}", delta); - assert!(delta > 0.2); - assert!(delta < 0.4) +#[cfg(test)] +mod tests { + use super::*; + use crate::now_float; + + #[test] + fn test_timer() { + let started = now_float(); + let ti = Timer::sleep(0.2); + let delta = now_float() - started; + assert!(delta < 0.04, "{}", delta); + crate::block_on(ti); + let delta = now_float() - started; + println!("time delta is {}", delta); + assert!(delta > 0.2); + assert!(delta < 0.4) + } } diff --git a/mm2src/common/executor/spawner.rs b/mm2src/common/executor/spawner.rs new file mode 100644 index 0000000000..169c97ce14 --- /dev/null +++ b/mm2src/common/executor/spawner.rs @@ -0,0 +1,24 @@ +use crate::executor::AbortSettings; +use futures::Future as Future03; + +pub trait BoxFutureSpawner { + fn spawn_boxed(&self, f: Box + Send + Unpin + 'static>); +} + +impl BoxFutureSpawner for S { + fn spawn_boxed(&self, f: Box + Send + Unpin + 'static>) { self.spawn(f) } +} + +pub trait SpawnFuture { + /// Spawns the given `f` future. + fn spawn(&self, f: F) + where + F: Future03 + Send + 'static; +} + +pub trait SpawnAbortable { + /// Spawns the `fut` future with the specified abort `settings`. + fn spawn_with_settings(&self, fut: F, settings: AbortSettings) + where + F: Future03 + Send + 'static; +} diff --git a/mm2src/common/executor/wasm_executor.rs b/mm2src/common/executor/wasm_executor.rs index c31f582371..422f564623 100644 --- a/mm2src/common/executor/wasm_executor.rs +++ b/mm2src/common/executor/wasm_executor.rs @@ -1,4 +1,7 @@ +use crate::executor::AbortOnDropHandle; use crate::now_float; +use crate::number_type_casting::SafeTypeCastingNumbers; +use futures::future::{abortable, FutureExt}; use futures::task::{Context, Poll}; use std::future::Future; use std::pin::Pin; @@ -9,19 +12,31 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { - /// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout + // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout fn setTimeout(closure: &Closure, delay_ms: u32) -> i32; - /// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearTimeout + // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearTimeout fn clearTimeout(id: i32); } +/// # Important +/// +/// The `spawn` function must be used carefully to avoid hanging pointers. +/// Please consider using `AbortableQueue`, `AbortableSimpleMap` or `spawn_abortable` instead. pub fn spawn(future: impl Future + Send + 'static) { spawn_local(future) } -pub fn spawn_boxed(future: Box + Send + Unpin + 'static>) { spawn_local(future) } - +/// # Important +/// +/// The `spawn` function must be used carefully to avoid hanging pointers. +/// Please consider using `AbortableQueue`, `AbortableSimpleMap` or `spawn_abortable` instead. pub fn spawn_local(future: impl Future + 'static) { wasm_bindgen_futures::spawn_local(future) } +pub fn spawn_local_abortable(future: impl Future + 'static) -> AbortOnDropHandle { + let (abortable, handle) = abortable(future); + spawn_local(abortable.then(|_| futures::future::ready(()))); + AbortOnDropHandle::from(handle) +} + /// The timer uses [`setTimeout`] and [`clearTimeout`] for scheduling. /// See the [example](https://rustwasm.github.io/docs/wasm-bindgen/reference/passing-rust-closures-to-js.html#heap-allocated-closures). /// @@ -46,11 +61,11 @@ impl Timer { pub fn sleep(secs: f64) -> Timer { let dur = Duration::from_secs_f64(secs); - let delay_ms = gstuff::duration_to_ms(dur) as u32; + let delay_ms = gstuff::duration_to_ms(dur); Timer::sleep_ms(delay_ms) } - pub fn sleep_ms(delay_ms: u32) -> Timer { + pub fn sleep_ms(delay_ms: u64) -> Timer { fn on_timeout(state: &Arc>) { let mut state = match state.lock() { Ok(s) => s, @@ -70,7 +85,7 @@ impl Timer { // we should hold the closure until the callback function is called let closure = Closure::new(move || on_timeout(&state_c)); - let timeout_id = setTimeout(&closure, delay_ms); + let timeout_id = setTimeout(&closure, delay_ms.into_or_max()); Timer { timeout_id, _closure: closure, diff --git a/mm2src/common/fmt.rs b/mm2src/common/fmt.rs new file mode 100644 index 0000000000..46a25a421f --- /dev/null +++ b/mm2src/common/fmt.rs @@ -0,0 +1,68 @@ +use std::fmt; + +/// An alternative to the `write!` standard library macro that can never fail. +/// The macro takes a `WriteSafe` writer as a `$dst` destination. +#[macro_export] +macro_rules! write_safe { + ($dst:expr, $($arg:tt)*) => { + $dst.write_safe(format_args!($($arg)*)) + } +} + +/// The trait is implemented for those types for those [`std::fmt::Write::write_fmt`] never fails. +pub trait WriteSafe: fmt::Write { + fn write_safe(&mut self, args: fmt::Arguments<'_>) { + fmt::Write::write_fmt(self, args).expect("`write_fmt` should never fail for `WriteSafe` types") + } +} + +impl WriteSafe for String {} + +pub trait WriteJoin: Iterator + Sized +where + Self::Item: fmt::Display, +{ + fn write_join(mut self, writer: &mut W, sep: &str) -> fmt::Result + where + W: fmt::Write, + { + if let Some(item) = self.next() { + write!(writer, "{}", item)?; + } + for item in self { + write!(writer, "{}{}", sep, item)?; + } + Ok(()) + } +} + +impl WriteJoin for I +where + I: Iterator + Sized, + T: fmt::Display, +{ +} + +pub trait WriteSafeJoin: Iterator + Sized +where + Self::Item: fmt::Display, +{ + fn write_safe_join(mut self, writer: &mut W, sep: &str) + where + W: WriteSafe, + { + if let Some(item) = self.next() { + write_safe!(writer, "{}", item); + } + for item in self { + write_safe!(writer, "{}{}", sep, item); + } + } +} + +impl WriteSafeJoin for I +where + I: Iterator + Sized, + T: fmt::Display, +{ +} diff --git a/mm2src/common/jsonrpc_client.rs b/mm2src/common/jsonrpc_client.rs index 63f2ea0a55..94a1ca809b 100644 --- a/mm2src/common/jsonrpc_client.rs +++ b/mm2src/common/jsonrpc_client.rs @@ -48,7 +48,7 @@ macro_rules! rpc_req { } pub type JsonRpcResponseFut = - Box + Send + 'static>; + Box + Send + 'static>; pub type RpcRes = Box + Send + 'static>; /// Address of server from which an Rpc response was received @@ -237,6 +237,16 @@ impl fmt::Display for JsonRpcError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) } } +impl JsonRpcError { + pub fn new(client_info: String, request: JsonRpcRequestEnum, error: JsonRpcErrorType) -> Self { + Self { + client_info, + request, + error, + } + } +} + #[derive(Clone, Debug)] pub enum JsonRpcErrorType { /// Invalid outgoing request error @@ -247,9 +257,12 @@ pub enum JsonRpcErrorType { Parse(JsonRpcRemoteAddr, String), /// The JSON-RPC error returned from server Response(JsonRpcRemoteAddr, Json), + Internal(String), } impl JsonRpcErrorType { + pub fn parse_error(remote_addr: &str, err: String) -> Self { Self::Parse(remote_addr.to_string().into(), err) } + /// Whether the error type is [`JsonRpcErrorType::Transport`]. #[inline] pub fn is_transport(&self) -> bool { matches!(self, JsonRpcErrorType::Transport(_)) } @@ -273,9 +286,11 @@ pub trait JsonRpcClient { /// Sends the given single `request` to the remote and tries to decode the response into `T`. fn send_request(&self, request: JsonRpcRequest) -> RpcRes { let client_info = self.client_info(); + let request = JsonRpcRequestEnum::Single(request); Box::new( - self.transport(JsonRpcRequestEnum::Single(request.clone())) - .then(move |result| process_transport_single_result(result, client_info, request)), + self.transport(request.clone()) + .then(move |result| process_transport_single_result(result)) + .map_err(|e| JsonRpcError::new(client_info, request, e)), ) } } @@ -301,9 +316,11 @@ pub trait JsonRpcBatchClient: JsonRpcClient { fn send_batch_request(&self, request: JsonRpcBatchRequest) -> RpcRes> { try_fu!(self.validate_batch_request(&request)); let client_info = self.client_info(); + let batch_request = JsonRpcRequestEnum::Batch(request.clone()); Box::new( - self.transport(JsonRpcRequestEnum::Batch(request.clone())) - .then(move |result| process_transport_batch_result(result, client_info, request)), + self.transport(batch_request.clone()) + .then(move |result| process_transport_batch_result(result, request)) + .map_err(|e| JsonRpcError::new(client_info, batch_request, e)), ) } @@ -334,9 +351,11 @@ pub trait JsonRpcMultiClient: JsonRpcClient { request: JsonRpcRequest, ) -> RpcRes { let client_info = self.client_info(); + let request = JsonRpcRequestEnum::Single(request); Box::new( - self.transport_exact(to_addr.to_owned(), JsonRpcRequestEnum::Single(request.clone())) - .then(move |result| process_transport_single_result(result, client_info, request)), + self.transport_exact(to_addr.to_owned(), request.clone()) + .then(move |result| process_transport_single_result(result)) + .map_err(|e| JsonRpcError::new(client_info, request, e)), ) } } @@ -344,75 +363,49 @@ pub trait JsonRpcMultiClient: JsonRpcClient { /// Checks if the given `result` is success and contains `JsonRpcResponse`. /// Tries to decode the batch response into `T`. fn process_transport_single_result( - result: Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String>, - client_info: String, - request: JsonRpcRequest, -) -> Result { - let request = JsonRpcRequestEnum::Single(request); - + result: Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), JsonRpcErrorType>, +) -> Result { match result { - Ok((remote_addr, JsonRpcResponseEnum::Single(single))) => { - process_single_response(client_info, remote_addr, request, single) - }, - Ok((remote_addr, JsonRpcResponseEnum::Batch(batch))) => { - let error = ERRL!("Expeced single response, found batch response: {:?}", batch); - Err(JsonRpcError { - client_info, - request, - error: JsonRpcErrorType::Parse(remote_addr, error), - }) - }, - Err(e) => Err(JsonRpcError { - client_info, - request, - error: JsonRpcErrorType::Transport(e), - }), + Ok((remote_addr, JsonRpcResponseEnum::Single(single))) => process_single_response(remote_addr, single), + Ok((remote_addr, JsonRpcResponseEnum::Batch(batch))) => Err(JsonRpcErrorType::Parse( + remote_addr, + ERRL!("Expected single response, found batch response: {:?}", batch), + )), + Err(e) => Err(e), } } /// Checks if the given `result` is success and contains `JsonRpcBatchResponse`. /// Tries to decode the batch response into `Vec` in the same order in which they were requested. fn process_transport_batch_result( - result: Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String>, - client_info: String, + result: Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), JsonRpcErrorType>, request: JsonRpcBatchRequest, -) -> Result, JsonRpcError> { +) -> Result, JsonRpcErrorType> { let orig_ids: Vec<_> = request.orig_sequence_ids().collect(); - let request = JsonRpcRequestEnum::Batch(request); let (remote_addr, batch) = match result { Ok((remote_addr, JsonRpcResponseEnum::Batch(batch))) => (remote_addr, batch), Ok((remote_addr, JsonRpcResponseEnum::Single(single))) => { - let error = ERRL!("Expected batch response, found single response: {:?}", single); - return Err(JsonRpcError { - client_info, - request, - error: JsonRpcErrorType::Parse(remote_addr, error), - }); - }, - Err(e) => { - return Err(JsonRpcError { - client_info, - request, - error: JsonRpcErrorType::Transport(e), - }) + return Err(JsonRpcErrorType::Parse( + remote_addr, + ERRL!("Expected batch response, found single response: {:?}", single), + )); }, + Err(e) => return Err(e), }; // Turn the vector of responses into a hashmap by their IDs to get quick access to the content of the responses. let mut response_map: HashMap = batch.into_iter().map(|res| (res.id.clone(), res)).collect(); if response_map.len() != orig_ids.len() { - let error = ERRL!( - "Expected '{}' elements in batch response, found '{}'", - orig_ids.len(), - response_map.len() - ); - return Err(JsonRpcError { - client_info, - request, - error: JsonRpcErrorType::Parse(remote_addr, error), - }); + return Err(JsonRpcErrorType::Parse( + remote_addr, + ERRL!( + "Expected '{}' elements in batch response, found '{}'", + orig_ids.len(), + response_map.len() + ), + )); } let mut result = Vec::with_capacity(orig_ids.len()); @@ -420,21 +413,14 @@ fn process_transport_batch_result( let single_resp = match response_map.remove(id) { Some(res) => res, None => { - let error = ERRL!("Batch response doesn't contain '{}' identifier", id); - return Err(JsonRpcError { - client_info, - request, - error: JsonRpcErrorType::Parse(remote_addr, error), - }); + return Err(JsonRpcErrorType::Parse( + remote_addr, + ERRL!("Batch response doesn't contain '{}' identifier", id), + )); }, }; - result.push(process_single_response( - client_info.clone(), - remote_addr.clone(), - request.clone(), - single_resp, - )?); + result.push(process_single_response(remote_addr.clone(), single_resp)?); } Ok(result) } @@ -442,25 +428,17 @@ fn process_transport_batch_result( /// Tries to decode the given single `response` into `T` if it doesn't contain an error, /// otherwise returns `JsonRpcError`. fn process_single_response( - client_info: String, remote_addr: JsonRpcRemoteAddr, - request: JsonRpcRequestEnum, response: JsonRpcResponse, -) -> Result { +) -> Result { if !response.error.is_null() { - return Err(JsonRpcError { - client_info, - request, - error: JsonRpcErrorType::Response(remote_addr, response.error), - }); + return Err(JsonRpcErrorType::Response(remote_addr, response.error)); } - json::from_value(response.result.clone()).map_err(|e| JsonRpcError { - client_info, - request, - error: JsonRpcErrorType::Parse( + json::from_value(response.result.clone()).map_err(|e| { + JsonRpcErrorType::Parse( remote_addr, ERRL!("error {:?} parsing result from response {:?}", e, response), - ), + ) }) } diff --git a/mm2src/common/log.rs b/mm2src/common/log.rs index 4c3f9853fb..733d0e8f83 100644 --- a/mm2src/common/log.rs +++ b/mm2src/common/log.rs @@ -1,7 +1,7 @@ //! Human-readable logging and statuses. -use super::executor::{spawn, Timer}; use super::{now_ms, writeln}; +use crate::executor::{spawn_abortable, AbortOnDropHandle, Timer}; use crate::filename; use chrono::format::strftime::StrftimeItems; use chrono::format::DelayedFormat; @@ -155,6 +155,7 @@ pub fn short_log_time(ms: u64) -> DelayedFormat> { // NB: Given that the debugging logs are targeted at the developers and not the users // I think it's better to output the time in GMT here // in order for the developers to more easily match the events between the various parts of the peer-to-peer system. + #[allow(deprecated)] let time = Utc.timestamp_millis(ms as i64); time.format("%d %H:%M:%S") } @@ -464,6 +465,7 @@ impl Default for LogEntry { impl LogEntry { pub fn format(&self, buf: &mut String) -> Result<(), fmt::Error> { + #[allow(deprecated)] let time = Local.timestamp_millis(self.time as i64); let time_formatted = time.format("%Y-%m-%d %H:%M:%S %z"); let emotion = if self.emotion.is_empty() { "·" } else { &self.emotion }; @@ -601,6 +603,8 @@ pub struct LogState { gravity: PaMutex>>, /// Keeps track of the log level that the log state is initiated with level: LogLevel, + /// `log_dashboard_sometimes` abort handle if it has been spawned. + _dashboard_abort_handle: Option, } #[derive(Clone)] @@ -608,7 +612,7 @@ pub struct LogArc(pub Arc); impl Deref for LogArc { type Target = LogState; - fn deref(&self) -> &LogState { &*self.0 } + fn deref(&self) -> &LogState { &self.0 } } impl LogArc { @@ -730,20 +734,21 @@ impl LogState { tail: Arc::new(PaMutex::new(VecDeque::with_capacity(64))), gravity: PaMutex::new(None), level: LogLevel::default(), + _dashboard_abort_handle: None, } } /// Initialize according to the MM command-line configuration. pub fn mm(_conf: &Json) -> LogState { let dashboard = Arc::new(PaMutex::new(Vec::new())); - - spawn(log_dashboard_sometimes(Arc::downgrade(&dashboard))); + let abort_handle = spawn_abortable(log_dashboard_sometimes(Arc::downgrade(&dashboard))); LogState { dashboard, tail: Arc::new(PaMutex::new(VecDeque::with_capacity(64))), gravity: PaMutex::new(None), level: LogLevel::default(), + _dashboard_abort_handle: Some(abort_handle), } } @@ -769,7 +774,7 @@ impl LogState { pub fn with_tail(&self, cb: &mut dyn FnMut(&VecDeque)) { let tail = self.tail.lock(); - cb(&*tail) + cb(&tail) } pub fn with_gravity_tail(&self, cb: &mut dyn FnMut(&VecDeque)) { @@ -777,7 +782,7 @@ impl LogState { if let Some(ref gravity) = gravity { gravity.flush(); let tail = gravity.tail.lock(); - cb(&*tail); + cb(&tail); } } diff --git a/mm2src/common/log/wasm_log.rs b/mm2src/common/log/wasm_log.rs index ce74fb06ef..3d7f83a16b 100644 --- a/mm2src/common/log/wasm_log.rs +++ b/mm2src/common/log/wasm_log.rs @@ -101,6 +101,10 @@ impl WasmCallback { } } }; + + // There is no async operations within the loop except reading from the `rx` receiver. + // So we're sure that once the `tx` sender is dropped, the loop will be stopped immediately. + // Please note that `WasmCallBack` is set on MarketMaker start/restart. spawn_local(fut); WasmCallback { tx } } @@ -115,6 +119,10 @@ impl WasmCallback { console::log_1(&msg_js); } }; + + // The future can be spawned safely since it doesn't hold any shared pointer, + // and will be stopped immediately once `WasmCallback` is dropped. + // Please note that `WasmCallBack` is set on MarketMaker start/restart. spawn_local(fut); WasmCallback { tx } } diff --git a/mm2src/common/mm_metrics/mod.rs b/mm2src/common/mm_metrics/mod.rs deleted file mode 100644 index 6c84d4de48..0000000000 --- a/mm2src/common/mm_metrics/mod.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::log::LogWeak; -use serde_json::{Value as Json, Value}; -use std::collections::HashMap; -use std::sync::{Arc, Weak}; - -#[cfg(not(target_arch = "wasm32"))] mod native; -#[cfg(not(target_arch = "wasm32"))] -pub use native::{prometheus, Clock, Metrics, TrySink}; - -#[cfg(target_arch = "wasm32")] mod wasm; -#[cfg(target_arch = "wasm32")] pub use wasm::{Clock, Metrics}; - -pub trait MetricsOps { - /// If the instance was not initialized yet, create the `receiver` else return an error. - fn init(&self) -> Result<(), String>; - - /// Create new Metrics instance and spawn the metrics recording into the log, else return an error. - fn init_with_dashboard(&self, log_state: LogWeak, record_interval: f64) -> Result<(), String>; - - /// Handle for sending metric samples. - fn clock(&self) -> Result; - - /// Collect the metrics as Json. - fn collect_json(&self) -> Result; -} - -pub trait ClockOps { - fn now(&self) -> u64; -} - -#[derive(Clone, Default)] -pub struct MetricsArc(pub Arc); - -impl MetricsOps for MetricsArc { - fn init(&self) -> Result<(), String> { self.0.init() } - - fn init_with_dashboard(&self, log_state: LogWeak, record_interval: f64) -> Result<(), String> { - self.0.init_with_dashboard(log_state, record_interval) - } - - fn clock(&self) -> Result { self.0.clock() } - - fn collect_json(&self) -> Result { self.0.collect_json() } -} - -impl MetricsArc { - /// Create new `Metrics` instance - pub fn new() -> MetricsArc { MetricsArc(Arc::new(Default::default())) } - - /// Try to obtain the `Metrics` from the weak pointer. - pub fn from_weak(weak: &MetricsWeak) -> Option { weak.0.upgrade().map(MetricsArc) } - - /// Create a weak pointer from `MetricsWeak`. - pub fn weak(&self) -> MetricsWeak { MetricsWeak(Arc::downgrade(&self.0)) } -} - -#[derive(Clone, Default)] -pub struct MetricsWeak(pub Weak); - -impl MetricsWeak { - /// Create a default MmWeak without allocating any memory. - pub fn new() -> MetricsWeak { MetricsWeak::default() } - - pub fn dropped(&self) -> bool { self.0.strong_count() == 0 } -} - -#[derive(Serialize, Debug, Default, Deserialize)] -pub struct MetricsJson { - pub metrics: Vec, -} - -#[derive(Eq, Debug, Deserialize, PartialEq, Serialize)] -#[serde(rename_all = "lowercase")] -#[serde(tag = "type")] -pub enum MetricType { - Counter { - key: String, - labels: HashMap, - value: u64, - }, - Gauge { - key: String, - labels: HashMap, - value: i64, - }, - Histogram { - key: String, - labels: HashMap, - #[serde(flatten)] - quantiles: HashMap, - }, -} diff --git a/mm2src/common/mm_metrics/native.rs b/mm2src/common/mm_metrics/native.rs deleted file mode 100644 index fafa3beab0..0000000000 --- a/mm2src/common/mm_metrics/native.rs +++ /dev/null @@ -1,756 +0,0 @@ -use super::*; -use crate::executor::{spawn, Timer}; -use crate::log::error; -use gstuff::Constructible; -use hdrhistogram::Histogram; -use itertools::Itertools; -use metrics_core::{Builder, Drain, Key, Label, Observe, Observer, ScopedString}; -use metrics_runtime::{observers::PrometheusBuilder, Receiver}; -use metrics_util::{parse_quantiles, Quantile}; -use serde_json as json; -use std::collections::HashMap; -use std::fmt; -use std::slice::Iter; - -use crate::log::{LogArc, Tag}; -pub use metrics_runtime::Sink; - -/// Increment counter if an MmArc is not dropped yet and metrics system is initialized already. -#[macro_export] -macro_rules! mm_counter { - ($metrics:expr, $name:expr, $value:expr) => {{ - if let Some(mut sink) = $crate::mm_metrics::TrySink::try_sink(&$metrics) { - sink.increment_counter($name, $value); - } - }}; - ($metrics:expr, $name:expr, $value:expr, $($label_key:expr => $label_val:expr),+) => {{ - use metrics::labels; - if let Some(mut sink) = $crate::mm_metrics::TrySink::try_sink(&$metrics) { - let labels = labels!( $($label_key => $label_val),+ ); - sink.increment_counter_with_labels($name, $value, labels); - } - }}; -} - -/// Update gauge if an MmArc is not dropped yet and metrics system is initialized already. -#[macro_export] -macro_rules! mm_gauge { - ($metrics:expr, $name:expr, $value:expr) => {{ - if let Some(mut sink) = $crate::mm_metrics::TrySink::try_sink(&$metrics) { - sink.update_gauge($name, $value); - } - }}; - - ($metrics:expr, $name:expr, $value:expr, $($label_key:expr => $label_val:expr),+) => {{ - use metrics::labels; - if let Some(mut sink) = $crate::mm_metrics::TrySink::try_sink(&$metrics) { - let labels = labels!( $($label_key => $label_val),+ ); - sink.update_gauge_with_labels($name, $value, labels); - } - }}; -} - -/// Pass new timing value if an MmArc is not dropped yet and metrics system is initialized already. -#[macro_export] -macro_rules! mm_timing { - ($metrics:expr, $name:expr, $start:expr, $end:expr) => {{ - if let Some(mut sink) = $crate::mm_metrics::TrySink::try_sink(&$metrics) { - sink.record_timing($name, $start, $end); - } - }}; - - ($metrics:expr, $name:expr, $start:expr, $end:expr, $($label_key:expr => $label_val:expr),+) => {{ - use metrics::labels; - if let Some(mut sink) = $crate::mm_metrics::TrySink::try_sink(&$metrics) { - let labels = labels!( $($label_key => $label_val),+ ); - sink.record_timing_with_labels($name, $start, $end, labels); - } - }}; -} - -/// Default quantiles are "min" and "max" -const QUANTILES: &[f64] = &[0.0, 1.0]; - -pub trait TrySink { - fn try_sink(&self) -> Option; -} - -impl TrySink for MetricsArc { - fn try_sink(&self) -> Option { self.0.sink().ok() } -} - -impl TrySink for MetricsWeak { - fn try_sink(&self) -> Option { - let metrics = MetricsArc::from_weak(self)?; - metrics.0.sink().ok() - } -} - -pub struct Clock { - sink: Sink, -} - -impl From for Clock { - fn from(sink: Sink) -> Self { Clock { sink } } -} - -impl ClockOps for Clock { - fn now(&self) -> u64 { self.sink.now() } -} - -#[derive(Default)] -pub struct Metrics { - /// `Receiver` receives and collect all the metrics sent through the `sink`. - /// The `receiver` can be initialized only once time. - receiver: Constructible, -} - -impl MetricsOps for Metrics { - fn init(&self) -> Result<(), String> { - if self.receiver.is_some() { - return ERR!("metrics system is initialized already"); - } - - let receiver = try_s!(Receiver::builder().build()); - let _ = try_s!(self.receiver.pin(receiver)); - - Ok(()) - } - - fn init_with_dashboard(&self, log_state: LogWeak, record_interval: f64) -> Result<(), String> { - self.init()?; - - let controller = self.receiver.as_option().unwrap().controller(); - - let observer = TagObserver::new(QUANTILES); - let exporter = TagExporter { - log_state, - controller, - observer, - }; - - spawn(exporter.run(record_interval)); - - Ok(()) - } - - fn clock(&self) -> Result { self.sink().map_err(|e| ERRL!("{}", e)).map(Clock::from) } - - fn collect_json(&self) -> Result { - let receiver = try_s!(self.try_receiver()); - let controller = receiver.controller(); - - let mut observer = JsonObserver::new(QUANTILES); - - controller.observe(&mut observer); - - observer.into_json() - } -} - -impl Metrics { - /// Try get receiver. - fn try_receiver(&self) -> Result<&Receiver, String> { - self.receiver.ok_or("metrics system is not initialized yet".into()) - } - - fn sink(&self) -> Result { Ok(try_s!(self.try_receiver()).sink()) } - - /// Collect the metrics in Prometheus format. - pub fn collect_prometheus_format(&self) -> Result { - let receiver = try_s!(self.try_receiver()); - let controller = receiver.controller(); - - let mut observer = PrometheusBuilder::new().set_quantiles(QUANTILES).build(); - controller.observe(&mut observer); - - Ok(observer.drain()) - } -} - -type MetricName = ScopedString; - -type MetricLabels = Vec