diff --git a/Cargo.lock b/Cargo.lock index 14af4d5dc6..78cfcd86a6 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", ] @@ -408,9 +408,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -689,7 +689,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", ] @@ -699,8 +699,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", ] @@ -710,8 +710,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", ] @@ -770,8 +770,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", ] @@ -983,6 +983,7 @@ dependencies = [ "bitcoin", "bitcoin_hashes", "bitcrypto", + "blake2b_simd", "byteorder", "bytes 0.4.12", "cfg-if 1.0.0", @@ -1001,6 +1002,7 @@ dependencies = [ "ethcore-transaction", "ethereum-types", "ethkey", + "ff 0.8.0", "futures 0.1.29", "futures 0.3.28", "futures-util", @@ -1014,6 +1016,7 @@ dependencies = [ "itertools", "js-sys", "jsonrpc-core", + "jubjub", "keys", "lazy_static", "libc", @@ -1069,15 +1072,18 @@ dependencies = [ "spv_validation", "tendermint-config", "tendermint-rpc", + "time 0.3.20", "tiny-bip39", "tokio", "tokio-rustls", "tokio-tungstenite-wasm", "tonic", "tonic-build", + "tower-service", "url", "utxo_signer", "uuid 1.2.2", + "wagyu-zcash-parameters", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -1088,6 +1094,7 @@ dependencies = [ "zbase32", "zcash_client_backend", "zcash_client_sqlite", + "zcash_extras", "zcash_primitives", "zcash_proofs", ] @@ -1409,7 +1416,7 @@ dependencies = [ "crossbeam-utils 0.7.2", "lazy_static", "maybe-uninit", - "memoffset 0.5.4", + "memoffset 0.5.6", "scopeguard", ] @@ -1649,8 +1656,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", ] @@ -1667,8 +1674,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", ] @@ -1690,8 +1697,8 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "strsim 0.10.0", "syn 1.0.95", ] @@ -1703,7 +1710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", - "quote 1.0.28", + "quote 1.0.33", "syn 1.0.95", ] @@ -1789,8 +1796,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", ] @@ -1800,8 +1807,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", ] @@ -2045,8 +2052,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", ] @@ -2057,7 +2064,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", ] @@ -2066,8 +2073,8 @@ name = "enum_derives" 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", ] @@ -2087,7 +2094,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.0#debae777f20f5b5fd263e26877258f7600b52cab" dependencies = [ "blake2b_simd", "byteorder", @@ -2238,8 +2245,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", ] @@ -2500,9 +2507,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.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -2776,12 +2783,11 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.13.0", - "bitflags", + "base64 0.21.7", "bytes 1.4.0", "headers-core", "http 0.2.7", @@ -3153,8 +3159,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", ] @@ -3563,7 +3569,7 @@ version = "0.45.0" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" dependencies = [ "asynchronous-codec", - "base64 0.21.2", + "base64 0.21.7", "byteorder", "bytes 1.4.0", "either", @@ -3751,9 +3757,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.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -4119,9 +4125,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", ] @@ -4163,7 +4169,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" dependencies = [ - "base64 0.21.2", + "base64 0.21.7", "hyper", "indexmap 1.9.3", "ipnet", @@ -4181,9 +4187,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.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -4555,6 +4561,7 @@ version = "0.1.0" dependencies = [ "async-stream", "async-trait", + "base64 0.21.7", "bytes 1.4.0", "cfg-if 1.0.0", "common", @@ -4564,6 +4571,8 @@ dependencies = [ "futures-util", "gstuff", "http 0.2.7", + "http-body 0.4.5", + "httparse", "hyper", "js-sys", "lazy_static", @@ -4573,13 +4582,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", @@ -4624,7 +4637,7 @@ dependencies = [ "serde_bytes", "sha2 0.9.9", "smallvec 1.6.1", - "syn 2.0.23", + "syn 2.0.38", "tokio", "void", ] @@ -4701,8 +4714,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", ] @@ -4901,8 +4914,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", ] @@ -4965,20 +4978,11 @@ 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", ] -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - [[package]] name = "number_prefix" version = "0.4.0" @@ -5040,8 +5044,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", ] @@ -5086,8 +5090,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", ] @@ -5115,7 +5119,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", ] @@ -5237,8 +5241,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]] @@ -5287,9 +5291,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.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -5390,7 +5394,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", ] @@ -5444,8 +5448,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", ] @@ -5456,8 +5460,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", ] @@ -5467,9 +5471,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.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -5483,9 +5487,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", ] @@ -5508,8 +5512,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", ] @@ -5553,8 +5557,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", ] @@ -5673,11 +5677,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]] @@ -5874,6 +5878,7 @@ dependencies = [ "libc", "rand_core 0.4.2", "rdrand", + "wasm-bindgen", "winapi", ] @@ -5947,7 +5952,7 @@ checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" dependencies = [ "pem", "ring", - "time 0.3.11", + "time 0.3.20", "yasna", ] @@ -6011,8 +6016,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", ] @@ -6229,7 +6234,7 @@ dependencies = [ "hashlink", "libsqlite3-sys", "smallvec 1.6.1", - "time 0.3.11", + "time 0.3.20", ] [[package]] @@ -6366,7 +6371,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.21.2", + "base64 0.21.7", ] [[package]] @@ -6429,8 +6434,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", ] @@ -6622,17 +6627,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.164" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] @@ -6659,13 +6664,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.164" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -6686,19 +6691,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", ] @@ -6720,8 +6725,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ "darling", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -7190,8 +7195,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", ] @@ -7492,8 +7497,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", ] @@ -7616,8 +7621,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", ] @@ -7646,8 +7651,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", ] @@ -7794,8 +7799,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", @@ -7874,19 +7879,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.23" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "unicode-ident", ] @@ -7911,8 +7916,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", ] @@ -7995,7 +8000,7 @@ dependencies = [ "subtle", "subtle-encoding", "tendermint-proto", - "time 0.3.11", + "time 0.3.20", "zeroize", ] @@ -8028,7 +8033,7 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time 0.3.11", + "time 0.3.20", ] [[package]] @@ -8050,7 +8055,7 @@ dependencies = [ "tendermint-config", "tendermint-proto", "thiserror", - "time 0.3.11", + "time 0.3.20", "url", "uuid 0.8.2", "walkdir", @@ -8123,9 +8128,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.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -8140,21 +8145,31 @@ dependencies = [ [[package]] name = "time" -version = "0.3.11" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ "itoa 1.0.1", - "libc", - "num_threads", + "js-sys", + "serde", + "time-core", "time-macros", ] +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + [[package]] name = "time-macros" -version = "0.2.4" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] [[package]] name = "tiny-bip39" @@ -8253,9 +8268,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.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -8379,9 +8394,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", ] @@ -8455,9 +8470,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.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -8799,6 +8814,56 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "wagyu-zcash-parameters" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c904628658374e651288f000934c33ef738b2d8b3e65d4100b70b395dbe2bb" +dependencies = [ + "wagyu-zcash-parameters-1", + "wagyu-zcash-parameters-2", + "wagyu-zcash-parameters-3", + "wagyu-zcash-parameters-4", + "wagyu-zcash-parameters-5", + "wagyu-zcash-parameters-6", +] + +[[package]] +name = "wagyu-zcash-parameters-1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bf2e21bb027d3f8428c60d6a720b54a08bf6ce4e6f834ef8e0d38bb5695da8" + +[[package]] +name = "wagyu-zcash-parameters-2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616ab2e51e74cc48995d476e94de810fb16fc73815f390bf2941b046cc9ba2c" + +[[package]] +name = "wagyu-zcash-parameters-3" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14da1e2e958ff93c0830ee68e91884069253bf3462a67831b02b367be75d6147" + +[[package]] +name = "wagyu-zcash-parameters-4" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f058aeef03a2070e8666ffb5d1057d8bb10313b204a254a6e6103eb958e9a6d6" + +[[package]] +name = "wagyu-zcash-parameters-5" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffe916b30e608c032ae1b734f02574a3e12ec19ab5cc5562208d679efe4969d" + +[[package]] +name = "wagyu-zcash-parameters-6" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b6d5a78adc3e8f198e9cd730f219a695431467f7ec29dcfc63ade885feebe1" + [[package]] name = "waker-fn" version = "1.1.0" @@ -8857,9 +8922,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -8881,7 +8946,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", ] @@ -8891,9 +8956,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.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8924,8 +8989,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]] @@ -9389,7 +9454,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "time 0.3.11", + "time 0.3.20", ] [[package]] @@ -9401,8 +9466,9 @@ checksum = "0f9079049688da5871a7558ddacb7f04958862c703e68258594cb7a862b5e33f" [[package]] name = "zcash_client_backend" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.0#debae777f20f5b5fd263e26877258f7600b52cab" dependencies = [ + "async-trait", "base64 0.13.0", "bech32", "bls12_381", @@ -9417,7 +9483,7 @@ dependencies = [ "protobuf-codegen-pure", "rand_core 0.5.1", "subtle", - "time 0.3.11", + "time 0.3.20", "zcash_note_encryption", "zcash_primitives", ] @@ -9425,8 +9491,9 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.3.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.0#debae777f20f5b5fd263e26877258f7600b52cab" dependencies = [ + "async-trait", "bech32", "bs58 0.4.0", "ff 0.8.0", @@ -9436,7 +9503,25 @@ dependencies = [ "protobuf", "rand_core 0.5.1", "rusqlite", - "time 0.3.11", + "time 0.3.20", + "tokio", + "zcash_client_backend", + "zcash_extras", + "zcash_primitives", +] + +[[package]] +name = "zcash_extras" +version = "0.1.0" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.0#debae777f20f5b5fd263e26877258f7600b52cab" +dependencies = [ + "async-trait", + "ff 0.8.0", + "group 0.8.0", + "jubjub", + "protobuf", + "rand_core 0.5.1", + "time 0.3.20", "zcash_client_backend", "zcash_primitives", ] @@ -9444,7 +9529,7 @@ dependencies = [ [[package]] name = "zcash_note_encryption" version = "0.0.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.0#debae777f20f5b5fd263e26877258f7600b52cab" dependencies = [ "blake2b_simd", "byteorder", @@ -9458,7 +9543,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.0#debae777f20f5b5fd263e26877258f7600b52cab" dependencies = [ "aes", "bitvec 0.18.5", @@ -9488,7 +9573,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.0#debae777f20f5b5fd263e26877258f7600b52cab" dependencies = [ "bellman", "blake2b_simd", @@ -9518,8 +9603,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 4ce5597b4f..49e1be331f 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -107,9 +107,9 @@ uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } # We don't need the default web3 features at all since we added our own web3 transport using shared HYPER instance. web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.19.0", default-features = false } zbase32 = "0.1.2" -zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } -zcash_primitives = { features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } -zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } +zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.0" } +zcash_extras = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.0" } +zcash_primitives = {features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.0" } [target.'cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))'.dependencies] bincode = { version = "1.3.3", default-features = false, optional = true } @@ -121,15 +121,23 @@ 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" instant = "0.1.12" +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", features = ["wasm-bindgen"] } +tonic = { version = "0.7", default-features = false, features = ["prost", "codegen", "compression"] } +tower-service = "0.3" wasm-bindgen = "0.2.86" wasm-bindgen-futures = { version = "0.4.1" } wasm-bindgen-test = { version = "0.3.2" } web-sys = { version = "0.3.55", features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.0", default-features = false, features = ["local-prover"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dirs = { version = "1" } @@ -151,7 +159,8 @@ tokio = { version = "1.20" } tokio-rustls = { version = "0.23" } tonic = { version = "0.7", features = ["tls", "tls-webpki-roots", "compression"] } webpki-roots = { version = "0.22" } -zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } +zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.0" } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.0", default-features =false, features = ["local-prover", "multicore"] } [target.'cfg(windows)'.dependencies] winapi = "0.3" @@ -159,6 +168,9 @@ winapi = "0.3" [dev-dependencies] mm2_test_helpers = { path = "../mm2_test_helpers" } +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wagyu-zcash-parameters = { version = "0.2" } + [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/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 278ce1c124..a3fef246a6 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::{DexFee, IguanaPrivKey}; use common::{block_on, now_sec}; use crypto::privkey::key_pair_from_seed; +#[cfg(not(target_arch = "wasm32"))] use ethkey::{Generator, Random}; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_test_helpers::{for_tests::{eth_jst_testnet_conf, eth_testnet_conf, ETH_DEV_NODE, ETH_DEV_NODES, @@ -88,6 +89,7 @@ fn eth_coin_for_test( eth_coin_from_keypair(coin_type, urls, fallback_swap_contract, key_pair) } +#[cfg(not(target_arch = "wasm32"))] fn random_eth_coin_for_test( coin_type: EthCoinType, urls: &[&str], @@ -157,6 +159,7 @@ fn eth_coin_from_keypair( (ctx, eth_coin) } +#[cfg(not(target_arch = "wasm32"))] pub fn fill_eth(to_addr: Address, amount: f64) { let wei_per_eth: u64 = 1_000_000_000_000_000_000; let amount_in_wei = (amount * wei_per_eth as f64) as u64; @@ -464,6 +467,7 @@ fn test_gas_station() { assert_eq!(expected_eth_polygon, res_polygon); } +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_impl_manual_fee() { let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None); @@ -501,6 +505,7 @@ fn test_withdraw_impl_manual_fee() { assert_eq!(expected, tx_details.fee_details); } +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_impl_fee_details() { let (_ctx, coin) = eth_coin_for_test( 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/hd_wallet_storage/wasm_storage.rs b/mm2src/coins/hd_wallet_storage/wasm_storage.rs index 76ad67494f..d25363a854 100644 --- a/mm2src/coins/hd_wallet_storage/wasm_storage.rs +++ b/mm2src/coins/hd_wallet_storage/wasm_storage.rs @@ -94,11 +94,11 @@ pub struct HDAccountTable { } impl TableSignature for HDAccountTable { - fn table_name() -> &'static str { "hd_account" } + const TABLE_NAME: &'static str = "hd_account"; 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())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index(WALLET_ID_INDEX, &["coin", "hd_wallet_rmd160"], false)?; table.create_multi_index( WALLET_ACCOUNT_ID_INDEX, diff --git a/mm2src/coins/lp_price.rs b/mm2src/coins/lp_price.rs index e3835435db..5cf7e4c08f 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 613603ebd2..62ffa601b4 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 151d3af02d..9bd2a35e75 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/nft/storage/wasm/wasm_storage.rs b/mm2src/coins/nft/storage/wasm/wasm_storage.rs index 7cb1c9b998..aabe945d8c 100644 --- a/mm2src/coins/nft/storage/wasm/wasm_storage.rs +++ b/mm2src/coins/nft/storage/wasm/wasm_storage.rs @@ -895,11 +895,11 @@ impl NftListTable { } impl TableSignature for NftListTable { - fn table_name() -> &'static str { "nft_list_cache_table" } + const TABLE_NAME: &'static str = "nft_list_cache_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if is_initial_upgrade(old_version, new_version) { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index( CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, &["chain", "token_address", "token_id"], @@ -976,11 +976,11 @@ impl NftTransferHistoryTable { } impl TableSignature for NftTransferHistoryTable { - fn table_name() -> &'static str { "nft_transfer_history_cache_table" } + const TABLE_NAME: &'static str = "nft_transfer_history_cache_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if is_initial_upgrade(old_version, new_version) { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index( CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, &["chain", "token_address", "token_id"], @@ -1009,11 +1009,11 @@ pub(crate) struct LastScannedBlockTable { } impl TableSignature for LastScannedBlockTable { - fn table_name() -> &'static str { "last_scanned_block_table" } + const TABLE_NAME: &'static str = "last_scanned_block_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if is_initial_upgrade(old_version, new_version) { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_index("chain", true)?; } Ok(()) diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 70204b5a0f..912222d7bc 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -1,18 +1,22 @@ use super::*; -use crate::utxo::rpc_clients::UnspentInfo; use crate::{DexFee, TxFeeDetails, WaitForHTLCTxSpendArgs}; -use chain::OutPoint; use common::{block_on, wait_until_sec, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::Secp256k1Secret; use itertools::Itertools; use keys::{Address, AddressBuilder}; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_number::bigdecimal::Zero; -use mocktopus::mocking::{MockResult, Mockable}; use rpc::v1::types::ToTxHash; use std::convert::TryFrom; use std::mem::discriminant; +cfg_native!( + use crate::utxo::rpc_clients::UnspentInfo; + + use mocktopus::mocking::{MockResult, Mockable}; + use chain::OutPoint; +); + const EXPECTED_TX_FEE: i64 = 1000; const CONTRACT_CALL_GAS_FEE: i64 = (QRC20_GAS_LIMIT_DEFAULT * QRC20_GAS_PRICE_DEFAULT) as i64; const SWAP_PAYMENT_GAS_FEE: i64 = (QRC20_PAYMENT_GAS_LIMIT * QRC20_GAS_PRICE_DEFAULT) as i64; @@ -58,6 +62,7 @@ fn check_tx_fee(coin: &Qrc20Coin, expected_tx_fee: ActualTxFee) { assert_eq!(actual_tx_fee, expected_tx_fee); } +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_to_p2sh_address_should_fail() { let priv_key = [ @@ -91,6 +96,7 @@ fn test_withdraw_to_p2sh_address_should_fail() { assert_eq!(err, expect); } +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_impl_fee_details() { Qrc20Coin::get_unspent_ordered_list.mock_safe(|coin, _| { diff --git a/mm2src/coins/rpc_command/init_withdraw.rs b/mm2src/coins/rpc_command/init_withdraw.rs index e7c6d94ae2..2483511925 100644 --- a/mm2src/coins/rpc_command/init_withdraw.rs +++ b/mm2src/coins/rpc_command/init_withdraw.rs @@ -132,7 +132,6 @@ impl RpcTask for WithdrawTask { match self.coin { MmCoinEnum::UtxoCoin(ref standard_utxo) => standard_utxo.init_withdraw(ctx, request, task_handle).await, MmCoinEnum::QtumCoin(ref qtum) => qtum.init_withdraw(ctx, request, task_handle).await, - #[cfg(not(target_arch = "wasm32"))] MmCoinEnum::ZCoin(ref z) => z.init_withdraw(ctx, request, task_handle).await, _ => MmError::err(WithdrawError::CoinDoesntSupportInitWithdraw { coin: self.coin.ticker().to_owned(), 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 d0e147a30e..5ac52ef31d 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -158,14 +158,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/tx_history_storage/wasm/tx_history_storage_v1.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs index 90ef077cde..c52fefd76d 100644 --- a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs @@ -73,11 +73,11 @@ pub(crate) struct TxHistoryTableV1 { } impl TableSignature for TxHistoryTableV1 { - fn table_name() -> &'static str { "tx_history" } + const TABLE_NAME: &'static str = "tx_history"; 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())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_index("history_id", true)?; } diff --git a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs index 1b19565bf7..b55b04ad86 100644 --- a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs @@ -414,11 +414,11 @@ impl TxHistoryTableV2 { } impl TableSignature for TxHistoryTableV2 { - fn table_name() -> &'static str { "tx_history_v2" } + const TABLE_NAME: &'static str = "tx_history_v2"; 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())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index(TxHistoryTableV2::WALLET_ID_INDEX, &["coin", "hd_wallet_rmd160"], false)?; table.create_multi_index( TxHistoryTableV2::WALLET_ID_INTERNAL_ID_INDEX, @@ -468,11 +468,11 @@ impl TxCacheTableV2 { } impl TableSignature for TxCacheTableV2 { - fn table_name() -> &'static str { "tx_cache_v2" } + const TABLE_NAME: &'static str = "tx_cache_v2"; 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())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index(TxCacheTableV2::COIN_TX_HASH_INDEX, &["coin", "tx_hash"], true)?; } Ok(()) diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index ccddca9924..56a1c9289c 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -2873,7 +2873,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/utxo/utxo_block_header_storage/wasm/block_header_table.rs b/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs index 315a94cdd8..756e50614f 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs @@ -15,11 +15,11 @@ impl BlockHeaderStorageTable { } impl TableSignature for BlockHeaderStorageTable { - fn table_name() -> &'static str { "block_header_storage_cache_table" } + const TABLE_NAME: &'static str = "block_header_storage_cache_table"; 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())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index(Self::TICKER_HEIGHT_INDEX, &["ticker", "height"], true)?; table.create_multi_index(Self::HASH_TICKER_INDEX, &["hash", "ticker"], true)?; table.create_index("ticker", false)?; diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 6f10e9d791..188a644ba6 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -770,16 +770,16 @@ pub trait UtxoCoinBuilderCommonOps { .ok_or_else(|| format!("avg_blocktime not specified in {} coin config", self.ticker())) .map_to_mm(UtxoCoinBuildError::ErrorCalculatingStartingHeight)?; let blocks_per_day = DAY_IN_SECONDS / avg_blocktime; - let current_time_s = now_sec(); + let current_time_sec = now_sec(); - if current_time_s < date_s { + if current_time_sec < date_s { return MmError::err(UtxoCoinBuildError::ErrorCalculatingStartingHeight(format!( "{} sync date must be earlier then current date", self.ticker() ))); }; - let secs_since_date = current_time_s - date_s; + let secs_since_date = current_time_sec - date_s; let days_since_date = (secs_since_date / DAY_IN_SECONDS) - 1; let blocks_to_sync = (days_since_date * blocks_per_day) + blocks_per_day; diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index ce107259b3..a40aaec887 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -27,10 +27,11 @@ use crate::utxo::utxo_common_tests::TEST_COIN_DECIMALS; use crate::utxo::utxo_common_tests::{self, utxo_coin_fields_for_test, utxo_coin_from_fields, TEST_COIN_NAME}; use crate::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; use crate::utxo::utxo_tx_history_v2::{UtxoTxDetailsParams, UtxoTxHistoryOps}; -#[cfg(not(target_arch = "wasm32"))] use crate::WithdrawFee; use crate::{BlockHeightAndTime, CoinBalance, ConfirmPaymentInput, DexFee, IguanaPrivKey, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, SpendPaymentArgs, StakingInfosDetails, SwapOps, TradePreimageValue, - TxFeeDetails, TxMarshalingErr, ValidateFeeArgs, WaitForHTLCTxSpendArgs, INVALID_SENDER_ERR_LOG}; + TxFeeDetails, TxMarshalingErr, ValidateFeeArgs, INVALID_SENDER_ERR_LOG}; +#[cfg(not(target_arch = "wasm32"))] +use crate::{WaitForHTLCTxSpendArgs, WithdrawFee}; use chain::{BlockHeader, BlockHeaderBits, OutPoint}; use common::executor::Timer; use common::{block_on, wait_until_sec, OrdRange, PagingOptionsEnum, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -4521,6 +4522,7 @@ fn test_spv_conf_with_verification() { .contains("max_stored_block_headers 2000 must be greater than retargeting interval")); } +#[cfg(not(target_arch = "wasm32"))] fn rick_blocker_5() -> BlockHeader { let header = "0400000028a4f1aa8be606c8bf8195b2e95d478a83314ff9ad7b017457d9e58d00d1710bb43f41db65677e3fdb83ddbd8cfb4a7ad2e110f74bc19726dc949576e003a1ecfbc2f4300c01f0b7820d00e3347c8da4ee614674376cbc45359daa54f9b5493e381b405d0f0f0f2001003cfb15008ad9f4fab1ff4076f8919f743193f007c0db28f5106e003b0000fd400500acba878991f600ed8c022758be9ff9752ef175e7530324df4d1b87f5a03ca5c2c3fce10b08743bd5ba03912703b8f305f7dd382487d437d9b1823cdc11a00f59a20b235ef57502a0a7ad6fc7d3d242e8f4477a01fb8834ac4dc6e2e40e4909f9edc0db07c0f98df40e5a61327311b005c98a727694ebaabcb366b92dda4af9e3f6e72c5461dd81d6daccbd1fca8ec17597df7585947b54deb83554859776b5bcefadfa566ff12c04ac624f9416e76beccec35694ae0ed11dc17a911f114225be62cf5b971628f364f57d8348d95fdc415b0d2a7a477ea130d3320108739edf761f85f81efd6c0e4eafa8166b05bd74af7928b0786b63ae499dba38065be13e7541b7f4e26727d0fa6887e265e09709b940ca87295ce5984de7d4058b5d340b162935fa46ee20cac955379e3c8fa1ff92fb354bb2a0fedf697b683a5875f4ed2bcef984d296b0c1e07a52920f1dd5a60140c7c1245a52ed196df3292db8bfff52923b0a8615b6a99a5fcf1e5f461f01a04b1c3bb517fe16553e1f8e8aa20bd3cc2cac6d3242a2ce373737b57cec4637907fd236e0d44d91d59533484ec23634b93645c10a858d83805d731f300aa27a162e172216d7fc21170b4d232767e4c66f9a871224f13480e89c2edb0e6e1ef5cf75d9203839cc0282fd7852319232057f30793bb5552d94ebf3ffcc67b73f44e80c3de79b9d8d7f0175939722054bc2ddfb84288dff8c7554f191d6ee1b65c40b75d4435712d4e88c64d6379ab7e578bcd8117501504faa7a3be3a6a2826fd7a3e5e9efb1d3642937f3a35be5793be8e1d4acf9dd2dcd356d6e4c7d0c8b87587b8ad901b9ce71792ae0bdae27811b52300e6809e4691bfc7f738252e7c197e228cce5fda6130f8f518e5059530b731fe8afbf51308aa8da3bd31b1d1eb22cca1a896aed281397925265cd861a7eadb80124363dec8cb508aea7c277f04b9841888dd932471349e651ce2622a59065932f463ffce6b19a975d6914336ab49394afd17dfb9a448157007ea1437b1483587bc7de0dec5103cafad76704e91e9ea2b0b9a8570b935d5c65478e7195b08161be4625b8d5fd3658e6164cf2d6898ecbf1f14945fdd75bb991a3d9ffac713a3a7a81a31a765b9c37a578976aa15e66c97c957f4651dc5fc492c2111d8724d375a8293a36e0ddcf2a01facf30401d8677611522882e1447e4c8be5fa9ad073fb3fdcc6f673981484089090fe4c05bfaae173503e0f99c7407b297852d216463924d365d26b4cd63401a46bd7ed969ddb235044eb2373645144976c7f713720c0238ade9d3aae1d2b153e82d093232d4b12b2108ec564ae0e855e09252f1434c28d90bb298ab6d1750498bf90d93c8797901911548b81af1ba185be52c0dff9c1b11812941d2d527c95c4382879298f364077710b5efd56d1bf39148aedc4fcd9e8bddb4c36a3f901dc11f9493d1fbdfe80c88fa8866c1465c939c0d71cb57e78822b5fc3023578aa2d6b9cd3ebaa54f22876b935f251183d8a68459cab30cd19bcb4e4c1e1a5a83e4687a4795dc23732e81b9f024f70db96e412831d26e61d4fa292a95648e0b614d9a148cd852df1bf26a34ea971e63f8c634133ab7b13ac8045f6d6e20af2313b38d12cb8cee54a7aba7a7cd7e8b1b5e0b0931d4665a0bb36b63f325161b571fdd4f159f470e443e9b0cfb193bf4eea5fa9715dc6132cb8ed97f7f097837471a5147d14f2066cd3dcd50460d70180a7a24e2b5b9ab20caf952d2ea1b51747afec975f76d0313a98e444f20938bf709530960f9fbf5af9857cbe3410d37f3cba10ff57642861586b7c1b1c57019602f1529df9d6e45ca2f7663519c58915e9e299d5beee73cb4553238566844f571374d3f6a247dd8ecbbc893"; @@ -4528,6 +4530,7 @@ fn rick_blocker_5() -> BlockHeader { BlockHeader::try_from_string_with_coin_variant(header.to_string(), "RICK".into()).unwrap() } +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_block_header_utxo_loop_with_reorg() { use crate::utxo::utxo_builder::{block_header_utxo_loop, BlockHeaderUtxoLoopExtraArgs}; diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index cdd34171e9..3ec97cd558 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1,20 +1,19 @@ use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; #[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::WithdrawTaskHandleShared; -#[cfg(not(target_arch = "wasm32"))] -use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawInProgressStatus}; +use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::utxo::rpc_clients::{ElectrumRpcRequest, UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; 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, @@ -28,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}; @@ -63,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; @@ -83,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::*; @@ -109,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 { @@ -131,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)] @@ -351,39 +354,39 @@ impl ZCoin { async fn my_balance_sat(&self) -> Result> { let wallet_db = self.z_fields.light_wallet_db.clone(); async_blocking(move || { - let balance = get_balance(&wallet_db.db.lock(), AccountId::default())?.into(); + let db_guard = wallet_db.db.inner(); + let db_guard = db_guard.lock().unwrap(); + let balance = get_balance(&db_guard, AccountId::default())?.into(); Ok(balance) }) .await } #[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 guard = wallet_db.db.lock(); - let latest_db_block = match guard - .block_height_extrema() - .map_err(|err| SpendableNotesError::DBClientError(err.to_string()))? - { - Some((_, latest)) => latest, - None => return Ok(Vec::new()), - }; - get_spendable_notes(&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?; @@ -401,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, @@ -488,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())?, @@ -504,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, @@ -539,7 +539,8 @@ impl ZCoin { ) -> Result> { let wallet_db = self.z_fields.light_wallet_db.clone(); async_blocking(move || { - let db_guard = wallet_db.db.lock(); + let db_guard = wallet_db.db.inner(); + let db_guard = db_guard.lock().unwrap(); let conn = db_guard.sql_conn(); let total_sql = SqlBuilder::select_from(TRANSACTIONS_TABLE) @@ -765,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. +pub(crate) struct TxBuilderSpawner { + pub(crate) 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. + pub(crate) 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)] @@ -774,12 +825,13 @@ 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, } // ZcoinRpcMode reprs available RPC modes for interacting with the Zcoin network. It includes /// modes for both native and light client, each with their own configuration options. +#[allow(unused)] #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "rpc", content = "rpc_data")] pub enum ZcoinRpcMode { @@ -793,6 +845,9 @@ pub enum ZcoinRpcMode { /// Specifies the parameters for synchronizing the wallet from a specific block. This overrides the /// `CheckPointBlockInfo` configuration in the coin settings. sync_params: Option, + /// Indicates that synchronization parameters will be skipped and continue sync from last synced block. + /// Will use `sync_params` if no last synced block found. + skip_sync_params: Option, }, } @@ -810,7 +865,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, @@ -819,6 +873,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( @@ -834,14 +891,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); @@ -923,16 +980,16 @@ 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 => { - let native_client = self.native_client()?; - init_native_client(&self, native_client, blocks_db, &z_spending_key).await? + init_native_client(&self, self.native_client()?, blocks_db, &z_spending_key).await? }, ZcoinRpcMode::Light { light_wallet_d_servers, sync_params, + skip_sync_params, .. } => { init_light_client( @@ -940,6 +997,7 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { light_wallet_d_servers.clone(), blocks_db, sync_params, + skip_sync_params.unwrap_or_default(), &z_spending_key, ) .await? @@ -957,12 +1015,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) + }) } } @@ -1012,12 +1068,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()))) + + BlockDbImpl::new(&ctx, ticker, cache_db_path) + .map_err(|err| MmError::new(ZcoinClientInitError::ZcoinStorageError(err.to_string()))) .await } @@ -1041,7 +1098,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) + .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`. @@ -1481,10 +1561,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, @@ -1495,6 +1571,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<()> { @@ -1927,7 +2007,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.rs b/mm2src/coins/z_coin/storage.rs index 548ea0303f..5b1f3f1f00 100644 --- a/mm2src/coins/z_coin/storage.rs +++ b/mm2src/coins/z_coin/storage.rs @@ -1,5 +1,205 @@ +use crate::z_coin::{ValidateBlocksError, ZcoinConsensusParams, ZcoinStorageError}; + pub mod blockdb; pub use blockdb::*; pub mod walletdb; pub use walletdb::*; + +use mm2_err_handle::mm_error::MmResult; +#[cfg(target_arch = "wasm32")] +use walletdb::wasm::storage::DataConnStmtCacheWasm; +#[cfg(debug_assertions)] +use zcash_client_backend::data_api::error::Error; +use zcash_client_backend::data_api::PrunedBlock; +use zcash_client_backend::proto::compact_formats::CompactBlock; +use zcash_client_backend::wallet::{AccountId, WalletTx}; +use zcash_client_backend::welding_rig::scan_block; +#[cfg(not(target_arch = "wasm32"))] +use zcash_client_sqlite::for_async::DataConnStmtCacheAsync; +use zcash_extras::{WalletRead, WalletWrite}; +use zcash_primitives::block::BlockHash; +use zcash_primitives::consensus::BlockHeight; +use zcash_primitives::merkle_tree::CommitmentTree; +use zcash_primitives::sapling::Nullifier; +use zcash_primitives::zip32::ExtendedFullViewingKey; + +pub type ZcoinStorageRes = MmResult; + +#[derive(Clone)] +pub struct DataConnStmtCacheWrapper { + #[cfg(not(target_arch = "wasm32"))] + cache: DataConnStmtCacheAsync, + #[cfg(target_arch = "wasm32")] + cache: DataConnStmtCacheWasm, +} + +impl DataConnStmtCacheWrapper { + #[cfg(not(target_arch = "wasm32"))] + pub fn new(cache: DataConnStmtCacheAsync) -> Self { Self { cache } } + #[cfg(target_arch = "wasm32")] + pub fn new(cache: DataConnStmtCacheWasm) -> Self { Self { cache } } + #[cfg(not(target_arch = "wasm32"))] + #[inline] + pub fn inner(&self) -> &DataConnStmtCacheAsync { &self.cache } + #[cfg(target_arch = "wasm32")] + #[inline] + pub fn inner(&self) -> &DataConnStmtCacheWasm { &self.cache } +} + +pub struct CompactBlockRow { + pub(crate) height: BlockHeight, + pub(crate) data: Vec, +} + +#[derive(Clone)] +pub enum BlockProcessingMode { + Validate, + Scan(DataConnStmtCacheWrapper), +} + +/// Checks that the scanned blocks in the data database, when combined with the recent +/// `CompactBlock`s in the cache database, form a valid chain. +/// +/// This function is built on the core assumption that the information provided in the +/// cache database is more likely to be accurate than the previously-scanned information. +/// This follows from the design (and trust) assumption that the `lightwalletd` server +/// provides accurate block information as of the time it was requested. +/// +pub async fn validate_chain( + block: CompactBlock, + prev_height: &mut BlockHeight, + prev_hash: &mut Option, +) -> Result<(), ValidateBlocksError> { + let current_height = block.height(); + if current_height != *prev_height + 1 { + Err(ValidateBlocksError::block_height_discontinuity( + *prev_height + 1, + current_height, + )) + } else if prev_hash.is_none() || (prev_hash.as_ref() == Some(&block.prev_hash())) { + Ok(()) + } else { + Err(ValidateBlocksError::prev_hash_mismatch(current_height)) + }?; + + *prev_height = current_height; + *prev_hash = Some(block.hash()); + + Ok(()) +} + +/// Scans new blocks added to the cache for any transactions received by +/// the tracked accounts. +/// +/// This function returns without error after scanning new blocks, allowing +/// the caller to update their UI with scanning progress. Repeatedly calling this +/// function will process sequential ranges of blocks. +/// +/// The function focuses on cached blocks with heights greater than the +/// highest scanned block in `data`. Cached blocks with lower heights are not +/// verified against previously-scanned blocks. This function **assumes** that +/// the caller is handling rollbacks. +/// +/// For brand-new light client databases, the function starts scanning from the +/// Sapling activation height. This height can be fast-forwarded to a more recent +/// block by initializing the client database with a starting block (e.g., calling +/// `init_blocks_table` before this function if using `zcash_client_sqlite`). +/// +/// Scanned blocks are required to be height-sequential. If a block is missing from +/// the cache, an error will be returned with kind [`ChainInvalid::BlockHeightDiscontinuity`]. +/// +pub async fn scan_cached_block( + data: &DataConnStmtCacheWrapper, + params: &ZcoinConsensusParams, + block: &CompactBlock, + last_height: &mut BlockHeight, +) -> Result<(), ValidateBlocksError> { + let mut data_guard = data.inner().clone(); + // Fetch the ExtendedFullViewingKeys we are tracking + let extfvks = data_guard.get_extended_full_viewing_keys().await?; + let extfvks: Vec<(&AccountId, &ExtendedFullViewingKey)> = extfvks.iter().collect(); + + // Get the most recent CommitmentTree + let mut tree = data_guard + .get_commitment_tree(*last_height) + .await + .map(|t| t.unwrap_or_else(CommitmentTree::empty))?; + // Get most recent incremental witnesses for the notes we are tracking + let mut witnesses = data_guard.get_witnesses(*last_height).await?; + + // Get the nullifiers for the notes we are tracking + let mut nullifiers = data_guard.get_nullifiers().await?; + + let current_height = block.height(); + // Scanned blocks MUST be height-sequential. + if current_height != (*last_height + 1) { + return Err(ValidateBlocksError::block_height_discontinuity( + *last_height + 1, + current_height, + )); + } + + let txs: Vec> = { + let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.1).collect(); + scan_block( + params, + block.clone(), + &extfvks, + &nullifiers, + &mut tree, + &mut witness_refs[..], + ) + }; + + // Enforce that all roots match. This is slow, so only include in debug builds. + #[cfg(debug_assertions)] + { + let cur_root = tree.root(); + if witnesses.iter().any(|row| row.1.root() != cur_root) { + return Err(Error::InvalidWitnessAnchor(row.0, current_height).into()); + } + for tx in &txs { + for output in tx.shielded_outputs.iter() { + if output.witness.root() != cur_root { + return Err(Error::InvalidNewWitnessAnchor( + output.index, + tx.txid, + current_height, + output.witness.root(), + ) + .into()); + } + } + } + } + + let new_witnesses = data_guard + .advance_by_block( + &(PrunedBlock { + block_height: current_height, + block_hash: BlockHash::from_slice(&block.hash), + block_time: block.time, + commitment_tree: &tree, + transactions: &txs, + }), + &witnesses, + ) + .await?; + + let spent_nf: Vec = txs + .iter() + .flat_map(|tx| tx.shielded_spends.iter().map(|spend| spend.nf)) + .collect(); + nullifiers.retain(|(_, nf)| !spent_nf.contains(nf)); + nullifiers.extend( + txs.iter() + .flat_map(|tx| tx.shielded_outputs.iter().map(|out| (out.account, out.nf))), + ); + + witnesses.extend(new_witnesses); + + *last_height = current_height; + + Ok(()) +} diff --git a/mm2src/coins/z_coin/storage/blockdb/block_idb.rs b/mm2src/coins/z_coin/storage/blockdb/block_idb.rs deleted file mode 100644 index a1d4eda6d9..0000000000 --- a/mm2src/coins/z_coin/storage/blockdb/block_idb.rs +++ /dev/null @@ -1,52 +0,0 @@ -use async_trait::async_trait; -use mm2_db::indexed_db::{BeBigUint, DbIdentifier, DbInstance, DbUpgrader, IndexedDb, IndexedDbBuilder, InitDbResult, - OnUpgradeResult, TableSignature}; - -const DB_VERSION: u32 = 1; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct BlockDbTable { - height: BeBigUint, - data: Vec, - ticker: String, -} - -impl BlockDbTable { - pub const TICKER_HEIGHT_INDEX: &str = "block_height_ticker_index"; -} - -impl TableSignature for BlockDbTable { - fn table_name() -> &'static str { "compactblocks" } - - 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::TICKER_HEIGHT_INDEX, &["ticker", "height"], true)?; - table.create_index("ticker", false)?; - } - Ok(()) - } -} - -pub struct BlockDbInner { - pub inner: IndexedDb, -} - -impl BlockDbInner { - pub fn _get_inner(&self) -> &IndexedDb { &self.inner } -} - -#[async_trait] -impl DbInstance for BlockDbInner { - const DB_NAME: &'static str = "z_compactblocks_cache"; - - async fn init(db_id: DbIdentifier) -> InitDbResult { - let inner = IndexedDbBuilder::new(db_id) - .with_version(DB_VERSION) - .with_table::() - .build() - .await?; - - Ok(Self { inner }) - } -} diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs new file mode 100644 index 0000000000..112fb67400 --- /dev/null +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs @@ -0,0 +1,261 @@ +use crate::z_coin::storage::{scan_cached_block, validate_chain, BlockDbImpl, BlockProcessingMode, CompactBlockRow, + ZcoinConsensusParams, ZcoinStorageRes}; +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, ConstructibleDb, DbIdentifier, DbInstance, DbLocked, DbUpgrader, IndexedDb, + IndexedDbBuilder, InitDbResult, MultiIndex, OnUpgradeResult, TableSignature}; +use mm2_err_handle::prelude::*; +use protobuf::Message; +use std::path::PathBuf; +use zcash_client_backend::proto::compact_formats::CompactBlock; +use zcash_extras::WalletRead; +use zcash_primitives::block::BlockHash; +use zcash_primitives::consensus::BlockHeight; + +const DB_NAME: &str = "z_compactblocks_cache"; +const DB_VERSION: u32 = 1; + +pub type BlockDbInnerLocked<'a> = DbLocked<'a, BlockDbInner>; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BlockDbTable { + height: u32, + data: Vec, + ticker: String, +} + +impl BlockDbTable { + pub const TICKER_HEIGHT_INDEX: &'static str = "ticker_height_index"; +} + +impl TableSignature for BlockDbTable { + const TABLE_NAME: &'static str = "compactblocks"; + + 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::TICKER_HEIGHT_INDEX, &["ticker", "height"], true)?; + table.create_index("ticker", false)?; + table.create_index("height", false)?; + } + Ok(()) + } +} + +pub struct BlockDbInner(IndexedDb); + +#[async_trait] +impl DbInstance for BlockDbInner { + 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 BlockDbInner { + pub fn get_inner(&self) -> &IndexedDb { &self.0 } +} + +impl BlockDbImpl { + pub async fn new(ctx: &MmArc, ticker: String, _path: PathBuf) -> ZcoinStorageRes { + Ok(Self { + db: ConstructibleDb::new(ctx).into_shared(), + ticker, + }) + } + + async fn lock_db(&self) -> ZcoinStorageRes> { + self.db + .get_or_initialize() + .await + .mm_err(|err| ZcoinStorageError::DbError(err.to_string())) + } + + /// Get latest block of the current active ZCOIN. + pub async fn get_latest_block(&self) -> ZcoinStorageRes { + let ticker = self.ticker.clone(); + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let block_db = db_transaction.table::().await?; + let maybe_height = block_db + .cursor_builder() + .only("ticker", &ticker)? + .bound("height", 0u32, u32::MAX) + .reverse() + .where_first() + .open_cursor(BlockDbTable::TICKER_HEIGHT_INDEX) + .await? + .next() + .await?; + + Ok(maybe_height.map(|(_, item)| item.height).unwrap_or_else(|| 0)) + } + + /// Insert new block to BlockDbTable given the provided data. + pub async fn insert_block(&self, height: u32, cb_bytes: Vec) -> ZcoinStorageRes { + let ticker = self.ticker.clone(); + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let block_db = db_transaction.table::().await?; + + let indexes = MultiIndex::new(BlockDbTable::TICKER_HEIGHT_INDEX) + .with_value(&ticker)? + .with_value(BeBigUint::from(height))?; + let block = BlockDbTable { + height, + data: cb_bytes, + ticker, + }; + + Ok(block_db + .add_item_or_ignore_by_unique_multi_index(indexes, &block) + .await? + .get_id() as usize) + } + + /// Asynchronously rewinds the storage to a specified block height, effectively + /// removing data beyond the specified height from the storage. + pub async fn rewind_to_height(&self, height: BlockHeight) -> ZcoinStorageRes { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let block_db = db_transaction.table::().await?; + + let blocks = block_db + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("height", 0u32, u32::MAX) + .reverse() + .open_cursor(BlockDbTable::TICKER_HEIGHT_INDEX) + .await? + .collect() + .await?; + + for (_, block) in &blocks { + if block.height > u32::from(height) { + block_db + .delete_item_by_unique_multi_index( + MultiIndex::new(BlockDbTable::TICKER_HEIGHT_INDEX) + .with_value(&self.ticker)? + .with_value(block.height)?, + ) + .await?; + } + } + + Ok(blocks.last().map(|(_, block)| block.height).unwrap_or_default() as usize) + } + + #[allow(unused)] + pub(crate) async fn get_earliest_block(&self) -> ZcoinStorageRes { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let block_db = db_transaction.table::().await?; + let maybe_min_block = block_db + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("height", 0u32, u32::MAX) + .where_first() + .open_cursor(BlockDbTable::TICKER_HEIGHT_INDEX) + .await? + .next() + .await?; + + Ok(maybe_min_block.map(|(_, b)| b.height).unwrap_or(0)) + } + + /// Queries and retrieves a list of `CompactBlockRow` records from the database, starting + /// from a specified block height and optionally limited by a maximum number of blocks. + pub async fn query_blocks_by_limit( + &self, + from_height: BlockHeight, + limit: Option, + ) -> ZcoinStorageRes> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + 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", 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 blocks_to_scan.len() > limit as usize { + break; + } + }; + + blocks_to_scan.push(CompactBlockRow { + height: block.height.into(), + data: block.data, + }); + } + + Ok(blocks_to_scan) + } + + /// Processes blockchain blocks with a specified mode of operation, such as validation or scanning. + /// + /// Processes blocks based on the provided `BlockProcessingMode` and other parameters, + /// which may include a starting block height, validation criteria, and a processing limit. + pub(crate) async fn process_blocks_with_mode( + &self, + params: ZcoinConsensusParams, + mode: BlockProcessingMode, + validate_from: Option<(BlockHeight, BlockHash)>, + limit: Option, + ) -> ZcoinStorageRes<()> { + let mut from_height = match &mode { + BlockProcessingMode::Validate => validate_from + .map(|(height, _)| height) + .unwrap_or(BlockHeight::from_u32(params.sapling_activation_height) - 1), + BlockProcessingMode::Scan(data) => data.inner().block_height_extrema().await.map(|opt| { + opt.map(|(_, max)| max) + .unwrap_or(BlockHeight::from_u32(params.sapling_activation_height) - 1) + })?, + }; + let mut prev_height = from_height; + let mut prev_hash: Option = validate_from.map(|(_, hash)| hash); + + 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()))?; + + if block.height() != cbr.height { + return MmError::err(ZcoinStorageError::CorruptedData(format!( + "Block height {} did not match row's height field value {}", + block.height(), + cbr.height + ))); + } + + match &mode.clone() { + BlockProcessingMode::Validate => { + validate_chain(block, &mut prev_height, &mut prev_hash).await?; + }, + BlockProcessingMode::Scan(data) => { + scan_cached_block(data, ¶ms, &block, &mut from_height).await?; + }, + } + } + + Ok(()) + } +} diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs new file mode 100644 index 0000000000..74c790bf89 --- /dev/null +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs @@ -0,0 +1,234 @@ +use crate::z_coin::storage::{scan_cached_block, validate_chain, BlockDbImpl, BlockProcessingMode, CompactBlockRow, + ZcoinStorageRes}; +use crate::z_coin::z_coin_errors::ZcoinStorageError; +use crate::z_coin::ZcoinConsensusParams; + +use common::async_blocking; +use db_common::sqlite::rusqlite::{params, Connection}; +use db_common::sqlite::{query_single_row, run_optimization_pragmas, rusqlite}; +use itertools::Itertools; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use protobuf::Message; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use zcash_client_backend::data_api::error::Error as ChainError; +use zcash_client_backend::proto::compact_formats::CompactBlock; +use zcash_client_sqlite::error::{SqliteClientError as ZcashClientError, SqliteClientError}; +use zcash_extras::NoteId; +use zcash_extras::WalletRead; +use zcash_primitives::block::BlockHash; +use zcash_primitives::consensus::BlockHeight; + +impl From for ZcoinStorageError { + fn from(value: ZcashClientError) -> Self { + match value { + SqliteClientError::CorruptedData(err) => Self::CorruptedData(err), + SqliteClientError::IncorrectHrpExtFvk => Self::IncorrectHrpExtFvk, + SqliteClientError::InvalidNote => Self::InvalidNote(value.to_string()), + SqliteClientError::InvalidNoteId => Self::InvalidNoteId, + SqliteClientError::TableNotEmpty => Self::TableNotEmpty(value.to_string()), + SqliteClientError::Bech32(err) => Self::DecodingError(err.to_string()), + SqliteClientError::Base58(err) => Self::DecodingError(err.to_string()), + SqliteClientError::DbError(err) => Self::DecodingError(err.to_string()), + SqliteClientError::Io(err) => Self::IoError(err.to_string()), + SqliteClientError::InvalidMemo(err) => Self::InvalidMemo(err.to_string()), + SqliteClientError::BackendError(err) => Self::BackendError(err.to_string()), + } + } +} + +impl From> for ZcoinStorageError { + fn from(value: ChainError) -> Self { Self::SqliteError(ZcashClientError::from(value)) } +} + +impl BlockDbImpl { + #[cfg(all(not(test)))] + pub async fn new(_ctx: &MmArc, ticker: String, path: PathBuf) -> ZcoinStorageRes { + async_blocking(move || { + let conn = Connection::open(path).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + let conn = Arc::new(Mutex::new(conn)); + let conn_clone = conn.clone(); + let conn_clone = conn_clone.lock().unwrap(); + run_optimization_pragmas(&conn_clone).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + conn_clone + .execute( + "CREATE TABLE IF NOT EXISTS compactblocks ( + height INTEGER PRIMARY KEY, + data BLOB NOT NULL + )", + [], + ) + .map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + + Ok(Self { db: conn, ticker }) + }) + .await + } + + #[cfg(all(test))] + pub(crate) async fn new(ctx: &MmArc, ticker: String, _path: PathBuf) -> ZcoinStorageRes { + let ctx = ctx.clone(); + async_blocking(move || { + let conn = ctx + .sqlite_connection + .clone_or(Arc::new(Mutex::new(Connection::open_in_memory().unwrap()))); + let conn_clone = conn.clone(); + let conn_clone = conn_clone.lock().unwrap(); + run_optimization_pragmas(&conn_clone).map_err(|err| ZcoinStorageError::DbError(err.to_string()))?; + conn_clone + .execute( + "CREATE TABLE IF NOT EXISTS compactblocks ( + height INTEGER PRIMARY KEY, + data BLOB NOT NULL + )", + [], + ) + .map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + + Ok(BlockDbImpl { db: conn, ticker }) + }) + .await + } + + pub(crate) async fn get_latest_block(&self) -> ZcoinStorageRes { + let db = self.db.clone(); + Ok(async_blocking(move || { + query_single_row( + &db.lock().unwrap(), + "SELECT height FROM compactblocks ORDER BY height DESC LIMIT 1", + [], + |row| row.get(0), + ) + }) + .await + .map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))? + .unwrap_or(0)) + } + + pub(crate) async fn insert_block(&self, height: u32, cb_bytes: Vec) -> ZcoinStorageRes { + let db = self.db.clone(); + async_blocking(move || { + let db = db.lock().unwrap(); + let insert = db + .prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)") + .map_to_mm(|err| ZcoinStorageError::AddToStorageErr(err.to_string()))? + .execute(params![height, cb_bytes]) + .map_to_mm(|err| ZcoinStorageError::AddToStorageErr(err.to_string()))?; + + Ok(insert) + }) + .await + } + + pub(crate) async fn rewind_to_height(&self, height: BlockHeight) -> ZcoinStorageRes { + let db = self.db.clone(); + async_blocking(move || { + db.lock() + .unwrap() + .execute("DELETE from compactblocks WHERE height > ?1", [u32::from(height)]) + .map_to_mm(|err| ZcoinStorageError::RemoveFromStorageErr(err.to_string())) + }) + .await + } + + pub(crate) async fn get_earliest_block(&self) -> ZcoinStorageRes { + let db = self.db.clone(); + Ok(async_blocking(move || { + query_single_row( + &db.lock().unwrap(), + "SELECT MIN(height) from compactblocks", + [], + |row| row.get::<_, Option>(0), + ) + }) + .await + .map_to_mm(|err| ZcoinStorageError::GetFromStorageError(err.to_string()))? + .flatten() + .unwrap_or(0)) + } + + pub(crate) async fn query_blocks_by_limit( + &self, + from_height: BlockHeight, + limit: Option, + ) -> ZcoinStorageRes>> { + let db = self.db.clone(); + async_blocking(move || { + // Fetch the CompactBlocks we need to scan + let db = db.lock().unwrap(); + let mut stmt_blocks = db + .prepare( + "SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC \ + LIMIT ?", + ) + .map_to_mm(|err| ZcoinStorageError::AddToStorageErr(err.to_string()))?; + + let rows = stmt_blocks + .query_map( + params![u32::from(from_height), limit.unwrap_or(u32::max_value()),], + |row| { + Ok(CompactBlockRow { + height: BlockHeight::from_u32(row.get(0)?), + data: row.get(1)?, + }) + }, + ) + .map_to_mm(|err| ZcoinStorageError::AddToStorageErr(err.to_string()))?; + + Ok(rows.collect_vec()) + }) + .await + } + + pub(crate) async fn process_blocks_with_mode( + &self, + params: ZcoinConsensusParams, + mode: BlockProcessingMode, + validate_from: Option<(BlockHeight, BlockHash)>, + limit: Option, + ) -> ZcoinStorageRes<()> { + let ticker = self.ticker.to_owned(); + let mut from_height = match &mode { + BlockProcessingMode::Validate => validate_from + .map(|(height, _)| height) + .unwrap_or(BlockHeight::from_u32(params.sapling_activation_height) - 1), + BlockProcessingMode::Scan(data) => { + let data = data.inner(); + data.block_height_extrema().await.map(|opt| { + opt.map(|(_, max)| max) + .unwrap_or(BlockHeight::from_u32(params.sapling_activation_height) - 1) + })? + }, + }; + + let rows = 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 row_result in rows { + let cbr = row_result.map_err(|err| ZcoinStorageError::AddToStorageErr(err.to_string()))?; + let block = CompactBlock::parse_from_bytes(&cbr.data) + .map_err(|err| ZcoinStorageError::ChainError(err.to_string()))?; + + if block.height() != cbr.height { + return MmError::err(ZcoinStorageError::CorruptedData(format!( + "{ticker}, Block height {} did not match row's height field value {}", + block.height(), + cbr.height + ))); + } + + match &mode.clone() { + BlockProcessingMode::Validate => { + validate_chain(block, &mut prev_height, &mut prev_hash).await?; + }, + BlockProcessingMode::Scan(data) => { + scan_cached_block(data, ¶ms, &block, &mut from_height).await?; + }, + } + } + Ok(()) + } +} diff --git a/mm2src/coins/z_coin/storage/blockdb/mod.rs b/mm2src/coins/z_coin/storage/blockdb/mod.rs index cd9aceb6ef..7e2ef49fe7 100644 --- a/mm2src/coins/z_coin/storage/blockdb/mod.rs +++ b/mm2src/coins/z_coin/storage/blockdb/mod.rs @@ -1,47 +1,18 @@ -#[cfg(target_arch = "wasm32")] pub(crate) mod block_idb; - -use mm2_core::mm_ctx::MmArc; -use std::path::Path; -use zcash_client_backend::data_api::BlockSource; -use zcash_client_backend::proto::compact_formats::CompactBlock; -use zcash_primitives::consensus::BlockHeight; - -cfg_native!( - use db_common::sqlite::rusqlite::{params, Connection}; - use db_common::sqlite::{query_single_row, run_optimization_pragmas}; - use protobuf::Message; - use mm2_err_handle::prelude::*; - use std::sync::{Arc, Mutex}; - use zcash_client_sqlite::error::{SqliteClientError as ZcashClientError, SqliteClientError}; - use zcash_client_sqlite::NoteId; - use zcash_client_backend::data_api::error::Error as ChainError; - - struct CompactBlockRow { - height: BlockHeight, - data: Vec, - } -); - -#[derive(Debug, Display)] -pub enum BlockDbError { - #[cfg(not(target_arch = "wasm32"))] - SqliteError(SqliteClientError), - #[cfg(target_arch = "wasm32")] - IndexedDBError(String), - CorruptedData(String), -} - #[cfg(not(target_arch = "wasm32"))] -impl From for BlockDbError { - fn from(value: SqliteClientError) -> Self { Self::SqliteError(value) } -} +pub(crate) mod blockdb_sql_storage; #[cfg(not(target_arch = "wasm32"))] -impl From> for BlockDbError { - fn from(value: ChainError) -> Self { Self::SqliteError(SqliteClientError::from(value)) } -} +use db_common::sqlite::rusqlite::Connection; +#[cfg(not(target_arch = "wasm32"))] use std::sync::{Arc, Mutex}; -/// A wrapper for the db connection to the block cache database. +#[cfg(target_arch = "wasm32")] +pub(crate) mod blockdb_idb_storage; +#[cfg(target_arch = "wasm32")] +use blockdb_idb_storage::BlockDbInner; +#[cfg(target_arch = "wasm32")] use mm2_db::indexed_db::SharedDb; + +/// A wrapper for the db connection to the block cache database in native and browser. +#[derive(Clone)] pub struct BlockDbImpl { #[cfg(not(target_arch = "wasm32"))] pub db: Arc>, @@ -51,163 +22,116 @@ pub struct BlockDbImpl { ticker: String, } -#[cfg(not(target_arch = "wasm32"))] -impl BlockDbImpl { - pub async fn new(_ctx: MmArc, ticker: String, path: impl AsRef) -> MmResult { - let conn = Connection::open(path).map_err(|err| BlockDbError::SqliteError(SqliteClientError::from(err)))?; - run_optimization_pragmas(&conn).map_err(|err| BlockDbError::SqliteError(SqliteClientError::from(err)))?; - conn.execute( - "CREATE TABLE IF NOT EXISTS compactblocks ( - height INTEGER PRIMARY KEY, - data BLOB NOT NULL - )", - [], - ) - .map_to_mm(|err| BlockDbError::SqliteError(SqliteClientError::from(err)))?; - - Ok(Self { - db: Arc::new(Mutex::new(conn)), - ticker, - }) - } +#[cfg(any(test, target_arch = "wasm32"))] +mod block_db_storage_tests { + use crate::z_coin::storage::BlockDbImpl; + use common::log::info; + use std::path::PathBuf; - pub(crate) fn get_latest_block(&self) -> Result { - Ok(query_single_row( - &self.db.lock().unwrap(), - "SELECT height FROM compactblocks ORDER BY height DESC LIMIT 1", - [], - |row| row.get(0), - )? - .unwrap_or(0)) - } + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; - pub(crate) fn insert_block(&self, height: u32, cb_bytes: Vec) -> Result { - self.db - .lock() - .unwrap() - .prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)") - .map_err(|err| BlockDbError::SqliteError(SqliteClientError::from(err)))? - .execute(params![height, cb_bytes]) - .map_err(|err| BlockDbError::SqliteError(SqliteClientError::from(err))) - } + const TICKER: &str = "ARRR"; + const HEADERS: &[(u32, &str)] = &[(1900000, + "10E0FB731A2044797F3BB78323A7717007F1E289A3689E0B5B3433385DBD8E6F6A17000000002220735484676853C744A8CA0FEA105081C54A8C50A151E42E31EC7E20040000000028EBACFD9306"), (1900001, + "10E1FB731A20A261B624D0E42238255A69F96E45EEA341B5E4125A7DD710118D150B00000000222044797F3BB78323A7717007F1E289A3689E0B5B3433385DBD8E6F6A170000000028FEACFD9306"), (1900002,"10E2FB731A208747587DE8DDED766591FA6C9859D77BFC9C293B054F3D38A9BC5E08000000002220A261B624D0E42238255A69F96E45EEA341B5E4125A7DD710118D150B0000000028F7ADFD93063AC002080212201D7165BCACD3245EED7324367EB34199EA2ED502726933484FEFA6A220AA330F22220A208DD3C9362FBCF766BEF2DFA3A3B186BBB43CA456DB9690EFD06978FC822056D22A7A0A20245E73ED6EB4B73805D3929F841CCD7E01523E2B8A0F29D721CD82547A470C711220D6BAF6AF4783FF265451B8A7A5E4271EA72F034890DA234427082F84F08256DD1A34EAABEE115A1FCDED194189F586C6DC2099E8C5F47BD68B210146EDFFCB39649EB55504910EC590E6E9908B6114ED3DDFD5861FDC2A7A0A2079E70D202FEE537011284A30F1531BCF627613CBBAAFABBB24CE56600FE94B6C122041E9FBA0E6197A58532F61BD7617CACEC8C2F10C77AA8B99B2E535EE1D3C36171A341B6A04C5EC9A2AE8CDF0433C9AAD36C647139C9542759E2758FD4A10ED0C78F8087BE5AEE92EA8834E6CE116C8A5737B7607BD523AC002080312202790606A461DA171221480A3FC414CCF9C273FE6F0C2E3CFA6C85D6CDE8EFE5C22220A201767E6E3B390FAB4C79E46131C54ED91A987EEA2286DB80F240D431AC07A750C2A7A0A20E86C11A660EB72F1449BA0CEB57FFB313A4047880C33ADED93945ED9C477581B12201752816751ABAB19398A4A5CFE429724D820588BCFEDC7D88B399D9B24FB4C111A34DB38AE57231FBE768063E08D8EC70E3486FF89A74E0840B6F5D8412F1C7E2C5D884AA08E2F7EDA42836B80B4433C83CDDC8B51DE2A7A0A20E2FEF897A286A8D5AD9E0485F287CE1A73970EADA899DBE3FC77043846E06B1E1220F0A046829B17CC8B5B750281CD20A1E28F983E599AA2A1C8F3BD97BE49C55CEB1A3488DCDA1444CBACE213100507FC83627D83624EF2AD47C25160F5E604595158C98EBC3549C0A07359FB42D8437A70AB472FB64AA13AC002080412201EDD399E68128B97F6F98E31C1965361528AC07665114D09F9D119C089791E9222220A20B9471453950609CF8C2EDF721FE7D0D2D211BBD158283E8D6B80EAAB312968EF2A7A0A201FF6F7D74ABBAC9D4E5A95F63861C19FE3D18083ABE2EACE7B8A70E7E5FCB51812206753F2992061EF3FC0C37FC0D1352A386514B2CC1AEB39AC835A8D9BFBD022D91A34BA41719ECF19520BD7D6EFB08AAF5018282026781D0FE5697811B34E0DEFE4D4691585D4994056E109DC19FFE63CAB29CA4F26682A7A0A200E570E832326625C9D8536DBAC389529A090FC54C3F378E25431405751BBFF391220D27A030843C93522B2D232644E7AC7CF235494B126FDAEA9F5980FA1AECE746E1A34EF8BD98D7DD39659714E7851E47F57A52741F564F0275CE8A82F2665C70EA5887B0CE8501CF509A8265ECB155A00A0629B463C253AC00208051220E1F375AD9EC6A774E444ECC5EB6F07237B1DE9EAA1A9FD7AEF392D6F40BA705822220A20D8298A06C9657E042DC69473B23A74C94E51AF684DA6281CE7F797791F486AD42A7A0A209216A5DBC616291688CDFB075A5E639FA8000ADD006438C4BCE98D000AE0DF3512202C20533A17279C46EC995DBF819673039E5810DCD2DA024DAEF64053CD7B562D1A346928F93BB25B03519AC83B297F77E2F54F62B1E722E6F8D886ADF709455C2C0B930CE429EA24ECD15354085F7FA3F2A4077DE76D2A7A0A203AE3F07AB8AB4C76B246A0D7CA9321F84081144E9B7E3AE0CEC0139B392E443812200791064E9E188BF1D1373BEEFAE7458F12F976B15896CD69970019B4560A5F721A3428ADC7816F15528F65372E585E07D1CD6C0DFB3F3BA7BD263BB4E5A3ADAAFD84CD55FFBDD23787163F52711A22935EB52A30EB37") + ]; + + pub(crate) async fn test_insert_block_and_get_latest_block_impl() { + let ctx = mm_ctx_with_custom_db(); + let db = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) + .await + .unwrap(); + // insert block + for header in HEADERS.iter() { + db.insert_block(header.0, hex::decode(header.1).unwrap()).await.unwrap(); + } - pub(crate) fn rewind_to_height(&self, height: u32) -> Result { - self.db - .lock() - .unwrap() - .execute("DELETE from compactblocks WHERE height > ?1", [height]) - .map_err(|err| BlockDbError::SqliteError(SqliteClientError::from(err))) + // get last block header + let last_height = db.get_latest_block().await.unwrap(); + assert_eq!(1900002, last_height) } - fn with_blocks( - &self, - from_height: BlockHeight, - limit: Option, - mut with_row: F, - ) -> Result<(), SqliteClientError> - where - F: FnMut(CompactBlock) -> Result<(), SqliteClientError>, - { - // Fetch the CompactBlocks we need to scan - let stmt_blocks = self.db.lock().unwrap(); - let mut stmt_blocks = stmt_blocks.prepare( - "SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC \ - LIMIT ?", - )?; - - let rows = stmt_blocks.query_map( - params![u32::from(from_height), limit.unwrap_or(u32::max_value()),], - |row| { - Ok(CompactBlockRow { - height: BlockHeight::from_u32(row.get(0)?), - data: row.get(1)?, - }) - }, - )?; - - for row_result in rows { - let cbr = row_result?; - let block = CompactBlock::parse_from_bytes(&cbr.data).map_err(ChainError::from)?; - - if block.height() != cbr.height { - return Err(SqliteClientError::CorruptedData(format!( - "Block height {} did not match row's height field value {}", - block.height(), - cbr.height - ))); - } - - with_row(block)?; + pub(crate) async fn test_rewind_to_height_impl() { + let ctx = mm_ctx_with_custom_db(); + let db = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) + .await + .unwrap(); + // insert block + for header in HEADERS.iter() { + db.insert_block(header.0, hex::decode(header.1).unwrap()).await.unwrap(); } - Ok(()) - } + // rewind height to 1900000 + let rewind_result = db.rewind_to_height(1900000.into()).await; + assert!(rewind_result.is_ok()); - pub(crate) async fn get_earliest_block(&self) -> Result { - Ok(query_single_row( - &self.db.lock().unwrap(), - "SELECT MIN(height) from compactblocks", - [], - |row| row.get::<_, Option>(0), - )? - .flatten() - .unwrap_or(0)) + // get last height - we expect it to be 1900000 + let last_height = db.get_latest_block().await.unwrap(); + assert_eq!(1900000, last_height); + info!("Rewinding to height ended!"); + + // get last height - we expect it to be 1900000 + let last_height = db.get_latest_block().await.unwrap(); + assert_eq!(1900000, last_height) } -} -#[cfg(not(target_arch = "wasm32"))] -impl BlockSource for BlockDbImpl { - type Error = SqliteClientError; - - fn with_blocks(&self, from_height: BlockHeight, limit: Option, with_row: F) -> Result<(), Self::Error> - where - F: FnMut(CompactBlock) -> Result<(), Self::Error>, - { - self.with_blocks(from_height, limit, with_row) + #[allow(unused)] + pub(crate) async fn test_process_blocks_with_mode_impl() { + let ctx = mm_ctx_with_custom_db(); + let db = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) + .await + .unwrap(); + // insert block + for header in HEADERS.iter() { + let inserted_id = db.insert_block(header.0, hex::decode(header.1).unwrap()).await.unwrap(); + assert_eq!(1, inserted_id); + } + + // get last height - we expect it to be 1900002 + let block_height = db.get_latest_block().await.unwrap(); + assert_eq!(1900002, block_height); } } -cfg_wasm32!( - use crate::z_coin::storage::blockdb::block_idb::BlockDbInner; - use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; - use mm2_err_handle::prelude::*; +#[cfg(all(test, not(target_arch = "wasm32")))] +mod native_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 common::block_on; - pub type BlockDbRes = MmResult; - pub type BlockDbInnerLocked<'a> = DbLocked<'a, BlockDbInner>; + #[test] + fn test_insert_block_and_get_latest_block() { block_on(test_insert_block_and_get_latest_block_impl()) } - impl BlockDbImpl { - pub async fn new(ctx: MmArc, ticker: String, _path: impl AsRef) -> Result { - Ok(Self { - db: ConstructibleDb::new(&ctx).into_shared(), - ticker, - }) - } + #[test] + fn test_rewind_to_height() { block_on(test_rewind_to_height_impl()) } +} - #[allow(unused)] - async fn lock_db(&self) -> BlockDbRes> { - self.db - .get_or_initialize() - .await - .mm_err(|err| BlockDbError::IndexedDBError(err.to_string())) - } +#[cfg(target_arch = "wasm32")] +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::*; - pub fn get_latest_block(&self) -> Result { todo!() } + wasm_bindgen_test_configure!(run_in_browser); - pub fn insert_block(&self, _height: u32, _cb_bytes: Vec) -> Result { todo!() } + #[wasm_bindgen_test] + async fn test_insert_block_and_get_latest_block() { test_insert_block_and_get_latest_block_impl().await } - pub fn rewind_to_height(&self, _height: u32) -> Result { todo!() } + #[wasm_bindgen_test] + async fn test_rewind_to_height() { test_rewind_to_height_impl().await } - pub fn with_blocks(&self, _from_height: BlockHeight, _limit: Option, mut _with_row: F) -> Result<(), - BlockDbError> - where F: FnMut(CompactBlock) -> Result<(), BlockDbError> - { todo!() } - } + #[wasm_bindgen_test] + async fn test_transport() { + register_wasm_log(); + let client = LightRpcClient::new(vec!["https://pirate.battlefield.earth:8581".to_string()]) + .await + .unwrap(); + let latest_height = client.get_block_height().await; - impl BlockSource for BlockDbImpl { - type Error = BlockDbError; - fn with_blocks(&self, _from_height: BlockHeight, _limit: Option, _with_row: F) -> Result<(), - Self::Error> - where F: FnMut(CompactBlock) -> Result<(), Self::Error>, - { todo!() } + assert!(latest_height.is_ok()); + info!("LATEST BLOCK: {latest_height:?}"); } -); +} diff --git a/mm2src/coins/z_coin/storage/walletdb/mod.rs b/mm2src/coins/z_coin/storage/walletdb/mod.rs index 656993cd4b..244d72f1a6 100644 --- a/mm2src/coins/z_coin/storage/walletdb/mod.rs +++ b/mm2src/coins/z_coin/storage/walletdb/mod.rs @@ -1,89 +1,21 @@ -use crate::z_coin::{ZCoinBuilder, ZcoinClientInitError}; -use mm2_err_handle::prelude::*; -use zcash_primitives::zip32::ExtendedSpendingKey; - cfg_native!( - use crate::z_coin::{CheckPointBlockInfo, ZcoinConsensusParams}; - use crate::z_coin::z_rpc::create_wallet_db; + use crate::z_coin::ZcoinConsensusParams; - use parking_lot::Mutex; - use std::sync::Arc; - use zcash_client_sqlite::WalletDb; - use zcash_primitives::zip32::ExtendedFullViewingKey; -); + pub mod wallet_sql_storage; -cfg_wasm32!( - mod wallet_idb; - use wallet_idb::WalletDbInner; + use zcash_client_sqlite::for_async::WalletDbAsync; ); -#[derive(Debug, Display)] -pub enum WalletDbError { - ZcoinClientInitError(ZcoinClientInitError), - ZCoinBuildError(String), - IndexedDBError(String), -} +#[cfg(target_arch = "wasm32")] pub mod wasm; +#[cfg(target_arch = "wasm32")] +use wasm::storage::WalletIndexedDb; #[derive(Clone)] pub struct WalletDbShared { #[cfg(not(target_arch = "wasm32"))] - pub db: Arc>>, + pub db: WalletDbAsync, #[cfg(target_arch = "wasm32")] - pub db: SharedDb, + pub db: WalletIndexedDb, #[allow(unused)] ticker: String, } - -#[cfg(not(target_arch = "wasm32"))] -impl<'a> WalletDbShared { - pub async fn new( - zcoin_builder: &ZCoinBuilder<'a>, - checkpoint_block: Option, - z_spending_key: &ExtendedSpendingKey, - continue_from_prev_sync: bool, - ) -> MmResult { - let wallet_db = create_wallet_db( - zcoin_builder - .db_dir_path - .join(format!("{}_wallet.db", zcoin_builder.ticker)), - zcoin_builder.protocol_info.consensus_params.clone(), - checkpoint_block, - ExtendedFullViewingKey::from(z_spending_key), - continue_from_prev_sync, - ) - .await - .mm_err(WalletDbError::ZcoinClientInitError)?; - - Ok(Self { - db: Arc::new(Mutex::new(wallet_db)), - ticker: zcoin_builder.ticker.to_string(), - }) - } -} - -cfg_wasm32!( - use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; - - pub type WalletDbRes = MmResult; - pub type WalletDbInnerLocked<'a> = DbLocked<'a, WalletDbInner>; - - impl<'a> WalletDbShared { - pub async fn new( - zcoin_builder: &ZCoinBuilder<'a>, - _z_spending_key: &ExtendedSpendingKey, - ) -> MmResult { - Ok(Self { - db: ConstructibleDb::new(zcoin_builder.ctx).into_shared(), - ticker: zcoin_builder.ticker.to_string(), - }) - } - - #[allow(unused)] - async fn lock_db(&self) -> WalletDbRes> { - self.db - .get_or_initialize() - .await - .mm_err(|err| WalletDbError::IndexedDBError(err.to_string())) - } - } -); diff --git a/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs b/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs new file mode 100644 index 0000000000..3a957d375f --- /dev/null +++ b/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs @@ -0,0 +1,121 @@ +use crate::z_coin::storage::{WalletDbShared, ZcoinStorageRes}; +use crate::z_coin::{CheckPointBlockInfo, ZCoinBuilder, ZcoinClientInitError, ZcoinConsensusParams, ZcoinStorageError}; +use common::async_blocking; +use common::log::info; +use db_common::sqlite::{query_single_row, run_optimization_pragmas}; +use mm2_err_handle::prelude::*; +use std::path::PathBuf; +use zcash_client_sqlite::for_async::init::{init_accounts_table, init_blocks_table, init_wallet_db}; +use zcash_client_sqlite::for_async::WalletDbAsync; +use zcash_extras::{WalletRead, WalletWrite}; +use zcash_primitives::block::BlockHash; +use zcash_primitives::consensus::BlockHeight; +use zcash_primitives::transaction::TxId; +use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; + +/// `create_wallet_db` is responsible for creating a new Zcoin wallet database, initializing it +/// with the provided parameters, and executing various initialization steps. These steps include checking and +/// potentially rewinding the database to a specified synchronization height, performing optimizations, and +/// setting up the initial state of the wallet database. +pub async fn create_wallet_db( + wallet_db_path: PathBuf, + consensus_params: ZcoinConsensusParams, + checkpoint_block: Option, + evk: ExtendedFullViewingKey, + continue_from_prev_sync: bool, +) -> Result, MmError> { + let db = async_blocking(move || { + WalletDbAsync::for_path(wallet_db_path, consensus_params) + .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::ZcoinStorageError(err.to_string())) + }) + .await?; + + init_wallet_db(&db) + .await + .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?; + let min_sync_height = extrema.map(|(min, _)| u32::from(min)); + let init_block_height = checkpoint_block.clone().map(|block| block.height); + + // Check if the initial block height is less than the previous synchronization height and + // Rewind walletdb to the minimum possible height. + if get_evk.is_empty() || (!continue_from_prev_sync && init_block_height != min_sync_height) { + // let user know we're clearing cache and resyncing from new provided height. + if min_sync_height.unwrap_or(0) > 0 { + info!("Older/Newer sync height detected!, rewinding walletdb to new height: {init_block_height:?}"); + } + let mut wallet_ops = db.get_update_ops().expect("get_update_ops always returns Ok"); + wallet_ops + .rewind_to_height(u32::MIN.into()) + .await + .map_to_mm(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string()))?; + if let Some(block) = checkpoint_block.clone() { + init_blocks_table( + &db, + BlockHeight::from_u32(block.height), + BlockHash(block.hash.0), + block.time, + &block.sapling_tree.0, + ) + .await?; + } + } + + if get_evk.is_empty() { + init_accounts_table(&db, &[evk]).await?; + } + + Ok(db) +} + +impl<'a> WalletDbShared { + pub async fn new( + builder: &ZCoinBuilder<'a>, + checkpoint_block: Option, + z_spending_key: &ExtendedSpendingKey, + continue_from_prev_sync: bool, + ) -> ZcoinStorageRes { + let ticker = builder.ticker; + let consensus_params = builder.protocol_info.consensus_params.clone(); + let wallet_db = create_wallet_db( + builder.db_dir_path.join(format!("{ticker}_wallet.db")), + consensus_params, + checkpoint_block, + ExtendedFullViewingKey::from(z_spending_key), + continue_from_prev_sync, + ) + .await + .map_err(|err| ZcoinStorageError::InitDbError { + ticker: ticker.to_string(), + err: err.to_string(), + })?; + + Ok(Self { + db: wallet_db, + ticker: ticker.to_string(), + }) + } + + pub async fn is_tx_imported(&self, tx_id: TxId) -> ZcoinStorageRes { + let db = self.db.inner(); + async_blocking(move || { + let conn = db.lock().unwrap(); + const QUERY: &str = "SELECT EXISTS (SELECT 1 FROM transactions WHERE txid = ?1);"; + Ok( + query_single_row(conn.sql_conn(), QUERY, [tx_id.0.to_vec()], |row| row.get::<_, i64>(0)) + .map(|v| v.is_some()) + .unwrap_or_default(), + ) + }) + .await + } +} diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs new file mode 100644 index 0000000000..ff35564385 --- /dev/null +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs @@ -0,0 +1,1148 @@ +pub mod storage; +pub mod tables; + +use crate::z_coin::ZcoinStorageError; + +use ff::PrimeField; +use mm2_err_handle::prelude::*; +use mm2_number::BigInt; +use num_traits::ToPrimitive; +use std::convert::TryInto; +use zcash_client_backend::wallet::SpendableNote; +use zcash_primitives::merkle_tree::IncrementalWitness; +use zcash_primitives::sapling::Diversifier; +use zcash_primitives::sapling::Rseed; +use zcash_primitives::transaction::components::Amount; + +struct SpendableNoteConstructor { + diversifier: Vec, + value: BigInt, + rcm: Vec, + witness: Vec, +} + +fn to_spendable_note(note: SpendableNoteConstructor) -> MmResult { + let diversifier = { + let d = note.diversifier; + if d.len() != 11 { + return MmError::err(ZcoinStorageError::CorruptedData( + "Invalid diversifier length".to_string(), + )); + } + let mut tmp = [0; 11]; + tmp.copy_from_slice(&d); + Diversifier(tmp) + }; + + let note_value = Amount::from_i64(note.value.to_i64().expect("BigInt is too large to fit in an i64")).unwrap(); + + let rseed = { + let rcm_bytes = note.rcm.clone(); + + // We store rcm directly in the data DB, regardless of whether the note + // used a v1 or v2 note plaintext, so for the purposes of spending let's + // pretend this is a pre-ZIP 212 note. + let rcm = jubjub::Fr::from_repr( + rcm_bytes[..] + .try_into() + .map_to_mm(|_| ZcoinStorageError::InvalidNote("Invalid note".to_string()))?, + ) + .ok_or_else(|| MmError::new(ZcoinStorageError::InvalidNote("Invalid note".to_string())))?; + Rseed::BeforeZip212(rcm) + }; + + let witness = { + let d = note.witness; + IncrementalWitness::read(&d[..]).map_to_mm(|err| ZcoinStorageError::IoError(err.to_string()))? + }; + + Ok(SpendableNote { + diversifier, + note_value, + rseed, + witness, + }) +} + +#[cfg(test)] +mod wasm_test { + use crate::z_coin::storage::walletdb::WalletIndexedDb; + use crate::z_coin::storage::{BlockDbImpl, BlockProcessingMode, DataConnStmtCacheWasm, DataConnStmtCacheWrapper}; + use crate::z_coin::{ValidateBlocksError, ZcoinConsensusParams, ZcoinStorageError}; + use crate::ZcoinProtocolInfo; + use mm2_core::mm_ctx::MmArc; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + use protobuf::Message; + use std::path::PathBuf; + use wasm_bindgen_test::*; + use zcash_client_backend::wallet::{AccountId, OvkPolicy}; + use zcash_extras::fake_compact_block; + use zcash_extras::fake_compact_block_spending; + use zcash_extras::wallet::create_spend_to_address; + use zcash_extras::WalletRead; + use zcash_primitives::block::BlockHash; + use zcash_primitives::consensus::{BlockHeight, Network, NetworkUpgrade, Parameters}; + use zcash_primitives::transaction::components::Amount; + use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; + use zcash_proofs::prover::LocalTxProver; + + wasm_bindgen_test_configure!(run_in_browser); + + const TICKER: &str = "ARRR"; + + async fn test_prover() -> LocalTxProver { + let (spend_buf, output_buf) = wagyu_zcash_parameters::load_sapling_parameters(); + LocalTxProver::from_bytes(&spend_buf[..], &output_buf[..]) + } + + fn consensus_params() -> ZcoinConsensusParams { + let protocol_info = serde_json::from_value::(json!({ + "consensus_params": { + "overwinter_activation_height": 152855, + "sapling_activation_height": u32::from(sapling_activation_height()), + "blossom_activation_height": null, + "heartwood_activation_height": null, + "canopy_activation_height": null, + "coin_type": 133, + "hrp_sapling_extended_spending_key": "secret-extended-key-main", + "hrp_sapling_extended_full_viewing_key": "zxviews", + "hrp_sapling_payment_address": "zs", + "b58_pubkey_address_prefix": [ + 28, + 184 + ], + "b58_script_address_prefix": [ + 28, + 189 + ] + } + })) + .unwrap(); + + protocol_info.consensus_params + } + + pub fn sapling_activation_height() -> BlockHeight { + Network::TestNetwork.activation_height(NetworkUpgrade::Sapling).unwrap() + } + + async fn wallet_db_from_zcoin_builder_for_test(ctx: &MmArc, ticker: &str) -> WalletIndexedDb { + WalletIndexedDb::new(ctx, ticker, consensus_params()).await.unwrap() + } + + #[wasm_bindgen_test] + async fn test_empty_database_has_no_balance() { + let ctx = mm_ctx_with_custom_db(); + let db = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvks = [ExtendedFullViewingKey::from(&extsk)]; + assert!(db.init_accounts_table(&extfvks).await.is_ok()); + + // The account should be empty + assert_eq!(db.get_balance(AccountId(0)).await.unwrap(), Amount::zero()); + + // We can't get an anchor height, as we have not scanned any blocks. + assert_eq!(db.get_target_and_anchor_heights().await.unwrap(), None); + + // An invalid account has zero balance + assert!(db.get_address(AccountId(1)).await.is_err()); + assert_eq!(db.get_balance(AccountId(0)).await.unwrap(), Amount::zero()); + } + + #[wasm_bindgen_test] + async fn test_init_accounts_table_only_works_once() { + let ctx = mm_ctx_with_custom_db(); + let db = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + + // We can call the function as many times as we want with no data + assert!(db.init_accounts_table(&[]).await.is_ok()); + assert!(db.init_accounts_table(&[]).await.is_ok()); + + // First call with data should initialise the accounts table. + let extfvks = [ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[]))]; + assert!(db.init_accounts_table(&extfvks).await.is_ok()); + + // Subsequent calls should return an error + assert!(db.init_accounts_table(&extfvks).await.is_ok()); + } + + #[wasm_bindgen_test] + async fn test_init_blocks_table_only_works_once() { + let ctx = mm_ctx_with_custom_db(); + let db = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + + // First call with data should initialise the blocks table + assert!(db + .init_blocks_table(BlockHeight::from(1), BlockHash([1; 32]), 1, &[]) + .await + .is_ok()); + + // Subsequent calls should return an error + assert!(db + .init_blocks_table(BlockHeight::from(2), BlockHash([2; 32]), 2, &[]) + .await + .is_err()); + } + + #[wasm_bindgen_test] + async fn init_accounts_table_stores_correct_address() { + let ctx = mm_ctx_with_custom_db(); + let db = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvks = [ExtendedFullViewingKey::from(&extsk)]; + assert!(db.init_accounts_table(&extfvks).await.is_ok()); + + // The account's address should be in the data DB. + let pa = db.get_address(AccountId(0)).await.unwrap(); + assert_eq!(pa.unwrap(), extsk.default_address().unwrap().1); + } + + #[wasm_bindgen_test] + async fn test_valid_chain_state() { + // init blocks_db + let ctx = mm_ctx_with_custom_db(); + let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) + .await + .unwrap(); + + // init walletdb. + let walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + assert!(walletdb.init_accounts_table(&[extfvk.clone()]).await.is_ok()); + + // Empty chain should be valid + let consensus_params = consensus_params(); + blockdb + .process_blocks_with_mode( + consensus_params.clone(), + BlockProcessingMode::Validate, + walletdb.get_max_height_hash().await.unwrap(), + None, + ) + .await + .unwrap(); + + // create a fake compactBlock sending value to the address + let (cb, _) = fake_compact_block( + sapling_activation_height(), + BlockHash([0; 32]), + extfvk.clone(), + Amount::from_u64(5).unwrap(), + ); + let cb_bytes = cb.write_to_bytes().unwrap(); + blockdb.insert_block(cb.height as u32, cb_bytes).await.unwrap(); + + // Cache-only chain should be valid + blockdb + .process_blocks_with_mode( + consensus_params.clone(), + BlockProcessingMode::Validate, + walletdb.get_max_height_hash().await.unwrap(), + None, + ) + .await + .unwrap(); + + // scan the cache + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .unwrap(); + + // Data-only chain should be valid + let max_height_hash = walletdb.get_max_height_hash().await.unwrap(); + blockdb + .process_blocks_with_mode( + consensus_params.clone(), + BlockProcessingMode::Validate, + max_height_hash, + None, + ) + .await + .unwrap(); + + // Create a second fake CompactBlock sending more value to the address + let (cb2, _) = fake_compact_block( + sapling_activation_height() + 1, + cb.hash(), + extfvk, + Amount::from_u64(7).unwrap(), + ); + let cb_bytes = cb2.write_to_bytes().unwrap(); + blockdb.insert_block(cb2.height as u32, cb_bytes).await.unwrap(); + + // Data+cache chain should be valid + blockdb + .process_blocks_with_mode( + consensus_params.clone(), + BlockProcessingMode::Validate, + walletdb.get_max_height_hash().await.unwrap(), + None, + ) + .await + .unwrap(); + + // Scan the cache again + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .unwrap(); + + // Data+cache chain should be valid + blockdb + .process_blocks_with_mode( + consensus_params.clone(), + BlockProcessingMode::Validate, + walletdb.get_max_height_hash().await.unwrap(), + None, + ) + .await + .unwrap(); + } + + #[wasm_bindgen_test] + async fn invalid_chain_cache_disconnected() { + // init blocks_db + let ctx = mm_ctx_with_custom_db(); + let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) + .await + .unwrap(); + + // init walletdb. + let walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + let consensus_params = consensus_params(); + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + assert!(walletdb.init_accounts_table(&[extfvk.clone()]).await.is_ok()); + + // Create some fake compactBlocks + let (cb, _) = fake_compact_block( + sapling_activation_height(), + BlockHash([0; 32]), + extfvk.clone(), + Amount::from_u64(5).unwrap(), + ); + let (cb2, _) = fake_compact_block( + sapling_activation_height() + 1, + cb.hash(), + extfvk.clone(), + Amount::from_u64(7).unwrap(), + ); + let cb_bytes = cb.write_to_bytes().unwrap(); + blockdb.insert_block(cb.height as u32, cb_bytes).await.unwrap(); + let cb2_bytes = cb2.write_to_bytes().unwrap(); + blockdb.insert_block(cb2.height as u32, cb2_bytes).await.unwrap(); + + // Scan the cache again + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .unwrap(); + + // Data-only chain should be valid + blockdb + .process_blocks_with_mode( + consensus_params.clone(), + BlockProcessingMode::Validate, + walletdb.get_max_height_hash().await.unwrap(), + None, + ) + .await + .unwrap(); + + // Create more fake CompactBlocks that don't connect to the scanned ones + let (cb3, _) = fake_compact_block( + sapling_activation_height() + 2, + BlockHash([1; 32]), + extfvk.clone(), + Amount::from_u64(8).unwrap(), + ); + let (cb4, _) = fake_compact_block( + sapling_activation_height() + 3, + cb3.hash(), + extfvk, + Amount::from_u64(3).unwrap(), + ); + let cb3_bytes = cb3.write_to_bytes().unwrap(); + blockdb.insert_block(cb3.height as u32, cb3_bytes).await.unwrap(); + let cb4_bytes = cb4.write_to_bytes().unwrap(); + blockdb.insert_block(cb4.height as u32, cb4_bytes).await.unwrap(); + + // Data+cache chain should be invalid at the data/cache boundary + let validate_chain = blockdb + .process_blocks_with_mode( + consensus_params.clone(), + BlockProcessingMode::Validate, + walletdb.get_max_height_hash().await.unwrap(), + None, + ) + .await + .unwrap_err(); + match validate_chain.get_inner() { + ZcoinStorageError::ValidateBlocksError(ValidateBlocksError::ChainInvalid { height, .. }) => { + assert_eq!(*height, sapling_activation_height() + 2) + }, + _ => panic!(), + } + } + + #[wasm_bindgen_test] + async fn test_invalid_chain_reorg() { + // init blocks_db + let ctx = mm_ctx_with_custom_db(); + let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) + .await + .unwrap(); + + // init walletdb. + let walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + let consensus_params = consensus_params(); + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + assert!(walletdb.init_accounts_table(&[extfvk.clone()]).await.is_ok()); + + // Create some fake compactBlocks + let (cb, _) = fake_compact_block( + sapling_activation_height(), + BlockHash([0; 32]), + extfvk.clone(), + Amount::from_u64(5).unwrap(), + ); + let (cb2, _) = fake_compact_block( + sapling_activation_height() + 1, + cb.hash(), + extfvk.clone(), + Amount::from_u64(7).unwrap(), + ); + let cb_bytes = cb.write_to_bytes().unwrap(); + blockdb.insert_block(cb.height as u32, cb_bytes).await.unwrap(); + let cb2_bytes = cb2.write_to_bytes().unwrap(); + blockdb.insert_block(cb2.height as u32, cb2_bytes).await.unwrap(); + + // Scan the cache again + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .unwrap(); + + // Data-only chain should be valid + blockdb + .process_blocks_with_mode( + consensus_params.clone(), + BlockProcessingMode::Validate, + walletdb.get_max_height_hash().await.unwrap(), + None, + ) + .await + .unwrap(); + + // Create more fake CompactBlocks that that contains a reorg + let (cb3, _) = fake_compact_block( + sapling_activation_height() + 2, + cb2.hash(), + extfvk.clone(), + Amount::from_u64(8).unwrap(), + ); + let (cb4, _) = fake_compact_block( + sapling_activation_height() + 3, + BlockHash([1; 32]), + extfvk, + Amount::from_u64(3).unwrap(), + ); + let cb3_bytes = cb3.write_to_bytes().unwrap(); + blockdb.insert_block(cb3.height as u32, cb3_bytes).await.unwrap(); + let cb4_bytes = cb4.write_to_bytes().unwrap(); + blockdb.insert_block(cb4.height as u32, cb4_bytes).await.unwrap(); + + // Data+cache chain should be invalid at the data/cache boundary + let validate_chain = blockdb + .process_blocks_with_mode( + consensus_params.clone(), + BlockProcessingMode::Validate, + walletdb.get_max_height_hash().await.unwrap(), + None, + ) + .await + .unwrap_err(); + match validate_chain.get_inner() { + ZcoinStorageError::ValidateBlocksError(ValidateBlocksError::ChainInvalid { height, .. }) => { + assert_eq!(*height, sapling_activation_height() + 3) + }, + _ => panic!(), + } + } + + #[wasm_bindgen_test] + async fn test_data_db_rewinding() { + // init blocks_db + let ctx = mm_ctx_with_custom_db(); + let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) + .await + .unwrap(); + + // init walletdb. + let walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + let consensus_params = consensus_params(); + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + assert!(walletdb.init_accounts_table(&[extfvk.clone()]).await.is_ok()); + + // Account balance should be zero + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), Amount::zero()); + + // Create some fake compactBlocks sending value to the address + let value = Amount::from_u64(5).unwrap(); + let value2 = Amount::from_u64(7).unwrap(); + let (cb, _) = fake_compact_block(sapling_activation_height(), BlockHash([0; 32]), extfvk.clone(), value); + let (cb2, _) = fake_compact_block(sapling_activation_height() + 1, cb.hash(), extfvk, value2); + let cb_bytes = cb.write_to_bytes().unwrap(); + blockdb.insert_block(cb.height as u32, cb_bytes).await.unwrap(); + let cb2_bytes = cb2.write_to_bytes().unwrap(); + blockdb.insert_block(cb2.height as u32, cb2_bytes).await.unwrap(); + + // Scan the cache + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .unwrap(); + + // Account balance should reflect both received notes + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value + value2); + + // Rewind to height of last scanned block + walletdb + .rewind_to_height(sapling_activation_height() + 1) + .await + .unwrap(); + + // Account balance should should be unaltered + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value + value2); + + // Rewind so one block is dropped. + walletdb.rewind_to_height(sapling_activation_height()).await.unwrap(); + + // Account balance should only contain the first received note + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value); + + // Scan the cache again + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .unwrap(); + + // Account balance should again reflect both received notes + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value + value2); + } + + #[wasm_bindgen_test] + async fn test_scan_cached_blocks_requires_sequential_blocks() { + // init blocks_db + let ctx = mm_ctx_with_custom_db(); + let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) + .await + .unwrap(); + + // init walletdb. + let walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + let consensus_params = consensus_params(); + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + assert!(walletdb.init_accounts_table(&[extfvk.clone()]).await.is_ok()); + + // Create a block with height SAPLING_ACTIVATION_HEIGHT + let value = Amount::from_u64(50000).unwrap(); + let (cb1, _) = fake_compact_block(sapling_activation_height(), BlockHash([0; 32]), extfvk.clone(), value); + let cb1_bytes = cb1.write_to_bytes().unwrap(); + blockdb.insert_block(cb1.height as u32, cb1_bytes).await.unwrap(); + + // Scan cache + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .unwrap(); + + // We cannot scan a block of height SAPLING_ACTIVATION_HEIGHT + 2 next + let (cb2, _) = fake_compact_block(sapling_activation_height() + 1, cb1.hash(), extfvk.clone(), value); + let cb2_bytes = cb2.write_to_bytes().unwrap(); + let (cb3, _) = fake_compact_block(sapling_activation_height() + 2, cb2.hash(), extfvk.clone(), value); + let cb3_bytes = cb3.write_to_bytes().unwrap(); + blockdb.insert_block(cb3.height as u32, cb3_bytes).await.unwrap(); + // Scan the cache again + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + let scan = blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .unwrap_err(); + match scan.get_inner() { + ZcoinStorageError::ValidateBlocksError(err) => { + let actual = err.to_string(); + let expected = ValidateBlocksError::block_height_discontinuity( + sapling_activation_height() + 1, + sapling_activation_height() + 2, + ); + assert_eq!(expected.to_string(), actual) + }, + _ => panic!("Should have failed"), + } + + // if we add a block of height SPALING_ACTIVATION_HEIGHT +!, we can now scan both; + blockdb.insert_block(cb2.height as u32, cb2_bytes).await.unwrap(); + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + assert!(blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .is_ok()); + + assert_eq!( + walletdb.get_balance(AccountId(0)).await.unwrap(), + Amount::from_u64(150_000).unwrap() + ); + } + + #[wasm_bindgen_test] + async fn test_scan_cached_blokcs_finds_received_notes() { + // init blocks_db + let ctx = mm_ctx_with_custom_db(); + let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) + .await + .unwrap(); + + // init walletdb. + let walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + let consensus_params = consensus_params(); + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + assert!(walletdb.init_accounts_table(&[extfvk.clone()]).await.is_ok()); + + // Account balance should be zero + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), Amount::zero()); + + // Create a fake compactblock sending value to the address + let value = Amount::from_u64(5).unwrap(); + let (cb1, _) = fake_compact_block(sapling_activation_height(), BlockHash([0; 32]), extfvk.clone(), value); + let cb1_bytes = cb1.write_to_bytes().unwrap(); + blockdb.insert_block(cb1.height as u32, cb1_bytes).await.unwrap(); + + // Scan the cache + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + assert!(blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .is_ok()); + + // Account balance should reflect the received note + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value); + + // Create a second fake Compactblock sending more value to the address + let value2 = Amount::from_u64(7).unwrap(); + let (cb2, _) = fake_compact_block(sapling_activation_height() + 1, cb1.hash(), extfvk.clone(), value2); + let cb2_bytes = cb2.write_to_bytes().unwrap(); + blockdb.insert_block(cb2.height as u32, cb2_bytes).await.unwrap(); + + // Scan the cache again + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + assert!(blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .is_ok()); + + // Account balance should reflect the received note + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value + value2); + } + + #[wasm_bindgen_test] + async fn test_scan_cached_blocks_finds_change_notes() { + // init blocks_db + let ctx = mm_ctx_with_custom_db(); + let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) + .await + .unwrap(); + + // init walletdb. + let walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + let consensus_params = consensus_params(); + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + assert!(walletdb.init_accounts_table(&[extfvk.clone()]).await.is_ok()); + + // Account balance should be zero + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), Amount::zero()); + + // Create a fake compactblock sending value to the address + let value = Amount::from_u64(5).unwrap(); + let (cb1, nf) = fake_compact_block(sapling_activation_height(), BlockHash([0; 32]), extfvk.clone(), value); + let cb1_bytes = cb1.write_to_bytes().unwrap(); + blockdb.insert_block(cb1.height as u32, cb1_bytes).await.unwrap(); + + // Scan the cache + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + assert!(blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await + .is_ok()); + + // Account balance should reflect the received note + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value); + + // Create a second fake Compactblock spending value from the address + let extsk2 = ExtendedSpendingKey::master(&[0]); + let to2 = extsk2.default_address().unwrap().1; + let value2 = Amount::from_u64(2).unwrap(); + let cb2 = fake_compact_block_spending( + sapling_activation_height() + 1, + cb1.hash(), + (nf, value), + extfvk, + to2, + value2, + ); + let cb2_bytes = cb2.write_to_bytes().unwrap(); + blockdb.insert_block(cb2.height as u32, cb2_bytes).await.unwrap(); + + // Scan the cache again + let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + let scan = blockdb + .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + .await; + assert!(scan.is_ok()); + + // Account balance should equal the change + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value - value2); + } + + fn network() -> Network { Network::TestNetwork } + + // Todo: Uncomment after improving tx creation time + // https://github.com/KomodoPlatform/komodo-defi-framework/issues/2000 + // #[wasm_bindgen_test] + // async fn create_to_address_fails_on_unverified_notes() { + // // init blocks_db + // let ctx = mm_ctx_with_custom_db(); + // let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()).await.unwrap(); + // + // // init walletdb. + // let mut walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + // let consensus_params = consensus_params(); + // + // // Add an account to the wallet + // let extsk = ExtendedSpendingKey::master(&[]); + // let extfvk = ExtendedFullViewingKey::from(&extsk); + // assert!(walletdb.init_accounts_table(&[extfvk.clone()]).await.is_ok()); + // + // // Account balance should be zero + // assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), Amount::zero()); + // + // // Add funds to the wallet in a single note + // let value = Amount::from_u64(50000).unwrap(); + // let (cb, _) = fake_compact_block(sapling_activation_height(), BlockHash([0; 32]), extfvk.clone(), value); + // let cb_bytes = cb.write_to_bytes().unwrap(); + // blockdb.insert_block(cb.height as u32, cb_bytes).await.unwrap(); + // + // // Scan the cache + // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + // assert!(blockdb + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + // .await + // .is_ok()); + // + // // Verified balance matches total balance + // let (_, anchor_height) = walletdb.get_target_and_anchor_heights().await.unwrap().unwrap(); + // assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value); + // assert_eq!( + // walletdb.get_balance_at(AccountId(0), anchor_height).await.unwrap(), + // value + // ); + // + // // Add more funds to the wallet in a second note + // let (cb, _) = fake_compact_block(sapling_activation_height() + 1, cb.hash(), extfvk.clone(), value); + // let cb_bytes = cb.write_to_bytes().unwrap(); + // blockdb.insert_block(cb.height as u32, cb_bytes).await.unwrap(); + // + // // Scan the cache + // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + // assert!(blockdb + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + // .await + // .is_ok()); + // + // // Verified balance does not include the second note + // let (_, anchor_height2) = walletdb.get_target_and_anchor_heights().await.unwrap().unwrap(); + // assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value + value); + // assert_eq!( + // walletdb.get_balance_at(AccountId(0), anchor_height2).await.unwrap(), + // value + // ); + // + // // Spend fails because there are insufficient verified notes + // let extsk2 = ExtendedSpendingKey::master(&[]); + // let to = extsk2.default_address().unwrap().1.into(); + // match create_spend_to_address( + // &mut walletdb, + // &network(), + // test_prover().await, + // AccountId(0), + // &extsk, + // &to, + // Amount::from_u64(70000).unwrap(), + // None, + // OvkPolicy::Sender, + // ) + // .await + // { + // Ok(_) => panic!("Should have failed"), + // Err(e) => assert!(e + // .to_string() + // .contains("Insufficient balance (have 50000, need 71000 including fee)")), + // } + // + // // Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second + // // note is verified + // for i in 2..10 { + // let (cb, _) = fake_compact_block(sapling_activation_height() + i, cb.hash(), extfvk.clone(), value); + // let cb_bytes = cb.write_to_bytes().unwrap(); + // blockdb.insert_block(cb.height as u32, cb_bytes).await.unwrap(); + // } + // + // // Scan the cache + // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + // assert!(blockdb + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + // .await + // .is_ok()); + // + // // Second spend still fails + // match create_spend_to_address( + // &mut walletdb, + // &network(), + // test_prover().await, + // AccountId(0), + // &extsk, + // &to, + // Amount::from_u64(70000).unwrap(), + // None, + // OvkPolicy::Sender, + // ) + // .await + // { + // Ok(_) => panic!("Should have failed"), + // Err(e) => assert!(e + // .to_string() + // .contains("Insufficient balance (have 50000, need 71000 including fee)")), + // } + // + // // Mine block 11 so that the second note becomes verified + // let (cb, _) = fake_compact_block(sapling_activation_height() + 10, cb.hash(), extfvk, value); + // let cb_bytes = cb.write_to_bytes().unwrap(); + // blockdb.insert_block(cb.height as u32, cb_bytes).await.unwrap(); + // // Scan the cache + // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + // assert!(blockdb + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + // .await + // .is_ok()); + // + // // Second spend should now succeed + // create_spend_to_address( + // &mut walletdb, + // &network(), + // test_prover().await, + // AccountId(0), + // &extsk, + // &to, + // Amount::from_u64(70000).unwrap(), + // None, + // OvkPolicy::Sender, + // ) + // .await + // .unwrap(); + // } + + #[wasm_bindgen_test] + async fn test_create_to_address_fails_on_incorrect_extsk() { + // init walletdb. + let ctx = mm_ctx_with_custom_db(); + let mut walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + + // Add two accounts to the wallet + let extsk0 = ExtendedSpendingKey::master(&[]); + let extsk1 = ExtendedSpendingKey::master(&[0]); + let extfvks = [ + ExtendedFullViewingKey::from(&extsk0), + ExtendedFullViewingKey::from(&extsk1), + ]; + assert!(walletdb.init_accounts_table(&extfvks).await.is_ok()); + + let to = extsk0.default_address().unwrap().1.into(); + match create_spend_to_address( + &mut walletdb, + &network(), + test_prover().await, + AccountId(0), + &extsk1, + &to, + Amount::from_u64(1).unwrap(), + None, + OvkPolicy::Sender, + ) + .await + { + Ok(_) => panic!("Should have failed"), + Err(e) => assert!(e.to_string().contains("Incorrect ExtendedSpendingKey for account 0")), + } + + match create_spend_to_address( + &mut walletdb, + &network(), + test_prover().await, + AccountId(1), + &extsk0, + &to, + Amount::from_u64(1).unwrap(), + None, + OvkPolicy::Sender, + ) + .await + { + Ok(_) => panic!("Should have failed"), + Err(e) => assert!(e.to_string().contains("Incorrect ExtendedSpendingKey for account 1")), + } + } + + #[wasm_bindgen_test] + async fn test_create_to_address_fails_with_no_blocks() { + // init walletdb. + let ctx = mm_ctx_with_custom_db(); + let mut walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + + // Add two accounts to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvks = [ExtendedFullViewingKey::from(&extsk)]; + assert!(walletdb.init_accounts_table(&extfvks).await.is_ok()); + + let to = extsk.default_address().unwrap().1.into(); + match create_spend_to_address( + &mut walletdb, + &network(), + test_prover().await, + AccountId(0), + &extsk, + &to, + Amount::from_u64(1).unwrap(), + None, + OvkPolicy::Sender, + ) + .await + { + Ok(_) => panic!("Should have failed"), + Err(e) => assert!(e.to_string().contains("Must scan blocks first")), + } + } + + #[wasm_bindgen_test] + async fn test_create_to_address_fails_on_insufficient_balance() { + // init walletdb. + let ctx = mm_ctx_with_custom_db(); + let mut walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + + assert!(walletdb + .init_blocks_table(BlockHeight::from(1), BlockHash([1; 32]), 1, &[]) + .await + .is_ok()); + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvks = [ExtendedFullViewingKey::from(&extsk)]; + assert!(walletdb.init_accounts_table(&extfvks).await.is_ok()); + let to = extsk.default_address().unwrap().1.into(); + + // Account balance should be zero + assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), Amount::zero()); + + // We cannot spend anything + match create_spend_to_address( + &mut walletdb, + &network(), + test_prover().await, + AccountId(0), + &extsk, + &to, + Amount::from_u64(1).unwrap(), + None, + OvkPolicy::Sender, + ) + .await + { + Ok(_) => panic!("Should have failed"), + Err(e) => assert!(e + .to_string() + .contains("Insufficient balance (have 0, need 1001 including fee)")), + } + } + + // Todo: Uncomment after improving tx creation time + // https://github.com/KomodoPlatform/komodo-defi-framework/issues/2000 + // #[wasm_bindgen_test] + // async fn test_create_to_address_fails_on_locked_notes() { + // register_wasm_log(); + // + // // init blocks_db + // let ctx = mm_ctx_with_custom_db(); + // let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()).await.unwrap(); + // + // // init walletdb. + // let mut walletdb = wallet_db_from_zcoin_builder_for_test(&ctx, TICKER).await; + // let consensus_params = consensus_params(); + // + // // Add an account to the wallet + // let extsk = ExtendedSpendingKey::master(&[]); + // let extfvk = ExtendedFullViewingKey::from(&extsk); + // assert!(walletdb.init_accounts_table(&[extfvk.clone()]).await.is_ok()); + // + // // Add funds to the wallet in a single note + // let value = Amount::from_u64(50000).unwrap(); + // let (cb, _) = fake_compact_block(sapling_activation_height(), BlockHash([0; 32]), extfvk, value); + // let cb_bytes = cb.write_to_bytes().unwrap(); + // blockdb.insert_block(cb.height as u32, cb_bytes).await.unwrap(); + // + // // Scan the cache + // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + // blockdb + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + // .await + // .unwrap(); + // assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value); + // + // // Send some of the funds to another address + // let extsk2 = ExtendedSpendingKey::master(&[]); + // let to = extsk2.default_address().unwrap().1.into(); + // create_spend_to_address( + // &mut walletdb, + // &network(), + // test_prover().await, + // AccountId(0), + // &extsk, + // &to, + // Amount::from_u64(15000).unwrap(), + // None, + // OvkPolicy::Sender, + // ) + // .await + // .unwrap(); + // + // // A second spend fails because there are no usable notes + // match create_spend_to_address( + // &mut walletdb, + // &network(), + // test_prover().await, + // AccountId(0), + // &extsk, + // &to, + // Amount::from_u64(2000).unwrap(), + // None, + // OvkPolicy::Sender, + // ) + // .await + // { + // Ok(_) => panic!("Should have failed"), + // Err(e) => assert!(e + // .to_string() + // .contains("Insufficient balance (have 0, need 3000 including fee)")), + // } + // + // // Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 21 (that don't send us funds) + // // until just before the first transaction expires + // for i in 1..22 { + // let (cb, _) = fake_compact_block( + // sapling_activation_height() + i, + // cb.hash(), + // ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])), + // value, + // ); + // let cb_bytes = cb.write_to_bytes().unwrap(); + // blockdb.insert_block(cb.height as u32, cb_bytes).await.unwrap(); + // } + // // Scan the cache + // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + // blockdb + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + // .await + // .unwrap(); + // + // // Second spend still fails + // match create_spend_to_address( + // &mut walletdb, + // &network(), + // test_prover().await, + // AccountId(0), + // &extsk, + // &to, + // Amount::from_u64(2000).unwrap(), + // None, + // OvkPolicy::Sender, + // ) + // .await + // { + // Ok(_) => panic!("Should have failed"), + // Err(e) => assert!(e + // .to_string() + // .contains("Insufficient balance (have 0, need 3000 including fee)")), + // } + // + // // Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires + // let (cb, _) = fake_compact_block( + // sapling_activation_height() + 22, + // cb.hash(), + // ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])), + // value, + // ); + // let cb_bytes = cb.write_to_bytes().unwrap(); + // blockdb.insert_block(cb.height as u32, cb_bytes).await.unwrap(); + // // Scan the cache + // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); + // blockdb + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan), None, None) + // .await + // .unwrap(); + // + // // Second spend should now succeed + // create_spend_to_address( + // &mut walletdb, + // &network(), + // test_prover().await, + // AccountId(0), + // &extsk, + // &to, + // Amount::from_u64(2000).unwrap(), + // None, + // OvkPolicy::Sender, + // ) + // .await + // .unwrap(); + // } +} diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs new file mode 100644 index 0000000000..e55b9e64d0 --- /dev/null +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs @@ -0,0 +1,1484 @@ +use crate::z_coin::storage::walletdb::wasm::tables::{WalletDbAccountsTable, WalletDbBlocksTable, + WalletDbReceivedNotesTable, WalletDbSaplingWitnessesTable, + WalletDbSentNotesTable, WalletDbTransactionsTable}; +use crate::z_coin::storage::wasm::{to_spendable_note, SpendableNoteConstructor}; +use crate::z_coin::storage::ZcoinStorageRes; +use crate::z_coin::z_coin_errors::ZcoinStorageError; +use crate::z_coin::{CheckPointBlockInfo, WalletDbShared, ZCoinBuilder, ZcoinConsensusParams}; + +use async_trait::async_trait; +use common::log::info; +use ff::PrimeField; +use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::{ConstructibleDb, DbIdentifier, DbInstance, DbLocked, IndexedDb, IndexedDbBuilder, + InitDbResult, MultiIndex, SharedDb}; +use mm2_err_handle::prelude::*; +use mm2_number::num_bigint::ToBigInt; +use mm2_number::BigInt; +use num_traits::{FromPrimitive, ToPrimitive}; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::ops::Deref; +use zcash_client_backend::address::RecipientAddress; +use zcash_client_backend::data_api::{PrunedBlock, ReceivedTransaction, SentTransaction}; +use zcash_client_backend::encoding::{decode_extended_full_viewing_key, decode_payment_address, + encode_extended_full_viewing_key, encode_payment_address}; +use zcash_client_backend::wallet::{AccountId, SpendableNote, WalletTx}; +use zcash_client_backend::DecryptedOutput; +use zcash_extras::{NoteId, ShieldedOutput, WalletRead, WalletWrite}; +use zcash_primitives::block::BlockHash; +use zcash_primitives::consensus::{BlockHeight, NetworkUpgrade, Parameters}; +use zcash_primitives::memo::{Memo, MemoBytes}; +use zcash_primitives::merkle_tree::{CommitmentTree, IncrementalWitness}; +use zcash_primitives::sapling::{Node, Nullifier, PaymentAddress}; +use zcash_primitives::transaction::components::Amount; +use zcash_primitives::transaction::{Transaction, TxId}; +use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; + +const DB_NAME: &str = "wallet_db_cache"; +const DB_VERSION: u32 = 1; + +pub type WalletDbInnerLocked<'a> = DbLocked<'a, WalletDbInner>; + +macro_rules! num_to_bigint { + ($value: ident) => { + $value.to_bigint().ok_or_else(|| { + $crate::z_coin::z_coin_errors::ZcoinStorageError::CorruptedData( + "Number is too large to fit in a BigInt".to_string(), + ) + }) + }; +} + +impl<'a> WalletDbShared { + pub async fn new( + builder: &ZCoinBuilder<'a>, + checkpoint_block: Option, + z_spending_key: &ExtendedSpendingKey, + continue_from_prev_sync: bool, + ) -> ZcoinStorageRes { + let ticker = builder.ticker; + let consensus_params = builder.protocol_info.consensus_params.clone(); + let db = WalletIndexedDb::new(builder.ctx, ticker, consensus_params).await?; + let extrema = db.block_height_extrema().await?; + let get_evk = db.get_extended_full_viewing_keys().await?; + let evk = ExtendedFullViewingKey::from(z_spending_key); + let min_sync_height = extrema.map(|(min, _)| u32::from(min)); + let init_block_height = checkpoint_block.clone().map(|block| block.height); + + if get_evk.is_empty() || (!continue_from_prev_sync && init_block_height != min_sync_height) { + // let user know we're clearing cache and resyncing from new provided height. + if min_sync_height.unwrap_or(0) > 0 { + info!("Older/Newer sync height detected!, rewinding walletdb to new height: {init_block_height:?}"); + } + db.rewind_to_height(BlockHeight::from(u32::MIN)).await?; + if let Some(block) = checkpoint_block { + db.init_blocks_table( + BlockHeight::from_u32(block.height), + BlockHash(block.hash.0), + block.time, + &block.sapling_tree.0, + ) + .await?; + } + } + + if get_evk.is_empty() { + db.init_accounts_table(&[evk]).await?; + }; + + Ok(Self { + db, + ticker: ticker.to_string(), + }) + } + + pub async fn is_tx_imported(&self, tx_id: TxId) -> MmResult { + self.db.is_tx_imported(tx_id).await + } +} + +pub struct WalletDbInner(pub IndexedDb); + +impl WalletDbInner { + pub fn get_inner(&self) -> &IndexedDb { &self.0 } +} + +#[async_trait] +impl DbInstance for WalletDbInner { + const DB_NAME: &'static str = DB_NAME; + + async fn init(db_id: DbIdentifier) -> InitDbResult { + Ok(Self( + IndexedDbBuilder::new(db_id) + .with_version(DB_VERSION) + .with_table::() + .with_table::() + .with_table::() + .with_table::() + .with_table::() + .with_table::() + .build() + .await?, + )) + } +} + +#[derive(Clone)] +pub struct WalletIndexedDb { + pub db: SharedDb, + pub ticker: String, + pub params: ZcoinConsensusParams, +} + +impl<'a> WalletIndexedDb { + pub async fn new( + ctx: &MmArc, + ticker: &str, + consensus_params: ZcoinConsensusParams, + ) -> MmResult { + let db = Self { + db: ConstructibleDb::new(ctx).into_shared(), + ticker: ticker.to_string(), + params: consensus_params, + }; + + Ok(db) + } + + async fn lock_db(&self) -> ZcoinStorageRes> { + self.db + .get_or_initialize() + .await + .mm_err(|err| ZcoinStorageError::DbError(err.to_string())) + } + + pub async fn is_tx_imported(&self, tx_id: TxId) -> ZcoinStorageRes { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + + let tx_table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(WalletDbTransactionsTable::TICKER_TXID_INDEX) + .with_value(&self.ticker)? + .with_value(tx_id.0.to_vec())?; + let maybe_tx = tx_table.get_items_by_multi_index(index_keys).await?; + + if !maybe_tx.is_empty() { + Ok(true) + } else { + Ok(false) + } + } + + 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?; + let walletdb_account_table = db_transaction.table::().await?; + + // check if account exists + let maybe_min_account = walletdb_account_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("height", 0u32, u32::MAX) + .where_first() + .open_cursor(WalletDbAccountsTable::TICKER_ACCOUNT_INDEX) + .await? + .next() + .await?; + if maybe_min_account.is_some() { + return MmError::err(ZcoinStorageError::TableNotEmpty( + "Account table is not empty".to_string(), + )); + } + + // Insert accounts + for (account, extfvk) in extfvks.iter().enumerate() { + let account_int = num_to_bigint!(account)?; + + let address = extfvk.default_address().unwrap().1; + let address = encode_payment_address(self.params.hrp_sapling_payment_address(), &address); + + let account = WalletDbAccountsTable { + account: account_int.clone(), + extfvk: encode_extended_full_viewing_key(self.params.hrp_sapling_extended_full_viewing_key(), extfvk), + address, + ticker: self.ticker.clone(), + }; + + let index_keys = MultiIndex::new(WalletDbAccountsTable::TICKER_ACCOUNT_INDEX) + .with_value(&self.ticker)? + .with_value(account_int)?; + + walletdb_account_table + .replace_item_by_unique_multi_index(index_keys, &account) + .await?; + } + + Ok(()) + } + + pub(crate) async fn init_blocks_table( + &self, + height: BlockHeight, + hash: BlockHash, + time: u32, + sapling_tree: &[u8], + ) -> ZcoinStorageRes<()> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let walletdb_account_table = db_transaction.table::().await?; + + // check if account exists + let maybe_min_account = walletdb_account_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("height", 0u32, u32::MAX) + .where_first() + .open_cursor(WalletDbBlocksTable::TICKER_HEIGHT_INDEX) + .await? + .next() + .await?; + if maybe_min_account.is_some() { + return MmError::err(ZcoinStorageError::TableNotEmpty( + "Account table is not empty".to_string(), + )); + } + + let block = WalletDbBlocksTable { + height: u32::from(height), + hash: hash.0.to_vec(), + time, + sapling_tree: sapling_tree.to_vec(), + ticker: self.ticker.clone(), + }; + let walletdb_blocks_table = db_transaction.table::().await?; + let height = u32::from(height); + let index_keys = MultiIndex::new(WalletDbBlocksTable::TICKER_HEIGHT_INDEX) + .with_value(&self.ticker)? + .with_value(num_to_bigint!(height)?)?; + walletdb_blocks_table + .replace_item_by_unique_multi_index(index_keys, &block) + .await?; + + Ok(()) + } +} + +impl WalletIndexedDb { + pub async fn insert_block( + &self, + block_height: BlockHeight, + block_hash: BlockHash, + block_time: u32, + commitment_tree: &CommitmentTree, + ) -> ZcoinStorageRes<()> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let walletdb_blocks_table = db_transaction.table::().await?; + + let mut encoded_tree = Vec::new(); + commitment_tree.write(&mut encoded_tree).unwrap(); + + let hash = &block_hash.0[..]; + let block = WalletDbBlocksTable { + height: u32::from(block_height), + hash: hash.to_vec(), + time: block_time, + sapling_tree: encoded_tree, + ticker: self.ticker.clone(), + }; + + let index_keys = MultiIndex::new(WalletDbBlocksTable::TICKER_HEIGHT_INDEX) + .with_value(&self.ticker)? + .with_value(u32::from(block_height))?; + + Ok(walletdb_blocks_table + .replace_item_by_unique_multi_index(index_keys, &block) + .await + .map(|_| ())?) + } + + pub async fn get_balance(&self, account: AccountId) -> ZcoinStorageRes { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let rec_note_table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(WalletDbReceivedNotesTable::TICKER_ACCOUNT_INDEX) + .with_value(&self.ticker)? + .with_value(account.0.to_bigint().unwrap())?; + let maybe_notes = rec_note_table.get_items_by_multi_index(index_keys).await?; + + let tx_table = db_transaction.table::().await?; + let txs = tx_table.get_items("ticker", &self.ticker).await?; + + let balance: i64 = maybe_notes + .iter() + .map(|(_, note)| { + txs.iter() + .filter_map(|(tx_id, tx)| { + if *tx_id == note.tx && note.spent.is_none() && tx.block.is_some() { + Some(note.value.to_i64().expect("BigInt is too large to fit in an i64")) + } else { + None + } + }) + .sum::() + }) + .sum(); + + match Amount::from_i64(balance) { + Ok(amount) if !amount.is_negative() => Ok(amount), + _ => MmError::err(ZcoinStorageError::CorruptedData( + "Sum of values in received_notes is out of range".to_string(), + )), + } + } + + pub async fn put_tx_data(&self, tx: &Transaction, created_at: Option) -> ZcoinStorageRes { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let tx_table = db_transaction.table::().await?; + + let mut raw_tx = vec![]; + tx.write(&mut raw_tx).unwrap(); + let txid = tx.txid().0.to_vec(); + + let index_keys = MultiIndex::new(WalletDbTransactionsTable::TICKER_TXID_INDEX) + .with_value(&self.ticker)? + .with_value(&txid)?; + let single_tx = tx_table.get_item_by_unique_multi_index(index_keys).await?; + if let Some((id_tx, some_tx)) = single_tx { + let updated_tx = WalletDbTransactionsTable { + txid: txid.clone(), + created: some_tx.created, + block: some_tx.block, + tx_index: some_tx.tx_index, + expiry_height: Some(u32::from(tx.expiry_height)), + raw: Some(raw_tx), + ticker: self.ticker.clone(), + }; + tx_table.replace_item(id_tx, &updated_tx).await?; + + return Ok(id_tx as i64); + }; + + let new_tx = WalletDbTransactionsTable { + txid: txid.clone(), + created: created_at, + block: None, + tx_index: None, + expiry_height: Some(u32::from(tx.expiry_height)), + raw: Some(raw_tx), + ticker: self.ticker.clone(), + }; + let index_keys = MultiIndex::new(WalletDbTransactionsTable::TICKER_TXID_INDEX) + .with_value(&self.ticker)? + .with_value(txid)?; + + Ok(tx_table + .replace_item_by_unique_multi_index(index_keys, &new_tx) + .await? + .into()) + } + + pub async fn put_tx_meta(&self, tx: &WalletTx, height: BlockHeight) -> ZcoinStorageRes { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let tx_table = db_transaction.table::().await?; + + let txid = tx.txid.0.to_vec(); + let index_keys = MultiIndex::new(WalletDbTransactionsTable::TICKER_TXID_INDEX) + .with_value(&self.ticker)? + .with_value(&txid)?; + let single_tx = tx_table.get_item_by_unique_multi_index(index_keys).await?; + + if let Some((id_tx, some_tx)) = single_tx { + let updated_tx = WalletDbTransactionsTable { + txid: some_tx.txid.clone(), + created: some_tx.created, + block: Some(u32::from(height)), + tx_index: Some(tx.index as i64), + expiry_height: some_tx.expiry_height, + raw: some_tx.raw, + ticker: self.ticker.clone(), + }; + tx_table.replace_item(id_tx, &updated_tx).await?; + + return Ok(id_tx as i64); + }; + + let new_tx = WalletDbTransactionsTable { + txid: txid.clone(), + created: None, + block: Some(u32::from(height)), + tx_index: Some(tx.index as i64), + expiry_height: None, + raw: None, + ticker: self.ticker.clone(), + }; + let index_keys = MultiIndex::new(WalletDbTransactionsTable::TICKER_TXID_INDEX) + .with_value(&self.ticker)? + .with_value(txid)?; + + Ok(tx_table + .replace_item_by_unique_multi_index(index_keys, &new_tx) + .await? + .into()) + } + + pub async fn mark_spent(&self, tx_ref: i64, nf: &Nullifier) -> ZcoinStorageRes<()> { + let ticker = self.ticker.clone(); + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let received_notes_table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(WalletDbReceivedNotesTable::TICKER_NF_INDEX) + .with_value(&ticker)? + .with_value(nf.0.to_vec())?; + let maybe_note = received_notes_table.get_item_by_unique_multi_index(index_keys).await?; + + if let Some((id, note)) = maybe_note { + let new_received_note = WalletDbReceivedNotesTable { + tx: note.tx, + output_index: note.output_index, + account: note.account, + diversifier: note.diversifier, + value: note.value, + rcm: note.rcm, + nf: note.nf, + is_change: note.is_change, + memo: note.memo, + spent: Some(num_to_bigint!(tx_ref)?), + ticker, + }; + received_notes_table.replace_item(id, &new_received_note).await?; + + return Ok(()); + } + + MmError::err(ZcoinStorageError::GetFromStorageError("note not found".to_string())) + } + + pub async fn put_received_note(&self, output: &T, tx_ref: i64) -> ZcoinStorageRes { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + + let rcm = output.note().rcm().to_repr(); + let account = BigInt::from(output.account().0); + let diversifier = output.to().diversifier().0.to_vec(); + let value = output.note().value.into(); + let rcm = rcm.to_vec(); + let memo = output.memo().map(|m| m.as_slice().to_vec()); + let is_change = output.is_change(); + let tx = tx_ref as u32; + let output_index = output.index() as u32; + let nf_bytes = output.nullifier().map(|nf| nf.0.to_vec()); + + let received_note_table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(WalletDbReceivedNotesTable::TICKER_TX_OUTPUT_INDEX) + .with_value(&self.ticker)? + .with_value(tx)? + .with_value(output_index)?; + let current_note = received_note_table.get_item_by_unique_multi_index(index_keys).await?; + + let id = if let Some((id, note)) = current_note { + let temp_note = WalletDbReceivedNotesTable { + tx, + output_index, + account: note.account, + diversifier, + value, + rcm, + nf: note.nf.or(nf_bytes), + is_change: note.is_change.or(is_change), + memo: note.memo.or(memo), + spent: note.spent, + ticker: self.ticker.clone(), + }; + received_note_table.replace_item(id, &temp_note).await? + } else { + let new_note = WalletDbReceivedNotesTable { + tx, + output_index, + account, + diversifier, + value, + rcm, + nf: nf_bytes, + is_change, + memo, + spent: None, + ticker: self.ticker.clone(), + }; + + let index_keys = MultiIndex::new(WalletDbReceivedNotesTable::TICKER_TX_OUTPUT_INDEX) + .with_value(&self.ticker)? + .with_value(tx)? + .with_value(num_to_bigint!(output_index)?)?; + received_note_table + .replace_item_by_unique_multi_index(index_keys, &new_note) + .await? + }; + + Ok(NoteId::ReceivedNoteId(id.into())) + } + + pub async fn insert_witness( + &self, + note_id: i64, + witness: &IncrementalWitness, + height: BlockHeight, + ) -> ZcoinStorageRes<()> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let witness_table = db_transaction.table::().await?; + + let mut encoded = Vec::new(); + witness.write(&mut encoded).unwrap(); + + let note_id_int = BigInt::from_i64(note_id).unwrap(); + let witness = WalletDbSaplingWitnessesTable { + note: note_id_int, + block: u32::from(height), + witness: encoded, + ticker: self.ticker.clone(), + }; + + Ok(witness_table.add_item(&witness).await.map(|_| ())?) + } + + pub async fn prune_witnesses(&self, below_height: BlockHeight) -> ZcoinStorageRes<()> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let witness_table = db_transaction.table::().await?; + + let mut maybe_witness = witness_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("block", 0u32, (below_height - 1).into()) + .open_cursor(WalletDbSaplingWitnessesTable::TICKER_BLOCK_INDEX) + .await?; + + while let Some((id, _)) = maybe_witness.next().await? { + witness_table.delete_item(id).await?; + } + + Ok(()) + } + + pub async fn update_expired_notes(&self, height: BlockHeight) -> ZcoinStorageRes<()> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + // fetch received_notes. + let received_notes_table = db_transaction.table::().await?; + let maybe_notes = received_notes_table.get_items("ticker", &self.ticker).await?; + + // fetch transactions with block < height . + let txs_table = db_transaction.table::().await?; + let mut maybe_txs = txs_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("expiry_height", 0u32, u32::from(height - 1)) + .reverse() + .open_cursor(WalletDbTransactionsTable::TICKER_EXP_HEIGHT_INDEX) + .await?; + + while let Some((id, note)) = maybe_txs.next().await? { + if note.block.is_none() { + if let Some(curr) = maybe_notes.iter().find(|(_, n)| n.spent == id.to_bigint()) { + let temp_note = WalletDbReceivedNotesTable { + tx: curr.1.tx, + output_index: curr.1.output_index, + account: curr.1.account.clone(), + diversifier: curr.1.diversifier.clone(), + value: curr.1.value.clone(), + rcm: curr.1.rcm.clone(), + nf: curr.1.nf.clone(), + is_change: curr.1.is_change, + memo: curr.1.memo.clone(), + spent: None, + ticker: self.ticker.clone(), + }; + + received_notes_table.replace_item(curr.0, &temp_note).await?; + } + }; + } + + Ok(()) + } + + pub async fn put_sent_note(&self, output: &DecryptedOutput, tx_ref: i64) -> ZcoinStorageRes<()> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + + let tx_ref = num_to_bigint!(tx_ref)?; + let output_index = output.index; + let output_index = num_to_bigint!(output_index)?; + let from_account = output.account.0; + let from_account = num_to_bigint!(from_account)?; + let value = output.note.value; + let value = num_to_bigint!(value)?; + let address = encode_payment_address(self.params.hrp_sapling_payment_address(), &output.to); + + let sent_note_table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(WalletDbSentNotesTable::TICKER_TX_OUTPUT_INDEX) + .with_value(&self.ticker)? + .with_value(&tx_ref)? + .with_value(&output_index)?; + let maybe_note = sent_note_table.get_item_by_unique_multi_index(index_keys).await?; + + let update_note = WalletDbSentNotesTable { + tx: tx_ref.clone(), + output_index: output_index.clone(), + from_account, + address, + value, + memo: Some(output.memo.as_slice().to_vec()), + ticker: self.ticker.clone(), + }; + if let Some((id, _)) = maybe_note { + sent_note_table.replace_item(id, &update_note).await?; + } else { + let index_keys = MultiIndex::new(WalletDbReceivedNotesTable::TICKER_TX_OUTPUT_INDEX) + .with_value(&self.ticker)? + .with_value(tx_ref)? + .with_value(output_index)?; + sent_note_table + .replace_item_by_unique_multi_index(index_keys, &update_note) + .await?; + } + + Ok(()) + } + + pub async fn insert_sent_note( + &self, + tx_ref: i64, + output_index: usize, + account: AccountId, + to: &RecipientAddress, + value: Amount, + memo: Option<&MemoBytes>, + ) -> ZcoinStorageRes<()> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let sent_note_table = db_transaction.table::().await?; + + let tx_ref = num_to_bigint!(tx_ref)?; + let output_index = num_to_bigint!(output_index)?; + let from_account = account.0; + let from_account = num_to_bigint!(from_account)?; + let value = i64::from(value); + let value = num_to_bigint!(value)?; + let address = to.encode(&self.params); + let new_note = WalletDbSentNotesTable { + tx: tx_ref.clone(), + output_index: output_index.clone(), + from_account, + address, + value, + memo: memo.map(|m| m.as_slice().to_vec()), + ticker: self.ticker.clone(), + }; + let index_keys = MultiIndex::new(WalletDbReceivedNotesTable::TICKER_TX_OUTPUT_INDEX) + .with_value(&self.ticker)? + .with_value(tx_ref)? + .with_value(output_index)?; + + Ok(sent_note_table + .replace_item_by_unique_multi_index(index_keys, &new_note) + .await + .map(|_| ())?) + } + + /// Asynchronously rewinds the storage to a specified block height, effectively + /// removing data beyond the specified height from the storage. + pub async fn rewind_to_height(&self, block_height: BlockHeight) -> ZcoinStorageRes<()> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + + let block_height = u32::from(block_height); + + // Recall where we synced up to previously. + let blocks_table = db_transaction.table::().await?; + let maybe_height = blocks_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("height", 0u32, u32::MAX) + .reverse() + .where_first() + .open_cursor(WalletDbBlocksTable::TICKER_HEIGHT_INDEX) + .await? + .next() + .await? + .map(|(_, item)| { + item.height + .to_u32() + .ok_or_else(|| ZcoinStorageError::GetFromStorageError("height is too large".to_string())) + }) + .transpose()?; + let sapling_activation_height = self + .params + .activation_height(NetworkUpgrade::Sapling) + .ok_or_else(|| ZcoinStorageError::BackendError("Sapling not active".to_string()))?; + let maybe_height = maybe_height.unwrap_or_else(|| (sapling_activation_height - 1).into()); + + if block_height >= maybe_height { + return Ok(()); + }; + + // Decrement witnesses. + let db_transaction = locked_db.get_inner().transaction().await?; + let witnesses_table = db_transaction.table::().await?; + let maybe_witnesses_cursor = witnesses_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("block", block_height + 1, u32::MAX) + .open_cursor(WalletDbSaplingWitnessesTable::TICKER_BLOCK_INDEX) + .await? + .collect() + .await?; + + for (id, _witness) in maybe_witnesses_cursor { + witnesses_table.delete_item(id).await?; + } + + // Un-mine transactions. + let db_transaction = locked_db.get_inner().transaction().await?; + let transactions_table = db_transaction.table::().await?; + let mut maybe_txs_cursor = transactions_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("block", block_height + 1, u32::MAX) + .open_cursor(WalletDbTransactionsTable::TICKER_BLOCK_INDEX) + .await?; + while let Some((_, tx)) = maybe_txs_cursor.next().await? { + let modified_tx = WalletDbTransactionsTable { + txid: tx.txid.clone(), + created: tx.created.clone(), + block: None, + tx_index: None, + expiry_height: tx.expiry_height, + raw: tx.raw.clone(), + ticker: self.ticker.clone(), + }; + let index_keys = MultiIndex::new(WalletDbTransactionsTable::TICKER_TXID_INDEX) + .with_value(&self.ticker)? + .with_value(tx.txid)?; + transactions_table + .replace_item_by_unique_multi_index(index_keys, &modified_tx) + .await?; + } + + // Now that they aren't depended on, delete scanned blocks. + let db_transaction = locked_db.get_inner().transaction().await?; + let blocks_table = db_transaction.table::().await?; + let maybe_blocks = blocks_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("height", block_height + 1, u32::MAX) + .open_cursor(WalletDbBlocksTable::TICKER_HEIGHT_INDEX) + .await? + .collect() + .await?; + + for (_, block) in maybe_blocks { + let index_keys = MultiIndex::new(WalletDbBlocksTable::TICKER_HEIGHT_INDEX) + .with_value(&self.ticker)? + .with_value(block.height)?; + blocks_table.delete_item_by_unique_multi_index(index_keys).await?; + } + + Ok(()) + } +} + +#[async_trait] +impl WalletRead for WalletIndexedDb { + type Error = MmError; + type NoteRef = NoteId; + type TxRef = i64; + + async fn block_height_extrema(&self) -> Result, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let block_headers_db = db_transaction.table::().await?; + let earlist_block = block_headers_db + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("height", 0u32, u32::MAX) + .where_first() + .open_cursor(WalletDbBlocksTable::TICKER_HEIGHT_INDEX) + .await? + .next() + .await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let block_headers_db = db_transaction.table::().await?; + let latest_block = block_headers_db + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("height", 0u32, u32::MAX) + .reverse() + .where_first() + .open_cursor(WalletDbBlocksTable::TICKER_HEIGHT_INDEX) + .await? + .next() + .await?; + + if let (Some(min), Some(max)) = (earlist_block, latest_block) { + Ok(Some((BlockHeight::from(min.1.height), BlockHeight::from(max.1.height)))) + } else { + Ok(None) + } + } + + async fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let block_headers_db = db_transaction.table::().await?; + let index_keys = MultiIndex::new(WalletDbBlocksTable::TICKER_HEIGHT_INDEX) + .with_value(&self.ticker)? + .with_value(u32::from(block_height))?; + + Ok(block_headers_db + .get_item_by_unique_multi_index(index_keys) + .await? + .map(|(_, block)| BlockHash::from_slice(&block.hash[..]))) + } + + async fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let block_headers_db = db_transaction.table::().await?; + let index_keys = MultiIndex::new(WalletDbTransactionsTable::TICKER_TXID_INDEX) + .with_value(&self.ticker)? + .with_value(txid.0.to_vec())?; + + Ok(block_headers_db + .get_item_by_unique_multi_index(index_keys) + .await? + .and_then(|(_, tx)| tx.block.map(BlockHeight::from))) + } + + async fn get_address(&self, account: AccountId) -> Result, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let block_headers_db = db_transaction.table::().await?; + let account_num = account.0; + let index_keys = MultiIndex::new(WalletDbAccountsTable::TICKER_ACCOUNT_INDEX) + .with_value(&self.ticker)? + .with_value(num_to_bigint!(account_num)?)?; + + let address = block_headers_db + .get_item_by_unique_multi_index(index_keys) + .await? + .map(|(_, account)| account.address) + .ok_or_else(|| ZcoinStorageError::GetFromStorageError("Invalid account/not found".to_string()))?; + + decode_payment_address(self.params.hrp_sapling_payment_address(), &address).map_to_mm(|err| { + ZcoinStorageError::DecodingError(format!( + "Error occurred while decoding account address: {err:?} - ticker: {}", + self.ticker + )) + }) + } + + async fn get_extended_full_viewing_keys(&self) -> Result, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let accounts_table = db_transaction.table::().await?; + let maybe_accounts = accounts_table.get_items("ticker", &self.ticker).await?; + + let mut res_accounts: HashMap = HashMap::new(); + for (_, account) in maybe_accounts { + let extfvk = + decode_extended_full_viewing_key(self.params.hrp_sapling_extended_full_viewing_key(), &account.extfvk) + .map_to_mm(|err| ZcoinStorageError::DecodingError(format!("{err:?} - ticker: {}", self.ticker))) + .and_then(|k| k.ok_or_else(|| MmError::new(ZcoinStorageError::IncorrectHrpExtFvk))); + let acc_id = account + .account + .to_u32() + .ok_or_else(|| ZcoinStorageError::GetFromStorageError("Invalid account id".to_string()))?; + + res_accounts.insert(AccountId(acc_id), extfvk?); + } + + Ok(res_accounts) + } + + async fn is_valid_account_extfvk( + &self, + account: AccountId, + extfvk: &ExtendedFullViewingKey, + ) -> Result { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let accounts_table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(WalletDbAccountsTable::TICKER_ACCOUNT_INDEX) + .with_value(&self.ticker)? + .with_value(account.0.to_bigint())?; + + let account = accounts_table.get_item_by_unique_multi_index(index_keys).await?; + + if let Some((_, account)) = account { + let expected = + decode_extended_full_viewing_key(self.params.hrp_sapling_extended_full_viewing_key(), &account.extfvk) + .map_to_mm(|err| ZcoinStorageError::DecodingError(format!("{err:?} - ticker: {}", self.ticker))) + .and_then(|k| k.ok_or_else(|| MmError::new(ZcoinStorageError::IncorrectHrpExtFvk)))?; + + return Ok(&expected == extfvk); + } + + Ok(false) + } + + async fn get_balance_at(&self, account: AccountId, anchor_height: BlockHeight) -> Result { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + + let tx_table = db_transaction.table::().await?; + // Retrieves a list of transaction IDs (txid) from the transactions table + // that match the provided account ID. + let txids = tx_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("block", 0u32, u32::from(anchor_height)) + .open_cursor(WalletDbTransactionsTable::TICKER_BLOCK_INDEX) + .await? + .collect() + .await? + .into_iter() + .map(|(id, _)| id) + .collect::>(); + + let received_notes_table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(WalletDbReceivedNotesTable::TICKER_ACCOUNT_INDEX) + .with_value(&self.ticker)? + .with_value(account.0.to_bigint().unwrap())?; + let maybe_notes = received_notes_table.get_items_by_multi_index(index_keys).await?; + + let mut value: i64 = 0; + for (_, note) in maybe_notes { + if txids.contains(¬e.tx) && note.spent.is_none() { + value += note.value.to_i64().ok_or_else(|| { + MmError::new(ZcoinStorageError::GetFromStorageError("price is too large".to_string())) + })? + } + } + + match Amount::from_i64(value) { + Ok(amount) if !amount.is_negative() => Ok(amount), + _ => MmError::err(ZcoinStorageError::CorruptedData( + "Sum of values in received_notes is out of range".to_string(), + )), + } + } + + async fn get_memo(&self, id_note: Self::NoteRef) -> Result { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + + let memo = match id_note { + NoteId::SentNoteId(id_note) => { + let sent_notes_table = db_transaction.table::().await?; + let notes = sent_notes_table.get_items("ticker", &self.ticker).await?; + notes + .into_iter() + .find(|(id, _)| *id as i64 == id_note) + .map(|(_, n)| n.memo) + }, + NoteId::ReceivedNoteId(id_note) => { + let received_notes_table = db_transaction.table::().await?; + let notes = received_notes_table.get_items("ticker", &self.ticker).await?; + notes + .into_iter() + .find(|(id, _)| *id as i64 == id_note) + .map(|(_, n)| n.memo) + }, + }; + + if let Some(Some(memo)) = memo { + return MemoBytes::from_bytes(&memo) + .and_then(Memo::try_from) + .map_to_mm(|err| ZcoinStorageError::InvalidMemo(err.to_string())); + }; + + MmError::err(ZcoinStorageError::GetFromStorageError("Memo not found".to_string())) + } + + async fn get_commitment_tree( + &self, + block_height: BlockHeight, + ) -> Result>, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let blocks_table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(WalletDbBlocksTable::TICKER_HEIGHT_INDEX) + .with_value(&self.ticker)? + .with_value(u32::from(block_height))?; + + let block = blocks_table + .get_item_by_unique_multi_index(index_keys) + .await? + .map(|(_, account)| account); + + if let Some(block) = block { + return Ok(Some( + CommitmentTree::read(&block.sapling_tree[..]) + .map_to_mm(|e| ZcoinStorageError::DecodingError(e.to_string()))?, + )); + } + + Ok(None) + } + + async fn get_witnesses( + &self, + block_height: BlockHeight, + ) -> Result)>, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + + let sapling_witness_table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(WalletDbSaplingWitnessesTable::TICKER_BLOCK_INDEX) + .with_value(&self.ticker)? + .with_value(u32::from(block_height))?; + let maybe_witnesses = sapling_witness_table.get_items_by_multi_index(index_keys).await?; + + // Retrieves a list of transaction IDs (id_tx) from the transactions table + // that match the provided account ID and have not been spent (spent IS NULL). + let mut witnesses = vec![]; + for (_, witness) in maybe_witnesses { + let id_note = witness.note.to_i64().unwrap(); + let id_note = NoteId::ReceivedNoteId(id_note.to_i64().expect("invalid value")); + let witness = IncrementalWitness::read(witness.witness.as_slice()) + .map(|witness| (id_note, witness)) + .map_to_mm(|err| ZcoinStorageError::DecodingError(err.to_string()))?; + witnesses.push(witness) + } + + Ok(witnesses) + } + + async fn get_nullifiers(&self) -> Result, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + + // Received notes + let received_notes_table = db_transaction.table::().await?; + let maybe_notes = received_notes_table.get_items("ticker", &self.ticker).await?; + + // Transactions + let txs_table = db_transaction.table::().await?; + let maybe_txs = txs_table.get_items("ticker", &self.ticker).await?; + + let mut nullifiers = vec![]; + for (_, note) in maybe_notes { + let matching_tx = maybe_txs.iter().find(|(id_tx, _tx)| id_tx.to_bigint() == note.spent); + + if let Some((_, tx)) = matching_tx { + if tx.block.is_none() { + nullifiers.push(( + AccountId( + note.account + .to_u32() + .ok_or_else(|| ZcoinStorageError::GetFromStorageError("Invalid amount".to_string()))?, + ), + Nullifier::from_slice(¬e.nf.clone().ok_or_else(|| { + ZcoinStorageError::GetFromStorageError("Error while putting tx_meta".to_string()) + })?) + .unwrap(), + )); + } + } else { + nullifiers.push(( + AccountId( + note.account + .to_u32() + .ok_or_else(|| ZcoinStorageError::GetFromStorageError("Invalid amount".to_string()))?, + ), + Nullifier::from_slice(¬e.nf.clone().ok_or_else(|| { + ZcoinStorageError::GetFromStorageError("Error while putting tx_meta".to_string()) + })?) + .unwrap(), + )); + } + } + + Ok(nullifiers) + } + + async fn get_spendable_notes( + &self, + account: AccountId, + anchor_height: BlockHeight, + ) -> Result, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + + // Received notes + let received_notes_table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(WalletDbReceivedNotesTable::TICKER_ACCOUNT_INDEX) + .with_value(&self.ticker)? + .with_value(account.0.to_bigint())?; + let maybe_notes = received_notes_table.get_items_by_multi_index(index_keys).await?; + + // Transactions + let txs_table = db_transaction.table::().await?; + let txs = txs_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("block", 0u32, u32::from(anchor_height + 1)) + .open_cursor(WalletDbTransactionsTable::TICKER_BLOCK_INDEX) + .await? + .collect() + .await? + .into_iter() + .map(|(i, item)| (i, item)) + .collect::>(); + // Witnesses + let witnesses_table = db_transaction.table::().await?; + let witnesses = witnesses_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("block", 0u32, u32::from(anchor_height + 1)) + .open_cursor(WalletDbSaplingWitnessesTable::TICKER_BLOCK_INDEX) + .await? + .collect() + .await? + .into_iter() + .map(|(_, item)| item) + .collect::>(); + + let mut spendable_notes = vec![]; + for (id_note, note) in maybe_notes { + let id_note = num_to_bigint!(id_note)?; + let witness = witnesses.iter().find(|wit| wit.note == id_note); + let tx = txs.iter().find(|(id, _tx)| *id == note.tx); + + if let (Some(witness), Some(_)) = (witness, tx) { + if note.spent.is_none() { + let spend = SpendableNoteConstructor { + diversifier: note.diversifier.clone(), + value: note.value.clone(), + rcm: note.rcm.to_owned(), + witness: witness.witness.clone(), + }; + spendable_notes.push(to_spendable_note(spend)?); + } + } + } + + Ok(spendable_notes) + } + + async fn select_spendable_notes( + &self, + account: AccountId, + target_value: Amount, + anchor_height: BlockHeight, + ) -> Result, Self::Error> { + // The goal of this SQL statement is to select the oldest notes until the required + // value has been reached, and then fetch the witnesses at the desired height for the + // selected notes. This is achieved in several steps: + // + // 1) Use a window function to create a view of all notes, ordered from oldest to + // newest, with an additional column containing a running sum: + // - Unspent notes accumulate the values of all unspent notes in that note's + // account, up to itself. + // - Spent notes accumulate the values of all notes in the transaction they were + // spent in, up to itself. + // + // 2) Select all unspent notes in the desired account, along with their running sum. + // + // 3) Select all notes for which the running sum was less than the required value, as + // well as a single note for which the sum was greater than or equal to the + // required value, bringing the sum of all selected notes across the threshold. + // + // 4) Match the selected notes against the witnesses at the desired height. + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + + // Received notes + let received_notes_table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(WalletDbReceivedNotesTable::TICKER_ACCOUNT_INDEX) + .with_value(&self.ticker)? + .with_value(account.0.to_bigint().unwrap())?; + let maybe_notes = received_notes_table.get_items_by_multi_index(index_keys).await?; + + // Transactions + let db_transaction = locked_db.get_inner().transaction().await?; + let txs_table = db_transaction.table::().await?; + let txs = txs_table + .cursor_builder() + .only("ticker", &self.ticker)? + .bound("block", 0u32, u32::from(anchor_height)) + .open_cursor(WalletDbTransactionsTable::TICKER_BLOCK_INDEX) + .await? + .collect() + .await?; + + // Sapling Witness + let db_transaction = locked_db.get_inner().transaction().await?; + let witness_table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(WalletDbSaplingWitnessesTable::TICKER_BLOCK_INDEX) + .with_value(&self.ticker)? + .with_value(u32::from(anchor_height))?; + let witnesses = witness_table.get_items_by_multi_index(index_keys).await?; + + let mut running_sum = 0; + let mut notes = vec![]; + for (id_note, note) in &maybe_notes { + let value = note.value.clone().to_i64().expect("price is too large"); + if note.spent.is_none() { + running_sum += value; + notes.push((id_note, value, note, running_sum)); + } + } + + let final_notes: Vec<_> = notes + .iter() + .filter_map(|(id_note, value, note, running_sum)| { + txs.iter() + .find(|(id_tx, _tx)| *id_tx == note.tx) + .map(|_| (id_note, value, note, running_sum)) + }) + .collect(); + let mut unspent_notes: Vec<_> = final_notes + .iter() + .filter(|(_, _, _, sum)| **sum < i64::from(target_value)) + .cloned() + .collect(); + + if let Some(note) = final_notes.iter().find(|(_, _, _, sum)| **sum >= target_value.into()) { + unspent_notes.push(*note); + }; + + // Step 4: Get witnesses for selected notes + let mut spendable_notes = Vec::new(); + for (id_note, _, note, _) in &unspent_notes { + let noteid_bigint = num_to_bigint!(id_note)?; + if let Some((_, witness)) = witnesses.iter().find(|(_, w)| w.note == noteid_bigint) { + let spendable = to_spendable_note(SpendableNoteConstructor { + diversifier: note.diversifier.clone(), + value: note.value.clone(), + rcm: note.rcm.clone(), + witness: witness.witness.clone(), + })?; + spendable_notes.push(spendable); + } + } + + Ok(spendable_notes) + } +} + +#[async_trait] +impl WalletWrite for WalletIndexedDb { + async fn advance_by_block( + &mut self, + block: &PrunedBlock, + updated_witnesses: &[(Self::NoteRef, IncrementalWitness)], + ) -> Result)>, Self::Error> { + let selfi = self.deref(); + selfi + .insert_block( + block.block_height, + block.block_hash, + block.block_time, + block.commitment_tree, + ) + .await?; + + let mut new_witnesses = vec![]; + for tx in block.transactions { + let tx_row = selfi.put_tx_meta(tx, block.block_height).await?; + + // Mark notes as spent and remove them from the scanning cache + for spend in &tx.shielded_spends { + selfi.mark_spent(tx_row, &spend.nf).await?; + } + + for output in &tx.shielded_outputs { + let received_note_id = selfi.put_received_note(output, tx_row).await?; + + // Save witness for note. + new_witnesses.push((received_note_id, output.witness.clone())); + } + } + + // Insert current new_witnesses into the database. + for (received_note_id, witness) in updated_witnesses.iter().chain(new_witnesses.iter()) { + if let NoteId::ReceivedNoteId(rnid) = *received_note_id { + selfi.insert_witness(rnid, witness, block.block_height).await?; + } else { + return MmError::err(ZcoinStorageError::InvalidNoteId); + } + } + + // Prune the stored witnesses (we only expect rollbacks of at most 100 blocks). + let below_height = if block.block_height < BlockHeight::from(100) { + BlockHeight::from(0) + } else { + block.block_height - 100 + }; + selfi.prune_witnesses(below_height).await?; + + // Update now-expired transactions that didn't get mined. + selfi.update_expired_notes(block.block_height).await?; + + Ok(new_witnesses) + } + + async fn store_received_tx(&mut self, received_tx: &ReceivedTransaction) -> Result { + let selfi = self.deref(); + let tx_ref = selfi.put_tx_data(received_tx.tx, None).await?; + + for output in received_tx.outputs { + if output.outgoing { + selfi.put_sent_note(output, tx_ref).await?; + } else { + selfi.put_received_note(output, tx_ref).await?; + } + } + + Ok(tx_ref) + } + + async fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result { + let selfi = self.deref(); + let tx_ref = selfi.put_tx_data(sent_tx.tx, Some(sent_tx.created.to_string())).await?; + + // Mark notes as spent. + for spend in &sent_tx.tx.shielded_spends { + selfi.mark_spent(tx_ref, &spend.nullifier).await?; + } + + selfi + .insert_sent_note( + tx_ref, + sent_tx.output_index, + sent_tx.account, + sent_tx.recipient_address, + sent_tx.value, + sent_tx.memo.as_ref(), + ) + .await?; + + // Return the row number of the transaction, so the caller can fetch it for sending. + Ok(tx_ref) + } + + async fn rewind_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> { + let selfi = self.deref(); + selfi.rewind_to_height(block_height).await + } +} + +#[derive(Clone)] +pub struct DataConnStmtCacheWasm(pub WalletIndexedDb); + +#[async_trait] +impl WalletRead for DataConnStmtCacheWasm { + type Error = MmError; + type NoteRef = NoteId; + type TxRef = i64; + + async fn block_height_extrema(&self) -> Result, Self::Error> { + self.0.block_height_extrema().await + } + + async fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { + self.0.get_block_hash(block_height).await + } + + async fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { + self.0.get_tx_height(txid).await + } + + async fn get_address(&self, account: AccountId) -> Result, Self::Error> { + self.0.get_address(account).await + } + + async fn get_extended_full_viewing_keys(&self) -> Result, Self::Error> { + self.0.get_extended_full_viewing_keys().await + } + + async fn is_valid_account_extfvk( + &self, + account: AccountId, + extfvk: &ExtendedFullViewingKey, + ) -> Result { + self.0.is_valid_account_extfvk(account, extfvk).await + } + + async fn get_balance_at(&self, account: AccountId, anchor_height: BlockHeight) -> Result { + self.0.get_balance_at(account, anchor_height).await + } + + async fn get_memo(&self, id_note: Self::NoteRef) -> Result { self.0.get_memo(id_note).await } + + async fn get_commitment_tree( + &self, + block_height: BlockHeight, + ) -> Result>, Self::Error> { + self.0.get_commitment_tree(block_height).await + } + + async fn get_witnesses( + &self, + block_height: BlockHeight, + ) -> Result)>, Self::Error> { + self.0.get_witnesses(block_height).await + } + + async fn get_nullifiers(&self) -> Result, Self::Error> { self.0.get_nullifiers().await } + + async fn get_spendable_notes( + &self, + account: AccountId, + anchor_height: BlockHeight, + ) -> Result, Self::Error> { + self.0.get_spendable_notes(account, anchor_height).await + } + + async fn select_spendable_notes( + &self, + account: AccountId, + target_value: Amount, + anchor_height: BlockHeight, + ) -> Result, Self::Error> { + self.0 + .select_spendable_notes(account, target_value, anchor_height) + .await + } +} + +#[async_trait] +impl WalletWrite for DataConnStmtCacheWasm { + async fn advance_by_block( + &mut self, + block: &PrunedBlock, + updated_witnesses: &[(Self::NoteRef, IncrementalWitness)], + ) -> Result)>, Self::Error> { + self.0.advance_by_block(block, updated_witnesses).await + } + + async fn store_received_tx(&mut self, received_tx: &ReceivedTransaction) -> Result { + self.0.store_received_tx(received_tx).await + } + + async fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result { + self.0.store_sent_tx(sent_tx).await + } + + async fn rewind_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> { + self.0.rewind_to_height(block_height).await + } +} diff --git a/mm2src/coins/z_coin/storage/walletdb/wallet_idb.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/tables.rs similarity index 50% rename from mm2src/coins/z_coin/storage/walletdb/wallet_idb.rs rename to mm2src/coins/z_coin/storage/walletdb/wasm/tables.rs index e3abf591ce..53471571d4 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wallet_idb.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/tables.rs @@ -1,15 +1,12 @@ -use async_trait::async_trait; -use mm2_db::indexed_db::{BeBigUint, DbIdentifier, DbInstance, DbUpgrader, IndexedDb, IndexedDbBuilder, InitDbResult, - OnUpgradeResult, TableSignature}; - -const DB_VERSION: u32 = 1; +use mm2_db::indexed_db::{DbUpgrader, OnUpgradeResult, TableSignature}; +use mm2_number::BigInt; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct WalletDbAccountsTable { - account: BeBigUint, - extfvk: String, - address: String, - ticker: String, + pub account: BigInt, + pub extfvk: String, + pub address: String, + pub ticker: String, } impl WalletDbAccountsTable { @@ -17,15 +14,25 @@ impl WalletDbAccountsTable { /// * ticker /// * account pub const TICKER_ACCOUNT_INDEX: &str = "ticker_account_index"; + /// A **unique** index that consists of the following properties: + /// * ticker + /// * account + /// * extfvk + pub const TICKER_ACCOUNT_EXTFVK_INDEX: &str = "ticker_account_extfvk_index"; } impl TableSignature for WalletDbAccountsTable { - fn table_name() -> &'static str { "walletdb_accounts" } + const TABLE_NAME: &'static str = "walletdb_accounts"; 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())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index(Self::TICKER_ACCOUNT_INDEX, &["ticker", "account"], true)?; + table.create_multi_index( + Self::TICKER_ACCOUNT_EXTFVK_INDEX, + &["ticker", "account", "extfvk"], + false, + )?; table.create_index("ticker", false)?; } Ok(()) @@ -34,30 +41,30 @@ impl TableSignature for WalletDbAccountsTable { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct WalletDbBlocksTable { - height: BeBigUint, - hash: String, - time: BeBigUint, - sapling_tree: String, - ticker: String, + pub height: u32, + pub hash: Vec, + pub time: u32, + pub sapling_tree: Vec, + pub ticker: String, } impl WalletDbBlocksTable { /// A **unique** index that consists of the following properties: /// * ticker /// * height - pub const TICKER_HEIGHT_INDEX: &str = "ticker_height_index"; + pub const TICKER_HEIGHT_INDEX: &'static str = "ticker_height_index"; /// A **unique** index that consists of the following properties: /// * ticker /// * hash - pub const TICKER_HASH_INDEX: &str = "ticker_hash_index"; + pub const TICKER_HASH_INDEX: &'static str = "ticker_hash_index"; } impl TableSignature for WalletDbBlocksTable { - fn table_name() -> &'static str { "walletdb_blocks" } + const TABLE_NAME: &'static str = "walletdb_blocks"; 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())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index(Self::TICKER_HEIGHT_INDEX, &["ticker", "height"], true)?; table.create_multi_index(Self::TICKER_HASH_INDEX, &["ticker", "hash"], true)?; table.create_index("ticker", false)?; @@ -68,31 +75,41 @@ impl TableSignature for WalletDbBlocksTable { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct WalletDbTransactionsTable { - id_tx: BeBigUint, - txid: String, // unique - created: String, - block: BeBigUint, - tx_index: BeBigUint, - expiry_height: BeBigUint, - raw: String, - ticker: String, + /// Unique field + pub txid: Vec, + pub created: Option, + pub block: Option, + pub tx_index: Option, + pub expiry_height: Option, + pub raw: Option>, + pub ticker: String, } impl WalletDbTransactionsTable { /// A **unique** index that consists of the following properties: /// * ticker - /// * id_tx /// * txid - pub const TICKER_ID_TX_INDEX: &'static str = "ticker_id_tx_index"; + pub const TICKER_TXID_INDEX: &'static str = "ticker_txid_index"; + /// A **unique** index that consists of the following properties: + /// * ticker + /// * block + pub const TICKER_BLOCK_INDEX: &'static str = "ticker_block_index"; + /// A **unique** index that consists of the following properties: + /// * ticker + /// * expiry_height + pub const TICKER_EXP_HEIGHT_INDEX: &'static str = "ticker_expiry_height_index"; } impl TableSignature for WalletDbTransactionsTable { - fn table_name() -> &'static str { "walletdb_transactions" } + const TABLE_NAME: &'static str = "walletdb_transactions"; 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::TICKER_ID_TX_INDEX, &["ticker", "id_tx", "txid"], true)?; + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_multi_index(Self::TICKER_TXID_INDEX, &["ticker", "txid"], true)?; + table.create_multi_index(Self::TICKER_BLOCK_INDEX, &["ticker", "block"], false)?; + table.create_multi_index(Self::TICKER_EXP_HEIGHT_INDEX, &["ticker", "expiry_height"], false)?; + table.create_index("ticker", false)?; } Ok(()) } @@ -100,21 +117,28 @@ impl TableSignature for WalletDbTransactionsTable { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct WalletDbReceivedNotesTable { - id_note: BeBigUint, - tx: BeBigUint, - output_index: BeBigUint, - account: BeBigUint, - diversifier: String, - value: BeBigUint, - rcm: String, - nf: String, // unique - is_change: BeBigUint, - memo: String, - spent: BeBigUint, - ticker: String, + /// references transactions(id_tx) + pub tx: u32, + pub output_index: u32, + /// references accounts(account) + pub account: BigInt, + pub diversifier: Vec, + pub value: BigInt, + pub rcm: Vec, + /// Unique field + pub nf: Option>, + pub is_change: Option, + pub memo: Option>, + /// references transactions(id_tx) + pub spent: Option, + pub ticker: String, } impl WalletDbReceivedNotesTable { + /// A **unique** index that consists of the following properties: + /// * ticker + /// * account + pub const TICKER_ACCOUNT_INDEX: &'static str = "ticker_account_index"; /// A **unique** index that consists of the following properties: /// * ticker /// * note_id @@ -124,21 +148,23 @@ impl WalletDbReceivedNotesTable { /// * ticker /// * tx /// * output_index - pub const TICKER_NOTES_TX_OUTPUT_INDEX: &'static str = "ticker_notes_tx_output_index"; + pub const TICKER_TX_OUTPUT_INDEX: &'static str = "ticker_tx_output_index"; + /// A **unique** index that consists of the following properties: + /// * ticker + /// * tx + /// * output_index + pub const TICKER_NF_INDEX: &'static str = "ticker_nf_index"; } impl TableSignature for WalletDbReceivedNotesTable { - fn table_name() -> &'static str { "walletdb_received_notes" } + const TABLE_NAME: &'static str = "walletdb_received_notes"; 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::TICKER_NOTES_ID_NF_INDEX, &["ticker", "id_note", "nf"], true)?; - table.create_multi_index( - Self::TICKER_NOTES_TX_OUTPUT_INDEX, - &["ticker", "tx", "output_index"], - true, - )?; + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_multi_index(Self::TICKER_NF_INDEX, &["ticker", "nf"], true)?; + table.create_multi_index(Self::TICKER_ACCOUNT_INDEX, &["ticker", "account"], false)?; + table.create_multi_index(Self::TICKER_TX_OUTPUT_INDEX, &["ticker", "tx", "output_index"], false)?; table.create_index("ticker", false)?; } Ok(()) @@ -147,33 +173,30 @@ impl TableSignature for WalletDbReceivedNotesTable { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct WalletDbSaplingWitnessesTable { - id_witness: BeBigUint, - note: BeBigUint, - block: BeBigUint, - witness: String, - ticker: String, + /// REFERENCES received_notes(id_note) + pub note: BigInt, + /// REFERENCES blocks(height) + pub block: u32, + pub witness: Vec, + pub ticker: String, } impl WalletDbSaplingWitnessesTable { /// A **unique** index that consists of the following properties: /// * ticker - /// * note /// * block + pub const TICKER_BLOCK_INDEX: &'static str = "ticker_block_index"; pub const TICKER_NOTE_BLOCK_INDEX: &'static str = "ticker_note_block_index"; - /// A **unique** index that consists of the following properties: - /// * ticker - /// * id_witness - pub const TICKER_ID_WITNESS_INDEX: &'static str = "ticker_id_witness_index"; } impl TableSignature for WalletDbSaplingWitnessesTable { - fn table_name() -> &'static str { "walletdb_sapling_witness" } + const TABLE_NAME: &'static str = "walletdb_sapling_witness"; 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())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index(Self::TICKER_NOTE_BLOCK_INDEX, &["ticker", "note", "block"], true)?; - table.create_multi_index(Self::TICKER_ID_WITNESS_INDEX, &["ticker", "id_witness"], true)?; + table.create_multi_index(Self::TICKER_BLOCK_INDEX, &["ticker", "block"], false)?; table.create_index("ticker", false)?; } Ok(()) @@ -182,14 +205,15 @@ impl TableSignature for WalletDbSaplingWitnessesTable { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct WalletDbSentNotesTable { - id_note: BeBigUint, - tx: BeBigUint, - output_index: BeBigUint, - from_account: BeBigUint, - address: String, - value: BeBigUint, - memo: String, - ticker: String, + /// REFERENCES transactions(id_tx) + pub tx: BigInt, + pub output_index: BigInt, + /// REFERENCES accounts(account) + pub from_account: BigInt, + pub address: String, + pub value: BigInt, + pub memo: Option>, + pub ticker: String, } impl WalletDbSentNotesTable { @@ -201,42 +225,14 @@ impl WalletDbSentNotesTable { } impl TableSignature for WalletDbSentNotesTable { - fn table_name() -> &'static str { "walletdb_sent_notes" } + const TABLE_NAME: &'static str = "walletdb_sent_notes"; 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::TICKER_TX_OUTPUT_INDEX, &["ticker", "tx", "output_index"], true)?; + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_multi_index(Self::TICKER_TX_OUTPUT_INDEX, &["ticker", "tx", "output_index"], false)?; table.create_index("ticker", false)?; } Ok(()) } } - -pub struct WalletDbInner { - pub inner: IndexedDb, -} - -impl WalletDbInner { - pub fn _get_inner(&self) -> &IndexedDb { &self.inner } -} - -#[async_trait] -impl DbInstance for WalletDbInner { - const DB_NAME: &'static str = "wallet_db_cache"; - - async fn init(db_id: DbIdentifier) -> InitDbResult { - let inner = IndexedDbBuilder::new(db_id) - .with_version(DB_VERSION) - .with_table::() - .with_table::() - .with_table::() - .with_table::() - .with_table::() - .with_table::() - .build() - .await?; - - Ok(Self { inner }) - } -} diff --git a/mm2src/coins/z_coin/z_coin_errors.rs b/mm2src/coins/z_coin/z_coin_errors.rs index a708ad1013..f556435f57 100644 --- a/mm2src/coins/z_coin/z_coin_errors.rs +++ b/mm2src/coins/z_coin/z_coin_errors.rs @@ -1,7 +1,6 @@ use crate::my_tx_history_v2::MyTxHistoryErrorV2; use crate::utxo::rpc_clients::UtxoRpcError; use crate::utxo::utxo_builder::UtxoCoinBuildError; -use crate::z_coin::storage::WalletDbError; use crate::NumConversError; use crate::PrivKeyPolicyNotAllowed; use crate::WithdrawError; @@ -11,17 +10,24 @@ use common::jsonrpc_client::JsonRpcError; use db_common::sqlite::rusqlite::Error as SqliteError; use derive_more::Display; use http::uri::InvalidUri; +#[cfg(target_arch = "wasm32")] +use mm2_db::indexed_db::cursor_prelude::*; +#[cfg(target_arch = "wasm32")] +use mm2_db::indexed_db::{DbTransactionError, InitDbError}; +use mm2_err_handle::mm_error::MmError; use mm2_number::BigDecimal; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; +use zcash_client_backend::data_api::error::ChainInvalid; #[cfg(not(target_arch = "wasm32"))] use zcash_client_sqlite::error::SqliteClientError; +#[cfg(target_arch = "wasm32")] use zcash_extras::NoteId; +use zcash_primitives::consensus::BlockHeight; use zcash_primitives::transaction::builder::Error as ZTxBuilderError; /// Represents possible errors that might occur while interacting with Zcoin rpc. #[derive(Debug, Display)] #[non_exhaustive] pub enum UpdateBlocksCacheErr { - #[cfg(not(target_arch = "wasm32"))] GrpcError(tonic::Status), UtxoRpcError(UtxoRpcError), InternalError(String), @@ -31,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) } } @@ -61,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), @@ -69,13 +78,17 @@ pub enum ZcoinClientInitError { UtxoCoinBuildError(UtxoCoinBuildError), } +impl From for ZcoinClientInitError { + fn from(err: ZcoinStorageError) -> Self { ZcoinClientInitError::ZcoinStorageError(err.to_string()) } +} + impl From for ZcoinClientInitError { fn from(err: UpdateBlocksCacheErr) -> Self { ZcoinClientInitError::UpdateBlocksCacheErr(err) } } #[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)] @@ -116,6 +129,8 @@ pub enum GenTxError { LightClientErr(String), FailedToCreateNote, SpendableNotesError(String), + #[cfg(target_arch = "wasm32")] + Internal(String), } impl From for GenTxError { @@ -163,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()), } } } @@ -229,6 +246,7 @@ pub enum ZCoinBuildError { Io(std::io::Error), RpcClientInitErr(ZcoinClientInitError), ZCashParamsNotFound, + ZCashParamsError(String), ZDerivationPathNotSet, SaplingParamsInvalidChecksum, } @@ -291,4 +309,192 @@ 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)] +pub enum ValidateBlocksError { + #[display(fmt = "Chain Invalid occurred at height: {height:?} — with error {err:?}")] + ChainInvalid { + height: BlockHeight, + err: ChainInvalid, + }, + GetFromStorageError(String), + IoError(String), + DbError(String), + DecodingError(String), + TableNotEmpty(String), + InvalidNote(String), + InvalidNoteId, + IncorrectHrpExtFvk(String), + CorruptedData(String), + InvalidMemo(String), + BackendError(String), + ZcoinStorageError(String), +} + +impl From for ZcoinStorageError { + fn from(value: ValidateBlocksError) -> Self { Self::ValidateBlocksError(value) } +} +impl From> for ValidateBlocksError { + fn from(value: MmError) -> Self { Self::ZcoinStorageError(value.to_string()) } +} + +impl ValidateBlocksError { + /// The hash of the parent block given by a proposed new chain tip does not match the hash of the current chain tip. + pub fn prev_hash_mismatch(height: BlockHeight) -> ValidateBlocksError { + ValidateBlocksError::ChainInvalid { + height, + err: ChainInvalid::PrevHashMismatch, + } + } + + /// The block height field of the proposed new chain tip is not equal to the height of the previous chain tip + 1. + /// This variant stores a copy of the incorrect height value for reporting purposes. + pub fn block_height_discontinuity(height: BlockHeight, found: BlockHeight) -> ValidateBlocksError { + ValidateBlocksError::ChainInvalid { + height, + err: ChainInvalid::BlockHeightDiscontinuity(found), + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for ValidateBlocksError { + fn from(value: SqliteClientError) -> Self { + match value { + SqliteClientError::CorruptedData(err) => Self::CorruptedData(err), + SqliteClientError::IncorrectHrpExtFvk => Self::IncorrectHrpExtFvk(value.to_string()), + SqliteClientError::InvalidNote => Self::InvalidNote(value.to_string()), + SqliteClientError::InvalidNoteId => Self::InvalidNoteId, + SqliteClientError::TableNotEmpty => Self::TableNotEmpty(value.to_string()), + SqliteClientError::Bech32(_) | SqliteClientError::Base58(_) => Self::DecodingError(value.to_string()), + SqliteClientError::DbError(err) => Self::DbError(err.to_string()), + SqliteClientError::Io(err) => Self::IoError(err.to_string()), + SqliteClientError::InvalidMemo(err) => Self::InvalidMemo(err.to_string()), + SqliteClientError::BackendError(err) => Self::BackendError(err.to_string()), + } + } +} + +/// The `ZcoinStorageError` enum encapsulates different types of errors that may occur +/// when interacting with storage operations specific to the Zcoin blockchain. +#[derive(Debug, Display)] +pub enum ZcoinStorageError { + #[cfg(not(target_arch = "wasm32"))] + SqliteError(SqliteClientError), + ValidateBlocksError(ValidateBlocksError), + #[display(fmt = "Chain Invalid occurred at height: {height:?} — with error {err:?}")] + ChainInvalid { + height: BlockHeight, + err: ChainInvalid, + }, + IoError(String), + DbError(String), + DecodingError(String), + TableNotEmpty(String), + InvalidNote(String), + InvalidNoteId, + #[display(fmt = "Incorrect Hrp extended full viewing key")] + IncorrectHrpExtFvk, + CorruptedData(String), + InvalidMemo(String), + BackendError(String), + #[display(fmt = "Add to storage err: {}", _0)] + AddToStorageErr(String), + #[display(fmt = "Remove from storage err: {}", _0)] + RemoveFromStorageErr(String), + #[display(fmt = "Get from storage err: {}", _0)] + GetFromStorageError(String), + #[display(fmt = "Error getting {ticker} block height from storage: {err}")] + BlockHeightNotFound { + ticker: String, + err: String, + }, + #[display(fmt = "Storage Initialization err: {err} - ticker: {ticker}")] + InitDbError { + ticker: String, + err: String, + }, + 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")] +impl From> for ZcoinStorageError { + fn from(value: zcash_client_backend::data_api::error::Error) -> Self { + Self::BackendError(value.to_string()) + } +} + +#[cfg(target_arch = "wasm32")] +impl From for ZcoinStorageError { + fn from(e: InitDbError) -> Self { + match &e { + InitDbError::NotSupported(_) => ZcoinStorageError::NotSupported(e.to_string()), + InitDbError::EmptyTableList + | InitDbError::DbIsOpenAlready { .. } + | InitDbError::InvalidVersion(_) + | InitDbError::OpeningError(_) + | InitDbError::TypeMismatch { .. } + | InitDbError::UnexpectedState(_) + | InitDbError::UpgradingError { .. } => ZcoinStorageError::InternalError(e.to_string()), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl From for ZcoinStorageError { + fn from(e: DbTransactionError) -> Self { + match e { + DbTransactionError::ErrorSerializingItem(_) | DbTransactionError::ErrorDeserializingItem(_) => { + ZcoinStorageError::DecodingError(e.to_string()) + }, + DbTransactionError::ErrorUploadingItem(_) => ZcoinStorageError::AddToStorageErr(e.to_string()), + DbTransactionError::ErrorGettingItems(_) | DbTransactionError::ErrorCountingItems(_) => { + ZcoinStorageError::GetFromStorageError(e.to_string()) + }, + DbTransactionError::ErrorDeletingItems(_) => ZcoinStorageError::RemoveFromStorageErr(e.to_string()), + DbTransactionError::NoSuchTable { .. } + | DbTransactionError::ErrorCreatingTransaction(_) + | DbTransactionError::ErrorOpeningTable { .. } + | DbTransactionError::ErrorSerializingIndex { .. } + | DbTransactionError::UnexpectedState(_) + | DbTransactionError::TransactionAborted + | DbTransactionError::MultipleItemsByUniqueIndex { .. } + | DbTransactionError::NoSuchIndex { .. } + | DbTransactionError::InvalidIndex { .. } => ZcoinStorageError::InternalError(e.to_string()), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl From for ZcoinStorageError { + fn from(value: CursorError) -> Self { + match value { + CursorError::ErrorSerializingIndexFieldValue { .. } + | CursorError::ErrorDeserializingIndexValue { .. } + | CursorError::ErrorDeserializingItem(_) => Self::DecodingError(value.to_string()), + CursorError::ErrorOpeningCursor { .. } + | CursorError::AdvanceError { .. } + | CursorError::InvalidKeyRange { .. } + | CursorError::IncorrectNumberOfKeysPerIndex { .. } + | CursorError::UnexpectedState(_) + | CursorError::IncorrectUsage { .. } + | CursorError::TypeMismatch { .. } => Self::DbError(value.to_string()), + } + } +} diff --git a/mm2src/coins/z_coin/z_htlc.rs b/mm2src/coins/z_coin/z_htlc.rs index 6690977bba..0f7d9fba6f 100644 --- a/mm2src/coins/z_coin/z_htlc.rs +++ b/mm2src/coins/z_coin/z_htlc.rs @@ -5,7 +5,7 @@ // taker payment spend - https://zombie.explorer.lordofthechains.com/tx/af6bb0f99f9a5a070a0c1f53d69e4189b0e9b68f9d66e69f201a6b6d9f93897e // maker payment spend - https://rick.explorer.dexstats.info/tx/6a2dcc866ad75cebecb780a02320073a88bcf5e57ddccbe2657494e7747d591e -use super::ZCoin; +use super::{GenTxError, ZCoin}; use crate::utxo::rpc_clients::{UtxoRpcClientEnum, UtxoRpcError}; use crate::utxo::utxo_common::payment_script; use crate::utxo::{sat_from_big_decimal, UtxoAddressFormat}; @@ -21,18 +21,18 @@ use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; use script::Script; use script::{Builder as ScriptBuilder, Opcode}; +use secp256k1::SecretKey; +use zcash_primitives::consensus; use zcash_primitives::legacy::Script as ZCashScript; use zcash_primitives::memo::MemoBytes; +use zcash_primitives::transaction::builder::Builder as ZTxBuilder; use zcash_primitives::transaction::builder::Error as ZTxBuilderError; +use zcash_primitives::transaction::components::OutPoint as ZCashOutpoint; use zcash_primitives::transaction::components::{Amount, TxOut}; use zcash_primitives::transaction::Transaction as ZTransaction; cfg_native!( use common::async_blocking; - use secp256k1::SecretKey; - use zcash_primitives::consensus; - use zcash_primitives::transaction::builder::Builder as ZTxBuilder; - use zcash_primitives::transaction::components::OutPoint as ZCashOutpoint; ); /// Sends HTLC output from the coin's my_z_addr @@ -106,6 +106,7 @@ pub async fn z_send_dex_fee( #[allow(clippy::large_enum_variant, clippy::upper_case_acronyms, unused)] pub enum ZP2SHSpendError { ZTxBuilderError(ZTxBuilderError), + GenTxError(GenTxError), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), Rpc(UtxoRpcError), #[display(fmt = "{:?} {}", _0, _1)] @@ -140,7 +141,6 @@ impl ZP2SHSpendError { } /// Spends P2SH output 0 to the coin's my_z_addr -#[cfg(not(target_arch = "wasm32"))] pub async fn z_p2sh_spend( coin: &ZCoin, p2sh_tx: ZTransaction, @@ -176,11 +176,17 @@ pub async fn z_p2sh_spend( None, )?; - let (zcash_tx, _) = async_blocking({ - let prover = coin.z_fields.z_tx_prover.clone(); - move || tx_builder.build(consensus::BranchId::Sapling, prover.as_ref()) - }) - .await?; + let prover = coin.z_fields.z_tx_prover.clone(); + #[cfg(not(target_arch = "wasm32"))] + let (zcash_tx, _) = async_blocking(move || tx_builder.build(consensus::BranchId::Sapling, prover.as_ref())).await?; + + #[cfg(target_arch = "wasm32")] + let (zcash_tx, _) = + crate::z_coin::TxBuilderSpawner::request_tx_result(tx_builder, consensus::BranchId::Sapling, prover.clone()) + .await + .mm_err(ZP2SHSpendError::GenTxError)? + .tx_result + .mm_err(ZP2SHSpendError::GenTxError)?; let mut tx_buffer = Vec::with_capacity(1024); zcash_tx.write(&mut tx_buffer)?; @@ -192,16 +198,3 @@ pub async fn z_p2sh_spend( .map(|_| zcash_tx.clone()) .mm_err(|e| ZP2SHSpendError::TxRecoverable(zcash_tx.into(), e.to_string())) } - -#[cfg(target_arch = "wasm32")] -pub async fn z_p2sh_spend( - _coin: &ZCoin, - _p2sh_tx: ZTransaction, - _tx_locktime: u32, - _input_sequence: u32, - _redeem_script: Script, - _script_data: Script, - _htlc_keypair: &KeyPair, -) -> Result> { - todo!() -} 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..91a2ec51b4 --- /dev/null +++ b/mm2src/coins/z_coin/z_params/indexeddb.rs @@ -0,0 +1,182 @@ +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 previously stored. + 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); + sapling_spend_chunks.push(&sapling_spend[start..end]); + 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..d86a7181a0 --- /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 5996762444..55af1ac36b 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -1,80 +1,73 @@ -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::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, CompactBlock as TonicCompactBlock}; + cfg_native!( - use crate::{RpcCommonOps, ZTransaction}; - use crate::utxo::rpc_clients::{UtxoRpcClientOps, NO_TX_ERROR_CODE}; - use crate::utxo::utxo_builder::{UtxoCoinBuilderCommonOps, DAY_IN_SECONDS}; - use crate::z_coin::storage::BlockDbError; - use crate::z_coin::CheckPointBlockInfo; - - use db_common::sqlite::rusqlite::Connection; - use db_common::sqlite::{query_single_row, run_optimization_pragmas}; - use common::{async_blocking, now_sec}; - use common::executor::Timer; - use common::log::{debug, error, info, LogOnError}; - use common::Future01CompatExt; - use futures::channel::mpsc::channel; - use group::GroupEncoding; - use hex::{FromHex, FromHexError}; + use crate::ZTransaction; + use crate::utxo::rpc_clients::{UtxoRpcClientOps}; + use crate::z_coin::z_coin_errors::{ZcoinStorageError, ValidateBlocksError}; + use crate::utxo::rpc_clients::NativeClient; + + use futures::compat::Future01CompatExt; use http::Uri; - use prost::Message; - use rpc::v1::types::{Bytes, H256 as H256Json}; - use std::path::PathBuf; - use std::pin::Pin; - use std::str::FromStr; - use std::time::Duration; - use tokio::task::block_in_place; + use group::GroupEncoding; + use std::convert::TryInto; + use std::num::TryFromIntError; use tonic::transport::{Channel, ClientTlsConfig}; - use zcash_client_backend::data_api::{WalletRead, WalletWrite}; - use zcash_client_backend::data_api::chain::{scan_cached_blocks, validate_chain}; - use zcash_client_backend::data_api::error::Error as ChainError; - use zcash_primitives::block::BlockHash; - use zcash_primitives::zip32::ExtendedFullViewingKey; - use zcash_client_sqlite::error::SqliteClientError as ZcashClientError; - use zcash_client_sqlite::wallet::init::{init_accounts_table, init_blocks_table, init_wallet_db}; - use zcash_client_sqlite::WalletDb; - - 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 tonic::codegen::StdError; + + use z_coin_grpc::{CompactOutput as TonicCompactOutput, CompactSpend as TonicCompactSpend, CompactTx as TonicCompactTx}; ); -#[cfg(not(target_arch = "wasm32"))] -pub type OnCompactBlockFn<'a> = dyn FnMut(TonicCompactBlock) -> Result<(), MmError> + Send + 'a; +cfg_wasm32!( + use futures_util::future::try_join_all; + use mm2_net::wasm::tonic_client::TonicClient; -#[cfg(target_arch = "wasm32")] -#[allow(unused)] -pub type OnCompactBlockFn<'a> = dyn FnMut(String) -> Result<(), MmError> + Send + 'a; + const MAX_CHUNK_SIZE: u64 = 20000; +); /// ZRpcOps trait provides asynchronous methods for performing various operations related to /// Zcoin blockchain and wallet synchronization. #[async_trait] -pub trait ZRpcOps { +pub trait ZRpcOps: Send + Sync + 'static { /// Asynchronously retrieve the current block height from the Zcoin network. - async fn get_block_height(&mut self) -> Result>; + async fn get_block_height(&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>; + async fn get_tree_state(&self, height: u64) -> Result>; /// Asynchronously scan and process blocks within a specified block height range. /// @@ -82,43 +75,113 @@ pub trait ZRpcOps { /// and including `last_block`. It invokes the provided `on_block` function for each compact /// block within the specified range. async fn scan_blocks( - &mut self, + &self, start_block: u64, last_block: u64, - on_block: &mut OnCompactBlockFn, + db: &BlockDbImpl, + handler: &mut SaplingSyncLoopHandle, ) -> Result<(), MmError>; - async fn check_tx_existence(&mut self, tx_id: TxId) -> bool; + async fn check_tx_existence(&self, tx_id: TxId) -> bool; /// Retrieves checkpoint block information from the database at a specific height. /// /// 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, + &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; + +#[derive(Clone)] +pub struct LightRpcClient(pub(crate) Arc>>>); + +impl LightRpcClient { + pub async fn new(lightwalletd_urls: Vec) -> Result> { + let mut rpc_clients = Vec::new(); + if lightwalletd_urls.is_empty() { + return MmError::err(ZcoinClientInitError::EmptyLightwalletdUris); + } + + #[cfg(not(target_arch = "wasm32"))] + 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 Self::connect_endpoint(endpoint).await { + Ok(tonic_channel) => tonic_channel.accept_gzip(), + Err(err) => { + errors.push(UrlIterError::ConnectionFailure(err)); + continue; + }, + }; + + cfg_wasm32!( + let client = CompactTxStreamerClient::new(TonicClient::new(url.to_string())).accept_gzip(); + ); + + 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 + #[cfg(not(target_arch = "wasm32"))] + if rpc_clients.is_empty() { + return MmError::err(ZcoinClientInitError::UrlIterFailure(errors)); + } + + Ok(LightRpcClient(AsyncMutex::new(rpc_clients).into())) + } + + /// 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: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(CompactTxStreamerClient::new(conn)) + } } -#[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 { - let mut clients = self.rpc_clients.lock().await; + let mut clients = self.0.lock().await; for (i, mut client) in clients.clone().into_iter().enumerate() { let request = tonic::Request::new(ChainSpec {}); // use get_latest_block method as a health check - if client.get_latest_block(request).await.is_ok() { + let latest = client.get_latest_block(request).await; + if latest.is_ok() { clients.rotate_left(i); return Ok(client); } @@ -129,10 +192,27 @@ impl RpcCommonOps for LightRpcClient { } } -#[cfg(not(target_arch = "wasm32"))] +async fn handle_block_cache_update( + db: &BlockDbImpl, + handler: &mut SaplingSyncLoopHandle, + block: Result, + last_block: u64, +) -> Result<(), MmError> { + let block = block.map_err(|_| UpdateBlocksCacheErr::DecodeError("Error getting block".to_string()))?; + 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()) + .await + .map_err(|err| UpdateBlocksCacheErr::ZcashDBError(err.to_string()))?; + + handler.notify_blocks_cache_status(block.height, last_block); + Ok(()) +} + #[async_trait] impl ZRpcOps for LightRpcClient { - async fn get_block_height(&mut self) -> Result> { + async fn get_block_height(&self) -> Result> { let request = tonic::Request::new(ChainSpec {}); let block = self .get_live_client() @@ -145,8 +225,7 @@ impl ZRpcOps for LightRpcClient { Ok(block.height) } - #[cfg(not(target_arch = "wasm32"))] - async fn get_tree_state(&mut self, height: u64) -> Result> { + async fn get_tree_state(&self, height: u64) -> Result> { let request = tonic::Request::new(BlockId { height, hash: vec![] }); Ok(self @@ -158,12 +237,72 @@ impl ZRpcOps for LightRpcClient { .into_inner()) } + #[cfg(target_arch = "wasm32")] async fn scan_blocks( - &mut self, + &self, start_block: u64, last_block: u64, - on_block: &mut OnCompactBlockFn, + db: &BlockDbImpl, + handler: &mut SaplingSyncLoopHandle, ) -> Result<(), MmError> { + let mut requests = Vec::new(); + let mut current_start = start_block; + let selfi = self.get_live_client().await?; + + /// Wraps the client (`selfi`) in an `Arc` to enable safe cloning and sharing across futures. + /// This is necessary to avoid the error "cannot return reference to local variable `selfi`." + async fn get_block_range_wrapper( + mut selfi: CompactTxStreamerClient, + request: BlockRange, + ) -> Result>, tonic::Status> { + selfi.get_block_range(tonic::Request::new(request)).await + } + + // Generate multiple gRPC requests to fetch block ranges efficiently. + // It takes the starting block height and the last block height as input and constructs requests for fetching + // consecutive block ranges within the specified limits. + while current_start <= last_block { + let current_end = if current_start + MAX_CHUNK_SIZE - 1 <= last_block { + current_start + MAX_CHUNK_SIZE - 1 + } else { + last_block + }; + + let block_range = BlockRange { + start: Some(BlockId { + height: current_start, + hash: Vec::new(), + }), + end: Some(BlockId { + height: current_end, + hash: Vec::new(), + }), + }; + + requests.push(get_block_range_wrapper(selfi.clone(), block_range)); + current_start = current_end + 1; + } + + let responses = try_join_all(requests).await?; + for response in responses { + let mut response = response.into_inner(); + while let Some(block) = response.next().await { + handle_block_cache_update(db, handler, block, last_block).await?; + } + } + + Ok(()) + } + + #[cfg(not(target_arch = "wasm32"))] + async fn scan_blocks( + &self, + start_block: u64, + last_block: u64, + db: &BlockDbImpl, + handler: &mut SaplingSyncLoopHandle, + ) -> Result<(), MmError> { + let mut selfi = self.get_live_client().await?; let request = tonic::Request::new(BlockRange { start: Some(BlockId { height: start_block, @@ -174,22 +313,19 @@ impl ZRpcOps for LightRpcClient { hash: Vec::new(), }), }); - let mut response = self - .get_live_client() - .await? + let mut response = selfi .get_block_range(request) .await .map_to_mm(UpdateBlocksCacheErr::GrpcError)? .into_inner(); - // without Pin method get_mut is not found in current scope - while let Some(block) = Pin::new(&mut response).get_mut().message().await? { - debug!("Got block {:?}", block); - on_block(block)?; + while let Some(block) = response.next().await { + handle_block_cache_update(db, handler, block, last_block).await?; } + Ok(()) } - async fn check_tx_existence(&mut self, tx_id: TxId) -> bool { + async fn check_tx_existence(&self, tx_id: TxId) -> bool { let mut attempts = 0; loop { if let Ok(mut client) = self.get_live_client().await { @@ -216,10 +352,10 @@ impl ZRpcOps for LightRpcClient { true } - #[cfg(not(target_arch = "wasm32"))] async fn checkpoint_block_from_height( - &mut self, + &self, height: u64, + ticker: &str, ) -> MmResult, UpdateBlocksCacheErr> { let tree_state = self.get_tree_state(height).await?; let hash = H256Json::from_str(&tree_state.hash) @@ -230,6 +366,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, @@ -242,18 +379,18 @@ impl ZRpcOps for LightRpcClient { #[cfg(not(target_arch = "wasm32"))] #[async_trait] impl ZRpcOps for NativeClient { - async fn get_block_height(&mut self) -> Result> { + async fn get_block_height(&self) -> Result> { 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 get_tree_state(&self, _height: u64) -> Result> { todo!() } async fn scan_blocks( - &mut self, + &self, start_block: u64, last_block: u64, - on_block: &mut OnCompactBlockFn, + db: &BlockDbImpl, + handler: &mut SaplingSyncLoopHandle, ) -> Result<(), MmError> { for height in start_block..=last_block { let block = self.get_block_by_height(height).await?; @@ -321,12 +458,20 @@ impl ZRpcOps for NativeClient { header: Vec::new(), vtx: compact_txs, }; - on_block(compact_block)?; + let height: u32 = compact_block + .height + .try_into() + .map_to_mm(|err: TryFromIntError| UpdateBlocksCacheErr::DecodeError(err.to_string()))?; + db.insert_block(height, compact_block.encode_to_vec()) + .await + .map_err(|err| UpdateBlocksCacheErr::ZcashDBError(err.to_string()))?; + handler.notify_blocks_cache_status(compact_block.height, last_block); } + Ok(()) } - async fn check_tx_existence(&mut self, tx_id: TxId) -> bool { + async fn check_tx_existence(&self, tx_id: TxId) -> bool { let mut attempts = 0; loop { match self.get_raw_transaction_bytes(&H256Json::from(tx_id.0)).compat().await { @@ -346,135 +491,41 @@ impl ZRpcOps for NativeClient { true } - #[cfg(not(target_arch = "wasm32"))] async fn checkpoint_block_from_height( - &mut self, + &self, _height: u64, + _ticker: &str, ) -> MmResult, UpdateBlocksCacheErr> { todo!() } } -/// `create_wallet_db` is responsible for creating a new Zcoin wallet database, initializing it -/// with the provided parameters, and executing various initialization steps. These steps include checking and -/// potentially rewinding the database to a specified synchronization height, performing optimizations, and -/// setting up the initial state of the wallet database. -#[cfg(not(target_arch = "wasm32"))] -pub async fn create_wallet_db( - wallet_db_path: PathBuf, - consensus_params: ZcoinConsensusParams, - checkpoint_block: Option, - evk: ExtendedFullViewingKey, - continue_from_prev_sync: bool, -) -> Result, MmError> { - async_blocking({ - move || -> Result, MmError> { - let db = WalletDb::for_path(wallet_db_path, consensus_params) - .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; - let extrema = db.block_height_extrema()?; - let min_sync_height = extrema.map(|(min, _)| u32::from(min)); - let init_block_height = checkpoint_block.clone().map(|block| block.height); - - run_optimization_pragmas(db.sql_conn()) - .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; - init_wallet_db(&db).map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; - - // Check if the initial block height is less than the previous synchronization height and - // Rewind walletdb to the minimum possible height. - if db.get_extended_full_viewing_keys()?.is_empty() - || (!continue_from_prev_sync && init_block_height != min_sync_height) - { - // let user know we're clearing cache and resyncing from new provided height. - if min_sync_height.unwrap_or(0) > 0 { - info!("Older/Newer sync height detected!, rewinding walletdb to new height: {init_block_height:?}"); - } - - let mut wallet_ops = db.get_update_ops().expect("get_update_ops always returns Ok"); - wallet_ops - .rewind_to_height(u32::MIN.into()) - .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; - if let Some(block) = checkpoint_block.clone() { - init_blocks_table( - &db, - BlockHeight::from_u32(block.height), - BlockHash(block.hash.0), - block.time, - &block.sapling_tree.0, - )?; - } - } - - if db.get_extended_full_viewing_keys()?.is_empty() { - init_accounts_table(&db, &[evk])?; - } - Ok(db) - } - }) - .await -} - -#[cfg(not(target_arch = "wasm32"))] pub(super) async fn init_light_client<'a>( builder: &ZCoinBuilder<'a>, lightwalletd_urls: Vec, blocks_db: BlockDbImpl, sync_params: &Option, + skip_sync_params: bool, z_spending_key: &ExtendedSpendingKey, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { 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 light_rpc_clients = LightRpcClient::new(lightwalletd_urls).await?; + let min_height = blocks_db.get_earliest_block().await? as u64; let current_block_height = light_rpc_clients .get_block_height() .await .mm_err(ZcoinClientInitError::UpdateBlocksCacheErr)?; let sapling_activation_height = builder.protocol_info.consensus_params.sapling_activation_height as u64; - let sync_height = match sync_params { + let sync_height = match *sync_params { Some(SyncStartPoint::Date(date)) => builder - .calculate_starting_height_from_date(*date, current_block_height) + .calculate_starting_height_from_date(date, current_block_height) .mm_err(ZcoinClientInitError::UtxoCoinBuildError)? .unwrap_or(sapling_activation_height), - Some(SyncStartPoint::Height(height)) => *height, + Some(SyncStartPoint::Height(height)) => height, Some(SyncStartPoint::Earliest) => sapling_activation_height, None => builder .calculate_starting_height_from_date(now_sec() - DAY_IN_SECONDS, current_block_height) @@ -482,23 +533,21 @@ 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()))?; - if !continue_from_prev_sync && (sync_height != min_height as u64) { + // check if no sync_params was provided and continue syncing from last height in db if it's > 0 or skip_sync_params is true. + let continue_from_prev_sync = + (min_height > 0 && sync_params.is_none()) || (skip_sync_params && min_height < sapling_activation_height); + let wallet_db = + WalletDbShared::new(builder, maybe_checkpoint_block, z_spending_key, continue_from_prev_sync).await?; + // Check min_height in blocks_db and rewind blocks_db to 0 if sync_height != min_height + if !continue_from_prev_sync && (sync_height != min_height) { // let user know we're clearing cache and resyncing from new provided height. 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) - .map_err(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; + blocks_db.rewind_to_height(u32::MIN.into()).await?; }; let sync_handle = SaplingSyncLoopHandle { @@ -527,18 +576,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>, @@ -559,7 +596,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, @@ -582,31 +619,8 @@ 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!() -} - -#[cfg(not(target_arch = "wasm32"))] -fn is_tx_imported(conn: &Connection, tx_id: TxId) -> bool { - const QUERY: &str = "SELECT id_tx FROM transactions WHERE txid = ?1;"; - match query_single_row(conn, QUERY, [tx_id.0.to_vec()], |row| row.get::<_, i64>(0)) { - Ok(Some(_)) => true, - Ok(None) | Err(_) => false, - } -} - -#[cfg(target_arch = "wasm32")] -#[allow(unused)] -fn is_tx_imported(_conn: String, _tx_id: TxId) -> bool { todo!() } - pub struct SaplingSyncRespawnGuard { - pub(super) sync_handle: Option<(SaplingSyncLoopHandle, Box)>, + pub(super) sync_handle: Option<(SaplingSyncLoopHandle, Box)>, pub(super) abort_handle: Arc>, } @@ -641,7 +655,6 @@ impl SaplingSyncRespawnGuard { /// the first synchronization block, the current scanned block, and the latest block. /// - `TemporaryError(String)`: Represents a temporary error state, with an associated error message /// providing details about the error. -/// - `RequestingWalletBalance`: Indicates the process of requesting the wallet balance. /// - `Finishing`: Represents the finishing state of an operation. pub enum SyncStatus { UpdatingBlocksCache { @@ -690,14 +703,13 @@ pub struct SaplingSyncLoopHandle { sync_status_notifier: AsyncSender, /// If new tx is required to be generated, we stop the sync and respawn it after tx is sent /// This watcher waits for such notification - on_tx_gen_watcher: AsyncReceiver)>>, + on_tx_gen_watcher: AsyncReceiver)>>, watch_for_tx: Option, scan_blocks_per_iteration: u32, scan_interval_ms: u64, first_sync_block: FirstSyncBlock, } -#[cfg(not(target_arch = "wasm32"))] impl SaplingSyncLoopHandle { fn first_sync_block(&self) -> FirstSyncBlock { self.first_sync_block.clone() } @@ -736,17 +748,12 @@ impl SaplingSyncLoopHandle { .debug_log_with_msg("No one seems interested in SyncStatus"); } - async fn update_blocks_cache( - &mut self, - rpc: &mut (dyn ZRpcOps + Send), - ) -> Result<(), MmError> { + async fn update_blocks_cache(&mut self, rpc: &dyn ZRpcOps) -> Result<(), MmError> { let current_block = rpc.get_block_height().await?; - let current_block_in_db = block_in_place(|| self.blocks_db.get_latest_block())?; + let block_db = self.blocks_db.clone(); + let current_block_in_db = &self.blocks_db.get_latest_block().await?; let wallet_db = self.wallet_db.clone(); - let extrema = block_in_place(|| { - let conn = wallet_db.db.lock(); - conn.block_height_extrema() - })?; + let extrema = wallet_db.db.block_height_extrema().await?; let mut from_block = self .consensus_params .sapling_activation_height @@ -757,48 +764,50 @@ impl SaplingSyncLoopHandle { } if current_block >= from_block { - rpc.scan_blocks(from_block, current_block, &mut |block: TonicCompactBlock| { - block_in_place(|| self.blocks_db.insert_block(block.height as u32, block.encode_to_vec())) - .map_err(|err| UpdateBlocksCacheErr::ZcashDBError(err.to_string()))?; - self.notify_blocks_cache_status(block.height, current_block); - Ok(()) - }) - .await?; + rpc.scan_blocks(from_block, current_block, &block_db, self).await?; } + self.current_block = BlockHeight::from_u32(current_block as u32); Ok(()) } /// 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> { - // required to avoid immutable borrow of self - let wallet_db_arc = self.wallet_db.clone(); - let wallet_guard = wallet_db_arc.db.lock(); - let mut wallet_ops = wallet_guard.get_update_ops().expect("get_update_ops always returns Ok"); - - if let Err(e) = validate_chain( - &self.consensus_params, - &self.blocks_db, - wallet_ops.get_max_height_hash()?, - ) { - match e { - ZcashClientError::BackendError(ChainError::InvalidChain(lower_bound, _)) => { + async fn scan_validate_and_update_blocks(&mut self) -> Result<(), MmError> { + let blocks_db = self.blocks_db.clone(); + let wallet_db = self.wallet_db.clone().db; + let mut wallet_ops = wallet_db.get_update_ops().expect("get_update_ops always returns Ok"); + + if let Err(e) = blocks_db + .process_blocks_with_mode( + self.consensus_params.clone(), + BlockProcessingMode::Validate, + wallet_ops.get_max_height_hash().await?, + None, + ) + .await + { + match e.into_inner() { + ZcoinStorageError::ValidateBlocksError(ValidateBlocksError::ChainInvalid { + height: lower_bound, + .. + }) => { let rewind_height = if lower_bound > BlockHeight::from_u32(10) { lower_bound - 10 } else { BlockHeight::from_u32(0) }; - wallet_ops.rewind_to_height(rewind_height)?; - self.blocks_db.rewind_to_height(rewind_height.into())?; + wallet_ops.rewind_to_height(rewind_height).await?; + blocks_db.rewind_to_height(rewind_height).await?; }, - e => return MmError::err(BlockDbError::SqliteError(e)), + e => return MmError::err(e), } } - let current_block = BlockHeight::from_u32(self.blocks_db.get_latest_block()?); + let latest_block_height = blocks_db.get_latest_block().await?; + let current_block = BlockHeight::from_u32(latest_block_height); loop { - match wallet_ops.block_height_extrema()? { + match wallet_ops.block_height_extrema().await? { Some((_, max_in_wallet)) => { if max_in_wallet >= current_block { break; @@ -809,20 +818,25 @@ impl SaplingSyncLoopHandle { None => self.notify_building_wallet_db(0, current_block.into()), } - scan_cached_blocks( - &self.consensus_params, - &self.blocks_db, - &mut wallet_ops, - Some(self.scan_blocks_per_iteration), - )?; + let scan = DataConnStmtCacheWrapper::new(wallet_ops.clone()); + blocks_db + .process_blocks_with_mode( + self.consensus_params.clone(), + BlockProcessingMode::Scan(scan), + None, + Some(self.scan_blocks_per_iteration), + ) + .await?; + if self.scan_interval_ms > 0 { - std::thread::sleep(Duration::from_millis(self.scan_interval_ms)); + Timer::sleep_ms(self.scan_interval_ms).await; } } + Ok(()) } - async fn check_watch_for_tx_existence(&mut self, rpc: &mut (dyn ZRpcOps + Send)) { + async fn check_watch_for_tx_existence(&mut self, rpc: &dyn ZRpcOps) { if let Some(tx_id) = self.watch_for_tx { if !rpc.check_tx_existence(tx_id).await { self.watch_for_tx = None; @@ -831,31 +845,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. @@ -877,22 +866,21 @@ 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) { +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 { + if let Err(e) = sync_handle.update_blocks_cache(client.as_ref()).await { error!("Error {} on blocks cache update", e); sync_handle.notify_on_error(e.to_string()); Timer::sleep(10.).await; continue; } - if let Err(e) = block_in_place(|| sync_handle.scan_blocks()) { + if let Err(e) = sync_handle.scan_validate_and_update_blocks().await { error!("Error {} on scan_blocks", e); sync_handle.notify_on_error(e.to_string()); Timer::sleep(10.).await; @@ -901,13 +889,16 @@ async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle, mut c sync_handle.notify_sync_finished(); - sync_handle.check_watch_for_tx_existence(client.as_mut()).await; + sync_handle.check_watch_for_tx_existence(client.as_ref()).await; if let Some(tx_id) = sync_handle.watch_for_tx { - if !block_in_place(|| is_tx_imported(sync_handle.wallet_db.db.lock().sql_conn(), tx_id)) { - info!("Tx {} is not imported yet", tx_id); - Timer::sleep(10.).await; - continue; + let walletdb = &sync_handle.wallet_db; + if let Ok(is_tx_imported) = walletdb.is_tx_imported(tx_id).await { + if !is_tx_imported { + info!("Tx {} is not imported yet", tx_id); + Timer::sleep(10.).await; + continue; + } } sync_handle.watch_for_tx = None; } @@ -926,13 +917,8 @@ 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)>>; +type NewTxNotifier = AsyncSender)>>; pub(super) struct SaplingSyncConnector { sync_watcher: SyncWatcher, 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 fc3982e17e..f52bf2f3a7 100644 --- a/mm2src/coins_activation/src/lib.rs +++ b/mm2src/coins_activation/src/lib.rs @@ -26,10 +26,9 @@ mod tendermint_token_activation; mod tendermint_with_assets_activation; mod token; mod utxo_activation; - #[cfg(not(target_arch = "wasm32"))] pub use utxo_activation::for_tests; -#[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 79132a6d39..a2dcbfd493 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -192,6 +192,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 9a1c06d83b..b64cd23a43 100644 --- a/mm2src/crypto/src/hw_rpc_task.rs +++ b/mm2src/crypto/src/hw_rpc_task.rs @@ -24,8 +24,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_bitcoin/keys/Cargo.toml b/mm2src/mm2_bitcoin/keys/Cargo.toml index 7017d8ff24..990bd3dff1 100644 --- a/mm2src/mm2_bitcoin/keys/Cargo.toml +++ b/mm2src/mm2_bitcoin/keys/Cargo.toml @@ -13,7 +13,7 @@ bech32 = "0.9.1" bitcrypto = { path = "../crypto" } derive_more = "0.99" lazy_static = "1.4" -rand = "0.6" +rand = {version = "0.6", features = ["wasm-bindgen"] } primitives = { path = "../primitives" } secp256k1 = { version = "0.20", features = ["rand", "recovery"] } serde = { version = "1.0", features = ["derive"] } diff --git a/mm2src/mm2_db/src/indexed_db/indexed_cursor.rs b/mm2src/mm2_db/src/indexed_db/indexed_cursor.rs index 096733e68a..4a0b851ac0 100644 --- a/mm2src/mm2_db/src/indexed_db/indexed_cursor.rs +++ b/mm2src/mm2_db/src/indexed_db/indexed_cursor.rs @@ -253,7 +253,7 @@ mod tests { } impl TableSignature for SwapTable { - fn table_name() -> &'static str { "swap_test_table" } + const TABLE_NAME: &'static str = "swap_test_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, _new_version: u32) -> OnUpgradeResult<()> { if old_version > 0 { @@ -327,7 +327,7 @@ mod tests { } impl TableSignature for TimestampTable { - fn table_name() -> &'static str { "timestamp_table" } + const TABLE_NAME: &'static str = "timestamp_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, _new_version: u32) -> OnUpgradeResult<()> { if old_version > 0 { diff --git a/mm2src/mm2_db/src/indexed_db/indexed_db.rs b/mm2src/mm2_db/src/indexed_db/indexed_db.rs index 9521938ef6..7f0822bf56 100644 --- a/mm2src/mm2_db/src/indexed_db/indexed_db.rs +++ b/mm2src/mm2_db/src/indexed_db/indexed_db.rs @@ -63,7 +63,7 @@ pub mod cursor_prelude { } pub trait TableSignature: DeserializeOwned + Serialize + 'static { - fn table_name() -> &'static str; + const TABLE_NAME: &'static str; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()>; } @@ -134,7 +134,7 @@ impl IndexedDbBuilder { pub fn with_table(mut self) -> IndexedDbBuilder { let on_upgrade_needed_cb = Box::new(Table::on_upgrade_needed); - self.tables.insert(Table::table_name().to_owned(), on_upgrade_needed_cb); + self.tables.insert(Table::TABLE_NAME.to_owned(), on_upgrade_needed_cb); self } @@ -248,7 +248,7 @@ impl DbTransaction<'_> { pub async fn table(&self) -> DbTransactionResult> { let (result_tx, result_rx) = oneshot::channel(); let event = internal::DbTransactionEvent::OpenTable { - table_name: Table::table_name().to_owned(), + table_name: Table::TABLE_NAME.to_owned(), result_tx, }; let transaction_event_tx = send_event_recv_response(&self.event_tx, event, result_rx).await?; @@ -314,12 +314,19 @@ pub enum AddOrIgnoreResult { ExistAlready(ItemId), } +impl AddOrIgnoreResult { + pub fn get_id(&self) -> ItemId { + match self { + AddOrIgnoreResult::Added(id) => *id, + AddOrIgnoreResult::ExistAlready(id) => *id, + } + } +} impl<'transaction, Table: TableSignature> DbTable<'transaction, Table> { /// Adds the given item to the 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 @@ -499,7 +506,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()))?; @@ -922,7 +929,7 @@ mod tests { } impl TableSignature for TxTable { - fn table_name() -> &'static str { "tx_table" } + const TABLE_NAME: &'static str = "tx_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, _new_version: u32) -> OnUpgradeResult<()> { if old_version > 0 { @@ -1296,7 +1303,7 @@ mod tests { struct UpgradableTable; impl TableSignature for UpgradableTable { - fn table_name() -> &'static str { "upgradable_table" } + const TABLE_NAME: &'static str = "upgradable_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { let mut versions = LAST_VERSIONS.lock().expect("!old_new_versions.lock()"); @@ -1429,7 +1436,7 @@ mod tests { } impl TableSignature for SwapTable { - fn table_name() -> &'static str { "swap_table" } + const TABLE_NAME: &'static str = "swap_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, _new_version: u32) -> OnUpgradeResult<()> { if old_version > 0 { diff --git a/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs b/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs index cbf9967d42..1f05370c29 100644 --- a/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs +++ b/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs @@ -386,11 +386,11 @@ impl AccountTable { } impl TableSignature for AccountTable { - fn table_name() -> &'static str { "gui_account" } + const TABLE_NAME: &'static str = "gui_account"; 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())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index( AccountTable::ACCOUNT_ID_INDEX, &["account_type", "account_idx", "device_pubkey"], @@ -470,11 +470,11 @@ impl TryFrom for EnabledAccountId { } impl TableSignature for EnabledAccountTable { - fn table_name() -> &'static str { "gui_enabled_account" } + const TABLE_NAME: &'static str = "gui_enabled_account"; 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())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index( AccountTable::ACCOUNT_ID_INDEX, &["account_type", "account_idx", "device_pubkey"], diff --git a/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs b/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs index c96c2ef024..3e6c1665c0 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs @@ -50,10 +50,10 @@ pub mod tables { } impl TableSignature for MyActiveMakerOrdersTable { - fn table_name() -> &'static str { "my_active_maker_orders" } + const TABLE_NAME: &'static str = "my_active_maker_orders"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - on_upgrade_swap_table_by_uuid_v1(upgrader, old_version, new_version, Self::table_name()) + on_upgrade_swap_table_by_uuid_v1(upgrader, old_version, new_version, Self::TABLE_NAME) } } @@ -64,10 +64,10 @@ pub mod tables { } impl TableSignature for MyActiveTakerOrdersTable { - fn table_name() -> &'static str { "my_active_taker_orders" } + const TABLE_NAME: &'static str = "my_active_taker_orders"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - on_upgrade_swap_table_by_uuid_v1(upgrader, old_version, new_version, Self::table_name()) + on_upgrade_swap_table_by_uuid_v1(upgrader, old_version, new_version, Self::TABLE_NAME) } } @@ -78,10 +78,10 @@ pub mod tables { } impl TableSignature for MyHistoryOrdersTable { - fn table_name() -> &'static str { "my_history_orders" } + const TABLE_NAME: &'static str = "my_history_orders"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - on_upgrade_swap_table_by_uuid_v1(upgrader, old_version, new_version, Self::table_name()) + on_upgrade_swap_table_by_uuid_v1(upgrader, old_version, new_version, Self::TABLE_NAME) } } @@ -101,11 +101,11 @@ pub mod tables { } impl TableSignature for MyFilteringHistoryOrdersTable { - fn table_name() -> &'static str { "my_filtering_history_orders" } + const TABLE_NAME: &'static str = "my_filtering_history_orders"; 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())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_index("uuid", true)?; // TODO add other indexes during [`MyOrdersStorage::select_orders_by_filter`] implementation. } diff --git a/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs b/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs index cc45036397..3f11bc963a 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs @@ -53,10 +53,10 @@ pub mod tables { } impl TableSignature for SwapLockTable { - fn table_name() -> &'static str { "swap_lock" } + const TABLE_NAME: &'static str = "swap_lock"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - on_upgrade_swap_table_by_uuid_v1(upgrader, old_version, new_version, Self::table_name()) + on_upgrade_swap_table_by_uuid_v1(upgrader, old_version, new_version, Self::TABLE_NAME) } } @@ -67,10 +67,10 @@ pub mod tables { } impl TableSignature for SavedSwapTable { - fn table_name() -> &'static str { "saved_swap" } + const TABLE_NAME: &'static str = "saved_swap"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - on_upgrade_swap_table_by_uuid_v1(upgrader, old_version, new_version, Self::table_name()) + on_upgrade_swap_table_by_uuid_v1(upgrader, old_version, new_version, Self::TABLE_NAME) } } @@ -90,13 +90,13 @@ pub mod tables { } impl TableSignature for MySwapsFiltersTable { - fn table_name() -> &'static str { "my_swaps" } + const TABLE_NAME: &'static str = "my_swaps"; fn on_upgrade_needed(upgrader: &DbUpgrader, mut old_version: u32, new_version: u32) -> OnUpgradeResult<()> { while old_version < new_version { match old_version { 0 => { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_index("uuid", true)?; table.create_index("started_at", false)?; table.create_multi_index("with_my_coin", &["my_coin", "started_at"], false)?; @@ -108,7 +108,7 @@ pub mod tables { )?; }, 1 => { - let table = upgrader.open_table(Self::table_name())?; + let table = upgrader.open_table(Self::TABLE_NAME)?; table.create_multi_index(IS_FINISHED_SWAP_TYPE_INDEX, &["is_finished", "swap_type"], false)?; }, unsupported_version => { @@ -162,7 +162,7 @@ pub mod tables { } impl TableSignature for SwapsMigrationTable { - fn table_name() -> &'static str { "swaps_migration" } + const TABLE_NAME: &'static str = "swaps_migration"; fn on_upgrade_needed(upgrader: &DbUpgrader, mut old_version: u32, new_version: u32) -> OnUpgradeResult<()> { while old_version < new_version { @@ -172,7 +172,7 @@ pub mod tables { // from version 1 to 2 in order to avoid breaking existing databases }, 1 => { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_index("migration", true)?; }, unsupported_version => { diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index f1e2174ef1..4924003cc0 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -30,6 +30,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( @@ -57,7 +58,6 @@ use std::net::SocketAddr; cfg_native! { use coins::lightning::LightningCoin; - use coins::z_coin::ZCoin; } pub async fn process_single_request( @@ -266,16 +266,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 4bb1fe41f2..5899f98b42 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 { @@ -240,3 +246,23 @@ async fn trade_v2_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 56d4fde57f..39712f53ab 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 4f84176fea..6bacad2302 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..456e3d6688 100644 --- a/mm2src/mm2_net/Cargo.toml +++ b/mm2src/mm2_net/Cargo.toml @@ -30,15 +30,27 @@ 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.7" +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.1.2" +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..d054637e2d --- /dev/null +++ b/mm2src/mm2_net/src/wasm/body_stream.rs @@ -0,0 +1,413 @@ +/// 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::*; +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::convert::TryInto; +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 = u32::from_be_bytes(data_length_bytes.to_vec().try_into().unwrap()); + + 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(u32::from_be_bytes(this.buf.take(4).to_vec().try_into().unwrap())); + }, + 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 2e48970181..e836da8c68 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, Window, WorkerGlobalScope}; +use web_sys::{Request as JsRequest, RequestInit, RequestMode, Response as JsResponse, Window, WorkerGlobalScope}; /// 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())), + } +} + /// This function is a wrapper around the `fetch_with_request`, providing compatibility across /// different execution environments, such as window and worker. fn compatible_fetch_with_request(js_request: &web_sys::Request) -> MmResult { @@ -140,6 +177,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; @@ -162,6 +206,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 uri = request.uri; @@ -173,7 +228,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 @@ -247,11 +302,7 @@ impl FetchRequest { let resp_array_fut = match js_response.array_buffer() { Ok(blob) => blob, Err(e) => { - let error = format!( - "Expected blob, found {:?}: {}", - js_response, - common::stringify_js_error(&e) - ); + let error = format!("Expected blob, found {:?}: {}", js_response, stringify_js_error(&e)); return MmError::err(SlurpError::ErrorDeserializing { uri, error }); }, }; @@ -266,6 +317,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 6ade556de7..1728d92464 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] = &["https://pirate.battlefield.earth:8581"]; pub const DEFAULT_RPC_PASSWORD: &str = "pass"; pub const QRC20_ELECTRUMS: &[&str] = &[ "electrum1.cipig.net:10071", @@ -3234,6 +3244,33 @@ pub async fn coins_needed_for_kickstart(mm: &MarketMakerIt) -> Vec { result.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() {