diff --git a/Cargo.lock b/Cargo.lock index 99aa0cac83..c67a8649c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,7 +208,24 @@ dependencies = [ "serde_json", "serde_urlencoded 0.6.1", "url", - "wildmatch", + "wildmatch 1.1.0", +] + +[[package]] +name = "attohttpc" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072" +dependencies = [ + "flate2", + "http", + "log", + "native-tls", + "openssl", + "serde", + "serde_json", + "url", + "wildmatch 2.1.0", ] [[package]] @@ -260,6 +277,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874f8444adcb4952a8bc51305c8be95c8ec8237bb0d2e78d2e039f771f8828a0" + [[package]] name = "binascii" version = "0.1.4" @@ -277,14 +300,16 @@ dependencies = [ [[package]] name = "bip32" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d0f0fc59c7ba0333eed9dcc1b6980baa7b7a4dc7c6c5885994d0674f7adf34" +checksum = "873faa4363bfc54c36a48321da034c92a0645a363eed34d948683ffc1706e37f" dependencies = [ "bs58", - "hkd32", "hmac", - "k256", + "k256 0.10.1", + "once_cell", + "pbkdf2", + "rand_core 0.6.3", "ripemd160", "sha2", "subtle 2.4.1", @@ -342,9 +367,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.0.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcd555c66291d5f836dbb6883b48660ece810fe25a31f3bdfb911945dff2691f" +checksum = "526c210b4520e416420759af363083471656e819a75e831b8d2c9d5a584f2413" dependencies = [ "arrayref", "arrayvec 0.7.1", @@ -406,8 +431,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54757888b09a69be70b5ec303e382a74227392086ba808cb01eeca29233a2397" dependencies = [ "digest 0.9.0", - "ff", - "group", + "ff 0.10.1", + "group 0.10.0", "pairing", "rand_core 0.6.3", "subtle 2.4.1", @@ -746,6 +771,41 @@ dependencies = [ "url", ] +[[package]] +name = "console-api" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f67643a7d716307ad10b3e3aef02826382acbe349a3e7605ac57556148bc87" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tonic-build", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829835c211a0247cd11e65e13cec8696b879374879c35ce162ce8098b23c90d4" +dependencies = [ + "console-api", + "crossbeam-channel", + "futures", + "hdrhistogram", + "humantime 2.1.0", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "console_error_panic_hook" version = "0.1.6" @@ -762,6 +822,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdab415d6744056100f40250a66bc430c1a46f7a02e20bc11c94c79a0f0464df" +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + [[package]] name = "const_fn" version = "0.4.8" @@ -774,6 +840,13 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "contracts-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -875,8 +948,9 @@ dependencies = [ [[package]] name = "cosmos-sdk-proto" -version = "0.8.0" -source = "git+https://github.com/cosmos/cosmos-rust?rev=e5a1872083abb3d88fa62dda966e7f5408deba58#e5a1872083abb3d88fa62dda966e7f5408deba58" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0254ffee603f5301d6a66963d9e1cc5091479c22e2e925e1f7689c8027a0828" dependencies = [ "prost", "prost-types", @@ -885,15 +959,16 @@ dependencies = [ [[package]] name = "cosmrs" -version = "0.3.0" -source = "git+https://github.com/cosmos/cosmos-rust?rev=e5a1872083abb3d88fa62dda966e7f5408deba58#e5a1872083abb3d88fa62dda966e7f5408deba58" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "505ea048e9ff2f906d6b954f9f8157d903ca468bfb301d906b40ecc25ba6838d" dependencies = [ "bip32", "cosmos-sdk-proto", - "ecdsa", + "ecdsa 0.13.4", "eyre", "getrandom 0.2.3", - "k256", + "k256 0.10.1", "prost", "prost-types", "rand_core 0.6.3", @@ -913,7 +988,7 @@ checksum = "a380b87642204557629c9b72988c47b55fbfe6d474960adba56b22331504956a" dependencies = [ "digest 0.9.0", "ed25519-zebra", - "k256", + "k256 0.9.6", "rand_core 0.5.1", "thiserror", ] @@ -1118,6 +1193,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +dependencies = [ + "generic-array 0.14.4", + "rand_core 0.6.3", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "crypto-mac" version = "0.7.0" @@ -1220,9 +1307,9 @@ dependencies = [ [[package]] name = "cw-storage-plus" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b8b840947313c1a1cccf056836cd79a60b4526bdcd6582995be37dc97be4ae" +checksum = "7d7ee1963302b0ac2a9d42fe0faec826209c17452bfd36fbfd9d002a88929261" dependencies = [ "cosmwasm-std", "schemars", @@ -1335,7 +1422,16 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2adca118c71ecd9ae094d4b68257b3fdfcb711a612b9eec7b5a0d27a5a70a5b4" dependencies = [ - "const-oid", + "const-oid 0.6.1", +] + +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid 0.7.1", ] [[package]] @@ -1544,17 +1640,29 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372" dependencies = [ - "der", - "elliptic-curve", + "der 0.4.3", + "elliptic-curve 0.10.6", "hmac", "signature", ] [[package]] -name = "ed25519" -version = "1.2.0" +name = "ecdsa" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4620d40f6d2601794401d6dd95a5cf69b6c157852539470eeda433a99b3c0efc" +checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9" +dependencies = [ + "der 0.5.1", + "elliptic-curve 0.11.6", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74e1069e39f1454367eb2de793ed062fac4c35c2934b76a81d90dd9abcd28816" dependencies = [ "signature", ] @@ -1599,16 +1707,33 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "beca177dcb8eb540133e7680baff45e7cc4d93bf22002676cec549f82343721b" dependencies = [ - "crypto-bigint", - "ff", + "crypto-bigint 0.2.10", + "ff 0.10.1", "generic-array 0.14.4", - "group", - "pkcs8", + "group 0.10.0", + "pkcs8 0.7.6", "rand_core 0.6.3", "subtle 2.4.1", "zeroize", ] +[[package]] +name = "elliptic-curve" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decb3a27ea454a5f23f96eb182af0671c12694d64ecc33dada74edd1301f6cfc" +dependencies = [ + "crypto-bigint 0.3.2", + "der 0.5.1", + "ff 0.11.0", + "generic-array 0.14.4", + "group 0.11.0", + "rand_core 0.6.3", + "sec1", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "embed_plist" version = "1.2.0" @@ -1729,7 +1854,7 @@ dependencies = [ "humantime-serde", "isocountry", "log", - "mixnet-contract", + "mixnet-contract-common", "network-defaults", "okapi", "pretty_env_logger", @@ -1780,6 +1905,16 @@ dependencies = [ "subtle 2.4.1", ] +[[package]] +name = "ff" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2958d04124b9f27f175eaeb9a9f383d026098aa837eadd8ba22c11f13a05b9e" +dependencies = [ + "rand_core 0.6.3", + "subtle 2.4.1", +] + [[package]] name = "field-offset" version = "0.3.4" @@ -1841,6 +1976,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "fixedbitset" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" + [[package]] name = "flate2" version = "1.0.22" @@ -2422,7 +2563,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" dependencies = [ "byteorder", - "ff", + "ff 0.10.1", + "rand_core 0.6.3", + "subtle 2.4.1", +] + +[[package]] +name = "group" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" +dependencies = [ + "ff 0.11.0", "rand_core 0.6.3", "subtle 2.4.1", ] @@ -2485,9 +2637,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.4" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f3675cfef6a30c8031cf9e6493ebdc3bb3272a3fea3923c4210d1830e6a472" +checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689" dependencies = [ "bytes", "fnv", @@ -2540,6 +2692,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "hdrhistogram" +version = "7.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6490be71f07a5f62b564bc58e36953f675833df11c7e4a0647bee7a07ca1ec5e" +dependencies = [ + "base64", + "byteorder", + "flate2", + "nom", + "num-traits", +] + [[package]] name = "headers" version = "0.3.4" @@ -2595,20 +2760,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e4590e13640f19f249fe3e4eca5113bc4289f2497710378190e7f4bd96f45b" -[[package]] -name = "hkd32" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f2a5541afe0725f0b95619d6af614f48c1b176385b8aa30918cfb8c4bfafc8" -dependencies = [ - "hmac", - "once_cell", - "pbkdf2", - "rand_core 0.6.3", - "sha2", - "zeroize", -] - [[package]] name = "hkdf" version = "0.11.0" @@ -2656,9 +2807,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ "bytes", "http", @@ -2710,9 +2861,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.13" +version = "0.14.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d1cfb9e4f68655fa04c01f59edb405b6074a0f7118ea881e5026e4a1cd8593" +checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" dependencies = [ "bytes", "futures-channel", @@ -2768,6 +2919,18 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -3050,8 +3213,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea" dependencies = [ "cfg-if 1.0.0", - "ecdsa", - "elliptic-curve", + "ecdsa 0.12.4", + "elliptic-curve 0.10.6", + "sha2", +] + +[[package]] +name = "k256" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7511aa19fa182a8a4885760c4e5675b17173b02ae86ec5d376d34f5278c874b9" +dependencies = [ + "cfg-if 1.0.0", + "ecdsa 0.13.4", + "elliptic-curve 0.11.6", + "sec1", "sha2", "sha3", ] @@ -3309,10 +3485,11 @@ dependencies = [ ] [[package]] -name = "mixnet-contract" +name = "mixnet-contract-common" version = "0.1.0" dependencies = [ "az", + "contracts-common", "cosmwasm-std", "fixed", "log", @@ -3370,6 +3547,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + [[package]] name = "native-tls" version = "0.2.8" @@ -3438,6 +3621,7 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" name = "network-defaults" version = "0.1.0" dependencies = [ + "cfg-if 1.0.0", "hex-literal", "serde", "time 0.3.3", @@ -3767,9 +3951,12 @@ name = "nym-validator-api" version = "0.12.0" dependencies = [ "anyhow", + "attohttpc 0.18.0", + "cfg-if 1.0.0", "clap", "coconut-interface", "config", + "console-subscriber", "credentials", "crypto", "dirs", @@ -3779,7 +3966,7 @@ dependencies = [ "getset", "humantime-serde", "log", - "mixnet-contract", + "mixnet-contract-common", "nymsphinx", "pin-project", "pretty_env_logger", @@ -3798,6 +3985,7 @@ dependencies = [ "tokio", "topology", "url", + "validator-api-requests", "validator-client", "vergen", "version-checker", @@ -3813,9 +4001,9 @@ dependencies = [ "criterion", "digest 0.9.0", "doc-comment", - "ff", + "ff 0.10.1", "getrandom 0.2.3", - "group", + "group 0.10.0", "itertools", "rand 0.8.4", "serde", @@ -3829,7 +4017,7 @@ name = "nymsphinx" version = "0.1.0" dependencies = [ "crypto", - "mixnet-contract", + "mixnet-contract-common", "nymsphinx-acknowledgements", "nymsphinx-addressing", "nymsphinx-anonymous-replies", @@ -4062,7 +4250,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7de9d09263c9966e8196fe0380c9dbbc7ea114b5cf371ba29004bc1f9c6db7f3" dependencies = [ - "group", + "group 0.10.0", ] [[package]] @@ -4155,9 +4343,9 @@ checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" [[package]] name = "pbkdf2" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739" dependencies = [ "crypto-mac 0.11.1", ] @@ -4279,6 +4467,16 @@ dependencies = [ "sha-1 0.8.2", ] +[[package]] +name = "petgraph" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "phf" version = "0.8.0" @@ -4415,8 +4613,19 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447" dependencies = [ - "der", - "spki", + "der 0.4.3", + "spki 0.4.1", +] + +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der 0.5.1", + "spki 0.5.4", + "zeroize", ] [[package]] @@ -4610,6 +4819,26 @@ dependencies = [ "prost-derive", ] +[[package]] +name = "prost-build" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" +dependencies = [ + "bytes", + "heck", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost", + "prost-types", + "regex", + "tempfile", + "which", +] + [[package]] name = "prost-derive" version = "0.9.0" @@ -5055,6 +5284,17 @@ dependencies = [ "winreg", ] +[[package]] +name = "rfc6979" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525" +dependencies = [ + "crypto-bigint 0.3.2", + "hmac", + "zeroize", +] + [[package]] name = "rfd" version = "0.4.4" @@ -5409,6 +5649,19 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sec1" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" +dependencies = [ + "der 0.5.1", + "generic-array 0.14.4", + "pkcs8 0.8.0", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "secp256k1" version = "0.20.3" @@ -5702,6 +5955,15 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -5874,7 +6136,17 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c01a0c15da1b0b0e1494112e7af814a678fec9bd157881b49beac661e9b6f32" dependencies = [ - "der", + "der 0.4.3", +] + +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der 0.5.1", ] [[package]] @@ -6440,7 +6712,7 @@ version = "1.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79a0579dcc6fb883fe90dd3c66d76b8b8f4a1786e1e915e314b2017a500ede09" dependencies = [ - "attohttpc", + "attohttpc 0.17.0", "bincode", "cfg_aliases", "dirs-next", @@ -6590,18 +6862,17 @@ dependencies = [ [[package]] name = "tendermint" -version = "0.23.0" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d1cdb0236becb17ab35a2ed1566503e412fd910944dc940239857bb7663652" +checksum = "a9ef686b8ecd36550d0581f0989c9d8607090b23005df479d8e6ba348b5800b9" dependencies = [ "async-trait", "bytes", - "chrono", "ed25519", "ed25519-dalek", "flex-error", "futures", - "k256", + "k256 0.10.1", "num-traits", "once_cell", "prost", @@ -6616,14 +6887,15 @@ dependencies = [ "subtle 2.4.1", "subtle-encoding", "tendermint-proto", + "time 0.3.3", "zeroize", ] [[package]] name = "tendermint-config" -version = "0.23.0" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1f94250d30e3011130a09756b05985d8dbfbd562cf261b5a17e36d89a37992" +checksum = "ecc127f82e7a8c7337c1f293d65a821380d3407c4c44bc979ef4da0ebc3b31ed" dependencies = [ "flex-error", "serde", @@ -6635,12 +6907,11 @@ dependencies = [ [[package]] name = "tendermint-proto" -version = "0.23.0" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff16a7b42bdbcf31c8cd10a4cffc7631f2a301360ba3a3f71dde48eabfa5bced" +checksum = "a9e0a0251fd81bed7420bea0f4d91c2a58f6c9fa34d5b2f70bed0ba8890de5aa" dependencies = [ "bytes", - "chrono", "flex-error", "num-derive", "num-traits", @@ -6649,20 +6920,20 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", + "time 0.3.3", ] [[package]] name = "tendermint-rpc" -version = "0.23.0" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7842dcd5edb60b077572aa92ff8b29fc810b9b463310f9810a2607474130db4" +checksum = "626c493e9ced3a9de37583bbd929f4b6dbd49aa513ab9b4776aa44a9332ce9b5" dependencies = [ "async-trait", "bytes", - "chrono", "flex-error", "futures", - "getrandom 0.1.16", + "getrandom 0.2.3", "http", "hyper", "hyper-proxy", @@ -6677,6 +6948,7 @@ dependencies = [ "tendermint-config", "tendermint-proto", "thiserror", + "time 0.3.3", "tokio", "tracing", "url", @@ -6836,11 +7108,10 @@ dependencies = [ [[package]] name = "tokio" -version = "1.12.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" dependencies = [ - "autocfg 1.0.1", "bytes", "libc", "memchr", @@ -6851,14 +7122,25 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "tokio-macros", + "tracing", "winapi", ] [[package]] -name = "tokio-macros" -version = "1.3.0" +name = "tokio-io-timeout" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", @@ -6948,6 +7230,49 @@ dependencies = [ "serde", ] +[[package]] +name = "tonic" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff08f4649d10a70ffa3522ca559031285d8e421d727ac85c60825761818f5d0a" +dependencies = [ + "async-stream", + "async-trait", + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "prost-derive", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tonic-build" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9403f1bafde247186684b230dc6f38b5cd514584e8bec1dd32514be4745fa757" +dependencies = [ + "proc-macro2", + "prost-build", + "quote", + "syn", +] + [[package]] name = "topology" version = "0.1.0" @@ -6955,13 +7280,40 @@ dependencies = [ "bs58", "crypto", "log", - "mixnet-contract", + "mixnet-contract-common", "nymsphinx-addressing", "nymsphinx-types", "rand 0.7.3", "version-checker", ] +[[package]] +name = "tower" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5651b5f6860a99bd1adb59dbfe1db8beb433e73709d9032b413a77e2fb7c066a" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project", + "pin-project-lite", + "rand 0.8.4", + "slab", + "tokio", + "tokio-stream", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + [[package]] name = "tower-service" version = "0.3.1" @@ -6975,10 +7327,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8" dependencies = [ "cfg-if 1.0.0", + "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.20" @@ -6988,6 +7353,27 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77be66445c4eeebb934a7340f227bfe7b338173d3f8c00a60a5a58005c9faecf" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -7187,6 +7573,14 @@ dependencies = [ "getrandom 0.2.3", ] +[[package]] +name = "validator-api-requests" +version = "0.1.0" +dependencies = [ + "serde", + "ts-rs", +] + [[package]] name = "validator-client" version = "0.1.0" @@ -7201,7 +7595,7 @@ dependencies = [ "flate2", "itertools", "log", - "mixnet-contract", + "mixnet-contract-common", "network-defaults", "prost", "reqwest", @@ -7211,6 +7605,7 @@ dependencies = [ "thiserror", "ts-rs", "url", + "validator-api-requests", "vesting-contract", ] @@ -7275,12 +7670,18 @@ dependencies = [ "config", "cosmwasm-std", "cw-storage-plus", - "getrandom 0.2.3", - "mixnet-contract", - "rand 0.8.4", + "mixnet-contract-common", "schemars", "serde", "thiserror", + "vesting-contract-common", +] + +[[package]] +name = "vesting-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", ] [[package]] @@ -7591,6 +7992,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "which" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +dependencies = [ + "either", + "lazy_static", + "libc", +] + [[package]] name = "whoami" version = "1.1.4" @@ -7613,6 +8025,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a" +[[package]] +name = "wildmatch" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 5537a3d5a5..90a214fe8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,9 @@ members = [ "common/credentials", "common/crypto", "common/bandwidth-claim-contract", - "common/mixnet-contract", + "common/cosmwasm-smart-contracts/contracts-common", + "common/cosmwasm-smart-contracts/mixnet-contract", + "common/cosmwasm-smart-contracts/vesting-contract", "common/mixnode-common", "common/network-defaults", "common/nonexhaustive-delayqueue", @@ -52,6 +54,7 @@ members = [ "mixnode", "service-providers/network-requester", "validator-api", + "validator-api/validator-api-requests", ] default-members = [ diff --git a/README.md b/README.md index 14531cf0a5..4a10ea119a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ The platform is composed of multiple Rust crates. Top-level executable binary cr ### Building -Platform build instructions are available on [our docs site](https://nymtech.net/docs/0.11.0/overview/index/). +Platform build instructions are available on [our docs site](https://nymtech.net/docs/stable/run-nym-nodes/build-nym). +Wallet build instructions are also available on [our docs site](https://nymtech.net/docs/stable/nym-apps/wallet#for-developers). ### Developing diff --git a/clients/client-core/src/client/key_manager.rs b/clients/client-core/src/client/key_manager.rs index f852af3806..2e7069a6a8 100644 --- a/clients/client-core/src/client/key_manager.rs +++ b/clients/client-core/src/client/key_manager.rs @@ -79,9 +79,9 @@ impl KeyManager { ))?; let gateway_shared_key: SharedKeys = - pemstore::load_key(&client_pathfinder.gateway_shared_key().to_owned())?; + pemstore::load_key(client_pathfinder.gateway_shared_key())?; - let ack_key: AckKey = pemstore::load_key(&client_pathfinder.ack_key().to_owned())?; + let ack_key: AckKey = pemstore::load_key(client_pathfinder.ack_key())?; // TODO: ack key is never stored so it is generated now. But perhaps it should be stored // after all for consistency sake? diff --git a/clients/client-core/src/client/reply_key_storage.rs b/clients/client-core/src/client/reply_key_storage.rs index e934e4c5b3..e9ed2be1c8 100644 --- a/clients/client-core/src/client/reply_key_storage.rs +++ b/clients/client-core/src/client/reply_key_storage.rs @@ -59,7 +59,7 @@ impl ReplyKeyStorage { ) -> Result<(), ReplyKeyStorageError> { let digest = encryption_key.compute_digest(); - let insertion_result = match self.db.insert(digest.to_vec(), encryption_key.to_bytes()) { + let insertion_result = match self.db.insert(digest, encryption_key.to_bytes()) { Err(e) => Err(ReplyKeyStorageError::DbWriteError(e)), Ok(existing_key) => { if existing_key.is_some() { @@ -79,7 +79,7 @@ impl ReplyKeyStorage { &self, key_digest: EncryptionKeyDigest, ) -> Result, ReplyKeyStorageError> { - let removal_result = match self.db.remove(&key_digest.to_vec()) { + let removal_result = match self.db.remove(key_digest) { Err(e) => Err(ReplyKeyStorageError::DbReadError(e)), Ok(existing_key) => { Ok(existing_key.map(|existing_key| self.read_encryption_key(existing_key))) diff --git a/clients/native/examples/websocket_binarysend.rs b/clients/native/examples/websocket_binarysend.rs index 3e9f563a0d..7ddf53e913 100644 --- a/clients/native/examples/websocket_binarysend.rs +++ b/clients/native/examples/websocket_binarysend.rs @@ -35,7 +35,7 @@ async fn send_file_with_reply() { let (mut ws_stream, _) = connect_async(uri).await.unwrap(); let recipient = get_self_address(&mut ws_stream).await; - println!("our full address is: {}", recipient.to_string()); + println!("our full address is: {}", recipient); let read_data = std::fs::read("examples/dummy_file").unwrap(); @@ -83,7 +83,7 @@ async fn send_file_without_reply() { let (mut ws_stream, _) = connect_async(uri).await.unwrap(); let recipient = get_self_address(&mut ws_stream).await; - println!("our full address is: {}", recipient.to_string()); + println!("our full address is: {}", recipient); let read_data = std::fs::read("examples/dummy_file").unwrap(); diff --git a/clients/native/examples/websocket_textsend.rs b/clients/native/examples/websocket_textsend.rs index 6804ea53b4..a6d00aa55f 100644 --- a/clients/native/examples/websocket_textsend.rs +++ b/clients/native/examples/websocket_textsend.rs @@ -36,7 +36,7 @@ async fn send_text_with_reply() { let (mut ws_stream, _) = connect_async(uri).await.unwrap(); let recipient = get_self_address(&mut ws_stream).await; - println!("our full address is: {}", recipient.to_string()); + println!("our full address is: {}", recipient); let send_request = json!({ "type" : "send", @@ -76,7 +76,7 @@ async fn send_text_without_reply() { let (mut ws_stream, _) = connect_async(uri).await.unwrap(); let recipient = get_self_address(&mut ws_stream).await; - println!("our full address is: {}", recipient.to_string()); + println!("our full address is: {}", recipient); let send_request = json!({ "type" : "send", diff --git a/clients/validator/src/index.ts b/clients/validator/src/index.ts index 33f44c6aab..76b475b8d3 100644 --- a/clients/validator/src/index.ts +++ b/clients/validator/src/index.ts @@ -433,6 +433,14 @@ export default class ValidatorClient implements INymClient { return (this.client as ISigningClient).undelegateFromMixNode(this.mixnetContract, mixIdentity, fee, memo); } + public async updateMixnodeConfig( + mixIdentity: string, + fee: StdFee | 'auto' | number, + profitPercentage: number, + ): Promise { + return (this.client as ISigningClient).updateMixnodeConfig(this.mixnetContract, mixIdentity, profitPercentage, fee); + } + public async updateContractStateParams( newParams: ContractStateParams, fee?: StdFee | 'auto' | number, @@ -441,4 +449,4 @@ export default class ValidatorClient implements INymClient { this.assertSigning(); return (this.client as ISigningClient).updateContractStateParams(this.mixnetContract, newParams, fee, memo); } -} +} \ No newline at end of file diff --git a/clients/validator/src/signing-client.ts b/clients/validator/src/signing-client.ts index eb79efd51a..8b027e9480 100644 --- a/clients/validator/src/signing-client.ts +++ b/clients/validator/src/signing-client.ts @@ -178,6 +178,13 @@ export interface ISigningClient extends IQueryClient, ICosmWasmSigning, INymSign memo?: string, ): Promise; + updateMixnodeConfig( + mixnetContractAddress: string, + mixIdentity: string, + profitMarginPercent: number, + fee: StdFee | 'auto' | number, + ): Promise; + updateContractStateParams( mixnetContractAddress: string, newParams: ContractStateParams, @@ -448,6 +455,20 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig ); } + updateMixnodeConfig( + mixnetContractAddress: string, + mixIdentity: string, + profitMarginPercent: number, + fee: StdFee | 'auto' | number, + ): Promise { + return this.execute( + this.clientAddress, + mixnetContractAddress, + { update_mixnode_config: { profit_margin_percent: profitMarginPercent, mix_identity: mixIdentity } }, + fee, + ); + } + updateContractStateParams( mixnetContractAddress: string, newParams: ContractStateParams, diff --git a/clients/validator/src/types.ts b/clients/validator/src/types.ts index 9b8dc11b2c..786b72ff6c 100644 --- a/clients/validator/src/types.ts +++ b/clients/validator/src/types.ts @@ -135,6 +135,7 @@ export type MixNode = { sphinx_key: string; identity_key: string; version: string; + profit_margin_percent: number; }; export type GatewayBond = { diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 236bd71754..e5f831efbe 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -9,7 +9,7 @@ rust-version = "1.56" [dependencies] base64 = "0.13" -mixnet-contract = { path="../../../common/mixnet-contract" } +mixnet-contract-common = { path= "../../cosmwasm-smart-contracts/mixnet-contract" } vesting-contract = { path="../../../contracts/vesting" } serde = { version="1", features=["derive"] } serde_json = "1" @@ -20,6 +20,7 @@ url = { version = "2.2", features = ["serde"] } coconut-interface = { path = "../../coconut-interface" } network-defaults = { path = "../../network-defaults" } +validator-api-requests = { path = "../../../validator-api/validator-api-requests" } # required for nymd-client # at some point it might be possible to make it wasm-compatible @@ -27,8 +28,7 @@ network-defaults = { path = "../../network-defaults" } async-trait = { version = "0.1.51", optional = true } bip39 = { version = "1", features = ["rand"], optional = true } config = { path = "../../config", optional = true } -#cosmrs = { version = "0.3", features = ["rpc", "bip32", "cosmwasm"], optional = true } -cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev="e5a1872083abb3d88fa62dda966e7f5408deba58", features = ["rpc", "bip32", "cosmwasm"], optional = true } +cosmrs = { version = "0.4.1", features = ["rpc", "bip32", "cosmwasm"], optional = true } prost = { version = "0.9", default-features = false, optional = true } flate2 = { version = "1.0.20", optional = true } sha2 = { version = "0.9.5", optional = true } @@ -38,3 +38,4 @@ ts-rs = {version = "5.1", optional = true} [features] nymd-client = ["async-trait", "bip39", "config", "cosmrs", "prost", "flate2", "sha2", "itertools", "cosmwasm-std"] +typescript-types = ["ts-rs", "validator-api-requests/ts-rs"] diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 8ad6e843d7..2caf379fe6 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -6,21 +6,26 @@ use crate::nymd::{ error::NymdError, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient, }; #[cfg(feature = "nymd-client")] -use mixnet_contract::ContractStateParams; +use mixnet_contract_common::ContractStateParams; use crate::{validator_api, ValidatorClientError}; use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse}; #[cfg(feature = "nymd-client")] -use mixnet_contract::{ +use mixnet_contract_common::{ Delegation, MixnetContractVersion, MixnodeRewardingStatusResponse, RewardingIntervalResponse, }; -use mixnet_contract::{GatewayBond, MixNodeBond}; +use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond}; #[cfg(feature = "nymd-client")] use std::str::FromStr; use url::Url; +use validator_api_requests::models::{ + CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, + StakeSaturationResponse, +}; #[cfg(feature = "nymd-client")] +#[must_use] pub struct Config { api_url: Url, nymd_url: Url, @@ -208,7 +213,7 @@ impl Client { pub async fn get_rewarding_status( &self, - mix_identity: mixnet_contract::IdentityKey, + mix_identity: mixnet_contract_common::IdentityKey, rewarding_interval_nonce: u32, ) -> Result where @@ -297,8 +302,8 @@ impl Client { pub async fn get_all_nymd_single_mixnode_delegations( &self, - identity: mixnet_contract::IdentityKey, - ) -> Result, ValidatorClientError> + identity: mixnet_contract_common::IdentityKey, + ) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync, { @@ -420,6 +425,12 @@ impl ApiClient { Ok(self.validator_api.get_active_mixnodes().await?) } + pub async fn get_cached_rewarded_mixnodes( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_rewarded_mixnodes().await?) + } + pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { Ok(self.validator_api.get_mixnodes().await?) } @@ -428,6 +439,55 @@ impl ApiClient { Ok(self.validator_api.get_gateways().await?) } + pub async fn get_gateway_core_status_count( + &self, + identity: IdentityKeyRef<'_>, + since: Option, + ) -> Result { + Ok(self + .validator_api + .get_gateway_core_status_count(identity, since) + .await?) + } + + pub async fn get_mixnode_core_status_count( + &self, + identity: IdentityKeyRef<'_>, + since: Option, + ) -> Result { + Ok(self + .validator_api + .get_mixnode_core_status_count(identity, since) + .await?) + } + + pub async fn get_mixnode_status( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + Ok(self.validator_api.get_mixnode_status(identity).await?) + } + + pub async fn get_mixnode_reward_estimation( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + Ok(self + .validator_api + .get_mixnode_reward_estimation(identity) + .await?) + } + + pub async fn get_mixnode_stake_saturation( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + Ok(self + .validator_api + .get_mixnode_stake_saturation(identity) + .await?) + } + pub async fn blind_sign( &self, request_body: &BlindSignRequestBody, diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index 9ef577d97f..251501114d 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -9,6 +9,7 @@ pub mod validator_api; pub use crate::client::ApiClient; pub use crate::error::ValidatorClientError; +pub use validator_api_requests::*; #[cfg(feature = "nymd-client")] pub use client::{Client, Config}; diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs index 977b5a75f1..53067c6690 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs @@ -160,10 +160,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient { { let init_msg = cosmwasm::MsgInstantiateContract { sender: sender_address.clone(), - admin: options - .as_mut() - .map(|options| options.admin.take()) - .flatten(), + admin: options.as_mut().and_then(|options| options.admin.take()), code_id, // now this is a weird one. the protobuf files say this field is optional, // but if you omit it, the initialisation will fail CheckTx diff --git a/common/client-libs/validator-client/src/nymd/fee/helpers.rs b/common/client-libs/validator-client/src/nymd/fee/helpers.rs index cc16e21cb5..0ddbf15356 100644 --- a/common/client-libs/validator-client/src/nymd/fee/helpers.rs +++ b/common/client-libs/validator-client/src/nymd/fee/helpers.rs @@ -20,6 +20,7 @@ pub enum Operation { BondMixnodeOnBehalf, UnbondMixnode, UnbondMixnodeOnBehalf, + UpdateMixnodeConfig, DelegateToMixnode, DelegateToMixnodeOnBehalf, UndelegateFromMixnode, @@ -57,6 +58,7 @@ impl fmt::Display for Operation { Operation::BondMixnode => f.write_str("BondMixnode"), Operation::BondMixnodeOnBehalf => f.write_str("BondMixnodeOnBehalf"), Operation::UnbondMixnode => f.write_str("UnbondMixnode"), + Operation::UpdateMixnodeConfig => f.write_str("UpdateMixnodeConfig"), Operation::UnbondMixnodeOnBehalf => f.write_str("UnbondMixnodeOnBehalf"), Operation::BondGateway => f.write_str("BondGateway"), Operation::BondGatewayOnBehalf => f.write_str("BondGatewayOnBehalf"), @@ -94,6 +96,7 @@ impl Operation { Operation::BondMixnodeOnBehalf => 200_000u64.into(), Operation::UnbondMixnode => 175_000u64.into(), Operation::UnbondMixnodeOnBehalf => 175_000u64.into(), + Operation::UpdateMixnodeConfig => 175_000u64.into(), Operation::DelegateToMixnode => 175_000u64.into(), Operation::DelegateToMixnodeOnBehalf => 175_000u64.into(), Operation::UndelegateFromMixnode => 175_000u64.into(), diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 72f72a220f..dfaa1abbb8 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -13,7 +13,7 @@ use cosmrs::rpc::{Error as TendermintRpcError, HttpClientUrl}; use cosmwasm_std::{Coin, Uint128}; pub use fee::gas_price::GasPrice; use fee::helpers::Operation; -use mixnet_contract::{ +use mixnet_contract_common::{ ContractStateParams, Delegation, ExecuteMsg, Gateway, GatewayBond, GatewayOwnershipResponse, IdentityKey, LayerDistribution, MixNode, MixNodeBond, MixOwnershipResponse, MixnetContractVersion, MixnodeRewardingStatusResponse, PagedAllDelegationsResponse, @@ -289,7 +289,7 @@ impl NymdClient { pub async fn get_rewarding_status( &self, - mix_identity: mixnet_contract::IdentityKey, + mix_identity: mixnet_contract_common::IdentityKey, rewarding_interval_nonce: u32, ) -> Result where @@ -773,6 +773,31 @@ impl NymdClient { .await } + /// Update the configuration of a mixnode. Right now, only possible for profit margin. + pub async fn update_mixnode_config( + &self, + profit_margin_percent: u8, + ) -> Result + where + C: SigningCosmWasmClient + Sync, + { + let fee = self.operation_fee(Operation::UpdateMixnodeConfig); + + let req = ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + }; + self.client + .execute( + self.address(), + self.mixnet_contract_address()?, + &req, + fee, + "Updating mixnode configuration from rust!", + Vec::new(), + ) + .await + } + /// Delegates specified amount of stake to particular mixnode. pub async fn delegate_to_mixnode( &self, diff --git a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs index 558a5af2ba..8c862f0851 100644 --- a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs @@ -8,7 +8,7 @@ use crate::nymd::fee::helpers::Operation; use crate::nymd::{cosmwasm_coin_to_cosmos_coin, NymdClient}; use async_trait::async_trait; use cosmwasm_std::Coin; -use mixnet_contract::{Gateway, IdentityKey, IdentityKeyRef, MixNode}; +use mixnet_contract_common::{Gateway, IdentityKey, IdentityKeyRef, MixNode}; use vesting_contract::messages::ExecuteMsg as VestingExecuteMsg; #[async_trait] diff --git a/common/client-libs/validator-client/src/nymd/wallet/mod.rs b/common/client-libs/validator-client/src/nymd/wallet/mod.rs index 195ebc9239..8abbccb7a0 100644 --- a/common/client-libs/validator-client/src/nymd/wallet/mod.rs +++ b/common/client-libs/validator-client/src/nymd/wallet/mod.rs @@ -133,6 +133,7 @@ impl DirectSecp256k1HdWallet { } } +#[must_use] pub struct DirectSecp256k1HdWalletBuilder { /// The password to use when deriving a BIP39 seed from a mnemonic. bip39_password: String, diff --git a/common/client-libs/validator-client/src/validator_api/mod.rs b/common/client-libs/validator-client/src/validator_api/mod.rs index 8b993a5b7e..c30a76b174 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -1,16 +1,25 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::validator_api::error::ValidatorAPIError; +use crate::validator_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse}; -use mixnet_contract::{GatewayBond, MixNodeBond}; +use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use url::Url; +use validator_api_requests::models::{ + CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, + StakeSaturationResponse, +}; pub mod error; pub(crate) mod routes; type PathSegments<'a> = &'a [&'a str]; +type Params<'a, K, V> = &'a [(K, V)]; + +const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[]; pub struct Client { url: Url, @@ -30,24 +39,33 @@ impl Client { self.url = new_url } - async fn query_validator_api(&self, path: PathSegments<'_>) -> Result + async fn query_validator_api( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result where for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef, { - let url = create_api_url(&self.url, path); + let url = create_api_url(&self.url, path, params); Ok(self.reqwest_client.get(url).send().await?.json().await?) } - async fn post_validator_api( + async fn post_validator_api( &self, path: PathSegments<'_>, + params: Params<'_, K, V>, json_body: &B, ) -> Result where B: Serialize + ?Sized, for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef, { - let url = create_api_url(&self.url, path); + let url = create_api_url(&self.url, path, params); Ok(self .reqwest_client .post(url) @@ -59,18 +77,159 @@ impl Client { } pub async fn get_mixnodes(&self) -> Result, ValidatorAPIError> { - self.query_validator_api(&[routes::API_VERSION, routes::MIXNODES]) + self.query_validator_api(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) .await } pub async fn get_gateways(&self) -> Result, ValidatorAPIError> { - self.query_validator_api(&[routes::API_VERSION, routes::GATEWAYS]) + self.query_validator_api(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } pub async fn get_active_mixnodes(&self) -> Result, ValidatorAPIError> { - self.query_validator_api(&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE]) + self.query_validator_api( + &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], + NO_PARAMS, + ) + .await + } + + pub async fn get_rewarded_mixnodes(&self) -> Result, ValidatorAPIError> { + self.query_validator_api( + &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], + NO_PARAMS, + ) + .await + } + + pub async fn get_probs_mixnode_rewarded( + &self, + mixnode_id: &str, + ) -> Result, ValidatorAPIError> { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::MIXNODES, + routes::REWARDED, + routes::INCLUSION_CHANCE, + mixnode_id, + ], + NO_PARAMS, + ) + .await + } + + pub async fn get_gateway_core_status_count( + &self, + identity: IdentityKeyRef<'_>, + since: Option, + ) -> Result { + if let Some(since) = since { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS_ROUTES, + routes::GATEWAY, + identity, + CORE_STATUS_COUNT, + ], + &[(SINCE_ARG, since.to_string())], + ) .await + } else { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS_ROUTES, + routes::GATEWAY, + identity, + ], + NO_PARAMS, + ) + .await + } + } + + pub async fn get_mixnode_core_status_count( + &self, + identity: IdentityKeyRef<'_>, + since: Option, + ) -> Result { + if let Some(since) = since { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS_ROUTES, + routes::MIXNODE, + identity, + CORE_STATUS_COUNT, + ], + &[(SINCE_ARG, since.to_string())], + ) + .await + } else { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS_ROUTES, + routes::MIXNODE, + identity, + ], + NO_PARAMS, + ) + .await + } + } + + pub async fn get_mixnode_status( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS_ROUTES, + routes::MIXNODE, + identity, + routes::STATUS, + ], + NO_PARAMS, + ) + .await + } + + pub async fn get_mixnode_reward_estimation( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS_ROUTES, + routes::MIXNODE, + identity, + routes::REWARD_ESTIMATION, + ], + NO_PARAMS, + ) + .await + } + + pub async fn get_mixnode_stake_saturation( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS_ROUTES, + routes::MIXNODE, + identity, + routes::STAKE_SATURATION, + ], + NO_PARAMS, + ) + .await } pub async fn blind_sign( @@ -79,6 +238,7 @@ impl Client { ) -> Result { self.post_validator_api( &[routes::API_VERSION, routes::COCONUT_BLIND_SIGN], + NO_PARAMS, request_body, ) .await @@ -87,13 +247,20 @@ impl Client { pub async fn get_coconut_verification_key( &self, ) -> Result { - self.query_validator_api(&[routes::API_VERSION, routes::COCONUT_VERIFICATION_KEY]) - .await + self.query_validator_api( + &[routes::API_VERSION, routes::COCONUT_VERIFICATION_KEY], + NO_PARAMS, + ) + .await } } // utility function that should solve the double slash problem in validator API forever. -fn create_api_url(base: &Url, segments: PathSegments<'_>) -> Url { +fn create_api_url, V: AsRef>( + base: &Url, + segments: PathSegments<'_>, + params: Params<'_, K, V>, +) -> Url { let mut url = base.clone(); let mut path_segments = url .path_segments_mut() @@ -108,6 +275,10 @@ fn create_api_url(base: &Url, segments: PathSegments<'_>) -> Url { // and can be dropped drop(path_segments); + if !params.is_empty() { + url.query_pairs_mut().extend_pairs(params); + } + url } @@ -122,51 +293,66 @@ mod tests { // works with 1 segment assert_eq!( "http://foomp.com/foo", - create_api_url(&base_url, &["foo"]).as_str() + create_api_url(&base_url, &["foo"], NO_PARAMS).as_str() ); // works with 2 segments assert_eq!( "http://foomp.com/foo/bar", - create_api_url(&base_url, &["foo", "bar"]).as_str() + create_api_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str() ); // works with leading slash assert_eq!( "http://foomp.com/foo", - create_api_url(&base_url, &["/foo"]).as_str() + create_api_url(&base_url, &["/foo"], NO_PARAMS).as_str() ); assert_eq!( "http://foomp.com/foo/bar", - create_api_url(&base_url, &["/foo", "bar"]).as_str() + create_api_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str() ); assert_eq!( "http://foomp.com/foo/bar", - create_api_url(&base_url, &["foo", "/bar"]).as_str() + create_api_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str() ); // works with trailing slash assert_eq!( "http://foomp.com/foo", - create_api_url(&base_url, &["foo/"]).as_str() + create_api_url(&base_url, &["foo/"], NO_PARAMS).as_str() ); assert_eq!( "http://foomp.com/foo/bar", - create_api_url(&base_url, &["foo/", "bar"]).as_str() + create_api_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str() ); assert_eq!( "http://foomp.com/foo/bar", - create_api_url(&base_url, &["foo", "bar/"]).as_str() + create_api_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str() ); // works with both leading and trailing slash assert_eq!( "http://foomp.com/foo", - create_api_url(&base_url, &["/foo/"]).as_str() + create_api_url(&base_url, &["/foo/"], NO_PARAMS).as_str() ); assert_eq!( "http://foomp.com/foo/bar", - create_api_url(&base_url, &["/foo/", "/bar/"]).as_str() + create_api_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str() + ); + + // adds params + assert_eq!( + "http://foomp.com/foo/bar?foomp=baz", + create_api_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar?arg1=val1&arg2=val2", + create_api_url( + &base_url, + &["/foo/", "/bar/"], + &[("arg1", "val1"), ("arg2", "val2")] + ) + .as_str() ); } } diff --git a/common/client-libs/validator-client/src/validator_api/routes.rs b/common/client-libs/validator-client/src/validator_api/routes.rs index 4a22b48150..e7785d4ec5 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -8,6 +8,19 @@ pub const MIXNODES: &str = "mixnodes"; pub const GATEWAYS: &str = "gateways"; pub const ACTIVE: &str = "active"; +pub const REWARDED: &str = "rewarded"; -pub const COCONUT_BLIND_SIGN: &str = "blind_sign"; -pub const COCONUT_VERIFICATION_KEY: &str = "verification_key"; +pub const COCONUT_BLIND_SIGN: &str = "blind-sign"; +pub const COCONUT_VERIFICATION_KEY: &str = "verification-key"; + +pub const STATUS_ROUTES: &str = "status"; +pub const MIXNODE: &str = "mixnode"; +pub const GATEWAY: &str = "gateway"; + +pub const CORE_STATUS_COUNT: &str = "core-status-count"; +pub const SINCE_ARG: &str = "since"; + +pub const STATUS: &str = "status"; +pub const REWARD_ESTIMATION: &str = "reward-estimation"; +pub const STAKE_SATURATION: &str = "stake-saturation"; +pub const INCLUSION_CHANCE: &str = "inclusion-probability"; diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml new file mode 100644 index 0000000000..26c0b3d4af --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "contracts-common" +version = "0.1.0" +authors = ["Jędrzej Stuczyński "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cosmwasm-std = "1.0.0-beta3" diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/events.rs b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs new file mode 100644 index 0000000000..d9b5b363ca --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs @@ -0,0 +1,30 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Event; + +/// Looks up value of particular attribute in the provided event. If it fails to find it, +/// the function panics. +/// +/// # Arguments +/// +/// * `event`: event to search through. +/// * `key`: key associated with the particular attribute +pub fn must_find_attribute(event: &Event, key: &str) -> String { + may_find_attribute(event, key).unwrap() +} + +/// Looks up value of particular attribute in the provided event. Returns None if it does not exist. +/// +/// # Arguments +/// +/// * `event`: event to search through. +/// * `key`: key associated with the particular attribute +pub fn may_find_attribute(event: &Event, key: &str) -> Option { + for attr in &event.attributes { + if attr.key == key { + return Some(attr.value.clone()); + } + } + None +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs new file mode 100644 index 0000000000..58d367a2f5 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs @@ -0,0 +1,4 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod events; diff --git a/common/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml similarity index 77% rename from common/mixnet-contract/Cargo.toml rename to common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 6f17d0e879..ba0edd57b1 100644 --- a/common/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "mixnet-contract" +name = "mixnet-contract-common" version = "0.1.0" authors = ["Jędrzej Stuczyński "] edition = "2018" @@ -14,10 +14,12 @@ serde_repr = "0.1" schemars = "0.8" ts-rs = { version = "5.1", optional = true } thiserror = "1.0" -network-defaults = { path = "../network-defaults" } +network-defaults = { path = "../../network-defaults" } fixed = { version = "1.1", features = ["serde"] } az = "1.1" log = "0.4.14" +contracts-common = { path = "../contracts-common" } + [features] default = [] diff --git a/common/mixnet-contract/src/delegation.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs similarity index 100% rename from common/mixnet-contract/src/delegation.rs rename to common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs diff --git a/common/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs similarity index 100% rename from common/mixnet-contract/src/error.rs rename to common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs new file mode 100644 index 0000000000..d464e96acf --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -0,0 +1,353 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mixnode::NodeRewardResult; +use crate::{ContractStateParams, Delegation, IdentityKeyRef, Layer}; +use cosmwasm_std::{Addr, Coin, Event, Uint128}; + +pub use contracts_common::events::*; + +// event types +pub const DELEGATION_EVENT_TYPE: &str = "delegation"; +pub const UNDELEGATION_EVENT_TYPE: &str = "undelegation"; +pub const GATEWAY_BONDING_EVENT_TYPE: &str = "gateway_bonding"; +pub const GATEWAY_UNBONDING_EVENT_TYPE: &str = "gateway_unbonding"; +pub const MIXNODE_BONDING_EVENT_TYPE: &str = "mixnode_bonding"; +pub const MIXNODE_UNBONDING_EVENT_TYPE: &str = "mixnode_unbonding"; +pub const SETTINGS_UPDATE_EVENT_TYPE: &str = "settings_update"; +pub const BEGIN_REWARDING_EVENT_TYPE: &str = "begin_rewarding"; +pub const OPERATOR_REWARDING_EVENT_TYPE: &str = "mix_rewarding"; +pub const MIX_DELEGATORS_REWARDING_EVENT_TYPE: &str = "mix_delegators_rewarding"; +pub const FINISH_REWARDING_EVENT_TYPE: &str = "finish_rewarding"; + +// attributes that are used in multiple places +pub const OWNER_KEY: &str = "owner"; +pub const AMOUNT_KEY: &str = "amount"; +pub const PROXY_KEY: &str = "proxy"; + +// event-specific attributes + +// delegation/undelegation +pub const DELEGATOR_KEY: &str = "delegator"; +pub const DELEGATION_TARGET_KEY: &str = "delegation_target"; +pub const DELEGATION_HEIGHT_KEY: &str = "delegation_latest_block_height"; + +// bonding/unbonding +pub const NODE_IDENTITY_KEY: &str = "identity"; +pub const ASSIGNED_LAYER_KEY: &str = "assigned_layer"; + +// settings change +pub const OLD_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "old_minimum_mixnode_pledge"; +pub const OLD_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "old_minimum_gateway_pledge"; +pub const OLD_MIXNODE_REWARDED_SET_SIZE_KEY: &str = "old_mixnode_rewarded_set_size"; +pub const OLD_MIXNODE_ACTIVE_SET_SIZE_KEY: &str = "old_mixnode_active_set_size"; +pub const OLD_ACTIVE_SET_WORK_FACTOR_KEY: &str = "old_active_set_work_factor"; + +pub const NEW_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "new_minimum_mixnode_pledge"; +pub const NEW_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "new_minimum_gateway_pledge"; +pub const NEW_MIXNODE_REWARDED_SET_SIZE_KEY: &str = "new_mixnode_rewarded_set_size"; +pub const NEW_MIXNODE_ACTIVE_SET_SIZE_KEY: &str = "new_mixnode_active_set_size"; +pub const NEW_ACTIVE_SET_WORK_FACTOR_KEY: &str = "new_active_set_work_factor"; + +// rewarding +pub const REWARDING_INTERVAL_NONCE_KEY: &str = "rewarding_interval_nonce"; +pub const TOTAL_MIXNODE_REWARD_KEY: &str = "total_node_reward"; +pub const OPERATOR_REWARD_KEY: &str = "operator_reward"; +pub const LAMBDA_KEY: &str = "lambda"; +pub const SIGMA_KEY: &str = "sigma"; +pub const DISTRIBUTED_DELEGATION_REWARDS_KEY: &str = "distributed_delegation_rewards"; +pub const FURTHER_DELEGATIONS_TO_REWARD_KEY: &str = "further_delegations"; +pub const NO_REWARD_REASON_KEY: &str = "no_reward_reason"; +pub const BOND_NOT_FOUND_VALUE: &str = "bond_not_found"; +pub const BOND_TOO_FRESH_VALUE: &str = "bond_too_fresh"; +pub const ZERO_UPTIME_VALUE: &str = "zero_uptime"; + +pub fn new_delegation_event( + delegator: &Addr, + proxy: &Option, + amount: &Coin, + mix_identity: IdentityKeyRef, +) -> Event { + let mut event = Event::new(DELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); + + if let Some(proxy) = proxy { + event = event.add_attribute(PROXY_KEY, proxy) + } + + // coin implements Display trait and we use that implementation here + event + .add_attribute(AMOUNT_KEY, amount.to_string()) + .add_attribute(DELEGATION_TARGET_KEY, mix_identity) +} + +pub fn new_undelegation_event( + delegator: &Addr, + proxy: &Option, + old_delegation: &Delegation, + mix_identity: IdentityKeyRef, +) -> Event { + let mut event = Event::new(UNDELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); + + if let Some(proxy) = proxy { + event = event.add_attribute(PROXY_KEY, proxy) + } + + // coin implements Display trait and we use that implementation here + event + .add_attribute(AMOUNT_KEY, old_delegation.amount.to_string()) + .add_attribute( + DELEGATION_HEIGHT_KEY, + old_delegation.block_height.to_string(), + ) + .add_attribute(DELEGATION_TARGET_KEY, mix_identity) +} + +pub fn new_gateway_bonding_event( + owner: &Addr, + proxy: &Option, + amount: &Coin, + identity: IdentityKeyRef, +) -> Event { + let mut event = Event::new(GATEWAY_BONDING_EVENT_TYPE) + .add_attribute(OWNER_KEY, owner) + .add_attribute(NODE_IDENTITY_KEY, identity); + + if let Some(proxy) = proxy { + event = event.add_attribute(PROXY_KEY, proxy) + } + + // coin implements Display trait and we use that implementation here + event.add_attribute(AMOUNT_KEY, amount.to_string()) +} + +pub fn new_gateway_unbonding_event( + owner: &Addr, + proxy: &Option, + amount: &Coin, + identity: IdentityKeyRef, +) -> Event { + let mut event = Event::new(GATEWAY_UNBONDING_EVENT_TYPE) + .add_attribute(OWNER_KEY, owner) + .add_attribute(NODE_IDENTITY_KEY, identity); + + if let Some(proxy) = proxy { + event = event.add_attribute(PROXY_KEY, proxy) + } + + // coin implements Display trait and we use that implementation here + event.add_attribute(AMOUNT_KEY, amount.to_string()) +} + +pub fn new_mixnode_bonding_event( + owner: &Addr, + proxy: &Option, + amount: &Coin, + identity: IdentityKeyRef, + assigned_layer: Layer, +) -> Event { + let mut event = Event::new(MIXNODE_BONDING_EVENT_TYPE) + .add_attribute(OWNER_KEY, owner) + .add_attribute(NODE_IDENTITY_KEY, identity); + + if let Some(proxy) = proxy { + event = event.add_attribute(PROXY_KEY, proxy) + } + + // coin implements Display trait and we use that implementation here + event + .add_attribute(ASSIGNED_LAYER_KEY, assigned_layer) + .add_attribute(AMOUNT_KEY, amount.to_string()) +} + +pub fn new_mixnode_unbonding_event( + owner: &Addr, + proxy: &Option, + amount: &Coin, + identity: IdentityKeyRef, +) -> Event { + let mut event = Event::new(MIXNODE_UNBONDING_EVENT_TYPE) + .add_attribute(OWNER_KEY, owner) + .add_attribute(NODE_IDENTITY_KEY, identity); + + if let Some(proxy) = proxy { + event = event.add_attribute(PROXY_KEY, proxy) + } + + // coin implements Display trait and we use that implementation here + event.add_attribute(AMOUNT_KEY, amount.to_string()) +} + +pub fn new_settings_update_event( + old_params: &ContractStateParams, + new_params: &ContractStateParams, +) -> Event { + let mut event = Event::new(SETTINGS_UPDATE_EVENT_TYPE); + + if old_params.minimum_mixnode_pledge != new_params.minimum_mixnode_pledge { + event = event + .add_attribute( + OLD_MINIMUM_MIXNODE_PLEDGE_KEY, + old_params.minimum_mixnode_pledge, + ) + .add_attribute( + NEW_MINIMUM_MIXNODE_PLEDGE_KEY, + new_params.minimum_mixnode_pledge, + ) + } + + if old_params.minimum_gateway_pledge != new_params.minimum_gateway_pledge { + event = event + .add_attribute( + OLD_MINIMUM_GATEWAY_PLEDGE_KEY, + old_params.minimum_gateway_pledge, + ) + .add_attribute( + NEW_MINIMUM_GATEWAY_PLEDGE_KEY, + new_params.minimum_gateway_pledge, + ) + } + + if old_params.mixnode_rewarded_set_size != new_params.mixnode_rewarded_set_size { + event = event + .add_attribute( + OLD_MIXNODE_REWARDED_SET_SIZE_KEY, + old_params.mixnode_rewarded_set_size.to_string(), + ) + .add_attribute( + NEW_MIXNODE_REWARDED_SET_SIZE_KEY, + new_params.mixnode_rewarded_set_size.to_string(), + ) + } + + if old_params.mixnode_active_set_size != new_params.mixnode_active_set_size { + event = event + .add_attribute( + OLD_MIXNODE_ACTIVE_SET_SIZE_KEY, + old_params.mixnode_active_set_size.to_string(), + ) + .add_attribute( + NEW_MIXNODE_ACTIVE_SET_SIZE_KEY, + new_params.mixnode_active_set_size.to_string(), + ) + } + + if old_params.active_set_work_factor != new_params.active_set_work_factor { + event = event + .add_attribute( + OLD_ACTIVE_SET_WORK_FACTOR_KEY, + old_params.active_set_work_factor.to_string(), + ) + .add_attribute( + NEW_ACTIVE_SET_WORK_FACTOR_KEY, + new_params.active_set_work_factor.to_string(), + ) + } + + event +} + +pub fn new_begin_rewarding_event(rewarding_interval_nonce: u32) -> Event { + Event::new(BEGIN_REWARDING_EVENT_TYPE).add_attribute( + REWARDING_INTERVAL_NONCE_KEY, + rewarding_interval_nonce.to_string(), + ) +} + +pub fn new_finish_rewarding_event(rewarding_interval_nonce: u32) -> Event { + Event::new(FINISH_REWARDING_EVENT_TYPE).add_attribute( + REWARDING_INTERVAL_NONCE_KEY, + rewarding_interval_nonce.to_string(), + ) +} + +pub fn new_not_found_mix_operator_rewarding_event( + rewarding_interval_nonce: u32, + identity: IdentityKeyRef, +) -> Event { + Event::new(OPERATOR_REWARDING_EVENT_TYPE) + .add_attribute( + REWARDING_INTERVAL_NONCE_KEY, + rewarding_interval_nonce.to_string(), + ) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(NO_REWARD_REASON_KEY, BOND_NOT_FOUND_VALUE) +} + +pub fn new_too_fresh_bond_mix_operator_rewarding_event( + rewarding_interval_nonce: u32, + identity: IdentityKeyRef, +) -> Event { + Event::new(OPERATOR_REWARDING_EVENT_TYPE) + .add_attribute( + REWARDING_INTERVAL_NONCE_KEY, + rewarding_interval_nonce.to_string(), + ) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(NO_REWARD_REASON_KEY, BOND_TOO_FRESH_VALUE) +} + +pub fn new_zero_uptime_mix_operator_rewarding_event( + rewarding_interval_nonce: u32, + identity: IdentityKeyRef, +) -> Event { + Event::new(OPERATOR_REWARDING_EVENT_TYPE) + .add_attribute( + REWARDING_INTERVAL_NONCE_KEY, + rewarding_interval_nonce.to_string(), + ) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(NO_REWARD_REASON_KEY, ZERO_UPTIME_VALUE) +} + +pub fn new_mix_operator_rewarding_event( + rewarding_interval_nonce: u32, + identity: IdentityKeyRef, + node_reward_result: NodeRewardResult, + operator_reward: Uint128, + delegation_rewards_distributed: Uint128, + further_delegations: bool, +) -> Event { + Event::new(OPERATOR_REWARDING_EVENT_TYPE) + .add_attribute( + REWARDING_INTERVAL_NONCE_KEY, + rewarding_interval_nonce.to_string(), + ) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute( + TOTAL_MIXNODE_REWARD_KEY, + node_reward_result.reward().to_string(), + ) + .add_attribute(LAMBDA_KEY, node_reward_result.lambda().to_string()) + .add_attribute(SIGMA_KEY, node_reward_result.sigma().to_string()) + .add_attribute(OPERATOR_REWARD_KEY, operator_reward) + .add_attribute( + DISTRIBUTED_DELEGATION_REWARDS_KEY, + delegation_rewards_distributed, + ) + .add_attribute( + FURTHER_DELEGATIONS_TO_REWARD_KEY, + further_delegations.to_string(), + ) +} + +pub fn new_mix_delegators_rewarding_event( + rewarding_interval_nonce: u32, + identity: IdentityKeyRef, + delegation_rewards_distributed: Uint128, + further_delegations: bool, +) -> Event { + Event::new(MIX_DELEGATORS_REWARDING_EVENT_TYPE) + .add_attribute( + REWARDING_INTERVAL_NONCE_KEY, + rewarding_interval_nonce.to_string(), + ) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute( + DISTRIBUTED_DELEGATION_REWARDS_KEY, + delegation_rewards_distributed, + ) + .add_attribute( + FURTHER_DELEGATIONS_TO_REWARD_KEY, + further_delegations.to_string(), + ) +} diff --git a/common/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs similarity index 100% rename from common/mixnet-contract/src/gateway.rs rename to common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs diff --git a/common/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs similarity index 97% rename from common/mixnet-contract/src/lib.rs rename to common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index 813fa3b894..6f1854cc12 100644 --- a/common/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -3,6 +3,7 @@ mod delegation; pub mod error; +pub mod events; mod gateway; pub mod mixnode; mod msg; diff --git a/common/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs similarity index 97% rename from common/mixnet-contract/src/mixnode.rs rename to common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index e53af230ab..ff77ec628a 100644 --- a/common/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -53,6 +53,16 @@ pub enum Layer { Three = 3, } +impl From for String { + fn from(layer: Layer) -> Self { + if layer == Layer::Gateway { + "gateway".to_string() + } else { + (layer as u8).to_string() + } + } +} + #[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)] pub struct NodeRewardParams { period_reward_pool: Uint128, @@ -243,7 +253,7 @@ impl DelegatorRewardParams { } } -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct NodeRewardResult { reward: U128, lambda: U128, @@ -327,6 +337,11 @@ impl MixNodeBond { self.total_delegation.clone() } + pub fn stake_saturation(&self, circulating_supply: u128, rewarded_set_size: u32) -> U128 { + self.total_bond_to_circulating_supply(circulating_supply) + * U128::from_num(rewarded_set_size) + } + pub fn pledge_to_circulating_supply(&self, circulating_supply: u128) -> U128 { U128::from_num(self.pledge_amount().amount.u128()) / U128::from_num(circulating_supply) } diff --git a/common/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs similarity index 98% rename from common/mixnet-contract/src/msg.rs rename to common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index c744a4f56e..8f74824ec0 100644 --- a/common/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -20,6 +20,9 @@ pub enum ExecuteMsg { owner_signature: String, }, UnbondMixnode {}, + UpdateMixnodeConfig { + profit_margin_percent: u8, + }, BondGateway { gateway: Gateway, owner_signature: String, diff --git a/common/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs similarity index 100% rename from common/mixnet-contract/src/types.rs rename to common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs diff --git a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml new file mode 100644 index 0000000000..278244b8f0 --- /dev/null +++ b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "vesting-contract-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cosmwasm-std = "1.0.0-beta3" diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs new file mode 100644 index 0000000000..946383b7e4 --- /dev/null +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs @@ -0,0 +1,133 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Addr, Coin, Event, Timestamp}; + +// event types +pub const WITHDRAW_EVENT_TYPE: &str = "vested_coins_withdraw"; +pub const OWNERSHIP_TRANSFER_EVENT_TYPE: &str = "ownership_transfer"; +pub const STAKING_ADDRESS_UPDATE_EVENT_TYPE: &str = "staking_address_update"; +pub const NEW_PERIODIC_VESTING_ACCOUNT_EVENT_TYPE: &str = "new_periodic_vesting_account"; + +pub const VESTING_DELEGATION_EVENT_TYPE: &str = "vesting_delegation"; +pub const VESTING_UNDELEGATION_EVENT_TYPE: &str = "vesting_undelegation"; +pub const VESTING_GATEWAY_BONDING_EVENT_TYPE: &str = "vesting_gateway_bonding"; +pub const VESTING_GATEWAY_UNBONDING_EVENT_TYPE: &str = "vesting_gateway_unbonding"; +pub const VESTING_MIXNODE_BONDING_EVENT_TYPE: &str = "vesting_mixnode_bonding"; +pub const VESTING_MIXNODE_UNBONDING_EVENT_TYPE: &str = "vesting_mixnode_unbonding"; + +pub const TRACK_MIXNODE_UNBOND_EVENT_TYPE: &str = "track_mixnode_unbond"; +pub const TRACK_GATEWAY_UNBOND_EVENT_TYPE: &str = "track_gateway_unbond"; +pub const TRACK_UNDELEGATION_EVENT_TYPE: &str = "track_undelegation"; + +// attributes that are used in multiple places +pub const OWNER_KEY: &str = "owner"; +pub const AMOUNT_KEY: &str = "amount"; + +// event-specific attributes + +// withdraw +pub const REMAINING_SPENDABLE_KEY: &str = "remaining_spendable"; + +// ownership transfer +pub const FROM_ACCOUNT_KEY: &str = "from"; +pub const TO_ACCOUNT_KEY: &str = "to"; +pub const NO_VALUE_VALUE: &str = "none"; + +// new vesting account +pub const START_TIME_KEY: &str = "start_time"; +pub const STAKING_ADDRESS_KEY: &str = "staking_address"; + +// OPEN QUESTION: would it make sense to also emit amount of vesting/locked coins here? +// however, then it would require additional storage reads. +pub fn new_vested_coins_withdraw_event( + address: &Addr, + amount: &Coin, + remaining_spendable: &Coin, +) -> Event { + Event::new(WITHDRAW_EVENT_TYPE) + .add_attribute(OWNER_KEY, address) + .add_attribute(AMOUNT_KEY, amount.to_string()) + .add_attribute(REMAINING_SPENDABLE_KEY, remaining_spendable.to_string()) +} + +pub fn new_ownership_transfer_event(from: &Addr, to: &Addr) -> Event { + Event::new(OWNERSHIP_TRANSFER_EVENT_TYPE) + .add_attribute(FROM_ACCOUNT_KEY, from) + .add_attribute(TO_ACCOUNT_KEY, to) +} + +pub fn new_staking_address_update_event(from: &Option, to: &Option) -> Event { + let mut event = Event::new(OWNERSHIP_TRANSFER_EVENT_TYPE); + + if let Some(from) = from { + event = event.add_attribute(FROM_ACCOUNT_KEY, from) + } else { + event = event.add_attribute(FROM_ACCOUNT_KEY, NO_VALUE_VALUE); + } + + if let Some(to) = to { + event = event.add_attribute(TO_ACCOUNT_KEY, to) + } else { + event = event.add_attribute(TO_ACCOUNT_KEY, NO_VALUE_VALUE); + } + + event +} + +pub fn new_periodic_vesting_account_event( + owner_address: &Addr, + amount: &Coin, + staking_address: &Option, + start_time: Timestamp, +) -> Event { + let mut event = Event::new(NEW_PERIODIC_VESTING_ACCOUNT_EVENT_TYPE) + .add_attribute(OWNER_KEY, owner_address) + .add_attribute(AMOUNT_KEY, amount.to_string()); + + if let Some(staking_address) = staking_address { + event = event.add_attribute(STAKING_ADDRESS_KEY, staking_address); + } + + event.add_attribute(START_TIME_KEY, start_time.to_string()) +} + +// In most cases the events are rather barebone as there's no point in attaching +// bunch of data to them as it would be redundant. It is because in most cases when the event is emitted +// a call to the mixnet contract is made that throws another event with relevant attributes already attached. + +pub fn new_vesting_gateway_bonding_event() -> Event { + Event::new(VESTING_GATEWAY_BONDING_EVENT_TYPE) +} + +pub fn new_vesting_gateway_unbonding_event() -> Event { + Event::new(VESTING_GATEWAY_UNBONDING_EVENT_TYPE) +} + +pub fn new_vesting_mixnode_bonding_event() -> Event { + Event::new(VESTING_MIXNODE_BONDING_EVENT_TYPE) +} + +pub fn new_vesting_mixnode_unbonding_event() -> Event { + Event::new(VESTING_MIXNODE_UNBONDING_EVENT_TYPE) +} + +pub fn new_vesting_delegation_event() -> Event { + Event::new(VESTING_DELEGATION_EVENT_TYPE) +} + +pub fn new_vesting_undelegation_event() -> Event { + Event::new(VESTING_UNDELEGATION_EVENT_TYPE) +} + +pub fn new_track_mixnode_unbond_event() -> Event { + Event::new(TRACK_MIXNODE_UNBOND_EVENT_TYPE) +} + +pub fn new_track_gateway_unbond_event() -> Event { + Event::new(TRACK_GATEWAY_UNBOND_EVENT_TYPE) +} + +pub fn new_track_undelegation_event() -> Event { + Event::new(TRACK_UNDELEGATION_EVENT_TYPE) +} diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs new file mode 100644 index 0000000000..002130e5fd --- /dev/null +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -0,0 +1,4 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod events; diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index cf0032de13..f55a364a68 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" [dependencies] aes = { version = "0.7.4", features = ["ctr"] } bs58 = "0.4.0" -blake3 = { version = "1.0.0", features = ["traits-preview"] } +blake3 = { version = "~1.2.0", features = ["traits-preview"] } digest = "0.9.0" generic-array = "0.14" hkdf = "0.11.0" diff --git a/common/crypto/src/hkdf.rs b/common/crypto/src/hkdf.rs index 5300947448..141b5c607c 100644 --- a/common/crypto/src/hkdf.rs +++ b/common/crypto/src/hkdf.rs @@ -22,7 +22,7 @@ where let hkdf = Hkdf::::new(salt, ikm); let mut okm = vec![0u8; okm_length]; - hkdf.expand(info.unwrap_or_else(|| &[]), &mut okm)?; + hkdf.expand(info.unwrap_or(&[]), &mut okm)?; Ok(okm) } diff --git a/common/mixnode-common/src/verloc/mod.rs b/common/mixnode-common/src/verloc/mod.rs index 52060cb804..623bdc9e18 100644 --- a/common/mixnode-common/src/verloc/mod.rs +++ b/common/mixnode-common/src/verloc/mod.rs @@ -77,6 +77,7 @@ impl Config { } } +#[must_use] pub struct ConfigBuilder(Config); impl ConfigBuilder { diff --git a/common/network-defaults/Cargo.toml b/common/network-defaults/Cargo.toml index ba50e9c473..dfd640b08a 100644 --- a/common/network-defaults/Cargo.toml +++ b/common/network-defaults/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +cfg-if = "1.0.0" hex-literal = "0.3.3" serde = {version = "1.0", features = ["derive"]} url = "2.2" diff --git a/common/network-defaults/src/all.rs b/common/network-defaults/src/all.rs new file mode 100644 index 0000000000..e0b5f11127 --- /dev/null +++ b/common/network-defaults/src/all.rs @@ -0,0 +1,130 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::{milhon, qa, sandbox, ValidatorDetails}; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum Network { + MILHON, + QA, + SANDBOX, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NetworkDetails { + bech32_prefix: String, + denom: String, + mixnet_contract_address: String, + vesting_contract_address: String, + bandwidth_claim_contract_address: String, + rewarding_validator_address: String, + validators: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SupportedNetworks { + networks: HashMap, +} + +impl SupportedNetworks { + pub fn new(support: Vec) -> Self { + let mut networks = HashMap::new(); + + for network in support { + match network { + Network::MILHON => networks.insert( + Network::MILHON, + NetworkDetails { + bech32_prefix: String::from(milhon::BECH32_PREFIX), + denom: String::from(milhon::DENOM), + mixnet_contract_address: String::from(milhon::MIXNET_CONTRACT_ADDRESS), + vesting_contract_address: String::from(milhon::VESTING_CONTRACT_ADDRESS), + bandwidth_claim_contract_address: String::from( + milhon::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + ), + rewarding_validator_address: String::from( + milhon::REWARDING_VALIDATOR_ADDRESS, + ), + validators: milhon::validators(), + }, + ), + Network::QA => networks.insert( + Network::QA, + NetworkDetails { + bech32_prefix: String::from(qa::BECH32_PREFIX), + denom: String::from(qa::DENOM), + mixnet_contract_address: String::from(qa::MIXNET_CONTRACT_ADDRESS), + vesting_contract_address: String::from(qa::VESTING_CONTRACT_ADDRESS), + bandwidth_claim_contract_address: String::from( + qa::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + ), + rewarding_validator_address: String::from(qa::REWARDING_VALIDATOR_ADDRESS), + validators: qa::validators(), + }, + ), + Network::SANDBOX => networks.insert( + Network::SANDBOX, + NetworkDetails { + bech32_prefix: String::from(sandbox::BECH32_PREFIX), + denom: String::from(sandbox::DENOM), + mixnet_contract_address: String::from(sandbox::MIXNET_CONTRACT_ADDRESS), + vesting_contract_address: String::from(sandbox::VESTING_CONTRACT_ADDRESS), + bandwidth_claim_contract_address: String::from( + sandbox::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + ), + rewarding_validator_address: String::from( + sandbox::REWARDING_VALIDATOR_ADDRESS, + ), + validators: sandbox::validators(), + }, + ), + }; + } + SupportedNetworks { networks } + } + + pub fn bech32_prefix(&self, network: Network) -> Option<&str> { + self.networks + .get(&network) + .map(|network_details| network_details.bech32_prefix.as_str()) + } + + pub fn denom(&self, network: Network) -> Option<&str> { + self.networks + .get(&network) + .map(|network_details| network_details.denom.as_str()) + } + + pub fn mixnet_contract_address(&self, network: Network) -> Option<&str> { + self.networks + .get(&network) + .map(|network_details| network_details.mixnet_contract_address.as_str()) + } + + pub fn vesting_contract_address(&self, network: Network) -> Option<&str> { + self.networks + .get(&network) + .map(|network_details| network_details.vesting_contract_address.as_str()) + } + + pub fn bandwidth_claim_contract_address(&self, network: Network) -> Option<&str> { + self.networks + .get(&network) + .map(|network_details| network_details.bandwidth_claim_contract_address.as_str()) + } + + pub fn rewarding_validator_address(&self, network: Network) -> Option<&str> { + self.networks + .get(&network) + .map(|network_details| network_details.rewarding_validator_address.as_str()) + } + + pub fn validators(&self, network: Network) -> Option<&Vec> { + self.networks + .get(&network) + .map(|network_details| &network_details.validators) + } +} diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index a7f7f2003b..c3f1638b24 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -5,16 +5,63 @@ use std::time::Duration; use time::OffsetDateTime; use url::Url; +pub mod all; pub mod eth_contract; -#[cfg(network = "milhon")] -pub mod milhon; -#[cfg(network = "sandbox")] -pub mod sandbox; +mod milhon; +mod qa; +mod sandbox; -#[cfg(network = "milhon")] -pub use milhon::*; -#[cfg(network = "sandbox")] -pub use sandbox::*; +cfg_if::cfg_if! { + if #[cfg(network = "milhon")] { + pub const BECH32_PREFIX: &str = milhon::BECH32_PREFIX; + pub const DENOM: &str = milhon::DENOM; + + pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = milhon::MIXNET_CONTRACT_ADDRESS; + pub const DEFAULT_VESTING_CONTRACT_ADDRESS: &str = milhon::VESTING_CONTRACT_ADDRESS; + pub const DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = milhon::BANDWIDTH_CLAIM_CONTRACT_ADDRESS; + pub const DEFAULT_REWARDING_VALIDATOR_ADDRESS: &str = milhon::REWARDING_VALIDATOR_ADDRESS; + + pub fn default_validators() -> Vec { + milhon::validators() + } + + pub fn default_network() -> all::Network { + all::Network::MILHON + } + } else if #[cfg(network = "qa")] { + pub const BECH32_PREFIX: &str = qa::BECH32_PREFIX; + pub const DENOM: &str = qa::DENOM; + + pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = qa::MIXNET_CONTRACT_ADDRESS; + pub const DEFAULT_VESTING_CONTRACT_ADDRESS: &str = qa::VESTING_CONTRACT_ADDRESS; + pub const DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = qa::BANDWIDTH_CLAIM_CONTRACT_ADDRESS; + pub const DEFAULT_REWARDING_VALIDATOR: &str = qa::REWARDING_VALIDATOR_ADDRESS; + + pub fn default_validators() -> Vec { + qa::validators() + } + + pub fn default_network() -> all::Network { + all::Network::QA + } + } else if #[cfg(network = "sandbox")] { + pub const BECH32_PREFIX: &str = sandbox::BECH32_PREFIX; + pub const DENOM: &str = sandbox::DENOM; + + pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = sandbox::MIXNET_CONTRACT_ADDRESS; + pub const DEFAULT_VESTING_CONTRACT_ADDRESS: &str = sandbox::VESTING_CONTRACT_ADDRESS; + pub const DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = sandbox::BANDWIDTH_CLAIM_CONTRACT_ADDRESS; + pub const DEFAULT_REWARDING_VALIDATOR: &str = sandbox::REWARDING_VALIDATOR_ADDRESS; + + pub fn default_validators() -> Vec { + sandbox::validators() + } + + pub fn default_network() -> all::Network { + all::Network::SANDBOX + } + } +} #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ValidatorDetails { @@ -47,25 +94,6 @@ impl ValidatorDetails { } } -#[cfg(network = "milhon")] -pub fn default_validators() -> Vec { - vec![ - ValidatorDetails::new( - "https://testnet-milhon-validator1.nymtech.net", - Some("https://testnet-milhon-validator1.nymtech.net/api"), - ), - ValidatorDetails::new("https://testnet-milhon-validator2.nymtech.net", None), - ] -} - -#[cfg(network = "sandbox")] -pub fn default_validators() -> Vec { - vec![ValidatorDetails::new( - "https://sandbox-validator.nymtech.net", - Some("https://sandbox-validator.nymtech.net/api"), - )] -} - pub fn default_nymd_endpoints() -> Vec { default_validators() .iter() @@ -80,6 +108,13 @@ pub fn default_api_endpoints() -> Vec { .collect() } +pub const ETH_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("9fEE3e28c17dbB87310A51F13C4fbf4331A6f102"); +// Name of the event triggered by the eth contract. If the event name is changed, +// this would also need to be changed; It is currently tested against the json abi +pub const ETH_EVENT_NAME: &str = "Burned"; +pub const ETH_BURN_FUNCTION_NAME: &str = "burnTokenForAccessCode"; + // Ethereum constants used for token bridge /// How much bandwidth (in bytes) one token can buy const BYTES_PER_TOKEN: u64 = 1024 * 1024 * 1024; diff --git a/common/network-defaults/src/milhon.rs b/common/network-defaults/src/milhon.rs index 28e28588a4..a4e4f3e640 100644 --- a/common/network-defaults/src/milhon.rs +++ b/common/network-defaults/src/milhon.rs @@ -1,17 +1,23 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub const BECH32_PREFIX: &str = "punk"; -pub const DENOM: &str = "upunk"; +use crate::ValidatorDetails; -pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = "punk10pyejy66429refv3g35g2t7am0was7yalwrzen"; -pub const DEFAULT_VESTING_CONTRACT_ADDRESS: &str = ""; -pub const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = "punk1jld76tqw4wnpfenmay2xkv86nr3j0w426eka82"; -pub const REWARDING_VALIDATOR_ADDRESS: &str = "punk1v9qauwdq5terag6uvfsdytcs2d0sdmfdy7hgk3"; -pub const ETH_CONTRACT_ADDRESS: [u8; 20] = - hex_literal::hex!("9fEE3e28c17dbB87310A51F13C4fbf4331A6f102"); +pub(crate) const BECH32_PREFIX: &str = "punk"; +pub(crate) const DENOM: &str = "upunk"; -// Name of the event triggered by the eth contract. If the event name is changed, -// this would also need to be changed; It is currently tested against the json abi -pub const ETH_EVENT_NAME: &str = "Burned"; -pub const ETH_BURN_FUNCTION_NAME: &str = "burnTokenForAccessCode"; +pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "punk10pyejy66429refv3g35g2t7am0was7yalwrzen"; +pub(crate) const VESTING_CONTRACT_ADDRESS: &str = ""; +pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = + "punk1jld76tqw4wnpfenmay2xkv86nr3j0w426eka82"; +pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "punk1v9qauwdq5terag6uvfsdytcs2d0sdmfdy7hgk3"; + +pub(crate) fn validators() -> Vec { + vec![ + ValidatorDetails::new( + "https://testnet-milhon-validator1.nymtech.net", + Some("https://testnet-milhon-validator1.nymtech.net/api"), + ), + ValidatorDetails::new("https://testnet-milhon-validator2.nymtech.net", None), + ] +} diff --git a/common/network-defaults/src/qa.rs b/common/network-defaults/src/qa.rs new file mode 100644 index 0000000000..1e5fd029ae --- /dev/null +++ b/common/network-defaults/src/qa.rs @@ -0,0 +1,20 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ValidatorDetails; + +pub(crate) const BECH32_PREFIX: &str = "nymt"; +pub(crate) const DENOM: &str = "unymt"; + +pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt14hj2tavq8fpesdwxxcu44rty3hh90vhuysqrsr"; +pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv"; +pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = + "nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv"; +pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "nymt1dn52nx8wv9wkqmrvj6tcmdzh4es6jt8tr7f6j9"; + +pub(crate) fn validators() -> Vec { + vec![ValidatorDetails::new( + "https://qa-validator.nymtech.net", + Some("https://qa-validator.nymtech.net/api"), + )] +} diff --git a/common/network-defaults/src/sandbox.rs b/common/network-defaults/src/sandbox.rs index 951b1a7f39..81b766882e 100644 --- a/common/network-defaults/src/sandbox.rs +++ b/common/network-defaults/src/sandbox.rs @@ -1,17 +1,20 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub const BECH32_PREFIX: &str = "nymt"; -pub const DENOM: &str = "unymt"; +use crate::ValidatorDetails; -pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = "nymt1ghd753shjuwexxywmgs4xz7x2q732vcnstz02j"; -pub const DEFAULT_VESTING_CONTRACT_ADDRESS: &str = "nymt1nc5tatafv6eyq7llkr2gv50ff9e22mnfp9pc5s"; -pub const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = "nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv"; -pub const REWARDING_VALIDATOR_ADDRESS: &str = "nymt17zujduc46wvkwvp6f062mm5xhr7jc3fewvqu9e"; -pub const ETH_CONTRACT_ADDRESS: [u8; 20] = - hex_literal::hex!("9fEE3e28c17dbB87310A51F13C4fbf4331A6f102"); +pub(crate) const BECH32_PREFIX: &str = "nymt"; +pub(crate) const DENOM: &str = "unymt"; -// Name of the event triggered by the eth contract. If the event name is changed, -// this would also need to be changed; It is currently tested against the json abi -pub const ETH_EVENT_NAME: &str = "Burned"; -pub const ETH_BURN_FUNCTION_NAME: &str = "burnTokenForAccessCode"; +pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt1ghd753shjuwexxywmgs4xz7x2q732vcnstz02j"; +pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt1nc5tatafv6eyq7llkr2gv50ff9e22mnfp9pc5s"; +pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = + "nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv"; +pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "nymt17zujduc46wvkwvp6f062mm5xhr7jc3fewvqu9e"; + +pub(crate) fn validators() -> Vec { + vec![ValidatorDetails::new( + "https://sandbox-validator.nymtech.net", + Some("https://sandbox-validator.nymtech.net/api"), + )] +} diff --git a/common/nymsphinx/Cargo.toml b/common/nymsphinx/Cargo.toml index 4c2feb78a3..ce94c101b0 100644 --- a/common/nymsphinx/Cargo.toml +++ b/common/nymsphinx/Cargo.toml @@ -25,7 +25,7 @@ crypto = { path = "../crypto" } topology = { path = "../topology" } [dev-dependencies] -mixnet-contract = { path = "../mixnet-contract" } +mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } # do not include this when compiling into wasm as it somehow when combined together with reqwest, it will require # net2 via tokio-util -> tokio -> mio -> net2 diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index 06ead921fe..3f56f9bada 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -54,6 +54,7 @@ impl From for PreparationError { /// an optional reply-SURB, padding it to appropriate length, encrypting its content, /// and chunking into appropriate size [`Fragment`]s. #[cfg_attr(not(target_arch = "wasm32"), derive(Clone))] +#[must_use] pub struct MessagePreparer { /// Instance of a cryptographically secure random number generator. rng: R, @@ -386,13 +387,7 @@ where // (note: surb_ack_bytes contains SURB_ACK_FIRST_HOP || SURB_ACK_DATA ) let packet_payload: Vec<_> = surb_ack_bytes .into_iter() - .chain( - reply_surb - .encryption_key() - .compute_digest() - .to_vec() - .into_iter(), - ) + .chain(reply_surb.encryption_key().compute_digest().iter().copied()) .chain(reply_content.into_iter()) .collect(); diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index c3e9aebd9d..1707983ca3 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -58,6 +58,7 @@ impl MessageReceiver { } /// Allows setting non-default number of expected mix hops in the network. + #[must_use] pub fn with_mix_hops(mut self, hops: u8) -> Self { self.num_mix_hops = hops; self @@ -186,7 +187,7 @@ impl Default for MessageReceiver { mod message_receiver { use super::*; use crypto::asymmetric::identity; - use mixnet_contract::Layer; + use mixnet_contract_common::Layer; use nymsphinx_addressing::clients::Recipient; use rand::rngs::OsRng; use std::collections::HashMap; diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index 199505b83f..7d7bb7ecc3 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -13,7 +13,7 @@ rand = { version = "0.7.3", features = ["wasm-bindgen"] } ## internal crypto = { path = "../crypto" } -mixnet-contract = { path = "../mixnet-contract" } +mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } nymsphinx-addressing = { path = "../nymsphinx/addressing" } nymsphinx-types = { path = "../nymsphinx/types" } version-checker = { path = "../version-checker" } diff --git a/common/topology/src/filter.rs b/common/topology/src/filter.rs index 652b5f92e9..6c1a3c2fe1 100644 --- a/common/topology/src/filter.rs +++ b/common/topology/src/filter.rs @@ -9,6 +9,7 @@ pub trait Versioned: Clone { } pub trait VersionFilterable { + #[must_use] fn filter_by_version(&self, expected_version: &str) -> Self; } diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index a0c068bfd6..70284f1c3e 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -3,7 +3,7 @@ use crate::{filter, NetworkAddress}; use crypto::asymmetric::{encryption, identity}; -use mixnet_contract::GatewayBond; +use mixnet_contract_common::GatewayBond; use nymsphinx_addressing::nodes::{NodeIdentity, NymNodeRoutingAddress}; use nymsphinx_types::Node as SphinxNode; use std::convert::{TryFrom, TryInto}; diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index eb32901f18..7d5b9843d4 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -3,7 +3,7 @@ use crate::filter::VersionFilterable; use log::warn; -use mixnet_contract::{GatewayBond, MixNodeBond}; +use mixnet_contract_common::{GatewayBond, MixNodeBond}; use nymsphinx_addressing::nodes::NodeIdentity; use nymsphinx_types::Node as SphinxNode; use rand::Rng; @@ -206,10 +206,12 @@ impl NymTopology { true } + #[must_use] pub fn filter_system_version(&self, expected_version: &str) -> Self { self.filter_node_versions(expected_version, expected_version) } + #[must_use] pub fn filter_node_versions( &self, expected_mix_version: &str, @@ -272,7 +274,7 @@ mod converting_mixes_to_vec { use crypto::asymmetric::{encryption, identity}; use super::*; - use mixnet_contract::Layer; + use mixnet_contract_common::Layer; #[test] fn returns_a_vec_with_hashmap_values() { diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 2f2faf5a4d..f28c9175b1 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -3,7 +3,7 @@ use crate::{filter, NetworkAddress}; use crypto::asymmetric::{encryption, identity}; -use mixnet_contract::{Layer, MixNodeBond}; +use mixnet_contract_common::{Layer, MixNodeBond}; use nymsphinx_addressing::nodes::NymNodeRoutingAddress; use nymsphinx_types::Node as SphinxNode; use std::convert::{TryFrom, TryInto}; diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 5874784f8e..e0ad7ddf60 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -236,11 +236,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "contracts-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", +] + [[package]] name = "cosmwasm-crypto" -version = "1.0.0-beta3" +version = "1.0.0-beta4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a380b87642204557629c9b72988c47b55fbfe6d474960adba56b22331504956a" +checksum = "f903ebbabc0d4880dbc76148efb8be8fc29fa4bf294c440c3d70da1c8bcafff7" dependencies = [ "digest 0.9.0", "ed25519-zebra", @@ -251,9 +258,9 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.0.0-beta3" +version = "1.0.0-beta4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "866713b2fe13f23038c7d8824c3059d1f28dd94685fb406d1533c4eeeefeefae" +checksum = "832bebef577ecb394603de8e2bf0de429b74aa364e17dec18e15ce37e71b0cae" dependencies = [ "syn", ] @@ -270,9 +277,9 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.0.0-beta3" +version = "1.0.0-beta4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dbb9939b31441dfa9af3ec9740c8a24d585688401eff1b6b386abb7ad0d10a8" +checksum = "6238c45840cc9de5a39f0f619e3a4f7c38c5d2c6ac9e3e4d72751ee045e6d7da" dependencies = [ "base64", "cosmwasm-crypto", @@ -286,9 +293,9 @@ dependencies = [ [[package]] name = "cosmwasm-storage" -version = "1.0.0-beta3" +version = "1.0.0-beta4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a4e55f0d64fed54cd2202301b8d466af8de044589247dabd77a4222f52f749" +checksum = "f472c0bfd584a4510702537d0b65c5331095e76099586a12f749fd69afda9c5e" dependencies = [ "cosmwasm-std", "serde", @@ -326,7 +333,7 @@ dependencies = [ "log", "nymsphinx-types", "pemstore", - "rand 0.7.3", + "rand", "subtle-encoding", "x25519-dalek", ] @@ -387,9 +394,9 @@ dependencies = [ [[package]] name = "cw-storage-plus" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b8b840947313c1a1cccf056836cd79a60b4526bdcd6582995be37dc97be4ae" +checksum = "7d7ee1963302b0ac2a9d42fe0faec826209c17452bfd36fbfd9d002a88929261" dependencies = [ "cosmwasm-std", "schemars", @@ -458,7 +465,7 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek", "ed25519", - "rand 0.7.3", + "rand", "serde", "sha2", "zeroize", @@ -592,10 +599,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.10.2+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -833,8 +838,31 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "mixnet-contract" version = "0.1.0" +dependencies = [ + "bs58", + "config", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "crypto", + "cw-storage-plus", + "fixed", + "mixnet-contract-common", + "rand", + "rand_chacha", + "schemars", + "serde", + "thiserror", + "vergen", + "vesting-contract", +] + +[[package]] +name = "mixnet-contract-common" +version = "0.1.0" dependencies = [ "az", + "contracts-common", "cosmwasm-std", "fixed", "log", @@ -845,32 +873,11 @@ dependencies = [ "thiserror", ] -[[package]] -name = "mixnet-contracts" -version = "0.1.0" -dependencies = [ - "bs58", - "config", - "cosmwasm-schema", - "cosmwasm-std", - "cosmwasm-storage", - "crypto", - "cw-storage-plus", - "fixed", - "mixnet-contract", - "rand 0.7.3", - "rand_chacha 0.2.2", - "schemars", - "serde", - "thiserror", - "vergen", - "vesting-contract", -] - [[package]] name = "network-defaults" version = "0.1.0" dependencies = [ + "cfg-if", "hex-literal", "serde", "time 0.3.5", @@ -1067,21 +1074,9 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha 0.2.2", + "rand_chacha", "rand_core 0.5.1", - "rand_hc 0.2.0", -] - -[[package]] -name = "rand" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.3", - "rand_hc 0.3.1", + "rand_hc", ] [[package]] @@ -1094,16 +1089,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.3", -] - [[package]] name = "rand_core" version = "0.5.1" @@ -1129,7 +1114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56" dependencies = [ "num-traits", - "rand 0.7.3", + "rand", ] [[package]] @@ -1141,15 +1126,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core 0.6.3", -] - [[package]] name = "regex" version = "1.5.4" @@ -1227,9 +1203,9 @@ dependencies = [ [[package]] name = "serde-json-wasm" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50eef3672ec8fa45f3457fd423ba131117786784a895548021976117c1ded449" +checksum = "042ac496d97e5885149d34139bad1d617192770d7eb8f1866da2317ff4501853" dependencies = [ "serde", ] @@ -1330,7 +1306,7 @@ dependencies = [ "hmac", "lioness", "log", - "rand 0.7.3", + "rand", "rand_distr", "sha2", "subtle 2.4.1", @@ -1558,12 +1534,18 @@ dependencies = [ "config", "cosmwasm-std", "cw-storage-plus", - "getrandom 0.2.3", - "mixnet-contract", - "rand 0.8.4", + "mixnet-contract-common", "schemars", "serde", "thiserror", + "vesting-contract-common", +] + +[[package]] +name = "vesting-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", ] [[package]] diff --git a/contracts/basic-bandwidth-generation/package-lock.json b/contracts/basic-bandwidth-generation/package-lock.json index d05d4eca51..9e252c5408 100644 --- a/contracts/basic-bandwidth-generation/package-lock.json +++ b/contracts/basic-bandwidth-generation/package-lock.json @@ -6524,9 +6524,9 @@ } }, "follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", "dev": true }, "for-each": { @@ -19345,9 +19345,9 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "shelljs": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", - "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", "dev": true, "requires": { "glob": "^7.0.0", diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 25de04f9db..d1ca3066fe 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "mixnet-contracts" +name = "mixnet-contract" version = "0.1.0" authors = ["Dave Hrycyszyn "] edition = "2018" @@ -16,13 +16,13 @@ exclude = [ crate-type = ["cdylib", "rlib"] [dependencies] -mixnet-contract = { path = "../../common/mixnet-contract" } +mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } config = { path = "../../common/config"} vesting-contract = { path = "../vesting" } -cosmwasm-std = "1.0.0-beta3" -cosmwasm-storage = "1.0.0-beta3" -cw-storage-plus = "0.10.3" +cosmwasm-std = "1.0.0-beta4" +cosmwasm-storage = "1.0.0-beta4" +cw-storage-plus = "0.11.1" bs58 = "0.4.0" schemars = "0.8" diff --git a/contracts/mixnet/examples/schema.rs b/contracts/mixnet/examples/schema.rs index 0059dfb5a4..8f390777ca 100644 --- a/contracts/mixnet/examples/schema.rs +++ b/contracts/mixnet/examples/schema.rs @@ -4,7 +4,7 @@ extern crate mixnet_contract; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; -use mixnet_contract::{ExecuteMsg, InstantiateMsg, MixNodeBond, QueryMsg}; +use mixnet_contract_common::{ExecuteMsg, InstantiateMsg, MixNodeBond, QueryMsg}; use std::env::current_dir; use std::fs::create_dir_all; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index d46c7aa3c7..420a011ae0 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -23,7 +23,9 @@ use crate::rewards::storage as rewards_storage; use cosmwasm_std::{ entry_point, to_binary, Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Uint128, }; -use mixnet_contract::{ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use mixnet_contract_common::{ + ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, +}; /// Constant specifying minimum of coin required to bond a gateway pub const INITIAL_GATEWAY_PLEDGE: Uint128 = Uint128::new(100_000_000); @@ -104,6 +106,14 @@ pub fn execute( ExecuteMsg::UnbondMixnode {} => { crate::mixnodes::transactions::try_remove_mixnode(deps, info) } + ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + } => crate::mixnodes::transactions::try_update_mixnode_config( + deps, + env, + info, + profit_margin_percent, + ), ExecuteMsg::BondGateway { gateway, owner_signature, @@ -292,14 +302,14 @@ pub mod tests { use config::defaults::DENOM; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{coins, from_binary}; - use mixnet_contract::PagedMixnodeResponse; + use mixnet_contract_common::PagedMixnodeResponse; #[test] fn initialize_contract() { let mut deps = mock_dependencies(); let env = mock_env(); let msg = InstantiateMsg { - rewarding_validator_address: config::defaults::REWARDING_VALIDATOR_ADDRESS.to_string(), + rewarding_validator_address: config::defaults::DEFAULT_REWARDING_VALIDATOR.to_string(), }; let info = mock_info("creator", &[]); diff --git a/contracts/mixnet/src/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index 4bc9ceed71..258f1d548d 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -7,10 +7,10 @@ use cosmwasm_std::Deps; use cosmwasm_std::Order; use cosmwasm_std::StdResult; use cw_storage_plus::{Bound, PrimaryKey}; -use mixnet_contract::PagedAllDelegationsResponse; -use mixnet_contract::PagedDelegatorDelegationsResponse; -use mixnet_contract::PagedMixDelegationsResponse; -use mixnet_contract::{Delegation, IdentityKey}; +use mixnet_contract_common::{ + Delegation, IdentityKey, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedMixDelegationsResponse, +}; pub(crate) fn query_all_network_delegations_paged( deps: Deps, @@ -140,7 +140,7 @@ pub(crate) mod tests { #[cfg(test)] mod querying_for_mixnode_delegations_paged { use super::*; - use mixnet_contract::IdentityKey; + use mixnet_contract_common::IdentityKey; #[test] fn retrieval_obeys_limits() { @@ -281,7 +281,7 @@ pub(crate) mod tests { mod querying_for_all_mixnode_delegations_paged { use super::*; use crate::support::tests::test_helpers; - use mixnet_contract::IdentityKey; + use mixnet_contract_common::IdentityKey; #[test] fn retrieval_obeys_limits() { @@ -444,7 +444,7 @@ pub(crate) mod tests { // add delegation from a different address let delegation = Delegation::new( - delegation_owner2.clone(), + delegation_owner2, node_identity1.clone(), coin(1234, DENOM), 1234, @@ -474,7 +474,7 @@ pub(crate) mod tests { // add delegation for a different node let delegation = Delegation::new( delegation_owner1.clone(), - node_identity2.clone(), + node_identity2, coin(1234, DENOM), 1234, None, @@ -493,11 +493,7 @@ pub(crate) mod tests { identity: node_identity1.clone(), address: Addr::unchecked(delegation_owner1.clone()) }), - query_mixnode_delegation( - deps.as_ref(), - node_identity1.clone(), - delegation_owner1.to_string() - ) + query_mixnode_delegation(deps.as_ref(), node_identity1, delegation_owner1.to_string()) ) } diff --git a/contracts/mixnet/src/delegations/storage.rs b/contracts/mixnet/src/delegations/storage.rs index 46fd0cc7fd..11316efd79 100644 --- a/contracts/mixnet/src/delegations/storage.rs +++ b/contracts/mixnet/src/delegations/storage.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex}; -use mixnet_contract::{Addr, Delegation, IdentityKey}; +use mixnet_contract_common::{Addr, Delegation, IdentityKey}; // storage prefixes const DELEGATION_PK_NAMESPACE: &str = "dl"; @@ -17,9 +17,9 @@ pub(crate) const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 250; type PrimaryKey = Vec; pub(crate) struct DelegationIndex<'a> { - pub(crate) owner: MultiIndex<'a, (Addr, PrimaryKey), Delegation>, + pub(crate) owner: MultiIndex<'a, Addr, Delegation>, - pub(crate) mixnode: MultiIndex<'a, (IdentityKey, PrimaryKey), Delegation>, + pub(crate) mixnode: MultiIndex<'a, IdentityKey, Delegation>, } impl<'a> IndexList for DelegationIndex<'a> { @@ -46,12 +46,12 @@ impl<'a> IndexList for DelegationIndex<'a> { pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, DelegationIndex<'a>> { let indexes = DelegationIndex { owner: MultiIndex::new( - |d, pk| (d.owner.clone(), pk), + |d| d.owner.clone(), DELEGATION_PK_NAMESPACE, DELEGATION_OWNER_IDX_NAMESPACE, ), mixnode: MultiIndex::new( - |d, pk| (d.node_identity.clone(), pk), + |d| d.node_identity.clone(), DELEGATION_PK_NAMESPACE, DELEGATION_MIXNODE_IDX_NAMESPACE, ), @@ -64,7 +64,7 @@ pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, Delega mod tests { use crate::delegations::storage; use cosmwasm_std::Addr; - use mixnet_contract::IdentityKey; + use mixnet_contract_common::IdentityKey; #[cfg(test)] mod reverse_mix_delegations { @@ -74,7 +74,7 @@ mod tests { use cosmwasm_std::testing::mock_env; use cosmwasm_std::{coin, Order}; use cw_storage_plus::PrimaryKey; - use mixnet_contract::Delegation; + use mixnet_contract_common::Delegation; #[test] fn reverse_mix_delegation_exists() { @@ -94,7 +94,7 @@ mod tests { storage::delegations() .save( &mut deps.storage, - (node_identity.clone(), delegation_owner.clone()).joined_key(), + (node_identity, delegation_owner.clone()).joined_key(), &dummy_data, ) .unwrap(); @@ -131,7 +131,7 @@ mod tests { // add delegation for a different node let dummy_data = Delegation::new( delegation_owner1.clone(), - node_identity2.clone(), + node_identity2, delegation.clone(), mock_env().block.height, None, @@ -156,14 +156,14 @@ mod tests { let dummy_data = Delegation::new( delegation_owner2.clone(), node_identity1.clone(), - delegation.clone(), + delegation, mock_env().block.height, None, ); storage::delegations() .save( &mut deps.storage, - (node_identity1.clone(), delegation_owner2.clone()).joined_key(), + (node_identity1.clone(), delegation_owner2).joined_key(), &dummy_data, ) .unwrap(); diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index 5de8764e93..9d060e891c 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -7,8 +7,8 @@ use crate::support::helpers::generate_storage_key; use config::defaults::DENOM; use cosmwasm_std::{coins, wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response}; use cw_storage_plus::PrimaryKey; -use mixnet_contract::Delegation; -use mixnet_contract::IdentityKey; +use mixnet_contract_common::events::{new_delegation_event, new_undelegation_event}; +use mixnet_contract_common::{Delegation, IdentityKey}; use vesting_contract::messages::ExecuteMsg as VestingContractExecuteMsg; fn validate_delegation_stake(mut delegation: Vec) -> Result { @@ -113,16 +113,21 @@ pub(crate) fn _try_delegate_to_mixnode( } None => Delegation::new( delegate.to_owned(), - mix_identity, - amount, + mix_identity.clone(), + amount.clone(), env.block.height, - proxy, + proxy.clone(), ), }) }, )?; - Ok(Response::default()) + Ok(Response::new().add_event(new_delegation_event( + &delegate, + &proxy, + &amount, + &mix_identity, + ))) } pub(crate) fn try_remove_delegation_from_mixnode( @@ -204,14 +209,19 @@ pub(crate) fn _try_remove_delegation_from_mixnode( let msg = Some(VestingContractExecuteMsg::TrackUndelegation { owner: delegate.as_str().to_string(), mix_identity: mix_identity.clone(), - amount: old_delegation.amount, + amount: old_delegation.amount.clone(), }); let track_undelegation_msg = wasm_execute(proxy, &msg, coins(0, DENOM))?; response = response.add_message(track_undelegation_msg); } - Ok(response) + Ok(response.add_event(new_undelegation_event( + &delegate, + &proxy, + &old_delegation, + &mix_identity, + ))) } } } @@ -530,7 +540,7 @@ mod tests { try_delegate_to_mixnode( deps.as_mut(), env1, - mock_info(delegation_owner1.as_str(), &[delegation1.clone()]), + mock_info(delegation_owner1.as_str(), &[delegation1]), identity.clone(), ) .unwrap(); @@ -544,7 +554,7 @@ mod tests { try_delegate_to_mixnode( deps.as_mut(), env2, - mock_info(delegation_owner2.as_str(), &[delegation2.clone()]), + mock_info(delegation_owner2.as_str(), &[delegation2]), identity.clone(), ) .unwrap(); @@ -699,7 +709,7 @@ mod tests { try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info(delegation_owner.as_str(), &vec![delegation_amount.clone()]), + mock_info(delegation_owner.as_str(), &[delegation_amount.clone()]), identity.clone(), ) .unwrap(); @@ -722,6 +732,7 @@ mod tests { #[cfg(test)] mod removing_mix_stake_delegation { + use crate::delegations::queries::query_mixnode_delegation; use cosmwasm_std::coin; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; @@ -774,11 +785,27 @@ mod tests { identity.clone(), ) .unwrap(); - assert_eq!( - Ok(Response::new().add_message(BankMsg::Send { + let delegation = query_mixnode_delegation( + deps.as_ref(), + identity.clone(), + delegation_owner.clone().into_string(), + ) + .unwrap(); + + let expected_response = Response::new() + .add_message(BankMsg::Send { to_address: delegation_owner.clone().into(), amount: coins(100, DENOM), - })), + }) + .add_event(new_undelegation_event( + &delegation_owner, + &None, + &delegation, + &identity, + )); + + assert_eq!( + Ok(expected_response), try_remove_delegation_from_mixnode( deps.as_mut(), mock_info(delegation_owner.as_str(), &[]), @@ -819,12 +846,27 @@ mod tests { identity.clone(), ) .unwrap(); - try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap(); - assert_eq!( - Ok(Response::new().add_message(BankMsg::Send { + let delegation = query_mixnode_delegation( + deps.as_ref(), + identity.clone(), + delegation_owner.clone().into_string(), + ) + .unwrap(); + let expected_response = Response::new() + .add_message(BankMsg::Send { to_address: delegation_owner.clone().into(), amount: coins(100, DENOM), - })), + }) + .add_event(new_undelegation_event( + &delegation_owner, + &None, + &delegation, + &identity, + )); + + try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap(); + assert_eq!( + Ok(expected_response), try_remove_delegation_from_mixnode( deps.as_mut(), mock_info(delegation_owner.as_str(), &[]), @@ -853,7 +895,7 @@ mod tests { assert!(try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info(delegation_owner1.as_str(), &[delegation1.clone()]), + mock_info(delegation_owner1.as_str(), &[delegation1]), identity.clone(), ) .is_ok()); diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index 3cdccc1f13..46e85e9de0 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -3,7 +3,7 @@ use config::defaults::DENOM; use cosmwasm_std::{Addr, StdError}; -use mixnet_contract::IdentityKey; +use mixnet_contract_common::IdentityKey; use thiserror::Error; /// Custom errors for contract failure conditions. diff --git a/contracts/mixnet/src/gateways/queries.rs b/contracts/mixnet/src/gateways/queries.rs index 2305f470c6..4125ca2143 100644 --- a/contracts/mixnet/src/gateways/queries.rs +++ b/contracts/mixnet/src/gateways/queries.rs @@ -5,7 +5,9 @@ use super::storage; use crate::mixnodes::storage::{BOND_PAGE_DEFAULT_LIMIT, BOND_PAGE_MAX_LIMIT}; // Keeps gateway and mixnode retrieval in sync by re-using the constant. Could be split into its own constant. use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; -use mixnet_contract::{GatewayBond, GatewayOwnershipResponse, IdentityKey, PagedGatewayResponse}; +use mixnet_contract_common::{ + GatewayBond, GatewayOwnershipResponse, IdentityKey, PagedGatewayResponse, +}; pub(crate) fn query_gateways_paged( deps: Deps, diff --git a/contracts/mixnet/src/gateways/storage.rs b/contracts/mixnet/src/gateways/storage.rs index 33b5ef9ab0..723a8c6ea0 100644 --- a/contracts/mixnet/src/gateways/storage.rs +++ b/contracts/mixnet/src/gateways/storage.rs @@ -3,7 +3,7 @@ use cosmwasm_std::Addr; use cw_storage_plus::{Index, IndexList, IndexedMap, UniqueIndex}; -use mixnet_contract::{GatewayBond, IdentityKeyRef}; +use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; // storage prefixes const GATEWAYS_PK_NAMESPACE: &str = "gt"; @@ -41,9 +41,7 @@ mod tests { use cosmwasm_std::StdResult; use cosmwasm_std::Storage; use cosmwasm_std::{coin, Addr, Uint128}; - use mixnet_contract::GatewayBond; - use mixnet_contract::IdentityKey; - use mixnet_contract::{Gateway, IdentityKeyRef}; + use mixnet_contract_common::{Gateway, GatewayBond, IdentityKey, IdentityKeyRef}; // currently this is only used in tests but may become useful later on pub(crate) fn read_gateway_pledge_amount( @@ -87,7 +85,7 @@ mod tests { let gateway_bond = GatewayBond { pledge_amount: coin(pledge_amount, DENOM), - owner: node_owner.clone(), + owner: node_owner, block_height: 12_345, gateway: Gateway { identity_key: node_identity.clone(), diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index 14cd6881c6..6426b44357 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -9,7 +9,8 @@ use config::defaults::DENOM; use cosmwasm_std::{ coins, wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Uint128, }; -use mixnet_contract::{Gateway, GatewayBond, Layer}; +use mixnet_contract_common::events::{new_gateway_bonding_event, new_gateway_unbonding_event}; +use mixnet_contract_common::{Gateway, GatewayBond, Layer}; use vesting_contract::messages::ExecuteMsg as VestingContractExecuteMsg; pub fn try_add_gateway( @@ -97,12 +98,24 @@ pub(crate) fn _try_add_gateway( &gateway.identity_key, )?; - let bond = GatewayBond::new(pledge, owner, env.block.height, gateway, proxy); + let gateway_identity = gateway.identity_key.clone(); + let bond = GatewayBond::new( + pledge.clone(), + owner.clone(), + env.block.height, + gateway, + proxy.clone(), + ); storage::gateways().save(deps.storage, bond.identity(), &bond)?; mixnet_params_storage::increment_layer_count(deps.storage, Layer::Gateway)?; - Ok(Response::new()) + Ok(Response::new().add_event(new_gateway_bonding_event( + &owner, + &proxy, + &pledge, + &gateway_identity, + ))) } pub fn try_remove_gateway_on_behalf( @@ -155,23 +168,24 @@ pub(crate) fn _try_remove_gateway( // decrement layer count mixnet_params_storage::decrement_layer_count(deps.storage, Layer::Gateway)?; - let mut response = Response::new() - .add_message(return_tokens) - .add_attribute("action", "unbond") - .add_attribute("address", owner.clone()) - .add_attribute("gateway_bond", gateway_bond.to_string()); + let mut response = Response::new().add_message(return_tokens); if let Some(proxy) = &proxy { let msg = VestingContractExecuteMsg::TrackUnbondGateway { owner: owner.as_str().to_string(), - amount: gateway_bond.pledge_amount, + amount: gateway_bond.pledge_amount(), }; let track_unbond_message = wasm_execute(proxy, &msg, coins(0, DENOM))?; response = response.add_message(track_unbond_message); } - Ok(response) + Ok(response.add_event(new_gateway_unbonding_event( + &owner, + &proxy, + &gateway_bond.pledge_amount, + gateway_bond.identity(), + ))) } fn validate_gateway_pledge( @@ -212,12 +226,10 @@ pub mod tests { use crate::support::tests; use crate::support::tests::test_helpers; use config::defaults::DENOM; - use cosmwasm_std::attr; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coins, BankMsg, Response}; use cosmwasm_std::{from_binary, Addr, Uint128}; - use mixnet_contract::Gateway; - use mixnet_contract::{ExecuteMsg, PagedGatewayResponse, QueryMsg}; + use mixnet_contract_common::{ExecuteMsg, Gateway, PagedGatewayResponse, QueryMsg}; #[test] fn gateway_add() { @@ -454,19 +466,6 @@ pub mod tests { let msg = ExecuteMsg::UnbondGateway {}; let remove_fred = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - // we should see log messages come back showing an unbond message - let expected_attributes = vec![ - attr("action", "unbond"), - attr("address", "fred"), - attr( - "gateway_bond", - format!( - "amount: {} {}, owner: fred, identity: {}", - INITIAL_GATEWAY_PLEDGE, DENOM, fred_identity - ), - ), - ]; - // we should see a funds transfer from the contract back to fred let expected_message = BankMsg::Send { to_address: String::from(info.sender), @@ -474,11 +473,17 @@ pub mod tests { }; // run the executor and check that we got back the correct results - let expected = Response::new() - .add_attributes(expected_attributes) - .add_message(expected_message); + let expected_response = + Response::new() + .add_message(expected_message) + .add_event(new_gateway_unbonding_event( + &Addr::unchecked("fred"), + &None, + &tests::fixtures::good_gateway_pledge()[0], + &fred_identity, + )); - assert_eq!(remove_fred, expected); + assert_eq!(expected_response, remove_fred); // only 1 node now exists, owned by bob: let gateway_bonds = tests::queries::get_gateways(&mut deps); diff --git a/contracts/mixnet/src/mixnet_contract_settings/models.rs b/contracts/mixnet/src/mixnet_contract_settings/models.rs index 0e5dcd8cb4..6b9d5d73f9 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/models.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/models.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use cosmwasm_std::Addr; -use mixnet_contract::ContractStateParams; +use mixnet_contract_common::ContractStateParams; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/contracts/mixnet/src/mixnet_contract_settings/queries.rs b/contracts/mixnet/src/mixnet_contract_settings/queries.rs index 2178ed8930..68427a8173 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -3,7 +3,9 @@ use super::storage; use cosmwasm_std::{Deps, StdResult}; -use mixnet_contract::{ContractStateParams, MixnetContractVersion, RewardingIntervalResponse}; +use mixnet_contract_common::{ + ContractStateParams, MixnetContractVersion, RewardingIntervalResponse, +}; pub(crate) fn query_contract_settings_params(deps: Deps) -> StdResult { storage::CONTRACT_STATE diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 3ec217c352..2f2bd48664 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -5,8 +5,7 @@ use crate::mixnet_contract_settings::models::ContractState; use cosmwasm_std::StdResult; use cosmwasm_std::Storage; use cw_storage_plus::Item; -use mixnet_contract::Layer; -use mixnet_contract::LayerDistribution; +use mixnet_contract_common::{Layer, LayerDistribution}; pub(crate) const CONTRACT_STATE: Item = Item::new("config"); pub(crate) const LAYERS: Item = Item::new("layers"); diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index 21bd496be6..e087584c4d 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -6,7 +6,8 @@ use crate::error::ContractError; use cosmwasm_std::DepsMut; use cosmwasm_std::MessageInfo; use cosmwasm_std::Response; -use mixnet_contract::ContractStateParams; +use mixnet_contract_common::events::new_settings_update_event; +use mixnet_contract_common::ContractStateParams; pub(crate) fn try_update_contract_settings( deps: DepsMut, @@ -34,10 +35,12 @@ pub(crate) fn try_update_contract_settings( return Err(ContractError::InvalidActiveSetSize); } + let response = Response::new().add_event(new_settings_update_event(&state.params, ¶ms)); + state.params = params; storage::CONTRACT_STATE.save(deps.storage, &state)?; - Ok(Response::default()) + Ok(response) } #[cfg(test)] @@ -49,7 +52,7 @@ pub mod tests { use crate::support::tests::test_helpers; use cosmwasm_std::testing::mock_info; use cosmwasm_std::Response; - use mixnet_contract::ContractStateParams; + use mixnet_contract_common::ContractStateParams; #[test] fn updating_contract_settings() { @@ -63,14 +66,13 @@ pub mod tests { active_set_work_factor: 10, }; + let initial_params = storage::CONTRACT_STATE + .load(deps.as_ref().storage) + .unwrap() + .params; + // sanity check to ensure new_params are different than the default ones - assert_ne!( - new_params, - storage::CONTRACT_STATE - .load(deps.as_ref().storage) - .unwrap() - .params - ); + assert_ne!(new_params, initial_params); // cannot be updated from non-owner account let info = mock_info("not-the-creator", &[]); @@ -80,7 +82,10 @@ pub mod tests { // but works fine from the creator account let info = mock_info("creator", &[]); let res = try_update_contract_settings(deps.as_mut(), info, new_params.clone()); - assert_eq!(res, Ok(Response::default())); + assert_eq!( + res, + Ok(Response::new().add_event(new_settings_update_event(&initial_params, &new_params))) + ); // and the state is actually updated let current_state = storage::CONTRACT_STATE.load(deps.as_ref().storage).unwrap(); @@ -90,21 +95,21 @@ pub mod tests { let info = mock_info("creator", &[]); let mut new_params = current_state.params.clone(); new_params.mixnode_rewarded_set_size = new_params.mixnode_active_set_size - 1; - let res = try_update_contract_settings(deps.as_mut(), info, new_params.clone()); + let res = try_update_contract_settings(deps.as_mut(), info, new_params); assert_eq!(Err(ContractError::InvalidActiveSetSize), res); // error is thrown for 0 size rewarded set let info = mock_info("creator", &[]); let mut new_params = current_state.params.clone(); new_params.mixnode_rewarded_set_size = 0; - let res = try_update_contract_settings(deps.as_mut(), info, new_params.clone()); + let res = try_update_contract_settings(deps.as_mut(), info, new_params); assert_eq!(Err(ContractError::ZeroRewardedSet), res); // error is thrown for 0 size active set let info = mock_info("creator", &[]); - let mut new_params = current_state.params.clone(); + let mut new_params = current_state.params; new_params.mixnode_active_set_size = 0; - let res = try_update_contract_settings(deps.as_mut(), info, new_params.clone()); + let res = try_update_contract_settings(deps.as_mut(), info, new_params); assert_eq!(Err(ContractError::ZeroActiveSet), res); } } diff --git a/contracts/mixnet/src/mixnodes/bonding_queries.rs b/contracts/mixnet/src/mixnodes/bonding_queries.rs index 4e485f1b77..e916e7263e 100644 --- a/contracts/mixnet/src/mixnodes/bonding_queries.rs +++ b/contracts/mixnet/src/mixnodes/bonding_queries.rs @@ -4,7 +4,9 @@ use super::storage; use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; -use mixnet_contract::{IdentityKey, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse}; +use mixnet_contract_common::{ + IdentityKey, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse, +}; pub fn query_mixnodes_paged( deps: Deps, diff --git a/contracts/mixnet/src/mixnodes/layer_queries.rs b/contracts/mixnet/src/mixnodes/layer_queries.rs index 2ce21648f3..7a45c58167 100644 --- a/contracts/mixnet/src/mixnodes/layer_queries.rs +++ b/contracts/mixnet/src/mixnodes/layer_queries.rs @@ -3,7 +3,7 @@ use crate::mixnet_contract_settings::storage as mixnet_params_storage; use cosmwasm_std::{Deps, StdResult}; -use mixnet_contract::LayerDistribution; +use mixnet_contract_common::LayerDistribution; pub(crate) fn query_layer_distribution(deps: Deps) -> StdResult { mixnet_params_storage::LAYERS.load(deps.storage) diff --git a/contracts/mixnet/src/mixnodes/storage.rs b/contracts/mixnet/src/mixnodes/storage.rs index cbbcf749b4..15d365f0d9 100644 --- a/contracts/mixnet/src/mixnodes/storage.rs +++ b/contracts/mixnet/src/mixnodes/storage.rs @@ -4,7 +4,7 @@ use config::defaults::DENOM; use cosmwasm_std::{StdResult, Storage, Uint128}; use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex}; -use mixnet_contract::{Addr, Coin, IdentityKeyRef, Layer, MixNode, MixNodeBond}; +use mixnet_contract_common::{Addr, Coin, IdentityKeyRef, Layer, MixNode, MixNodeBond}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; @@ -49,7 +49,6 @@ pub(crate) struct StoredMixnodeBond { pub layer: Layer, pub block_height: u64, pub mix_node: MixNode, - pub profit_margin_percent: Option, pub proxy: Option, } @@ -60,7 +59,6 @@ impl StoredMixnodeBond { layer: Layer, block_height: u64, mix_node: MixNode, - profit_margin_percent: Option, proxy: Option, ) -> Self { StoredMixnodeBond { @@ -69,7 +67,6 @@ impl StoredMixnodeBond { layer, block_height, mix_node, - profit_margin_percent, proxy, } } @@ -141,8 +138,7 @@ mod tests { use config::defaults::DENOM; use cosmwasm_std::testing::MockStorage; use cosmwasm_std::{coin, Addr, Uint128}; - use mixnet_contract::IdentityKey; - use mixnet_contract::MixNode; + use mixnet_contract_common::{IdentityKey, MixNode}; #[test] fn mixnode_single_read_retrieval() { @@ -173,14 +169,13 @@ mod tests { let mixnode_bond = StoredMixnodeBond { pledge_amount: coin(pledge_value, DENOM), - owner: node_owner.clone(), + owner: node_owner, layer: Layer::One, block_height: 12_345, mix_node: MixNode { identity_key: node_identity.clone(), ..tests::fixtures::mix_node_fixture() }, - profit_margin_percent: None, proxy: None, }; diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 3f7c3d40e3..252128324a 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -11,7 +11,8 @@ use config::defaults::DENOM; use cosmwasm_std::{ coins, wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Uint128, }; -use mixnet_contract::MixNode; +use mixnet_contract_common::events::{new_mixnode_bonding_event, new_mixnode_unbonding_event}; +use mixnet_contract_common::MixNode; use vesting_contract::messages::ExecuteMsg as VestingContractExecuteMsg; pub fn try_add_mixnode( @@ -109,13 +110,12 @@ fn _try_add_mixnode( let layer = layer_distribution.choose_with_fewest(); let stored_bond = StoredMixnodeBond::new( - pledge_amount, - owner, + pledge_amount.clone(), + owner.clone(), layer, env.block.height, mix_node, - None, - proxy, + proxy.clone(), ); // technically we don't have to set the total_delegation bucket, but it makes things easier @@ -133,7 +133,13 @@ fn _try_add_mixnode( mixnet_params_storage::increment_layer_count(deps.storage, stored_bond.layer)?; - Ok(Response::new()) + Ok(Response::new().add_event(new_mixnode_bonding_event( + &owner, + &proxy, + &pledge_amount, + identity, + stored_bond.layer, + ))) } pub fn try_remove_mixnode_on_behalf( @@ -186,22 +192,60 @@ pub(crate) fn _try_remove_mixnode( // decrement layer count mixnet_params_storage::decrement_layer_count(deps.storage, mixnode_bond.layer)?; - let mut response = Response::new() - .add_message(return_tokens) - .add_attribute("action", "unbond") - .add_attribute("mixnode_bond", mixnode_bond.to_string()); + let mut response = Response::new().add_message(return_tokens); if let Some(proxy) = &proxy { let msg = VestingContractExecuteMsg::TrackUnbondMixnode { owner: owner.as_str().to_string(), - amount: mixnode_bond.pledge_amount, + amount: mixnode_bond.pledge_amount(), }; let track_unbond_message = wasm_execute(proxy, &msg, coins(0, DENOM))?; response = response.add_message(track_unbond_message); } - Ok(response) + Ok(response.add_event(new_mixnode_unbonding_event( + &owner, + &proxy, + &mixnode_bond.pledge_amount, + mixnode_bond.identity(), + ))) +} + +pub(crate) fn try_update_mixnode_config( + deps: DepsMut, + env: Env, + info: MessageInfo, + profit_margin_percent: u8, +) -> Result { + let owner = deps.api.addr_validate(info.sender.as_ref())?; + let mix_identity = storage::mixnodes() + .idx + .owner + .item(deps.storage, owner.clone())? + .ok_or(ContractError::NoAssociatedMixNodeBond { owner })? + .1 + .identity() + .clone(); + + // We don't have to check lower bound as its an u8 + if profit_margin_percent > 100 { + return Err(ContractError::InvalidProfitMarginPercent( + profit_margin_percent, + )); + } + + storage::mixnodes().update(deps.storage, &mix_identity, |mixnode_bond_opt| { + mixnode_bond_opt + .map(|mut mixnode_bond| { + mixnode_bond.mix_node.profit_margin_percent = profit_margin_percent; + mixnode_bond.block_height = env.block.height; + mixnode_bond + }) + .ok_or(ContractError::NoBondFound) + })?; + + Ok(Response::new()) } fn validate_mixnode_pledge( @@ -242,13 +286,12 @@ pub mod tests { use crate::support::tests; use crate::support::tests::test_helpers; use config::defaults::DENOM; - use cosmwasm_std::attr; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coins, BankMsg, Response}; use cosmwasm_std::{from_binary, Addr, Uint128}; - use mixnet_contract::Layer; - use mixnet_contract::MixNode; - use mixnet_contract::{ExecuteMsg, LayerDistribution, PagedMixnodeResponse, QueryMsg}; + use mixnet_contract_common::{ + ExecuteMsg, Layer, LayerDistribution, MixNode, PagedMixnodeResponse, QueryMsg, + }; #[test] fn mixnode_add() { @@ -507,18 +550,6 @@ pub mod tests { let msg = ExecuteMsg::UnbondMixnode {}; let remove_fred = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - // we should see log messages come back showing an unbond message - let expected_attributes = vec![ - attr("action", "unbond"), - attr( - "mixnode_bond", - format!( - "amount: {}{}, owner: fred, identity: {}", - INITIAL_MIXNODE_PLEDGE, DENOM, fred_identity - ), - ), - ]; - // we should see a funds transfer from the contract back to fred let expected_message = BankMsg::Send { to_address: String::from(info.sender), @@ -526,11 +557,17 @@ pub mod tests { }; // run the executor and check that we got back the correct results - let expected = Response::new() - .add_attributes(expected_attributes) - .add_message(expected_message); + let expected_response = + Response::new() + .add_message(expected_message) + .add_event(new_mixnode_unbonding_event( + &Addr::unchecked("fred"), + &None, + &tests::fixtures::good_gateway_pledge()[0], + &fred_identity, + )); - assert_eq!(remove_fred, expected); + assert_eq!(expected_response, remove_fred); // only 1 node now exists, owned by bob: let mix_node_bonds = tests::queries::get_mix_nodes(&mut deps); @@ -587,6 +624,77 @@ pub mod tests { ); } + #[test] + fn updating_mixnode_config() { + let sender = "bob"; + let mut deps = test_helpers::init_contract(); + let info = mock_info(sender, &[]); + + // try updating a non existing mixnode bond + let msg = ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent: 10, + }; + let ret = execute(deps.as_mut(), mock_env(), info.clone(), msg); + assert_eq!( + ret, + Err(ContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked(sender) + }) + ); + + test_helpers::add_mixnode( + sender, + tests::fixtures::good_mixnode_pledge(), + deps.as_mut(), + ); + + // check the initial profit margin is set to the fixture value + let fixture_profit_margin = tests::fixtures::mix_node_fixture().profit_margin_percent; + assert_eq!( + fixture_profit_margin, + storage::mixnodes() + .idx + .owner + .item(deps.as_ref().storage, Addr::unchecked("bob")) + .unwrap() + .unwrap() + .1 + .mix_node + .profit_margin_percent + ); + + // try updating with an invalid value + let profit_margin_percent = 101; + let msg = ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + }; + let ret = execute(deps.as_mut(), mock_env(), info.clone(), msg); + assert_eq!( + ret, + Err(ContractError::InvalidProfitMarginPercent( + profit_margin_percent + )) + ); + + let profit_margin_percent = fixture_profit_margin + 10; + let msg = ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + }; + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!( + profit_margin_percent, + storage::mixnodes() + .idx + .owner + .item(deps.as_ref().storage, Addr::unchecked("bob")) + .unwrap() + .unwrap() + .1 + .mix_node + .profit_margin_percent + ); + } + #[test] fn validating_mixnode_bond() { // you must send SOME funds @@ -649,6 +757,6 @@ pub mod tests { assert_eq!(alice_node.mix_node.identity_key, alice_identity); assert_eq!(alice_node.layer, Layer::One); assert_eq!(bob_node.mix_node.identity_key, bob_identity); - assert_eq!(bob_node.layer, mixnet_contract::Layer::Two); + assert_eq!(bob_node.layer, mixnet_contract_common::Layer::Two); } } diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index 593e2eb7f9..e532c948f5 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -5,8 +5,8 @@ use super::storage; use crate::error::ContractError; use crate::mixnodes::storage as mixnodes_storage; use cosmwasm_std::{Addr, Storage, Uint128}; -use mixnet_contract::mixnode::DelegatorRewardParams; -use mixnet_contract::{ +use mixnet_contract_common::mixnode::DelegatorRewardParams; +use mixnet_contract_common::{ IdentityKey, IdentityKeyRef, PendingDelegatorRewarding, RewardingResult, RewardingStatus, }; @@ -64,7 +64,7 @@ pub(crate) fn update_rewarding_status( if let Some(next_start) = next_start { storage::REWARDING_STATUS.save( storage, - (rewarding_interval_nonce.into(), mix_identity), + (rewarding_interval_nonce, mix_identity), &RewardingStatus::PendingNextDelegatorPage(PendingDelegatorRewarding { running_results: rewarding_results, next_start, @@ -74,7 +74,7 @@ pub(crate) fn update_rewarding_status( } else { storage::REWARDING_STATUS.save( storage, - (rewarding_interval_nonce.into(), mix_identity), + (rewarding_interval_nonce, mix_identity), &RewardingStatus::Complete(rewarding_results), )?; } diff --git a/contracts/mixnet/src/rewards/queries.rs b/contracts/mixnet/src/rewards/queries.rs index 7d643c073d..d9f82ef7b8 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -4,7 +4,7 @@ use super::storage; use cosmwasm_std::Uint128; use cosmwasm_std::{Deps, StdResult}; -use mixnet_contract::{IdentityKey, MixnodeRewardingStatusResponse}; +use mixnet_contract_common::{IdentityKey, MixnodeRewardingStatusResponse}; pub(crate) fn query_reward_pool(deps: Deps) -> StdResult { storage::REWARD_POOL.load(deps.storage) @@ -19,10 +19,8 @@ pub(crate) fn query_rewarding_status( mix_identity: IdentityKey, rewarding_interval_nonce: u32, ) -> StdResult { - let status = storage::REWARDING_STATUS.may_load( - deps.storage, - (rewarding_interval_nonce.into(), mix_identity), - )?; + let status = storage::REWARDING_STATUS + .may_load(deps.storage, (rewarding_interval_nonce, mix_identity))?; Ok(MixnodeRewardingStatusResponse { status }) } @@ -46,7 +44,9 @@ pub(crate) mod tests { }; use config::defaults::DENOM; use cosmwasm_std::{coin, Addr}; - use mixnet_contract::{RewardingResult, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT}; + use mixnet_contract_common::{ + RewardingResult, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT, + }; #[test] fn returns_empty_status_for_unrewarded_nodes() { @@ -75,14 +75,14 @@ pub(crate) mod tests { try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); try_reward_mixnode( deps.as_mut(), - env.clone(), + env, info.clone(), node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), 1, ) .unwrap(); - try_finish_mixnode_rewarding(deps.as_mut(), info.clone(), 1).unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); assert!(query_rewarding_status(deps.as_ref(), node_identity, 2) .unwrap() @@ -120,7 +120,7 @@ pub(crate) mod tests { 1, ) .unwrap(); - try_finish_mixnode_rewarding(deps.as_mut(), info.clone(), 1).unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); let res = query_rewarding_status(deps.as_ref(), node_identity, 1).unwrap(); assert!(matches!(res.status, Some(RewardingStatus::Complete(..)))); @@ -151,10 +151,7 @@ pub(crate) mod tests { try_delegate_to_mixnode( deps.as_mut(), env.clone(), - mock_info( - &*format!("delegator{:04}", i), - &vec![coin(200_000000, DENOM)], - ), + mock_info(&*format!("delegator{:04}", i), &[coin(200_000000, DENOM)]), node_identity.clone(), ) .unwrap(); @@ -167,7 +164,7 @@ pub(crate) mod tests { try_reward_mixnode( deps.as_mut(), - env.clone(), + env, info.clone(), node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), @@ -176,13 +173,8 @@ pub(crate) mod tests { .unwrap(); // rewards all pending - try_reward_next_mixnode_delegators( - deps.as_mut(), - info.clone(), - node_identity.to_string(), - 2, - ) - .unwrap(); + try_reward_next_mixnode_delegators(deps.as_mut(), info, node_identity.to_string(), 2) + .unwrap(); let res = query_rewarding_status(deps.as_ref(), node_identity, 2).unwrap(); assert!(matches!(res.status, Some(RewardingStatus::Complete(..)))); @@ -222,10 +214,7 @@ pub(crate) mod tests { try_delegate_to_mixnode( deps.as_mut(), env.clone(), - mock_info( - &*format!("delegator{:04}", i), - &vec![coin(200_000000, DENOM)], - ), + mock_info(&*format!("delegator{:04}", i), &[coin(200_000000, DENOM)]), node_identity.clone(), ) .unwrap(); @@ -238,7 +227,7 @@ pub(crate) mod tests { try_reward_mixnode( deps.as_mut(), - env.clone(), + env, info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), diff --git a/contracts/mixnet/src/rewards/storage.rs b/contracts/mixnet/src/rewards/storage.rs index 56ce6a4964..838b68f092 100644 --- a/contracts/mixnet/src/rewards/storage.rs +++ b/contracts/mixnet/src/rewards/storage.rs @@ -4,11 +4,12 @@ use crate::error::ContractError; use config::defaults::TOTAL_SUPPLY; use cosmwasm_std::{StdResult, Storage, Uint128}; -use cw_storage_plus::{Item, Map, U32Key}; -use mixnet_contract::{IdentityKey, RewardingStatus}; +use cw_storage_plus::{Item, Map}; +use mixnet_contract_common::{IdentityKey, RewardingStatus}; pub(crate) const REWARD_POOL: Item = Item::new("pool"); -pub(crate) const REWARDING_STATUS: Map<(U32Key, IdentityKey), RewardingStatus> = Map::new("rm"); +// TODO: Do we need a migration for this? +pub(crate) const REWARDING_STATUS: Map<(u32, IdentityKey), RewardingStatus> = Map::new("rm"); // approximately 1 day (assuming 5s per block) pub(crate) const MINIMUM_BLOCK_AGE_FOR_REWARDING: u64 = 17280; diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index 7457f0e3ed..5a838239d0 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -9,8 +9,13 @@ use crate::mixnodes::storage as mixnodes_storage; use crate::rewards::helpers; use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, StdResult, Storage, Uint128}; use cw_storage_plus::{Bound, PrimaryKey}; -use mixnet_contract::mixnode::{DelegatorRewardParams, NodeRewardParams}; -use mixnet_contract::{ +use mixnet_contract_common::events::{ + new_begin_rewarding_event, new_finish_rewarding_event, new_mix_delegators_rewarding_event, + new_mix_operator_rewarding_event, new_not_found_mix_operator_rewarding_event, + new_too_fresh_bond_mix_operator_rewarding_event, new_zero_uptime_mix_operator_rewarding_event, +}; +use mixnet_contract_common::mixnode::{DelegatorRewardParams, NodeRewardParams}; +use mixnet_contract_common::{ IdentityKey, RewardingResult, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT, }; @@ -106,10 +111,7 @@ pub(crate) fn try_begin_mixnode_rewarding( mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &state)?; - Ok(Response::new().add_attribute( - "rewarding interval nonce", - rewarding_interval_nonce.to_string(), - )) + Ok(Response::new().add_event(new_begin_rewarding_event(rewarding_interval_nonce))) } fn reward_mix_delegators( @@ -199,7 +201,7 @@ pub(crate) fn try_reward_next_mixnode_delegators( match storage::REWARDING_STATUS.may_load( deps.storage, - (rewarding_interval_nonce.into(), mix_identity.clone()), + (rewarding_interval_nonce, mix_identity.clone()), )? { None => { // we haven't called 'regular' try_reward_mixnode, i.e. the operator itself @@ -232,21 +234,26 @@ pub(crate) fn try_reward_next_mixnode_delegators( let mut rewarding_results = next_page_info.running_results; rewarding_results.total_delegator_reward += delegation_rewarding_result.total_rewarded; - let round_increase = delegation_rewarding_result.total_rewarded.to_string(); + let round_increase = delegation_rewarding_result.total_rewarded; let more_delegators = delegation_rewarding_result.start_next.is_some(); helpers::update_rewarding_status( deps.storage, rewarding_interval_nonce, - mix_identity, + mix_identity.clone(), rewarding_results, delegation_rewarding_result.start_next, next_page_info.rewarding_params, )?; - Ok(Response::new() - .add_attribute("current round delegation increase", round_increase) - .add_attribute("more delegators to reward", more_delegators.to_string())) + Ok( + Response::new().add_event(new_mix_delegators_rewarding_event( + rewarding_interval_nonce, + &mix_identity, + round_increase, + more_delegators, + )), + ) } } } @@ -264,7 +271,7 @@ pub(crate) fn try_reward_mixnode( // check if the mixnode hasn't been rewarded in this rewarding interval already match storage::REWARDING_STATUS.may_load( deps.storage, - (rewarding_interval_nonce.into(), mix_identity.clone()), + (rewarding_interval_nonce, mix_identity.clone()), )? { None => (), Some(RewardingStatus::Complete(_)) => { @@ -283,68 +290,91 @@ pub(crate) fn try_reward_mixnode( let current_bond = match mixnodes_storage::read_full_mixnode_bond(deps.storage, &mix_identity)? { Some(bond) => bond, - None => return Ok(Response::new().add_attribute("result", "bond not found")), + None => { + return Ok( + Response::new().add_event(new_not_found_mix_operator_rewarding_event( + rewarding_interval_nonce, + &mix_identity, + )), + ) + } }; - let mut node_reward = "0".to_string(); - let mut operator_reward = Uint128::zero(); - let mut total_delegation_increase = Uint128::zero(); - let mut more_delegators = false; - - // check if node is old enough for rewarding and if its uptime is non-zero - if current_bond.block_height + storage::MINIMUM_BLOCK_AGE_FOR_REWARDING <= env.block.height - && params.uptime() > 0 - { - let mut node_reward_params = params; - node_reward_params.set_reward_blockstamp(env.block.height); - - let operator_reward_result = current_bond.reward(&node_reward_params); - node_reward = operator_reward_result.reward().to_string(); - - // Omitting the price per packet function now, it follows that base operator reward is the node_reward - operator_reward = Uint128::new(current_bond.operator_reward(&node_reward_params)); - - let delegator_params = DelegatorRewardParams::new(¤t_bond, node_reward_params); - let delegation_rewarding_result = - reward_mix_delegators(deps.storage, mix_identity.clone(), None, delegator_params)?; - - helpers::update_post_rewarding_storage( - deps.storage, - &mix_identity, - operator_reward, - delegation_rewarding_result.total_rewarded, - )?; - - let rewarding_results = RewardingResult { - operator_reward, - total_delegator_reward: delegation_rewarding_result.total_rewarded, - }; - - total_delegation_increase = rewarding_results.total_delegator_reward; - more_delegators = delegation_rewarding_result.start_next.is_some(); - - helpers::update_rewarding_status( - deps.storage, - rewarding_interval_nonce, - mix_identity, - rewarding_results, - delegation_rewarding_result.start_next, - delegator_params, - )?; - } else { - // node is not eligible for rewarding, so we're done immediately + // check if node is old enough for rewarding + if current_bond.block_height + storage::MINIMUM_BLOCK_AGE_FOR_REWARDING > env.block.height { storage::REWARDING_STATUS.save( deps.storage, - (rewarding_interval_nonce.into(), mix_identity), + (rewarding_interval_nonce, mix_identity.clone()), &RewardingStatus::Complete(Default::default()), )?; + + return Ok( + Response::new().add_event(new_too_fresh_bond_mix_operator_rewarding_event( + rewarding_interval_nonce, + &mix_identity, + )), + ); } - Ok(Response::new() - .add_attribute("node reward", node_reward) - .add_attribute("operator reward", operator_reward) - .add_attribute("total delegation increase", total_delegation_increase) - .add_attribute("more delegators to reward", more_delegators.to_string())) + // check if it has non-zero uptime + if params.uptime() == 0 { + storage::REWARDING_STATUS.save( + deps.storage, + (rewarding_interval_nonce, mix_identity.clone()), + &RewardingStatus::Complete(Default::default()), + )?; + + return Ok( + Response::new().add_event(new_zero_uptime_mix_operator_rewarding_event( + rewarding_interval_nonce, + &mix_identity, + )), + ); + } + + let mut node_reward_params = params; + node_reward_params.set_reward_blockstamp(env.block.height); + + let node_reward_result = current_bond.reward(&node_reward_params); + + // Omitting the price per packet function now, it follows that base operator reward is the node_reward + let operator_reward = Uint128::new(current_bond.operator_reward(&node_reward_params)); + + let delegator_params = DelegatorRewardParams::new(¤t_bond, node_reward_params); + let delegation_rewarding_result = + reward_mix_delegators(deps.storage, mix_identity.clone(), None, delegator_params)?; + + helpers::update_post_rewarding_storage( + deps.storage, + &mix_identity, + operator_reward, + delegation_rewarding_result.total_rewarded, + )?; + + let rewarding_results = RewardingResult { + operator_reward, + total_delegator_reward: delegation_rewarding_result.total_rewarded, + }; + let total_delegator_reward = rewarding_results.total_delegator_reward; + let further_delegations = delegation_rewarding_result.start_next.is_some(); + + helpers::update_rewarding_status( + deps.storage, + rewarding_interval_nonce, + mix_identity.clone(), + rewarding_results, + delegation_rewarding_result.start_next, + delegator_params, + )?; + + Ok(Response::new().add_event(new_mix_operator_rewarding_event( + rewarding_interval_nonce, + &mix_identity, + node_reward_result, + operator_reward, + total_delegator_reward, + further_delegations, + ))) } pub(crate) fn try_finish_mixnode_rewarding( @@ -374,7 +404,7 @@ pub(crate) fn try_finish_mixnode_rewarding( state.rewarding_in_progress = false; mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &state)?; - Ok(Response::new()) + Ok(Response::new().add_event(new_finish_rewarding_event(rewarding_interval_nonce))) } #[cfg(test)] @@ -394,11 +424,15 @@ pub mod tests { use config::defaults::DENOM; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::Coin; - use cosmwasm_std::{attr, Order}; + use cosmwasm_std::Order; use cosmwasm_std::{coin, coins, Addr, Uint128}; - use mixnet_contract::mixnode::NodeRewardParams; - use mixnet_contract::{Delegation, MixNode}; - use mixnet_contract::{IdentityKey, Layer}; + use mixnet_contract_common::events::{ + must_find_attribute, BOND_TOO_FRESH_VALUE, DISTRIBUTED_DELEGATION_REWARDS_KEY, + FURTHER_DELEGATIONS_TO_REWARD_KEY, NO_REWARD_REASON_KEY, OPERATOR_REWARDING_EVENT_TYPE, + OPERATOR_REWARD_KEY, TOTAL_MIXNODE_REWARD_KEY, + }; + use mixnet_contract_common::mixnode::NodeRewardParams; + use mixnet_contract_common::{Delegation, IdentityKey, Layer, MixNode}; #[cfg(test)] mod beginning_mixnode_rewarding { @@ -426,7 +460,7 @@ pub mod tests { let res = try_begin_mixnode_rewarding( deps.as_mut(), - env.clone(), + env, mock_info(rewarding_validator_address.as_ref(), &[]), 1, ); @@ -452,7 +486,7 @@ pub mod tests { let res = try_begin_mixnode_rewarding( deps.as_mut(), - env.clone(), + env, mock_info(rewarding_validator_address.as_ref(), &[]), 2, ); @@ -547,7 +581,7 @@ pub mod tests { let res = try_begin_mixnode_rewarding( deps.as_mut(), - env.clone(), + env, mock_info(rewarding_validator_address.as_ref(), &[]), 43, ); @@ -774,9 +808,9 @@ pub mod tests { let res = try_reward_mixnode( deps.as_mut(), - env.clone(), + env, info, - node_identity.clone(), + node_identity, tests::fixtures::node_rewarding_params_fixture(100), 1, ); @@ -836,9 +870,9 @@ pub mod tests { let res = try_reward_mixnode( deps.as_mut(), - env.clone(), + env, info, - node_identity.clone(), + node_identity, tests::fixtures::node_rewarding_params_fixture(100), 1, ); @@ -900,7 +934,7 @@ pub mod tests { deps.as_mut(), env, info, - node_identity.clone(), + node_identity, tests::fixtures::node_rewarding_params_fixture(100), 2, ); @@ -923,14 +957,13 @@ pub mod tests { let initial_delegation = 20000_000000; let mixnode_bond = StoredMixnodeBond { pledge_amount: coin(initial_bond, DENOM), - owner: node_owner.clone(), + owner: node_owner, layer: Layer::One, block_height: env.block.height, mix_node: MixNode { identity_key: node_identity.clone(), ..tests::fixtures::mix_node_fixture() }, - profit_margin_percent: Some(10), proxy: None, }; @@ -988,13 +1021,11 @@ pub mod tests { .unwrap() .u128() ); - - assert_eq!(res.attributes[0], attr("node reward", "0")); - assert_eq!(res.attributes[1], attr("operator reward", "0")); - assert_eq!(res.attributes[2], attr("total delegation increase", "0")); + assert_eq!(1, res.events.len()); + assert_eq!(OPERATOR_REWARDING_EVENT_TYPE, res.events[0].ty); assert_eq!( - res.attributes[3], - attr("more delegators to reward", false.to_string()) + BOND_TOO_FRESH_VALUE, + must_find_attribute(&res.events[0], NO_REWARD_REASON_KEY) ); // reward can happen now, but only for bonded node @@ -1027,12 +1058,18 @@ pub mod tests { .u128() ); - assert_ne!(res.attributes[0], attr("node reward", "0")); - assert_ne!(res.attributes[1], attr("operator reward", "0")); - assert_eq!(res.attributes[2], attr("total delegation increase", "0")); + assert_eq!(1, res.events.len()); + let event = &res.events[0]; + assert_eq!(OPERATOR_REWARDING_EVENT_TYPE, event.ty); + assert_ne!("0", must_find_attribute(event, TOTAL_MIXNODE_REWARD_KEY)); + assert_ne!("0", must_find_attribute(event, OPERATOR_REWARD_KEY)); assert_eq!( - res.attributes[3], - attr("more delegators to reward", false.to_string()) + "0", + must_find_attribute(event, DISTRIBUTED_DELEGATION_REWARDS_KEY) + ); + assert_eq!( + false.to_string(), + must_find_attribute(event, FURTHER_DELEGATIONS_TO_REWARD_KEY) ); // reward happens now, both for node owner and delegators @@ -1047,7 +1084,7 @@ pub mod tests { try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 3).unwrap(); let res = try_reward_mixnode( deps.as_mut(), - env.clone(), + env, info.clone(), node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), @@ -1070,12 +1107,18 @@ pub mod tests { > initial_delegation ); - assert_ne!(res.attributes[0], attr("node reward", "0")); - assert_ne!(res.attributes[1], attr("operator reward", "0")); - assert_ne!(res.attributes[2], attr("total delegation increase", "0")); + assert_eq!(1, res.events.len()); + let event = &res.events[0]; + assert_eq!(OPERATOR_REWARDING_EVENT_TYPE, event.ty); + assert_ne!("0", must_find_attribute(event, TOTAL_MIXNODE_REWARD_KEY)); + assert_ne!("0", must_find_attribute(event, OPERATOR_REWARD_KEY)); + assert_ne!( + "0", + must_find_attribute(event, DISTRIBUTED_DELEGATION_REWARDS_KEY) + ); assert_eq!( - res.attributes[3], - attr("more delegators to reward", false.to_string()) + false.to_string(), + must_find_attribute(event, FURTHER_DELEGATIONS_TO_REWARD_KEY) ); } @@ -1198,7 +1241,7 @@ pub mod tests { ); assert_eq!( mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &node_identity.clone()) + .load(&deps.storage, &node_identity) .unwrap() .u128(), pre_reward_delegation + mix1_delegator1_reward + mix1_delegator2_reward @@ -1214,7 +1257,7 @@ pub mod tests { // it's all correctly saved match storage::REWARDING_STATUS - .load(deps.as_ref().storage, (1.into(), node_identity.clone())) + .load(deps.as_ref().storage, (1u32, node_identity)) .unwrap() { RewardingStatus::Complete(result) => assert_eq!( @@ -1244,6 +1287,7 @@ pub mod tests { .unwrap(); let rewarding_validator_address = current_state.rewarding_validator_address; + #[allow(clippy::inconsistent_digit_grouping)] let mix_bond = Uint128::new(10000_000_000); let delegation_value = 2000_000000; @@ -1263,7 +1307,7 @@ pub mod tests { env.clone(), mock_info( &*format!("delegator{}", i), - &vec![coin(delegation_value, DENOM)], + &[coin(delegation_value, DENOM)], ), node_identity.clone(), ) @@ -1291,8 +1335,8 @@ pub mod tests { ) .unwrap(); assert_eq!( - res.attributes[3], - attr("more delegators to reward", false.to_string()) + false.to_string(), + must_find_attribute(&res.events[0], FURTHER_DELEGATIONS_TO_REWARD_KEY) ); try_finish_mixnode_rewarding( @@ -1325,6 +1369,7 @@ pub mod tests { .unwrap(); let rewarding_validator_address = current_state.rewarding_validator_address; + #[allow(clippy::inconsistent_digit_grouping)] let mix_bond = Uint128::new(10000_000_000); let delegation_value = 2000_000000; @@ -1344,7 +1389,7 @@ pub mod tests { env.clone(), mock_info( &*format!("delegator{}", i), - &vec![coin(delegation_value, DENOM)], + &[coin(delegation_value, DENOM)], ), node_identity.clone(), ) @@ -1372,8 +1417,8 @@ pub mod tests { ) .unwrap(); assert_eq!( - res.attributes[3], - attr("more delegators to reward", false.to_string()) + false.to_string(), + must_find_attribute(&res.events[0], FURTHER_DELEGATIONS_TO_REWARD_KEY) ); try_finish_mixnode_rewarding( @@ -1406,6 +1451,7 @@ pub mod tests { .unwrap(); let rewarding_validator_address = current_state.rewarding_validator_address; + #[allow(clippy::inconsistent_digit_grouping)] let mix_bond = Uint128::new(10000_000_000); let delegation_value = 2000_000000; @@ -1425,7 +1471,7 @@ pub mod tests { env.clone(), mock_info( &*format!("delegator{:04}", i), - &vec![coin(delegation_value, DENOM)], + &[coin(delegation_value, DENOM)], ), node_identity.clone(), ) @@ -1453,8 +1499,8 @@ pub mod tests { ) .unwrap(); assert_eq!( - res.attributes[3], - attr("more delegators to reward", true.to_string()) + true.to_string(), + must_find_attribute(&res.events[0], FURTHER_DELEGATIONS_TO_REWARD_KEY) ); try_finish_mixnode_rewarding( @@ -1477,7 +1523,7 @@ pub mod tests { let delegation = test_helpers::read_delegation( &deps.storage, - node_identity.clone(), + node_identity, format!("delegator{:04}", MIXNODE_DELEGATORS_PAGE_LIMIT), ) .unwrap(); @@ -1493,6 +1539,8 @@ pub mod tests { let mut env = mock_env(); let node_owner: Addr = Addr::unchecked("bob"); + + #[allow(clippy::inconsistent_digit_grouping)] let node_identity = test_helpers::add_mixnode( node_owner.as_str(), coins(10000_000_000, DENOM), @@ -1512,7 +1560,7 @@ pub mod tests { env.clone(), mock_info( &*format!("delegator{:04}", i), - &vec![coin(base_delegation, DENOM)], + &[coin(base_delegation, DENOM)], ), node_identity.clone(), ) @@ -1531,7 +1579,7 @@ pub mod tests { for delegation in delegations_storage::delegations() .idx .mixnode - .prefix(node_identity.clone()) + .prefix(node_identity) .range(deps.as_ref().storage, None, None, Order::Ascending) { actual_reward += @@ -1546,6 +1594,8 @@ pub mod tests { // with paging let node_owner: Addr = Addr::unchecked("alice"); + + #[allow(clippy::inconsistent_digit_grouping)] let node_identity = test_helpers::add_mixnode( node_owner.as_str(), coins(10000_000_000, DENOM), @@ -1565,7 +1615,7 @@ pub mod tests { env.clone(), mock_info( &*format!("delegator{:04}", i), - &vec![coin(base_delegation, DENOM)], + &[coin(base_delegation, DENOM)], ), node_identity.clone(), ) @@ -1585,7 +1635,7 @@ pub mod tests { .idx .mixnode .prefix(node_identity.clone()) - .range(deps.as_ref().storage, None, None, Order::Ascending) + .range_raw(deps.as_ref().storage, None, None, Order::Ascending) { let (primary_key, delegation) = delegation.unwrap(); let delegator_reward = Uint128::new(delegation.amount.amount.u128() - base_delegation); @@ -1629,7 +1679,7 @@ pub mod tests { for delegation in delegations_storage::delegations() .idx .mixnode - .prefix(node_identity.clone()) + .prefix(node_identity) .range(deps.as_ref().storage, Some(start), None, Order::Ascending) { actual_reward += @@ -1686,7 +1736,7 @@ pub mod tests { try_begin_mixnode_rewarding( deps.as_mut(), - env.clone(), + env, mock_info(rewarding_validator_address.as_ref(), &[]), 1, ) @@ -1718,6 +1768,8 @@ pub mod tests { let rewarding_validator_address = current_state.rewarding_validator_address; let node_owner: Addr = Addr::unchecked("alice"); + + #[allow(clippy::inconsistent_digit_grouping)] let node_identity = test_helpers::add_mixnode( node_owner.as_str(), coins(10000_000_000, DENOM), @@ -1753,7 +1805,7 @@ pub mod tests { assert_eq!( Err(ContractError::MixnodeAlreadyRewarded { - identity: node_identity.clone() + identity: node_identity }), res ); @@ -1767,6 +1819,8 @@ pub mod tests { // there was another page of delegators, but they were already dealt with let node_owner: Addr = Addr::unchecked("bob"); + + #[allow(clippy::inconsistent_digit_grouping)] let node_identity = test_helpers::add_mixnode( node_owner.as_str(), coins(10000_000_000, DENOM), @@ -1777,10 +1831,7 @@ pub mod tests { try_delegate_to_mixnode( deps.as_mut(), env.clone(), - mock_info( - &*format!("delegator{:04}", i), - &vec![coin(2000_000000, DENOM)], - ), + mock_info(&*format!("delegator{:04}", i), &[coin(2000_000000, DENOM)]), node_identity.clone(), ) .unwrap(); @@ -1799,7 +1850,7 @@ pub mod tests { try_reward_mixnode( deps.as_mut(), - env.clone(), + env, info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), @@ -1825,7 +1876,7 @@ pub mod tests { assert_eq!( Err(ContractError::MixnodeAlreadyRewarded { - identity: node_identity.clone() + identity: node_identity }), res ); @@ -1848,6 +1899,7 @@ pub mod tests { .unwrap(); let rewarding_validator_address = current_state.rewarding_validator_address; + #[allow(clippy::inconsistent_digit_grouping)] let mix_bond = Uint128::new(10000_000_000); let delegation_value = 2000_000000; @@ -1869,7 +1921,7 @@ pub mod tests { env.clone(), mock_info( &*format!("delegator{:04}", i), - &vec![coin(delegation_value, DENOM)], + &[coin(delegation_value, DENOM)], ), node_identity.clone(), ) @@ -1889,7 +1941,7 @@ pub mod tests { try_reward_mixnode( deps.as_mut(), - env.clone(), + env, info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), @@ -1945,6 +1997,7 @@ pub mod tests { .unwrap(); let rewarding_validator_address = current_state.rewarding_validator_address; + #[allow(clippy::inconsistent_digit_grouping)] let mix_bond = Uint128::new(10000_000_000); let delegation_value = 2000_000000; @@ -1966,7 +2019,7 @@ pub mod tests { env.clone(), mock_info( &*format!("delegator{:04}", i), - &vec![coin(delegation_value, DENOM)], + &[coin(delegation_value, DENOM)], ), node_identity.clone(), ) @@ -1979,7 +2032,7 @@ pub mod tests { try_delegate_to_mixnode( deps.as_mut(), env.clone(), - mock_info("delegator0123", &vec![coin(delegation_value, DENOM)]), + mock_info("delegator0123", &[coin(delegation_value, DENOM)]), node_identity.clone(), ) .unwrap(); @@ -1989,7 +2042,7 @@ pub mod tests { env.clone(), mock_info( &*format!("delegator{:04}", 123 + MIXNODE_DELEGATORS_PAGE_LIMIT), - &vec![coin(delegation_value, DENOM)], + &[coin(delegation_value, DENOM)], ), node_identity.clone(), ) @@ -2008,7 +2061,7 @@ pub mod tests { try_reward_mixnode( deps.as_mut(), - env.clone(), + env, info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index a5994b42d2..94c273879e 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -5,7 +5,7 @@ use crate::error::ContractError; use crate::gateways::storage as gateways_storage; use crate::mixnodes::storage as mixnodes_storage; use cosmwasm_std::{Addr, Deps, Storage}; -use mixnet_contract::IdentityKeyRef; +use mixnet_contract_common::IdentityKeyRef; pub fn generate_storage_key(address: &Addr, proxy: Option<&Addr>) -> Vec { if let Some(proxy) = &proxy { diff --git a/contracts/mixnet/src/support/tests/fixtures.rs b/contracts/mixnet/src/support/tests/fixtures.rs index af4f6ab14f..cd1861e344 100644 --- a/contracts/mixnet/src/support/tests/fixtures.rs +++ b/contracts/mixnet/src/support/tests/fixtures.rs @@ -6,8 +6,8 @@ use crate::mixnodes::storage as mixnodes_storage; use crate::{mixnodes::storage::StoredMixnodeBond, support::tests}; use config::defaults::{DENOM, TOTAL_SUPPLY}; use cosmwasm_std::{coin, Addr, Coin}; -use mixnet_contract::mixnode::NodeRewardParams; -use mixnet_contract::{Gateway, GatewayBond, Layer, MixNode}; +use mixnet_contract_common::mixnode::NodeRewardParams; +use mixnet_contract_common::{Gateway, GatewayBond, Layer, MixNode}; pub fn mix_node_fixture() -> MixNode { MixNode { @@ -59,7 +59,6 @@ pub(crate) fn stored_mixnode_bond_fixture(owner: &str) -> mixnodes_storage::Stor ..super::fixtures::mix_node_fixture() }, None, - None, ) } diff --git a/contracts/mixnet/src/support/tests/messages.rs b/contracts/mixnet/src/support/tests/messages.rs index e7f8af7a91..fabea69ad4 100644 --- a/contracts/mixnet/src/support/tests/messages.rs +++ b/contracts/mixnet/src/support/tests/messages.rs @@ -1,4 +1,4 @@ -use mixnet_contract::{ExecuteMsg, Gateway, IdentityKey, MixNode}; +use mixnet_contract_common::{ExecuteMsg, Gateway, IdentityKey, MixNode}; use rand::thread_rng; use crate::support::tests; diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 7b0b99fff8..547c68ffdd 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -28,7 +28,7 @@ pub mod test_helpers { use cosmwasm_std::{Addr, StdResult, Storage}; use cosmwasm_std::{Empty, MemoryStorage}; use cw_storage_plus::PrimaryKey; - use mixnet_contract::{Delegation, Gateway, IdentityKeyRef, InstantiateMsg, MixNode}; + use mixnet_contract_common::{Delegation, Gateway, IdentityKeyRef, InstantiateMsg, MixNode}; use rand::thread_rng; pub fn add_mixnode(sender: &str, stake: Vec, deps: DepsMut) -> String { @@ -81,7 +81,7 @@ pub mod test_helpers { pub fn init_contract() -> OwnedDeps> { let mut deps = mock_dependencies(); let msg = InstantiateMsg { - rewarding_validator_address: config::defaults::REWARDING_VALIDATOR_ADDRESS.to_string(), + rewarding_validator_address: config::defaults::DEFAULT_REWARDING_VALIDATOR.to_string(), }; let env = mock_env(); let info = mock_info("creator", &[]); diff --git a/contracts/mixnet/src/support/tests/queries.rs b/contracts/mixnet/src/support/tests/queries.rs index 4c8d00195e..9a9900501b 100644 --- a/contracts/mixnet/src/support/tests/queries.rs +++ b/contracts/mixnet/src/support/tests/queries.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{ testing::{mock_env, MockApi, MockQuerier, MockStorage}, Addr, Coin, OwnedDeps, }; -use mixnet_contract::{ +use mixnet_contract_common::{ GatewayBond, MixNodeBond, PagedGatewayResponse, PagedMixnodeResponse, QueryMsg, }; diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index 931d7c2d75..dd61e3c325 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -14,14 +14,13 @@ exclude = [ crate-type = ["cdylib", "rlib"] [dependencies] -mixnet-contract = { path = "../../common/mixnet-contract" } +mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } +vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" } config = { path = "../../common/config" } cosmwasm-std = { version = "1.0.0-beta3"} -cw-storage-plus = { version = "0.10.3", features = ["iterator"] } +cw-storage-plus = { version = "0.11.1", features = ["iterator"] } schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -thiserror = { version = "1.0.23" } -rand = {version = "0.8.4", features = ["std_rng"]} -getrandom = { version = "0.2.3", features = ["js"]} \ No newline at end of file +thiserror = { version = "1.0.23" } \ No newline at end of file diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 44a68ae853..89f469f1b5 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -7,10 +7,15 @@ use crate::traits::{ use crate::vesting::{populate_vesting_periods, Account}; use config::defaults::{DEFAULT_MIXNET_CONTRACT_ADDRESS, DENOM}; use cosmwasm_std::{ - entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, + coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Timestamp, Uint128, }; -use mixnet_contract::{Gateway, IdentityKey, MixNode}; +use mixnet_contract_common::{Gateway, IdentityKey, MixNode}; +use vesting_contract_common::events::{ + new_ownership_transfer_event, new_periodic_vesting_account_event, + new_staking_address_update_event, new_track_gateway_unbond_event, + new_track_mixnode_unbond_event, new_track_undelegation_event, new_vested_coins_withdraw_event, +}; // We're using a 24 month vesting period with 3 months sub-periods. // There are 8 three month periods in two years @@ -96,6 +101,10 @@ pub fn try_withdraw_vested_coins( info: MessageInfo, deps: DepsMut, ) -> Result { + if amount.denom != DENOM { + return Err(ContractError::WrongDenom(amount.denom, DENOM.to_string())); + } + let address = info.sender.clone(); let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; if address != account.owner_address() { @@ -111,12 +120,16 @@ pub fn try_withdraw_vested_coins( let send_tokens = BankMsg::Send { to_address: account.owner_address().as_str().to_string(), - amount: vec![amount], + amount: vec![amount.clone()], }; Ok(Response::new() - .add_attribute("action", "whitdraw") - .add_message(send_tokens)) + .add_message(send_tokens) + .add_event(new_vested_coins_withdraw_event( + &address, + &amount, + &coin(new_balance, &amount.denom), + ))) } else { Err(ContractError::InsufficientSpendable( account.owner_address().as_str().to_string(), @@ -135,7 +148,7 @@ fn try_transfer_ownership( let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; if address == account.owner_address() { account.transfer_ownership(&to_address, deps.storage)?; - Ok(Response::default()) + Ok(Response::new().add_event(new_ownership_transfer_event(&address, &to_address))) } else { Err(ContractError::NotOwner(account.owner_address().to_string())) } @@ -150,8 +163,9 @@ fn try_update_staking_address( let to_address = to_address.and_then(|x| deps.api.addr_validate(&x).ok()); let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; if address == account.owner_address() { - account.update_staking_address(to_address, deps.storage)?; - Ok(Response::default()) + let old = account.staking_address().cloned(); + account.update_staking_address(to_address.clone(), deps.storage)?; + Ok(Response::new().add_event(new_staking_address_update_event(&old, &to_address))) } else { Err(ContractError::NotOwner(account.owner_address().to_string())) } @@ -186,7 +200,7 @@ pub fn try_track_unbond_gateway( } let account = account_from_address(owner, deps.storage, deps.api)?; account.try_track_unbond_gateway(amount, deps.storage)?; - Ok(Response::default()) + Ok(Response::new().add_event(new_track_gateway_unbond_event())) } pub fn try_bond_mixnode( @@ -217,7 +231,7 @@ pub fn try_track_unbond_mixnode( } let account = account_from_address(owner, deps.storage, deps.api)?; account.try_track_unbond_mixnode(amount, deps.storage)?; - Ok(Response::default()) + Ok(Response::new().add_event(new_track_mixnode_unbond_event())) } fn try_track_undelegation( @@ -232,7 +246,7 @@ fn try_track_undelegation( } let account = account_from_address(address, deps.storage, deps.api)?; account.track_undelegation(mix_identity, amount, deps.storage)?; - Ok(Response::default()) + Ok(Response::new().add_event(new_track_undelegation_event())) } fn try_delegate_to_mixnode( @@ -275,15 +289,24 @@ fn try_create_periodic_vesting_account( }; let start_time = start_time.unwrap_or_else(|| env.block.time.seconds()); let periods = populate_vesting_periods(start_time, NUM_VESTING_PERIODS); + + let start_time = Timestamp::from_seconds(start_time); Account::new( - owner_address, - staking_address, - coin, - Timestamp::from_seconds(start_time), + owner_address.clone(), + staking_address.clone(), + coin.clone(), + start_time, periods, deps.storage, )?; - Ok(Response::default()) + Ok( + Response::new().add_event(new_periodic_vesting_account_event( + &owner_address, + &coin, + &staking_address, + start_time, + )), + ) } #[entry_point] diff --git a/contracts/vesting/src/errors.rs b/contracts/vesting/src/errors.rs index 3651ac48b5..fa1b8fc5f7 100644 --- a/contracts/vesting/src/errors.rs +++ b/contracts/vesting/src/errors.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{Addr, StdError}; -use mixnet_contract::IdentityKey; +use mixnet_contract_common::IdentityKey; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -24,11 +24,11 @@ pub enum ContractError { ImprobableVestingAmount(u128), #[error("Address {0} has already bonded a node")] AlreadyBonded(String), - #[error("Recieved empty funds vector")] + #[error("Received empty funds vector")] EmptyFunds, - #[error("Recieved wrong denom: {0}, expected {1}")] + #[error("Received wrong denom: {0}, expected {1}")] WrongDenom(String, String), - #[error("Recieved multiple denoms, expected 1")] + #[error("Received multiple denoms, expected 1")] MultipleDenoms, #[error("No delegations found for account {0}, mix_identity {1}")] NoSuchDelegation(Addr, IdentityKey), diff --git a/contracts/vesting/src/messages.rs b/contracts/vesting/src/messages.rs index fffec2ca15..f32661818a 100644 --- a/contracts/vesting/src/messages.rs +++ b/contracts/vesting/src/messages.rs @@ -1,6 +1,5 @@ use cosmwasm_std::{Coin, Timestamp}; -use mixnet_contract::IdentityKey; -use mixnet_contract::{Gateway, MixNode}; +use mixnet_contract_common::{Gateway, IdentityKey, MixNode}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index f8227d8581..eb24ba5409 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -1,8 +1,9 @@ use crate::errors::ContractError; use crate::vesting::Account; use cosmwasm_std::{Addr, Api, Storage}; -use cw_storage_plus::Map; +use cw_storage_plus::{Item, Map}; +pub const KEY: Item = Item::new("key"); const ACCOUNTS: Map = Map::new("acc"); pub fn delete_account(address: &Addr, storage: &mut dyn Storage) -> Result<(), ContractError> { diff --git a/contracts/vesting/src/traits/bonding.rs b/contracts/vesting/src/traits/bonding.rs index 73ec75024b..9ab5066d72 100644 --- a/contracts/vesting/src/traits/bonding.rs +++ b/contracts/vesting/src/traits/bonding.rs @@ -1,6 +1,6 @@ use crate::errors::ContractError; use cosmwasm_std::{Coin, Env, Response, Storage}; -use mixnet_contract::{Gateway, MixNode}; +use mixnet_contract_common::{Gateway, MixNode}; pub trait MixnodeBondingAccount { fn try_bond_mixnode( diff --git a/contracts/vesting/src/traits/delegating_account.rs b/contracts/vesting/src/traits/delegating_account.rs index bc133911d9..c2d7104893 100644 --- a/contracts/vesting/src/traits/delegating_account.rs +++ b/contracts/vesting/src/traits/delegating_account.rs @@ -1,6 +1,6 @@ use crate::errors::ContractError; use cosmwasm_std::{Coin, Env, Response, Storage, Timestamp, Uint128}; -use mixnet_contract::IdentityKey; +use mixnet_contract_common::IdentityKey; pub trait DelegatingAccount { fn try_delegate_to_mixnode( diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index 20b67c971e..845279f355 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -3,8 +3,11 @@ use crate::traits::DelegatingAccount; use config::defaults::{DEFAULT_MIXNET_CONTRACT_ADDRESS, DENOM}; use cosmwasm_std::{wasm_execute, Coin, Env, Order, Response, Storage, Timestamp, Uint128}; use cw_storage_plus::Map; -use mixnet_contract::ExecuteMsg as MixnetExecuteMsg; -use mixnet_contract::IdentityKey; +use mixnet_contract_common::ExecuteMsg as MixnetExecuteMsg; +use mixnet_contract_common::IdentityKey; +use vesting_contract_common::events::{ + new_vesting_delegation_event, new_vesting_undelegation_event, +}; use super::Account; @@ -34,8 +37,8 @@ impl DelegatingAccount for Account { self.track_delegation(env.block.time, mix_identity, current_balance, coin, storage)?; Ok(Response::new() - .add_attribute("action", "delegate to mixnode on behalf") - .add_message(delegate_to_mixnode)) + .add_message(delegate_to_mixnode) + .add_event(new_vesting_delegation_event())) } fn try_undelegate_from_mixnode( @@ -64,8 +67,8 @@ impl DelegatingAccount for Account { )?; Ok(Response::new() - .add_attribute("action", "undelegate to mixnode on behalf") - .add_message(undelegate_from_mixnode)) + .add_message(undelegate_from_mixnode) + .add_event(new_vesting_undelegation_event())) } fn track_delegation( @@ -108,8 +111,8 @@ impl DelegatingAccount for Account { // Iterate over keys matching the prefix and remove them from the map let block_times = delegations - .prefix_de(mix_bytes) - .keys_de(storage, None, None, Order::Ascending) + .prefix(mix_bytes) + .keys(storage, None, None, Order::Ascending) // Scan will blow up on first error .scan((), |_, x| x.ok()) .collect::>(); diff --git a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs index b4100557b2..ce54d8ab60 100644 --- a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs @@ -3,7 +3,10 @@ use crate::errors::ContractError; use crate::traits::GatewayBondingAccount; use config::defaults::DEFAULT_MIXNET_CONTRACT_ADDRESS; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; -use mixnet_contract::{ExecuteMsg as MixnetExecuteMsg, Gateway}; +use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, Gateway}; +use vesting_contract_common::events::{ + new_vesting_gateway_bonding_event, new_vesting_gateway_unbonding_event, +}; use super::Account; @@ -50,8 +53,8 @@ impl GatewayBondingAccount for Account { self.save_gateway_pledge(pledge_data, storage)?; Ok(Response::new() - .add_attribute("action", "bond gateway on behalf") - .add_message(bond_gateway_msg)) + .add_message(bond_gateway_msg) + .add_event(new_vesting_gateway_bonding_event())) } fn try_unbond_gateway(&self, storage: &dyn Storage) -> Result { @@ -63,8 +66,8 @@ impl GatewayBondingAccount for Account { let unbond_msg = wasm_execute(DEFAULT_MIXNET_CONTRACT_ADDRESS, &msg, vec![])?; Ok(Response::new() - .add_attribute("action", "unbond gateway on behalf") - .add_message(unbond_msg)) + .add_message(unbond_msg) + .add_event(new_vesting_gateway_unbonding_event())) } else { Err(ContractError::NoBondFound( self.owner_address().as_str().to_string(), diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index e62f9fea00..ea78f580e1 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -3,7 +3,10 @@ use crate::errors::ContractError; use crate::traits::MixnodeBondingAccount; use config::defaults::DEFAULT_MIXNET_CONTRACT_ADDRESS; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; -use mixnet_contract::{ExecuteMsg as MixnetExecuteMsg, MixNode}; +use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, MixNode}; +use vesting_contract_common::events::{ + new_vesting_mixnode_bonding_event, new_vesting_mixnode_unbonding_event, +}; use super::Account; @@ -50,8 +53,8 @@ impl MixnodeBondingAccount for Account { self.save_mixnode_pledge(pledge_data, storage)?; Ok(Response::new() - .add_attribute("action", "bond mixnode on behalf") - .add_message(bond_mixnode_mag)) + .add_message(bond_mixnode_mag) + .add_event(new_vesting_mixnode_bonding_event())) } fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result { @@ -63,8 +66,8 @@ impl MixnodeBondingAccount for Account { let unbond_msg = wasm_execute(DEFAULT_MIXNET_CONTRACT_ADDRESS, &msg, vec![])?; Ok(Response::new() - .add_attribute("action", "unbond mixnode on behalf") - .add_message(unbond_msg)) + .add_message(unbond_msg) + .add_event(new_vesting_mixnode_unbonding_event())) } else { Err(ContractError::NoBondFound( self.owner_address().as_str().to_string(), diff --git a/contracts/vesting/src/vesting/account/mod.rs b/contracts/vesting/src/vesting/account/mod.rs index 424a6ca27f..b10e60f81e 100644 --- a/contracts/vesting/src/vesting/account/mod.rs +++ b/contracts/vesting/src/vesting/account/mod.rs @@ -1,12 +1,10 @@ use super::{PledgeData, VestingPeriod}; use crate::contract::NUM_VESTING_PERIODS; use crate::errors::ContractError; -use crate::storage::save_account; +use crate::storage::{save_account, KEY}; use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128}; use cw_storage_plus::{Item, Map}; -use mixnet_contract::IdentityKey; -use rand::rngs::StdRng; -use rand::{RngCore, SeedableRng}; +use mixnet_contract_common::IdentityKey; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -20,17 +18,10 @@ const BALANCE_SUFFIX: &str = "ba"; const PLEDGE_SUFFIX: &str = "bo"; const GATEWAY_SUFFIX: &str = "ga"; -fn generate_storage_key(b: &[u8], storage: &dyn Storage) -> Result { - let mut rng = StdRng::seed_from_u64(b.iter().fold(0, |acc, x| acc + *x as u64)); - // Be paranoid and check for collisions - loop { - let key = rng.next_u64().to_string(); - let balance_key = format!("{}{}", key, BALANCE_SUFFIX); - let balance: Item = Item::new(&balance_key); - if balance.may_load(storage)?.is_none() { - return Ok(key); - } - } +fn generate_storage_key(storage: &mut dyn Storage) -> Result { + let key = KEY.may_load(storage)?.unwrap_or(0) + 1; + KEY.save(storage, &key)?; + Ok(key) } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -52,7 +43,7 @@ impl Account { periods: Vec, storage: &mut dyn Storage, ) -> Result { - let storage_key = generate_storage_key(owner_address.as_bytes(), storage)?; + let storage_key = generate_storage_key(storage)?.to_string(); let amount = coin.amount; let account = Account { owner_address, @@ -205,8 +196,8 @@ impl Account { let key = self.delegations_key(); let delegations: Map<(&[u8], u64), Uint128> = Map::new(&key); delegations - .prefix_de(mix.as_bytes()) - .keys_de(storage, None, None, Order::Ascending) + .prefix(mix.as_bytes()) + .keys(storage, None, None, Order::Ascending) // Scan will blow up on first error .scan((), |_, x| x.ok()) .collect::>() diff --git a/contracts/vesting/src/vesting/account/vesting_account.rs b/contracts/vesting/src/vesting/account/vesting_account.rs index 00ffdac957..8ffc6f6852 100644 --- a/contracts/vesting/src/vesting/account/vesting_account.rs +++ b/contracts/vesting/src/vesting/account/vesting_account.rs @@ -119,7 +119,7 @@ impl VestingAccount for Account { let delegations: Map<(&[u8], u64), Uint128> = Map::new(&delegations_key); let delegations_keys = delegations - .keys_de(storage, None, None, Order::Ascending) + .keys(storage, None, None, Order::Ascending) .scan((), |_, x| x.ok()) .filter(|(_mix, block_time)| *block_time < start_time) .map(|(mix, block_time)| (mix, block_time)) diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index 35d6d03d08..b0a0f6e71a 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -46,7 +46,7 @@ mod tests { use config::defaults::DENOM; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coins, Addr, Coin, Timestamp, Uint128}; - use mixnet_contract::{Gateway, MixNode}; + use mixnet_contract_common::{Gateway, MixNode}; #[test] fn test_account_creation() { @@ -78,10 +78,7 @@ mod tests { // Test key collision avoidance let account_again = vesting_account_fixture(&mut deps.storage, &env); - assert_eq!( - created_account.balance_key(), - "5032709489228919411ba".to_string() - ); + assert_eq!(created_account.balance_key(), "1ba".to_string()); assert_ne!(created_account.balance_key(), account_again.balance_key()); } @@ -127,7 +124,7 @@ mod tests { let msg = ExecuteMsg::WithdrawVestedCoins { amount: Coin { amount: Uint128::new(1), - denom: "nym".to_string(), + denom: DENOM.to_string(), }, }; let info = mock_info("new_owner", &[]); diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 3719d89d7a..24a9357530 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -22,6 +22,6 @@ log = "0.4.0" pretty_env_logger = "0.4.0" thiserror = "1.0.29" -mixnet-contract = { path = "../common/mixnet-contract" } +mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } network-defaults = { path = "../common/network-defaults" } validator-client = { path = "../common/client-libs/validator-client", features=["nymd-client"] } diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index a09231ceae..55cd428798 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -1,10 +1,12 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_nodes::location::{GeoLocation, Location}; +use crate::state::ExplorerApiStateContext; use log::{info, warn}; use reqwest::Error as ReqwestError; use thiserror::Error; -use crate::mix_nodes::{GeoLocation, Location}; -use crate::state::ExplorerApiStateContext; - pub(crate) struct GeoLocateTask { state: ExplorerApiStateContext, } @@ -34,7 +36,15 @@ impl GeoLocateTask { } async fn locate_mix_nodes(&mut self) { - let mixnode_bonds = self.state.inner.mix_nodes.get().await.value; + // I'm unwrapping to the default value to get rid of an extra indentation level from the `if let Some(...) = ...` + // If the value is None, we'll unwrap to an empty hashmap and the `values()` loop won't do any work anyway + let mixnode_bonds = self + .state + .inner + .mix_nodes + .get_mixnodes() + .await + .unwrap_or_default(); for (i, cache_item) in mixnode_bonds.values().enumerate() { if self diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index 7312962d77..999e9f3fc7 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -36,13 +36,11 @@ impl ExplorerApi { async fn run(&mut self) { info!("Explorer API starting up..."); - info!( - "Using validator API - {}", - network_defaults::default_api_endpoints()[0].clone() - ); + let validator_api_url = network_defaults::default_api_endpoints()[0].clone(); + info!("Using validator API - {}", validator_api_url); // spawn concurrent tasks - mix_nodes::tasks::MixNodesTasks::new(self.state.clone()).start(); + mix_nodes::tasks::MixNodesTasks::new(self.state.clone(), validator_api_url).start(); country_statistics::distribution::CountryStatisticsDistributionTask::new( self.state.clone(), ) diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index 79b2b5750e..c3888f393e 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -1,16 +1,16 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_node::models::{NodeDescription, NodeStats}; +use crate::mix_nodes::delegations::{get_mixnode_delegations, get_single_mixnode_delegations}; +use crate::state::ExplorerApiStateContext; +use mixnet_contract_common::Delegation; use reqwest::Error as ReqwestError; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; -use serde::Serialize; - -use mixnet_contract::{Addr, Coin, Delegation, Layer, MixNode}; - -use crate::mix_node::models::{NodeDescription, NodeStats}; -use crate::mix_nodes::{get_mixnode_delegations, get_single_mixnode_delegations, Location}; -use crate::state::ExplorerApiStateContext; pub fn mix_node_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![ @@ -21,16 +21,6 @@ pub fn mix_node_make_default_routes(settings: &OpenApiSettings) -> (Vec, ] } -#[derive(Clone, Debug, Serialize, JsonSchema)] -pub(crate) struct PrettyMixNodeBondWithLocation { - pub location: Option, - pub pledge_amount: Coin, - pub total_delegation: Coin, - pub owner: Addr, - pub layer: Layer, - pub mix_node: MixNode, -} - #[openapi(tag = "mix_node")] #[get("//delegations")] pub(crate) async fn get_delegations(pubkey: &str) -> Json> { diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 8aca0dc33e..835bfe3991 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -1,11 +1,33 @@ -use std::sync::Arc; -use std::time::SystemTime; - -use serde::Deserialize; -use serde::Serialize; -use tokio::sync::RwLock; +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use crate::mix_node::cache::Cache; +use crate::mix_nodes::location::Location; +use mixnet_contract_common::{Addr, Coin, Layer, MixNode}; +use serde::Deserialize; +use serde::Serialize; +use std::sync::Arc; +use std::time::SystemTime; +use tokio::sync::RwLock; + +#[derive(Clone, Debug, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum MixnodeStatus { + Active, // in both the active set and the rewarded set + Standby, // only in the rewarded set + Inactive, // in neither the rewarded set nor the active set +} + +#[derive(Clone, Debug, Serialize, JsonSchema)] +pub(crate) struct PrettyDetailedMixNodeBond { + pub location: Option, + pub status: MixnodeStatus, + pub pledge_amount: Coin, + pub total_delegation: Coin, + pub owner: Addr, + pub layer: Layer, + pub mix_node: MixNode, +} pub(crate) struct MixNodeCache { pub(crate) descriptions: Cache, diff --git a/explorer-api/src/mix_nodes/delegations.rs b/explorer-api/src/mix_nodes/delegations.rs new file mode 100644 index 0000000000..a6f0ee1c05 --- /dev/null +++ b/explorer-api/src/mix_nodes/delegations.rs @@ -0,0 +1,31 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use mixnet_contract_common::Delegation; + +pub(crate) async fn get_single_mixnode_delegations(pubkey: &str) -> Vec { + let client = super::utils::new_nymd_client(); + let delegates = match client + .get_all_nymd_single_mixnode_delegations(pubkey.to_string()) + .await + { + Ok(result) => result, + Err(e) => { + error!("Could not get delegations for mix node {}: {:?}", pubkey, e); + vec![] + } + }; + delegates +} + +pub(crate) async fn get_mixnode_delegations() -> Vec { + let client = super::utils::new_nymd_client(); + let delegates = match client.get_all_network_delegations().await { + Ok(result) => result, + Err(e) => { + error!("Could not get all mix delegations: {:?}", e); + vec![] + } + }; + delegates +} diff --git a/explorer-api/src/mix_nodes/http.rs b/explorer-api/src/mix_nodes/http.rs index efe75f6221..a2b8b0eaba 100644 --- a/explorer-api/src/mix_nodes/http.rs +++ b/explorer-api/src/mix_nodes/http.rs @@ -1,12 +1,11 @@ +use crate::mix_node::models::PrettyDetailedMixNodeBond; +use crate::state::ExplorerApiStateContext; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; -use crate::mix_node::http::PrettyMixNodeBondWithLocation; -use crate::state::ExplorerApiStateContext; - pub fn mix_nodes_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![settings: list] } @@ -15,6 +14,6 @@ pub fn mix_nodes_make_default_routes(settings: &OpenApiSettings) -> (Vec, #[get("/")] pub(crate) async fn list( state: &State, -) -> Json> { - Json(state.inner.mix_nodes.get_mixnodes_with_location().await) +) -> Json> { + Json(state.inner.mix_nodes.get_detailed_mixnodes().await) } diff --git a/explorer-api/src/mix_nodes/location.rs b/explorer-api/src/mix_nodes/location.rs new file mode 100644 index 0000000000..80f24f6635 --- /dev/null +++ b/explorer-api/src/mix_nodes/location.rs @@ -0,0 +1,62 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_nodes::utils::map_2_letter_to_3_letter_country_code; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::{Duration, SystemTime}; + +pub(crate) type LocationCache = HashMap; + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub(crate) struct GeoLocation { + pub(crate) ip: String, + pub(crate) country_code: String, + pub(crate) country_name: String, + pub(crate) region_code: String, + pub(crate) region_name: String, + pub(crate) city: String, + pub(crate) zip_code: String, + pub(crate) time_zone: String, + pub(crate) latitude: f32, + pub(crate) longitude: f32, + pub(crate) metro_code: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct LocationCacheItem { + pub(crate) location: Option, + pub(crate) valid_until: SystemTime, +} + +#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize)] +pub(crate) struct Location { + pub(crate) two_letter_iso_country_code: String, + pub(crate) three_letter_iso_country_code: String, + pub(crate) country_name: String, + pub(crate) lat: f32, + pub(crate) lng: f32, +} + +impl Location { + pub(crate) fn new(geo_location: GeoLocation) -> Self { + let three_letter_iso_country_code = map_2_letter_to_3_letter_country_code(&geo_location); + Location { + country_name: geo_location.country_name, + two_letter_iso_country_code: geo_location.country_code, + three_letter_iso_country_code, + lat: geo_location.latitude, + lng: geo_location.longitude, + } + } +} + +impl LocationCacheItem { + pub(crate) fn new_from_location(location: Option) -> Self { + LocationCacheItem { + location, + valid_until: SystemTime::now() + Duration::from_secs(60 * 60 * 24), // valid for 1 day + } + } +} diff --git a/explorer-api/src/mix_nodes/mod.rs b/explorer-api/src/mix_nodes/mod.rs index dcbd5638e2..dacff38e32 100644 --- a/explorer-api/src/mix_nodes/mod.rs +++ b/explorer-api/src/mix_nodes/mod.rs @@ -1,241 +1,14 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::time::Duration; + +pub(crate) mod delegations; pub(crate) mod http; +pub(crate) mod location; +pub(crate) mod models; pub(crate) mod tasks; -mod utils; +pub(crate) mod utils; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, SystemTime}; - -use rocket::tokio::sync::RwLock; -use serde::{Deserialize, Serialize}; - -use crate::mix_node::http::PrettyMixNodeBondWithLocation; -use crate::mix_nodes::utils::map_2_letter_to_3_letter_country_code; -use mixnet_contract::{Delegation, MixNodeBond}; -use network_defaults::{ - default_api_endpoints, default_nymd_endpoints, DEFAULT_MIXNET_CONTRACT_ADDRESS, -}; -use validator_client::nymd::QueryNymdClient; - -pub(crate) type LocationCache = HashMap; - -#[allow(dead_code)] -#[derive(Debug, Deserialize)] -pub(crate) struct GeoLocation { - pub(crate) ip: String, - pub(crate) country_code: String, - pub(crate) country_name: String, - pub(crate) region_code: String, - pub(crate) region_name: String, - pub(crate) city: String, - pub(crate) zip_code: String, - pub(crate) time_zone: String, - pub(crate) latitude: f32, - pub(crate) longitude: f32, - pub(crate) metro_code: u32, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub(crate) struct LocationCacheItem { - pub(crate) location: Option, - pub(crate) valid_until: SystemTime, -} - -#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize)] -pub(crate) struct Location { - pub(crate) two_letter_iso_country_code: String, - pub(crate) three_letter_iso_country_code: String, - pub(crate) country_name: String, - pub(crate) lat: f32, - pub(crate) lng: f32, -} - -impl Location { - pub(crate) fn new(geo_location: GeoLocation) -> Self { - let three_letter_iso_country_code = map_2_letter_to_3_letter_country_code(&geo_location); - Location { - country_name: geo_location.country_name, - two_letter_iso_country_code: geo_location.country_code, - three_letter_iso_country_code, - lat: geo_location.latitude, - lng: geo_location.longitude, - } - } -} - -impl LocationCacheItem { - pub(crate) fn new_from_location(location: Option) -> Self { - LocationCacheItem { - location, - valid_until: SystemTime::now() + Duration::from_secs(60 * 60 * 24), // valid for 1 day - } - } -} - -#[derive(Clone, Debug)] -pub(crate) struct MixNodesResult { - pub(crate) valid_until: SystemTime, - pub(crate) value: HashMap, - location_cache: LocationCache, -} - -#[derive(Clone)] -pub(crate) struct ThreadsafeMixNodesResult { - inner: Arc>, -} - -impl ThreadsafeMixNodesResult { - pub(crate) fn new() -> Self { - ThreadsafeMixNodesResult { - inner: Arc::new(RwLock::new(MixNodesResult { - value: HashMap::new(), - valid_until: SystemTime::now() - Duration::from_secs(60), // in the past - location_cache: LocationCache::new(), - })), - } - } - - pub(crate) fn new_with_location_cache(location_cache: LocationCache) -> Self { - ThreadsafeMixNodesResult { - inner: Arc::new(RwLock::new(MixNodesResult { - value: HashMap::new(), - valid_until: SystemTime::now() - Duration::from_secs(60), // in the past - location_cache, - })), - } - } - - pub(crate) async fn is_location_valid(&self, identity_key: &str) -> bool { - self.inner - .read() - .await - .location_cache - .get(identity_key) - .map(|cache_item| cache_item.valid_until > SystemTime::now()) - .unwrap_or(false) - } - - pub(crate) async fn get_location_cache(&self) -> LocationCache { - self.inner.read().await.location_cache.clone() - } - - pub(crate) async fn set_location(&self, identity_key: &str, location: Option) { - let mut guard = self.inner.write().await; - - // cache the location for this mix node so that it can be used when the mix node list is refreshed - guard.location_cache.insert( - identity_key.to_string(), - LocationCacheItem::new_from_location(location), - ); - } - - pub(crate) async fn get(&self) -> MixNodesResult { - // check ttl - let valid_until = self.inner.read().await.valid_until; - - if valid_until < SystemTime::now() { - // force reload - self.refresh().await; - } - - // return in-memory cache - self.inner.read().await.clone() - } - - pub(crate) async fn get_mixnodes_with_location(&self) -> Vec { - let guard = self.inner.read().await; - guard - .value - .values() - .map(|bond| { - let location = guard.location_cache.get(&bond.mix_node.identity_key); - let copy = bond.clone(); - PrettyMixNodeBondWithLocation { - location: location.and_then(|l| l.location.clone()), - pledge_amount: copy.pledge_amount, - total_delegation: copy.total_delegation, - owner: copy.owner, - layer: copy.layer, - mix_node: copy.mix_node, - } - }) - .collect() - } - - pub(crate) async fn refresh(&self) { - // get mixnodes and cache the new value - let value = retrieve_mixnodes().await; - let location_cache = self.inner.read().await.location_cache.clone(); - *self.inner.write().await = MixNodesResult { - value: value - .into_iter() - .map(|bond| (bond.mix_node.identity_key.to_string(), bond)) - .collect(), - valid_until: SystemTime::now() + Duration::from_secs(30), - location_cache, - }; - } -} - -pub(crate) async fn retrieve_mixnodes() -> Vec { - let client = new_validator_client(); - - info!("About to retrieve mixnode bonds..."); - - let bonds: Vec = match client.get_cached_mixnodes().await { - Ok(result) => result, - Err(e) => { - error!("Unable to retrieve mixnode bonds: {:?}", e); - vec![] - } - }; - info!("Fetched {} mixnode bonds", bonds.len()); - bonds -} - -pub(crate) async fn get_single_mixnode_delegations(pubkey: &str) -> Vec { - let client = new_nymd_client(); - let delegates = match client - .get_all_nymd_single_mixnode_delegations(pubkey.to_string()) - .await - { - Ok(result) => result, - Err(e) => { - error!("Could not get delegations for mix node {}: {:?}", pubkey, e); - vec![] - } - }; - delegates -} - -pub(crate) async fn get_mixnode_delegations() -> Vec { - let client = new_nymd_client(); - let delegates = match client.get_all_network_delegations().await { - Ok(result) => result, - Err(e) => { - error!("Could not get all mix delegations: {:?}", e); - vec![] - } - }; - delegates -} - -fn new_nymd_client() -> validator_client::Client { - let mixnet_contract = DEFAULT_MIXNET_CONTRACT_ADDRESS.to_string(); - let nymd_url = default_nymd_endpoints()[0].clone(); - let api_url = default_api_endpoints()[0].clone(); - - let client_config = validator_client::Config::new( - nymd_url, - api_url, - Some(mixnet_contract.parse().unwrap()), - None, - ); - - validator_client::Client::new_query(client_config).expect("Failed to connect to nymd!") -} - -// TODO: inject constants -fn new_validator_client() -> validator_client::ApiClient { - validator_client::ApiClient::new(default_api_endpoints()[0].clone()) -} +pub(crate) const MIXNODES_CACHE_REFRESH_RATE: Duration = Duration::from_secs(30); +pub(crate) const MIXNODES_CACHE_ENTRY_TTL: Duration = Duration::from_secs(60); diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs new file mode 100644 index 0000000000..7a07131fc1 --- /dev/null +++ b/explorer-api/src/mix_nodes/models.rs @@ -0,0 +1,150 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_node::models::{MixnodeStatus, PrettyDetailedMixNodeBond}; +use crate::mix_nodes::location::{Location, LocationCache, LocationCacheItem}; +use crate::mix_nodes::MIXNODES_CACHE_ENTRY_TTL; +use mixnet_contract_common::MixNodeBond; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use std::time::{Duration, SystemTime}; +use tokio::sync::RwLock; + +#[derive(Clone, Debug)] +pub(crate) struct MixNodesResult { + pub(crate) valid_until: SystemTime, + pub(crate) all_mixnodes: HashMap, + active_mixnodes: HashSet, + rewarded_mixnodes: HashSet, +} + +impl MixNodesResult { + fn new() -> Self { + MixNodesResult { + valid_until: SystemTime::now() - Duration::from_secs(60), // in the past + all_mixnodes: HashMap::new(), + active_mixnodes: HashSet::new(), + rewarded_mixnodes: HashSet::new(), + } + } + + fn determine_node_status(&self, public_key: &str) -> MixnodeStatus { + if self.active_mixnodes.contains(public_key) { + MixnodeStatus::Active + } else if self.rewarded_mixnodes.contains(public_key) { + MixnodeStatus::Standby + } else { + MixnodeStatus::Inactive + } + } + + fn is_valid(&self) -> bool { + self.valid_until >= SystemTime::now() + } + + fn get_mixnode(&self, pubkey: &str) -> Option { + if self.is_valid() { + self.all_mixnodes.get(pubkey).cloned() + } else { + None + } + } + + fn get_mixnodes(&self) -> Option> { + if self.is_valid() { + Some(self.all_mixnodes.clone()) + } else { + None + } + } +} + +#[derive(Clone)] +pub(crate) struct ThreadsafeMixNodesResult { + mixnode_results: Arc>, + location_cache: Arc>, +} + +impl ThreadsafeMixNodesResult { + pub(crate) fn new() -> Self { + ThreadsafeMixNodesResult { + mixnode_results: Arc::new(RwLock::new(MixNodesResult::new())), + location_cache: Arc::new(RwLock::new(LocationCache::new())), + } + } + + pub(crate) fn new_with_location_cache(location_cache: LocationCache) -> Self { + ThreadsafeMixNodesResult { + mixnode_results: Arc::new(RwLock::new(MixNodesResult::new())), + location_cache: Arc::new(RwLock::new(location_cache)), + } + } + + pub(crate) async fn is_location_valid(&self, identity_key: &str) -> bool { + self.location_cache + .read() + .await + .get(identity_key) + .map(|cache_item| cache_item.valid_until > SystemTime::now()) + .unwrap_or(false) + } + + pub(crate) async fn get_location_cache(&self) -> LocationCache { + self.location_cache.read().await.clone() + } + + pub(crate) async fn set_location(&self, identity_key: &str, location: Option) { + // cache the location for this mix node so that it can be used when the mix node list is refreshed + self.location_cache.write().await.insert( + identity_key.to_string(), + LocationCacheItem::new_from_location(location), + ); + } + + pub(crate) async fn get_mixnode(&self, pubkey: &str) -> Option { + self.mixnode_results.read().await.get_mixnode(pubkey) + } + + pub(crate) async fn get_mixnodes(&self) -> Option> { + self.mixnode_results.read().await.get_mixnodes() + } + + pub(crate) async fn get_detailed_mixnodes(&self) -> Vec { + let mixnodes_guard = self.mixnode_results.read().await; + let location_guard = self.location_cache.read().await; + + mixnodes_guard + .all_mixnodes + .values() + .map(|bond| { + let location = location_guard.get(&bond.mix_node.identity_key); + let copy = bond.clone(); + PrettyDetailedMixNodeBond { + location: location.and_then(|l| l.location.clone()), + status: mixnodes_guard.determine_node_status(&bond.mix_node.identity_key), + pledge_amount: copy.pledge_amount, + total_delegation: copy.total_delegation, + owner: copy.owner, + layer: copy.layer, + mix_node: copy.mix_node, + } + }) + .collect() + } + + pub(crate) async fn update_cache( + &self, + all_bonds: Vec, + rewarded_nodes: HashSet, + active_nodes: HashSet, + ) { + let mut guard = self.mixnode_results.write().await; + guard.all_mixnodes = all_bonds + .into_iter() + .map(|bond| (bond.mix_node.identity_key.to_string(), bond)) + .collect(); + guard.rewarded_mixnodes = rewarded_nodes; + guard.active_mixnodes = active_nodes; + guard.valid_until = SystemTime::now() + MIXNODES_CACHE_ENTRY_TTL; + } +} diff --git a/explorer-api/src/mix_nodes/tasks.rs b/explorer-api/src/mix_nodes/tasks.rs index f89cd61217..b6257c1660 100644 --- a/explorer-api/src/mix_nodes/tasks.rs +++ b/explorer-api/src/mix_nodes/tasks.rs @@ -1,24 +1,93 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_nodes::MIXNODES_CACHE_REFRESH_RATE; use crate::state::ExplorerApiStateContext; +use mixnet_contract_common::MixNodeBond; +use reqwest::Url; +use std::future::Future; +use validator_client::ValidatorClientError; pub(crate) struct MixNodesTasks { state: ExplorerApiStateContext, + validator_api_client: validator_client::ApiClient, } impl MixNodesTasks { - pub(crate) fn new(state: ExplorerApiStateContext) -> Self { - MixNodesTasks { state } + pub(crate) fn new(state: ExplorerApiStateContext, validator_api_endpoint: Url) -> Self { + MixNodesTasks { + state, + validator_api_client: validator_client::ApiClient::new(validator_api_endpoint), + } + } + + // a helper to remove duplicate code when grabbing active/rewarded/all mixnodes + async fn retrieve_mixnodes<'a, F, Fut>(&'a self, f: F) -> Vec + where + F: FnOnce(&'a validator_client::ApiClient) -> Fut, + Fut: Future, ValidatorClientError>>, + { + let bonds = match f(&self.validator_api_client).await { + Ok(result) => result, + Err(e) => { + error!("Unable to retrieve mixnode bonds: {:?}", e); + vec![] + } + }; + + info!("Fetched {} mixnode bonds", bonds.len()); + bonds + } + + async fn retrieve_all_mixnodes(&self) -> Vec { + info!("About to retrieve all mixnode bonds..."); + self.retrieve_mixnodes(validator_client::ApiClient::get_cached_mixnodes) + .await + } + + async fn retrieve_rewarded_mixnodes(&self) -> Vec { + info!("About to retrieve rewarded mixnode bonds..."); + self.retrieve_mixnodes(validator_client::ApiClient::get_cached_rewarded_mixnodes) + .await + } + + async fn retrieve_active_mixnodes(&self) -> Vec { + info!("About to retrieve active mixnode bonds..."); + self.retrieve_mixnodes(validator_client::ApiClient::get_cached_active_mixnodes) + .await + } + + async fn update_mixnode_cache(&self) { + let all_bonds = self.retrieve_all_mixnodes().await; + let rewarded_nodes = self + .retrieve_rewarded_mixnodes() + .await + .into_iter() + .map(|bond| bond.mix_node.identity_key) + .collect(); + let active_nodes = self + .retrieve_active_mixnodes() + .await + .into_iter() + .map(|bond| bond.mix_node.identity_key) + .collect(); + self.state + .inner + .mix_nodes + .update_cache(all_bonds, rewarded_nodes, active_nodes) + .await; } pub(crate) fn start(self) { info!("Spawning mix nodes task runner..."); tokio::spawn(async move { - let mut interval_timer = tokio::time::interval(std::time::Duration::from_secs(2)); + let mut interval_timer = tokio::time::interval(MIXNODES_CACHE_REFRESH_RATE); loop { // wait for the next interval tick interval_timer.tick().await; info!("Updating mix node cache..."); - self.state.inner.mix_nodes.refresh().await; + self.update_mixnode_cache().await; info!("Done"); } }); diff --git a/explorer-api/src/mix_nodes/utils.rs b/explorer-api/src/mix_nodes/utils.rs index 7e46ce7afd..542bfe8660 100644 --- a/explorer-api/src/mix_nodes/utils.rs +++ b/explorer-api/src/mix_nodes/utils.rs @@ -1,5 +1,12 @@ -use crate::mix_nodes::GeoLocation; +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_nodes::location::GeoLocation; use isocountry::CountryCode; +use network_defaults::{ + default_api_endpoints, default_nymd_endpoints, DEFAULT_MIXNET_CONTRACT_ADDRESS, +}; +use validator_client::nymd::QueryNymdClient; pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String { match CountryCode::for_alpha2(&geo.country_code) { @@ -13,3 +20,18 @@ pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String } } } + +pub(crate) fn new_nymd_client() -> validator_client::Client { + let mixnet_contract = DEFAULT_MIXNET_CONTRACT_ADDRESS.to_string(); + let nymd_url = default_nymd_endpoints()[0].clone(); + let api_url = default_api_endpoints()[0].clone(); + + let client_config = validator_client::Config::new( + nymd_url, + api_url, + Some(mixnet_contract.parse().unwrap()), + None, + ); + + validator_client::Client::new_query(client_config).expect("Failed to connect to nymd!") +} diff --git a/explorer-api/src/ping/http.rs b/explorer-api/src/ping/http.rs index 006938bec6..67a2730bb7 100644 --- a/explorer-api/src/ping/http.rs +++ b/explorer-api/src/ping/http.rs @@ -8,7 +8,7 @@ use rocket_okapi::okapi::openapi3::OpenApi; use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; -use mixnet_contract::MixNodeBond; +use mixnet_contract_common::MixNodeBond; use crate::ping::models::PingResponse; use crate::state::ExplorerApiStateContext; diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 3202abc9ea..676c4f9423 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -5,13 +5,14 @@ use chrono::{DateTime, Utc}; use log::info; use serde::{Deserialize, Serialize}; -use mixnet_contract::MixNodeBond; +use mixnet_contract_common::MixNodeBond; use crate::country_statistics::country_nodes_distribution::{ ConcurrentCountryNodesDistribution, CountryNodesDistribution, }; use crate::mix_node::models::ThreadsafeMixNodeCache; -use crate::mix_nodes::{LocationCache, ThreadsafeMixNodesResult}; +use crate::mix_nodes::location::LocationCache; +use crate::mix_nodes::models::ThreadsafeMixNodesResult; use crate::ping::models::ThreadsafePingCache; // TODO: change to an environment variable with a default value @@ -27,7 +28,7 @@ pub struct ExplorerApiState { impl ExplorerApiState { pub(crate) async fn get_mix_node(&self, pubkey: &str) -> Option { - self.mix_nodes.get().await.value.get(pubkey).cloned() + self.mix_nodes.get_mixnode(pubkey).await } } diff --git a/gateway/gateway-requests/src/registration/handshake/shared_key.rs b/gateway/gateway-requests/src/registration/handshake/shared_key.rs index afa0d2a842..23c69409cc 100644 --- a/gateway/gateway-requests/src/registration/handshake/shared_key.rs +++ b/gateway/gateway-requests/src/registration/handshake/shared_key.rs @@ -148,9 +148,9 @@ impl SharedKeys { pub fn to_bytes(&self) -> Vec { self.encryption_key - .to_vec() - .into_iter() - .chain(self.mac_key.to_vec().into_iter()) + .iter() + .copied() + .chain(self.mac_key.iter().copied()) .collect() } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs b/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs index 2c0cb6e8ea..f163daae6b 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs @@ -20,7 +20,7 @@ use credentials::token::bandwidth::TokenCredential; use crypto::asymmetric::identity::{PublicKey, Signature}; use gateway_client::bandwidth::eth_contract; use network_defaults::{ - BANDWIDTH_CLAIM_CONTRACT_ADDRESS, DENOM, ETH_EVENT_NAME, ETH_MIN_BLOCK_DEPTH, + DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS, DENOM, ETH_EVENT_NAME, ETH_MIN_BLOCK_DEPTH, }; use validator_client::nymd::{ AccountId, CosmosCoin, Decimal, Denom, NymdClient, SigningNymdClient, @@ -44,7 +44,7 @@ impl ERC20Bridge { Mnemonic::from_str(&cosmos_mnemonic).expect("Invalid Cosmos mnemonic provided"); let nymd_client = NymdClient::connect_with_mnemonic( nymd_url.as_ref(), - AccountId::from_str(BANDWIDTH_CLAIM_CONTRACT_ADDRESS).ok(), + AccountId::from_str(DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS).ok(), None, mnemonic, None, diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 7ab0c36296..0eb700103b 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -164,6 +164,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874f8444adcb4952a8bc51305c8be95c8ec8237bb0d2e78d2e039f771f8828a0" + [[package]] name = "bincode" version = "1.3.3" @@ -175,14 +181,16 @@ dependencies = [ [[package]] name = "bip32" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d0f0fc59c7ba0333eed9dcc1b6980baa7b7a4dc7c6c5885994d0674f7adf34" +checksum = "873faa4363bfc54c36a48321da034c92a0645a363eed34d948683ffc1706e37f" dependencies = [ "bs58", - "hkd32", "hmac", - "k256", + "k256 0.10.1", + "once_cell", + "pbkdf2", + "rand_core 0.6.3", "ripemd160", "sha2", "subtle 2.4.1", @@ -292,8 +300,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54757888b09a69be70b5ec303e382a74227392086ba808cb01eeca29233a2397" dependencies = [ "digest 0.9.0", - "ff", - "group", + "ff 0.10.1", + "group 0.10.0", "pairing", "rand_core 0.6.3", "subtle 2.4.1", @@ -555,12 +563,25 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b" +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "contracts-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -651,8 +672,9 @@ dependencies = [ [[package]] name = "cosmos-sdk-proto" -version = "0.8.0" -source = "git+https://github.com/cosmos/cosmos-rust?rev=e5a1872083abb3d88fa62dda966e7f5408deba58#e5a1872083abb3d88fa62dda966e7f5408deba58" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0254ffee603f5301d6a66963d9e1cc5091479c22e2e925e1f7689c8027a0828" dependencies = [ "prost", "prost-types", @@ -661,15 +683,16 @@ dependencies = [ [[package]] name = "cosmrs" -version = "0.3.0" -source = "git+https://github.com/cosmos/cosmos-rust?rev=e5a1872083abb3d88fa62dda966e7f5408deba58#e5a1872083abb3d88fa62dda966e7f5408deba58" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "505ea048e9ff2f906d6b954f9f8157d903ca468bfb301d906b40ecc25ba6838d" dependencies = [ "bip32", "cosmos-sdk-proto", - "ecdsa", + "ecdsa 0.13.4", "eyre", "getrandom 0.2.3", - "k256", + "k256 0.10.1", "prost", "prost-types", "rand_core 0.6.3", @@ -689,7 +712,7 @@ checksum = "a380b87642204557629c9b72988c47b55fbfe6d474960adba56b22331504956a" dependencies = [ "digest 0.9.0", "ed25519-zebra", - "k256", + "k256 0.9.6", "rand_core 0.5.1", "thiserror", ] @@ -833,6 +856,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +dependencies = [ + "generic-array 0.14.4", + "rand_core 0.6.3", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "crypto-mac" version = "0.7.0" @@ -919,9 +954,9 @@ dependencies = [ [[package]] name = "cw-storage-plus" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b8b840947313c1a1cccf056836cd79a60b4526bdcd6582995be37dc97be4ae" +checksum = "7d7ee1963302b0ac2a9d42fe0faec826209c17452bfd36fbfd9d002a88929261" dependencies = [ "cosmwasm-std", "schemars", @@ -989,7 +1024,16 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" dependencies = [ - "const-oid", + "const-oid 0.6.2", +] + +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid 0.7.1", ] [[package]] @@ -1147,12 +1191,24 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372" dependencies = [ - "der", - "elliptic-curve", + "der 0.4.5", + "elliptic-curve 0.10.6", "hmac", "signature", ] +[[package]] +name = "ecdsa" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9" +dependencies = [ + "der 0.5.1", + "elliptic-curve 0.11.6", + "rfc6979", + "signature", +] + [[package]] name = "ed25519" version = "1.3.0" @@ -1202,16 +1258,33 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "beca177dcb8eb540133e7680baff45e7cc4d93bf22002676cec549f82343721b" dependencies = [ - "crypto-bigint", - "ff", + "crypto-bigint 0.2.11", + "ff 0.10.1", "generic-array 0.14.4", - "group", - "pkcs8", + "group 0.10.0", + "pkcs8 0.7.6", "rand_core 0.6.3", "subtle 2.4.1", "zeroize", ] +[[package]] +name = "elliptic-curve" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decb3a27ea454a5f23f96eb182af0671c12694d64ecc33dada74edd1301f6cfc" +dependencies = [ + "crypto-bigint 0.3.2", + "der 0.5.1", + "ff 0.11.0", + "generic-array 0.14.4", + "group 0.11.0", + "rand_core 0.6.3", + "sec1", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "embed_plist" version = "1.2.0" @@ -1274,6 +1347,16 @@ dependencies = [ "subtle 2.4.1", ] +[[package]] +name = "ff" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2958d04124b9f27f175eaeb9a9f383d026098aa837eadd8ba22c11f13a05b9e" +dependencies = [ + "rand_core 0.6.3", + "subtle 2.4.1", +] + [[package]] name = "field-offset" version = "0.3.4" @@ -1775,7 +1858,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" dependencies = [ "byteorder", - "ff", + "ff 0.10.1", + "rand_core 0.6.3", + "subtle 2.4.1", +] + +[[package]] +name = "group" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" +dependencies = [ + "ff 0.11.0", "rand_core 0.6.3", "subtle 2.4.1", ] @@ -1936,20 +2030,6 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" -[[package]] -name = "hkd32" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f2a5541afe0725f0b95619d6af614f48c1b176385b8aa30918cfb8c4bfafc8" -dependencies = [ - "hmac", - "once_cell", - "pbkdf2", - "rand_core 0.6.3", - "sha2", - "zeroize", -] - [[package]] name = "hkdf" version = "0.11.0" @@ -2291,8 +2371,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea" dependencies = [ "cfg-if 1.0.0", - "ecdsa", - "elliptic-curve", + "ecdsa 0.12.4", + "elliptic-curve 0.10.6", + "sha2", +] + +[[package]] +name = "k256" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7511aa19fa182a8a4885760c4e5675b17173b02ae86ec5d376d34f5278c874b9" +dependencies = [ + "cfg-if 1.0.0", + "ecdsa 0.13.4", + "elliptic-curve 0.11.6", + "sec1", "sha2", "sha3", ] @@ -2503,10 +2596,11 @@ dependencies = [ ] [[package]] -name = "mixnet-contract" +name = "mixnet-contract-common" version = "0.1.0" dependencies = [ "az", + "contracts-common", "cosmwasm-std", "fixed", "log", @@ -2586,6 +2680,7 @@ checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" name = "network-defaults" version = "0.1.0" dependencies = [ + "cfg-if 1.0.0", "hex-literal", "serde", "time 0.3.5", @@ -2711,10 +2806,11 @@ dependencies = [ "credentials", "dirs", "eyre", - "mixnet-contract", + "mixnet-contract-common", "rand 0.6.5", "serde", "serde_json", + "strum 0.23.0", "tauri", "tauri-build", "tendermint-rpc", @@ -2732,9 +2828,9 @@ dependencies = [ "bls12_381", "bs58", "digest 0.9.0", - "ff", + "ff 0.10.1", "getrandom 0.2.3", - "group", + "group 0.10.0", "itertools", "rand 0.8.4", "serde", @@ -2855,7 +2951,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7de9d09263c9966e8196fe0380c9dbbc7ea114b5cf371ba29004bc1f9c6db7f3" dependencies = [ - "group", + "group 0.10.0", ] [[package]] @@ -2928,9 +3024,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pbkdf2" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739" dependencies = [ "crypto-mac 0.11.1", ] @@ -3165,8 +3261,19 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447" dependencies = [ - "der", - "spki", + "der 0.4.5", + "spki 0.4.1", +] + +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der 0.5.1", + "spki 0.5.4", + "zeroize", ] [[package]] @@ -3683,6 +3790,17 @@ dependencies = [ "winreg", ] +[[package]] +name = "rfc6979" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525" +dependencies = [ + "crypto-bigint 0.3.2", + "hmac", + "zeroize", +] + [[package]] name = "rfd" version = "0.4.4" @@ -3852,6 +3970,19 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sec1" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" +dependencies = [ + "der 0.5.1", + "generic-array 0.14.4", + "pkcs8 0.8.0", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "security-framework" version = "2.4.2" @@ -4180,7 +4311,17 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c01a0c15da1b0b0e1494112e7af814a678fec9bd157881b49beac661e9b6f32" dependencies = [ - "der", + "der 0.4.5", +] + +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der 0.5.1", ] [[package]] @@ -4261,6 +4402,15 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" +[[package]] +name = "strum" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" +dependencies = [ + "strum_macros 0.23.1", +] + [[package]] name = "strum_macros" version = "0.18.0" @@ -4285,6 +4435,19 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "1.0.0" @@ -4707,9 +4870,9 @@ dependencies = [ [[package]] name = "tendermint" -version = "0.23.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9015fdeab074f9b8f97dcb89c2bb2ec8537c89423e95551e8d7ecdfbab58a329" +checksum = "a9ef686b8ecd36550d0581f0989c9d8607090b23005df479d8e6ba348b5800b9" dependencies = [ "async-trait", "bytes", @@ -4717,7 +4880,7 @@ dependencies = [ "ed25519-dalek", "flex-error", "futures", - "k256", + "k256 0.10.1", "num-traits", "once_cell", "prost", @@ -4738,9 +4901,9 @@ dependencies = [ [[package]] name = "tendermint-config" -version = "0.23.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b2e6d4442bab49319dbacdfd79c5929bc6e0b35d1e0d959ff5b79fddf3f018" +checksum = "ecc127f82e7a8c7337c1f293d65a821380d3407c4c44bc979ef4da0ebc3b31ed" dependencies = [ "flex-error", "serde", @@ -4752,9 +4915,9 @@ dependencies = [ [[package]] name = "tendermint-proto" -version = "0.23.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da86f6e52ced9c2f24c4ae57662ce8a44dd90ee7bc47bae27a710b02d48e193c" +checksum = "a9e0a0251fd81bed7420bea0f4d91c2a58f6c9fa34d5b2f70bed0ba8890de5aa" dependencies = [ "bytes", "flex-error", @@ -4770,15 +4933,15 @@ dependencies = [ [[package]] name = "tendermint-rpc" -version = "0.23.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f5f4875c36798e5590894a5176cf8d75e4bc81ec34ced1b9a87609e7d56861" +checksum = "626c493e9ced3a9de37583bbd929f4b6dbd49aa513ab9b4776aa44a9332ce9b5" dependencies = [ "async-trait", "bytes", "flex-error", "futures", - "getrandom 0.1.16", + "getrandom 0.2.3", "http", "hyper", "hyper-proxy", @@ -5130,6 +5293,14 @@ dependencies = [ "getrandom 0.2.3", ] +[[package]] +name = "validator-api-requests" +version = "0.1.0" +dependencies = [ + "serde", + "ts-rs", +] + [[package]] name = "validator-client" version = "0.1.0" @@ -5144,7 +5315,7 @@ dependencies = [ "flate2", "itertools", "log", - "mixnet-contract", + "mixnet-contract-common", "network-defaults", "prost", "reqwest", @@ -5154,6 +5325,7 @@ dependencies = [ "thiserror", "ts-rs", "url", + "validator-api-requests", "vesting-contract", ] @@ -5188,12 +5360,18 @@ dependencies = [ "config", "cosmwasm-std", "cw-storage-plus", - "getrandom 0.2.3", - "mixnet-contract", - "rand 0.8.4", + "mixnet-contract-common", "schemars", "serde", "thiserror", + "vesting-contract-common", +] + +[[package]] +name = "vesting-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", ] [[package]] diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 8b2da3c711..7647fc3c2d 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -18,24 +18,25 @@ tauri-build = { version = "1.0.0-beta.4" } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } +strum = { version = "0.23", features = ["derive"] } tauri = { version = "1.0.0-beta.8", features = ["shell-open"] } tokio = { version = "1.10", features = ["sync"] } dirs = "4.0" bip39 = "1.0" thiserror = "1.0" tendermint-rpc = "0.23.0" -url = "2.0" +url = "2.2" rand = "0.6.5" eyre = "0.6.5" -cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev="e5a1872083abb3d88fa62dda966e7f5408deba58", features = ["rpc", "bip32", "cosmwasm"] } +cosmrs = { version = "0.4.1", features = ["rpc", "bip32", "cosmwasm"] } cosmwasm-std = "1.0.0-beta3" validator-client = { path = "../../common/client-libs/validator-client", features = [ "nymd-client", ] } -mixnet-contract = { path = "../../common/mixnet-contract" } +mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } config = { path = "../../common/config" } coconut-interface = { path = "../../common/coconut-interface" } credentials = { path = "../../common/credentials" } @@ -43,13 +44,13 @@ credentials = { path = "../../common/credentials" } [dev-dependencies] ts-rs = "5.1" -[dev-dependencies.mixnet-contract] -path = "../../common/mixnet-contract" +[dev-dependencies.mixnet-contract-common] +path = "../../common/cosmwasm-smart-contracts/mixnet-contract" features = ["ts-rs"] [dev-dependencies.validator-client] path = "../../common/client-libs/validator-client" -features = ["ts-rs"] +features = ["typescript-types"] [features] default = ["custom-protocol"] diff --git a/nym-wallet/src-tauri/src/coin.rs b/nym-wallet/src-tauri/src/coin.rs index ecfe2faa4f..5df1d9532b 100644 --- a/nym-wallet/src-tauri/src/coin.rs +++ b/nym-wallet/src-tauri/src/coin.rs @@ -46,6 +46,14 @@ impl FromStr for Denom { } } +impl TryFrom for Denom { + type Error = BackendError; + + fn try_from(value: CosmosDenom) -> Result { + Denom::from_str(&value.to_string()) + } +} + #[cfg_attr(test, derive(ts_rs::TS))] #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] pub struct Coin { diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index 90fbf670fa..95de5b345b 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -1,19 +1,18 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use config::defaults::{default_validators, ValidatorDetails, DEFAULT_MIXNET_CONTRACT_ADDRESS}; +use crate::network::Network; +use config::defaults::all::SupportedNetworks; use config::NymConfig; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -use std::str::FromStr; -use tendermint_rpc::Url; +use strum::IntoEnumIterator; +use url::Url; mod template; use template::config_template; -use crate::error::BackendError; - #[derive(Debug, Default, Deserialize, Serialize, Clone)] #[serde(deny_unknown_fields)] pub struct Config { @@ -23,21 +22,15 @@ pub struct Config { #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(deny_unknown_fields)] pub struct Base { - validators: Vec, - - /// Address of the validator contract managing the network - mixnet_contract_address: String, - - /// Mnemonic (currently of the network monitor) used for rewarding - mnemonic: String, + /// Information on all the networks that the wallet connects to. + networks: SupportedNetworks, } impl Default for Base { fn default() -> Self { + let networks = Network::iter().map(|network| network.into()).collect(); Base { - validators: default_validators(), - mixnet_contract_address: DEFAULT_MIXNET_CONTRACT_ADDRESS.to_string(), - mnemonic: String::default(), + networks: SupportedNetworks::new(networks), } } } @@ -68,23 +61,51 @@ impl NymConfig for Config { } impl Config { - pub fn get_nymd_validator_url(&self) -> Result { + pub fn get_nymd_validator_url(&self, network: Network) -> Url { // TODO make this a random choice - if let Some(validator_details) = self.base.validators.first() { - match tendermint_rpc::Url::from_str(&validator_details.nymd_url().to_string()) { - Ok(url) => Ok(url), - Err(e) => Err(e.into()), - } + if let Some(Some(validator_details)) = self + .base + .networks + .validators(network.into()) + .map(|validators| validators.first()) + { + validator_details.nymd_url() } else { panic!("No validators found in config") } } - pub fn get_mixnet_contract_address(&self) -> String { - self.base.mixnet_contract_address.clone() + pub fn get_validator_api_url(&self, network: Network) -> Url { + // TODO make this a random choice + if let Some(Some(validator_details)) = self + .base + .networks + .validators(network.into()) + .map(|validators| validators.first()) + { + validator_details.api_url().expect("no api url provided") + } else { + panic!("No validators found in config") + } } - // pub fn get_mnemonic(&self) -> String { - // self.base.mnemonic.clone() - // } + pub fn get_mixnet_contract_address(&self, network: Network) -> cosmrs::AccountId { + self + .base + .networks + .mixnet_contract_address(network.into()) + .expect("No mixnet contract address found in config") + .parse() + .expect("stored mixnet contract address is not a valid account address") + } + + pub fn get_vesting_contract_address(&self, network: Network) -> cosmrs::AccountId { + self + .base + .networks + .vesting_contract_address(network.into()) + .expect("No vesting contract address found in config") + .parse() + .expect("stored vesting contract address is not a valid account address") + } } diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 605cc78047..9101549fb5 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -1,6 +1,7 @@ use serde::{Serialize, Serializer}; use thiserror::Error; use validator_client::nymd::error::NymdError; +use validator_client::validator_api::error::ValidatorAPIError; #[derive(Error, Debug)] pub enum BackendError { @@ -29,12 +30,19 @@ pub enum BackendError { #[from] source: eyre::Report, }, + #[error("{source}")] + ValidatorApiError { + #[from] + source: ValidatorAPIError, + }, #[error("Client has not been initialized yet, connect with mnemonic to initialize")] ClientNotInitialized, #[error("No balance available for address {0}")] NoBalance(String), #[error("{0} is not a valid denomination string")] InvalidDenom(String), + #[error("The provided network is not supported (yet)")] + NetworkNotSupported(config::defaults::all::Network), } impl Serialize for BackendError { diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index e402137e39..da5619524d 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -3,7 +3,7 @@ windows_subsystem = "windows" )] -use mixnet_contract::{Gateway, MixNode}; +use mixnet_contract_common::{Gateway, MixNode}; use std::sync::Arc; use tauri::Menu; use tokio::sync::RwLock; @@ -13,12 +13,14 @@ mod coin; mod config; mod error; mod menu; +mod network; mod operations; mod state; mod utils; use crate::menu::AddDefaultSubmenus; use crate::operations::mixnet; +use crate::operations::validator_api; use crate::operations::vesting; use crate::state::State; @@ -29,6 +31,7 @@ fn main() { .invoke_handler(tauri::generate_handler![ mixnet::account::connect_with_mnemonic, mixnet::account::create_new_account, + mixnet::account::switch_network, mixnet::account::get_balance, mixnet::admin::get_contract_settings, mixnet::admin::update_contract_settings, @@ -36,13 +39,14 @@ fn main() { mixnet::bond::bond_mixnode, mixnet::bond::unbond_gateway, mixnet::bond::unbond_mixnode, + mixnet::bond::update_mixnode, mixnet::bond::mixnode_bond_details, mixnet::bond::gateway_bond_details, mixnet::delegate::delegate_to_mixnode, mixnet::delegate::get_reverse_mix_delegations_paged, mixnet::delegate::undelegate_from_mixnode, mixnet::send::send, - utils::get_approximate_fee, + utils::outdated_get_approximate_fee, utils::major_to_minor, utils::minor_to_major, utils::owns_gateway, @@ -62,6 +66,11 @@ fn main() { vesting::queries::original_vesting, vesting::queries::delegated_free, vesting::queries::delegated_vesting, + validator_api::status::mixnode_core_node_status, + validator_api::status::gateway_core_node_status, + validator_api::status::mixnode_status, + validator_api::status::mixnode_reward_estimation, + validator_api::status::mixnode_stake_saturation, ]) .menu(Menu::new().add_default_app_submenu_if_macos()) .run(tauri::generate_context!()) @@ -71,16 +80,22 @@ fn main() { #[cfg(test)] mod test { ts_rs::export! { - mixnet_contract::MixNode => "../src/types/rust/mixnode.ts", + mixnet_contract_common::MixNode => "../src/types/rust/mixnode.ts", crate::coin::Coin => "../src/types/rust/coin.ts", + crate::network::Network => "../src/types/rust/network.ts", crate::mixnet::account::Balance => "../src/types/rust/balance.ts", - mixnet_contract::Gateway => "../src/types/rust/gateway.ts", + mixnet_contract_common::Gateway => "../src/types/rust/gateway.ts", crate::mixnet::send::TauriTxResult => "../src/types/rust/tauritxresult.ts", crate::mixnet::send::TransactionDetails => "../src/types/rust/transactiondetails.ts", validator_client::nymd::fee::helpers::Operation => "../src/types/rust/operation.ts", crate::coin::Denom => "../src/types/rust/denom.ts", crate::utils::DelegationResult => "../src/types/rust/delegationresult.ts", crate::mixnet::account::Account => "../src/types/rust/account.ts", - crate::mixnet::admin::TauriContractStateParams => "../src/types/rust/stateparams.ts" + crate::mixnet::admin::TauriContractStateParams => "../src/types/rust/stateparams.ts", + validator_client::models::CoreNodeStatusResponse => "../src/types/corenodestatusresponse.ts", + validator_client::models::MixnodeStatus => "../src/types/rust/mixnodestatus.ts", + validator_client::models::MixnodeStatusResponse => "../src/types/rust/mixnodestatusresponse.ts", + validator_client::models::RewardEstimationResponse => "../src/types/rust/rewardestimationresponse.ts", + validator_client::models::StakeSaturationResponse => "../src/types/rust/stakesaturaionresponse.ts", } } diff --git a/nym-wallet/src-tauri/src/network.rs b/nym-wallet/src-tauri/src/network.rs new file mode 100644 index 0000000000..3c04f70253 --- /dev/null +++ b/nym-wallet/src-tauri/src/network.rs @@ -0,0 +1,45 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use strum::EnumIter; + +use crate::error::BackendError; +use config::defaults::all::Network as ConfigNetwork; + +#[allow(clippy::upper_case_acronyms)] +#[cfg_attr(test, derive(ts_rs::TS))] +#[derive(Copy, Clone, Debug, Deserialize, EnumIter, Eq, Hash, PartialEq, Serialize)] +pub enum Network { + QA, + SANDBOX, +} + +impl Default for Network { + fn default() -> Self { + Network::SANDBOX + } +} + +#[allow(clippy::from_over_into)] +impl Into for Network { + fn into(self) -> ConfigNetwork { + match self { + Network::QA => ConfigNetwork::QA, + Network::SANDBOX => ConfigNetwork::SANDBOX, + } + } +} + +impl TryFrom for Network { + type Error = BackendError; + + fn try_from(value: ConfigNetwork) -> Result { + match value { + ConfigNetwork::QA => Ok(Network::QA), + ConfigNetwork::SANDBOX => Ok(Network::SANDBOX), + _ => Err(BackendError::NetworkNotSupported(value)), + } + } +} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 5824b355a7..eac5a21677 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -1,14 +1,16 @@ -use crate::client; use crate::coin::{Coin, Denom}; -use crate::config::Config; use crate::error::BackendError; +use crate::network::Network; +use crate::nymd_client; use crate::state::State; + use bip39::{Language, Mnemonic}; use serde::{Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; use std::str::FromStr; use std::sync::Arc; +use strum::IntoEnumIterator; use tokio::sync::RwLock; -use validator_client::nymd::{AccountId, NymdClient, SigningNymdClient}; #[cfg_attr(test, derive(ts_rs::TS))] #[derive(Serialize, Deserialize)] @@ -16,7 +18,16 @@ pub struct Account { contract_address: String, client_address: String, denom: Denom, - mnemonic: Option, +} + +impl Account { + pub fn new(contract_address: String, client_address: String, denom: Denom) -> Self { + Account { + contract_address, + client_address, + denom, + } + } } #[cfg_attr(test, derive(ts_rs::TS))] @@ -32,34 +43,15 @@ pub async fn connect_with_mnemonic( state: tauri::State<'_, Arc>>, ) -> Result { let mnemonic = Mnemonic::from_str(&mnemonic)?; - let client = { - let r_state = state.read().await; - _connect_with_mnemonic(mnemonic, &r_state.config()) - }; - - let contract_address = client.mixnet_contract_address()?.to_string(); - let client_address = client.address().to_string(); - let denom = client.denom()?; - - let account = Account { - contract_address, - client_address, - denom: Denom::from_str(&denom.to_string())?, - mnemonic: None, - }; - - let mut w_state = state.write().await; - w_state.set_client(client); - - Ok(account) + _connect_with_mnemonic(mnemonic, state).await } #[tauri::command] pub async fn get_balance( state: tauri::State<'_, Arc>>, ) -> Result { - match client!(state) - .get_mixnet_balance(client!(state).address()) + match nymd_client!(state) + .get_mixnet_balance(nymd_client!(state).address()) .await { Ok(Some(coin)) => { @@ -73,7 +65,7 @@ pub async fn get_balance( }) } Ok(None) => Err(BackendError::NoBalance( - client!(state).address().to_string(), + nymd_client!(state).address().to_string(), )), Err(e) => Err(BackendError::from(e)), } @@ -84,9 +76,30 @@ pub async fn create_new_account( state: tauri::State<'_, Arc>>, ) -> Result { let rand_mnemonic = random_mnemonic(); - let mut client = connect_with_mnemonic(rand_mnemonic.to_string(), state).await?; - client.mnemonic = Some(rand_mnemonic.to_string()); - Ok(client) + let account = connect_with_mnemonic(rand_mnemonic.to_string(), state).await?; + Ok(account) +} + +#[tauri::command] +pub async fn switch_network( + state: tauri::State<'_, Arc>>, + network: Network, +) -> Result { + let account = { + let r_state = state.read().await; + let client = r_state.client(network)?; + + Account::new( + client.nymd.mixnet_contract_address()?.to_string(), + client.nymd.address().to_string(), + client.nymd.denom()?.try_into()?, + ) + }; + + let mut w_state = state.write().await; + w_state.set_network(network); + + Ok(account) } fn random_mnemonic() -> Mnemonic { @@ -94,15 +107,41 @@ fn random_mnemonic() -> Mnemonic { Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap() } -fn _connect_with_mnemonic(mnemonic: Mnemonic, config: &Config) -> NymdClient { - match NymdClient::connect_with_mnemonic( - config.get_nymd_validator_url().unwrap(), - Some(AccountId::from_str(&config.get_mixnet_contract_address()).unwrap()), - None, - mnemonic, - None, - ) { - Ok(client) => client, - Err(e) => panic!("{}", e), +async fn _connect_with_mnemonic( + mnemonic: Mnemonic, + state: tauri::State<'_, Arc>>, +) -> Result { + let default_network = Network::try_from(config::defaults::default_network())?; + let mut default_account = None; + for network in Network::iter() { + let client = { + let config = state.read().await.config(); + match validator_client::Client::new_signing( + validator_client::Config::new( + config.get_nymd_validator_url(network), + config.get_validator_api_url(network), + Some(config.get_mixnet_contract_address(network)), + Some(config.get_vesting_contract_address(network)), + ), + mnemonic.clone(), + ) { + Ok(client) => client, + Err(e) => panic!("{}", e), + } + }; + + if network == default_network { + default_account = Some(Account::new( + client.nymd.mixnet_contract_address()?.to_string(), + client.nymd.address().to_string(), + client.nymd.denom()?.try_into()?, + )); + } + + let mut w_state = state.write().await; + w_state.add_client(network, client); } + + default_account + .ok_or_else(|| BackendError::NetworkNotSupported(config::defaults::default_network())) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs index 961a57b4e7..14bf378378 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs @@ -1,8 +1,8 @@ -use crate::client; use crate::error::BackendError; +use crate::nymd_client; use crate::state::State; use cosmwasm_std::Uint128; -use mixnet_contract::ContractStateParams; +use mixnet_contract_common::ContractStateParams; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use std::sync::Arc; @@ -48,7 +48,7 @@ impl TryFrom for ContractStateParams { pub async fn get_contract_settings( state: tauri::State<'_, Arc>>, ) -> Result { - Ok(client!(state).get_contract_settings().await?.into()) + Ok(nymd_client!(state).get_contract_settings().await?.into()) } #[tauri::command] @@ -57,7 +57,7 @@ pub async fn update_contract_settings( state: tauri::State<'_, Arc>>, ) -> Result { let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; - client!(state) + nymd_client!(state) .update_contract_settings(mixnet_contract_settings_params.clone()) .await?; Ok(mixnet_contract_settings_params.into()) diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index b1801b576f..3180bbf991 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -1,9 +1,9 @@ -use crate::client; use crate::coin::Coin; use crate::error::BackendError; +use crate::nymd_client; use crate::state::State; use crate::{Gateway, MixNode}; -use mixnet_contract::{GatewayBond, MixNodeBond}; +use mixnet_contract_common::{GatewayBond, MixNodeBond}; use std::convert::TryInto; use std::sync::Arc; use tokio::sync::RwLock; @@ -15,7 +15,7 @@ pub async fn bond_gateway( owner_signature: String, state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - client!(state) + nymd_client!(state) .bond_gateway(gateway, owner_signature, pledge.try_into()?) .await?; Ok(()) @@ -25,7 +25,7 @@ pub async fn bond_gateway( pub async fn unbond_gateway( state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - client!(state).unbond_gateway().await?; + nymd_client!(state).unbond_gateway().await?; Ok(()) } @@ -33,7 +33,7 @@ pub async fn unbond_gateway( pub async fn unbond_mixnode( state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - client!(state).unbond_mixnode().await?; + nymd_client!(state).unbond_mixnode().await?; Ok(()) } @@ -44,19 +44,30 @@ pub async fn bond_mixnode( pledge: Coin, state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - client!(state) + nymd_client!(state) .bond_mixnode(mixnode, owner_signature, pledge.try_into()?) .await?; Ok(()) } +#[tauri::command] +pub async fn update_mixnode( + profit_margin_percent: u8, + state: tauri::State<'_, Arc>>, +) -> Result<(), BackendError> { + nymd_client!(state) + .update_mixnode_config(profit_margin_percent) + .await?; + Ok(()) +} + #[tauri::command] pub async fn mixnode_bond_details( state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { let guard = state.read().await; - let client = guard.client()?; - let bond = client.owns_mixnode(client.address()).await?; + let client = guard.current_client()?; + let bond = client.nymd.owns_mixnode(client.nymd.address()).await?; Ok(bond) } @@ -65,7 +76,7 @@ pub async fn gateway_bond_details( state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { let guard = state.read().await; - let client = guard.client()?; - let bond = client.owns_gateway(client.address()).await?; + let client = guard.current_client()?; + let bond = client.nymd.owns_gateway(client.nymd.address()).await?; Ok(bond) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 1fb96308e4..dbf56cc0ad 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -1,10 +1,10 @@ -use crate::client; use crate::coin::Coin; use crate::error::BackendError; +use crate::nymd_client; use crate::state::State; use crate::utils::DelegationResult; use cosmwasm_std::Coin as CosmWasmCoin; -use mixnet_contract::PagedDelegatorDelegationsResponse; +use mixnet_contract_common::PagedDelegatorDelegationsResponse; use std::convert::TryInto; use std::sync::Arc; use tokio::sync::RwLock; @@ -16,11 +16,11 @@ pub async fn delegate_to_mixnode( state: tauri::State<'_, Arc>>, ) -> Result { let delegation: CosmWasmCoin = amount.try_into()?; - client!(state) + nymd_client!(state) .delegate_to_mixnode(identity, &delegation) .await?; Ok(DelegationResult::new( - &client!(state).address().to_string(), + &nymd_client!(state).address().to_string(), identity, Some(delegation.into()), )) @@ -31,9 +31,11 @@ pub async fn undelegate_from_mixnode( identity: &str, state: tauri::State<'_, Arc>>, ) -> Result { - client!(state).remove_mixnode_delegation(identity).await?; + nymd_client!(state) + .remove_mixnode_delegation(identity) + .await?; Ok(DelegationResult::new( - &client!(state).address().to_string(), + &nymd_client!(state).address().to_string(), identity, None, )) @@ -44,8 +46,8 @@ pub async fn get_reverse_mix_delegations_paged( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) - .get_delegator_delegations_paged(client!(state).address().to_string(), None, None) + nymd_client!(state) + .get_delegator_delegations_paged(nymd_client!(state).address().to_string(), None, None) .await?, ) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/send.rs b/nym-wallet/src-tauri/src/operations/mixnet/send.rs index 417aa42177..1176d86227 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/send.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/send.rs @@ -1,6 +1,6 @@ -use crate::client; use crate::coin::Coin; use crate::error::BackendError; +use crate::nymd_client; use crate::state::State; use serde::{Deserialize, Serialize}; use std::convert::TryInto; @@ -51,13 +51,13 @@ pub async fn send( ) -> Result { let address = AccountId::from_str(address)?; let cosmos_amount: CosmosCoin = amount.clone().try_into()?; - let result = client!(state) + let result = nymd_client!(state) .send(&address, vec![cosmos_amount], memo) .await?; Ok(TauriTxResult::new( result, TransactionDetails { - from_address: client!(state).address().to_string(), + from_address: nymd_client!(state).address().to_string(), to_address: address.to_string(), amount, }, diff --git a/nym-wallet/src-tauri/src/operations/mod.rs b/nym-wallet/src-tauri/src/operations/mod.rs index 9bfd654e73..c49e27d4d2 100644 --- a/nym-wallet/src-tauri/src/operations/mod.rs +++ b/nym-wallet/src-tauri/src/operations/mod.rs @@ -1,2 +1,3 @@ pub mod mixnet; +pub mod validator_api; pub mod vesting; diff --git a/nym-wallet/src-tauri/src/operations/validator_api/mod.rs b/nym-wallet/src-tauri/src/operations/validator_api/mod.rs new file mode 100644 index 0000000000..3529bb0b4a --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/validator_api/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod status; diff --git a/nym-wallet/src-tauri/src/operations/validator_api/status.rs b/nym-wallet/src-tauri/src/operations/validator_api/status.rs new file mode 100644 index 0000000000..cb2fd9d17a --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/validator_api/status.rs @@ -0,0 +1,69 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::api_client; +use crate::error::BackendError; +use crate::state::State; +use std::sync::Arc; +use tokio::sync::RwLock; +use validator_client::models::{ + CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, +}; + +#[tauri::command] +pub async fn mixnode_core_node_status( + identity: &str, + since: Option, + state: tauri::State<'_, Arc>>, +) -> Result { + Ok( + api_client!(state) + .get_mixnode_core_status_count(identity, since) + .await?, + ) +} + +#[tauri::command] +pub async fn gateway_core_node_status( + identity: &str, + since: Option, + state: tauri::State<'_, Arc>>, +) -> Result { + Ok( + api_client!(state) + .get_gateway_core_status_count(identity, since) + .await?, + ) +} + +#[tauri::command] +pub async fn mixnode_status( + identity: &str, + state: tauri::State<'_, Arc>>, +) -> Result { + Ok(api_client!(state).get_mixnode_status(identity).await?) +} + +#[tauri::command] +pub async fn mixnode_reward_estimation( + identity: &str, + state: tauri::State<'_, Arc>>, +) -> Result { + Ok( + api_client!(state) + .get_mixnode_reward_estimation(identity) + .await?, + ) +} + +#[tauri::command] +pub async fn mixnode_stake_saturation( + identity: &str, + state: tauri::State<'_, Arc>>, +) -> Result { + Ok( + api_client!(state) + .get_mixnode_stake_saturation(identity) + .await?, + ) +} diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index 58a559b414..8962d9c58c 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -1,6 +1,6 @@ -use crate::client; use crate::coin::Coin; use crate::error::BackendError; +use crate::nymd_client; use crate::state::State; use crate::{Gateway, MixNode}; use std::convert::TryInto; @@ -15,7 +15,7 @@ pub async fn vesting_bond_gateway( owner_signature: String, state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - client!(state) + nymd_client!(state) .vesting_bond_gateway(gateway, &owner_signature, pledge.try_into()?) .await?; Ok(()) @@ -25,7 +25,7 @@ pub async fn vesting_bond_gateway( pub async fn vesting_unbond_gateway( state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - client!(state).vesting_unbond_gateway().await?; + nymd_client!(state).vesting_unbond_gateway().await?; Ok(()) } @@ -33,7 +33,7 @@ pub async fn vesting_unbond_gateway( pub async fn vesting_unbond_mixnode( state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - client!(state).vesting_unbond_mixnode().await?; + nymd_client!(state).vesting_unbond_mixnode().await?; Ok(()) } @@ -44,7 +44,7 @@ pub async fn vesting_bond_mixnode( pledge: Coin, state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - client!(state) + nymd_client!(state) .vesting_bond_mixnode(mixnode, &owner_signature, pledge.try_into()?) .await?; Ok(()) diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index ed7bb14135..c807ce2297 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -1,6 +1,6 @@ -use crate::client; use crate::coin::Coin; use crate::error::BackendError; +use crate::nymd_client; use crate::state::State; use crate::utils::DelegationResult; use cosmwasm_std::Coin as CosmWasmCoin; @@ -16,11 +16,11 @@ pub async fn vesting_delegate_to_mixnode( state: tauri::State<'_, Arc>>, ) -> Result { let delegation: CosmWasmCoin = amount.try_into()?; - client!(state) + nymd_client!(state) .vesting_delegate_to_mixnode(identity, &delegation) .await?; Ok(DelegationResult::new( - &client!(state).address().to_string(), + &nymd_client!(state).address().to_string(), identity, Some(delegation.into()), )) @@ -31,11 +31,11 @@ pub async fn vesting_undelegate_from_mixnode( identity: &str, state: tauri::State<'_, Arc>>, ) -> Result { - client!(state) + nymd_client!(state) .vesting_undelegate_from_mixnode(identity) .await?; Ok(DelegationResult::new( - &client!(state).address().to_string(), + &nymd_client!(state).address().to_string(), identity, None, )) diff --git a/nym-wallet/src-tauri/src/operations/vesting/queries.rs b/nym-wallet/src-tauri/src/operations/vesting/queries.rs index 3512ce8a9d..be6e68d663 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/queries.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/queries.rs @@ -1,6 +1,6 @@ -use crate::client; use crate::coin::Coin; use crate::error::BackendError; +use crate::nymd_client; use crate::state::State; use cosmwasm_std::Timestamp; use std::sync::Arc; @@ -14,7 +14,7 @@ pub async fn locked_coins( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) + nymd_client!(state) .locked_coins(address, block_time.map(Timestamp::from_seconds)) .await? .into(), @@ -28,7 +28,7 @@ pub async fn spendable_coins( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) + nymd_client!(state) .spendable_coins( vesting_account_address, block_time.map(Timestamp::from_seconds), @@ -45,7 +45,7 @@ pub async fn vested_coins( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) + nymd_client!(state) .vested_coins( vesting_account_address, block_time.map(Timestamp::from_seconds), @@ -62,7 +62,7 @@ pub async fn vesting_coins( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) + nymd_client!(state) .vesting_coins( vesting_account_address, block_time.map(Timestamp::from_seconds), @@ -78,7 +78,7 @@ pub async fn vesting_start_time( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) + nymd_client!(state) .vesting_start_time(vesting_account_address) .await? .seconds(), @@ -91,7 +91,7 @@ pub async fn vesting_end_time( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) + nymd_client!(state) .vesting_end_time(vesting_account_address) .await? .seconds(), @@ -104,7 +104,7 @@ pub async fn original_vesting( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) + nymd_client!(state) .original_vesting(vesting_account_address) .await? .into(), @@ -118,7 +118,7 @@ pub async fn delegated_free( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) + nymd_client!(state) .delegated_free( vesting_account_address, block_time.map(Timestamp::from_seconds), @@ -135,7 +135,7 @@ pub async fn delegated_vesting( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) + nymd_client!(state) .delegated_vesting( vesting_account_address, block_time.map(Timestamp::from_seconds), diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index 8603d0c592..b54355d8b4 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -1,18 +1,30 @@ use crate::config::Config; use crate::error::BackendError; -use validator_client::nymd::{NymdClient, SigningNymdClient}; +use crate::network::Network; +use validator_client::nymd::SigningNymdClient; +use validator_client::Client; + +use std::collections::HashMap; #[derive(Default)] pub struct State { config: Config, - signing_client: Option>, + signing_clients: HashMap>, + current_network: Network, } impl State { - pub fn client(&self) -> Result<&NymdClient, BackendError> { + pub fn client(&self, network: Network) -> Result<&Client, BackendError> { self - .signing_client - .as_ref() + .signing_clients + .get(&network) + .ok_or(BackendError::ClientNotInitialized) + } + + pub fn current_client(&self) -> Result<&Client, BackendError> { + self + .signing_clients + .get(&self.current_network) .ok_or(BackendError::ClientNotInitialized) } @@ -20,14 +32,32 @@ impl State { self.config.clone() } - pub fn set_client(&mut self, signing_client: NymdClient) { - self.signing_client = Some(signing_client) + pub fn add_client(&mut self, network: Network, client: Client) { + self.signing_clients.insert(network, client); + } + + pub fn set_network(&mut self, network: Network) { + self.current_network = network; } } #[macro_export] macro_rules! client { ($state:ident) => { - $state.read().await.client()? + $state.read().await.current_client()? + }; +} + +#[macro_export] +macro_rules! nymd_client { + ($state:ident) => { + $state.read().await.current_client()?.nymd + }; +} + +#[macro_export] +macro_rules! api_client { + ($state:ident) => { + $state.read().await.current_client()?.validator_api }; } diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index 83b35ae9be..7cb64f91f0 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -1,6 +1,6 @@ -use crate::client; use crate::coin::{Coin, Denom}; use crate::error::BackendError; +use crate::nymd_client; use crate::state::State; use crate::Operation; use serde::{Deserialize, Serialize}; @@ -24,8 +24,8 @@ pub async fn owns_mixnode( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) - .owns_mixnode(client!(state).address()) + nymd_client!(state) + .owns_mixnode(nymd_client!(state).address()) .await? .is_some(), ) @@ -36,8 +36,8 @@ pub async fn owns_gateway( state: tauri::State<'_, Arc>>, ) -> Result { Ok( - client!(state) - .owns_gateway(client!(state).address()) + nymd_client!(state) + .owns_gateway(nymd_client!(state).address()) .await? .is_some(), ) @@ -46,11 +46,11 @@ pub async fn owns_gateway( // NOTE: this uses OUTDATED defaults that might have no resemblance with the reality // as for the actual transaction, the gas cost is being simulated beforehand #[tauri::command] -pub async fn get_approximate_fee( +pub async fn outdated_get_approximate_fee( operation: Operation, state: tauri::State<'_, Arc>>, ) -> Result { - let approximate_fee = operation.default_fee(client!(state).gas_price()); + let approximate_fee = operation.default_fee(nymd_client!(state).gas_price()); let mut coin = Coin::new("0", &Denom::Major); for f in approximate_fee.amount { coin = coin + f.into(); diff --git a/nym-wallet/src/components/AppBar.tsx b/nym-wallet/src/components/AppBar.tsx index f2437d2c56..c30773a1fd 100644 --- a/nym-wallet/src/components/AppBar.tsx +++ b/nym-wallet/src/components/AppBar.tsx @@ -1,9 +1,10 @@ -import React, { useContext, useEffect } from 'react' +import React, { useContext } from 'react' import { AppBar as MuiAppBar, Divider, Grid, IconButton, Toolbar, Typography, useMediaQuery } from '@mui/material' import { Box } from '@mui/system' -import { Logout, SettingsOutlined } from '@mui/icons-material' +import { Logout } from '@mui/icons-material' import { ClientContext } from '../context/main' import { CopyToClipboard } from '.' +import { Node as NodeIcon } from '../svg-icons/node' export const AppBar = () => { const { userBalance, clientDetails, showSettings, logOut, handleShowSettings } = useContext(ClientContext) @@ -20,7 +21,6 @@ export const AppBar = () => { {matches && ( <> - { sx={{ color: showSettings ? 'primary.main' : 'nym.background.dark' }} size="small" > - + diff --git a/nym-wallet/src/components/Fee.tsx b/nym-wallet/src/components/Fee.tsx new file mode 100644 index 0000000000..c6b4995566 --- /dev/null +++ b/nym-wallet/src/components/Fee.tsx @@ -0,0 +1,28 @@ +import React, { useState, useEffect } from 'react' +import { Typography } from '@mui/material' +import { MAJOR_CURRENCY } from '../context/main' +import { Operation } from '../types' +import { getGasFee } from '../requests' + +export const Fee = ({ feeType }: { feeType: Operation }) => { + const [fee, setFee] = useState() + + const getFee = async () => { + const fee = await getGasFee(feeType) + setFee(fee.amount) + } + + useEffect(() => { + getFee() + }, []) + + if (fee) { + return ( + + Fee for this transaction: {`${fee} ${MAJOR_CURRENCY}`}{' '} + + ) + } + + return null +} diff --git a/nym-wallet/src/components/NodeStatus.tsx b/nym-wallet/src/components/NodeStatus.tsx new file mode 100644 index 0000000000..c91eddd09b --- /dev/null +++ b/nym-wallet/src/components/NodeStatus.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { Typography } from '@mui/material' +import { CircleOutlined, PauseCircleOutlined, CheckCircleOutline } from '@mui/icons-material' +import { MixnodeStatus } from '../types' + +export const NodeStatus = ({ status }: { status: MixnodeStatus }) => { + switch (status) { + case 'active': + return + case 'inactive': + return + case 'standby': + return + default: + null + } + return null +} + +const Active = () => ( + + Active + +) + +const Inactive = () => ( + + Inactive + +) + +const Standby = () => ( + + Standby + +) diff --git a/nym-wallet/src/components/NymCard.tsx b/nym-wallet/src/components/NymCard.tsx index bfaf7fa18a..2511b45658 100644 --- a/nym-wallet/src/components/NymCard.tsx +++ b/nym-wallet/src/components/NymCard.tsx @@ -1,28 +1,28 @@ import React from 'react' -import { Card, CardContent, CardHeader } from '@mui/material' +import { Box, Card, CardContent, CardHeader } from '@mui/material' import { styled } from '@mui/material/styles' export const NymCard: React.FC<{ title: string | React.ReactElement subheader?: string Action?: React.ReactNode + Icon?: any noPadding?: boolean -}> = ({ title, subheader, Action, noPadding, children }) => { +}> = ({ title, subheader, Action, Icon, noPadding, children }) => { return ( + {Icon && } {title} + + } subheader={subheader} data-testid={title} titleTypographyProps={{ variant: 'h5' }} subheaderTypographyProps={{ variant: 'subtitle1' }} - action={Action} - sx={{ - color: 'nym.background.dark', - py: 2.5, - px: 4, - borderBottom: (theme) => `1px solid ${theme.palette.grey[200]}`, - }} + action={{Action}} /> {noPadding ? {children} : {children}} @@ -30,7 +30,6 @@ export const NymCard: React.FC<{ } const CardContentNoPadding = styled(CardContent)(({ theme }) => ({ - background: theme.palette.grey[50], padding: 0, '&:last-child': { paddingBottom: 0, diff --git a/nym-wallet/src/components/index.ts b/nym-wallet/src/components/index.ts index 893c9dbafb..d8f23958b1 100644 --- a/nym-wallet/src/components/index.ts +++ b/nym-wallet/src/components/index.ts @@ -7,3 +7,4 @@ export * from './RequestStatus' export * from './NoClientError' export * from './SuccessResponse' export * from './TransactionDetails' +export * from './Fee' diff --git a/nym-wallet/src/context/main.tsx b/nym-wallet/src/context/main.tsx index c917d97600..14a45af145 100644 --- a/nym-wallet/src/context/main.tsx +++ b/nym-wallet/src/context/main.tsx @@ -1,8 +1,9 @@ import React, { createContext, useEffect, useState } from 'react' import { useHistory } from 'react-router-dom' -import { TClientDetails, TSignInWithMnemonic } from '../types' +import { TClientDetails, TMixnodeBondDetails, TSignInWithMnemonic } from '../types' import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance' import { config } from '../../config' +import { getMixnodeBondDetails } from '../requests' export const { MAJOR_CURRENCY, MINOR_CURRENCY, ADMIN_ADDRESS, NETWORK_NAME } = config @@ -14,9 +15,11 @@ export const urls = { type TClientContext = { mode: 'light' | 'dark' clientDetails?: TClientDetails + mixnodeDetails?: TMixnodeBondDetails | null userBalance: TUseuserBalance showAdmin: boolean showSettings: boolean + getBondDetails: () => Promise handleShowSettings: () => void handleShowAdmin: () => void logIn: (clientDetails: TSignInWithMnemonic) => void @@ -27,6 +30,7 @@ export const ClientContext = createContext({} as TClientContext) export const ClientContextProvider = ({ children }: { children: React.ReactNode }) => { const [clientDetails, setClientDetails] = useState() + const [mixnodeDetails, setMixnodeDetails] = useState() const [showAdmin, setShowAdmin] = useState(false) const [showSettings, setShowSettings] = useState(false) const [mode, setMode] = useState<'light' | 'dark'>('light') @@ -43,7 +47,10 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode } }, [clientDetails, userBalance.fetchBalance]) - const logIn = async (clientDetails: TSignInWithMnemonic) => setClientDetails(clientDetails) + const logIn = async (clientDetails: TSignInWithMnemonic) => { + await getBondDetails() + setClientDetails(clientDetails) + } const logOut = () => { setClientDetails(undefined) @@ -53,14 +60,21 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode const handleShowAdmin = () => setShowAdmin((show) => !show) const handleShowSettings = () => setShowSettings((show) => !show) + const getBondDetails = async () => { + const mixnodeDetails = await getMixnodeBondDetails() + setMixnodeDetails(mixnodeDetails) + } + return ( { return ( - + {userBalance.error && ( diff --git a/nym-wallet/src/pages/bond/BondForm.tsx b/nym-wallet/src/pages/bond/BondForm.tsx index 55609d15fc..e1e433568d 100644 --- a/nym-wallet/src/pages/bond/BondForm.tsx +++ b/nym-wallet/src/pages/bond/BondForm.tsx @@ -17,9 +17,9 @@ import { EnumNodeType } from '../../types/global' import { NodeTypeSelector } from '../../components/NodeTypeSelector' import { bond, majorToMinor } from '../../requests' import { validationSchema } from './validationSchema' -import { Coin, Gateway, MixNode } from '../../types' +import { Gateway, MixNode } from '../../types' import { ClientContext, MAJOR_CURRENCY } from '../../context/main' -import { checkHasEnoughFunds } from '../../utils' +import { Fee } from '../../components' type TBondFormFields = { withAdvancedOptions: boolean @@ -78,12 +78,10 @@ const formatData = (data: TBondFormFields) => { export const BondForm = ({ disabled, - fees, onError, onSuccess, }: { disabled: boolean - fees?: { [EnumNodeType.mixnode]: Coin; [EnumNodeType.gateway]?: Coin } onError: (message?: string) => void onSuccess: (details: { address: string; amount: string }) => void }) => { @@ -91,7 +89,6 @@ export const BondForm = ({ register, handleSubmit, setValue, - setError, watch, formState: { errors, isSubmitting }, } = useForm({ @@ -99,22 +96,18 @@ export const BondForm = ({ defaultValues, }) - const { userBalance } = useContext(ClientContext) + const { userBalance, getBondDetails } = useContext(ClientContext) const watchNodeType = watch('nodeType', defaultValues.nodeType) const watchAdvancedOptions = watch('withAdvancedOptions', defaultValues.withAdvancedOptions) const onSubmit = async (data: TBondFormFields) => { - const hasEnoughFunds = await checkHasEnoughFunds(data.amount) - if (!hasEnoughFunds) { - return setError('amount', { message: 'Not enough funds in wallet' }) - } - const formattedData = formatData(data) const pledge = await majorToMinor(data.amount) await bond({ type: data.nodeType, ownerSignature: data.ownerSignature, data: formattedData, pledge }) - .then(() => { + .then(async () => { + await getBondDetails() userBalance.fetchBalance() onSuccess({ address: data.identityKey, amount: data.amount }) }) @@ -125,7 +118,7 @@ export const BondForm = ({ return ( - + @@ -358,16 +351,9 @@ export const BondForm = ({ )} )} - {fees && ( - - - {' '} - {`Bonding fee: ${ - watchNodeType === EnumNodeType.mixnode ? fees.mixnode.amount : fees?.gateway?.amount - } ${MAJOR_CURRENCY}`} - - - )} + + {!disabled ? :
} + `1px solid ${theme.palette.grey[200]}`, - bgcolor: 'grey.100', - padding: 2, + padding: 3, + pt: 0, }} > diff --git a/nym-wallet/src/pages/bond/index.tsx b/nym-wallet/src/pages/bond/index.tsx index 77a84a0036..e53b704988 100644 --- a/nym-wallet/src/pages/bond/index.tsx +++ b/nym-wallet/src/pages/bond/index.tsx @@ -1,32 +1,27 @@ import React, { useContext, useEffect, useState } from 'react' import { Alert, Box, Button, CircularProgress } from '@mui/material' import { BondForm } from './BondForm' +import { SuccessView } from './SuccessView' import { NymCard } from '../../components' import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus' import { Layout } from '../../layouts' -import { getGasFee, unbond } from '../../requests' -import { TFee } from '../../types' +import { unbond } from '../../requests' import { useCheckOwnership } from '../../hooks/useCheckOwnership' import { ClientContext } from '../../context/main' -import { SuccessView } from './SuccessView' +import { Bond as BondIcon } from '../../svg-icons/bond' export const Bond = () => { const [status, setStatus] = useState(EnumRequestStatus.initial) const [error, setError] = useState() const [successDetails, setSuccessDetails] = useState<{ amount: string; address: string }>() - const [fees, setFees] = useState() const { checkOwnership, ownership } = useCheckOwnership() - const { userBalance } = useContext(ClientContext) + const { userBalance, getBondDetails } = useContext(ClientContext) useEffect(() => { if (status === EnumRequestStatus.initial) { const initialiseForm = async () => { await checkOwnership() - setFees({ - mixnode: await getGasFee('BondMixnode'), - gateway: await getGasFee('BondGateway'), - }) setStatus(EnumRequestStatus.initial) } initialiseForm() @@ -35,28 +30,36 @@ export const Bond = () => { return ( - + + {status === EnumRequestStatus.initial && ( + + Always ensure you leave yourself enough funds to UNBOND + + )} {ownership?.hasOwnership && ( - { - setStatus(EnumRequestStatus.loading) - await unbond(ownership.nodeType!) - userBalance.fetchBalance() - setStatus(EnumRequestStatus.initial) - }} - data-testid="unBond" - > - Unbond - - } - style={{ margin: 2 }} - > - {`Looks like you already have a ${ownership.nodeType} bonded.`} - + + { + setStatus(EnumRequestStatus.loading) + await unbond(ownership.nodeType!) + await getBondDetails() + await userBalance.fetchBalance() + setStatus(EnumRequestStatus.initial) + }} + data-testid="unBond" + color="inherit" + > + Unbond + + } + > + {`Looks like you already have a ${ownership.nodeType} bonded.`} + + )} {status === EnumRequestStatus.loading && ( { )} {status === EnumRequestStatus.initial && ( { setError(e) setStatus(EnumRequestStatus.error) @@ -99,9 +101,8 @@ export const Bond = () => { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', - borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`, - bgcolor: 'grey.100', - padding: 2, + padding: 3, + pt: 0, }} > diff --git a/nym-wallet/src/pages/delegate/SuccessView.tsx b/nym-wallet/src/pages/delegate/SuccessView.tsx index 28afc7aaa2..82258e6088 100644 --- a/nym-wallet/src/pages/delegate/SuccessView.tsx +++ b/nym-wallet/src/pages/delegate/SuccessView.tsx @@ -17,7 +17,7 @@ export const SuccessView: React.FC<{ details?: { amount: string; address: string diff --git a/nym-wallet/src/pages/delegate/index.tsx b/nym-wallet/src/pages/delegate/index.tsx index 4720553348..db18363628 100644 --- a/nym-wallet/src/pages/delegate/index.tsx +++ b/nym-wallet/src/pages/delegate/index.tsx @@ -1,95 +1,91 @@ -import React, { useEffect, useState } from 'react' -import { Alert, AlertTitle, Box, Button, CircularProgress } from '@mui/material' +import React, { useState } from 'react' +import { Alert, AlertTitle, Box, Button, Link, Typography } from '@mui/material' import { DelegateForm } from './DelegateForm' import { Layout } from '../../layouts' import { NymCard } from '../../components' import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus' -import { TFee } from '../../types' -import { getGasFee } from '../../requests' import { SuccessView } from './SuccessView' +import { Delegate as DelegateIcon } from '../../svg-icons' +import { urls } from '../../context/main' export const Delegate = () => { const [status, setStatus] = useState(EnumRequestStatus.initial) const [error, setError] = useState() const [successDetails, setSuccessDetails] = useState<{ amount: string; address: string }>() - const [isLoading, setIsLoading] = useState(true) - const [fees, setFees] = useState() - - useEffect(() => { - const getFees = async () => { - const mixnode = await getGasFee('DelegateToMixnode') - setFees({ - mixnode: mixnode, - }) - setIsLoading(false) - } - getFees() - }, []) return ( - - {isLoading && ( - - - - )} - <> - {status === EnumRequestStatus.initial && fees && ( - { - setStatus(EnumRequestStatus.error) - setError(message) - }} - onSuccess={(details) => { - setStatus(EnumRequestStatus.success) - setSuccessDetails(details) - }} - /> - )} - {status !== EnumRequestStatus.initial && ( - <> - - Delegation failed - An error occurred with the request: - {error} - - } - Success={} - /> - `1px solid ${theme.palette.grey[200]}`, - bgcolor: 'grey.100', - p: 2, + <> + + <> + {status === EnumRequestStatus.initial && ( + + Always ensure you leave yourself enough funds to UNDELEGATE + + )} + {status === EnumRequestStatus.initial && ( + { + setStatus(EnumRequestStatus.error) + setError(message) }} - > - - - - )} - - + + + + )} + + + + Checkout the{' '} + + list of mixnodes + {' '} + for uptime and performances to help make delegation decisions + + ) } diff --git a/nym-wallet/src/pages/internal-docs/ApiList.tsx b/nym-wallet/src/pages/internal-docs/ApiList.tsx index d55197cff6..997b177a00 100644 --- a/nym-wallet/src/pages/internal-docs/ApiList.tsx +++ b/nym-wallet/src/pages/internal-docs/ApiList.tsx @@ -139,7 +139,7 @@ export const ApiList = () => { diff --git a/nym-wallet/src/pages/receive/index.tsx b/nym-wallet/src/pages/receive/index.tsx index 66a41cd792..83ff2a9233 100644 --- a/nym-wallet/src/pages/receive/index.tsx +++ b/nym-wallet/src/pages/receive/index.tsx @@ -1,65 +1,38 @@ import React, { useContext } from 'react' import QRCode from 'qrcode.react' -import { Alert, Box, Card, Grid, Typography, useMediaQuery } from '@mui/material' +import { Alert, Box, Stack, Typography } from '@mui/material' import { CopyToClipboard, NymCard } from '../../components' import { Layout } from '../../layouts' -import { ClientContext } from '../../context/main' +import { ClientContext, MAJOR_CURRENCY } from '../../context/main' +import { ArrowBack } from '@mui/icons-material' export const Receive = () => { const { clientDetails } = useContext(ClientContext) - const matches = useMediaQuery('(min-width:769px)') return ( - - - - - You can receive tokens by providing this address to the sender - - - - + + + You can receive tokens by providing this address to the sender + + + - - - - {clientDetails?.client_address} - - - - - - {clientDetails && } - - - - - - + Your address: {clientDetails?.client_address} + + + + + {clientDetails && } + ) diff --git a/nym-wallet/src/pages/send/SendConfirmation.tsx b/nym-wallet/src/pages/send/SendConfirmation.tsx index b56aee04b5..96fced3fbb 100644 --- a/nym-wallet/src/pages/send/SendConfirmation.tsx +++ b/nym-wallet/src/pages/send/SendConfirmation.tsx @@ -49,7 +49,7 @@ export const SendConfirmation = ({ diff --git a/nym-wallet/src/pages/send/SendForm.tsx b/nym-wallet/src/pages/send/SendForm.tsx index cf0a75a0e9..2a7e718d2f 100644 --- a/nym-wallet/src/pages/send/SendForm.tsx +++ b/nym-wallet/src/pages/send/SendForm.tsx @@ -2,8 +2,9 @@ import React, { useContext } from 'react' import { Grid, InputAdornment, TextField, Typography } from '@mui/material' import { useFormContext } from 'react-hook-form' import { ClientContext, MAJOR_CURRENCY } from '../../context/main' +import { Fee } from '../../components' -export const SendForm = ({ transferFee }: { transferFee?: string }) => { +export const SendForm = () => { const { register, formState: { errors }, @@ -13,8 +14,7 @@ export const SendForm = ({ transferFee }: { transferFee?: string }) => { return ( - Your address - {clientDetails?.client_address} + Your address: {clientDetails?.client_address} @@ -48,9 +48,7 @@ export const SendForm = ({ transferFee }: { transferFee?: string }) => { /> - - Fee for this transaction: {`${transferFee} ${MAJOR_CURRENCY}`} - + ) diff --git a/nym-wallet/src/pages/send/SendReview.tsx b/nym-wallet/src/pages/send/SendReview.tsx index 5a96e40a01..f08fa183e9 100644 --- a/nym-wallet/src/pages/send/SendReview.tsx +++ b/nym-wallet/src/pages/send/SendReview.tsx @@ -1,10 +1,11 @@ -import React from 'react' +import React, { useContext } from 'react' import { Card, Divider, Grid, Typography } from '@mui/material' import { useFormContext } from 'react-hook-form' -import { MAJOR_CURRENCY } from '../../context/main' +import { ClientContext, MAJOR_CURRENCY } from '../../context/main' export const SendReview = ({ transferFee }: { transferFee?: string }) => { const { getValues } = useFormContext() + const { clientDetails } = useContext(ClientContext) const values = getValues() @@ -21,7 +22,7 @@ export const SendReview = ({ transferFee }: { transferFee?: string }) => { > - + @@ -33,7 +34,7 @@ export const SendReview = ({ transferFee }: { transferFee?: string }) => { - + diff --git a/nym-wallet/src/pages/send/SendWizard.tsx b/nym-wallet/src/pages/send/SendWizard.tsx index f3965b3d0d..a0fc1f73ab 100644 --- a/nym-wallet/src/pages/send/SendWizard.tsx +++ b/nym-wallet/src/pages/send/SendWizard.tsx @@ -102,7 +102,7 @@ export const SendWizard = () => { return ( - + { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', - borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`, - bgcolor: 'grey.100', - p: 2, + p: 3, }} > {activeStep === 1 && ( @@ -149,12 +147,13 @@ export const SendWizard = () => { )} diff --git a/nym-wallet/src/pages/send/index.tsx b/nym-wallet/src/pages/send/index.tsx index 43c51bf44e..7015225435 100644 --- a/nym-wallet/src/pages/send/index.tsx +++ b/nym-wallet/src/pages/send/index.tsx @@ -1,13 +1,14 @@ -import React, { useState } from 'react' +import React from 'react' import { NymCard } from '../../components' import { SendWizard } from './SendWizard' import { Layout } from '../../layouts' import { MAJOR_CURRENCY } from '../../context/main' +import { ArrowForward } from '@mui/icons-material' export const Send = () => { return ( - + diff --git a/nym-wallet/src/pages/settings/index.tsx b/nym-wallet/src/pages/settings/index.tsx index fca3b126dc..893f29e4d8 100644 --- a/nym-wallet/src/pages/settings/index.tsx +++ b/nym-wallet/src/pages/settings/index.tsx @@ -1,49 +1,39 @@ import React, { useContext, useEffect, useState } from 'react' -import { Alert, Box, Dialog, Typography } from '@mui/material' -import { SettingsOutlined } from '@mui/icons-material' +import { Alert, Box, Dialog } from '@mui/material' import { NymCard } from '../../components' import { ClientContext } from '../../context/main' import { Tabs } from './tabs' import { Profile } from './profile' import { SystemVariables } from './system-variables' import { NodeStats } from './node-stats' -import { Overview } from './overview' -import { getMixnodeBondDetails } from '../../requests' -import { TMixnodeBondDetails } from '../../types' +import { useSettingsState } from './useSettingsState' +import { NodeStatus } from '../../components/NodeStatus' +import { Node as NodeIcon } from '../../svg-icons/node' const tabs = ['Profile', 'System variables', 'Node stats'] export const Settings = () => { - const { showSettings, handleShowSettings } = useContext(ClientContext) const [selectedTab, setSelectedTab] = useState(0) - const [mixnodeDetails, setMixnodeDetails] = useState() - useEffect(() => { - const getBondDetails = async () => { - const details = await getMixnodeBondDetails() - setMixnodeDetails(details) - } - if (showSettings) getBondDetails() - }, [showSettings]) + const { mixnodeDetails, showSettings, handleShowSettings, getBondDetails } = useContext(ClientContext) + const { status, saturation, rewardEstimation } = useSettingsState(showSettings) - const handleTabChange = (event: React.SyntheticEvent, newTab: number) => setSelectedTab(newTab) + const handleTabChange = (_: React.SyntheticEvent, newTab: number) => setSelectedTab(newTab) return showSettings ? ( - Settings + + Node Settings } + Action={} noPadding > <> - - Node settings - - {!mixnodeDetails && ( You don't currently have a node running @@ -51,7 +41,12 @@ export const Settings = () => { )} {selectedTab === 0 && mixnodeDetails && } {selectedTab === 1 && mixnodeDetails && ( - + )} {selectedTab === 2 && mixnodeDetails && } diff --git a/nym-wallet/src/pages/settings/node-stats.tsx b/nym-wallet/src/pages/settings/node-stats.tsx index 24d186ffef..99b8a9d8a3 100644 --- a/nym-wallet/src/pages/settings/node-stats.tsx +++ b/nym-wallet/src/pages/settings/node-stats.tsx @@ -2,7 +2,6 @@ import { OpenInNew } from '@mui/icons-material' import { Button, Link, Stack, Typography } from '@mui/material' import React, { useEffect } from 'react' import { urls } from '../../context/main' -import { useCheckOwnership } from '../../hooks/useCheckOwnership' export const NodeStats = ({ mixnodeId }: { mixnodeId?: string }) => { return ( diff --git a/nym-wallet/src/pages/settings/overview.tsx b/nym-wallet/src/pages/settings/overview.tsx deleted file mode 100644 index 7554a56164..0000000000 --- a/nym-wallet/src/pages/settings/overview.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' -import { Divider, Stack, Typography } from '@mui/material' -import { CheckCircleOutline, CircleOutlined, PauseCircleOutlined } from '@mui/icons-material' -import { TMixnodeBondDetails } from '../../types' - -type TMixnodeStatus = 'active' | 'inactive' | 'standby' - -export const Overview = ({ - mixnodeStatus, - details, -}: { - mixnodeStatus?: TMixnodeStatus - details?: TMixnodeBondDetails | null -}) => ( - - Node identity {details?.mix_node.identity_key || 'n/a'} - {mixnodeStatus === 'active' && } - {mixnodeStatus === 'inactive' && } - {mixnodeStatus === 'standby' && } - - -) - -const ActiveMessage = () => ( - - Mixnode is active in this epoch - -) - -const InActiveMessage = () => ( - - Mixnode is active in this epoch - -) - -const StandbyMessage = () => ( - - Mixnode is on standy by in this epoch - -) diff --git a/nym-wallet/src/pages/settings/profile.tsx b/nym-wallet/src/pages/settings/profile.tsx index 228d63e8ad..7183ad675a 100644 --- a/nym-wallet/src/pages/settings/profile.tsx +++ b/nym-wallet/src/pages/settings/profile.tsx @@ -1,13 +1,18 @@ -import { Button, Stack, TextField } from '@mui/material' +import React, { useContext } from 'react' +import { Button, Divider, Stack, TextField, Typography } from '@mui/material' import { Box } from '@mui/system' -import React from 'react' -import { InfoTooltip } from '../../components/InfoToolTip' +import { ClientContext } from '../../context/main' export const Profile = () => { + const { mixnodeDetails } = useContext(ClientContext) return ( <> - + + + Node identity: {mixnodeDetails?.mix_node.identity_key || 'n/a'} + + @@ -18,12 +23,10 @@ export const Profile = () => { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', - borderTop: (theme) => `1px solid ${theme.palette.grey[300]}`, - bgcolor: 'grey.200', - padding: 2, + padding: 3, }} > - diff --git a/nym-wallet/src/pages/settings/system-variables.tsx b/nym-wallet/src/pages/settings/system-variables.tsx index befdbd72bc..68df022e22 100644 --- a/nym-wallet/src/pages/settings/system-variables.tsx +++ b/nym-wallet/src/pages/settings/system-variables.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from 'react' +import React, { useContext, useEffect, useState } from 'react' import { Box, Button, @@ -15,24 +15,29 @@ import { AccessTimeOutlined, PercentOutlined } from '@mui/icons-material' import { yupResolver } from '@hookform/resolvers/yup' import { useForm } from 'react-hook-form' import { InfoTooltip } from '../../components/InfoToolTip' -import { EnumNodeType, TMixnodeBondDetails } from '../../types' +import { TMixnodeBondDetails } from '../../types' import { validationSchema } from './validationSchema' -import { bond, unbond } from '../../requests' -import { ClientContext } from '../../context/main' +import { getGasFee, updateMixnode } from '../../requests' +import { ClientContext, MAJOR_CURRENCY } from '../../context/main' +import { Fee } from '../../components' type TFormData = { profitMarginPercent: number - signature: string } export const SystemVariables = ({ mixnodeDetails, - pledge, + saturation, + rewardEstimation, + onUpdate, }: { mixnodeDetails: TMixnodeBondDetails['mix_node'] - pledge: TMixnodeBondDetails['pledge_amount'] + saturation: number + rewardEstimation: number + onUpdate: () => void }) => { const [nodeUpdateResponse, setNodeUpdateResponse] = useState<'success' | 'failed'>() + const [configFee, setConfigFee] = useState() const { register, @@ -40,34 +45,27 @@ export const SystemVariables = ({ formState: { errors, isSubmitting }, } = useForm({ resolver: yupResolver(validationSchema), - defaultValues: { profitMarginPercent: mixnodeDetails.profit_margin_percent.toString(), signature: '' }, + defaultValues: { profitMarginPercent: mixnodeDetails.profit_margin_percent.toString() }, }) const { userBalance } = useContext(ClientContext) const onSubmit = async (data: TFormData) => { - await unbond(EnumNodeType.mixnode) - await bond({ - type: EnumNodeType.mixnode, - data: { ...mixnodeDetails, profit_margin_percent: data.profitMarginPercent }, - pledge: { denom: 'Minor', amount: pledge.amount }, - //hardcoded for the moment as required in bonding but not necessary - ownerSignature: data.signature, - }) - .then(() => { - userBalance.fetchBalance() - setNodeUpdateResponse('success') - }) - .catch((e) => { - setNodeUpdateResponse('failed') - console.log(e) - }) + try { + await updateMixnode({ profitMarginPercent: data.profitMarginPercent }) + await userBalance.fetchBalance() + onUpdate() + setNodeUpdateResponse('success') + } catch (e) { + setNodeUpdateResponse('failed') + console.log(e) + } } return ( <> - - + + - - } />} + Indicator={ + theme.palette.nym.fee, fontWeight: '600' }}> + {rewardEstimation} {MAJOR_CURRENCY} + + } /> - + } />} /> - } />} + Indicator={= 100} />} /> @@ -118,9 +111,8 @@ export const SystemVariables = ({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', - borderTop: (theme) => `1px solid ${theme.palette.grey[300]}`, - bgcolor: 'grey.200', - padding: 2, + p: 3, + pt: 0, }} > {nodeUpdateResponse === 'success' ? ( @@ -128,7 +120,7 @@ export const SystemVariables = ({ ) : nodeUpdateResponse === 'failed' ? ( Node updated failed ) : ( - + )} @@ -162,16 +155,21 @@ const DataField = ({ title, info, Indicator }: { title: string; info: string; In ) -const PercentIndicator = ({ value }: { value: number }) => { +const PercentIndicator = ({ value, warning }: { value: number; warning?: boolean }) => { return ( - + {value}% - + ) diff --git a/nym-wallet/src/pages/settings/useSettingsState.ts b/nym-wallet/src/pages/settings/useSettingsState.ts new file mode 100644 index 0000000000..715bf7f8bb --- /dev/null +++ b/nym-wallet/src/pages/settings/useSettingsState.ts @@ -0,0 +1,59 @@ +import { useContext, useEffect, useState } from 'react' +import { ClientContext } from '../../context/main' +import { + getMixnodeBondDetails, + getMixnodeRewardEstimation, + getMixnodeStakeSaturation, + getMixnodeStatus, + minorToMajor, +} from '../../requests' +import { MixnodeStatus } from '../../types' + +export const useSettingsState = (shouldUpdate: boolean) => { + const [status, setStatus] = useState('not_found') + const [saturation, setSaturation] = useState(0) + const [rewardEstimation, setRewardEstimation] = useState(0) + + const { mixnodeDetails } = useContext(ClientContext) + + const getStatus = async () => { + if (mixnodeDetails?.mix_node.identity_key) { + const status = await getMixnodeStatus(mixnodeDetails?.mix_node.identity_key) + setStatus(status.status) + } + } + + const getStakeSaturation = async () => { + if (mixnodeDetails?.mix_node.identity_key) { + const saturation = await getMixnodeStakeSaturation(mixnodeDetails?.mix_node.identity_key) + + if (saturation) { + setSaturation(Math.round(saturation.saturation * 100)) + } + } + } + + const getRewardEstimation = async () => { + if (mixnodeDetails?.mix_node.identity_key) { + const rewardEstimation = await getMixnodeRewardEstimation(mixnodeDetails?.mix_node.identity_key) + if (rewardEstimation) { + const toMajor = await minorToMajor(rewardEstimation.estimated_total_node_reward.toString()) + setRewardEstimation(parseInt(toMajor.amount)) + } + } + } + + useEffect(() => { + if (shouldUpdate) { + getStatus() + getStakeSaturation() + getRewardEstimation() + } + }, [shouldUpdate]) + + return { + status, + saturation, + rewardEstimation, + } +} diff --git a/nym-wallet/src/pages/settings/validationSchema.ts b/nym-wallet/src/pages/settings/validationSchema.ts index 4e76f3a6d4..5b96add517 100644 --- a/nym-wallet/src/pages/settings/validationSchema.ts +++ b/nym-wallet/src/pages/settings/validationSchema.ts @@ -2,5 +2,4 @@ import * as Yup from 'yup' export const validationSchema = Yup.object({ profitMarginPercent: Yup.number().typeError('profit margin percent must be a number').min(0).max(100).required(), - signature: Yup.string().required(), }) diff --git a/nym-wallet/src/pages/unbond/index.tsx b/nym-wallet/src/pages/unbond/index.tsx index 53291a39a6..f1ecadbd3c 100644 --- a/nym-wallet/src/pages/unbond/index.tsx +++ b/nym-wallet/src/pages/unbond/index.tsx @@ -1,15 +1,16 @@ import React, { useContext, useEffect, useState } from 'react' import { Alert, Box, Button, CircularProgress } from '@mui/material' -import { NymCard } from '../../components' +import { Fee, NymCard } from '../../components' import { Layout } from '../../layouts' import { useCheckOwnership } from '../../hooks/useCheckOwnership' import { ClientContext } from '../../context/main' import { unbond } from '../../requests' +import { Unbond as UnbondIcon } from '../../svg-icons' export const Unbond = () => { const [isLoading, setIsLoading] = useState(false) const { checkOwnership, ownership } = useCheckOwnership() - const { userBalance } = useContext(ClientContext) + const { userBalance, getBondDetails } = useContext(ClientContext) useEffect(() => { const initialiseForm = async () => { @@ -20,32 +21,39 @@ export const Unbond = () => { return ( - - {ownership?.hasOwnership && ( - { - setIsLoading(true) - await unbond(ownership.nodeType) - await userBalance.fetchBalance() - await checkOwnership() - setIsLoading(false) - }} - > - Unbond - - } - sx={{ m: 2 }} - > - {`Looks like you already have a ${ownership.nodeType} bonded.`} - - )} - {!ownership.hasOwnership && ( + + {ownership?.hasOwnership ? ( + <> + { + setIsLoading(true) + await unbond(ownership.nodeType) + await userBalance.fetchBalance() + await getBondDetails() + await checkOwnership() + setIsLoading(false) + }} + color="inherit" + > + Unbond + + } + sx={{ m: 2 }} + > + {`Looks like you already have a ${ownership.nodeType} bonded.`} + + + + + + + ) : ( You don't currently have a bonded node @@ -56,6 +64,7 @@ export const Unbond = () => { display: 'flex', justifyContent: 'center', p: 3, + pt: 0, }} > diff --git a/nym-wallet/src/pages/undelegate/UndelegateForm.tsx b/nym-wallet/src/pages/undelegate/UndelegateForm.tsx index aedb9884a9..c28032d740 100644 --- a/nym-wallet/src/pages/undelegate/UndelegateForm.tsx +++ b/nym-wallet/src/pages/undelegate/UndelegateForm.tsx @@ -6,6 +6,7 @@ import { validationSchema } from './validationSchema' import { EnumNodeType, TDelegation, TFee } from '../../types' import { ClientContext, MAJOR_CURRENCY } from '../../context/main' import { undelegate } from '../../requests' +import { Fee } from '../../components' type TFormData = { nodeType: EnumNodeType @@ -60,7 +61,7 @@ export const UndelegateForm = ({ return ( - + - - Fee for this transaction: {`${fees.mixnode.amount} ${MAJOR_CURRENCY}`}{' '} - + @@ -100,9 +99,8 @@ export const UndelegateForm = ({ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', - borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`, - bgcolor: 'grey.100', - p: 2, + p: 3, + pt: 0, }} > diff --git a/nym-wallet/src/pages/undelegate/index.tsx b/nym-wallet/src/pages/undelegate/index.tsx index 4e90d739f9..89cfb41470 100644 --- a/nym-wallet/src/pages/undelegate/index.tsx +++ b/nym-wallet/src/pages/undelegate/index.tsx @@ -6,6 +6,7 @@ import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus import { Alert, AlertTitle, Box, Button, CircularProgress } from '@mui/material' import { getGasFee, getReverseMixDelegations } from '../../requests' import { TFee, TPagedDelegations } from '../../types' +import { Undelegate as UndelegateIcon } from '../../svg-icons' export const Undelegate = () => { const [message, setMessage] = useState() @@ -42,7 +43,7 @@ export const Undelegate = () => { return ( - + {isLoading && ( { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', - borderTop: '1px solid grey[200]', - bgcolor: 'grey.100', - p: 2, + p: 3, + pt: 0, }} > diff --git a/nym-wallet/src/requests/index.ts b/nym-wallet/src/requests/index.ts index f07d219540..0afffa5bb2 100644 --- a/nym-wallet/src/requests/index.ts +++ b/nym-wallet/src/requests/index.ts @@ -6,11 +6,13 @@ import { EnumNodeType, Gateway, MixNode, + MixnodeStatusResponse, Operation, + RewardEstimationResponse, + StakeSaturationResponse, TauriContractStateParams, TauriTxResult, TCreateAccount, - TDelegation, TMixnodeBondDetails, TPagedDelegations, TSignInWithMnemonic, @@ -28,7 +30,7 @@ export const majorToMinor = async (amount: string): Promise => await invok // NOTE: this uses OUTDATED defaults that might have no resemblance with the reality // as for the actual transaction, the gas cost is being simulated beforehand export const getGasFee = async (operation: Operation): Promise => - await invoke('get_approximate_fee', { operation }) + await invoke('outdated_get_approximate_fee', { operation }) export const delegate = async ({ type, @@ -50,6 +52,7 @@ export const undelegate = async ({ export const send = async (args: { amount: Coin; address: string; memo: string }): Promise => await invoke('send', args) + export const checkMixnodeOwnership = async (): Promise => await invoke('owns_mixnode') export const checkGatewayOwnership = async (): Promise => await invoke('owns_gateway') @@ -83,3 +86,15 @@ export const getReverseGatewayDelegations = async (): Promise export const getMixnodeBondDetails = async (): Promise => await invoke('mixnode_bond_details') + +export const getMixnodeStakeSaturation = async (identity: string): Promise => + await invoke('mixnode_stake_saturation', { identity }) + +export const getMixnodeRewardEstimation = async (identity: string): Promise => + await invoke('mixnode_reward_estimation', { identity }) + +export const getMixnodeStatus = async (identity: string): Promise => + await invoke('mixnode_status', { identity }) + +export const updateMixnode = async ({ profitMarginPercent }: { profitMarginPercent: number }) => + await invoke('update_mixnode', { profitMarginPercent }) diff --git a/nym-wallet/src/svg-icons/node.tsx b/nym-wallet/src/svg-icons/node.tsx new file mode 100644 index 0000000000..dcaa794d86 --- /dev/null +++ b/nym-wallet/src/svg-icons/node.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { SvgIcon, SvgIconProps } from '@mui/material' + +export const Node = (props: SvgIconProps) => { + return ( + + + + ) +} diff --git a/nym-wallet/src/types/corenodestatusresponse.ts b/nym-wallet/src/types/corenodestatusresponse.ts new file mode 100644 index 0000000000..2a743b37a9 --- /dev/null +++ b/nym-wallet/src/types/corenodestatusresponse.ts @@ -0,0 +1,4 @@ +export interface CoreNodeStatusResponse { + identity: string; + count: number; +} \ No newline at end of file diff --git a/nym-wallet/src/types/global.ts b/nym-wallet/src/types/global.ts index 9523163a44..4d15f8214d 100644 --- a/nym-wallet/src/types/global.ts +++ b/nym-wallet/src/types/global.ts @@ -7,7 +7,7 @@ export enum EnumNodeType { export type TNodeOwnership = { hasOwnership: boolean - nodeType: EnumNodeType + nodeType?: EnumNodeType } export type TClientDetails = { diff --git a/nym-wallet/src/types/rust/account.ts b/nym-wallet/src/types/rust/account.ts index 196968055f..d07f41b6ab 100644 --- a/nym-wallet/src/types/rust/account.ts +++ b/nym-wallet/src/types/rust/account.ts @@ -4,5 +4,4 @@ export interface Account { contract_address: string; client_address: string; denom: Denom; - mnemonic: string | null; } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/index.ts b/nym-wallet/src/types/rust/index.ts index 8e22182af6..7f2af1e476 100644 --- a/nym-wallet/src/types/rust/index.ts +++ b/nym-wallet/src/types/rust/index.ts @@ -9,3 +9,8 @@ export * from './operation' export * from './stateparams' export * from './tauritxresult' export * from './transactiondetails' +export * from './mixnodestatus' +export * from './mixnodestatusresponse' +export * from './stakesaturaionresponse' +export * from './rewardestimationresponse' +export * from './mixnodestatus' diff --git a/nym-wallet/src/types/rust/mixnodestatus.ts b/nym-wallet/src/types/rust/mixnodestatus.ts new file mode 100644 index 0000000000..75849a8ce9 --- /dev/null +++ b/nym-wallet/src/types/rust/mixnodestatus.ts @@ -0,0 +1 @@ +export type MixnodeStatus = "Active" | "Standby" | "Inactive" | "NotFound"; \ No newline at end of file diff --git a/nym-wallet/src/types/rust/mixnodestatusresponse.ts b/nym-wallet/src/types/rust/mixnodestatusresponse.ts new file mode 100644 index 0000000000..6015807b4f --- /dev/null +++ b/nym-wallet/src/types/rust/mixnodestatusresponse.ts @@ -0,0 +1,5 @@ +import { MixnodeStatus } from "./mixnodestatus"; + +export interface MixnodeStatusResponse { + status: MixnodeStatus; +} \ No newline at end of file diff --git a/nym-wallet/src/types/rust/network.ts b/nym-wallet/src/types/rust/network.ts new file mode 100644 index 0000000000..037ec89ba1 --- /dev/null +++ b/nym-wallet/src/types/rust/network.ts @@ -0,0 +1 @@ +export type Network = "QA" | "SANDBOX"; \ No newline at end of file diff --git a/nym-wallet/src/types/rust/operation.ts b/nym-wallet/src/types/rust/operation.ts index 545c7bbe81..6c8d38c221 100644 --- a/nym-wallet/src/types/rust/operation.ts +++ b/nym-wallet/src/types/rust/operation.ts @@ -8,6 +8,7 @@ export type Operation = | "BondMixnodeOnBehalf" | "UnbondMixnode" | "UnbondMixnodeOnBehalf" + | "UpdateMixnodeConfig" | "DelegateToMixnode" | "DelegateToMixnodeOnBehalf" | "UndelegateFromMixnode" diff --git a/nym-wallet/src/types/rust/rewardestimationresponse.ts b/nym-wallet/src/types/rust/rewardestimationresponse.ts new file mode 100644 index 0000000000..973641f97c --- /dev/null +++ b/nym-wallet/src/types/rust/rewardestimationresponse.ts @@ -0,0 +1,9 @@ +export interface RewardEstimationResponse { + estimated_total_node_reward: bigint; + estimated_operator_reward: bigint; + estimated_delegators_reward: bigint; + current_epoch_start: bigint; + current_epoch_end: bigint; + current_epoch_uptime: number; + as_at: bigint; +} \ No newline at end of file diff --git a/nym-wallet/src/types/rust/stakesaturaionresponse.ts b/nym-wallet/src/types/rust/stakesaturaionresponse.ts new file mode 100644 index 0000000000..78044fb241 --- /dev/null +++ b/nym-wallet/src/types/rust/stakesaturaionresponse.ts @@ -0,0 +1,4 @@ +export interface StakeSaturationResponse { + saturation: number; + as_at: bigint; +} \ No newline at end of file diff --git a/nym-wallet/src/utils/index.ts b/nym-wallet/src/utils/index.ts index 94a4a3f59c..81024f16ff 100644 --- a/nym-wallet/src/utils/index.ts +++ b/nym-wallet/src/utils/index.ts @@ -1,7 +1,7 @@ import { invoke } from '@tauri-apps/api' import bs58 from 'bs58' import { minor, valid } from 'semver' -import { userBalance, majorToMinor } from '../requests' +import { userBalance, majorToMinor, getGasFee } from '../requests' import { Coin } from '../types' export const validateKey = (key: string, bytesLength: number): boolean => { @@ -86,8 +86,15 @@ export const validateRawPort = (rawPort: number): boolean => !isNaN(rawPort) && export const truncate = (text: string, trim: number) => text.substring(0, trim) + '...' +export const isGreaterThan = (a: number, b: number) => a > b + export const checkHasEnoughFunds = async (allocationValue: string) => { - const minorValue = await majorToMinor(allocationValue) - const walletValue = await userBalance() - return !(+walletValue.coin.amount - +minorValue.amount < 0) + try { + const walletValue = await userBalance() + const minorValue = await majorToMinor(allocationValue) + const remainingBalance = +walletValue.coin.amount - +minorValue.amount + return isGreaterThan(remainingBalance, 0) + } catch (e) { + console.log(e) + } } diff --git a/nym-wallet/tsconfig.json b/nym-wallet/tsconfig.json index e75fb24bc2..b3aa27c13b 100644 --- a/nym-wallet/tsconfig.json +++ b/nym-wallet/tsconfig.json @@ -1,76 +1,20 @@ { "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "esModuleInterop": true, "allowSyntheticDefaultImports": true, - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ - "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, - "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, - // "lib": [], /* Specify library files to be included in the compilation. */ - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ - "strict": true /* Enable all strict type-checking options. */, - "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - - /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - "typeRoots": [ - "../node_modules/@types", - "src/types", - "./" - ] /* List of folders to include type definitions from. */, - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - - /* Advanced Options */ - "skipLibCheck": true /* Skip type checking of declaration files. */, - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "strict": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": false, + "jsx": "react-jsx", + "sourceMap": true, + "baseUrl": "." }, - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "jest.config.js", "webpack.config.js", "webpack.prod.js", "webpack.common.js"] } diff --git a/testnet-faucet/yarn.lock b/testnet-faucet/yarn.lock index 15a51c7dc6..8e8a21efd3 100644 --- a/testnet-faucet/yarn.lock +++ b/testnet-faucet/yarn.lock @@ -2674,9 +2674,9 @@ find-root@^1.1.0: integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== follow-redirects@^1.0.0, follow-redirects@^1.14.0: - version "1.14.5" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" - integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA== + version "1.14.7" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" + integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== foreach@^2.0.5: version "2.0.5" diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 5f52d05c1d..77224e1816 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -39,7 +39,7 @@ time = { version = "0.3", features = ["serde-human-readable", "parsing"]} anyhow = "1" getset = "0.1.1" -rocket_sync_db_pools = {version = "0.1.0-rc.1", default-features = false} +rocket_sync_db_pools = { version = "0.1.0-rc.1", default-features = false } sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]} @@ -47,13 +47,17 @@ sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros" config = { path = "../common/config" } crypto = { path="../common/crypto" } gateway-client = { path="../common/client-libs/gateway-client" } -mixnet-contract = { path="../common/mixnet-contract" } +mixnet-contract-common = { path= "../common/cosmwasm-smart-contracts/mixnet-contract" } nymsphinx = { path="../common/nymsphinx" } topology = { path="../common/topology" } +validator-api-requests = { path = "validator-api-requests" } validator-client = { path="../common/client-libs/validator-client", features = ["nymd-client"] } version-checker = { path="../common/version-checker" } coconut-interface = { path = "../common/coconut-interface", optional = true } credentials = { path = "../common/credentials", optional = true } +# validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" +console-subscriber = { version = "0.1.1", optional = true} +cfg-if = "1.0" [features] coconut = ["coconut-interface", "credentials", "gateway-client/coconut"] @@ -61,4 +65,7 @@ coconut = ["coconut-interface", "credentials", "gateway-client/coconut"] [build-dependencies] tokio = { version = "1.4", features = ["rt-multi-thread", "macros"] } sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } -vergen = { version = "5", default-features = false, features = ["build", "git", "rustc", "cargo"] } \ No newline at end of file +vergen = { version = "5", default-features = false, features = ["build", "git", "rustc", "cargo"] } + +[dev-dependencies] +attohttpc = {version = "0.18.0", features = ["json"]} \ No newline at end of file diff --git a/validator-api/src/cache/mod.rs b/validator-api/src/cache/mod.rs index e35a84c41b..5efc838025 100644 --- a/validator-api/src/cache/mod.rs +++ b/validator-api/src/cache/mod.rs @@ -2,24 +2,48 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nymd_client::Client; +use crate::rewarding::EpochRewardParams; +use ::time::OffsetDateTime; use anyhow::Result; use config::defaults::VALIDATOR_API_VERSION; -use mixnet_contract::{ContractStateParams, GatewayBond, MixNodeBond, RewardingIntervalResponse}; +use mixnet_contract_common::{ + ContractStateParams, GatewayBond, IdentityKey, IdentityKeyRef, MixNodeBond, + RewardingIntervalResponse, +}; use rand::prelude::SliceRandom; use rand_chacha::rand_core::SeedableRng; use rand_chacha::ChaCha20Rng; use rocket::fairing::AdHoc; -use serde::Serialize; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::fmt; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::Duration; use tokio::sync::RwLock; use tokio::time; +use validator_api_requests::models::MixnodeStatus; use validator_client::nymd::hash::SHA256_HASH_SIZE; use validator_client::nymd::CosmWasmClient; pub(crate) mod routes; +#[derive(Debug, Serialize, Deserialize)] +pub struct InclusionProbabilityResponse { + in_active: f32, + in_reserve: f32, +} + +impl fmt::Display for InclusionProbabilityResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "in_active: {:.5}, in_reserve: {:.5}", + self.in_active, self.in_reserve + ) + } +} + pub struct ValidatorCacheRefresher { nymd_client: Client, cache: ValidatorCache, @@ -42,28 +66,40 @@ struct ValidatorCacheInner { current_mixnode_rewarded_set_size: AtomicU32, current_mixnode_active_set_size: AtomicU32, + + current_reward_params: RwLock>, +} + +fn current_unix_timestamp() -> i64 { + let now = OffsetDateTime::now_utc(); + now.unix_timestamp() } #[derive(Default, Serialize, Clone)] pub struct Cache { value: T, - as_at: u64, + as_at: i64, } impl Cache { + fn new(value: T) -> Self { + Cache { + value, + as_at: current_unix_timestamp(), + } + } + fn set(&mut self, value: T) { self.value = value; - self.as_at = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() + self.as_at = current_unix_timestamp() } fn renew(&mut self) { - self.as_at = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() + self.as_at = current_unix_timestamp() + } + + pub fn timestamp(&self) -> i64 { + self.as_at } pub fn into_inner(self) -> T { @@ -102,6 +138,8 @@ impl ValidatorCacheRefresher { ) .await?; + let epoch_rewarding_params = self.nymd_client.get_current_epoch_reward_params().await?; + info!( "Updating validator cache. There are {} mixnodes and {} gateways", mixnodes.len(), @@ -115,6 +153,7 @@ impl ValidatorCacheRefresher { contract_settings, current_rewarding_interval, rewarding_block_hash, + epoch_rewarding_params, ) .await; @@ -157,6 +196,7 @@ impl ValidatorCache { routes::get_gateways, routes::get_active_mixnodes, routes::get_rewarded_mixnodes, + routes::get_probs_mixnode_rewarded ], ) }) @@ -193,31 +233,152 @@ impl ValidatorCache { // seed our rng with the hash of the block of the most recent rewarding interval let mut rng = ChaCha20Rng::from_seed(block_hash.unwrap()); - // generate list of mixnodes and their relatively weight (by total stake) - let choices = mixnodes - .iter() - .map(|mix| { - // note that the theoretical maximum possible stake is equal to the total - // supply of all tokens, i.e. 1B (which is 1 quadrillion of native tokens, i.e. 10^15 ~ 2^50) - // which is way below maximum value of f64, so the cast is fine - let total_stake = mix.total_stake().unwrap_or_default() as f64; - (mix, total_stake) - }) // if for some reason node is invalid, treat it as 0 stake/weight - .collect::>(); + self.stake_weighted_choice(mixnodes, nodes_to_select as usize, &mut rng) + } + fn stake_weighted_choice( + &self, + mixnodes: &[MixNodeBond], + nodes_to_select: usize, + rng: &mut ChaCha20Rng, + ) -> Vec { + // generate list of mixnodes and their relatively weight (by total stake) + let choices = self.generate_mixnode_stake_tuples(mixnodes); // the unwrap here is fine as an error can only be thrown under one of the following conditions: // - our mixnode list is empty - we have already checked for that // - we have invalid weights, i.e. less than zero or NaNs - it shouldn't happen in our case as we safely cast down from u128 // - all weights are zero - it's impossible in our case as the list of nodes is not empty and weight is proportional to stake. You must have non-zero stake in order to bond // - we have more than u32::MAX values (which is incredibly unrealistic to have 4B mixnodes bonded... literally every other person on the planet would need one) choices - .choose_multiple_weighted(&mut rng, nodes_to_select as usize, |item| item.1) + .choose_multiple_weighted(rng, nodes_to_select, |item| item.1) .unwrap() - .map(|(bond, _weight)| *bond) + .map(|(bond, _weight)| bond) .cloned() .collect() } + fn generate_mixnode_stake_tuples(&self, mixnodes: &[MixNodeBond]) -> Vec<(MixNodeBond, f64)> { + mixnodes + .iter() + .map(|mix| { + // note that the theoretical maximum possible stake is equal to the total + // supply of all tokens, i.e. 1B (which is 1 quadrillion of native tokens, i.e. 10^15 ~ 2^50) + // which is way below maximum value of f64, so the cast is fine + let total_stake = mix.total_stake().unwrap_or_default() as f64; + (mix.clone(), total_stake) + }) // if for some reason node is invalid, treat it as 0 stake/weight + .collect() + } + + // Estimate probability that a node will end up in the rewarded set, by running the selection process 100 times and aggregating the results. + // If a node is in the active set it is not counted as being in the reserve set, the probabilities are exclusive. Cumulative probabilitiy + // can be obtained by summing the two probabilities returned. + async fn probs_mixnode_rewarded_calculate( + &self, + target_mixnode_id: IdentityKey, + mixnodes: Option>, + ) -> Option { + let mixnodes = if let Some(nodes) = mixnodes { + nodes + } else { + self.inner.mixnodes.read().await.value.clone() + }; + let total_bonded_tokens = mixnodes + .iter() + .fold(0u128, |acc, x| acc + x.total_stake().unwrap_or_default()) + as f64; + let target_mixnode = mixnodes + .iter() + .find(|x| x.identity() == &target_mixnode_id)?; + let rewarded_set_size = self + .inner + .current_mixnode_rewarded_set_size + .load(Ordering::SeqCst) as f64; + let active_set_size = self + .inner + .current_mixnode_active_set_size + .load(Ordering::SeqCst) as f64; + + // For running comparison tests below, needs improvement + // let rewarded_set_size = 720.; + // let active_set_size = 300.; + + let prob_one_draw = + target_mixnode.total_stake().unwrap_or_default() as f64 / total_bonded_tokens; + // Chance to be selected in any draw for active set + let prob_active_set = active_set_size * prob_one_draw; + // This is likely slightly too high, as we're not correcting form them not being selected in active, should be chance to be selected, minus the chance for being not selected in reserve + let prob_reserve_set = (rewarded_set_size - active_set_size) * prob_one_draw; + // (rewarded_set_size - active_set_size) * prob_one_draw * (1. - prob_active_set); + + Some(InclusionProbabilityResponse { + in_active: if prob_active_set > 1. { + 1. + } else { + prob_active_set + } as f32, + in_reserve: if prob_reserve_set > 1. { + 1. + } else { + prob_reserve_set + } as f32, + }) + } + + #[allow(dead_code)] + async fn probs_mixnode_rewarded_simulate( + &self, + target_mixnode_id: IdentityKey, + mixnodes: Option>, + ) -> Option { + let mut in_active = 0; + let mut in_reserve = 0; + let mixnodes = if let Some(nodes) = mixnodes { + nodes + } else { + self.inner.mixnodes.read().await.value.clone() + }; + let rewarded_set_size = self + .inner + .current_mixnode_rewarded_set_size + .load(Ordering::SeqCst) as usize; + let active_set_size = self + .inner + .current_mixnode_active_set_size + .load(Ordering::SeqCst) as usize; + let mut rng = ChaCha20Rng::from_entropy(); + + // For running comparison tests below, needs improvement + // let rewarded_set_size = 720.; + // let active_set_size = 300.; + + let it = 100; + + for _ in 0..it { + let mut rewarded_set = + self.stake_weighted_choice(&mixnodes, rewarded_set_size, &mut rng); + let reserve_set = rewarded_set + .split_off(active_set_size) + .iter() + .map(|bond| bond.identity().clone()) + .collect::>(); + let active_set = rewarded_set + .iter() + .map(|bond| bond.identity().clone()) + .collect::>(); + + if active_set.contains(&target_mixnode_id) { + in_active += 1; + } else if reserve_set.contains(&target_mixnode_id) { + in_reserve += 1; + } + } + Some(InclusionProbabilityResponse { + in_active: in_active as f32 / it as f32, + in_reserve: in_reserve as f32 / it as f32, + }) + } + async fn update_cache( &self, mixnodes: Vec, @@ -225,6 +386,7 @@ impl ValidatorCache { state: ContractStateParams, rewarding_interval: RewardingIntervalResponse, rewarding_block_hash: Option<[u8; SHA256_HASH_SIZE]>, + epoch_rewarding_params: EpochRewardParams, ) { // if the rewarding is currently in progress, don't mess with the rewarded/active sets // as most likely will be changed next time this function is called @@ -270,6 +432,11 @@ impl ValidatorCache { self.inner.mixnodes.write().await.set(mixnodes); self.inner.gateways.write().await.set(gateways); + self.inner + .current_reward_params + .write() + .await + .set(epoch_rewarding_params); } pub async fn mixnodes(&self) -> Cache> { @@ -305,6 +472,58 @@ impl ValidatorCache { } } + pub(crate) async fn epoch_reward_params(&self) -> Cache { + self.inner.current_reward_params.read().await.clone() + } + + pub async fn mixnode_details( + &self, + identity: IdentityKeyRef<'_>, + ) -> (Option, MixnodeStatus) { + // it might not be the most optimal to possibly iterate the entire vector to find (or not) + // the relevant value. However, the vectors are relatively small (< 10_000 elements) and + // the implementation for active/rewarded sets might change soon so there's no point in premature optimisation + // with HashSets + let rewarded_mixnodes = &self.inner.rewarded_mixnodes.read().await.value; + let active_set_size = self + .inner + .current_mixnode_active_set_size + .load(Ordering::SeqCst) as usize; + + // see if node is in the top active_set_size of rewarded nodes, i.e. it's active + if let Some(bond) = rewarded_mixnodes + .iter() + .take(active_set_size) + .find(|mix| mix.mix_node.identity_key == identity) + { + (Some(bond.clone()), MixnodeStatus::Active) + // see if it's in the bottom part of the rewarded set, i.e. it's in standby + } else if let Some(bond) = rewarded_mixnodes + .iter() + .skip(active_set_size) + .find(|mix| mix.mix_node.identity_key == identity) + { + (Some(bond.clone()), MixnodeStatus::Standby) + // if it's not in the rewarded set see if its bonded at all + } else if let Some(bond) = self + .inner + .mixnodes + .read() + .await + .value + .iter() + .find(|mix| mix.mix_node.identity_key == identity) + { + (Some(bond.clone()), MixnodeStatus::Inactive) + } else { + (None, MixnodeStatus::NotFound) + } + } + + pub async fn mixnode_status(&self, identity: IdentityKey) -> MixnodeStatus { + self.mixnode_details(&identity).await.1 + } + pub fn initialised(&self) -> bool { self.inner.initialised.load(Ordering::Relaxed) } @@ -332,6 +551,51 @@ impl ValidatorCacheInner { rewarded_mixnodes: RwLock::new(Cache::default()), current_mixnode_rewarded_set_size: Default::default(), current_mixnode_active_set_size: Default::default(), + current_reward_params: RwLock::new(Cache::new(EpochRewardParams::new_empty())), } } } + +// #[cfg(test)] +// mod test { +// use crate::cache::InclusionProbabilityResponse; + +// use super::ValidatorCache; +// use mixnet_contract_common::MixNodeBond; + +// #[tokio::test] +// async fn test_inclusion_probabilities() { +// let cache = ValidatorCache::new(); +// let response = attohttpc::get("https://sandbox-validator.nymtech.net/api/v1/mixnodes") +// .send() +// .unwrap(); +// let mixnodes: Vec = response.json().unwrap(); +// let calculated = cache +// .probs_mixnode_rewarded_calculate( +// "ysmgeYJQPBFzB2TgqBjN5BE3Rb79CgFANrnJaYd8woQ".to_string(), +// Some(mixnodes.clone()), +// ) +// .await +// .unwrap(); +// let mut simulated_avg = Vec::new(); +// for _ in 0..1000 { +// let simulated = cache +// .probs_mixnode_rewarded_simulate( +// "ysmgeYJQPBFzB2TgqBjN5BE3Rb79CgFANrnJaYd8woQ".to_string(), +// Some(mixnodes.clone()), +// ) +// .await +// .unwrap(); +// simulated_avg.push(simulated); +// } +// let simulated = simulated_avg.iter().fold((0., 0.), |acc, x| { +// (acc.0 + x.in_active, acc.1 + x.in_reserve) +// }); +// let simulated = InclusionProbabilityResponse { +// in_active: simulated.0 / 100., +// in_reserve: simulated.1 / 100., +// }; +// println!("calculted: {calculated}"); +// println!("simulated: {simulated}"); +// } +// } diff --git a/validator-api/src/cache/routes.rs b/validator-api/src/cache/routes.rs index cf59be469e..c08c520b7f 100644 --- a/validator-api/src/cache/routes.rs +++ b/validator-api/src/cache/routes.rs @@ -1,8 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::cache::InclusionProbabilityResponse; use crate::cache::ValidatorCache; -use mixnet_contract::{GatewayBond, MixNodeBond}; +use mixnet_contract_common::{GatewayBond, MixNodeBond}; use rocket::serde::json::Json; use rocket::State; @@ -25,3 +26,15 @@ pub(crate) async fn get_rewarded_mixnodes(cache: &State) -> Json pub(crate) async fn get_active_mixnodes(cache: &State) -> Json> { Json(cache.active_mixnodes().await.value) } + +#[get("/mixnodes/rewarded/inclusion-probability/")] +pub(crate) async fn get_probs_mixnode_rewarded( + cache: &State, + mixnode_id: String, +) -> Json> { + Json( + cache + .probs_mixnode_rewarded_calculate(mixnode_id, None) + .await, + ) +} diff --git a/validator-api/src/coconut/mod.rs b/validator-api/src/coconut/mod.rs index ace0c62851..cd50c7cdf8 100644 --- a/validator-api/src/coconut/mod.rs +++ b/validator-api/src/coconut/mod.rs @@ -62,7 +62,7 @@ fn blind_sign(request: InternalSignRequest, key_pair: &KeyPair) -> BlindedSignat .unwrap() } -#[post("/blind_sign", data = "")] +#[post("/blind-sign", data = "")] // Until we have serialization and deserialization traits we'll be using a crutch pub async fn post_blind_sign( blind_sign_request_body: Json, @@ -79,7 +79,7 @@ pub async fn post_blind_sign( Json(BlindedSignatureResponse::new(blinded_signature)) } -#[get("/verification_key")] +#[get("/verification-key")] pub async fn get_verification_key(key_pair: &State) -> Json { Json(VerificationKeyResponse::new(key_pair.verification_key())) } diff --git a/validator-api/src/config/mod.rs b/validator-api/src/config/mod.rs index c5513710ee..094a0f820e 100644 --- a/validator-api/src/config/mod.rs +++ b/validator-api/src/config/mod.rs @@ -36,7 +36,7 @@ const DEFAULT_MINIMUM_TEST_ROUTES: usize = 1; const DEFAULT_ROUTE_TEST_PACKETS: usize = 1000; const DEFAULT_PER_NODE_TEST_PACKETS: usize = 3; -const DEFAULT_CACHE_INTERVAL: Duration = Duration::from_secs(5); +const DEFAULT_CACHE_INTERVAL: Duration = Duration::from_secs(30); const DEFAULT_MONITOR_THRESHOLD: u8 = 60; #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index a74ef82f28..698cbc970c 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -302,7 +302,7 @@ fn override_config(mut config: Config, matches: &ArgMatches) -> Config { let monitor_threshold = monitor_threshold.expect("Provided monitor threshold is not a number!"); assert!( - !(monitor_threshold > 100), + monitor_threshold <= 100, "Provided monitor threshold is greater than 100!" ); config = config.with_minimum_epoch_monitor_threshold(monitor_threshold) @@ -399,6 +399,7 @@ fn expected_monitor_test_runs(config: &Config) -> usize { fn setup_rewarder( config: &Config, + first_epoch: Epoch, rocket: &Rocket, nymd_client: &Client, ) -> Option { @@ -407,10 +408,6 @@ fn setup_rewarder( let node_status_storage = rocket.state::().unwrap().clone(); let validator_cache = rocket.state::().unwrap().clone(); - let first_epoch = Epoch::new( - config.get_first_rewarding_epoch(), - config.get_epoch_length(), - ); Some(Rewarder::new( nymd_client.clone(), validator_cache, @@ -427,11 +424,16 @@ fn setup_rewarder( } } -async fn setup_rocket(config: &Config, liftoff_notify: Arc) -> Result> { +async fn setup_rocket( + config: &Config, + first_epoch: Epoch, + liftoff_notify: Arc, +) -> Result> { // let's build our rocket! let rocket = rocket::build() .attach(setup_cors()?) .attach(setup_liftoff_notify(liftoff_notify)) + .manage(first_epoch) .attach(ValidatorCache::stage()); #[cfg(feature = "coconut")] @@ -440,13 +442,17 @@ async fn setup_rocket(config: &Config, liftoff_notify: Arc) -> Result) -> Result<()> { .map_err(|err| err.into()); } + let first_epoch = Epoch::new( + config.get_first_rewarding_epoch(), + config.get_epoch_length(), + ); + let liftoff_notify = Arc::new(Notify::new()); // let's build our rocket! - let rocket = setup_rocket(&config, Arc::clone(&liftoff_notify)).await?; + let rocket = setup_rocket(&config, first_epoch, Arc::clone(&liftoff_notify)).await?; let monitor_builder = setup_network_monitor(&config, system_version, &rocket); let validator_cache = rocket.state::().unwrap().clone(); @@ -513,7 +524,7 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { let uptime_updater = HistoricalUptimeUpdater::new(storage); tokio::spawn(async move { uptime_updater.run().await }); - if let Some(rewarder) = setup_rewarder(&config, &rocket, &nymd_client) { + if let Some(rewarder) = setup_rewarder(&config, first_epoch, &rocket, &nymd_client) { info!("Periodic rewarding is starting..."); tokio::spawn(async move { rewarder.run().await }); } else { @@ -559,6 +570,11 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { async fn main() -> Result<()> { println!("Starting validator api..."); + cfg_if::cfg_if! {if #[cfg(feature = "console-subscriber")] { + // instriment tokio console subscriber needs RUSTFLAGS="--cfg tokio_unstable" at build time + console_subscriber::init(); + }} + setup_logging(); let args = parse_args(); run_validator_api(args).await diff --git a/validator-api/src/network_monitor/monitor/preparer.rs b/validator-api/src/network_monitor/monitor/preparer.rs index 160e5e8eda..6fed3f643a 100644 --- a/validator-api/src/network_monitor/monitor/preparer.rs +++ b/validator-api/src/network_monitor/monitor/preparer.rs @@ -8,7 +8,7 @@ use crate::network_monitor::test_packet::{NodeType, TestPacket}; use crate::network_monitor::test_route::TestRoute; use crypto::asymmetric::{encryption, identity}; use log::info; -use mixnet_contract::{Addr, GatewayBond, Layer, MixNodeBond}; +use mixnet_contract_common::{Addr, GatewayBond, Layer, MixNodeBond}; use nymsphinx::addressing::clients::Recipient; use nymsphinx::forwarding::packet::MixPacket; use rand::seq::SliceRandom; diff --git a/validator-api/src/node_status_api/mod.rs b/validator-api/src/node_status_api/mod.rs index 3316aec6f4..697fca4403 100644 --- a/validator-api/src/node_status_api/mod.rs +++ b/validator-api/src/node_status_api/mod.rs @@ -1,9 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::storage; use rocket::fairing::AdHoc; -use std::path::PathBuf; use std::time::Duration; pub(crate) mod local_guard; @@ -16,20 +14,35 @@ pub(crate) const FIFTEEN_MINUTES: Duration = Duration::from_secs(900); pub(crate) const ONE_HOUR: Duration = Duration::from_secs(3600); pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400); -pub(crate) fn stage(database_path: PathBuf) -> AdHoc { - AdHoc::on_ignite("SQLx Stage", |rocket| async { - rocket - .attach(storage::ValidatorApiStorage::stage(database_path)) - .mount( - "/v1/status", - routes![ - routes::mixnode_report, - routes::gateway_report, - routes::mixnode_uptime_history, - routes::gateway_uptime_history, - routes::mixnode_core_status_count, - routes::gateway_core_status_count, - ], - ) +pub(crate) fn stage_full() -> AdHoc { + AdHoc::on_ignite("Node Status API Stage", |rocket| async { + rocket.mount( + "/v1/status", + routes![ + routes::mixnode_report, + routes::gateway_report, + routes::mixnode_uptime_history, + routes::gateway_uptime_history, + routes::mixnode_core_status_count, + routes::gateway_core_status_count, + routes::get_mixnode_status, + routes::get_mixnode_reward_estimation, + routes::get_mixnode_stake_saturation, + ], + ) + }) +} + +// in the minimal variant we would not have access to endpoints relying on existence +// of the network monitor and the associated storage +pub(crate) fn stage_minimal() -> AdHoc { + AdHoc::on_ignite("Node Status API Stage", |rocket| async { + rocket.mount( + "/v1/status", + routes![ + routes::get_mixnode_status, + routes::get_mixnode_stake_saturation, + ], + ) }) } diff --git a/validator-api/src/node_status_api/models.rs b/validator-api/src/node_status_api/models.rs index a3fbe6ee63..4ad57950dd 100644 --- a/validator-api/src/node_status_api/models.rs +++ b/validator-api/src/node_status_api/models.rs @@ -208,22 +208,24 @@ pub struct HistoricalUptime { } pub(crate) struct ErrorResponse { - error: ValidatorApiStorageError, + error_message: String, status: Status, } impl ErrorResponse { - pub(crate) fn new(error: ValidatorApiStorageError, status: Status) -> Self { - ErrorResponse { error, status } + pub(crate) fn new(error_message: impl Into, status: Status) -> Self { + ErrorResponse { + error_message: error_message.into(), + status, + } } } impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { - let message = format!("{}", self.error); Response::build() .header(ContentType::Plain) - .sized_body(message.len(), Cursor::new(message)) + .sized_body(self.error_message.len(), Cursor::new(self.error_message)) .status(self.status) .ok() } @@ -269,9 +271,3 @@ impl Display for ValidatorApiStorageError { } } } - -#[derive(Serialize)] -pub struct CoreNodeStatus { - pub(crate) identity: String, - pub(crate) count: i32, -} diff --git a/validator-api/src/node_status_api/routes.rs b/validator-api/src/node_status_api/routes.rs index 7b732a5977..986c0d5919 100644 --- a/validator-api/src/node_status_api/routes.rs +++ b/validator-api/src/node_status_api/routes.rs @@ -2,92 +2,189 @@ // SPDX-License-Identifier: Apache-2.0 use crate::node_status_api::models::{ - CoreNodeStatus, ErrorResponse, GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, + ErrorResponse, GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, MixnodeUptimeHistory, }; use crate::storage::ValidatorApiStorage; +use crate::{Epoch, ValidatorCache}; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; +use time::OffsetDateTime; +use validator_api_requests::models::{ + CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, + StakeSaturationResponse, +}; -#[get("/mixnode//report")] +#[get("/mixnode//report")] pub(crate) async fn mixnode_report( storage: &State, - pubkey: &str, + identity: &str, ) -> Result, ErrorResponse> { storage - .construct_mixnode_report(pubkey) + .construct_mixnode_report(identity) .await .map(Json) - .map_err(|err| ErrorResponse::new(err, Status::NotFound)) + .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } -#[get("/gateway//report")] +#[get("/gateway//report")] pub(crate) async fn gateway_report( storage: &State, - pubkey: &str, + identity: &str, ) -> Result, ErrorResponse> { storage - .construct_gateway_report(pubkey) + .construct_gateway_report(identity) .await .map(Json) - .map_err(|err| ErrorResponse::new(err, Status::NotFound)) + .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } -#[get("/mixnode//history")] +#[get("/mixnode//history")] pub(crate) async fn mixnode_uptime_history( storage: &State, - pubkey: &str, + identity: &str, ) -> Result, ErrorResponse> { storage - .get_mixnode_uptime_history(pubkey) + .get_mixnode_uptime_history(identity) .await .map(Json) - .map_err(|err| ErrorResponse::new(err, Status::NotFound)) + .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } -#[get("/gateway//history")] +#[get("/gateway//history")] pub(crate) async fn gateway_uptime_history( storage: &State, - pubkey: &str, + identity: &str, ) -> Result, ErrorResponse> { storage - .get_gateway_uptime_history(pubkey) + .get_gateway_uptime_history(identity) .await .map(Json) - .map_err(|err| ErrorResponse::new(err, Status::NotFound)) + .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } -#[get("/mixnode//core-status-count?")] +#[get("/mixnode//core-status-count?")] pub(crate) async fn mixnode_core_status_count( storage: &State, - pubkey: &str, + identity: &str, since: Option, -) -> Json { +) -> Json { let count = storage - .get_core_mixnode_status_count(pubkey, since) + .get_core_mixnode_status_count(identity, since) .await .unwrap_or_default(); - Json(CoreNodeStatus { - identity: pubkey.to_string(), + Json(CoreNodeStatusResponse { + identity: identity.to_string(), count, }) } -#[get("/gateway//core-status-count?")] +#[get("/gateway//core-status-count?")] pub(crate) async fn gateway_core_status_count( storage: &State, - pubkey: &str, + identity: &str, since: Option, -) -> Json { +) -> Json { let count = storage - .get_core_gateway_status_count(pubkey, since) + .get_core_gateway_status_count(identity, since) .await .unwrap_or_default(); - Json(CoreNodeStatus { - identity: pubkey.to_string(), + Json(CoreNodeStatusResponse { + identity: identity.to_string(), count, }) } + +#[get("/mixnode//status")] +pub(crate) async fn get_mixnode_status( + cache: &State, + identity: String, +) -> Json { + Json(MixnodeStatusResponse { + status: cache.mixnode_status(identity).await, + }) +} + +#[get("/mixnode//reward-estimation")] +pub(crate) async fn get_mixnode_reward_estimation( + cache: &State, + storage: &State, + first_epoch: &State, + identity: String, +) -> Result, ErrorResponse> { + let (bond, status) = cache.mixnode_details(&identity).await; + if let Some(bond) = bond { + let epoch_reward_params = cache.epoch_reward_params().await; + let as_at = epoch_reward_params.timestamp(); + let epoch_reward_params = epoch_reward_params.into_inner(); + + let current_epoch = first_epoch.current(OffsetDateTime::now_utc()); + let uptime = storage + .get_average_mixnode_uptime_in_interval( + &identity, + current_epoch.start_unix_timestamp(), + current_epoch.end_unix_timestamp(), + ) + .await + .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))?; + + match epoch_reward_params.estimate_reward(&bond, uptime.u8(), status.is_active()) { + Ok(( + estimated_total_node_reward, + estimated_operator_reward, + estimated_delegators_reward, + )) => { + let reponse = RewardEstimationResponse { + estimated_total_node_reward, + estimated_operator_reward, + estimated_delegators_reward, + current_epoch_start: current_epoch.start_unix_timestamp(), + current_epoch_end: current_epoch.end_unix_timestamp(), + current_epoch_uptime: uptime.u8(), + as_at, + }; + Ok(Json(reponse)) + } + Err(e) => Err(ErrorResponse::new( + e.to_string(), + Status::InternalServerError, + )), + } + } else { + Err(ErrorResponse::new( + "mixnode bond not found", + Status::NotFound, + )) + } +} + +#[get("/mixnode//stake-saturation")] +pub(crate) async fn get_mixnode_stake_saturation( + cache: &State, + identity: String, +) -> Result, ErrorResponse> { + let (bond, _) = cache.mixnode_details(&identity).await; + if let Some(bond) = bond { + let epoch_reward_params = cache.epoch_reward_params().await; + let as_at = epoch_reward_params.timestamp(); + let epoch_reward_params = epoch_reward_params.into_inner(); + + let saturation = bond.stake_saturation( + epoch_reward_params.circulating_supply, + epoch_reward_params.rewarded_set_size, + ); + + Ok(Json(StakeSaturationResponse { + saturation: saturation.to_num(), + as_at, + })) + } else { + Err(ErrorResponse::new( + "mixnode bond not found", + Status::NotFound, + )) + } +} diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index f38503efd7..1481ad2682 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::Config; -use crate::rewarding::{error::RewardingError, MixnodeToReward}; +use crate::rewarding::{error::RewardingError, EpochRewardParams, MixnodeToReward}; use config::defaults::DEFAULT_VALIDATOR_API_PORT; -use mixnet_contract::{ +use mixnet_contract_common::{ ContractStateParams, Delegation, ExecuteMsg, GatewayBond, IdentityKey, MixNodeBond, MixnodeRewardingStatusResponse, RewardingIntervalResponse, MIXNODE_DELEGATORS_PAGE_LIMIT, }; @@ -98,34 +98,6 @@ impl Client { Ok(time) } - pub(crate) async fn get_reward_pool(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.0.read().await.get_reward_pool().await?) - } - - pub(crate) async fn get_circulating_supply(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.0.read().await.get_circulating_supply().await?) - } - - pub(crate) async fn get_sybil_resistance_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.0.read().await.get_sybil_resistance_percent().await?) - } - - pub(crate) async fn get_epoch_reward_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.0.read().await.get_epoch_reward_percent().await?) - } - pub(crate) async fn get_mixnodes(&self) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync, @@ -158,9 +130,34 @@ impl Client { self.0.read().await.get_current_rewarding_interval().await } + pub(crate) async fn get_current_epoch_reward_params( + &self, + ) -> Result + where + C: CosmWasmClient + Sync, + { + let this = self.0.read().await; + + let state = this.get_contract_settings().await?; + let reward_pool = this.get_reward_pool().await?; + let epoch_reward_percent = this.get_epoch_reward_percent().await?; + + let epoch_reward_params = EpochRewardParams { + reward_pool, + circulating_supply: this.get_circulating_supply().await?, + sybil_resistance_percent: this.get_sybil_resistance_percent().await?, + rewarded_set_size: state.mixnode_rewarded_set_size, + active_set_size: state.mixnode_active_set_size, + period_reward_pool: (reward_pool / 100) * epoch_reward_percent as u128, + active_set_work_factor: state.active_set_work_factor, + }; + + Ok(epoch_reward_params) + } + pub(crate) async fn get_rewarding_status( &self, - mix_identity: mixnet_contract::IdentityKey, + mix_identity: mixnet_contract_common::IdentityKey, rewarding_interval_nonce: u32, ) -> Result where diff --git a/validator-api/src/rewarding/error.rs b/validator-api/src/rewarding/error.rs index 347fa07ad6..4f20a878aa 100644 --- a/validator-api/src/rewarding/error.rs +++ b/validator-api/src/rewarding/error.rs @@ -23,6 +23,12 @@ pub(crate) enum RewardingError { #[error("Failed to query the smart contract - {0}")] ValidatorClientError(ValidatorClientError), + + #[error("Error downcasting u128 -> u64")] + DowncastingError { + #[from] + source: std::num::TryFromIntError, + }, } impl From for RewardingError { diff --git a/validator-api/src/rewarding/mod.rs b/validator-api/src/rewarding/mod.rs index bccd83e4ee..fc67a7bba8 100644 --- a/validator-api/src/rewarding/mod.rs +++ b/validator-api/src/rewarding/mod.rs @@ -12,8 +12,10 @@ use crate::storage::models::{ use crate::storage::ValidatorApiStorage; use config::defaults::DENOM; use log::{error, info}; -use mixnet_contract::mixnode::NodeRewardParams; -use mixnet_contract::{ExecuteMsg, IdentityKey, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT}; +use mixnet_contract_common::mixnode::NodeRewardParams; +use mixnet_contract_common::{ + ExecuteMsg, IdentityKey, MixNodeBond, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT, +}; use std::convert::TryInto; use std::time::Duration; use time::OffsetDateTime; @@ -23,14 +25,67 @@ use validator_client::nymd::SigningNymdClient; pub(crate) mod epoch; pub(crate) mod error; -struct EpochRewardParams { - reward_pool: u128, - circulating_supply: u128, - sybil_resistance_percent: u8, - rewarded_set_size: u32, - active_set_size: u32, - period_reward_pool: u128, - active_set_work_factor: u8, +#[derive(Copy, Clone)] +pub(crate) struct EpochRewardParams { + pub(crate) reward_pool: u128, + pub(crate) circulating_supply: u128, + pub(crate) sybil_resistance_percent: u8, + pub(crate) rewarded_set_size: u32, + pub(crate) active_set_size: u32, + pub(crate) period_reward_pool: u128, + pub(crate) active_set_work_factor: u8, +} + +impl EpochRewardParams { + // technically it's identical to what would have been derived with a Default implementation, + // however, I prefer to be explicit about it, as a `Default::default` value makes no sense + // apart from the `ValidatorCacheInner` context, where this value is not going to be touched anyway + // (it's guarded behind an `initialised` flag) + pub(crate) fn new_empty() -> Self { + EpochRewardParams { + reward_pool: 0, + circulating_supply: 0, + sybil_resistance_percent: 0, + rewarded_set_size: 0, + active_set_size: 0, + period_reward_pool: 0, + active_set_work_factor: 0, + } + } + + pub(crate) fn estimate_reward( + &self, + node: &MixNodeBond, + uptime: u8, + in_active_set: bool, + ) -> Result<(u64, u64, u64), RewardingError> { + let node_reward_params = NodeRewardParams::new( + self.period_reward_pool, + self.rewarded_set_size.into(), + self.active_set_size.into(), + 0, + self.circulating_supply, + uptime.into(), + self.sybil_resistance_percent, + in_active_set, + self.active_set_work_factor, + ); + + let total_node_reward = node.reward(&node_reward_params); + let operator_reward = node.operator_reward(&node_reward_params); + let delegators_reward = + node.reward_delegation(node.total_delegation().amount, &node_reward_params); + + Ok(( + total_node_reward + .reward() + .checked_to_num::() + .unwrap_or_default() + .try_into()?, + operator_reward.try_into()?, + delegators_reward.try_into()?, + )) + } } #[derive(Debug, Clone)] @@ -117,24 +172,6 @@ impl Rewarder { } } - async fn epoch_reward_params(&self) -> Result { - let state = self.nymd_client.get_contract_settings().await?; - let reward_pool = self.nymd_client.get_reward_pool().await?; - let epoch_reward_percent = self.nymd_client.get_epoch_reward_percent().await?; - - let epoch_reward_params = EpochRewardParams { - reward_pool, - circulating_supply: self.nymd_client.get_circulating_supply().await?, - sybil_resistance_percent: self.nymd_client.get_sybil_resistance_percent().await?, - rewarded_set_size: state.mixnode_rewarded_set_size, - active_set_size: state.mixnode_active_set_size, - period_reward_pool: (reward_pool / 100) * epoch_reward_percent as u128, - active_set_work_factor: state.active_set_work_factor, - }; - - Ok(epoch_reward_params) - } - /// Obtains the current number of delegators that have delegated their stake towards this particular mixnode. /// /// # Arguments @@ -170,7 +207,7 @@ impl Rewarder { // and the lack of port data / verloc data will eventually be balanced out anyway // by people hesitating to delegate to nodes without them and thus those nodes disappearing // from the active set (once introduced) - let epoch_reward_params = self.epoch_reward_params().await?; + let epoch_reward_params = self.nymd_client.get_current_epoch_reward_params().await?; info!("Rewarding pool stats"); info!( diff --git a/validator-api/validator-api-requests/Cargo.toml b/validator-api/validator-api-requests/Cargo.toml new file mode 100644 index 0000000000..946dd5703d --- /dev/null +++ b/validator-api/validator-api-requests/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "validator-api-requests" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = "1.0" +ts-rs = { version = "5.1", optional = true } + diff --git a/validator-api/validator-api-requests/src/lib.rs b/validator-api/validator-api-requests/src/lib.rs new file mode 100644 index 0000000000..9beea87f0d --- /dev/null +++ b/validator-api/validator-api-requests/src/lib.rs @@ -0,0 +1,4 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; diff --git a/validator-api/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs new file mode 100644 index 0000000000..a7e5640837 --- /dev/null +++ b/validator-api/validator-api-requests/src/models.rs @@ -0,0 +1,53 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum MixnodeStatus { + Active, // in both the active set and the rewarded set + Standby, // only in the rewarded set + Inactive, // in neither the rewarded set nor the active set, but is bonded + NotFound, // doesn't even exist in the bonded set +} + +impl MixnodeStatus { + pub fn is_active(&self) -> bool { + *self == MixnodeStatus::Active + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +pub struct CoreNodeStatusResponse { + pub identity: String, + pub count: i32, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +pub struct MixnodeStatusResponse { + pub status: MixnodeStatus, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +pub struct RewardEstimationResponse { + pub estimated_total_node_reward: u64, + pub estimated_operator_reward: u64, + pub estimated_delegators_reward: u64, + + pub current_epoch_start: i64, + pub current_epoch_end: i64, + pub current_epoch_uptime: u8, + pub as_at: i64, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +pub struct StakeSaturationResponse { + pub saturation: f32, + pub as_at: i64, +}