From 3d80b3a8182ae55e8fc9609b2364dc24c56f7c37 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 22 Dec 2023 04:14:24 +0100 Subject: [PATCH] feat(zcoin): implement zcoin/pirate transport layer for WASM (#1996) This commit implements a zcoin transport layer that adheres to a common interface shared with the native transport. Additionally, It introduces some helper functions within FetchRequest and performs some refactoring in the mm2_net module. zcash params fetching and saving to indexedDB is also part of this commit. --- Cargo.lock | 274 ++++++------ mm2src/coins/Cargo.toml | 8 +- .../eth/web3_transport/http_transport.rs | 2 +- mm2src/coins/lp_price.rs | 2 +- mm2src/coins/nft.rs | 2 +- mm2src/coins/nft/nft_tests.rs | 2 +- .../tendermint/rpc/tendermint_wasm_rpc.rs | 2 +- mm2src/coins/test_coin.rs | 4 +- mm2src/coins/utxo/rpc_clients.rs | 2 +- mm2src/coins/z_coin.rs | 214 ++++++--- .../storage/blockdb/blockdb_idb_storage.rs | 37 +- mm2src/coins/z_coin/storage/blockdb/mod.rs | 13 + .../storage/walletdb/wallet_sql_storage.rs | 8 +- .../z_coin/storage/walletdb/wasm/storage.rs | 6 +- mm2src/coins/z_coin/z_coin_errors.rs | 34 +- mm2src/coins/z_coin/z_params/indexeddb.rs | 184 ++++++++ mm2src/coins/z_coin/z_params/mod.rs | 86 ++++ mm2src/coins/z_coin/z_rpc.rs | 265 ++++++----- mm2src/coins_activation/src/context.rs | 3 - mm2src/coins_activation/src/lib.rs | 2 +- mm2src/coins_activation/src/prelude.rs | 2 - mm2src/common/common.rs | 2 + mm2src/crypto/src/hw_rpc_task.rs | 3 +- mm2src/mm2_db/src/indexed_db/indexed_db.rs | 3 +- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 10 +- mm2src/mm2_main/src/wasm_tests.rs | 32 +- .../tests/integration_tests_common/mod.rs | 34 +- .../tests/mm2_tests/orderbook_sync_tests.rs | 8 +- .../mm2_main/tests/mm2_tests/z_coin_tests.rs | 12 +- mm2src/mm2_net/Cargo.toml | 17 +- mm2src/mm2_net/src/grpc_web.rs | 13 +- mm2src/mm2_net/src/lib.rs | 3 +- mm2src/mm2_net/src/transport.rs | 2 +- mm2src/mm2_net/src/wasm/body_stream.rs | 414 ++++++++++++++++++ .../src/{wasm_http.rs => wasm/http.rs} | 92 +++- mm2src/mm2_net/src/wasm/mod.rs | 4 + mm2src/mm2_net/src/wasm/tonic_client.rs | 53 +++ mm2src/mm2_net/src/{ => wasm}/wasm_ws.rs | 0 mm2src/mm2_test_helpers/src/for_tests.rs | 41 +- 39 files changed, 1423 insertions(+), 472 deletions(-) create mode 100644 mm2src/coins/z_coin/z_params/indexeddb.rs create mode 100644 mm2src/coins/z_coin/z_params/mod.rs create mode 100644 mm2src/mm2_net/src/wasm/body_stream.rs rename mm2src/mm2_net/src/{wasm_http.rs => wasm/http.rs} (76%) create mode 100644 mm2src/mm2_net/src/wasm/mod.rs create mode 100644 mm2src/mm2_net/src/wasm/tonic_client.rs rename mm2src/mm2_net/src/{ => wasm}/wasm_ws.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index a67177b835..d5251ac69d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,8 +237,8 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -254,8 +254,8 @@ version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -700,7 +700,7 @@ dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", "proc-macro-crate 0.1.5", - "proc-macro2 1.0.63", + "proc-macro2 1.0.69", "syn 1.0.95", ] @@ -710,8 +710,8 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -721,8 +721,8 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -787,8 +787,8 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -1000,6 +1000,7 @@ dependencies = [ "bitcoin", "bitcoin_hashes", "bitcrypto", + "blake2b_simd", "byteorder", "bytes 0.4.12", "cfg-if 1.0.0", @@ -1094,6 +1095,7 @@ dependencies = [ "tokio-tungstenite-wasm", "tonic", "tonic-build", + "tower-service", "url", "utxo_signer", "uuid 1.2.2", @@ -1430,7 +1432,7 @@ dependencies = [ "crossbeam-utils 0.7.2", "lazy_static", "maybe-uninit", - "memoffset 0.5.4", + "memoffset 0.5.6", "scopeguard", ] @@ -1680,8 +1682,8 @@ dependencies = [ "cc", "codespan-reporting", "lazy_static", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "scratch", "syn 1.0.95", ] @@ -1698,8 +1700,8 @@ version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -1795,8 +1797,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -1806,8 +1808,8 @@ version = "0.99.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -2060,8 +2062,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ "heck", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -2072,7 +2074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c375b9c5eadb68d0a6efee2999fef292f45854c3444c86f09d8ab086ba942b0e" dependencies = [ "num-traits", - "quote 1.0.28", + "quote 1.0.33", "syn 1.0.95", ] @@ -2081,8 +2083,8 @@ name = "enum_from" version = "0.1.0" dependencies = [ "itertools", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -2247,8 +2249,8 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", "synstructure", ] @@ -2515,9 +2517,9 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.32", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -3156,8 +3158,8 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -3744,9 +3746,9 @@ source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15d dependencies = [ "heck", "proc-macro-warning", - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.32", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -4112,9 +4114,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" dependencies = [ "autocfg 1.1.0", ] @@ -4174,9 +4176,9 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.32", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -4547,6 +4549,8 @@ version = "0.1.0" dependencies = [ "async-stream", "async-trait", + "base64 0.21.2", + "byteorder", "bytes 1.4.0", "cfg-if 1.0.0", "common", @@ -4556,6 +4560,8 @@ dependencies = [ "futures-util", "gstuff", "http 0.2.7", + "http-body 0.4.5", + "httparse", "hyper", "js-sys", "lazy_static", @@ -4565,13 +4571,17 @@ dependencies = [ "mm2_p2p", "mm2_state_machine", "parking_lot 0.12.0", + "pin-project", "prost", "rand 0.7.3", "rustls 0.20.4", "serde", "serde_json", + "thiserror", "tokio", "tokio-rustls", + "tonic", + "tower-service", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -4616,7 +4626,7 @@ dependencies = [ "serde_bytes", "sha2 0.9.9", "smallvec 1.6.1", - "syn 2.0.32", + "syn 2.0.38", "tokio", "void", ] @@ -4693,8 +4703,8 @@ version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3048ef3680533a27f9f8e7d6a0bce44dc61e4895ea0f42709337fa1c8616fefe" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -4893,8 +4903,8 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -4957,8 +4967,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" dependencies = [ "proc-macro-crate 1.1.3", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -5029,8 +5039,8 @@ checksum = "44a0b52c2cbaef7dffa5fec1a43274afe8bd2a644fa9fc50a9ef4ff0269b1257" dependencies = [ "Inflector", "proc-macro-error", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -5075,8 +5085,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c45ed1f39709f5a89338fab50e59816b2e8815f5bb58276e7ddf9afd495f73f8" dependencies = [ "proc-macro-crate 1.1.3", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -5104,7 +5114,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f557c32c6d268a07c921471619c0295f5efad3a0e76d4f97a05c091a51d110b2" dependencies = [ - "proc-macro2 1.0.63", + "proc-macro2 1.0.69", "syn 1.0.95", "synstructure", ] @@ -5226,8 +5236,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" dependencies = [ "peg-runtime", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", ] [[package]] @@ -5276,9 +5286,9 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.32", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -5379,7 +5389,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28f53e8b192565862cf99343194579a022eb9c7dd3a8d03134734803c7b3125" dependencies = [ - "proc-macro2 1.0.63", + "proc-macro2 1.0.69", "syn 1.0.95", ] @@ -5433,8 +5443,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", "version_check", ] @@ -5445,8 +5455,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "version_check", ] @@ -5456,9 +5466,9 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70550716265d1ec349c41f70dd4f964b4fd88394efe4405f0c1da679c4799a07" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.32", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -5472,9 +5482,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -5497,8 +5507,8 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b6a5217beb0ad503ee7fa752d451c905113d70721b937126158f3106a48cc1" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -5542,8 +5552,8 @@ checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" dependencies = [ "anyhow", "itertools", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -5662,11 +5672,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.28" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ - "proc-macro2 1.0.63", + "proc-macro2 1.0.69", ] [[package]] @@ -6000,8 +6010,8 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c523ccaed8ac4b0288948849a350b37d3035827413c458b6a40ddb614bb4f72" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -6418,8 +6428,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e334bb10a245e28e5fd755cabcafd96cfcd167c99ae63a46924ca8d8703a3c" dependencies = [ "proc-macro-crate 1.1.3", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -6611,17 +6621,17 @@ dependencies = [ name = "ser_error_derive" version = "0.1.0" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "ser_error", "syn 1.0.95", ] [[package]] name = "serde" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] @@ -6648,13 +6658,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.32", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -6675,19 +6685,19 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] [[package]] name = "serde_urlencoded" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 0.4.6", + "itoa 1.0.1", "ryu", "serde", ] @@ -7158,8 +7168,8 @@ version = "1.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402fffb54bf5d335e6df26fc1719feecfbd7a22fafdf6649fe78380de3c47384" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "rustc_version 0.4.0", "syn 1.0.95", ] @@ -7460,8 +7470,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c834b4e02ac911b13c13aed08b3f847e722f6be79d31b1c660c1dbd2dee83cdb" dependencies = [ "bs58 0.4.0", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "rustversion", "syn 1.0.95", ] @@ -7584,8 +7594,8 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d676664972e22a0796176e81e7bec41df461d1edf52090955cdab55f2c956ff2" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -7614,8 +7624,8 @@ checksum = "22ecb916b9664ed9f90abef0ff5a3e61454c1efea5861b2997e03f39b59b955f" dependencies = [ "Inflector", "proc-macro-crate 1.1.3", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -7762,8 +7772,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f9799e6d412271cb2414597581128b03f3285f260ea49f5363d07df6a332b3e" dependencies = [ "Inflector", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "serde", "serde_json", "unicode-xid 0.2.0", @@ -7842,19 +7852,19 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.32" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "unicode-ident", ] @@ -7879,8 +7889,8 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", "unicode-xid 0.2.0", ] @@ -8184,9 +8194,9 @@ version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.32", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -8324,9 +8334,9 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.32", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -8450,9 +8460,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9263bf4c9bfaae7317c1c2faf7f18491d2fe476f70c414b73bf5d445b00ffa1" dependencies = [ "prettyplease", - "proc-macro2 1.0.63", + "proc-macro2 1.0.69", "prost-build", - "quote 1.0.28", + "quote 1.0.33", "syn 1.0.95", ] @@ -8526,9 +8536,9 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.32", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -8977,9 +8987,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.32", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -9001,7 +9011,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ - "quote 1.0.28", + "quote 1.0.33", "wasm-bindgen-macro-support", ] @@ -9011,9 +9021,9 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.32", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9044,8 +9054,8 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c2e18093f11c19ca4e188c177fecc7c372304c311189f12c2f9bea5b7324ac7" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", ] [[package]] @@ -9653,8 +9663,8 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", "synstructure", ] diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index ea7d1bbf19..ede0b14661 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -121,13 +121,17 @@ spl-token = { version = "3", optional = true } spl-associated-token-account = { version = "1", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] +blake2b_simd = "0.5" ff = "0.8" +futures-util = "0.3" jubjub = "0.5.1" js-sys = { version = "0.3.27" } mm2_db = { path = "../mm2_db" } mm2_metamask = { path = "../mm2_metamask" } mm2_test_helpers = { path = "../mm2_test_helpers" } -time = { version = "0.3.20" } +time = { version = "0.3.20", features = ["wasm-bindgen"] } +tonic = { version = "0.7", default-features = false, features = ["prost", "codegen"] } +tower-service = "0.3" wasm-bindgen = "0.2.86" wasm-bindgen-futures = { version = "0.4.1" } wasm-bindgen-test = { version = "0.3.2" } @@ -165,4 +169,4 @@ mm2_test_helpers = { path = "../mm2_test_helpers" } [build-dependencies] prost-build = { version = "0.10.4", default-features = false } -tonic-build = { version = "0.7", features = ["prost", "compression"] } +tonic-build = { version = "0.7", default-features = false, features = ["prost", "compression"] } diff --git a/mm2src/coins/eth/web3_transport/http_transport.rs b/mm2src/coins/eth/web3_transport/http_transport.rs index 997ff034d7..76bab61cd4 100644 --- a/mm2src/coins/eth/web3_transport/http_transport.rs +++ b/mm2src/coins/eth/web3_transport/http_transport.rs @@ -326,7 +326,7 @@ async fn send_request_once( event_handlers: &Vec, ) -> Result { use http::header::ACCEPT; - use mm2_net::wasm_http::FetchRequest; + use mm2_net::wasm::http::FetchRequest; // account for outgoing traffic event_handlers.on_outgoing_request(request_payload.as_bytes()); diff --git a/mm2src/coins/lp_price.rs b/mm2src/coins/lp_price.rs index d67721917c..a5137c43ef 100644 --- a/mm2src/coins/lp_price.rs +++ b/mm2src/coins/lp_price.rs @@ -200,7 +200,7 @@ async fn process_price_request(price_url: &str) -> Result Result> { debug!("Fetching price from: {}", price_url); - let (status, headers, body) = mm2_net::wasm_http::slurp_url(price_url).await?; + let (status, headers, body) = mm2_net::wasm::http::slurp_url(price_url).await?; let (status_code, body, _) = (status, std::str::from_utf8(&body)?.trim().into(), headers); if status_code != StatusCode::OK { return MmError::err(PriceServiceRequestError::HttpProcessError(body)); diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 40736a6414..c406c770b8 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -41,7 +41,7 @@ use web3::types::TransactionId; use mm2_net::native_http::send_request_to_uri; #[cfg(target_arch = "wasm32")] -use mm2_net::wasm_http::send_request_to_uri; +use mm2_net::wasm::http::send_request_to_uri; const MORALIS_API_ENDPOINT: &str = "api/v2"; /// query parameters for moralis request: The format of the token ID diff --git a/mm2src/coins/nft/nft_tests.rs b/mm2src/coins/nft/nft_tests.rs index 99ec3925de..346611986d 100644 --- a/mm2src/coins/nft/nft_tests.rs +++ b/mm2src/coins/nft/nft_tests.rs @@ -27,7 +27,7 @@ use mm2_net::native_http::send_request_to_uri; common::cfg_wasm32! { use wasm_bindgen_test::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - use mm2_net::wasm_http::send_request_to_uri; + use mm2_net::wasm::http::send_request_to_uri; } cross_test!(test_moralis_ipfs_bafy, { diff --git a/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs index bcbc07c874..ed10bb6aee 100644 --- a/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs +++ b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs @@ -7,7 +7,7 @@ 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 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}, diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 714aed56c8..07a7c88687 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -152,14 +152,14 @@ impl SwapOps for TestCoin { async fn search_for_swap_tx_spend_my( &self, - _: SearchForSwapTxSpendInput<'_>, + _input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { unimplemented!() } async fn search_for_swap_tx_spend_other( &self, - _: SearchForSwapTxSpendInput<'_>, + _input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { unimplemented!() } diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index c4cb848dde..8227d5e947 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -2757,7 +2757,7 @@ async fn connect_loop( static ref CONN_IDX: Arc = Arc::new(AtomicUsize::new(0)); } - use mm2_net::wasm_ws::ws_transport; + use mm2_net::wasm::wasm_ws::ws_transport; let delay = Arc::new(AtomicU64::new(0)); loop { diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index e663f96c3e..1e9a4d2fbe 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1,7 +1,6 @@ use crate::coin_errors::MyAddressError; #[cfg(not(target_arch = "wasm32"))] use crate::my_tx_history_v2::{MyTxHistoryErrorV2, MyTxHistoryRequestV2, MyTxHistoryResponseV2}; -#[cfg(not(target_arch = "wasm32"))] use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawInProgressStatus, WithdrawTaskHandle}; use crate::utxo::rpc_clients::{ElectrumRpcRequest, UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; @@ -9,10 +8,12 @@ use crate::utxo::utxo_builder::UtxoCoinBuildError; use crate::utxo::utxo_builder::{UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, payment_script}; -use crate::utxo::{utxo_common, ActualTxFee, AdditionalTxData, AddrFromStrError, Address, BroadcastTxErr, FeePolicy, - GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, - UnsupportedAddr, UtxoActivationParams, UtxoAddressFormat, UtxoArc, UtxoCoinFields, UtxoCommonOps, - UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; +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, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; +use crate::utxo::{UnsupportedAddr, UtxoFeeDetails}; +use crate::TxFeeDetails; use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, @@ -26,13 +27,14 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, Coi ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; +use crate::{NumConversError, TransactionDetails}; use crate::{Transaction, WithdrawError}; + use async_trait::async_trait; use bitcrypto::dhash256; use chain::constants::SEQUENCE_FINAL; use chain::{Transaction as UtxoTx, TransactionOutput}; use common::executor::{AbortableSystem, AbortedError}; -use common::sha256_digest; use common::{log, one_thousand_u32}; use crypto::privkey::{key_pair_from_secret, secp_privkey_from_hash}; use crypto::{Bip32DerPathOps, GlobalHDAccountArc}; @@ -61,11 +63,13 @@ use std::sync::Arc; use z_coin_errors::ZCoinBalanceError; use z_rpc::{SaplingSyncConnector, SaplingSyncGuard}; use zcash_client_backend::encoding::{decode_payment_address, encode_extended_spending_key, encode_payment_address}; -use zcash_client_backend::wallet::SpendableNote; -use zcash_primitives::consensus::{BlockHeight, NetworkUpgrade, Parameters, H0}; +use zcash_client_backend::wallet::{AccountId, SpendableNote}; +use zcash_extras::WalletRead; +use zcash_primitives::consensus::{BlockHeight, BranchId, NetworkUpgrade, Parameters, H0}; use zcash_primitives::memo::MemoBytes; use zcash_primitives::sapling::keys::OutgoingViewingKey; 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; @@ -81,25 +85,25 @@ use z_rpc::init_light_client; pub use z_rpc::{FirstSyncBlock, SyncStatus}; cfg_native!( - use crate::{NumConversError, TransactionDetails, TxFeeDetails}; - use crate::utxo::{UtxoFeeDetails, sat_from_big_decimal}; use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat}; - - use common::{async_blocking, calc_total_pages, PagingOptionsEnum}; + use common::{async_blocking, sha256_digest, calc_total_pages, PagingOptionsEnum}; use db_common::sqlite::offset_by_id; use db_common::sqlite::rusqlite::{Error as SqlError, Row}; use db_common::sqlite::sql_builder::{name, SqlBuilder, SqlName}; - use zcash_client_backend::data_api::WalletRead; - use zcash_client_backend::wallet::{AccountId}; use zcash_client_sqlite::error::SqliteClientError as ZcashClientError; - use zcash_client_sqlite::wallet::{get_balance}; - use zcash_client_sqlite::wallet::transact::get_spendable_notes; - use zcash_primitives::consensus; - use zcash_primitives::transaction::builder::Builder as ZTxBuilder; + use zcash_client_sqlite::wallet::get_balance; use zcash_proofs::default_params_folder; use z_rpc::{init_native_client}; ); +cfg_wasm32!( + use crate::z_coin::z_params::ZcashParamsWasmImpl; + use common::executor::AbortOnDropHandle; + use futures::channel::oneshot; + use rand::rngs::OsRng; + use zcash_primitives::transaction::builder::TransactionMetadata; +); + #[allow(unused)] mod z_coin_errors; use crate::z_coin::storage::{BlockDbImpl, WalletDbShared}; pub use z_coin_errors::*; @@ -107,6 +111,7 @@ pub use z_coin_errors::*; pub mod storage; #[cfg(all(test, feature = "zhtlc-native-tests"))] mod z_coin_native_tests; +#[cfg(target_arch = "wasm32")] mod z_params; /// `ZP2SHSpendError` compatible `TransactionErr` handling macro. macro_rules! try_ztx_s { @@ -129,13 +134,13 @@ macro_rules! try_ztx_s { const DEX_FEE_OVK: OutgoingViewingKey = OutgoingViewingKey([7; 32]); const DEX_FEE_Z_ADDR: &str = "zs1rp6426e9r6jkq2nsanl66tkd34enewrmr0uvj0zelhkcwmsy0uvxz2fhm9eu9rl3ukxvgzy2v9f"; -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"; cfg_native!( + const SAPLING_OUTPUT_NAME: &str = "sapling-output.params"; + const SAPLING_SPEND_NAME: &str = "sapling-spend.params"; const BLOCKS_TABLE: &str = "blocks"; const TRANSACTIONS_TABLE: &str = "transactions"; + const SAPLING_SPEND_EXPECTED_HASH: &str = "8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"; + const SAPLING_OUTPUT_EXPECTED_HASH: &str = "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"; ); #[derive(Clone, Debug, Serialize, Deserialize)] @@ -358,33 +363,30 @@ impl ZCoin { } #[cfg(target_arch = "wasm32")] - async fn my_balance_sat(&self) -> Result> { todo!() } + async fn my_balance_sat(&self) -> Result> { + let wallet_db = self.z_fields.light_wallet_db.clone(); + Ok(wallet_db.db.get_balance(AccountId::default()).await?.into()) + } - #[cfg(not(target_arch = "wasm32"))] async fn get_spendable_notes(&self) -> Result, MmError> { let wallet_db = self.z_fields.light_wallet_db.clone(); - async_blocking(move || { - let db_guard = wallet_db.db.inner(); - let db_guard = db_guard.lock().unwrap(); - let latest_db_block = match db_guard - .block_height_extrema() - .map_err(|err| SpendableNotesError::DBClientError(err.to_string()))? - { - Some((_, latest)) => latest, - None => return Ok(Vec::new()), - }; - get_spendable_notes(&db_guard, AccountId::default(), latest_db_block) - .map_err(|err| MmError::new(SpendableNotesError::DBClientError(err.to_string()))) - }) - .await - } + let db_guard = wallet_db.db; + let latest_db_block = match db_guard + .block_height_extrema() + .await + .map_err(|err| SpendableNotesError::DBClientError(err.to_string()))? + { + Some((_, latest)) => latest, + None => return Ok(Vec::new()), + }; - #[cfg(target_arch = "wasm32")] - #[allow(unused)] - async fn get_spendable_notes(&self) -> Result, MmError> { todo!() } + db_guard + .get_spendable_notes(AccountId::default(), latest_db_block) + .await + .map_err(|err| MmError::new(SpendableNotesError::DBClientError(err.to_string()))) + } /// Returns spendable notes - #[allow(unused)] async fn spendable_notes_ordered(&self) -> Result, MmError> { let mut unspents = self.get_spendable_notes().await?; @@ -402,7 +404,6 @@ impl ZCoin { } /// Generates a tx sending outputs from our address - #[cfg(not(target_arch = "wasm32"))] async fn gen_tx( &self, t_outputs: Vec, @@ -489,12 +490,19 @@ impl ZCoin { tx_builder.add_tx_out(output); } + #[cfg(not(target_arch = "wasm32"))] let (tx, _) = async_blocking({ let prover = self.z_fields.z_tx_prover.clone(); - move || tx_builder.build(consensus::BranchId::Sapling, prover.as_ref()) + move || tx_builder.build(BranchId::Sapling, prover.as_ref()) }) .await?; + #[cfg(target_arch = "wasm32")] + let (tx, _) = + TxBuilderSpawner::request_tx_result(tx_builder, BranchId::Sapling, self.z_fields.z_tx_prover.clone()) + .await? + .tx_result?; + let additional_data = AdditionalTxData { received_by_me, spent_by_me: sat_from_big_decimal(&total_input_amount, self.decimals())?, @@ -505,15 +513,6 @@ impl ZCoin { Ok((tx, additional_data, sync_guard)) } - #[cfg(target_arch = "wasm32")] - async fn gen_tx( - &self, - _t_outputs: Vec, - _z_outputs: Vec, - ) -> Result<(ZTransaction, AdditionalTxData, SaplingSyncGuard<'_>), MmError> { - todo!() - } - pub async fn send_outputs( &self, t_outputs: Vec, @@ -767,6 +766,56 @@ impl AsRef for ZCoin { fn as_ref(&self) -> &UtxoCoinFields { &self.utxo_arc } } +#[cfg(target_arch = "wasm32")] +type TxResult = MmResult<(zcash_primitives::transaction::Transaction, TransactionMetadata), GenTxError>; + +#[cfg(target_arch = "wasm32")] +/// Spawns an asynchronous task to build a transaction and sends the result through a oneshot channel. +struct TxBuilderSpawner { + pub tx_result: TxResult, + _abort_handle: AbortOnDropHandle, +} + +#[cfg(target_arch = "wasm32")] +impl TxBuilderSpawner { + fn spawn_build_tx( + builder: ZTxBuilder<'static, ZcoinConsensusParams, OsRng>, + branch_id: BranchId, + prover: Arc, + sender: oneshot::Sender, + ) -> AbortOnDropHandle { + let fut = async move { + sender + .send( + builder + .build(branch_id, prover.as_ref()) + .map_to_mm(GenTxError::TxBuilderError), + ) + .ok(); + }; + + common::executor::spawn_local_abortable(fut) + } + + /// Requests a transaction asynchronously using the provided builder, branch ID, and prover. + async fn request_tx_result( + builder: ZTxBuilder<'static, ZcoinConsensusParams, OsRng>, + branch_id: BranchId, + prover: Arc, + ) -> MmResult { + // Create a oneshot channel for communication between the spawned task and this function + let (tx, rx) = oneshot::channel(); + let abort_handle = Self::spawn_build_tx(builder, branch_id, prover, tx); + + Ok(Self { + tx_result: rx + .await + .map_to_mm(|_| GenTxError::Internal("Spawned future has been canceled".to_owned()))?, + _abort_handle: abort_handle, + }) + } +} + /// SyncStartPoint represents the starting point for synchronizing a wallet's blocks and transaction history. /// This can be specified as a date, a block height, or starting from the earliest available data. #[derive(Clone, Debug, Deserialize, Serialize)] @@ -776,7 +825,7 @@ pub enum SyncStartPoint { Date(u64), /// Synchronize from a specific block height. Height(u64), - /// Synchronize from the earliest available data(`sapling_activation_height` from coin config). + /// Synchronize from the earliest available data(sapling_activation_height from coin config). Earliest, } @@ -812,7 +861,6 @@ pub struct ZcoinActivationParams { pub account: u32, } -#[cfg(not(target_arch = "wasm32"))] pub async fn z_coin_from_conf_and_params( ctx: &MmArc, ticker: &str, @@ -821,6 +869,9 @@ pub async fn z_coin_from_conf_and_params( protocol_info: ZcoinProtocolInfo, priv_key_policy: PrivKeyBuildPolicy, ) -> Result> { + #[cfg(target_arch = "wasm32")] + let db_dir_path = PathBuf::new(); + #[cfg(not(target_arch = "wasm32"))] let db_dir_path = ctx.dbdir(); let z_spending_key = None; let builder = ZCoinBuilder::new( @@ -836,14 +887,14 @@ pub async fn z_coin_from_conf_and_params( builder.build().await } -#[allow(unused)] +#[cfg(not(target_arch = "wasm32"))] 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) } -#[allow(unused)] +#[cfg(not(target_arch = "wasm32"))] fn get_spend_output_paths(params_dir: PathBuf) -> Result<(PathBuf, PathBuf), ZCoinBuildError> { if !params_dir.exists() { return Err(ZCoinBuildError::ZCashParamsNotFound); @@ -925,7 +976,7 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { &my_z_addr, ); - let blocks_db = self.blocks_db().await?; + let blocks_db = self.init_blocks_db().await?; let (sync_state_connector, light_wallet_db) = match &self.z_coin_params.mode { #[cfg(not(target_arch = "wasm32"))] ZcoinRpcMode::Native => { @@ -959,12 +1010,10 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { sync_state_connector, }; - let z_coin = ZCoin { + Ok(ZCoin { utxo_arc, z_fields: Arc::new(z_fields), - }; - - Ok(z_coin) + }) } } @@ -1014,12 +1063,13 @@ impl<'a> ZCoinBuilder<'a> { } } - async fn blocks_db(&self) -> Result> { + async fn init_blocks_db(&self) -> Result> { let cache_db_path = self.db_dir_path.join(format!("{}_cache.db", self.ticker)); let ctx = self.ctx.clone(); let ticker = self.ticker.to_string(); + BlockDbImpl::new(ctx, ticker, cache_db_path) - .map_err(|err| MmError::new(ZcoinClientInitError::ZcashDBError(err.to_string()))) + .map_err(|err| MmError::new(ZcoinClientInitError::ZcoinStorageError(err.to_string()))) .await } @@ -1043,7 +1093,30 @@ impl<'a> ZCoinBuilder<'a> { } #[cfg(target_arch = "wasm32")] - async fn z_tx_prover(&self) -> Result> { todo!() } + async fn z_tx_prover(&self) -> Result> { + let params_db = ZcashParamsWasmImpl::new(self.ctx.clone()) + .await + .mm_err(|err| ZCoinBuildError::ZCashParamsError(err.to_string()))?; + let (sapling_spend, sapling_output) = if !params_db + .check_params() + .await + .mm_err(|err| ZCoinBuildError::ZCashParamsError(err.to_string()))? + { + // save params + params_db + .download_and_save_params() + .await + .mm_err(|err| ZCoinBuildError::ZCashParamsError(err.to_string()))? + } else { + // get params + params_db + .get_params() + .await + .mm_err(|err| ZCoinBuildError::ZCashParamsError(err.to_string()))? + }; + + Ok(LocalTxProver::from_bytes(&sapling_spend[..], &sapling_output[..])) + } } /// Initialize `ZCoin` with a forced `z_spending_key`. @@ -1485,10 +1558,6 @@ impl SwapOps for ZCoin { utxo_common::search_for_swap_tx_spend_other(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } - fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { - unimplemented!(); - } - #[inline] async fn extract_secret( &self, @@ -1499,6 +1568,10 @@ impl SwapOps for ZCoin { utxo_common::extract_secret(secret_hash, spend_tx) } + fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + fn is_auto_refundable(&self) -> bool { false } async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { @@ -1932,7 +2005,6 @@ impl UtxoCommonOps for ZCoin { } } -#[cfg(not(target_arch = "wasm32"))] #[async_trait] impl InitWithdrawCoin for ZCoin { async fn init_withdraw( diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs index e9e8879fce..553c23539d 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs @@ -4,9 +4,8 @@ use crate::z_coin::z_coin_errors::ZcoinStorageError; use async_trait::async_trait; use mm2_core::mm_ctx::MmArc; -use mm2_db::indexed_db::{BeBigUint, DbIdentifier, DbInstance, DbUpgrader, IndexedDb, IndexedDbBuilder, InitDbResult, - MultiIndex, OnUpgradeResult, TableSignature}; -use mm2_db::indexed_db::{ConstructibleDb, DbLocked}; +use mm2_db::indexed_db::{BeBigUint, ConstructibleDb, DbIdentifier, DbInstance, DbLocked, DbUpgrader, IndexedDb, + IndexedDbBuilder, InitDbResult, MultiIndex, OnUpgradeResult, TableSignature}; use mm2_err_handle::prelude::*; use protobuf::Message; use std::path::PathBuf; @@ -28,7 +27,7 @@ pub struct BlockDbTable { } impl BlockDbTable { - pub const TICKER_HEIGHT_INDEX: &str = "block_height_ticker_index"; + pub const TICKER_HEIGHT_INDEX: &str = "ticker_height_index"; } impl TableSignature for BlockDbTable { @@ -97,13 +96,7 @@ impl BlockDbImpl { .next() .await?; - let maybe_height = maybe_height.map(|(_, item)| item.height); - - let Some(height) = maybe_height else { - return MmError::err(ZcoinStorageError::GetFromStorageError(format!("{ticker} block height not found"))); - }; - - Ok(height) + Ok(maybe_height.map(|(_, item)| item.height).unwrap_or_else(|| 0)) } /// Insert new block to BlockDbTable given the provided data. @@ -176,17 +169,18 @@ impl BlockDbImpl { let block_db = db_transaction.table::().await?; // Fetch CompactBlocks block_db are needed for scanning. + let min = u32::from(from_height + 1); let mut maybe_blocks = block_db .cursor_builder() .only("ticker", &self.ticker)? - .bound("height", u32::from(from_height + 1), limit.unwrap_or(u32::MAX)) + .bound("height", min, u32::MAX) .open_cursor(BlockDbTable::TICKER_HEIGHT_INDEX) .await?; let mut blocks_to_scan = vec![]; while let Some((_, block)) = maybe_blocks.next().await? { if let Some(limit) = limit { - if block.height > limit { + if blocks_to_scan.len() > limit as usize { break; } }; @@ -204,7 +198,6 @@ impl BlockDbImpl { /// /// Processes blocks based on the provided `BlockProcessingMode` and other parameters, /// which may include a starting block height, validation criteria, and a processing limit. - #[allow(unused)] pub(crate) async fn process_blocks_with_mode( &self, params: ZcoinConsensusParams, @@ -221,23 +214,11 @@ impl BlockDbImpl { .unwrap_or(BlockHeight::from_u32(params.sapling_activation_height) - 1) })?, }; - - let blocks = self.query_blocks_by_limit(from_height, limit).await?; - let mut prev_height = from_height; let mut prev_hash: Option = validate_from.map(|(_, hash)| hash); - for block in blocks { - if let Some(limit) = limit { - if u32::from(block.height) > limit { - break; - } - } - - if block.height < from_height { - continue; - } - + let blocks_to_scan = self.query_blocks_by_limit(from_height, limit).await?; + for block in blocks_to_scan { let cbr = block; let block = CompactBlock::parse_from_bytes(&cbr.data) .map_to_mm(|err| ZcoinStorageError::DecodingError(err.to_string()))?; diff --git a/mm2src/coins/z_coin/storage/blockdb/mod.rs b/mm2src/coins/z_coin/storage/blockdb/mod.rs index 064a235c3a..ed9bc8fb2a 100644 --- a/mm2src/coins/z_coin/storage/blockdb/mod.rs +++ b/mm2src/coins/z_coin/storage/blockdb/mod.rs @@ -103,6 +103,9 @@ mod native_tests { mod wasm_tests { use crate::z_coin::storage::blockdb::block_db_storage_tests::{test_insert_block_and_get_latest_block_impl, test_rewind_to_height_impl}; + use crate::z_coin::z_rpc::{LightRpcClient, ZRpcOps}; + use common::log::info; + use common::log::wasm_log::register_wasm_log; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -112,4 +115,14 @@ mod wasm_tests { #[wasm_bindgen_test] async fn test_rewind_to_height() { test_rewind_to_height_impl().await } + + #[wasm_bindgen_test] + async fn test_transport() { + register_wasm_log(); + let mut client = LightRpcClient::new(vec!["http://pirate.battlefield.earth:8581".to_string()]) + .await + .unwrap(); + let tree_state = client.get_block_height().await; + info!("LATEST BLOCK: {tree_state:?}"); + } } diff --git a/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs b/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs index 4f41a01d2b..3a957d375f 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs @@ -26,20 +26,20 @@ pub async fn create_wallet_db( ) -> Result, MmError> { let db = async_blocking(move || { WalletDbAsync::for_path(wallet_db_path, consensus_params) - .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string())) + .map_to_mm(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string())) }) .await?; let db_inner = db.inner(); async_blocking(move || { let db_inner = db_inner.lock().unwrap(); run_optimization_pragmas(db_inner.sql_conn()) - .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string())) + .map_to_mm(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string())) }) .await?; init_wallet_db(&db) .await - .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; + .map_to_mm(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string()))?; let get_evk = db.get_extended_full_viewing_keys().await?; let extrema = db.block_height_extrema().await?; @@ -57,7 +57,7 @@ pub async fn create_wallet_db( wallet_ops .rewind_to_height(u32::MIN.into()) .await - .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; + .map_to_mm(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string()))?; if let Some(block) = checkpoint_block.clone() { init_blocks_table( &db, diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs index 82c3608b5f..57f81e18da 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs @@ -52,7 +52,7 @@ macro_rules! num_to_bigint { impl<'a> WalletDbShared { pub async fn new( - builder: ZCoinBuilder<'a>, + builder: &ZCoinBuilder<'a>, checkpoint_block: Option, z_spending_key: &ExtendedSpendingKey, continue_from_prev_sync: bool, @@ -170,6 +170,10 @@ impl<'a> WalletIndexedDb { } } + pub fn get_update_ops(&self) -> MmResult { + Ok(DataConnStmtCacheWasm(self.clone())) + } + pub(crate) async fn init_accounts_table(&self, extfvks: &[ExtendedFullViewingKey]) -> ZcoinStorageRes<()> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; diff --git a/mm2src/coins/z_coin/z_coin_errors.rs b/mm2src/coins/z_coin/z_coin_errors.rs index dea3ce8884..a2adab422f 100644 --- a/mm2src/coins/z_coin/z_coin_errors.rs +++ b/mm2src/coins/z_coin/z_coin_errors.rs @@ -28,7 +28,6 @@ use zcash_primitives::transaction::builder::Error as ZTxBuilderError; #[derive(Debug, Display)] #[non_exhaustive] pub enum UpdateBlocksCacheErr { - #[cfg(not(target_arch = "wasm32"))] GrpcError(tonic::Status), UtxoRpcError(UtxoRpcError), InternalError(String), @@ -38,7 +37,10 @@ pub enum UpdateBlocksCacheErr { DecodeError(String), } -#[cfg(not(target_arch = "wasm32"))] +impl From for UpdateBlocksCacheErr { + fn from(err: ZcoinStorageError) -> Self { UpdateBlocksCacheErr::ZcashDBError(err.to_string()) } +} + impl From for UpdateBlocksCacheErr { fn from(err: tonic::Status) -> Self { UpdateBlocksCacheErr::GrpcError(err) } } @@ -53,10 +55,6 @@ impl From for UpdateBlocksCacheErr { fn from(err: SqliteClientError) -> Self { UpdateBlocksCacheErr::ZcashDBError(err.to_string()) } } -impl From for UpdateBlocksCacheErr { - fn from(err: ZcoinStorageError) -> Self { UpdateBlocksCacheErr::ZcashDBError(err.to_string()) } -} - impl From for UpdateBlocksCacheErr { fn from(err: UtxoRpcError) -> Self { UpdateBlocksCacheErr::UtxoRpcError(err) } } @@ -72,7 +70,7 @@ impl From for UpdateBlocksCacheErr { #[derive(Debug, Display)] #[non_exhaustive] pub enum ZcoinClientInitError { - ZcashDBError(String), + ZcoinStorageError(String), EmptyLightwalletdUris, #[display(fmt = "Fail to init clients while iterating lightwalletd urls {:?}", _0)] UrlIterFailure(Vec), @@ -81,7 +79,7 @@ pub enum ZcoinClientInitError { } impl From for ZcoinClientInitError { - fn from(err: ZcoinStorageError) -> Self { ZcoinClientInitError::ZcashDBError(err.to_string()) } + fn from(err: ZcoinStorageError) -> Self { ZcoinClientInitError::ZcoinStorageError(err.to_string()) } } impl From for ZcoinClientInitError { @@ -90,7 +88,7 @@ impl From for ZcoinClientInitError { #[cfg(not(target_arch = "wasm32"))] impl From for ZcoinClientInitError { - fn from(err: SqliteClientError) -> Self { ZcoinClientInitError::ZcashDBError(err.to_string()) } + fn from(err: SqliteClientError) -> Self { ZcoinClientInitError::ZcoinStorageError(err.to_string()) } } #[derive(Debug, Display)] @@ -131,6 +129,8 @@ pub enum GenTxError { LightClientErr(String), FailedToCreateNote, SpendableNotesError(String), + #[cfg(target_arch = "wasm32")] + Internal(String), } impl From for GenTxError { @@ -178,6 +178,8 @@ impl From for WithdrawError { | GenTxError::LightClientErr(_) | GenTxError::SpendableNotesError(_) | GenTxError::FailedToCreateNote => WithdrawError::InternalError(gen_tx.to_string()), + #[cfg(target_arch = "wasm32")] + GenTxError::Internal(_) => WithdrawError::InternalError(gen_tx.to_string()), } } } @@ -243,6 +245,7 @@ pub enum ZCoinBuildError { Io(std::io::Error), RpcClientInitErr(ZcoinClientInitError), ZCashParamsNotFound, + ZCashParamsError(String), ZDerivationPathNotSet, SaplingParamsInvalidChecksum, } @@ -305,8 +308,13 @@ pub enum SpendableNotesError { } #[derive(Debug, Display)] -pub enum ZCoinBalanceError {} +pub enum ZCoinBalanceError { + BalanceError(String), +} +impl From for ZCoinBalanceError { + fn from(value: ZcoinStorageError) -> Self { ZCoinBalanceError::BalanceError(value.to_string()) } +} /// The `ValidateBlocksError` enum encapsulates different types of errors that may occur /// during the validation and scanning process of zcoin blocks. #[derive(Debug, Display)] @@ -413,6 +421,12 @@ pub enum ZcoinStorageError { ChainError(String), InternalError(String), NotSupported(String), + #[cfg(target_arch = "wasm32")] + ZcashParamsError(String), +} + +impl From for ZcoinStorageError { + fn from(err: UpdateBlocksCacheErr) -> Self { ZcoinStorageError::DbError(err.to_string()) } } #[cfg(target_arch = "wasm32")] diff --git a/mm2src/coins/z_coin/z_params/indexeddb.rs b/mm2src/coins/z_coin/z_params/indexeddb.rs new file mode 100644 index 0000000000..765329c4dc --- /dev/null +++ b/mm2src/coins/z_coin/z_params/indexeddb.rs @@ -0,0 +1,184 @@ +use crate::z_coin::z_coin_errors::ZcoinStorageError; + +use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::{ConstructibleDb, DbIdentifier, DbInstance, DbLocked, DbUpgrader, IndexedDb, IndexedDbBuilder, + InitDbResult, OnUpgradeResult, SharedDb, TableSignature}; +use mm2_err_handle::prelude::*; + +const CHAIN: &str = "z_coin"; +const DB_NAME: &str = "z_params"; +const DB_VERSION: u32 = 1; +const TARGET_SPEND_CHUNKS: usize = 12; + +pub(crate) type ZcashParamsWasmRes = MmResult; +pub(crate) type ZcashParamsInnerLocked<'a> = DbLocked<'a, ZcashParamsWasmInner>; + +/// Since sapling_spend data way is greater than indexeddb max_data(267386880) bytes to save, we need to split +/// sapling_spend and insert to db multiple times with index(sapling_spend_id) +#[derive(Clone, Debug, Deserialize, Serialize)] +struct ZcashParamsWasmTable { + sapling_spend_id: u8, + sapling_spend: Vec, + sapling_output: Vec, + ticker: String, +} + +impl ZcashParamsWasmTable { + const SPEND_OUTPUT_INDEX: &str = "sapling_spend_sapling_output_index"; +} + +impl TableSignature for ZcashParamsWasmTable { + const TABLE_NAME: &'static str = "z_params_bytes"; + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_multi_index(Self::SPEND_OUTPUT_INDEX, &["sapling_spend", "sapling_output"], true)?; + table.create_index("sapling_spend", false)?; + table.create_index("sapling_output", false)?; + table.create_index("sapling_spend_id", true)?; + table.create_index("ticker", false)?; + } + + Ok(()) + } +} + +pub(crate) struct ZcashParamsWasmInner(IndexedDb); + +#[async_trait::async_trait] +impl DbInstance for ZcashParamsWasmInner { + const 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 ZcashParamsWasmInner { + pub(crate) fn get_inner(&self) -> &IndexedDb { &self.0 } +} + +#[derive(Clone)] +pub(crate) struct ZcashParamsWasmImpl(SharedDb); + +impl ZcashParamsWasmImpl { + pub(crate) async fn new(ctx: MmArc) -> MmResult { + Ok(Self(ConstructibleDb::new(&ctx).into_shared())) + } + + async fn lock_db(&self) -> ZcashParamsWasmRes> { + self.0 + .get_or_initialize() + .await + .mm_err(|err| ZcoinStorageError::DbError(err.to_string())) + } + + /// Given sapling_spend, sapling_output and sapling_spend_id, save to indexeddb storage. + pub(crate) async fn save_params( + &self, + sapling_spend_id: u8, + sapling_spend: &[u8], + sapling_output: &[u8], + ) -> MmResult<(), ZcoinStorageError> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let params_db = db_transaction.table::().await?; + let params = ZcashParamsWasmTable { + sapling_spend_id, + sapling_spend: sapling_spend.to_vec(), + sapling_output: sapling_output.to_vec(), + ticker: CHAIN.to_string(), + }; + + Ok(params_db + .replace_item_by_unique_index("sapling_spend_id", sapling_spend_id as u32, ¶ms) + .await + .map(|_| ())?) + } + + /// Check if z_params is already save to storage previously. + pub(crate) async fn check_params(&self) -> MmResult { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let params_db = db_transaction.table::().await?; + let count = params_db.count_all().await?; + if count != TARGET_SPEND_CHUNKS { + params_db.delete_items_by_index("ticker", CHAIN).await?; + } + + Ok(count == TARGET_SPEND_CHUNKS) + } + + /// Get z_params from storage. + pub(crate) async fn get_params(&self) -> MmResult<(Vec, Vec), ZcoinStorageError> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let params_db = db_transaction.table::().await?; + let mut maybe_params = params_db + .cursor_builder() + .only("ticker", CHAIN)? + .open_cursor("ticker") + .await?; + + let mut sapling_spend = vec![]; + let mut sapling_output = vec![]; + + while let Some((_, params)) = maybe_params.next().await? { + sapling_spend.extend_from_slice(¶ms.sapling_spend); + if params.sapling_spend_id == 0 { + sapling_output = params.sapling_output + } + } + + Ok((sapling_spend, sapling_output)) + } + + /// Download and save z_params to storage. + pub(crate) async fn download_and_save_params(&self) -> MmResult<(Vec, Vec), ZcoinStorageError> { + let (sapling_spend, sapling_output) = super::download_parameters() + .await + .mm_err(|err| ZcoinStorageError::ZcashParamsError(err.to_string()))?; + + if sapling_spend.len() <= sapling_output.len() { + self.save_params(0, &sapling_spend, &sapling_output).await? + } else { + let spends = sapling_spend_to_chunks(&sapling_spend); + if let Some((first_spend, remaining_spends)) = spends.split_first() { + self.save_params(0, first_spend, &sapling_output).await?; + + for (i, spend) in remaining_spends.iter().enumerate() { + self.save_params((i + 1) as u8, spend, &[]).await?; + } + } + } + + Ok((sapling_spend, sapling_output)) + } +} + +/// Since sapling_spend data way is greater than indexeddb max_data(267386880) bytes to save, we need to split +/// sapling_spend into chunks of 12 and insert to db multiple times with index(sapling_spend_id) +fn sapling_spend_to_chunks(sapling_spend: &[u8]) -> Vec<&[u8]> { + // Calculate the target size for each chunk + let chunk_size = sapling_spend.len() / TARGET_SPEND_CHUNKS; + // Calculate the remainder for cases when the length is not perfectly divisible + let remainder = sapling_spend.len() % TARGET_SPEND_CHUNKS; + let mut sapling_spend_chunks = Vec::with_capacity(TARGET_SPEND_CHUNKS); + let mut start = 0; + for i in 0..TARGET_SPEND_CHUNKS { + let end = start + chunk_size + usize::from(i < remainder); + // Extract the current chunk from the original vector + sapling_spend_chunks.push(&sapling_spend[start..end]); + // Move the start index to the next position + start = end; + } + + sapling_spend_chunks +} diff --git a/mm2src/coins/z_coin/z_params/mod.rs b/mm2src/coins/z_coin/z_params/mod.rs new file mode 100644 index 0000000000..650596505b --- /dev/null +++ b/mm2src/coins/z_coin/z_params/mod.rs @@ -0,0 +1,86 @@ +mod indexeddb; +pub(crate) use indexeddb::ZcashParamsWasmImpl; + +use blake2b_simd::State; +use common::log::info; +use common::log::wasm_log::register_wasm_log; +use mm2_err_handle::prelude::MmResult; +use mm2_err_handle::prelude::*; +use mm2_net::wasm::http::FetchRequest; +use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; +use wasm_bindgen_test::*; + +const DOWNLOAD_URL: &str = "https://komodoplatform.com/downloads"; +const SAPLING_SPEND_NAME: &str = "sapling-spend.params"; +const SAPLING_OUTPUT_NAME: &str = "sapling-output.params"; +const SAPLING_SPEND_HASH: &str = "8270785a1a0d0bc77196f000ee6d221c9c9894f55307bd9357c3f0105d31ca63991ab91324160d8f53e2bbd3c2633a6eb8bdf5205d822e7f3f73edac51b2b70c"; +const SAPLING_OUTPUT_HASH: &str = "657e3d38dbb5cb5e7dd2970e8b03d69b4787dd907285b5a7f0790dcc8072f60bf593b32cc2d1c030e00ff5ae64bf84c5c3beb84ddc841d48264b4a171744d028"; + +#[derive(Debug, derive_more::Display)] +pub(crate) enum ZcashParamsError { + Transport(String), + ValidationError(String), +} + +/// Download, validate and return z_params from given `DOWNLOAD_URL` +async fn fetch_params(name: &str, expected_hash: &str) -> MmResult, ZcashParamsError> { + let (status, file) = FetchRequest::get(&format!("{DOWNLOAD_URL}/{name}")) + .cors() + .request_array() + .await + .mm_err(|err| ZcashParamsError::Transport(err.to_string()))?; + + if status != 200 { + return MmError::err(ZcashParamsError::Transport(format!( + "Expected status 200, got {} for {}", + status, name + ))); + } + + let hash = State::new().update(&file).finalize().to_hex(); + // Verify parameter file hash. + if &hash != expected_hash { + return Err(ZcashParamsError::ValidationError(format!( + "{} failed validation (expected: {}, actual: {}, fetched {} bytes)", + name, + expected_hash, + hash, + file.len() + )) + .into()); + } + + Ok(file) +} + +pub(crate) async fn download_parameters() -> MmResult<(Vec, Vec), ZcashParamsError> { + Ok(( + fetch_params(SAPLING_SPEND_NAME, SAPLING_SPEND_HASH).await?, + fetch_params(SAPLING_OUTPUT_NAME, SAPLING_OUTPUT_HASH).await?, + )) +} + +#[wasm_bindgen_test] +async fn test_download_save_and_get_params() { + register_wasm_log(); + info!("Testing download, save and get params"); + let ctx = mm_ctx_with_custom_db(); + let db = ZcashParamsWasmImpl::new(ctx).await.unwrap(); + // save params + let (sapling_spend, sapling_output) = db.download_and_save_params().await.unwrap(); + // get params + let (sapling_spend_db, sapling_output_db) = db.get_params().await.unwrap(); + assert_eq!(sapling_spend, sapling_spend_db); + assert_eq!(sapling_output, sapling_output_db); + info!("Testing download, save and get params successful"); +} + +#[wasm_bindgen_test] +async fn test_check_for_no_params() { + register_wasm_log(); + let ctx = mm_ctx_with_custom_db(); + let db = ZcashParamsWasmImpl::new(ctx).await.unwrap(); + // check for no params + let check_params = db.check_params().await.unwrap(); + assert!(!check_params) +} diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index d78aea033c..b9d88361f3 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -1,53 +1,61 @@ -use super::{z_coin_errors::*, BlockDbImpl, WalletDbShared, ZCoinBuilder, ZcoinConsensusParams}; -use crate::utxo::rpc_clients::NativeClient; +use super::{z_coin_errors::*, BlockDbImpl, CheckPointBlockInfo, WalletDbShared, ZCoinBuilder, ZcoinConsensusParams}; +use crate::utxo::rpc_clients::NO_TX_ERROR_CODE; +use crate::utxo::utxo_builder::{UtxoCoinBuilderCommonOps, DAY_IN_SECONDS}; +use crate::z_coin::storage::{BlockProcessingMode, DataConnStmtCacheWrapper}; use crate::z_coin::SyncStartPoint; +use crate::RpcCommonOps; use async_trait::async_trait; +use common::executor::Timer; use common::executor::{spawn_abortable, AbortOnDropHandle}; +use common::log::LogOnError; +use common::log::{debug, error, info}; +use common::now_sec; +use futures::channel::mpsc::channel; use futures::channel::mpsc::{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 hex::{FromHex, FromHexError}; use mm2_err_handle::prelude::*; use parking_lot::Mutex; +use prost::Message; +use rpc::v1::types::{Bytes, H256 as H256Json}; +use std::convert::TryFrom; +use std::pin::Pin; +use std::str::FromStr; use std::sync::Arc; +use z_coin_grpc::{BlockId, BlockRange, TreeState, TxFilter}; +use zcash_extras::{WalletRead, WalletWrite}; use zcash_primitives::consensus::BlockHeight; use zcash_primitives::transaction::TxId; use zcash_primitives::zip32::ExtendedSpendingKey; +pub(crate) mod z_coin_grpc { + tonic::include_proto!("pirate.wallet.sdk.rpc"); +} +use z_coin_grpc::compact_tx_streamer_client::CompactTxStreamerClient; +use z_coin_grpc::ChainSpec; + cfg_native!( - use super::CheckPointBlockInfo; - use crate::{RpcCommonOps, ZTransaction}; - use crate::utxo::rpc_clients::{UtxoRpcClientOps, NO_TX_ERROR_CODE}; - use crate::z_coin::storage::{BlockProcessingMode, DataConnStmtCacheWrapper}; + use crate::ZTransaction; + use crate::utxo::rpc_clients::{UtxoRpcClientOps}; use crate::z_coin::z_coin_errors::{ZcoinStorageError, ValidateBlocksError}; - use crate::utxo::utxo_builder::{UtxoCoinBuilderCommonOps, DAY_IN_SECONDS}; + use crate::utxo::rpc_clients::NativeClient; - use common::{now_sec}; - use common::executor::Timer; - use common::log::{debug, error, info, LogOnError}; - use futures::channel::mpsc::channel; use futures::compat::Future01CompatExt; use group::GroupEncoding; - use hex::{FromHex, FromHexError}; use http::Uri; - use prost::Message; - use std::convert::TryFrom; - use rpc::v1::types::{Bytes, H256 as H256Json}; use std::convert::TryInto; use std::num::TryFromIntError; - use std::pin::Pin; - use std::str::FromStr; use tonic::transport::{Channel, ClientTlsConfig}; - use zcash_extras::{WalletRead, WalletWrite}; + use tonic::codegen::StdError; - mod z_coin_grpc { - tonic::include_proto!("pirate.wallet.sdk.rpc"); - } - use z_coin_grpc::TreeState; - use z_coin_grpc::compact_tx_streamer_client::CompactTxStreamerClient; - use z_coin_grpc::{BlockId, BlockRange, ChainSpec, CompactBlock as TonicCompactBlock, - CompactOutput as TonicCompactOutput, CompactSpend as TonicCompactSpend, CompactTx as TonicCompactTx, - TxFilter}; + use z_coin_grpc::{CompactBlock as TonicCompactBlock, CompactOutput as TonicCompactOutput, + CompactSpend as TonicCompactSpend, CompactTx as TonicCompactTx}; +); + +cfg_wasm32!( + use mm2_net::wasm::tonic_client::TonicClient; ); /// ZRpcOps trait provides asynchronous methods for performing various operations related to @@ -58,7 +66,6 @@ pub trait ZRpcOps { async fn get_block_height(&mut self) -> Result>; /// Asynchronously retrieve the tree state for a specific block height from the Zcoin network. - #[cfg(not(target_arch = "wasm32"))] async fn get_tree_state(&mut self, height: u64) -> Result>; /// Asynchronously scan and process blocks within a specified block height range. @@ -81,22 +88,80 @@ pub trait ZRpcOps { /// checkpoint_block_from_height retrieves tree state information from rpc corresponding to the given /// height and constructs a `CheckPointBlockInfo` struct containing some needed details such as /// block height, hash, time, and sapling tree. - #[cfg(not(target_arch = "wasm32"))] async fn checkpoint_block_from_height( &mut self, height: u64, + ticker: &str, ) -> MmResult, UpdateBlocksCacheErr>; } #[cfg(not(target_arch = "wasm32"))] -struct LightRpcClient { - rpc_clients: AsyncMutex>>, +type RpcClientType = Channel; +#[cfg(target_arch = "wasm32")] +type RpcClientType = TonicClient; + +pub struct LightRpcClient { + rpc_clients: AsyncMutex>>, +} + +impl LightRpcClient { + #[allow(unused_mut)] + pub async fn new(lightwalletd_urls: Vec) -> Result> { + let mut rpc_clients = Vec::new(); + if lightwalletd_urls.is_empty() { + return MmError::err(ZcoinClientInitError::EmptyLightwalletdUris); + } + + let mut errors = Vec::new(); + for url in &lightwalletd_urls { + #[cfg(not(target_arch = "wasm32"))] + let uri = match Uri::from_str(url) { + Ok(uri) => uri, + Err(err) => { + errors.push(UrlIterError::InvalidUri(err)); + continue; + }, + }; + #[cfg(not(target_arch = "wasm32"))] + let endpoint = match Channel::builder(uri).tls_config(ClientTlsConfig::new()) { + Ok(endpoint) => endpoint, + Err(err) => { + errors.push(UrlIterError::TlsConfigFailure(err)); + continue; + }, + }; + #[cfg(not(target_arch = "wasm32"))] + let client = match connect_endpoint(endpoint).await { + Ok(tonic_channel) => tonic_channel, + Err(err) => { + errors.push(UrlIterError::ConnectionFailure(err)); + continue; + }, + }; + + #[cfg(target_arch = "wasm32")] + let client = CompactTxStreamerClient::new(TonicClient::new(url.to_string())); + + rpc_clients.push(client); + } + + #[cfg(not(target_arch = "wasm32"))] + 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)); + } + + Ok(LightRpcClient { + rpc_clients: AsyncMutex::new(rpc_clients), + }) + } } -#[cfg(not(target_arch = "wasm32"))] #[async_trait] impl RpcCommonOps for LightRpcClient { - type RpcClient = CompactTxStreamerClient; + type RpcClient = CompactTxStreamerClient; type Error = MmError; async fn get_live_client(&self) -> Result { @@ -104,7 +169,8 @@ impl RpcCommonOps for LightRpcClient { 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() { + let latest = client.get_latest_block(request).await; + if latest.is_ok() { clients.rotate_left(i); return Ok(client); } @@ -115,7 +181,17 @@ impl RpcCommonOps for LightRpcClient { } } +/// Attempt to create a new client by connecting to a given endpoint. #[cfg(not(target_arch = "wasm32"))] +pub async fn connect_endpoint(dst: D) -> Result, tonic::transport::Error> +where + D: std::convert::TryInto, + D::Error: Into, +{ + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(CompactTxStreamerClient::new(conn)) +} + #[async_trait] impl ZRpcOps for LightRpcClient { async fn get_block_height(&mut self) -> Result> { @@ -131,7 +207,6 @@ impl ZRpcOps for LightRpcClient { Ok(block.height) } - #[cfg(not(target_arch = "wasm32"))] async fn get_tree_state(&mut self, height: u64) -> Result> { let request = tonic::Request::new(BlockId { height, hash: vec![] }); @@ -169,7 +244,7 @@ impl ZRpcOps for LightRpcClient { .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); + debug!("Got block {}", block.height); let height = u32::try_from(block.height) .map_err(|_| UpdateBlocksCacheErr::DecodeError("Block height too large".to_string()))?; db.insert_block(height, block.encode_to_vec()) @@ -208,10 +283,10 @@ impl ZRpcOps for LightRpcClient { true } - #[cfg(not(target_arch = "wasm32"))] async fn checkpoint_block_from_height( &mut self, height: u64, + ticker: &str, ) -> MmResult, UpdateBlocksCacheErr> { let tree_state = self.get_tree_state(height).await?; let hash = H256Json::from_str(&tree_state.hash) @@ -222,6 +297,7 @@ impl ZRpcOps for LightRpcClient { .map_err(|err: FromHexError| UpdateBlocksCacheErr::DecodeError(err.to_string()))?, ); + info!("Final Derived Sync Height for {ticker} is: {height}"); Ok(Some(CheckPointBlockInfo { height: tree_state.height as u32, hash, @@ -238,7 +314,6 @@ impl ZRpcOps for NativeClient { Ok(self.get_block_count().compat().await?) } - #[cfg(not(target_arch = "wasm32"))] async fn get_tree_state(&mut self, _height: u64) -> Result> { todo!() } async fn scan_blocks( @@ -347,16 +422,15 @@ impl ZRpcOps for NativeClient { true } - #[cfg(not(target_arch = "wasm32"))] async fn checkpoint_block_from_height( &mut self, _height: u64, + _ticker: &str, ) -> MmResult, UpdateBlocksCacheErr> { todo!() } } -#[cfg(not(target_arch = "wasm32"))] pub(super) async fn init_light_client<'a>( builder: &ZCoinBuilder<'a>, lightwalletd_urls: Vec, @@ -367,45 +441,8 @@ pub(super) async fn init_light_client<'a>( let coin = builder.ticker.to_string(); 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 mut light_rpc_clients = LightRpcClient { - rpc_clients: AsyncMutex::new(rpc_clients), - }; + let mut light_rpc_clients = LightRpcClient::new(lightwalletd_urls).await?; let current_block_height = light_rpc_clients .get_block_height() @@ -425,14 +462,13 @@ pub(super) async fn init_light_client<'a>( .unwrap_or(sapling_activation_height), }; let maybe_checkpoint_block = light_rpc_clients - .checkpoint_block_from_height(sync_height.max(sapling_activation_height)) + .checkpoint_block_from_height(sync_height.max(sapling_activation_height), &coin) .await?; let min_height = blocks_db.get_earliest_block().await?; // check if no sync_params was provided and continue syncing from last height in db if it's > 0. let continue_from_prev_sync = min_height > 0 && sync_params.is_none(); - let wallet_db = WalletDbShared::new(builder, maybe_checkpoint_block, z_spending_key, continue_from_prev_sync) - .await - .mm_err(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; + let wallet_db = + WalletDbShared::new(builder, maybe_checkpoint_block, z_spending_key, continue_from_prev_sync).await?; // Get min_height in blocks_db and rewind blocks_db to 0 if sync_height != min_height let min_height = blocks_db.get_earliest_block().await?; @@ -441,11 +477,8 @@ pub(super) async fn init_light_client<'a>( if min_height > 0 { info!("Older/Newer sync height detected!, rewinding blocks_db to new height: {sync_height:?}"); } - blocks_db - .rewind_to_height(u32::MIN) - .await - .map_err(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; - } + blocks_db.rewind_to_height(u32::MIN).await?; + }; let sync_handle = SaplingSyncLoopHandle { coin, @@ -473,18 +506,6 @@ pub(super) async fn init_light_client<'a>( )) } -#[cfg(target_arch = "wasm32")] -#[allow(unused)] -pub(super) async fn init_light_client<'a>( - _builder: &ZCoinBuilder<'a>, - _lightwalletd_urls: Vec, - _blocks_db: BlockDbImpl, - _sync_params: &Option, - z_spending_key: &ExtendedSpendingKey, -) -> Result<(AsyncMutex, WalletDbShared), MmError> { - todo!() -} - #[cfg(not(target_arch = "wasm32"))] pub(super) async fn init_native_client<'a>( builder: &ZCoinBuilder<'a>, @@ -505,7 +526,7 @@ pub(super) async fn init_native_client<'a>( }; let wallet_db = WalletDbShared::new(builder, checkpoint_block, z_spending_key, true) .await - .mm_err(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; + .mm_err(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string()))?; let sync_handle = SaplingSyncLoopHandle { coin, @@ -528,16 +549,6 @@ pub(super) async fn init_native_client<'a>( )) } -#[cfg(target_arch = "wasm32")] -pub(super) async fn _init_native_client<'a>( - _builder: &ZCoinBuilder<'a>, - mut _native_client: NativeClient, - _blocks_db: BlockDbImpl, - _z_spending_key: &ExtendedSpendingKey, -) -> Result<(AsyncMutex, WalletDbShared), MmError> { - todo!() -} - pub struct SaplingSyncRespawnGuard { pub(super) sync_handle: Option<(SaplingSyncLoopHandle, Box)>, pub(super) abort_handle: Arc>, @@ -630,7 +641,6 @@ pub struct SaplingSyncLoopHandle { first_sync_block: FirstSyncBlock, } -#[cfg(not(target_arch = "wasm32"))] impl SaplingSyncLoopHandle { fn first_sync_block(&self) -> FirstSyncBlock { self.first_sync_block.clone() } @@ -699,7 +709,6 @@ impl SaplingSyncLoopHandle { /// Scans cached blocks, validates the chain and updates WalletDb. /// For more notes on the process, check https://github.com/zcash/librustzcash/blob/master/zcash_client_backend/src/data_api/chain.rs#L2 async fn scan_validate_and_update_blocks(&mut self) -> Result<(), MmError> { - // required to avoid immutable borrow of self let wallet_db = self.wallet_db.clone().db; let wallet_ops_guard = wallet_db.get_update_ops().expect("get_update_ops always returns Ok"); let mut wallet_ops_guard_clone = wallet_ops_guard.clone(); @@ -758,6 +767,7 @@ impl SaplingSyncLoopHandle { Timer::sleep_ms(self.scan_interval_ms).await; } } + Ok(()) } @@ -770,31 +780,6 @@ impl SaplingSyncLoopHandle { } } -#[cfg(target_arch = "wasm32")] -#[allow(unused)] -impl SaplingSyncLoopHandle { - fn notify_blocks_cache_status(&mut self, _current_scanned_block: u64, _latest_block: u64) { todo!() } - - fn notify_building_wallet_db(&mut self, _current_scanned_block: u64, _latest_block: u64) { todo!() } - - fn notify_on_error(&mut self, _error: String) { todo!() } - - fn notify_sync_finished(&mut self) { todo!() } - - async fn update_blocks_cache( - &mut self, - _rpc: &mut (dyn ZRpcOps + Send), - ) -> Result<(), MmError> { - todo!() - } - - /// Scans cached blocks, validates the chain and updates WalletDb. - /// For more notes on the process, check https://github.com/zcash/librustzcash/blob/master/zcash_client_backend/src/data_api/chain.rs#L2 - fn scan_blocks(&mut self) -> Result<(), MmError> { todo!() } - - async fn check_watch_for_tx_existence(&mut self, _rpc: &mut (dyn ZRpcOps + Send)) { todo!() } -} - /// For more info on shielded light client protocol, please check the https://zips.z.cash/zip-0307 /// /// It's important to note that unlike standard UTXOs, shielded outputs are not spendable until the transaction is confirmed. @@ -816,13 +801,12 @@ 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. -#[cfg(not(target_arch = "wasm32"))] 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(client.as_mut()).await { error!("Error {} on blocks cache update", e); @@ -868,11 +852,6 @@ async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle, mut c } } -#[cfg(target_arch = "wasm32")] -async fn light_wallet_db_sync_loop(mut _sync_handle: SaplingSyncLoopHandle, mut _client: Box) { - todo!() -} - type SyncWatcher = AsyncReceiver; type NewTxNotifier = AsyncSender)>>; diff --git a/mm2src/coins_activation/src/context.rs b/mm2src/coins_activation/src/context.rs index a86869e7ab..26835ef076 100644 --- a/mm2src/coins_activation/src/context.rs +++ b/mm2src/coins_activation/src/context.rs @@ -1,7 +1,6 @@ #[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; use mm2_core::mm_ctx::{from_ctx, MmArc}; use rpc_task::RpcTaskManager; @@ -10,7 +9,6 @@ use std::sync::Arc; pub struct CoinsActivationContext { pub(crate) init_utxo_standard_task_manager: UtxoStandardTaskManagerShared, 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, @@ -23,7 +21,6 @@ impl CoinsActivationContext { Ok(CoinsActivationContext { init_utxo_standard_task_manager: RpcTaskManager::new_shared(), 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/lib.rs b/mm2src/coins_activation/src/lib.rs index 34bd11e902..a3475f3d18 100644 --- a/mm2src/coins_activation/src/lib.rs +++ b/mm2src/coins_activation/src/lib.rs @@ -26,7 +26,7 @@ mod tendermint_token_activation; mod tendermint_with_assets_activation; mod token; mod utxo_activation; -#[cfg(not(target_arch = "wasm32"))] mod z_coin_activation; +mod z_coin_activation; 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; diff --git a/mm2src/coins_activation/src/prelude.rs b/mm2src/coins_activation/src/prelude.rs index 2126d8bc06..2061509ee4 100644 --- a/mm2src/coins_activation/src/prelude.rs +++ b/mm2src/coins_activation/src/prelude.rs @@ -1,5 +1,4 @@ use coins::utxo::UtxoActivationParams; -#[cfg(not(target_arch = "wasm32"))] use coins::z_coin::ZcoinActivationParams; use coins::{coin_conf, CoinBalance, CoinProtocol, MmCoinEnum}; use mm2_core::mm_ctx::MmArc; @@ -21,7 +20,6 @@ impl TxHistory for UtxoActivationParams { fn tx_history(&self) -> bool { self.tx_history } } -#[cfg(not(target_arch = "wasm32"))] impl TxHistory for ZcoinActivationParams { fn tx_history(&self) -> bool { false } } diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 9bcc277724..076ad2276d 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -191,6 +191,8 @@ pub const X_API_KEY: &str = "X-API-Key"; 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 APPLICATION_GRPC_WEB_TEXT: &str = "application/grpc-web-text"; +pub const APPLICATION_GRPC_WEB_TEXT_PROTO: &str = "application/grpc-web-text+proto"; pub const SATOSHIS: u64 = 100_000_000; diff --git a/mm2src/crypto/src/hw_rpc_task.rs b/mm2src/crypto/src/hw_rpc_task.rs index 41a0516ab6..11596889df 100644 --- a/mm2src/crypto/src/hw_rpc_task.rs +++ b/mm2src/crypto/src/hw_rpc_task.rs @@ -23,8 +23,7 @@ pub enum HwRpcTaskAwaitingStatus { EnterTrezorPassphrase, } -/// When it comes to interacting with a HW device, -/// this is a common user action in answer to awaiting RPC task status. +/// When it comes to interacting with a HW device, this is a common user action in answer to awaiting RPC task status. #[derive(Deserialize, Serialize)] #[serde(tag = "action_type")] pub enum HwRpcTaskUserAction { diff --git a/mm2src/mm2_db/src/indexed_db/indexed_db.rs b/mm2src/mm2_db/src/indexed_db/indexed_db.rs index 0a16e90429..3e2bfe5982 100644 --- a/mm2src/mm2_db/src/indexed_db/indexed_db.rs +++ b/mm2src/mm2_db/src/indexed_db/indexed_db.rs @@ -316,7 +316,6 @@ impl<'transaction, Table: TableSignature> DbTable<'transaction, Table> { /// https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/add pub async fn add_item(&self, item: &Table) -> DbTransactionResult { let item = json::to_value(item).map_to_mm(|e| DbTransactionError::ErrorSerializingItem(e.to_string()))?; - let (result_tx, result_rx) = oneshot::channel(); let event = internal::DbTableEvent::AddItem { item, result_tx }; send_event_recv_response(&self.event_tx, event, result_rx).await @@ -496,7 +495,7 @@ impl<'transaction, Table: TableSignature> DbTable<'transaction, Table> { send_event_recv_response(&self.event_tx, event, result_rx).await } - /// Adds the given `item` of replace the previous one. + /// Adds the given `item` or replace the previous one. /// https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/put pub async fn replace_item(&self, item_id: ItemId, item: &Table) -> DbTransactionResult { let item = json::to_value(item).map_to_mm(|e| DbTransactionError::ErrorSerializingItem(e.to_string()))?; diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index cf3c43ea3d..a775c18c4b 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -29,6 +29,7 @@ use coins::utxo::bch::BchCoin; use coins::utxo::qtum::QtumCoin; use coins::utxo::slp::SlpToken; use coins::utxo::utxo_standard::UtxoStandardCoin; +use coins::z_coin::ZCoin; use coins::{add_delegation, get_my_address, get_raw_transaction, get_staking_infos, nft, remove_delegation, sign_message, sign_raw_transaction, verify_message, withdraw}; #[cfg(all( @@ -55,7 +56,6 @@ use std::net::SocketAddr; cfg_native! { use coins::lightning::LightningCoin; - use coins::z_coin::ZCoin; } pub async fn process_single_request( @@ -260,16 +260,16 @@ async fn rpc_task_dispatcher( "withdraw::init" => handle_mmrpc(ctx, request, init_withdraw).await, "withdraw::status" => handle_mmrpc(ctx, request, withdraw_status).await, "withdraw::user_action" => handle_mmrpc(ctx, request, withdraw_user_action).await, + "enable_z_coin::init" => handle_mmrpc(ctx, request, init_standalone_coin::).await, + "enable_z_coin::cancel" => handle_mmrpc(ctx, request, cancel_init_standalone_coin::).await, + "enable_z_coin::status" => handle_mmrpc(ctx, request, init_standalone_coin_status::).await, + "enable_z_coin::user_action" => handle_mmrpc(ctx, request, init_standalone_coin_user_action::).await, #[cfg(not(target_arch = "wasm32"))] native_only_methods => match native_only_methods { "enable_lightning::cancel" => handle_mmrpc(ctx, request, cancel_init_l2::).await, "enable_lightning::init" => handle_mmrpc(ctx, request, init_l2::).await, "enable_lightning::status" => handle_mmrpc(ctx, request, init_l2_status::).await, "enable_lightning::user_action" => handle_mmrpc(ctx, request, init_l2_user_action::).await, - "enable_z_coin::cancel" => handle_mmrpc(ctx, request, cancel_init_standalone_coin::).await, - "enable_z_coin::init" => handle_mmrpc(ctx, request, init_standalone_coin::).await, - "enable_z_coin::status" => handle_mmrpc(ctx, request, init_standalone_coin_status::).await, - "enable_z_coin::user_action" => handle_mmrpc(ctx, request, init_standalone_coin_user_action::).await, _ => MmError::err(DispatcherError::NoSuchMethod), }, #[cfg(target_arch = "wasm32")] diff --git a/mm2src/mm2_main/src/wasm_tests.rs b/mm2src/mm2_main/src/wasm_tests.rs index a2178aad65..dbb8f5ff81 100644 --- a/mm2src/mm2_main/src/wasm_tests.rs +++ b/mm2src/mm2_main/src/wasm_tests.rs @@ -3,15 +3,21 @@ use common::executor::{spawn, Timer}; use common::log::wasm_log::register_wasm_log; use crypto::StandardHDCoinAddress; use mm2_core::mm_ctx::MmArc; +use mm2_number::BigDecimal; use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::electrums::{doc_electrums, marty_electrums}; -use mm2_test_helpers::for_tests::{check_recent_swaps, enable_electrum_json, morty_conf, rick_conf, start_swaps, - test_qrc20_history_impl, wait_for_swaps_finish_and_check_status, MarketMakerIt, - Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, MORTY, RICK}; +use mm2_test_helpers::for_tests::{check_recent_swaps, enable_electrum_json, enable_z_coin_light, morty_conf, + pirate_conf, rick_conf, start_swaps, test_qrc20_history_impl, + wait_for_swaps_finish_and_check_status, MarketMakerIt, Mm2InitPrivKeyPolicy, + Mm2TestConf, Mm2TestConfForSwap, ARRR, MORTY, PIRATE_ELECTRUMS, + PIRATE_LIGHTWALLETD_URLS, RICK}; use mm2_test_helpers::get_passphrase; +use mm2_test_helpers::structs::EnableCoinBalance; use serde_json::json; use wasm_bindgen_test::wasm_bindgen_test; +const PIRATE_TEST_BALANCE_SEED: &str = "pirate test seed"; + /// Starts the WASM version of MM. fn wasm_start(ctx: MmArc) { spawn(async move { @@ -184,3 +190,23 @@ async fn trade_test_rick_and_morty() { ) .await; } + +#[wasm_bindgen_test] +async fn activate_z_coin_light() { + register_wasm_log(); + let coins = json!([pirate_conf()]); + + let conf = Mm2TestConf::seednode(PIRATE_TEST_BALANCE_SEED, &coins); + let mm = MarketMakerIt::start_async(conf.conf, conf.rpc_password, Some(wasm_start)) + .await + .unwrap(); + + let activation_result = + enable_z_coin_light(&mm, ARRR, PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, None, None).await; + + let balance = match activation_result.wallet_balance { + EnableCoinBalance::Iguana(iguana) => iguana, + _ => panic!("Expected EnableCoinBalance::Iguana"), + }; + assert_eq!(balance.balance.spendable, BigDecimal::default()); +} diff --git a/mm2src/mm2_main/tests/integration_tests_common/mod.rs b/mm2src/mm2_main/tests/integration_tests_common/mod.rs index be7e8bcb46..bf230c1546 100644 --- a/mm2src/mm2_main/tests/integration_tests_common/mod.rs +++ b/mm2src/mm2_main/tests/integration_tests_common/mod.rs @@ -7,9 +7,9 @@ use mm2_main::mm2::{lp_main, LpMainParams}; use mm2_rpc::data::legacy::CoinInitResponse; use mm2_test_helpers::electrums::{doc_electrums, marty_electrums}; use mm2_test_helpers::for_tests::{enable_native as enable_native_impl, init_utxo_electrum, init_utxo_status, - init_z_coin_light, init_z_coin_status, MarketMakerIt}; -use mm2_test_helpers::structs::{InitTaskResult, InitUtxoStatus, InitZcoinStatus, RpcV2Response, - UtxoStandardActivationResult, ZCoinActivationResult}; + MarketMakerIt}; + +use mm2_test_helpers::structs::{InitTaskResult, InitUtxoStatus, RpcV2Response, UtxoStandardActivationResult}; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; use std::env::var; @@ -83,34 +83,6 @@ pub async fn enable_coins_rick_morty_electrum(mm: &MarketMakerIt) -> HashMap<&'s replies } -pub async fn enable_z_coin_light( - mm: &MarketMakerIt, - coin: &str, - electrums: &[&str], - lightwalletd_urls: &[&str], - starting_date: Option, - account: Option, -) -> ZCoinActivationResult { - let init = init_z_coin_light(mm, coin, electrums, lightwalletd_urls, starting_date, account).await; - let init: RpcV2Response = json::from_value(init).unwrap(); - let timeout = wait_until_ms(600000); - - loop { - if now_ms() > timeout { - panic!("{} initialization timed out", coin); - } - - let status = init_z_coin_status(mm, init.result.task_id).await; - println!("Status {}", json::to_string(&status).unwrap()); - let status: RpcV2Response = json::from_value(status).unwrap(); - match status.result { - InitZcoinStatus::Ok(result) => break result, - InitZcoinStatus::Error(e) => panic!("{} initialization error {:?}", coin, e), - _ => Timer::sleep(1.).await, - } - } -} - pub async fn enable_utxo_v2_electrum( mm: &MarketMakerIt, coin: &str, diff --git a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs index 05710fa5c4..00ac53672f 100644 --- a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs @@ -1,14 +1,14 @@ use crate::integration_tests_common::{enable_coins_eth_electrum, enable_coins_rick_morty_electrum, enable_electrum, - enable_electrum_json, enable_z_coin_light}; + enable_electrum_json}; use common::{block_on, log}; use http::StatusCode; use mm2_main::mm2::lp_ordermatch::MIN_ORDER_KEEP_ALIVE_INTERVAL; use mm2_number::{BigDecimal, BigRational, MmNumber}; use mm2_rpc::data::legacy::{AggregatedOrderbookEntry, CoinInitResponse, OrderbookResponse}; use mm2_test_helpers::electrums::doc_electrums; -use mm2_test_helpers::for_tests::{eth_jst_testnet_conf, eth_testnet_conf, get_passphrase, morty_conf, orderbook_v2, - rick_conf, zombie_conf, MarketMakerIt, Mm2TestConf, DOC_ELECTRUM_ADDRS, - ETH_DEV_NODES, MARTY_ELECTRUM_ADDRS, RICK, ZOMBIE_ELECTRUMS, +use mm2_test_helpers::for_tests::{enable_z_coin_light, eth_jst_testnet_conf, eth_testnet_conf, get_passphrase, + morty_conf, orderbook_v2, rick_conf, zombie_conf, MarketMakerIt, Mm2TestConf, + DOC_ELECTRUM_ADDRS, ETH_DEV_NODES, MARTY_ELECTRUM_ADDRS, RICK, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, ZOMBIE_TICKER}; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::{GetPublicKeyResult, OrderbookV2Response, RpcV2Response, SetPriceResponse}; diff --git a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs index 7fa96a4b03..d622bfa7e8 100644 --- a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs @@ -3,10 +3,10 @@ use common::executor::Timer; use common::{block_on, log, now_ms, now_sec, wait_until_ms}; use mm2_number::BigDecimal; use mm2_test_helpers::electrums::doc_electrums; -use mm2_test_helpers::for_tests::{disable_coin, init_withdraw, pirate_conf, rick_conf, send_raw_transaction, - withdraw_status, z_coin_tx_history, zombie_conf, MarketMakerIt, Mm2TestConf, ARRR, - PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, RICK, ZOMBIE_ELECTRUMS, - ZOMBIE_LIGHTWALLETD_URLS, ZOMBIE_TICKER}; +use mm2_test_helpers::for_tests::{disable_coin, enable_z_coin_light, init_withdraw, pirate_conf, rick_conf, + send_raw_transaction, withdraw_status, z_coin_tx_history, zombie_conf, + MarketMakerIt, Mm2TestConf, ARRR, PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, RICK, + ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, ZOMBIE_TICKER}; use mm2_test_helpers::structs::{EnableCoinBalance, InitTaskResult, RpcV2Response, TransactionDetails, WithdrawStatus, ZcoinHistoryRes}; use serde_json::{self as json, json, Value as Json}; @@ -109,8 +109,8 @@ fn activate_z_coin_light_with_changing_height() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, - Some(two_days_ago), None, + Some(two_days_ago), )); let new_first_sync_block = activation_result.first_sync_block; @@ -143,8 +143,8 @@ fn activate_z_coin_with_hd_account() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, - None, Some(hd_account_id), + None, )); let actual = match activation_result.wallet_balance { diff --git a/mm2src/mm2_net/Cargo.toml b/mm2src/mm2_net/Cargo.toml index 9c42cd3d38..1a65ef08a3 100644 --- a/mm2src/mm2_net/Cargo.toml +++ b/mm2src/mm2_net/Cargo.toml @@ -30,15 +30,28 @@ prost = "0.10" rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } serde = "1" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +thiserror = "1.0.30" [target.'cfg(target_arch = "wasm32")'.dependencies] +base64 = "0.21.2" +byteorder = "1.3" +futures-util = "0.3" gstuff = { version = "0.7", features = ["nightly"] } mm2_state_machine = { path = "../mm2_state_machine"} +http-body = "0.4" +httparse = "1.8.0" +js-sys = "0.3.27" +pin-project = "1.0.10" +tonic = { version = "0.7", default-features = false, features = ["prost", "codegen"] } +tower-service = "0.3" wasm-bindgen = "0.2.86" wasm-bindgen-test = { version = "0.3.2" } wasm-bindgen-futures = "0.4.21" -web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "MessageEvent", "WebSocket", "Worker"] } -js-sys = "0.3.27" +web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", + "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", + "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", + "IdbVersionChangeEvent", "MessageEvent", "ReadableStreamDefaultReader", "ReadableStream", "WebSocket", "Worker"] } + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] futures-util = { version = "0.3" } diff --git a/mm2src/mm2_net/src/grpc_web.rs b/mm2src/mm2_net/src/grpc_web.rs index 43ba449502..55f796df82 100644 --- a/mm2src/mm2_net/src/grpc_web.rs +++ b/mm2src/mm2_net/src/grpc_web.rs @@ -4,6 +4,7 @@ use crate::transport::SlurpError; use bytes::{Buf, BufMut, Bytes, BytesMut}; use common::{cfg_native, cfg_wasm32}; +use derive_more::Display; use http::header::{ACCEPT, CONTENT_TYPE}; use mm2_err_handle::prelude::*; use prost::DecodeError; @@ -15,7 +16,7 @@ cfg_native! { cfg_wasm32! { use common::{X_GRPC_WEB, APPLICATION_GRPC_WEB_PROTO}; - use crate::wasm_http::FetchRequest; + use crate::wasm::http::FetchRequest; } // one byte for the compression flag plus four bytes for the length @@ -92,14 +93,20 @@ where Ok(msg) } -#[derive(Debug)] +#[derive(Debug, thiserror::Error, Display)] pub enum PostGrpcWebErr { DecodeBody(String), EncodeBody(String), InvalidRequest(String), + BadResponse(String), Internal(String), PayloadTooShort(String), - Transport { uri: String, error: String }, + Status(String), + #[display(fmt = "Transport Error — uri: {uri} — error: {error}")] + Transport { + uri: String, + error: String, + }, } impl From for PostGrpcWebErr { diff --git a/mm2src/mm2_net/src/lib.rs b/mm2src/mm2_net/src/lib.rs index edd13738b9..954e25c5a0 100644 --- a/mm2src/mm2_net/src/lib.rs +++ b/mm2src/mm2_net/src/lib.rs @@ -8,7 +8,6 @@ pub mod transport; #[cfg(not(target_arch = "wasm32"))] pub mod native_tls; #[cfg(all(feature = "event-stream", not(target_arch = "wasm32")))] pub mod sse_handler; +#[cfg(target_arch = "wasm32")] pub mod wasm; #[cfg(all(feature = "event-stream", target_arch = "wasm32"))] pub mod wasm_event_stream; -#[cfg(target_arch = "wasm32")] pub mod wasm_http; -#[cfg(target_arch = "wasm32")] pub mod wasm_ws; diff --git a/mm2src/mm2_net/src/transport.rs b/mm2src/mm2_net/src/transport.rs index 27c039d556..8a6e7c4ea5 100644 --- a/mm2src/mm2_net/src/transport.rs +++ b/mm2src/mm2_net/src/transport.rs @@ -10,7 +10,7 @@ use serde_json::{Error, Value as Json}; pub use crate::native_http::{slurp_post_json, slurp_req, slurp_req_body, slurp_url, slurp_url_with_headers}; #[cfg(target_arch = "wasm32")] -pub use crate::wasm_http::{slurp_post_json, slurp_url, slurp_url_with_headers}; +pub use crate::wasm::http::{slurp_post_json, slurp_url, slurp_url_with_headers}; pub type SlurpResult = Result<(StatusCode, HeaderMap, Vec), MmError>; diff --git a/mm2src/mm2_net/src/wasm/body_stream.rs b/mm2src/mm2_net/src/wasm/body_stream.rs new file mode 100644 index 0000000000..f4a59603b0 --- /dev/null +++ b/mm2src/mm2_net/src/wasm/body_stream.rs @@ -0,0 +1,414 @@ +/// This module handles HTTP response decoding and trailer extraction for gRPC-Web communication/streaming. +/// # gRPC-Web Response Body Handling Module +/// +/// gRPC-Web is a protocol that enables web applications to communicate with gRPC services over HTTP/1.1. It is +/// particularly useful for browsers and other environments that do not support HTTP/2. This module provides +/// essential functionality to process and decode gRPC-Web responses in MM2 also support streaming. +/// +/// ## Key Components +/// +/// - **EncodedBytes**: This struct represents a buffer for encoded bytes. It manages the decoding of base64-encoded data and is used to handle response data and trailers based on the content type. The `new` method initializes an instance based on the content type. Other methods are available for handling encoding and decoding of data. +/// +/// - **ReadState**: An enumeration that represents the different states in which the response can be read. It keeps track of the progress of response processing, indicating whether data reading is complete or trailers have been encountered. +/// +/// - **ResponseBody**: This struct is the core of response handling. It is designed to work with gRPC-Web responses. It reads response data from a ReadableStream, decodes and processes the response, and extracts trailers if present. The `new` method initializes an instance of ResponseBody based on the ReadableStream and content type. It implements the `Body` trait to provide a standardized interface for reading response data and trailers. +/// +/// - **BodyStream**: A struct that represents a stream of bytes for the response body. It is used internally by ResponseBody to read the response data from a web stream. The `new` method creates a new instance based on an `IntoStream`, and the `empty` method creates an empty stream. This struct also implements the `Body` trait, providing methods to read data from the stream and return trailers. +use crate::grpc_web::PostGrpcWebErr; + +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use byteorder::{BigEndian, ByteOrder}; +use bytes::{BufMut, Bytes, BytesMut}; +use common::{APPLICATION_GRPC_WEB, APPLICATION_GRPC_WEB_PROTO, APPLICATION_GRPC_WEB_TEXT, + APPLICATION_GRPC_WEB_TEXT_PROTO}; +use futures_util::{ready, stream}; +use futures_util::{stream::empty, Stream}; +use http::{header::HeaderName, HeaderMap, HeaderValue}; +use http_body::Body; +use httparse::{Status, EMPTY_HEADER}; +use js_sys::{Object, Uint8Array}; +use pin_project::pin_project; +use std::ops::{Deref, DerefMut}; +use std::{pin::Pin, + task::{Context, Poll}}; +use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen_futures::JsFuture; +use web_sys::{ReadableStream, ReadableStreamDefaultReader}; + +/// If the 8th most significant bit of a frame is `0`, it indicates data; if `1`, it indicates a trailer. +const TRAILER_BIT: u8 = 0b10000000; + +/// Manages a buffer for storing response data and provides methods for appending and decoding data based on the content type. +pub struct EncodedBytes { + is_base64: bool, + raw_buf: BytesMut, + buf: BytesMut, +} + +impl EncodedBytes { + /// Creates a new `EncodedBytes` instance based on the content type. + pub fn new(content_type: &str) -> Result { + let is_base64 = match content_type { + APPLICATION_GRPC_WEB_TEXT | APPLICATION_GRPC_WEB_TEXT_PROTO => true, + APPLICATION_GRPC_WEB | APPLICATION_GRPC_WEB_PROTO => false, + _ => { + return Err(PostGrpcWebErr::InvalidRequest(format!( + "Unsupported Content-Type: {content_type}" + ))) + }, + }; + + Ok(Self { + is_base64, + raw_buf: BytesMut::new(), + buf: BytesMut::new(), + }) + } + + // This is to avoid passing a slice of bytes with a length that the base64 + // decoder would consider invalid. + #[inline] + fn max_decodable(&self) -> usize { (self.raw_buf.len() / 4) * 4 } + + fn decode_base64_chunk(&mut self) -> Result<(), PostGrpcWebErr> { + let index = self.max_decodable(); + + if self.raw_buf.len() >= index { + let decoded = BASE64_STANDARD + .decode(self.raw_buf.split_to(index)) + .map(Bytes::from) + .map_err(|err| PostGrpcWebErr::DecodeBody(err.to_string()))?; + self.buf.put(decoded); + } + + Ok(()) + } + + fn append(&mut self, bytes: Bytes) -> Result<(), PostGrpcWebErr> { + if self.is_base64 { + self.raw_buf.put(bytes); + self.decode_base64_chunk()?; + } else { + self.buf.put(bytes) + } + + Ok(()) + } + + fn take(&mut self, length: usize) -> BytesMut { + let new_buf = self.buf.split_off(length); + std::mem::replace(&mut self.buf, new_buf) + } +} + +impl Deref for EncodedBytes { + type Target = BytesMut; + + fn deref(&self) -> &Self::Target { &self.buf } +} + +impl DerefMut for EncodedBytes { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.buf } +} + +/// Represents the state of reading the response body, including compression flags, data lengths, trailers, and the done state. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReadState { + CompressionFlag, + DataLength, + Data(u32), + TrailerLength, + Trailer(u32), + Done, +} + +impl ReadState { + fn is_done(&self) -> bool { matches!(self, ReadState::Done) } + + fn finished_data(&self) -> bool { + matches!(self, ReadState::TrailerLength) + || matches!(self, ReadState::Trailer(_)) + || matches!(self, ReadState::Done) + } +} + +/// Handles the HTTP response body, decoding data, and extracting trailers +#[pin_project] +pub struct ResponseBody { + #[pin] + body_stream: BodyStream, + buf: EncodedBytes, + incomplete_data: BytesMut, + data: Option, + trailer: Option, + state: ReadState, + finished_stream: bool, +} + +impl ResponseBody { + /// Creates a new `ResponseBody` based on a ReadableStream and content type. + pub(crate) async fn new(body_stream: ReadableStream, content_type: &str) -> Result { + let body_stream: ReadableStreamDefaultReader = body_stream + .get_reader() + .dyn_into() + .map_err(|err| PostGrpcWebErr::BadResponse(format!("{err:?}")))?; + + Ok(Self { + body_stream: BodyStream::new(body_stream).await?, + buf: EncodedBytes::new(content_type)?, + incomplete_data: BytesMut::new(), + data: None, + trailer: None, + state: ReadState::CompressionFlag, + finished_stream: false, + }) + } + + fn read_stream(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if self.finished_stream { + return Poll::Ready(Ok(())); + } + + let this = self.project(); + + match ready!(this.body_stream.poll_data(cx)) { + Some(Ok(data)) => { + if let Err(e) = this.buf.append(data) { + return Poll::Ready(Err(e)); + } + + Poll::Ready(Ok(())) + }, + Some(Err(e)) => Poll::Ready(Err(e)), + None => { + *this.finished_stream = true; + Poll::Ready(Ok(())) + }, + } + } + + fn step(self: Pin<&mut Self>) -> Result<(), PostGrpcWebErr> { + let this = self.project(); + + loop { + match this.state { + ReadState::CompressionFlag => { + if this.buf.is_empty() { + // Can't read compression flag right now + return Ok(()); + }; + + let compression_flag = this.buf.take(1); + if compression_flag[0] & TRAILER_BIT == 0 { + this.incomplete_data.unsplit(compression_flag); + *this.state = ReadState::DataLength; + } else { + *this.state = ReadState::TrailerLength; + } + }, + ReadState::DataLength => { + if this.buf.len() < 4 { + // Can't read data length right now + return Ok(()); + }; + + let data_length_bytes = this.buf.take(4); + let data_length = BigEndian::read_u32(data_length_bytes.as_ref()); + + this.incomplete_data.extend_from_slice(&data_length_bytes); + *this.state = ReadState::Data(data_length); + }, + ReadState::Data(data_length) => { + let data_length = *data_length as usize; + if this.buf.len() < data_length { + // Can't read data right now + return Ok(()); + }; + + this.incomplete_data.unsplit(this.buf.take(data_length)); + + let new_data = this.incomplete_data.split(); + if let Some(data) = this.data { + data.unsplit(new_data); + } else { + *this.data = Some(new_data); + } + + *this.state = ReadState::CompressionFlag; + }, + ReadState::TrailerLength => { + if this.buf.len() < 4 { + // Can't read data length right now + return Ok(()); + }; + + *this.state = ReadState::Trailer(BigEndian::read_u32(this.buf.take(4).as_ref())); + }, + ReadState::Trailer(trailer_length) => { + let trailer_length = *trailer_length as usize; + if this.buf.len() < trailer_length { + // Can't read trailer right now + return Ok(()); + }; + + let mut trailer_bytes = this.buf.take(trailer_length); + trailer_bytes.put_u8(b'\n'); + + *this.trailer = Some(Self::parse_trailer(&trailer_bytes)?); + *this.state = ReadState::Done; + }, + ReadState::Done => return Ok(()), + } + } + } + + fn parse_trailer(trailer_bytes: &[u8]) -> Result { + let mut trailers_buf = [EMPTY_HEADER; 64]; + let parsed_trailers = match httparse::parse_headers(trailer_bytes, &mut trailers_buf) + .map_err(|err| PostGrpcWebErr::InvalidRequest(err.to_string()))? + { + Status::Complete((_, headers)) => Ok(headers), + Status::Partial => Err(PostGrpcWebErr::InvalidRequest( + "parse header not completed!".to_string(), + )), + }?; + + let mut trailers = HeaderMap::with_capacity(parsed_trailers.len()); + + for parsed_trailer in parsed_trailers { + let header_name = HeaderName::from_bytes(parsed_trailer.name.as_bytes()) + .map_err(|err| PostGrpcWebErr::InvalidRequest(err.to_string()))?; + let header_value = HeaderValue::from_bytes(parsed_trailer.value) + .map_err(|err| PostGrpcWebErr::InvalidRequest(err.to_string()))?; + trailers.insert(header_name, header_value); + } + + Ok(trailers) + } +} + +impl Body for ResponseBody { + type Data = Bytes; + + type Error = PostGrpcWebErr; + + fn poll_data(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>> { + // If reading data is finished return `None` + if self.state.finished_data() { + return Poll::Ready(self.data.take().map(|d| Ok(d.freeze()))); + } + + loop { + // Read bytes from stream + if let Err(e) = ready!(self.as_mut().read_stream(cx)) { + return Poll::Ready(Some(Err(e))); + } + + // Step the state machine + if let Err(e) = self.as_mut().step() { + return Poll::Ready(Some(Err(e))); + } + + if self.state.finished_data() { + // If we finished reading data continue return `None` + return Poll::Ready(self.data.take().map(|d| Ok(d.freeze()))); + } else if self.finished_stream { + // If stream is finished but data is not finished return error + return Poll::Ready(Some(Err(PostGrpcWebErr::InvalidRequest("Bad response".to_string())))); + } + } + } + + fn poll_trailers(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll, Self::Error>> { + // If the state machine is complete, return trailer + if self.state.is_done() { + return Poll::Ready(Ok(self.trailer.take())); + } + + loop { + // Read bytes from stream + if let Err(e) = ready!(self.as_mut().read_stream(cx)) { + return Poll::Ready(Err(e)); + } + + // Step the state machine + if let Err(e) = self.as_mut().step() { + return Poll::Ready(Err(e)); + } + + if self.state.is_done() { + // If state machine is done, return trailer + return Poll::Ready(Ok(self.trailer.take())); + } else if self.finished_stream { + // If stream is finished but state machine is not done, return error + return Poll::Ready(Err(PostGrpcWebErr::InvalidRequest("Bad response".to_string()))); + } + } + } +} + +/// Represents a stream of bytes for the response body. +pub struct BodyStream { + body_stream: Pin>>>, +} + +impl BodyStream { + /// Creates a new `BodyStream` based on an `ReadableStreamDefaultReader`. + pub async fn new(body_stream: ReadableStreamDefaultReader) -> Result { + let mut chunks = vec![]; + loop { + let value = JsFuture::from(body_stream.read()) + .await + .map_err(|err| PostGrpcWebErr::InvalidRequest(format!("{err:?}")))?; + let object: Object = value + .dyn_into() + .map_err(|err| PostGrpcWebErr::BadResponse(format!("{err:?}")))?; + let object_value = js_sys::Reflect::get(&object, &JsValue::from_str("value")) + .map_err(|err| PostGrpcWebErr::BadResponse(format!("{err:?}")))?; + let object_progress = js_sys::Reflect::get(&object, &JsValue::from_str("done")) + .map_err(|err| PostGrpcWebErr::BadResponse(format!("{err:?}")))?; + let chunk = Uint8Array::new(&object_value).to_vec(); + chunks.extend_from_slice(&chunk); + + if object_progress.as_bool().ok_or_else(|| { + PostGrpcWebErr::BadResponse("Expected done(bool) field in json object response".to_string()) + })? { + break; + } + } + + Ok(Self { + body_stream: Box::pin(stream::once(async { Ok(Bytes::from(chunks)) })), + }) + } + + /// Creates an empty `BodyStream`. + pub fn empty() -> Self { + let body_stream = empty(); + + Self { + body_stream: Box::pin(body_stream), + } + } +} + +// Implementations of the Body trait for ResponseBody and BodyStream. +impl Body for BodyStream { + type Data = Bytes; + + type Error = PostGrpcWebErr; + + fn poll_data(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>> { + self.body_stream.as_mut().poll_next(cx) + } + + fn poll_trailers(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll, Self::Error>> { + Poll::Ready(Ok(None)) + } +} + +// Additional safety traits for BodyStream. +unsafe impl Send for BodyStream {} +// Additional safety traits for BodyStream. +unsafe impl Sync for BodyStream {} diff --git a/mm2src/mm2_net/src/wasm_http.rs b/mm2src/mm2_net/src/wasm/http.rs similarity index 76% rename from mm2src/mm2_net/src/wasm_http.rs rename to mm2src/mm2_net/src/wasm/http.rs index 66362d60e3..2175df21c7 100644 --- a/mm2src/mm2_net/src/wasm_http.rs +++ b/mm2src/mm2_net/src/wasm/http.rs @@ -1,10 +1,13 @@ use crate::transport::{GetInfoFromUriError, SlurpError, SlurpResult}; +use crate::wasm::body_stream::ResponseBody; use common::executor::spawn_local; -use common::{stringify_js_error, APPLICATION_JSON}; +use common::{drop_mutability, stringify_js_error, APPLICATION_JSON}; use futures::channel::oneshot; use gstuff::ERRL; use http::header::{ACCEPT, CONTENT_TYPE}; -use http::{HeaderMap, StatusCode}; +use http::response::Builder; +use http::{HeaderMap, Response, StatusCode}; +use js_sys::Array; use js_sys::Uint8Array; use mm2_err_handle::prelude::*; use serde_json::Value as Json; @@ -12,7 +15,7 @@ use std::collections::HashMap; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; -use web_sys::{Request, RequestInit, RequestMode, Response as JsResponse}; +use web_sys::{Request as JsRequest, RequestInit, RequestMode, Response as JsResponse}; /// The result containing either a pair of (HTTP status code, body) or a stringified error. pub type FetchResult = Result<(StatusCode, T), MmError>; @@ -48,6 +51,40 @@ pub async fn slurp_post_json(url: &str, body: String) -> SlurpResult { .map(|(status_code, response)| (status_code, HeaderMap::new(), response.into_bytes())) } +/// Sets the response headers and extracts the content type. +/// +/// This function takes a `Builder` for a response and a `JsResponse` from which it extracts +/// the headers and the content type. +fn set_response_headers_and_content_type( + mut result: Builder, + response: &JsResponse, +) -> Result<(Builder, String), MmError> { + let headers = match js_sys::try_iter(response.headers().as_ref()) { + Ok(Some(headers)) => headers, + Ok(None) => return MmError::err(SlurpError::InvalidRequest("MissingHeaders".to_string())), + Err(err) => return MmError::err(SlurpError::InvalidRequest(format!("{err:?}"))), + }; + + let mut content_type = None; + for header in headers { + let pair: Array = header + .map_to_mm(|err| SlurpError::InvalidRequest(format!("{err:?}")))? + .into(); + if let (Some(header_name), Some(header_value)) = (pair.get(0).as_string(), pair.get(1).as_string()) { + if header_name == CONTENT_TYPE.as_str() { + content_type = Some(header_value.clone()); + } + result = result.header(header_name, header_value); + } + } + drop_mutability!(content_type); + + match content_type { + Some(content_type) => Ok((result, content_type)), + None => MmError::err(SlurpError::InvalidRequest("MissingContentType".to_string())), + } +} + pub struct FetchRequest { uri: String, method: FetchMethod, @@ -124,6 +161,13 @@ impl FetchRequest { } } + pub async fn fetch_stream_response(self) -> FetchResult> { + let (tx, rx) = oneshot::channel(); + Self::spawn_fetch_stream_response(self, tx); + rx.await + .map_to_mm(|_| SlurpError::Internal("Spawned future has been canceled".to_owned()))? + } + fn spawn_fetch_str(request: Self, tx: oneshot::Sender>) { let fut = async move { let result = Self::fetch_str(request).await; @@ -146,6 +190,17 @@ impl FetchRequest { spawn_local(fut); } + fn spawn_fetch_stream_response(request: Self, tx: oneshot::Sender>>) { + let fut = async move { + let result = Self::fetch_and_stream_response(request).await; + tx.send(result).ok(); + }; + + // The spawned future doesn't capture shared pointers, + // so we can use `spawn_local` here. + spawn_local(fut); + } + async fn fetch(request: Self) -> FetchResult { let window = web_sys::window().expect("!window"); let uri = request.uri; @@ -158,7 +213,7 @@ impl FetchRequest { req_init.mode(mode); } - let js_request = Request::new_with_str_and_init(&uri, &req_init) + let js_request = JsRequest::new_with_str_and_init(&uri, &req_init) .map_to_mm(|e| SlurpError::Internal(stringify_js_error(&e)))?; for (hkey, hval) in request.headers { js_request @@ -251,6 +306,35 @@ impl FetchRequest { Ok((status_code, array.to_vec())) } + + /// The private non-Send method that is called in a spawned future. + async fn fetch_and_stream_response(request: Self) -> FetchResult> { + let uri = request.uri.clone(); + let (status_code, js_response) = Self::fetch(request).await?; + + let resp_stream = match js_response.body() { + Some(txt) => txt, + None => { + return MmError::err(SlurpError::ErrorDeserializing { + uri, + error: format!("Expected readable stream, found {:?}:", js_response), + }); + }, + }; + + let builder = Response::builder().status(status_code); + let (builder, content_type) = set_response_headers_and_content_type(builder, &js_response)?; + let body = ResponseBody::new(resp_stream, &content_type) + .await + .map_to_mm(|err| SlurpError::InvalidRequest(format!("{err:?}")))?; + + Ok(( + status_code, + builder + .body(body) + .map_to_mm(|err| SlurpError::InvalidRequest(err.to_string()))?, + )) + } } enum FetchMethod { diff --git a/mm2src/mm2_net/src/wasm/mod.rs b/mm2src/mm2_net/src/wasm/mod.rs new file mode 100644 index 0000000000..a2c4efa3ed --- /dev/null +++ b/mm2src/mm2_net/src/wasm/mod.rs @@ -0,0 +1,4 @@ +pub mod body_stream; +pub mod http; +pub mod tonic_client; +pub mod wasm_ws; diff --git a/mm2src/mm2_net/src/wasm/tonic_client.rs b/mm2src/mm2_net/src/wasm/tonic_client.rs new file mode 100644 index 0000000000..84df389e73 --- /dev/null +++ b/mm2src/mm2_net/src/wasm/tonic_client.rs @@ -0,0 +1,53 @@ +use crate::grpc_web::PostGrpcWebErr; +use crate::wasm::body_stream::ResponseBody; +use crate::wasm::http::FetchRequest; + +use common::{APPLICATION_GRPC_WEB_PROTO, X_GRPC_WEB}; +use futures_util::Future; +use http::header::{ACCEPT, CONTENT_TYPE}; +use http::{Request, Response}; +use mm2_err_handle::prelude::{MmError, MmResult}; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tonic::body::BoxBody; +use tonic::codegen::Body; +use tower_service::Service; + +#[derive(Clone)] +pub struct TonicClient(String); + +impl TonicClient { + pub fn new(url: String) -> Self { Self(url) } +} + +impl Service> for TonicClient { + type Response = Response; + + type Error = MmError; + + type Future = Pin> + Send + 'static>>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } + + fn call(&mut self, request: Request) -> Self::Future { Box::pin(call(self.0.clone(), request)) } +} + +async fn call(base_url: String, request: Request) -> MmResult, PostGrpcWebErr> { + let base_url = format!("{base_url}{}", &request.uri().to_string()); + let body = request + .into_body() + .data() + .await + .transpose() + .map_err(|err| PostGrpcWebErr::Status(err.to_string()))?; + let body = body.ok_or_else(|| MmError::new(PostGrpcWebErr::InvalidRequest("Invalid request body".to_string())))?; + + Ok(FetchRequest::post(&base_url) + .body_bytes(body.to_vec()) + .header(CONTENT_TYPE.as_str(), APPLICATION_GRPC_WEB_PROTO) + .header(ACCEPT.as_str(), APPLICATION_GRPC_WEB_PROTO) + .header(X_GRPC_WEB, "1") + .fetch_stream_response() + .await? + .1) +} diff --git a/mm2src/mm2_net/src/wasm_ws.rs b/mm2src/mm2_net/src/wasm/wasm_ws.rs similarity index 100% rename from mm2src/mm2_net/src/wasm_ws.rs rename to mm2src/mm2_net/src/wasm/wasm_ws.rs diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index a2202d576a..5a2554089b 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -4,8 +4,8 @@ use crate::electrums::qtum_electrums; use crate::structs::*; use common::custom_futures::repeatable::{Ready, Retry}; use common::executor::Timer; -use common::log::debug; -use common::{cfg_native, now_float, now_ms, now_sec, repeatable, wait_until_ms, PagingOptionsEnum}; +use common::log::{debug, info}; +use common::{cfg_native, now_float, now_ms, now_sec, repeatable, wait_until_ms, wait_until_sec, PagingOptionsEnum}; use common::{get_utc_timestamp, log}; use crypto::{CryptoCtx, StandardHDCoinAddress}; use gstuff::{try_s, ERR, ERRL}; @@ -205,8 +205,18 @@ pub const ZOMBIE_LIGHTWALLETD_URLS: &[&str] = &[ "https://piratelightd3.cryptoforge.cc:443", "https://piratelightd4.cryptoforge.cc:443", ]; +#[cfg(not(target_arch = "wasm32"))] pub const PIRATE_ELECTRUMS: &[&str] = &["node1.chainkeeper.pro:10132"]; +#[cfg(target_arch = "wasm32")] +pub const PIRATE_ELECTRUMS: &[&str] = &[ + "electrum3.cipig.net:30008", + "electrum1.cipig.net:30008", + "electrum2.cipig.net:30008", +]; +#[cfg(not(target_arch = "wasm32"))] pub const PIRATE_LIGHTWALLETD_URLS: &[&str] = &["http://node1.chainkeeper.pro:443"]; +#[cfg(target_arch = "wasm32")] +pub const PIRATE_LIGHTWALLETD_URLS: &[&str] = &["http://pirate.battlefield.earth:8581"]; pub const DEFAULT_RPC_PASSWORD: &str = "pass"; pub const QRC20_ELECTRUMS: &[&str] = &[ "electrum1.cipig.net:10071", @@ -3102,6 +3112,33 @@ pub async fn get_locked_amount(mm: &MarketMakerIt, coin: &str) -> GetLockedAmoun response.result } +pub async fn enable_z_coin_light( + mm: &MarketMakerIt, + coin: &str, + electrums: &[&str], + lightwalletd_urls: &[&str], + account: Option, + starting_height: Option, +) -> ZCoinActivationResult { + let init = init_z_coin_light(mm, coin, electrums, lightwalletd_urls, starting_height, account).await; + let init: RpcV2Response = json::from_value(init).unwrap(); + let timeout = wait_until_sec(300); + + loop { + if now_sec() > timeout { + panic!("{} initialization timed out", coin); + } + let status = init_z_coin_status(mm, init.result.task_id).await; + info!("Status {}", json::to_string(&status).unwrap()); + let status: RpcV2Response = json::from_value(status).unwrap(); + match status.result { + InitZcoinStatus::Ok(result) => break result, + InitZcoinStatus::Error(e) => panic!("{} initialization error {:?}", coin, e), + _ => Timer::sleep(1.).await, + } + } +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_parse_env_file() {