diff --git a/ensemble/Cargo.lock b/ensemble/Cargo.lock new file mode 100644 index 00000000000..b165da6b681 --- /dev/null +++ b/ensemble/Cargo.lock @@ -0,0 +1,1502 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49f6183038e081170ebbbadee6678966c7d54728938a3e7de7f4e780770318f" +dependencies = [ + "byteorder", + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "bitflags 1.3.2", + "clap_lex", + "indexmap", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "cosmwasm-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea73e9162e6efde00018d55ed0061e93a108b5d6ec4548b4f8ce3c706249687" +dependencies = [ + "syn 1.0.109", +] + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519-zebra" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" +dependencies = [ + "curve25519-dalek", + "hashbrown", + "hex", + "rand_core 0.6.4", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint", + "der", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "errno" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fadroma" +version = "0.8.7" +dependencies = [ + "fadroma-derive-canonize", + "fadroma-derive-serde", + "fadroma-dsl", + "fadroma-proc-auth", + "schemars", + "secret-cosmwasm-std", + "serde", +] + +[[package]] +name = "fadroma-derive-canonize" +version = "0.3.5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fadroma-derive-serde" +version = "0.3.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fadroma-dsl" +version = "0.8.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fadroma-ensemble" +version = "0.1.0" +dependencies = [ + "anyhow", + "bincode2", + "criterion", + "fadroma", + "oorandom", + "proptest", + "secret-cosmwasm-std", + "serde", + "time", +] + +[[package]] +name = "fadroma-proc-auth" +version = "0.1.1" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "forward_ref" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2 0.10.8", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.4.1", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.7.5", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[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.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint", + "hmac", + "zeroize", +] + +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secret-cosmwasm-crypto" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8535d61c88d0a6c222df2cebb69859d8e9ba419a299a1bc84c904b0d9c00c7b2" +dependencies = [ + "digest 0.10.7", + "ed25519-zebra", + "k256", + "rand_core 0.6.4", + "thiserror", +] + +[[package]] +name = "secret-cosmwasm-std" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4393b01aa6587007161a6bb193859deaa8165ab06c8a35f253d329ff99e4d" +dependencies = [ + "base64", + "cosmwasm-derive", + "derivative", + "forward_ref", + "hex", + "schemars", + "secret-cosmwasm-crypto", + "serde", + "serde-json-wasm", + "thiserror", + "uint", +] + +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-json-wasm" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479b4dbc401ca13ee8ce902851b834893251404c4f3c65370a49e047a6be09a5" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + +[[package]] +name = "web-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/ensemble/Cargo.toml b/ensemble/Cargo.toml new file mode 100644 index 00000000000..6ba36e7a037 --- /dev/null +++ b/ensemble/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "fadroma-ensemble" +version = "0.1.0" +edition = "2021" +license = "AGPL-3.0" +keywords = ["blockchain", "cosmos", "cosmwasm", "smart-contract"] +description = "Testing framework for Secret Network" +repository = "https://github.com/hackbg/fadroma" +readme = "README.md" +authors = [ + "Asparuh Kamenov ", + "Adam A. ", + "denismaxim0v ", + "Chris Ricketts ", + "Tibor Hudik ", + "Wiz1991 ", + "hydropump3 <3ki2fiay@anonaddy.me>", + "Itzik " +] + +[lib] +path = "src/lib.rs" + +[package.metadata.docs.rs] +rustc-args = ["--cfg", "docsrs"] +all-features = true + +[features] +staking = [ "time/formatting", "secret-cosmwasm-std/staking" ] + +# Can't be used on the stable channel +#backtraces = [ "secret-cosmwasm-std/backtraces" ] + +[dependencies] +fadroma = { path = "..", features = [ "scrt", "scrt-staking" ] } +secret-cosmwasm-std = { version = "1.1.10", default-features = false, optional = true } +oorandom = { version = "11.1.3" } +anyhow = { version = "1.0.65" } +time = { optional = true, version = "0.3.17" } +serde = { version = "1.0.114", default-features = false, features = ["derive"] } + +# Enable iterator for testing (not supported in production) +[target.'cfg(not(target_arch="wasm32"))'.dependencies] +secret-cosmwasm-std = { version = "1.1.10", default-features = false, features = ["iterator", "random"], optional = true } + +[dev-dependencies] +criterion = "0.4.0" +bincode2 = "2.0.1" +proptest = "1.1.0" diff --git a/ensemble/README.md b/ensemble/README.md new file mode 100644 index 00000000000..540654afde1 --- /dev/null +++ b/ensemble/README.md @@ -0,0 +1,116 @@ +# Fadroma Ensemble + +![](https://img.shields.io/badge/version-0.1.0-blueviolet) + +**How to write multi-contract CosmWasm integration tests in Rust using `fadroma-ensemble`** + +## Introduction +Fadroma Ensemble provides a way to test multi-contract interactions without having to deploy contracts on-chain. + +## Getting started +To start testing with ensemble `ContractHarness` has to be implemented for each contract and registered by the `ContractEnsemble`. This approach allows a lot of flexibility for testing contracts. Mock implementations can be created, contract methods can be overridden, `Bank` interactions are also possible. + +### ContractHarness +`ContractHarness` defines entrypoints to any contract: `init`, `handle`, `query`. In order to implement contract we can use `DefaultImpl` from existing contract code, or override contract methods. You can also use the `impl_contract_harness!` macro. +```rust +// Here we create a ContractHarness implementation for an Oracle contract +use path::to::contracts::oracle; + +pub struct Oracle; +impl ContractHarness for Oracle { + // Use the method from the default implementation + fn instantiate(&self, deps: DepsMut, env: Env, info: MessageInfo, msg: Binary) -> AnyResult { + oracle::init( + deps, + env, + info, + from_binary(&msg)?, + oracle::DefaultImpl, + ).map_err(|x| anyhow::anyhow!(x)) + } + + fn execute(&self, deps: DepsMut, env: Env, info: MessageInfo, msg: Binary) -> AnyResult { + oracle::handle( + deps, + env, + info, + from_binary(&msg)?, + oracle::DefaultImpl, + ).map_err(|x| anyhow::anyhow!(x)) + } + + fn reply(&self, deps: DepsMut, env: Env, reply: Reply) -> AnyResult { + oracle::reply( + deps, + env, + reply, + oracle::DefaultImpl, + ).map_err(|x| anyhow::anyhow!(x)) + } + + // Override with some hardcoded value for the ease of testing + fn query(&self, deps: Deps, _env: Env, msg: Binary) -> AnyResult { + let msg = from_binary(&msg).unwrap(); + + let result = match msg { + oracle::QueryMsg::GetPrice { base_symbol: _, .. } => to_binary(&Uint128(1_000_000_000)), + // don't override the rest + _ => oracle::query(deps, from_binary(&msg)?, oracle::DefaultImpl) + }?; + + result + } +} +``` +### ContractEnsemble +`ContractEnsemble` is the centerpiece that takes care of managing contract storage and bank state and executing messages between contracts. Currently, supported messages are `CosmosMsg::Wasm` and `CosmosMsg::Bank`. It exposes methods like `register` for registering contract harnesses and `instantiate`, `execute`, `reply`, `query` for interacting with contracts and methods to inspect/alter the raw storage if needed. Just like on the blockchain, if any contract returns an error during exection, all state is reverted. + +```rust +#[test] +fn test_query_price() { + let mut ensemble = ContractEnsemble::new(); + + // register contract + let oracle = ensemble.register(Box::new(Oracle)); + + // instantiate + let oracle = ensemble.instantiate( + oracle.id, + &{}, + MockEnv::new( + "Admin", + "oracle" // This will be the contract address + ) + ).unwrap().instance; + + // query + let oracle::QueryMsg::GetPrice { price } = ensemble.query( + oracle.address, + &oracle::QueryMsg::GetPrice { base_symbol: "SCRT".into }, + ).unwrap(); + + assert_eq!(price, Uint128(1_000_000_000)); +} +``` + +### Simulating blocks +Since the ensemble is designed to simulate a blockchain environment it maintains an idea of block height and time. Block height increases automatically with each successful call to execute and instantiate messages (**sub-messages don't trigger this behaviour**). It is possible to configure as needed: blocks can be incremented by a fixed amount or by a random value within a provided range. In addition, the current block can be frozen so subsequent calls will not modify it if desired. + +Set the block height manually: + +```rust +let mut ensemble = ContractEnsemble::new(); + +ensemble.block_mut().height = 10; +ensemble.block_mut().time = 10000; +``` + +Use auto-increments (after each **successful** call) for block height and time when initializing the ensemble: + +```rust +// For exact increments +ensemble.block_mut().exact_increments(10, 7); + +// For random increments within specified ranges +ensemble.block_mut().random_increments(1..11, 1..9); +``` diff --git a/ensemble/src/bank.rs b/ensemble/src/bank.rs new file mode 100644 index 00000000000..776886d74b0 --- /dev/null +++ b/ensemble/src/bank.rs @@ -0,0 +1,141 @@ +use std::collections::HashMap; + +use fadroma::cosmwasm_std::{Uint128, Coin, coin}; +use super::{ + EnsembleResult, EnsembleError +}; + +pub type Balances = HashMap; + +#[derive(Clone, Default, Debug)] +pub(crate) struct Bank(pub HashMap); + +impl Bank { + pub fn add_funds(&mut self, address: &str, coin: Coin) { + self.assert_account_exists(address); + + let account = self.0.get_mut(address).unwrap(); + add_balance(account, coin); + } + + pub fn remove_funds( + &mut self, + address: &str, + coin: Coin + ) -> EnsembleResult<()> { + if !self.0.contains_key(address) { + return Err(EnsembleError::Bank( + format!("Account {} does not exist for remove balance", address) + )) + } + + let account = self.0.get_mut(address).unwrap(); + let balance = account.get_mut(&coin.denom); + + match balance { + Some(amount) => { + if *amount >= coin.amount { + *amount -= coin.amount; + } else { + return Err(EnsembleError::Bank(format!( + "Insufficient balance: account: {}, denom: {}, balance: {}, required: {}", + address, + coin.denom, + amount, + coin.amount + ))) + } + }, + None => { + return Err(EnsembleError::Bank(format!( + "Insufficient balance: account: {}, denom: {}, balance: {}, required: {}", + address, + coin.denom, + Uint128::zero(), + coin.amount + ))) + } + } + + Ok(()) + } + + pub fn transfer( + &mut self, + from: &str, + to: &str, + coin: Coin, + ) -> EnsembleResult<()> { + self.assert_account_exists(from); + self.assert_account_exists(to); + + let amount = self + .0 + .get_mut(from) + .unwrap() + .get_mut(&coin.denom) + .ok_or_else(|| { + EnsembleError::Bank(format!( + "Insufficient balance: sender: {}, denom: {}, balance: {}, required: {}", + from, + coin.denom, + Uint128::zero(), + coin.amount + )) + })?; + + *amount = amount.checked_sub(coin.amount).map_err(|_| { + EnsembleError::Bank(format!( + "Insufficient balance: sender: {}, denom: {}, balance: {}, required: {}", + from, coin.denom, amount, coin.amount + )) + })?; + + add_balance(self.0.get_mut(to).unwrap(), coin); + + Ok(()) + } + + pub fn query_balances(&self, address: &str, denom: Option) -> Vec { + let account = self.0.get(address); + + match account { + Some(account) => match denom { + Some(denom) => { + let amount = account.get(&denom); + + vec![coin(amount.cloned().unwrap_or_default().u128(), &denom)] + } + None => { + let mut result = Vec::new(); + + for (k, v) in account.iter() { + result.push(coin(v.u128(), k)); + } + + result + } + }, + None => match denom { + Some(denom) => vec![coin(0, &denom)], + None => vec![], + }, + } + } + + fn assert_account_exists(&mut self, address: &str) { + if !self.0.contains_key(address) { + self.0.insert(address.to_string(), Default::default()); + } + } +} + +fn add_balance(balances: &mut Balances, coin: Coin) { + let balance = balances.get_mut(&coin.denom); + + if let Some(amount) = balance { + *amount += coin.amount; + } else { + balances.insert(coin.denom, coin.amount); + } +} diff --git a/ensemble/src/block.rs b/ensemble/src/block.rs new file mode 100644 index 00000000000..8b36e7df6f9 --- /dev/null +++ b/ensemble/src/block.rs @@ -0,0 +1,149 @@ +use std::ops::Range; +use oorandom::Rand64; + +#[derive(Clone, Debug)] +pub struct Block { + pub height: u64, + pub time: u64, + incr: BlockIncrement, + is_frozen: bool +} + +#[derive(Clone, Debug)] +enum BlockIncrement { + Random { + height: Range, + time: Range + }, + Exact { + /// Block height increment + height: u64, + /// Seconds per block increment + time: u64 + } +} + +impl Block { + /// Will increase the block height by `height` and + /// block time by `height` * `time` for each increment. + /// + /// `time` is in seconds. + /// + /// This is the default strategy. + pub fn exact_increments(&mut self, height: u64, time: u64) { + assert!(height > 0 && time > 0, "Height and time must be bigger than 0. Call \"freeze\" if you want to stop incrementing blocks."); + + self.incr = BlockIncrement::Exact { height, time }; + } + + /// Will increase the block height by a number within the range of `height` and + /// block time by that same `height` * `time` for each increment. + /// + /// `time` is in seconds. + pub fn random_increments(&mut self, height: Range, time: Range) { + assert!(height.start > 0 && time.start > 0, "Height and time range start must be bigger than 0."); + + self.incr = BlockIncrement::Random { height, time }; + } + + /// Will stop incrementing blocks on each message execution + /// and calling `next` and `increment` will have no effect. + pub fn freeze(&mut self) { + self.is_frozen = true; + } + + /// Will resume incrementing blocks on each message execution. + pub fn unfreeze(&mut self) { + self.is_frozen = false; + } + + /// Increments the block height and time by the amount configured - once. + /// + /// # Examples + /// + /// ``` + /// use fadroma::ensemble::Block; + /// + /// let mut block = Block::default(); + /// block.exact_increments(1, 5); + /// + /// let old_height = block.height; + /// let old_time = block.time; + /// + /// block.next(); + /// + /// assert_eq!(block.height - old_height, 1); + /// assert_eq!(block.time - old_time, 5); + /// + /// ``` + #[inline] + pub fn next(&mut self) { + self.increment(1) + } + + ///Increments the block height and time by the amount configured, multiplied by the `times` parameter. + /// + /// # Examples + /// + /// ``` + /// use fadroma::ensemble::Block; + /// + /// let mut block = Block::default(); + /// block.exact_increments(1, 5); + /// + /// let old_height = block.height; + /// let old_time = block.time; + /// + /// block.increment(3); + /// + /// assert_eq!(block.height - old_height, 3); + /// assert_eq!(block.time - old_time, 15); + /// + /// ``` + pub fn increment(&mut self, times: u64) { + if self.is_frozen { + return; + } + + match self.incr.clone() { + BlockIncrement::Exact { height, time } => { + let height = height * times; + + self.height += height; + self.time += height * time; + }, + BlockIncrement::Random { height, time } => { + // TODO: randomize this seed + let mut rng = Rand64::new(347593485789348572u128); + + let rand_height = rng.rand_range(height); + let rand_time = rng.rand_range(time); + + let height = rand_height * times; + + self.height += height; + self.time += height * rand_time; + } + } + } +} + +impl Default for Block { + fn default() -> Self { + Self { + height: 1, + #[cfg(target_arch = "wasm32")] + time: 1600000000, + #[cfg(not(target_arch = "wasm32"))] + time: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + incr: BlockIncrement::Exact { + height: 1, + time: 10 + }, + is_frozen: false + } + } +} diff --git a/ensemble/src/ensemble.rs b/ensemble/src/ensemble.rs new file mode 100644 index 00000000000..1179fb5d438 --- /dev/null +++ b/ensemble/src/ensemble.rs @@ -0,0 +1,928 @@ +use std::{ + fmt::Debug, + convert::TryFrom +}; +use serde::{ + Serialize, + de::DeserializeOwned +}; +use fadroma::{ + prelude::{ContractCode, ContractLink}, + cosmwasm_std::{ + SubMsg, Deps, DepsMut, Env, Response, MessageInfo, Binary, Coin, Empty, + CosmosMsg, WasmMsg, BlockInfo, ContractInfo, BankMsg, Timestamp, Addr, + SubMsgResponse, SubMsgResult, Reply, Storage, Api, Querier, QuerierWrapper, + from_binary, to_binary, testing::MockApi + } +}; + +use oorandom::Rand64; + +#[cfg(feature = "ensemble-staking")] +use fadroma::cosmwasm_std::{ + Uint128, FullDelegation, Validator, Delegation, StakingMsg, DistributionMsg +}; + +use super::{ + bank::Balances, + block::Block, + env::MockEnv, + querier::EnsembleQuerier, + response::{ + ResponseVariants, ExecuteResponse, + InstantiateResponse, ReplyResponse + }, + state::State, + execution_state::{ExecutionState, MessageType}, + error::{EnsembleError, RegistryError}, + event::ProcessedEvents +}; + +#[cfg(feature = "ensemble-staking")] +use super::staking::Delegations; + +pub type AnyResult = anyhow::Result; +pub type EnsembleResult = core::result::Result; + +pub(crate) type SubMsgExecuteResult = EnsembleResult<(ResponseVariants, ProcessedEvents)>; + +/// The trait that allows the ensemble to execute your contract. Must be implemented +/// for each contract that will participate in the shared execution. Usually implemented +/// by calling the respective contract function for each method of the trait by passing +/// down the parameters of the method and calling `cosmwasm_std::from_binary()` on the +/// `msg` parameter. It can also be used to implement a mock contract directly. +pub trait ContractHarness { + fn instantiate(&self, deps: DepsMut, env: Env, info: MessageInfo, msg: Binary) -> AnyResult; + + fn execute(&self, deps: DepsMut, env: Env, info: MessageInfo, msg: Binary) -> AnyResult; + + fn query(&self, deps: Deps, env: Env, msg: Binary) -> AnyResult; + + fn reply(&self, _deps: DepsMut, _env: Env, _reply: Reply) -> AnyResult { + panic!("Reply entry point not implemented.") + } +} + +/// This the main type in the system that takes care of registering and executing contracts, +/// keeping the blockchain simulation state and allowing the manipulation of particular parameters +/// such as account funds, blocks or contract state in order to efficiently simulate testing scenarios. +/// +/// # Examples +/// +/// ``` +/// use fadroma::{ +/// cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, Response, Binary, from_binary, to_binary}, +/// storage::{load, save}, +/// ensemble::{ContractEnsemble, ContractHarness, MockEnv, EnsembleResult, AnyResult}, +/// serde::{Serialize, Deserialize}, +/// schemars::JsonSchema +/// }; +/// +/// const NUMBER_KEY: &[u8] = b"number"; +/// +/// struct Counter; +/// +/// #[derive(Serialize, Deserialize, JsonSchema)] +/// #[serde(rename_all = "snake_case")] +/// enum ExecuteMsg { +/// Increment, +/// Reset +/// } +/// +/// impl ContractHarness for Counter { +/// fn instantiate(&self, deps: DepsMut, env: Env, info: MessageInfo, _msg: Binary) -> AnyResult { +/// Ok(Response::default()) +/// } +/// +/// fn execute(&self, deps: DepsMut, env: Env, info: MessageInfo, msg: Binary) -> AnyResult { +/// match from_binary(&msg)? { +/// ExecuteMsg::Increment => { +/// let mut number: u64 = load(deps.storage, NUMBER_KEY)?.unwrap_or_default(); +/// number += 1; +/// +/// save(deps.storage, NUMBER_KEY, &number)?; +/// }, +/// ExecuteMsg::Reset => save(deps.storage, NUMBER_KEY, &0u64)? +/// }; +/// +/// Ok(Response::default()) +/// } +/// +/// fn query(&self, deps: Deps, env: Env, _msg: Binary) -> AnyResult { +/// let number: u64 = load(deps.storage, NUMBER_KEY)?.unwrap_or_default(); +/// let number = to_binary(&number)?; +/// +/// Ok(number) +/// } +/// } +/// +/// let mut ensemble = ContractEnsemble::new(); +/// let counter = ensemble.register(Box::new(Counter)); +/// let counter = ensemble.instantiate( +/// counter.id, +/// &(), +/// MockEnv::new("sender", "counter_address") +/// ) +/// .unwrap() +/// .instance; +/// +/// ensemble.execute( +/// &ExecuteMsg::Increment, +/// MockEnv::new("sender", counter.address.clone()) +/// ).unwrap(); +/// +/// let number: u64 = ensemble.query(&counter.address, &()).unwrap(); +/// assert_eq!(number, 1); +/// +/// ensemble.execute( +/// &ExecuteMsg::Reset, +/// MockEnv::new("sender", counter.address.clone()) +/// ).unwrap(); +/// +/// let number: u64 = ensemble.query(&counter.address, &()).unwrap(); +/// assert_eq!(number, 0); +/// ``` +#[derive(Debug)] +pub struct ContractEnsemble { + pub(crate) ctx: Box +} + +pub(crate) struct Context { + pub contracts: Vec, + #[cfg(feature = "ensemble-staking")] + pub delegations: Delegations, + pub state: State, + block: Block, + chain_id: String +} + +pub(crate) struct ContractUpload { + code_hash: String, + code: Box +} + +impl ContractEnsemble { + /// Creates a new instance of the ensemble that will use + /// "uscrt" as the native coin when the `scrt` feature is + /// enabled. Otherwise, will use `uatom`. + pub fn new() -> Self { + #[cfg(feature = "scrt")] + let denom = "uscrt"; + + #[cfg(not(feature = "scrt"))] + let denom = "uatom"; + + Self { + ctx: Box::new(Context::new(denom.into())) + } + } + + /// Creates a new instance of the ensemble that will use + /// the provided denomination as the native coin. + #[cfg(feature = "ensemble-staking")] + pub fn new_with_denom(native_denom: impl Into) -> Self { + Self { + ctx: Box::new(Context::new(native_denom.into())) + } + } + + /// Registers a contract with the ensemble which enables it to be + /// called by the sender or by other contracts. Corresponds to the + /// upload step of the real chain. + /// + /// Returns the code id that must be use to create an instance of it + /// and its unique code hash. + pub fn register(&mut self, code: Box) -> ContractCode { + let id = self.ctx.contracts.len() as u64; + let code_hash = format!("test_contract_{}", id); + + self.ctx.contracts.push(ContractUpload { + code_hash: code_hash.clone(), + code + }); + + ContractCode { + id, + code_hash + } + } + + /// Returns a reference to the current block state. + #[inline] + pub fn block(&self) -> &Block { + &self.ctx.block + } + + /// Returns a mutable reference to the current block state. + /// Can be used to manually advance the block time and height + /// or configure the auto advancement strategy. Auto advancement + /// occurs on successful message execution. + #[inline] + pub fn block_mut(&mut self) -> &mut Block { + &mut self.ctx.block + } + + /// Sets that chain id string i.e `env.block.chain_id`. + #[inline] + pub fn set_chain_id(&mut self, id: impl Into) { + self.ctx.chain_id = id.into(); + } + + /// Adds the given funds that will be associated with the + /// provided account's address. Can either be a contract or + /// a mock user's address. You need to use this method first + /// if you want to send a contract funds when using [`MockEnv::sent_funds`]. + #[inline] + pub fn add_funds(&mut self, address: impl AsRef, coins: Vec) { + for coin in coins { + self.ctx.state.bank.add_funds(address.as_ref(), coin); + } + } + + /// Removes the given funds from the provided account's + /// address. Can either be a contract or a mock user's address. + /// The account must already exist and have at least the given amount + /// in order for this to be a success. + #[inline] + pub fn remove_funds(&mut self, address: impl AsRef, coin: Coin) -> EnsembleResult<()> { + self.ctx.state.bank.remove_funds(address.as_ref(), coin) + } + + /// Transfers funds from one account to another. The `from` address + /// must have the sufficient amount. + #[inline] + pub fn transfer_funds( + &mut self, + from: impl AsRef, + to: impl AsRef, + coin: Coin + ) -> EnsembleResult<()> { + self.ctx.state.bank.transfer( + from.as_ref(), + to.as_ref(), + coin + ) + } + + /// Returns a reference to all the balances associated with the given + /// account. Returns [`None`] if the account doesn't exist or hasn't + /// received any funds before. + /// + /// # Examples + /// + /// ``` + /// use fadroma::{ + /// ensemble::ContractEnsemble, + /// cosmwasm_std::coin + /// }; + /// + /// let mut ensemble = ContractEnsemble::new(); + /// ensemble.add_funds("wallet", vec![coin(100, "uscrt")]); + /// + /// let balances = ensemble.balances("wallet").unwrap(); + /// assert_eq!(balances.get("uscrt").unwrap().u128(), 100); + /// + /// assert!(ensemble.balances("absent").is_none()); + /// ``` + #[inline] + pub fn balances(&self, address: impl AsRef) -> Option<&Balances> { + self.ctx.state.bank.0.get(address.as_ref()) + } + + /// Returns a mutable reference to all the balances associated with the + /// given account. Returns [`None`] if the account doesn't exist or hasn't + /// received any funds before. + /// + /// # Examples + /// + /// ``` + /// use fadroma::{ + /// ensemble::ContractEnsemble, + /// cosmwasm_std::{Uint128, coin} + /// }; + /// + /// let mut ensemble = ContractEnsemble::new(); + /// ensemble.add_funds("wallet", vec![coin(100, "uscrt")]); + /// + /// let balances = ensemble.balances_mut("wallet").unwrap(); + /// let uscrt_balance = balances.get_mut("uscrt").unwrap(); + /// *uscrt_balance -= Uint128::from(50u128); + /// + /// let balances = ensemble.balances("wallet").unwrap(); + /// assert_eq!(balances.get("uscrt").unwrap().u128(), 50); + /// + /// assert!(ensemble.balances("absent").is_none()); + #[inline] + pub fn balances_mut(&mut self, address: impl AsRef) -> Option<&mut Balances> { + self.ctx.state.bank.0.get_mut(address.as_ref()) + } + + /// Returns all active delegations associated with the given address. + #[inline] + #[cfg(feature = "ensemble-staking")] + pub fn delegations(&self, address: impl AsRef) -> Vec { + self.ctx.delegations.all_delegations(address.as_ref()) + } + + /// Creates a new delegation for the given address using the given validator. + #[inline] + #[cfg(feature = "ensemble-staking")] + pub fn delegation( + &self, + delegator: impl AsRef, + validator: impl AsRef, + ) -> Option { + self.ctx + .delegations + .delegation(delegator.as_ref(), validator.as_ref()) + } + + /// Adds the validator to the validator list. + #[inline] + #[cfg(feature = "ensemble-staking")] + pub fn add_validator(&mut self, validator: Validator) { + self.ctx.delegations.add_validator(validator); + } + + /// Distributes the given amount as rewards. + #[inline] + #[cfg(feature = "ensemble-staking")] + pub fn add_rewards(&mut self, amount: impl Into) { + self.ctx.delegations.distribute_rewards(amount.into()); + } + + /// Re-allow redelegating and deposit unbondings. + #[inline] + #[cfg(feature = "ensemble-staking")] + pub fn fast_forward_delegation_waits(&mut self) { + let unbondings = self.ctx.delegations.fast_forward_waits(); + + for unbonding in unbondings { + self.ctx.state.bank.add_funds( + unbonding.delegator.as_str(), + unbonding.amount + ); + } + } + + /// Provides read access to the storage associated with the given contract address. + /// + /// Returns `Err` if a contract with `address` wasn't found. + #[inline] + pub fn contract_storage(&self, address: impl AsRef, borrow: F) -> EnsembleResult<()> + where F: FnOnce(&dyn Storage) + { + let instance = self.ctx.state.instance(address.as_ref())?; + borrow(&instance.storage as &dyn Storage); + + Ok(()) + } + + /// Provides write access to the storage associated with the given contract address. + /// + /// Returns an `Err` if a contract with `address` wasn't found. In case an error + /// is returned from the closure, the updates to that storage are discarded. + pub fn contract_storage_mut(&mut self, address: impl AsRef, mutate: F) -> EnsembleResult<()> + where F: FnOnce(&mut dyn Storage) -> EnsembleResult<()> + { + self.ctx.state.push_scope(); + let result = self.ctx.state.borrow_storage_mut(address.as_ref(), mutate); + + if result.is_ok() { + self.ctx.state.commit(); + } else { + self.ctx.state.revert(); + } + + result + } + + /// Creates a new contract instance using the given code id. The code id + /// must be obtained by calling the [`ContractEnsemble::register`] method first. + /// + /// The contract will be assigned the address the was provided with + /// the `env.contract` parameter. + /// + /// The `instance` field of the response will contain this address and + /// the code hash associated with this instance. + pub fn instantiate( + &mut self, + code_id: u64, + msg: &T, + env: MockEnv + ) -> EnsembleResult { + let contract = self + .ctx + .contracts + .get(code_id as usize) + .ok_or_else(|| EnsembleError::registry(RegistryError::IdNotFound(code_id)))?; + + let sub_msg = SubMsg::new(WasmMsg::Instantiate { + code_id, + code_hash: contract.code_hash.clone(), + msg: to_binary(msg)?, + funds: env.sent_funds, + label: env.contract.into_string() + }); + + match self.ctx.execute_messages(sub_msg, env.sender.into_string())? { + ResponseVariants::Instantiate(resp) => Ok(resp), + _ => unreachable!() + } + } + + /// Executes the contract with the address provided in `env.contract`. + pub fn execute( + &mut self, + msg: &T, + env: MockEnv + ) -> EnsembleResult { + let address = env.contract.into_string(); + + let instance = self.ctx.state.instance(&address)?; + let code_hash = self.ctx.contracts[instance.index].code_hash.clone(); + + let sub_msg = SubMsg::new(WasmMsg::Execute { + contract_addr: address, + code_hash, + msg: to_binary(msg)?, + funds: env.sent_funds + }); + + match self.ctx.execute_messages(sub_msg, env.sender.into_string())? { + ResponseVariants::Execute(resp) => Ok(resp), + _ => unreachable!() + } + } + + /// Queries the contract associated with the given address and + /// attempts to deserialize its response to the given type parameter. + #[inline] + pub fn query( + &self, + address: impl AsRef, + msg: &T + ) -> EnsembleResult { + let result = self.query_raw(address, msg)?; + let result = from_binary(&result)?; + + Ok(result) + } + + /// Queries the contract associated with the given address without + /// attempting to deserialize its response. + #[inline] + pub fn query_raw( + &self, + address: impl AsRef, + msg: &T + ) -> EnsembleResult { + self.ctx.query(address.as_ref(), to_binary(msg)?) + } +} + +impl Context { + #[cfg(not(feature = "ensemble-staking"))] + fn new(_native_denom: String) -> Self { + Self { + contracts: vec![], + state: State::new(), + block: Block::default(), + chain_id: "fadroma-ensemble-testnet".into() + } + } + + #[cfg(feature = "ensemble-staking")] + fn new(native_denom: String) -> Self { + Self { + contracts: vec![], + state: State::new(), + delegations: Delegations::new(native_denom), + block: Block::default(), + chain_id: "fadroma-ensemble-testnet".into() + } + } + + fn instantiate( + &mut self, + id: u64, + msg: Binary, + env: MockEnv, + ) -> EnsembleResult { + // We check for validity in execute_sub_msg() + let contract = &self.contracts[id as usize]; + + let sender = env.sender.to_string(); + let address = env.contract.to_string(); + let code_hash = contract.code_hash.clone(); + + self.state.create_contract_instance(address.clone(), id as usize)?; + + let (env, msg_info) = self.create_msg_deps( + env, + code_hash.clone() + ); + + let querier = EnsembleQuerier::new(&self); + let response = self.state.borrow_storage_mut(&address, |storage| { + let deps = DepsMut:: { + storage, + api: &MockApi::default() as &dyn Api, + querier: QuerierWrapper::new(&querier as &dyn Querier) + }; + + let result = contract.code.instantiate(deps, env, msg_info, msg.clone())?; + + Ok(result) + })?; + + Ok(InstantiateResponse { + sent: Vec::with_capacity(response.messages.len()), + sender, + instance: ContractLink { + address: Addr::unchecked(address), + code_hash + }, + code_id: id, + msg, + response + }) + } + + fn execute(&mut self, msg: Binary, env: MockEnv) -> EnsembleResult { + let address = env.contract.to_string(); + let (index, code_hash) = { + let instance = self.state.instance(&address)?; + let code_hash = self.contracts[instance.index].code_hash.clone(); + + (instance.index, code_hash) + }; + + let (env, msg_info) = self.create_msg_deps(env, code_hash); + let sender = msg_info.sender.to_string(); + + let contract = &self.contracts[index]; + + let querier = EnsembleQuerier::new(&self); + let response = self.state.borrow_storage_mut(&address, |storage| { + let deps = DepsMut:: { + storage, + api: &MockApi::default() as &dyn Api, + querier: QuerierWrapper::new(&querier as &dyn Querier) + }; + + let result = contract.code.execute(deps, env, msg_info, msg.clone())?; + + Ok(result) + })?; + + Ok(ExecuteResponse { + sent: Vec::with_capacity(response.messages.len()), + sender, + address, + msg, + response + }) + } + + pub(crate) fn query(&self, address: &str, msg: Binary) -> EnsembleResult { + let instance = self.state.instance(address)?; + let contract = &self.contracts[instance.index]; + + let env = self.create_env(ContractLink { + address: Addr::unchecked(address), + code_hash: contract.code_hash.clone() + }); + + let querier = EnsembleQuerier::new(&self); + let deps = Deps:: { + storage: &instance.storage as &dyn Storage, + api: &MockApi::default() as &dyn Api, + querier: QuerierWrapper::new(&querier as &dyn Querier) + }; + + let result = contract.code.query(deps, env, msg)?; + + Ok(result) + } + + fn reply(&mut self, address: String, reply: Reply) -> EnsembleResult { + let (index, code_hash) = { + let instance = self.state.instance(&address)?; + let code_hash = self.contracts[instance.index].code_hash.clone(); + + (instance.index, code_hash) + }; + + let env = self.create_env(ContractLink { + address: Addr::unchecked(address.clone()), + code_hash + }); + + let contract = &self.contracts[index]; + + let querier = EnsembleQuerier::new(&self); + let response = self.state.borrow_storage_mut(&address, |storage| { + let deps = DepsMut:: { + storage, + api: &MockApi::default() as &dyn Api, + querier: QuerierWrapper::new(&querier as &dyn Querier) + }; + + let result = contract.code.reply(deps, env, reply.clone())?; + + Ok(result) + })?; + + Ok(ReplyResponse { + sent: Vec::with_capacity(response.messages.len()), + address, + reply, + response + }) + } + + fn execute_messages( + &mut self, + msg: SubMsg, + initial_sender: String + ) -> EnsembleResult { + let mut state = ExecutionState::new(msg, initial_sender); + + while let Some(msg_ty) = state.next() { + self.state.push_scope(); + + let result = match msg_ty { + MessageType::SubMsg { msg, sender } => { + self.execute_sub_msg(msg, sender) + } + MessageType::Reply { id, error, target } => { + let result = match error { + Some(err) => { + let reply = Reply { + id, + result: SubMsgResult::Err(err) + }; + + self.reply(target, reply) + }, + None => { + let reply = Reply { + id, + result: SubMsgResult::Ok(SubMsgResponse { + events: state.events().to_vec(), + data: state.data().cloned() + }) + }; + + self.reply(target, reply) + } + }; + + match result { + Ok(resp) => { + ProcessedEvents::try_from(&resp).and_then(|x| + Ok((resp.into(), x)) + ) + }, + Err(err) => Err(err) + } + } + }; + + match state.process_result(result) { + Ok(mut msgs_reverted) => { + while msgs_reverted > 0 { + self.state.revert_scope(); + msgs_reverted -= 1; + } + }, + Err(err) => { + self.state.revert(); + + return Err(err); + } + } + } + + self.block.next(); + self.state.commit(); + + Ok(state.finalize()) + } + + fn execute_sub_msg( + &mut self, + sub_msg: SubMsg, + sender: String, + ) -> SubMsgExecuteResult { + match sub_msg.msg { + CosmosMsg::Wasm(msg) => match msg { + WasmMsg::Execute { + contract_addr, + msg, + funds, + code_hash, + } => { + let index = self.state.instance(&contract_addr)?.index; + + if self.contracts[index].code_hash != code_hash { + return Err(EnsembleError::registry(RegistryError::InvalidCodeHash(code_hash))); + } + + let mut events = if funds.is_empty() { + ProcessedEvents::empty() + } else { + let transfer_resp = self.state.transfer_funds( + &sender, + &contract_addr, + funds.clone() + )?; + + ProcessedEvents::from(&transfer_resp) + }; + + let env = MockEnv::new( + sender, + contract_addr.clone() + ).sent_funds(funds); + + let execute_resp = self.execute(msg, env)?; + events.extend(&execute_resp)?; + + Ok((execute_resp.into(), events)) + } + WasmMsg::Instantiate { + code_id, + msg, + funds, + label, + code_hash + } => { + let contract = self + .contracts + .get(code_id as usize) + .ok_or_else(|| EnsembleError::registry(RegistryError::IdNotFound(code_id)))?; + + if contract.code_hash != code_hash { + return Err(EnsembleError::registry(RegistryError::InvalidCodeHash(code_hash))); + } + + let env = MockEnv::new_sanitized( + sender, + label + ).sent_funds(funds); + + let mut events = if env.sent_funds.is_empty() { + ProcessedEvents::empty() + } else { + let transfer_resp = self.state.transfer_funds( + env.sender(), + env.contract(), + env.sent_funds.clone() + )?; + + ProcessedEvents::from(&transfer_resp) + }; + + let instantiate_resp = self.instantiate( + code_id, + msg, + env + )?; + + events.extend(&instantiate_resp)?; + + Ok((instantiate_resp.into(), events)) + } + _ => panic!("Ensemble: Unsupported message: {:?}", msg) + } + CosmosMsg::Bank(msg) => match msg { + BankMsg::Send { + to_address, + amount, + } => { + let resp = self.state.transfer_funds( + &sender, + &to_address, + amount + )?; + + let events = ProcessedEvents::from(&resp); + + Ok((resp.into(), events)) + }, + _ => panic!("Ensemble: Unsupported message: {:?}", msg) + } + #[cfg(feature = "ensemble-staking")] + CosmosMsg::Staking(msg) => match msg { + StakingMsg::Delegate { validator, amount } => { + self.state.remove_funds(&sender, vec![amount.clone()])?; + + let resp = self.delegations.delegate( + sender.clone(), + validator, + amount + )?; + + let events = ProcessedEvents::from(&resp); + + Ok((resp.into(), events)) + } + StakingMsg::Undelegate { validator, amount } => { + let resp = self.delegations.undelegate( + sender.clone(), + validator, + amount.clone(), + )?; + + let events = ProcessedEvents::from(&resp); + + Ok((resp.into(), events)) + } + StakingMsg::Redelegate { + src_validator, + dst_validator, + amount, + } => { + let resp = self.delegations.redelegate( + sender.clone(), + src_validator, + dst_validator, + amount, + )?; + + let events = ProcessedEvents::from(&resp); + + Ok((resp.into(), events)) + }, + _ => panic!("Ensemble: Unsupported message: {:?}", msg) + }, + #[cfg(feature = "ensemble-staking")] + CosmosMsg::Distribution(msg) => match msg { + DistributionMsg::WithdrawDelegatorReward { validator } => { + // Query accumulated rewards so bank transaction can take place first + let withdraw_amount = match self.delegations.delegation(&sender, &validator) { + Some(amount) => amount.accumulated_rewards, + None => return Err(EnsembleError::Staking("Delegation not found".into())), + }; + + self.state.add_funds(sender.clone(), withdraw_amount); + + let resp = self.delegations.withdraw(sender, validator)?; + let events = ProcessedEvents::from(&resp); + + Ok((resp.into(), events)) + }, + _ => unimplemented!() + } + _ => panic!("Ensemble: Unsupported message: {:?}", sub_msg) + } + } + + #[inline] + fn create_msg_deps(&self, env: MockEnv, code_hash: String) -> (Env, MessageInfo) { + ( + self.create_env(ContractLink { + address: env.contract, + code_hash + }), + MessageInfo { + sender: env.sender, + funds: env.sent_funds + } + ) + } + + #[inline] + fn create_env(&self, contract: ContractLink) -> Env { + let seed = 94759574359011638572u128.wrapping_mul(self.block.height as u128); + + let mut rng = Rand64::new(seed); + let bytes = rng.rand_u64().to_le_bytes(); + + Env { + block: BlockInfo { + height: self.block.height, + time: Timestamp::from_seconds(self.block.time), + chain_id: self.chain_id.clone(), + random: Some(Binary::from(bytes)) + }, + transaction: None, + contract: ContractInfo { + address: contract.address, + code_hash: contract.code_hash, + } + } + } +} + +impl Debug for Context { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Context") + .field("contracts_len", &self.contracts.len()) + .field("block", &self.block) + .field("chain_id", &self.chain_id) + .finish() + } +} diff --git a/ensemble/src/env.rs b/ensemble/src/env.rs new file mode 100644 index 00000000000..aac144c9119 --- /dev/null +++ b/ensemble/src/env.rs @@ -0,0 +1,148 @@ +use serde::{Deserialize, Serialize}; +use fadroma::schemars::{self, JsonSchema}; +use fadroma::cosmwasm_std::{Addr, Coin}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct MockEnv { + pub sent_funds: Vec, + pub(crate) sender: Addr, + pub(crate) contract: Addr +} + +impl MockEnv { + /// The maximum length that the address is allowed to be. + /// We want to be consistent with how `cosmwasm_std::testing::MockApi` + /// works which is what the ensemble uses internally. + /// Otherwise if you canonize any addresses longer than that you will + /// get a cryptic error telling that your address is too long but + /// not exactly how much longer. This detail is intentionally hidden + /// in CosmWasm but there is no reason for you not to know in tests, + /// because if you ran into that error you'd just have to guess anyways... + /// so we tell you. + pub const MAX_ADDRESS_LEN: usize = 54; + + /// Constructs a new instance of [`MockEnv`]. + /// + /// # Arguments + /// + /// * `sender` - The address that executes the contract i.e `info.sender`. + /// * `contract` - The address of the contract to be executed/instantiated i.e `env.contract.address`. + /// + /// # Panics + /// + /// Panics if either the `sender` or `contract` arguments are longer than + /// [`MockEnv::MAX_ADDRESS_LEN`] bytes or have upper case letters. + /// + /// We do this in order to respect how `cosmwasm_std::testing::MockApi` works which + /// we use internally. This way we avoid any inconsistencies when you set an address that + /// has upper case letters but then it gets canonicalized and becomes all lower case. + pub fn new(sender: impl Into, contract: impl Into) -> Self { + let sender = sender.into(); + let contract = contract.into(); + + if !is_valid_address(&sender) || !is_valid_address(&contract) { + panic!( + "Addresses must be at most {} bytes long and have all lower case characters.", + MockEnv::MAX_ADDRESS_LEN + ); + } + + Self { + sender: Addr::unchecked(sender), + contract: Addr::unchecked(contract), + sent_funds: vec![] + } + } + + /// Any funds that the sender is transferring to the executed contract. + /// i.e `info.funds`. + #[inline] + pub fn sent_funds(mut self, funds: Vec) -> Self { + self.sent_funds = funds; + + self + } + + #[inline] + pub fn sender(&self) -> &str { + self.sender.as_str() + } + + #[inline] + pub fn contract(&self) -> &str { + self.contract.as_str() + } + + pub(crate) fn new_sanitized( + sender: impl Into, + contract: impl Into + ) -> Self { + let sender = sender.into(); + assert!(is_valid_address(&sender)); + + let mut contract = contract.into(); + contract.truncate(Self::MAX_ADDRESS_LEN); + + Self { + sender: Addr::unchecked(sender), + contract: Addr::unchecked(contract.to_lowercase()), + sent_funds: vec![] + } + } +} + +#[inline] +fn is_valid_address(addr: &str) -> bool { + addr.len() <= MockEnv::MAX_ADDRESS_LEN && + addr.to_lowercase() == addr +} + +#[cfg(test)] +mod tests { + use crate::cosmwasm_std::{testing::MockApi, Api}; + use super::*; + + // If this test fails it probably means that the MockApi + // logic in CosmWasm has changed. In that case, adjust + // MockEnv::MAX_ADDRESS_LEN accordingly. + #[test] + fn verify_cw_mock_api_max_len() { + let addr = "a".repeat(MockEnv::MAX_ADDRESS_LEN + 1); + + let api = MockApi::default(); + api.addr_canonicalize(&addr).unwrap_err(); + api.addr_canonicalize(&addr[0..addr.len() - 1]).unwrap(); + } + + #[test] + fn verify_cw_mock_api_converts_to_lowercase() { + let addr = "A".repeat(MockEnv::MAX_ADDRESS_LEN); + + let api = MockApi::default(); + let canon = api.addr_canonicalize(&addr).unwrap(); + assert!(canon.0.iter().all(|x| *x == 97)); + + let human = api.addr_humanize(&canon).unwrap(); + assert!(human.as_bytes().iter().all(|x| *x == 97)); + + assert!(is_valid_address(human.as_str())); + } + + #[test] + fn mock_env_respects_mock_api() { + let addr = "A".repeat(MockEnv::MAX_ADDRESS_LEN + 1); + assert!(!is_valid_address(&addr)); + + let env = MockEnv::new_sanitized("sender", addr); + assert_eq!(env.sender.as_str(), "sender"); + assert_eq!(env.contract.as_str(), "a".repeat(MockEnv::MAX_ADDRESS_LEN)); + + assert!(is_valid_address(env.sender.as_str())); + assert!(is_valid_address(env.contract.as_str())); + } + + #[test] + fn addresses_can_contain_spaces_and_special_characters() { + MockEnv::new("`~123!@#$%^&*()-=+\\/.,<>?[]{}", "this address has spaces"); + } +} diff --git a/ensemble/src/error.rs b/ensemble/src/error.rs new file mode 100644 index 00000000000..6a4cca0b140 --- /dev/null +++ b/ensemble/src/error.rs @@ -0,0 +1,89 @@ +use std::fmt::{Debug, Display}; + +use fadroma::cosmwasm_std::StdError; + +#[derive(Debug)] +pub enum EnsembleError { + ContractError(anyhow::Error), + ContractRegistry(RegistryError), + AttributeValidation(String), + Bank(String), + Staking(String), + Std(StdError) +} + +#[derive(Clone, PartialEq, Debug)] +pub enum RegistryError { + NotFound(String), + IdNotFound(u64), + DuplicateAddress(String), + InvalidCodeHash(String), +} + +#[derive(Clone, PartialEq, Debug)] +pub enum AttributeError { + EventTypeTooShort(String), + KeyReserved(String), + EmtpyKey(String), + EmtpyValue(String), +} + +impl EnsembleError { + /// Returns the error that the executed contract returned. + /// Panics if not a contract error. + #[inline] + pub fn unwrap_contract_error(self) -> anyhow::Error { + match self { + Self::ContractError(err) => err, + _ => panic!("called EnsembleError::unwrap_contract_error() on a non EnsembleError::ContractError") + } + } + + /// Returns `true` if the error occurred within the contract. + /// `false` otherwise. + #[inline] + pub fn is_contract_error(&self) -> bool { + matches!(self, EnsembleError::ContractError(_)) + } + + #[inline] + pub(crate) fn registry(err: RegistryError) -> Self { + Self::ContractRegistry(err) + } +} + +impl From for EnsembleError { + fn from(err: StdError) -> Self { + Self::Std(err) + } +} + +impl From for EnsembleError { + fn from(err: anyhow::Error) -> Self { + Self::ContractError(err) + } +} + +impl Display for EnsembleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Bank(msg) => f.write_fmt(format_args!("Ensemble error - Bank: {}", msg)), + Self::Staking(msg) => f.write_fmt(format_args!("Ensemble error - Staking: {}", msg)), + Self::ContractRegistry(err) => f.write_fmt(format_args!("Ensemble error - Contract registry: {}", err.to_string())), + Self::AttributeValidation(msg) => f.write_fmt(format_args!("Ensemble error - Event attribute validation: {}", msg)), + Self::Std(err) => Display::fmt(err, f), + Self::ContractError(err) => Display::fmt(err, f) + } + } +} + +impl Display for RegistryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotFound(address) => f.write_fmt(format_args!("Contract address {} not found", address)), + Self::DuplicateAddress(address) => f.write_fmt(format_args!("Contract instance with address {} already exists", address)), + Self::IdNotFound(id) => f.write_fmt(format_args!("Contract with id {} not found", id)), + Self::InvalidCodeHash(hash) => f.write_fmt(format_args!("Contract code hash {} is invalid", hash)), + } + } +} diff --git a/ensemble/src/event.rs b/ensemble/src/event.rs new file mode 100644 index 00000000000..5378a0b95c8 --- /dev/null +++ b/ensemble/src/event.rs @@ -0,0 +1,260 @@ +use std::convert::{TryFrom, TryInto}; + +#[cfg(feature = "ensemble-staking")] +use time::{OffsetDateTime, format_description::well_known::Rfc3339}; + +use fadroma::cosmwasm_std::{Response, Attribute, Event}; +use super::{ + EnsembleResult, EnsembleError, + response::{ + InstantiateResponse, ExecuteResponse, BankResponse, ReplyResponse + } +}; +#[cfg(feature = "ensemble-staking")] +use super::response::{StakingResponse, StakingOp, DistributionResponse, DistributionOp}; + +const CONTRACT_ATTR: &str = "contract_address"; + +pub struct ProcessedEvents(Vec); + +impl ProcessedEvents { + #[inline] + pub fn empty() -> Self { + Self(vec![]) + } + + pub fn extend>( + &mut self, + resp: T + ) -> EnsembleResult<()> { + let events = resp.try_into()?; + self.0.extend(events.0); + + Ok(()) + } + + #[inline] + pub fn take(self) -> Vec { + self.0 + } +} + +impl TryFrom<&InstantiateResponse> for ProcessedEvents { + type Error = EnsembleError; + + fn try_from(resp: &InstantiateResponse) -> Result { + validate_response(&resp.response)?; + + let address = resp.instance.address.as_str(); + let event = Event::new("instantiate") + .add_attribute(CONTRACT_ATTR, address) + .add_attribute("code_id", resp.code_id.to_string()); + + Ok(process_wasm_response( + &resp.response, + address.into(), + event + )) + } +} + +impl TryFrom<&ExecuteResponse> for ProcessedEvents { + type Error = EnsembleError; + + fn try_from(resp: &ExecuteResponse) -> Result { + validate_response(&resp.response)?; + + let address = resp.address.as_str(); + let event = Event::new("execute") + .add_attribute(CONTRACT_ATTR, address); + + Ok(process_wasm_response( + &resp.response, + address.into(), + event + )) + } +} + +impl TryFrom<&ReplyResponse> for ProcessedEvents { + type Error = EnsembleError; + + fn try_from(resp: &ReplyResponse) -> Result { + validate_response(&resp.response)?; + + let address = resp.address.as_str(); + let event = Event::new("reply") + .add_attribute(CONTRACT_ATTR, address); + + Ok(process_wasm_response( + &resp.response, + address.into(), + event + )) + } +} + +impl From<&BankResponse> for ProcessedEvents { + fn from(resp: &BankResponse) -> Self { + let coins: String = resp.coins.iter() + .map(|x| format!("{}{}", x.amount, x.denom)) + .collect::>() + .join(","); + + Self(vec![ + Event::new("coin_spent") + .add_attribute("amount", coins.clone()) + .add_attribute("spender", &resp.sender), + + Event::new("coin_received") + .add_attribute("amount", coins.clone()) + .add_attribute("receiver", &resp.receiver), + + Event::new("transfer") + .add_attribute("amount", coins) + .add_attribute("recipient", &resp.receiver) + .add_attribute("sender", &resp.sender) + ]) + } +} + +#[cfg(feature = "ensemble-staking")] +impl From<&StakingResponse> for ProcessedEvents { + fn from(resp: &StakingResponse) -> Self { + let amount_value = format!("{}{}", resp.amount.amount, resp.amount.denom); + + let event = match &resp.kind { + StakingOp::Delegate { validator } => + Event::new("delegate") + .add_attribute("validator", validator) + .add_attribute("amount", amount_value) + .add_attribute("new_shares", resp.amount.amount.to_string()), + StakingOp::Undelegate { validator } => { + let date = OffsetDateTime::now_utc().format(&Rfc3339).unwrap(); + + Event::new("unbond") + .add_attribute("validator", validator) + .add_attribute("amount", amount_value) + .add_attribute("completion_time", date) + } + StakingOp::Redelegate { src_validator, dst_validator } => + Event::new("redelegate") + .add_attribute("source_validator", src_validator) + .add_attribute("destination_validator", dst_validator) + .add_attribute("amount", amount_value) + }; + + Self(vec![event]) + } +} + +#[cfg(feature = "ensemble-staking")] +impl From<&DistributionResponse> for ProcessedEvents { + fn from(resp: &DistributionResponse) -> Self { + let event = match &resp.kind { + DistributionOp::WithdrawDelegatorReward { reward, validator } => + Event::new("withdraw_delegator_reward") + .add_attribute("validator", validator) + .add_attribute("sender", &resp.sender) + .add_attribute( + "amount", + format!("{}{}", reward.amount, reward.denom), + ), + _ => todo!() + }; + + Self(vec![event]) + } +} + +fn process_wasm_response( + response: &Response, + address: String, + event: Event +) -> ProcessedEvents { + let attributes = if response.attributes.is_empty() { + 0 + } else { + 1 + }; + + let mut events = Vec::with_capacity(response.events.len() + attributes + 1); + events.push(event); + + // Response::add_atrribute/s creates a new event in the array where + // the type is "wasm" and inserts them under it. And attribute with key + // "contract_address" is also inserted. + if !response.attributes.is_empty() { + // Attributes are inserted in LIFO order i.e in reverse. + events.push(Event::new("wasm") + .add_attributes(response.attributes.clone().into_iter().rev()) + .add_attribute(CONTRACT_ATTR, &address) + ); + } + + // Response::add_event/s creates a new event in the array where + // the type is prefixed with "wasm-". And attribute with key + // "contract_address" is also inserted. + let wasm_events = response.events.clone().into_iter().map(|mut x| { + x.ty = format!("wasm-{}", x.ty); + x.attributes.insert( + 0, + Attribute { + key: CONTRACT_ATTR.into(), + value: address.clone(), + encrypted: true + } + ); + + x + }); + + events.extend(wasm_events); + + ProcessedEvents(events) +} + +// Taken from https://github.com/CosmWasm/cw-multi-test/blob/03026ccd626f57869c57c9192a03da6625e4791d/src/wasm.rs#L231-L268 +fn validate_response(response: &Response) -> EnsembleResult<()> { + validate_attributes(&response.attributes)?; + + for event in &response.events { + validate_attributes(&event.attributes)?; + let ty = event.ty.trim(); + + if ty.len() < 2 { + return Err(EnsembleError::AttributeValidation( + format!("Attribute type cannot be less than 2 characters: {}", ty) + )); + } + } + + Ok(()) +} + +fn validate_attributes(attributes: &[Attribute]) -> EnsembleResult<()> { + for attr in attributes { + let key = attr.key.trim(); + let val = attr.value.trim(); + + if key.is_empty() { + return Err(EnsembleError::AttributeValidation( + format!("Attribute key for value {} cannot be empty", val) + )); + } + + if val.is_empty() { + return Err(EnsembleError::AttributeValidation( + format!("Attribute value with key {} cannot be empty", key) + )); + } + + if key.starts_with('_') { + return Err(EnsembleError::AttributeValidation( + format!("Attribute key {} cannot start with \"_\"", key) + )); + } + } + + Ok(()) +} diff --git a/ensemble/src/execution_state.rs b/ensemble/src/execution_state.rs new file mode 100644 index 00000000000..c432426d87a --- /dev/null +++ b/ensemble/src/execution_state.rs @@ -0,0 +1,363 @@ +use fadroma::{ + cosmwasm_std::{SubMsg, ReplyOn, Event, Binary}, +}; +use crate::{ + ResponseVariants, EnsembleResult, SubMsgExecuteResult +}; + +pub struct ExecutionState { + states: Vec, + next: Option +} + +pub enum MessageType { + SubMsg { + msg: SubMsg, + sender: String + }, + Reply { + id: u64, + error: Option, + target: String + } +} + +struct ExecutionLevel { + data: Option, + responses: Vec, + msgs: Vec, + msg_index: usize +} + +struct SubMsgNode { + msg: SubMsg, + state: SubMsgState, + events: Vec +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum SubMsgState { + NotExecuted, + ShouldReply, + Replying, + Done +} + +impl ExecutionState { + #[inline] + pub fn new(initial: SubMsg, sender: String) -> Self { + assert_eq!(initial.reply_on, ReplyOn::Never); + + let mut level = ExecutionLevel::new(vec![initial.clone()]); + level.current_mut().state = SubMsgState::Done; + + Self { + states: vec![level], + next: Some(MessageType::SubMsg { + msg: initial, + sender + }) + } + } + + pub fn process_result( + &mut self, + result: SubMsgExecuteResult + ) -> EnsembleResult { + match result { + Ok((response, events)) => { + + if let Some(cw_resp) = response.response() { + // Replies will overwrite the caller data if they return Some. + if response.is_reply() && cw_resp.data.is_some() { + let index = self.states.len() - 2; + self.states[index].data = cw_resp.data.clone(); + } else { + self.current_level_mut().data = cw_resp.data.clone(); + } + } + + let level = self.current_level_mut(); + level.current_mut().events.extend(events.take()); + + let messages = response.messages().to_vec(); + level.responses.push(response); + + if messages.len() > 0 { + self.states.push(ExecutionLevel::new(messages)); + } + + self.find_next( + None, + |reply_on| matches!(reply_on, ReplyOn::Always | ReplyOn::Success) + ); + + Ok(0) + }, + Err(err) if err.is_contract_error() => { + let revert_count = self.find_next( + Some(err.to_string()), + |reply_on| matches!(reply_on, ReplyOn::Always | ReplyOn::Error) + ); + + if self.next.is_none() { + // If a contract returned an error but no caller + // could "catch" it, the entire TX should be reverted. + Err(err) + } else { + // +1 because we have to revert the current scope as well + Ok(revert_count + 1) + } + }, + Err(err) => Err(err) + } + } + + #[inline] + pub fn next(&mut self) -> Option { + self.next.take() + } + + #[inline] + pub fn events(&self) -> &[Event] { + &self.current_level().current().events + } + + #[inline] + pub fn data(&mut self) -> Option<&Binary> { + self.current_level_mut().data.as_ref() + } + + pub fn finalize(mut self) -> ResponseVariants { + assert!(self.states.len() == 1 && self.next.is_none()); + assert_eq!(self.states[0].responses.len(), 1); + + self.states[0].responses.pop().unwrap() + } + + fn current_sender(&self) -> String { + let index = self.states.len() - 2; + + contract_address( + self.states[index].responses.last().unwrap() + ).to_string() + } + + fn find_next(&mut self, error: Option, test: F) -> usize + where F: Fn(&ReplyOn) -> bool + { + assert!(self.next.is_none()); + + let start_index = self.states.len() - 1; + let mut responses_thrown = 0; + + loop { + if self.states.is_empty() { + break; + } + + let index = self.states.len() - 1; + + match self.states[index].current().state { + SubMsgState::NotExecuted => { + let state = &mut self.states[index]; + let current = state.current_mut(); + + current.state = if current.msg.reply_on == ReplyOn::Never { + SubMsgState::Done + } else { + SubMsgState::ShouldReply + }; + + self.next = Some(MessageType::SubMsg { + msg: current.msg.clone(), + sender: self.current_sender() + }); + + break; + }, + SubMsgState::Done => { + if error.is_some() { + responses_thrown += self.pop().responses.len(); + } else { + let state = &mut self.states[index]; + state.next(); + + // If we don't have a next node and we are currently + // at the root then we are finished. + if !state.has_next() && !self.squash_latest() { + break; + } + } + } + SubMsgState::ShouldReply => { + let reply = self.find_reply(error.clone(), &test); + + if error.is_some() { + if reply.is_some() { + // We only do this if we have already recursed up + // (i.e this is not the first iteration of the loop) otherwise, + // the response wasn't added to begin with since we have an error. + if index != start_index { + let state = &mut self.states[index]; + state.responses.pop(); + + responses_thrown += 1; + } + } else { + responses_thrown += self.pop().responses.len(); + + continue; + } + } + + self.next = reply; + self.states[index].current_mut().state = SubMsgState::Replying; + + break; + } + SubMsgState::Replying => { + if error.is_some() { + responses_thrown += self.pop().responses.len(); + } else { + self.states[index].current_mut().state = SubMsgState::Done; + } + } + } + } + + responses_thrown + } + + fn find_reply( + &self, + error: Option, + test: &F + ) -> Option + where F: Fn(&ReplyOn) -> bool + { + if self.states.len() < 2 { + return None; + } + + let current = self.current_level().current(); + + if test(¤t.msg.reply_on) { + let index = self.states.len() - 2; + let target = contract_address( + self.states[index].responses.last().unwrap() + ).to_string(); + + let reply = MessageType::Reply { + id: current.msg.id, + error, + target + }; + + Some(reply) + } else { + None + } + } + + fn squash_latest(&mut self) -> bool { + if self.states.len() <= 1 { + return false; + } + + let latest = self.pop(); + let level = self.current_level_mut(); + + level.responses.last_mut().unwrap() + .add_responses(latest.responses); + + let len = latest.msgs.iter().map(|x| x.events.len()).sum(); + let mut events = Vec::with_capacity(len); + + for x in latest.msgs { + events.extend(x.events); + } + + level.current_mut().events.extend(events); + + true + } + + #[inline] + fn current_level_mut(&mut self) -> &mut ExecutionLevel { + self.states.last_mut().unwrap() + } + + #[inline] + fn current_level(&self) -> &ExecutionLevel { + self.states.last().unwrap() + } + + #[inline] + fn pop(&mut self) -> ExecutionLevel { + self.states.pop().unwrap() + } +} + +impl ExecutionLevel { + fn new(msgs: Vec) -> Self { + assert!(!msgs.is_empty()); + + Self { + data: None, + responses: Vec::with_capacity(msgs.len()), + msg_index: 0, + msgs: msgs.into_iter().map(|x| SubMsgNode::new(x)).collect() + } + } + + #[inline] + fn current(&self) -> &SubMsgNode { + &self.msgs[self.msg_index] + } + + #[inline] + fn current_mut(&mut self) -> &mut SubMsgNode { + &mut self.msgs[self.msg_index] + } + + #[inline] + fn next(&mut self) { + assert_eq!(self.current().state, SubMsgState::Done); + self.msg_index += 1; + } + + #[inline] + fn has_next(&self) -> bool { + if self.msg_index < self.msgs.len() { + true + } else { + false + } + } +} + +impl SubMsgNode { + #[inline] + fn new(msg: SubMsg) -> Self { + Self { + msg, + state: SubMsgState::NotExecuted, + events: vec![] + } + } +} + +#[inline] +fn contract_address(resp: &ResponseVariants) -> &str { + match resp { + ResponseVariants::Instantiate(resp) => resp.instance.address.as_str(), + ResponseVariants::Execute(resp) => &resp.address, + ResponseVariants::Reply(resp) => &resp.address, + ResponseVariants::Bank(_) => unreachable!(), + #[cfg(feature = "ensemble-staking")] + ResponseVariants::Staking(_) => unreachable!(), + #[cfg(feature = "ensemble-staking")] + ResponseVariants::Distribution(_) => unreachable!() + } +} diff --git a/ensemble/src/lib.rs b/ensemble/src/lib.rs new file mode 100644 index 00000000000..32727eb3377 --- /dev/null +++ b/ensemble/src/lib.rs @@ -0,0 +1,192 @@ +//! Test multiple contract interactions using unit tests. +//! *Feature flag: `ensemble`* + +mod bank; +mod ensemble; +mod env; +mod querier; +mod storage; +mod block; +mod response; +#[cfg(feature = "ensemble-staking")] +mod staking; +mod state; +mod execution_state; +mod error; +mod event; + +#[cfg(test)] +mod tests; + +pub use ensemble::*; +pub use env::*; +pub use querier::*; +pub use block::Block; +pub use response::*; +pub use error::*; +pub use anyhow; + +/// Generate a struct and implement [`ContractHarness`] for the given struct identifier, +/// using the provided entry point functions. +/// +/// Supports `init`, `execute` and `query` **or** +/// `init`, `execute`, `query` and `reply`. +/// +/// # Examples +/// +/// ``` +/// # #[macro_use] extern crate fadroma; +/// # use fadroma::cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, StdResult, Response, Binary, to_binary}; +/// # #[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +/// # pub struct InitMsg; +/// # #[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +/// # pub struct ExecuteMsg; +/// # #[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +/// # pub struct QueryMsg; +/// pub fn instantiate( +/// _deps: DepsMut, +/// _env: Env, +/// _info: MessageInfo, +/// _msg: InitMsg +/// ) -> StdResult { +/// Ok(Response::default()) +/// } +/// +/// pub fn execute( +/// _deps: DepsMut, +/// _env: Env, +/// _info: MessageInfo, +/// _msg: ExecuteMsg +/// ) -> StdResult { +/// Ok(Response::default()) +/// } +/// +/// pub fn query( +/// _deps: Deps, +/// _env: Env, +/// _msg: QueryMsg +/// ) -> StdResult { +/// to_binary(&true) +/// } +/// +/// contract_harness! { +/// pub NameOfStruct, +/// init: instantiate, +/// execute: execute, +/// query: query +/// } +/// ``` +#[macro_export] +macro_rules! contract_harness { + (@init $init:path) => { + fn instantiate( + &self, + deps: $crate::cosmwasm_std::DepsMut, + env: $crate::cosmwasm_std::Env, + info: $crate::cosmwasm_std::MessageInfo, + msg: $crate::cosmwasm_std::Binary + ) -> $crate::ensemble::AnyResult<$crate::cosmwasm_std::Response> { + let result = $init( + deps, + env, + info, + $crate::cosmwasm_std::from_binary(&msg)? + )?; + + Ok(result) + } + }; + + (@execute $execute:path) => { + fn execute( + &self, + deps: $crate::cosmwasm_std::DepsMut, + env: $crate::cosmwasm_std::Env, + info: $crate::cosmwasm_std::MessageInfo, + msg: $crate::cosmwasm_std::Binary + ) -> $crate::ensemble::AnyResult<$crate::cosmwasm_std::Response> { + let result = $execute( + deps, + env, + info, + $crate::cosmwasm_std::from_binary(&msg)? + )?; + + Ok(result) + } + }; + + (@query $query:path) => { + fn query( + &self, + deps: $crate::cosmwasm_std::Deps, + env: $crate::cosmwasm_std::Env, + msg: $crate::cosmwasm_std::Binary + ) -> $crate::ensemble::AnyResult<$crate::cosmwasm_std::Binary> { + let result = $query( + deps, + env, + $crate::cosmwasm_std::from_binary(&msg)? + )?; + + Ok(result) + } + }; + + (@reply $reply:path) => { + fn reply( + &self, + deps: $crate::cosmwasm_std::DepsMut, + env: $crate::cosmwasm_std::Env, + reply: $crate::cosmwasm_std::Reply + ) -> $crate::ensemble::AnyResult<$crate::cosmwasm_std::Response> { + let result = $reply( + deps, + env, + reply + )?; + + Ok(result) + } + }; + + (@trait_impl $visibility:vis $name:ident, $($contents:tt)*) => { + $visibility struct $name; + + impl $crate::ensemble::ContractHarness for $name { + $($contents)* + } + }; + + ( + $visibility:vis $name:ident, + init: $init:path, + execute: $execute:path, + query: $query:path, + reply: $reply:path + ) => { + $crate::contract_harness! { + @trait_impl + $visibility $name, + $crate::contract_harness!(@init $init); + $crate::contract_harness!(@execute $execute); + $crate::contract_harness!(@query $query); + $crate::contract_harness!(@reply $reply); + } + }; + + ( + $visibility:vis $name:ident, + init: $init:path, + execute: $execute:path, + query: $query:path + ) => { + $crate::contract_harness! { + @trait_impl + $visibility $name, + $crate::contract_harness!(@init $init); + $crate::contract_harness!(@execute $execute); + $crate::contract_harness!(@query $query); + } + }; +} diff --git a/ensemble/src/querier.rs b/ensemble/src/querier.rs new file mode 100644 index 00000000000..421e099a4e0 --- /dev/null +++ b/ensemble/src/querier.rs @@ -0,0 +1,142 @@ +use super::ensemble::Context; +use fadroma::cosmwasm_std::{ + Querier, QueryRequest, WasmQuery, BankQuery, QuerierResult, SystemResult, + SystemError, ContractResult, Empty, AllBalanceResponse, BalanceResponse, + from_slice, to_binary, testing::MockQuerier +}; +#[cfg(feature = "ensemble-staking")] +use crate::cosmwasm_std::{ + ValidatorResponse, AllValidatorsResponse, AllDelegationsResponse, + BondedDenomResponse, StakingQuery +}; + +pub struct EnsembleQuerier { + ctx: *const Context, + base: MockQuerier +} + +impl EnsembleQuerier { + pub(crate) fn new(ctx: &Context) -> Self { + Self { + ctx, + base: MockQuerier::new(&[]) + } + } +} + +macro_rules! querier_result { + ($x:expr) => { + { + let result = match $x { + Ok(bin) => ContractResult::Ok(bin), + Err(err) => ContractResult::Err(err.to_string()) + }; + + SystemResult::Ok(result) + } + }; +} + +impl Querier for EnsembleQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }) + } + }; + + let ctx = unsafe { &*(self.ctx) }; + + match request { + QueryRequest::Wasm(query) => match query { + WasmQuery::Smart { + contract_addr, msg, .. + } => { + if ctx.state.instance(&contract_addr).is_err() { + return SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr + }); + } + + querier_result!(ctx.query(&contract_addr, msg)) + } + WasmQuery::Raw { contract_addr, .. } => { + if cfg!(feature = "scrt") { + panic!("Raw queries are unsupported in Secret Network - keys and values in raw storage are encrypted and must be queried through a smart query."); + } else { + if ctx.state.instance(&contract_addr).is_err() { + return SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr + }); + } + + todo!() + } + } + _ => unimplemented!(), + }, + QueryRequest::Bank(query) => match query { + BankQuery::AllBalances { address } => { + let amount = ctx.state.bank.query_balances(&address, None); + + querier_result!(to_binary(&AllBalanceResponse { amount })) + } + BankQuery::Balance { address, denom } => { + let amount = ctx.state.bank.query_balances(&address, Some(denom)); + + querier_result!(to_binary(&BalanceResponse { + amount: amount.into_iter().next().unwrap() + })) + } + _ => unimplemented!(), + }, + #[cfg(feature = "ensemble-staking")] + QueryRequest::Staking(query) => match query { + StakingQuery::AllDelegations { delegator } => { + let delegations = ctx.delegations.all_delegations(&delegator); + + querier_result!(to_binary(&AllDelegationsResponse { delegations })) + } + StakingQuery::BondedDenom {} => { + let denom = ctx.delegations.bonded_denom(); + + querier_result!(to_binary(&BondedDenomResponse { + denom: denom.to_string(), + })) + } + StakingQuery::Delegation { + delegator, + validator + } => { + let delegation = ctx.delegations.delegation(&delegator, &validator); + + querier_result!(to_binary(&delegation)) + } + StakingQuery::AllValidators {} => { + let validators = ctx.delegations.validators(); + + querier_result!(to_binary(&AllValidatorsResponse { + validators: validators.to_vec(), + })) + } + StakingQuery::Validator { address } => { + let validator = ctx + .delegations + .validators() + .iter() + .filter(|validator| validator.address == address) + .next() + .cloned(); + + querier_result!(to_binary(&ValidatorResponse { validator })) + } + _ => unimplemented!(), + }, + _ => self.base.handle_query(&request) + } + } +} diff --git a/ensemble/src/response.rs b/ensemble/src/response.rs new file mode 100644 index 00000000000..233bf53b3b5 --- /dev/null +++ b/ensemble/src/response.rs @@ -0,0 +1,561 @@ +use std::iter::Iterator; + +use fadroma::{ + prelude::ContractLink, + cosmwasm_std::{Addr, Binary, Response, Coin, Reply, SubMsg} +}; + +#[derive(Clone, PartialEq, Debug)] +#[non_exhaustive] +pub enum ResponseVariants { + Instantiate(InstantiateResponse), + Execute(ExecuteResponse), + Reply(ReplyResponse), + Bank(BankResponse), + #[cfg(feature = "ensemble-staking")] + Staking(StakingResponse), + #[cfg(feature = "ensemble-staking")] + Distribution(DistributionResponse) +} + +#[derive(Clone, PartialEq, Debug)] +pub struct InstantiateResponse { + /// The address that triggered the instantiation. + pub sender: String, + /// The address and code hash of the new instance. + pub instance: ContractLink, + /// Code ID of the instantiated contract. + pub code_id: u64, + /// The init message that was sent. + pub msg: Binary, + /// The init response returned by the contract. + pub response: Response, + /// The responses for any messages that the instantiated contract initiated. + pub sent: Vec +} + +#[derive(Clone, PartialEq, Debug)] +pub struct ExecuteResponse { + /// The address that triggered the execute. + pub sender: String, + /// The contract that was called. + pub address: String, + /// The execute message that was sent. + pub msg: Binary, + /// The execute response returned by the contract. + pub response: Response, + /// The responses for any messages that the executed contract initiated. + pub sent: Vec +} + +#[derive(Clone, PartialEq, Debug)] +pub struct ReplyResponse { + /// The contract that was called. + pub address: String, + /// The execute message that was sent. + pub reply: Reply, + /// The execute response returned by the contract. + pub response: Response, + /// The responses for any messages that the executed contract initiated. + pub sent: Vec +} + +#[derive(Clone, PartialEq, Debug)] +pub struct BankResponse { + /// The address that sent the funds. + pub sender: String, + /// The address that the funds were sent to. + pub receiver: String, + /// The funds that were sent. + pub coins: Vec +} + +#[cfg(feature = "ensemble-staking")] +#[derive(Clone, PartialEq, Debug)] +pub struct StakingResponse { + /// The address that delegated the funds. + pub sender: String, + /// The funds that were sent. + pub amount: Coin, + /// The kind of staking operation that was performed. + pub kind: StakingOp +} + +#[cfg(feature = "ensemble-staking")] +#[derive(Clone, PartialEq, Debug)] +#[non_exhaustive] +pub enum StakingOp { + Delegate { + /// The address of the validator where the funds were sent. + validator: String + }, + Undelegate { + /// The address of the validator where the funds were sent. + validator: String + }, + Redelegate { + /// The address of the validator that the funds were redelegated from. + src_validator: String, + /// The address of the validator that the funds were redelegated to. + dst_validator: String + } +} + +#[cfg(feature = "ensemble-staking")] +#[derive(Clone, PartialEq, Debug)] +pub struct DistributionResponse { + /// The address that delegated the funds. + pub sender: String, + /// The kind of staking operation that was performed. + pub kind: DistributionOp +} + +#[cfg(feature = "ensemble-staking")] +#[derive(Clone, PartialEq, Debug)] +#[non_exhaustive] +pub enum DistributionOp { + WithdrawDelegatorReward { + /// The funds that were sent. + reward: Coin, + /// The address of the validator that the rewards where withdrawn from. + validator: String + }, + SetWithdrawAddress { + /// The that rewards will be withdrawn to. + address: String + } +} + +/// Iterator that iterates over all responses returned by +/// the various modules in **order of execution**. +pub struct Iter<'a> { + responses: &'a [ResponseVariants], + index: usize, + stack: Vec<&'a ResponseVariants> +} + +impl InstantiateResponse { + /// Returns an iterator that iterates over this instance's child responses. + /// Iteration follows the message execution order. + pub fn iter(&self) -> Iter<'_> { + Iter::new(&self.sent) + } +} + +impl ExecuteResponse { + /// Returns an iterator that iterates over this instance's child responses. + /// Iteration follows the message execution order. + pub fn iter(&self) -> Iter<'_> { + Iter::new(&self.sent) + } +} + +impl ResponseVariants { + #[inline] + pub fn is_instantiate(&self) -> bool { + matches!(&self, Self::Instantiate(_)) + } + + #[inline] + pub fn is_execute(&self) -> bool { + matches!(&self, Self::Execute(_)) + } + + #[inline] + pub fn is_reply(&self) -> bool { + matches!(&self, Self::Reply(_)) + } + + #[inline] + pub fn is_bank(&self) -> bool { + matches!(&self, Self::Bank(_)) + } + + #[inline] + #[cfg(feature = "ensemble-staking")] + pub fn is_staking(&self) -> bool { + matches!(&self, Self::Staking(_)) + } + + #[inline] + #[cfg(feature = "ensemble-staking")] + pub fn is_distribution(&self) -> bool { + matches!(&self, Self::Distribution(_)) + } + + /// Returns the messages that were created by this response. + /// Only instantiate, execute and reply can return a non-empty slice. + #[inline] + pub fn messages(&self) -> &[SubMsg] { + match self { + Self::Instantiate(resp) => &resp.response.messages, + Self::Execute(resp) => &resp.response.messages, + Self::Reply(resp) => &resp.response.messages, + Self::Bank(_) => &[], + #[cfg(feature = "ensemble-staking")] + Self::Staking(_) => &[], + #[cfg(feature = "ensemble-staking")] + Self::Distribution(_) => &[] + } + } + + #[inline] + pub(crate) fn add_responses(&mut self, responses: Vec) { + match self { + Self::Instantiate(resp) => resp.sent.extend(responses), + Self::Execute(resp) => resp.sent.extend(responses), + Self::Reply(resp) => resp.sent.extend(responses), + Self::Bank(_) => panic!("Trying to add a child response to a BankResponse."), + #[cfg(feature = "ensemble-staking")] + Self::Staking(_) => panic!("Trying to add a child response to a StakingResponse."), + #[cfg(feature = "ensemble-staking")] + Self::Distribution(_) => panic!("Trying to add a child response to a DistributionResponse."), + } + } + + pub(crate) fn response(&self) -> Option<&Response> { + match self { + Self::Instantiate(resp) => Some(&resp.response), + Self::Execute(resp) => Some(&resp.response), + Self::Reply(resp) => Some(&resp.response), + _ => None + } + } +} + +impl From for ResponseVariants { + #[inline] + fn from(value: InstantiateResponse) -> Self { + Self::Instantiate(value) + } +} + +impl From for ResponseVariants { + #[inline] + fn from(value: ExecuteResponse) -> Self { + Self::Execute(value) + } +} + +impl From for ResponseVariants { + #[inline] + fn from(value: ReplyResponse) -> Self { + Self::Reply(value) + } +} + +impl From for ResponseVariants { + #[inline] + fn from(value: BankResponse) -> Self { + Self::Bank(value) + } +} + +#[cfg(feature = "ensemble-staking")] +impl From for ResponseVariants { + #[inline] + fn from(value: StakingResponse) -> Self { + Self::Staking(value) + } +} + +#[cfg(feature = "ensemble-staking")] +impl From for ResponseVariants { + #[inline] + fn from(value: DistributionResponse) -> Self { + Self::Distribution(value) + } +} + +impl<'a> Iter<'a> { + /// Yields all responses that were initiated by the given `sender`. Reply responses are not included. + pub fn by_sender(self, sender: impl Into) -> impl Iterator { + let sender = sender.into(); + + self.filter(move |x| match x { + ResponseVariants::Instantiate(resp) => resp.sender == sender, + ResponseVariants::Execute(resp) => resp.sender == sender, + ResponseVariants::Reply(_) => false, + ResponseVariants::Bank(resp) => resp.sender == sender, + #[cfg(feature = "ensemble-staking")] + ResponseVariants::Staking(resp) => resp.sender == sender, + #[cfg(feature = "ensemble-staking")] + ResponseVariants::Distribution(resp) => resp.sender == sender, + }) + } + + fn new(responses: &'a [ResponseVariants]) -> Self { + Self { + responses, + index: 0, + stack: vec![] + } + } + + fn enqueue_children(&mut self, node: &'a ResponseVariants) { + match node { + ResponseVariants::Execute(resp) => + self.stack.extend(resp.sent.iter().rev()), + ResponseVariants::Reply(resp) => + self.stack.extend(resp.sent.iter().rev()), + ResponseVariants::Instantiate(resp) => + self.stack.extend(resp.sent.iter().rev()), + ResponseVariants::Bank(_) => { }, + #[cfg(feature = "ensemble-staking")] + ResponseVariants::Staking(_) => { }, + #[cfg(feature = "ensemble-staking")] + ResponseVariants::Distribution(_) => { } + } + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = &'a ResponseVariants; + + fn next(&mut self) -> Option { + if self.stack.is_empty() { + if self.index < self.responses.len() { + let node = &self.responses[self.index]; + + self.enqueue_children(node); + self.index += 1; + + return Some(node); + } + + return None; + } + + if let Some(node) = self.stack.pop() { + self.enqueue_children(node); + + Some(node) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::cell::RefCell; + use crate::cosmwasm_std::Uint128; + + thread_local!(static MSG_INDEX: RefCell = RefCell::new(0)); + + #[test] + fn iterate() { + let resp = mock_response(); + let mut iter = resp.iter(); + + assert_eq!(resp.sent.len(), 3); + assert_eq!(iter.next().unwrap(), &resp.sent[0]); + + if let ResponseVariants::Instantiate(inst) = &resp.sent[0] { + assert_eq!(inst.sent.len(), 3); + assert_eq!(iter.next().unwrap(), &inst.sent[0]); + + if let ResponseVariants::Instantiate(inst_2) = &inst.sent[0] { + assert_eq!(inst_2.sent.len(), 2); + assert_eq!(iter.next().unwrap(), &inst_2.sent[0]); + assert_eq!(iter.next().unwrap(), &inst_2.sent[1]); + + if let ResponseVariants::Execute(exec) = &inst_2.sent[0] { + assert_eq!(exec.sender, "C"); + assert_eq!(exec.address, "D"); + assert_eq!(exec.sent.len(), 0); + } else { + panic!() + } + + if let ResponseVariants::Execute(exec) = &inst_2.sent[1] { + assert_eq!(exec.sender, "C"); + assert_eq!(exec.address, "B"); + assert_eq!(exec.sent.len(), 0); + } else { + panic!() + } + } else { + panic!() + } + + assert_eq!(iter.next().unwrap(), &inst.sent[1]); + if let ResponseVariants::Execute(exec) = &inst.sent[1] { + assert_eq!(exec.sender, "B"); + assert_eq!(exec.address, "D"); + assert_eq!(exec.sent.len(), 0); + } else { + panic!() + } + + assert_eq!(iter.next().unwrap(), &inst.sent[2]); + if let ResponseVariants::Execute(exec) = &inst.sent[2] { + assert_eq!(exec.sender, "B"); + assert_eq!(exec.address, "A"); + assert_eq!(exec.sent.len(), 0); + } else { + panic!() + } + } else { + panic!() + } + + assert_eq!(iter.next().unwrap(), &resp.sent[1]); + if let ResponseVariants::Execute(exec) = &resp.sent[1] { + assert_eq!(exec.sent.len(), 1); + assert_eq!(iter.next().unwrap(), &exec.sent[0]); + + if let ResponseVariants::Bank(bank) = &exec.sent[0] { + assert_eq!(bank.sender, "C"); + assert_eq!(bank.receiver, "B"); + } else { + panic!() + } + } else { + panic!() + } + + assert_eq!(iter.next().unwrap(), &resp.sent[2]); + if let ResponseVariants::Bank(bank) = &resp.sent[2] { + assert_eq!(bank.sender, "A"); + assert_eq!(bank.receiver, "D"); + } else { + panic!() + } + + assert_eq!(iter.next(), None); + } + + #[test] + fn filter_by_sender() { + let resp = mock_response(); + let mut iter = resp.iter().by_sender("B"); + + if let ResponseVariants::Instantiate(inst) = &resp.sent[0] { + assert_eq!(iter.next().unwrap(), &inst.sent[0]); + assert_eq!(iter.next().unwrap(), &inst.sent[1]); + assert_eq!(iter.next().unwrap(), &inst.sent[2]); + assert_eq!(iter.next(), None); + } else { + panic!() + } + + let mut iter = resp.iter().by_sender("C"); + + if let ResponseVariants::Execute(exec) = iter.next().unwrap() { + assert_eq!(exec.sender, "C"); + assert_eq!(exec.address, "D"); + assert_eq!(exec.msg, Binary::from(b"message_3")); + assert_eq!(exec.sent.len(), 0); + } else { + panic!() + } + + if let ResponseVariants::Execute(exec) = iter.next().unwrap() { + assert_eq!(exec.sender, "C"); + assert_eq!(exec.address, "B"); + assert_eq!(exec.msg, Binary::from(b"message_4")); + assert_eq!(exec.sent.len(), 0); + } else { + panic!() + } + + if let ResponseVariants::Bank(bank) = iter.next().unwrap() { + assert_eq!(bank.sender, "C"); + assert_eq!(bank.receiver, "B"); + assert_eq!(bank.coins[0].amount, Uint128::new(800)); + } else { + panic!() + } + + assert_eq!(iter.next(), None); + } + + fn mock_response() -> ExecuteResponse { + // sender exec A + // A inst B + // B inst C + // C exec D + // C exec B + // B exec D + // B exec A + // A exec C + // C send B + // A send D + + let mut resp = execute_resp("sender", "A"); + + let mut instantiate = instantiate_resp("A"); + + let mut instantiate_2 = instantiate_resp("B"); + instantiate_2.sent.push(execute_resp("C", "D").into()); + instantiate_2.sent.push(execute_resp("C", "B").into()); + + instantiate.sent.push(instantiate_2.into()); + instantiate.sent.push(execute_resp("B", "D").into()); + instantiate.sent.push(execute_resp("B", "A").into()); + + resp.sent.push(instantiate.into()); + + let mut exec = execute_resp("A", "C"); + exec.sent.push(bank_resp("C", "B").into()); + + resp.sent.push(exec.into()); + resp.sent.push(bank_resp("A", "D").into()); + + MSG_INDEX.with(|x| { *x.borrow_mut() = 0; }); + + resp + } + + fn execute_resp(sender: impl Into, address: impl Into) -> ExecuteResponse { + let index = MSG_INDEX.with(|x| x.borrow().clone()); + + let resp = ExecuteResponse { + sender: sender.into(), + address: address.into(), + msg: Binary::from(format!("message_{}", index).as_bytes()), + response: Response::default(), + sent: vec![] + }; + + MSG_INDEX.with(|x| { *x.borrow_mut() += 1; }); + + resp + } + + fn instantiate_resp(sender: impl Into) -> InstantiateResponse { + let index = MSG_INDEX.with(|x| x.borrow().clone()); + + let resp = InstantiateResponse { + sender: sender.into(), + instance: ContractLink { + address: Addr::unchecked(""), + code_hash: String::new() + }, + code_id: 0, + msg: Binary::from(format!("message_{}", index).as_bytes()), + response: Response::default(), + sent: vec![] + }; + + MSG_INDEX.with(|x| { *x.borrow_mut() += 1; }); + + resp + } + + fn bank_resp(sender: impl Into, to: impl Into) -> BankResponse { + let index = MSG_INDEX.with(|x| x.borrow().clone()); + + BankResponse { + sender: sender.into(), + receiver: to.into(), + coins: vec![Coin { + denom: "uscrt".into(), + amount: Uint128::new(100 * index as u128) + }] + } + } +} diff --git a/ensemble/src/staking.rs b/ensemble/src/staking.rs new file mode 100644 index 00000000000..75a52d3287d --- /dev/null +++ b/ensemble/src/staking.rs @@ -0,0 +1,409 @@ +use std::collections::HashMap; + +use crate::prelude::*; +use super::{ + EnsembleResult, EnsembleError, + response::{ + StakingResponse, StakingOp, + DistributionResponse, DistributionOp + } +}; + +#[derive(Clone, Debug)] +pub(crate) struct DelegationWithUnbonding { + delegator: String, + validator: String, + amount: Coin, + unbonding_amount: Coin, + can_redelegate: Coin, + accumulated_rewards: Coin, +} + +pub(crate) type Delegator = HashMap; + +#[derive(Debug)] +pub(crate) struct Delegations { + /// Denom for bonded currency + bonded_denom: String, + /// List of all valid validators + validators: Vec, + /// Doubly hashed array of delegations for easy access + delegators: HashMap, +} + +impl Into for DelegationWithUnbonding { + fn into(self) -> Delegation { + Delegation { + delegator: Addr::unchecked(self.delegator), + validator: self.validator, + amount: self.amount, + } + } +} + +impl Into for DelegationWithUnbonding { + fn into(self) -> FullDelegation { + FullDelegation { + delegator: Addr::unchecked(self.delegator), + validator: self.validator, + amount: self.amount, + can_redelegate: self.can_redelegate, + accumulated_rewards: vec![self.accumulated_rewards], + } + } +} + +impl Delegations { + pub fn new(bonded_denom: String) -> Self { + Self { + bonded_denom, + validators: Default::default(), + delegators: Default::default(), + } + } + + pub fn add_validator(&mut self, new_validator: Validator) { + self.validators.push(new_validator); + } + + pub fn distribute_rewards(&mut self, amount: Uint128) { + for delegator in self.delegators.iter_mut() { + for delegation_pair in delegator.1 { + let delegation = delegation_pair.1; + let new_rewards = Coin { + denom: self.bonded_denom.clone(), + amount: delegation.accumulated_rewards.amount + amount, + }; + delegation.accumulated_rewards = new_rewards; + } + } + } + + pub fn fast_forward_waits(&mut self) -> Vec { + let mut unbondings = vec![]; + for delegator in self.delegators.iter_mut() { + for delegation_pair in delegator.1 { + let delegation = delegation_pair.1; + delegation.can_redelegate = delegation.amount.clone(); + if delegation.unbonding_amount.amount > Uint128::zero() { + unbondings.push(Delegation { + delegator: Addr::unchecked(delegation.delegator.clone()), + validator: delegation.validator.to_string(), + amount: delegation.unbonding_amount.clone() + }); + } + } + } + unbondings + } + + // Validator queries + pub fn bonded_denom(&self) -> &str { + &self.bonded_denom + } + + pub fn all_delegations(&self, delegator: &str) -> Vec { + match self.delegators.get(delegator) { + Some(delegations) => { + let mut return_delegations: Vec = vec![]; + for delegation in delegations { + return_delegations.push(delegation.1.clone().into()); + } + return_delegations + } + None => vec![] + } + } + + pub fn delegation( + &self, + delegator: &str, + validator: &str + ) -> Option { + match self.get_delegation(delegator, validator) { + Some(delegation) => Some(delegation.into()), + None => None, + } + } + + pub fn validators(&self) -> &[Validator] { + &self.validators + } + + // Validator transaction messages + pub fn delegate( + &mut self, + delegator: String, + validator: String, + amount: Coin + ) -> EnsembleResult { + if amount.denom != self.bonded_denom { + return Err(EnsembleError::Staking("Incorrect coin denom".into())); + } + if !self.validate_validator(&validator) { + return Err(EnsembleError::Staking("Validator not found".into())); + } + + let mut new_delegation = DelegationWithUnbonding { + delegator: delegator.clone(), + validator: validator.clone(), + amount: amount.clone(), + unbonding_amount: Coin { + denom: self.bonded_denom.clone(), + amount: Uint128::zero(), + }, + can_redelegate: amount.clone(), + accumulated_rewards: Coin { + denom: self.bonded_denom.clone(), + amount: Uint128::zero(), + }, + }; + + // Check if delegation pair exists, add amounts if so + match self.delegators.get_mut(&delegator) { + Some(cur_delegator) => { + match cur_delegator.get(&validator) { + Some(ref old_deleg) => { + let old_delegation = (*old_deleg).clone(); + new_delegation.amount = Coin { + denom: self.bonded_denom.clone(), + amount: old_delegation.amount.amount + amount.amount, + }; + new_delegation.unbonding_amount = old_delegation.unbonding_amount; + new_delegation.can_redelegate = Coin { + denom: self.bonded_denom.clone(), + amount: old_delegation.can_redelegate.amount + amount.amount, + }; + new_delegation.accumulated_rewards = old_delegation.accumulated_rewards; + }, + None => { } + } + }, + None => { } + }; + + self.insert_delegation(delegator.clone(), validator.clone(), new_delegation); + + Ok(StakingResponse { + sender: delegator, + amount, + kind: StakingOp::Delegate { + validator + } + }) + } + + pub fn undelegate( + &mut self, + delegator: String, + validator: String, + amount: Coin + ) -> EnsembleResult { + if amount.denom != self.bonded_denom { + return Err(EnsembleError::Staking("Incorrect coin denom".into())); + } + + match self.get_delegation(&delegator, &validator) { + Some(delegation) => { + if amount.amount > delegation.amount.amount { + return Err(EnsembleError::Staking("Insufficient funds".into())); + } + + let mut new_can_redelegate = delegation.can_redelegate.clone(); + if delegation.can_redelegate.amount + amount.amount > delegation.amount.amount { + new_can_redelegate.amount = delegation.amount.amount - amount.amount; + } + + let new_delegation = DelegationWithUnbonding { + delegator: delegator.clone(), + validator: validator.clone(), + amount: Coin { + denom: self.bonded_denom.clone(), + amount: delegation.amount.amount - amount.amount, + }, + unbonding_amount: Coin { + denom: self.bonded_denom.clone(), + amount: delegation.unbonding_amount.amount + amount.amount, + }, + can_redelegate: new_can_redelegate, + accumulated_rewards: delegation.accumulated_rewards, + }; + + self.insert_delegation(delegator.clone(), validator.clone(), new_delegation); + + Ok(StakingResponse { + sender: delegator, + amount, + kind: StakingOp::Undelegate { + validator + } + }) + }, + None => Err(EnsembleError::Staking("Delegation not found".into())) + } + } + + pub fn withdraw( + &mut self, + delegator: String, + validator: String, + ) -> EnsembleResult { + match self.get_delegation(&delegator, &validator) { + Some(delegation) => { + let new_delegation = DelegationWithUnbonding { + delegator: delegator.clone(), + validator: validator.clone(), + amount: delegation.amount, + unbonding_amount: delegation.unbonding_amount, + can_redelegate: delegation.can_redelegate, + accumulated_rewards: Coin { + denom: self.bonded_denom.clone(), + amount: Uint128::zero(), + }, + }; + self.insert_delegation(delegator.clone(), validator.clone(), new_delegation); + + Ok(DistributionResponse { + sender: delegator, + kind: DistributionOp::WithdrawDelegatorReward { + validator, + reward: delegation.accumulated_rewards + } + }) + }, + None => Err(EnsembleError::Staking("Delegation not found".into())) + } + } + + pub fn redelegate( + &mut self, + delegator: String, + src_validator: String, + dst_validator: String, + amount: Coin + ) -> EnsembleResult { + if amount.denom != self.bonded_denom { + return Err(EnsembleError::Staking("Incorrect coin denom".into())); + } + + match self.get_delegation(&delegator, &src_validator) { + Some(delegation) => { + if amount.amount > delegation.amount.amount { + return Err(EnsembleError::Staking("Insufficient funds".into())); + } + + if amount.amount > delegation.can_redelegate.amount { + return Err(EnsembleError::Staking("Insufficient funds to redelegate".into())); + } + + if !self.validate_validator(&dst_validator) { + return Err(EnsembleError::Staking("Destination validator does not exist".into())); + } + + let new_src_delegation = DelegationWithUnbonding { + delegator: delegator.clone(), + validator: src_validator.clone(), + amount: Coin { + denom: self.bonded_denom.clone(), + amount: delegation.amount.amount - amount.amount, + }, + unbonding_amount: delegation.unbonding_amount, + can_redelegate: Coin { + denom: self.bonded_denom.clone(), + amount: delegation.can_redelegate.amount - amount.amount, + }, + accumulated_rewards: delegation.accumulated_rewards, + }; + self.insert_delegation(delegator.clone(), src_validator.clone(), new_src_delegation); + + // Check if delegation already exists with dst validator + match self.get_delegation(&delegator, &dst_validator) { + Some(mut dst_delegation) => { + dst_delegation.amount.amount += amount.amount; + self.insert_delegation(delegator.clone(), dst_validator.clone(), dst_delegation); + } + None => { + let new_dst_delegation = DelegationWithUnbonding { + delegator: delegator.clone(), + validator: dst_validator.clone(), + amount: Coin { + denom: self.bonded_denom.clone(), + amount: amount.amount, + }, + unbonding_amount: Coin { + denom: self.bonded_denom.clone(), + amount: Uint128::zero(), + }, + can_redelegate: Coin { + denom: self.bonded_denom.clone(), + amount: Uint128::zero(), + }, + accumulated_rewards: Coin { + denom: self.bonded_denom.clone(), + amount: Uint128::zero(), + }, + }; + self.insert_delegation(delegator.clone(), dst_validator.clone(), new_dst_delegation); + } + }; + + Ok(StakingResponse { + sender: delegator, + amount, + kind: StakingOp::Redelegate { + src_validator, + dst_validator + } + }) + }, + None => Err(EnsembleError::Staking("Delegation not found".into())) + } + + } + + // Helper methods + fn get_delegation( + &self, + delegator: &str, + validator: &str, + ) -> Option { + match self.delegators.get(delegator) { + Some(cur_delegator) => { + match cur_delegator.get(validator).clone() { + Some(ref delegation) => Some((*delegation).clone()), + _ => None, + } + }, + _ => None, + } + } + + fn insert_delegation( + &mut self, + delegator: String, + validator: String, + new_delegation: DelegationWithUnbonding + ) -> Option { + match self.delegators.get_mut(&delegator) { + Some(cur_delegator) => { + cur_delegator.insert(validator, new_delegation) + }, + None => { + let mut new_delegator: Delegator = Default::default(); + new_delegator.insert(validator, new_delegation); + self.delegators.insert(delegator, new_delegator); + None + }, + } + } + + fn validate_validator(&self, validator: &str) -> bool { + for real_validator in self.validators.iter() { + if real_validator.address == *validator { + return true; + } + } + + false + } +} diff --git a/ensemble/src/state.rs b/ensemble/src/state.rs new file mode 100644 index 00000000000..252338000a9 --- /dev/null +++ b/ensemble/src/state.rs @@ -0,0 +1,458 @@ +use std::collections::HashMap; + +use fadroma::cosmwasm_std::{Coin, Storage}; + +use super::{ + EnsembleResult, + storage::TestStorage, + bank::Bank, + response::BankResponse, + error::{EnsembleError, RegistryError} +}; + +#[derive(Default, Debug)] +pub(crate) struct State { + pub instances: HashMap, + pub bank: Bank, + scopes: Vec +} + +#[derive(Debug)] +pub(crate) struct ContractInstance { + pub storage: TestStorage, + pub index: usize +} + +#[derive(Clone, Debug)] +pub enum Op { + CreateInstance { + address: String + }, + StorageWrite { + address: String, + key: Vec, + old: Option> + }, + #[allow(dead_code)] + BankAddFunds { + address: String, + coin: Coin + }, + #[allow(dead_code)] + BankRemoveFunds { + address: String, + coin: Coin + }, + BankTransferFunds { + from: String, + to: String, + coin: Coin + } +} + +#[derive(Default, Debug)] +struct Scope(Vec); + +impl State { + pub fn new() -> Self { + Self { + instances: HashMap::new(), + bank: Bank::default(), + scopes: vec![] + } + } + + pub fn create_contract_instance( + &mut self, + address: impl Into, + index: usize + ) -> EnsembleResult<()> { + assert!(self.scopes.len() > 0); + let address = address.into(); + + if self.instances.contains_key(&address) { + return Err(EnsembleError::registry(RegistryError::DuplicateAddress(address))); + } + + let storage = TestStorage::new(address.clone()); + self.instances.insert( + address.clone(), + ContractInstance { index, storage } + ); + + let scope = self.current_scope_mut(); + scope.0.push(Op::CreateInstance { address }); + + Ok(()) + } + + #[inline] + pub fn instance(&self, address: &str) -> EnsembleResult<&ContractInstance> { + match self.instances.get(address) { + Some(instance) => Ok(instance), + None => Err(EnsembleError::registry(RegistryError::NotFound(address.to_string()))) + } + } + + pub fn borrow_storage_mut(&mut self, address: &str, borrow: F) -> EnsembleResult + where F: FnOnce(&mut dyn Storage) -> EnsembleResult + { + if let Some(instance) = self.instances.get_mut(address) { + let result = borrow(&mut instance.storage as &mut dyn Storage); + + let ops = instance.storage.ops(); + self.push_ops(ops); + + result + } else { + Err(EnsembleError::registry(RegistryError::NotFound(address.into()))) + } + } + + #[inline] + pub fn commit(&mut self) { + self.scopes.clear(); + } + + #[inline] + pub fn revert(&mut self) { + while self.scopes.len() > 0 { + self.revert_scope(); + } + } + + #[inline] + pub fn push_scope(&mut self) { + self.scopes.push(Scope::default()); + } + + pub fn revert_scope(&mut self) { + assert!(self.scopes.len() > 0); + + let scope = self.scopes.pop().unwrap(); + + for op in scope.0 { + match op { + Op::CreateInstance { address } => { + self.instances.remove(&address); + } + Op::StorageWrite { address, key, old } => { + if let Some(instance) = self.instances.get_mut(&address) { + if let Some(old) = old { + instance.storage.backing.insert(key, old); + } else { + instance.storage.backing.remove(&key); + } + } + } + Op::BankAddFunds { address, coin } => { + self.bank.remove_funds(&address, coin).unwrap(); + } + Op::BankRemoveFunds { address, coin } => { + self.bank.add_funds(&address, coin); + } + Op::BankTransferFunds { from, to, coin } => { + self.bank.transfer(&to, &from, coin).unwrap(); + } + } + } + } + + #[allow(dead_code)] + pub fn add_funds( + &mut self, + address: impl Into, + coins: Vec + ) { + assert!(self.scopes.len() > 0); + + let address: String = address.into(); + + let scope = self.scopes.last_mut().unwrap(); + scope.0.reserve_exact(coins.len()); + + for coin in coins { + self.bank.add_funds(&address, coin.clone()); + + scope.0.push(Op::BankAddFunds { + address: address.clone(), + coin + }); + } + } + + #[allow(dead_code)] + pub fn remove_funds( + &mut self, + address: impl Into, + coins: Vec + ) -> EnsembleResult<()> { + assert!(self.scopes.len() > 0); + + let address: String = address.into(); + self.push_scope(); + + let temp = self.scopes.last_mut().unwrap(); + temp.0.reserve_exact(coins.len()); + + for coin in coins { + match self.bank.remove_funds(&address, coin.clone()) { + Ok(()) => { + temp.0.push(Op::BankRemoveFunds { + address: address.clone(), + coin + }); + }, + Err(err) => { + self.revert_scope(); + + return Err(err); + } + } + } + + let temp = self.scopes.pop().unwrap(); + self.current_scope_mut().0.extend(temp.0); + + Ok(()) + } + + pub fn transfer_funds( + &mut self, + from: impl Into, + to: impl Into, + coins: Vec + ) -> EnsembleResult { + assert!(self.scopes.len() > 0); + + let from = from.into(); + let to = to.into(); + + let res = BankResponse { + sender: from.clone(), + receiver: to.clone(), + coins: coins.clone() + }; + + self.push_scope(); + + let temp = self.scopes.last_mut().unwrap(); + temp.0.reserve_exact(coins.len()); + + for coin in coins { + match self.bank.transfer(&from, &to, coin.clone()) { + Ok(()) => { + temp.0.push(Op::BankTransferFunds { + from: from.clone(), + to: to.clone(), + coin: coin.clone() + }); + }, + Err(err) => { + self.revert_scope(); + + return Err(err); + } + } + } + + let temp = self.scopes.pop().unwrap(); + self.current_scope_mut().0.extend(temp.0); + + Ok(res) + } + + fn push_ops(&mut self, ops: Vec) { + let scope = self.current_scope_mut(); + scope.0.extend(ops); + } + + #[inline] + fn current_scope_mut(&mut self) -> &mut Scope { + assert!(self.scopes.len() > 0); + + self.scopes.last_mut().unwrap() + } +} + +#[cfg(test)] +mod tests { + use crate::cosmwasm_std::Storage; + use super::*; + + const CONTRACTS: &[&str] = &["A", "B", "C"]; + + #[test] + fn storage_revert_keeps_initial_value_and_removes_newly_set() { + let mut state = setup_storage(); + + state.push_scope(); + let store = storage_mut(&mut state, CONTRACTS[0]); + store.remove(b"a"); + store.set(b"b", b"yyz"); + store.remove(b"c"); + + let ops = store.ops(); + assert_eq!(ops.len(), 2); + assert_eq!(store.ops().len(), 0); + + drop(store); + + state.push_ops(ops); + state.revert(); + + let store = storage_mut(&mut state, CONTRACTS[0]); + assert_eq!(store.get(b"a"), Some(b"abc".to_vec())); + assert_eq!(store.get(b"b"), None); + } + + #[test] + fn storage_commit_saves_changes_and_clears_all_scopes() { + let mut state = setup_storage(); + + state.push_scope(); + let store = storage_mut(&mut state, CONTRACTS[0]); + store.remove(b"a"); + store.set(b"b", b"yyz"); + + let ops = store.ops(); + drop(store); + + state.push_ops(ops); + state.commit(); + + assert_eq!(state.scopes.len(), 0); + + let store = storage_mut(&mut state, CONTRACTS[0]); + assert_eq!(store.get(b"a"), None); + assert_eq!(store.get(b"b"), Some(b"yyz".to_vec())); + } + + #[test] + fn storage_revert_scope_affects_only_topmost_scope() { + let mut state = setup_storage(); + + state.push_scope(); + let store = storage_mut(&mut state, CONTRACTS[0]); + store.remove(b"a"); + store.set(b"b", b"yyz"); + + let ops = store.ops(); + drop(store); + + state.push_ops(ops); + + state.push_scope(); + let store = storage_mut(&mut state, CONTRACTS[1]); + store.set(b"a", b"yyz"); + + let ops = store.ops(); + drop(store); + + state.push_ops(ops); + state.revert_scope(); + + let store = storage_mut(&mut state, CONTRACTS[1]); + assert_eq!(store.get(b"a"), None); + + let store = storage_mut(&mut state, CONTRACTS[0]); + assert_eq!(store.get(b"a"), None); + assert_eq!(store.get(b"b"), Some(b"yyz".to_vec())); + + state.commit(); + + assert_eq!(state.scopes.len(), 0); + } + + #[test] + fn reverts_bank_add_remove_funds() { + let mut state = State::new(); + state.bank.add_funds(CONTRACTS[0], Coin::new(100, "uscrt")); + + state.push_scope(); + + state.add_funds(CONTRACTS[0], vec![Coin::new(100, "uscrt")]); + assert_eq!(state.scopes.last().unwrap().0.len(), 1); + + state.remove_funds(CONTRACTS[1], vec![Coin::new(100, "uscrt")]).unwrap_err(); + assert_eq!(state.scopes.last().unwrap().0.len(), 1); + + assert_eq!(check_balance(&state, CONTRACTS[1]), 0); + assert_eq!(check_balance(&state, CONTRACTS[0]), 200); + + state.revert(); + + assert_eq!(check_balance(&state, CONTRACTS[0]), 100); + } + + #[test] + fn reverts_bank_transfers() { + let mut state = State::new(); + state.bank.add_funds(CONTRACTS[0], Coin::new(100, "uscrt")); + + state.push_scope(); + state.add_funds(CONTRACTS[0], vec![Coin::new(100, "uscrt")]); + + assert_eq!(check_balance(&state, CONTRACTS[0]), 200); + + state.push_scope(); + state.remove_funds(CONTRACTS[0], vec![Coin::new(100, "uscrt")]).unwrap(); + state.transfer_funds( + CONTRACTS[0], + CONTRACTS[1], + vec![ + Coin::new(100, "uscrt"), + Coin::new(100, "atom") + ] + ).unwrap_err(); + + assert_eq!(check_balance(&state, CONTRACTS[1]), 0); + assert_eq!(check_balance(&state, CONTRACTS[0]), 100); + + state.transfer_funds( + CONTRACTS[0], + CONTRACTS[1], + vec![ + Coin::new(100, "uscrt"), + ] + ).unwrap(); + + assert_eq!(check_balance(&state, CONTRACTS[1]), 100); + assert_eq!(check_balance(&state, CONTRACTS[0]), 0); + + state.revert(); + + assert_eq!(check_balance(&state, CONTRACTS[1]), 0); + assert_eq!(check_balance(&state, CONTRACTS[0]), 100); + } + + fn check_balance(state: &State, address: &str) -> u128 { + let mut balances = state.bank.query_balances(address, Some("uscrt".into())); + assert_eq!(balances.len(), 1); + + balances.pop().unwrap().amount.u128() + } + + fn storage_mut<'a>(state: &'a mut State, address: &str) -> &'a mut TestStorage { + &mut state.instances.get_mut(address).unwrap().storage + } + + fn setup_storage() -> State { + let mut state = State::new(); + + state.push_scope(); + + state.create_contract_instance(CONTRACTS[0], 0).unwrap(); + state.create_contract_instance(CONTRACTS[1], 1).unwrap(); + state.create_contract_instance(CONTRACTS[2], 2).unwrap(); + + state.commit(); + + let mut storage = TestStorage::new(CONTRACTS[0]); + storage.backing.insert(b"a".to_vec(), b"abc".to_vec()); + + state.instances.get_mut(CONTRACTS[0]).unwrap().storage = storage; + + state + } +} diff --git a/ensemble/src/storage.rs b/ensemble/src/storage.rs new file mode 100644 index 00000000000..33cfc85087b --- /dev/null +++ b/ensemble/src/storage.rs @@ -0,0 +1,97 @@ +use std::{ + iter, + mem, + collections::BTreeMap, + ops::{Bound, RangeBounds} +}; + +use fadroma::cosmwasm_std::{ + Storage, Record, Order +}; + +use super::state::Op; + +#[derive(Clone, Debug)] +pub struct TestStorage { + pub backing: BTreeMap, Vec>, + pub ops: Vec, + address: String +} + +impl TestStorage { + pub fn new(address: impl Into) -> Self { + Self { + address: address.into(), + backing: BTreeMap::default(), + ops: vec![] + } + } + + #[inline] + pub fn ops(&mut self) -> Vec { + mem::take(&mut self.ops) + } +} + +impl Storage for TestStorage { + fn set(&mut self, key: &[u8], value: &[u8]) { + let address = self.address.clone(); + let old = self.get(key); + let key = key.to_vec(); + + self.backing.insert(key.clone(), value.to_vec()); + self.ops.push(Op::StorageWrite { address, key, old }); + } + + fn remove(&mut self, key: &[u8]) { + if let Some(old) = self.backing.remove(key) { + self.ops.push(Op::StorageWrite { + address: self.address.clone(), + key: key.to_vec(), + old: Some(old) + }); + } + } + + fn get(&self, key: &[u8]) -> Option> { + self.backing.get(key).cloned() + } + + fn range<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box + 'a> { + let bounds = range_bounds(start, end); + + // BTreeMap.range panics if range is start > end. + // However, this cases represent just empty range and we treat it as such. + match (bounds.start_bound(), bounds.end_bound()) { + (Bound::Included(start), Bound::Excluded(end)) if start > end => { + return Box::new(iter::empty()); + } + _ => {} + } + + let iter = self.backing.range(bounds); + match order { + Order::Ascending => Box::new(iter.map(clone_item)), + Order::Descending => Box::new(iter.rev().map(clone_item)), + } + } +} + +fn range_bounds(start: Option<&[u8]>, end: Option<&[u8]>) -> impl RangeBounds> { + ( + start.map_or(Bound::Unbounded, |x| Bound::Included(x.to_vec())), + end.map_or(Bound::Unbounded, |x| Bound::Excluded(x.to_vec())), + ) +} + +type BTreeMapPairRef<'a, T = Vec> = (&'a Vec, &'a T); + +fn clone_item(item_ref: BTreeMapPairRef) -> Record { + let (key, value) = item_ref; + (key.clone(), value.clone()) +} diff --git a/ensemble/tests/interactions.rs b/ensemble/tests/interactions.rs new file mode 100644 index 00000000000..ad728497a07 --- /dev/null +++ b/ensemble/tests/interactions.rs @@ -0,0 +1,751 @@ +use serde::{Deserialize, Serialize}; +use anyhow::{Result as AnyResult, bail}; + +use crate::{ + self as fadroma, + ensemble::{ + ContractEnsemble, ContractHarness, + MockEnv, EnsembleResult, EnsembleError, + ResponseVariants + } +}; +use crate::prelude::*; + +const SEND_AMOUNT: u128 = 100; +const SEND_DENOM: &str = "uscrt"; +const MULTIPLIER_MSG_TYPE: &str = "multiplier"; + +struct Counter; + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +struct CounterInit { + info: ContractCode, + fail: bool, + fail_multiplier: bool, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +enum CounterHandle { + Increment, + IncrementAndMultiply { by: u8 }, + RegisterMultiplier, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +enum CounterQuery { + Number, + Multiplier, +} + +impl ContractHarness for Counter { + fn instantiate( + &self, + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: Binary, + ) -> AnyResult { + let msg: CounterInit = from_binary(&msg)?; + + storage::save( + deps.storage, + b"mul", + &ContractLink { + address: Addr::unchecked(""), + code_hash: msg.info.code_hash.clone(), + }, + )?; + + if msg.fail { + bail!("Failed at Counter."); + } + + let instantiate_msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + code_id: msg.info.id, + code_hash: msg.info.code_hash, + funds: vec![coin(SEND_AMOUNT, SEND_DENOM)], + msg: to_binary(&MultiplierInit { + fail: msg.fail_multiplier, + })?, + label: "A".repeat(MockEnv::MAX_ADDRESS_LEN + 1) + }, + 0 + ); + + let mut response = Response::new(); + response.messages.push(instantiate_msg); + + Ok(response) + } + + fn execute( + &self, + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: Binary, + ) -> AnyResult { + let msg: CounterHandle = from_binary(&msg)?; + + match msg { + CounterHandle::Increment => { + increment(deps.storage)?; + } + CounterHandle::RegisterMultiplier => { + let mut contract_info: ContractLink = storage::load( + deps.storage, + b"mul" + )?.unwrap(); + + contract_info.address = info.sender; + + storage::save(deps.storage, b"mul", &contract_info)?; + } + CounterHandle::IncrementAndMultiply { by } => { + let number = increment(deps.storage)?; + let multiplier: ContractLink = storage::load(deps.storage, b"mul")?.unwrap(); + + return Ok(Response::new().add_message(WasmMsg::Execute { + contract_addr: multiplier.address.into_string(), + code_hash: multiplier.code_hash, + msg: to_binary(&MultiplierHandle { + number, + multiplier: by, + })?, + funds: vec![], + })); + } + } + + Ok(Response::default()) + } + + fn query(&self, deps: Deps, _env: Env, msg: Binary) -> AnyResult { + let msg: CounterQuery = from_binary(&msg)?; + + let bin = match msg { + CounterQuery::Number => { + let number: u8 = storage::load(deps.storage, b"num")?.unwrap_or_default(); + + to_binary(&number)? + } + CounterQuery::Multiplier => { + let multiplier: ContractLink = storage::load(deps.storage, b"mul")?.unwrap(); + + to_binary(&multiplier)? + } + }; + + Ok(bin) + } + + fn reply(&self, deps: DepsMut, _env: Env, reply: Reply) -> AnyResult { + assert_eq!(reply.id, 0); + + match reply.result { + SubMsgResult::Ok(result) => { + let ty = format!("wasm-{}", MULTIPLIER_MSG_TYPE); + + let event = result.events.into_iter() + .find(|x| x.ty == ty) + .unwrap(); + let attr = event.attributes.into_iter() + .find(|x| x.key == "address") + .unwrap(); + + let mut contract_info: ContractLink = storage::load(deps.storage, b"mul")?.unwrap(); + contract_info.address = Addr::unchecked(attr.value); + + storage::save(deps.storage, b"mul", &contract_info)?; + }, + SubMsgResult::Err(err) => bail!(err) + } + + Ok(Response::new()) + } +} + +fn increment(storage: &mut dyn Storage) -> StdResult { + let mut number: u8 = storage::load(storage, b"num")?.unwrap_or_default(); + number += 1; + + storage::save(storage, b"num", &number)?; + + Ok(number) +} + +struct Multiplier; + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +struct MultiplierInit { + fail: bool, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +struct MultiplierHandle { + number: u8, + multiplier: u8, +} + +impl ContractHarness for Multiplier { + fn instantiate( + &self, + _deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: Binary, + ) -> AnyResult { + let msg: MultiplierInit = from_binary(&msg)?; + + if msg.fail { + bail!("Failed at Multiplier."); + } + + Ok(Response::new().add_event( + Event::new(MULTIPLIER_MSG_TYPE) + .add_attribute_plaintext("address", env.contract.address.into_string()) + )) + } + + fn execute( + &self, + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: Binary, + ) -> AnyResult { + let msg: MultiplierHandle = from_binary(&msg)?; + + let result = msg + .number + .checked_mul(msg.multiplier) + .ok_or_else(|| StdError::generic_err("Mul overflow."))?; + + storage::save(deps.storage, b"last", &result)?; + + Ok(Response::default()) + } + + fn query(&self, deps: Deps, _env: Env, _msg: Binary) -> AnyResult { + let last: u8 = storage::load(deps.storage, b"last")?.unwrap(); + let result = to_binary(&last)?; + + Ok(result) + } +} + +#[derive(Debug)] +struct InitResult { + counter: ContractLink, + multiplier: ContractLink, +} + +fn init( + ensemble: &mut ContractEnsemble, + fail_counter: bool, + fail_multiplier: bool, +) -> EnsembleResult { + let counter = ensemble.register(Box::new(Counter)); + let multiplier = ensemble.register(Box::new(Multiplier)); + + let admin = "admin"; + ensemble.add_funds(admin, vec![coin(SEND_AMOUNT, SEND_DENOM)]); + + let msg = ensemble + .instantiate( + counter.id, + &CounterInit { + info: multiplier.clone(), + fail: fail_counter, + fail_multiplier + }, + MockEnv::new( + admin, + "counter" + ) + .sent_funds(vec![coin(SEND_AMOUNT, SEND_DENOM)]) + )?; + + let ResponseVariants::Instantiate(multiplier_init) = msg.iter().next().unwrap() else { + panic!("Expecting ResponseVariants::Instantiate"); + }; + + let multiplier = multiplier_init.instance.clone(); + assert_eq!(multiplier.address.as_ref(), "a".repeat(MockEnv::MAX_ADDRESS_LEN)); + + Ok(InitResult { + counter: msg.instance, + multiplier + }) +} + +struct BlockHeight; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +enum BlockHeightHandle { + Set, +} + +#[derive(Serialize, Deserialize, FadromaSerialize, FadromaDeserialize, Debug, PartialEq)] +struct Block { + height: u64, + time: u64, +} + +impl ContractHarness for BlockHeight { + fn instantiate(&self, deps: DepsMut, env: Env, _info: MessageInfo, _msg: Binary) -> AnyResult { + storage::save( + deps.storage, + b"block", + &Block { + height: env.block.height, + time: env.block.time.seconds(), + }, + )?; + + Ok(Response::default()) + } + + fn execute( + &self, + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: Binary, + ) -> AnyResult { + let msg: BlockHeightHandle = from_binary(&msg)?; + match msg { + BlockHeightHandle::Set => { + storage::save( + deps.storage, + b"block", + &Block { + height: env.block.height, + time: env.block.time.seconds(), + }, + )?; + } + }; + + Ok(Response::default()) + } + + fn query(&self, deps: Deps, _env: Env, _msg: Binary) -> AnyResult { + let block: Block = storage::load(deps.storage, b"block")?.unwrap(); + let result = to_binary(&block)?; + + Ok(result) + } +} + +#[test] +fn test_removes_instances_on_failed_init() { + let mut ensemble = ContractEnsemble::new(); + let result = init(&mut ensemble, false, false).unwrap(); + assert_eq!(ensemble.ctx.contracts.len(), 2); + assert_eq!(ensemble.ctx.state.instances.len(), 2); + + let balances = ensemble.balances(result.multiplier.address.clone()).unwrap(); + assert_eq!(balances.len(), 1); + assert_eq!( + *balances.get(SEND_DENOM).unwrap(), + Uint128::new(SEND_AMOUNT) + ); + + let number: u8 = ensemble + .query(result.counter.address.clone(), &CounterQuery::Number) + .unwrap(); + assert_eq!(number, 0); + + let multiplier: ContractLink = ensemble + .query(result.counter.address, &CounterQuery::Multiplier) + .unwrap(); + + assert_eq!(multiplier, result.multiplier); + + let mut ensemble = ContractEnsemble::new(); + let result = init(&mut ensemble, true, false).unwrap_err(); + assert_eq!(result.to_string(), "Failed at Counter."); + assert_eq!(ensemble.ctx.contracts.len(), 2); + assert_eq!(ensemble.ctx.state.instances.len(), 0); + + let mut ensemble = ContractEnsemble::new(); + let result = init(&mut ensemble, false, true).unwrap_err(); + assert_eq!( + result.to_string(), + "Failed at Multiplier." + ); + assert_eq!(ensemble.ctx.contracts.len(), 2); + assert_eq!(ensemble.ctx.state.instances.len(), 0); +} + +#[test] +fn test_reverts_state_on_fail() { + let sender = "sender"; + + let mut ensemble = ContractEnsemble::new(); + ensemble.add_funds(sender, vec![coin(SEND_AMOUNT * 2, SEND_DENOM)]); + + let result = init(&mut ensemble, false, false).unwrap(); + + ensemble + .execute( + &CounterHandle::Increment, + MockEnv::new(sender, result.counter.address.clone()), + ) + .unwrap(); + + let number: u8 = ensemble + .query(&result.counter.address, &CounterQuery::Number) + .unwrap(); + assert_eq!(number, 1); + + ensemble.contract_storage_mut(result.counter.address.clone(), |storage| { + storage.set(b"num", &[2]); + + Ok(()) + }) + .unwrap(); + + ensemble + .execute( + &CounterHandle::IncrementAndMultiply { by: 2 }, + MockEnv::new(sender, result.counter.address.clone()) + .sent_funds(vec![coin(SEND_AMOUNT, SEND_DENOM)]), + ) + .unwrap(); + + let balances = ensemble.balances(result.counter.address.clone()).unwrap(); + assert_eq!( + *balances.get(SEND_DENOM).unwrap(), + Uint128::new(SEND_AMOUNT) + ); + + let number: u8 = ensemble + .query(&result.counter.address, &CounterQuery::Number) + .unwrap(); + assert_eq!(number, 3); + + let number: u8 = ensemble + .query(&result.multiplier.address, &Empty {}) + .unwrap(); + assert_eq!(number, 6); + + let err = ensemble + .execute( + &CounterHandle::IncrementAndMultiply { by: 100 }, + MockEnv::new(sender, result.counter.address.clone()) + .sent_funds(vec![coin(SEND_AMOUNT, SEND_DENOM)]), + ) + .unwrap_err(); + + assert_eq!( + err.unwrap_contract_error().downcast::().unwrap(), + StdError::generic_err("Mul overflow.") + ); + + let number: u8 = ensemble + .query(&result.counter.address, &CounterQuery::Number) + .unwrap(); + assert_eq!(number, 3); + + let number: u8 = ensemble + .query(result.multiplier.address.clone(), &Empty {}) + .unwrap(); + assert_eq!(number, 6); + + let balances = ensemble.balances(result.counter.address.clone()).unwrap(); + assert_eq!( + *balances.get(SEND_DENOM).unwrap(), + Uint128::new(SEND_AMOUNT) + ); + + let number = ensemble.query_raw( + &result.counter.address, + &CounterQuery::Number + ).unwrap(); + + let number: u8 = from_binary(&number).unwrap(); + assert_eq!(number, 3); + + ensemble.contract_storage(&result.counter.address, |storage| { + let number: u8 = storage::load(storage, b"num").unwrap().unwrap(); + assert_eq!(number, 3); + }) + .unwrap(); +} + +#[test] +#[should_panic( + expected = "Insufficient balance: sender: sender, denom: uscrt, balance: 0, required: 100" +)] +fn insufficient_balance() { + let sender = "sender"; + + let mut ensemble = ContractEnsemble::new(); + ensemble.add_funds(sender, vec![coin(SEND_AMOUNT * 2, SEND_DENOM)]); + + let result = init(&mut ensemble, false, false).unwrap(); + + ensemble + .execute( + &CounterHandle::Increment, + MockEnv::new(sender, result.counter.address.clone()) + .sent_funds(vec![coin(SEND_AMOUNT, SEND_DENOM)]), + ) + .unwrap(); + + let balances = ensemble.balances_mut(sender.clone()).unwrap(); + balances.remove_entry(SEND_DENOM); + + ensemble + .execute( + &CounterHandle::Increment, + MockEnv::new(sender, result.counter.address.clone()) + .sent_funds(vec![coin(SEND_AMOUNT, SEND_DENOM)]), + ) + .unwrap(); +} + +#[test] +fn exact_increment() { + let admin = "admin"; + + let mut ensemble = ContractEnsemble::new(); + ensemble.block_mut().exact_increments(10, 7); + ensemble.block_mut().height = 0; + ensemble.block_mut().time = 0; + + let block_height_contract = ensemble.register(Box::new(BlockHeight)); + + let block_height = ensemble + .instantiate( + block_height_contract.id, + &Empty {}, + MockEnv::new( + admin, + "block_height" + ), + ) + .unwrap() + .instance; + + ensemble + .execute( + &BlockHeightHandle::Set, + MockEnv::new( + admin, + block_height.address.clone() + ), + ) + .unwrap(); + + let res: Block = ensemble + .query(&block_height.address, &Empty {}) + .unwrap(); + + assert_eq!( + res, + Block { + height: 10, + time: 70 + } + ); + + ensemble + .execute( + &BlockHeightHandle::Set, + MockEnv::new(admin, block_height.address.clone()), + ) + .unwrap(); + + let res: Block = ensemble + .query(&block_height.address, &Empty {}) + .unwrap(); + assert_eq!( + res, + Block { + height: 20, + time: 140 + } + ); +} + +#[test] +fn random_increment() { + let admin = "admin"; + + let mut ensemble = ContractEnsemble::new(); + ensemble.block_mut().random_increments(1..11, 1..9); + + let block_height_contract = ensemble.register(Box::new(BlockHeight)); + + let block_height = ensemble + .instantiate( + block_height_contract.id, + &Empty {}, + MockEnv::new( + admin, + "block_height" + ), + ) + .unwrap() + .instance; + + ensemble + .execute( + &BlockHeightHandle::Set, + MockEnv::new(admin, block_height.address.clone()), + ) + .unwrap(); + + let block: Block = ensemble + .query(&block_height.address, &Empty {}) + .unwrap(); + + assert!(block.height > 0); + assert!(block.time > 0); + + ensemble + .execute( + &BlockHeightHandle::Set, + MockEnv::new(admin, block_height.address.clone()), + ) + .unwrap(); + + let res: Block = ensemble + .query(&block_height.address, &Empty {}) + .unwrap(); + + assert!(block.height < res.height); + assert!(block.time < res.time); +} + +#[test] +fn block_freeze() { + let admin = "admin"; + + let mut ensemble = ContractEnsemble::new(); + + let old_height = ensemble.block().height; + let old_time = ensemble.block().time; + + ensemble.block_mut().freeze(); + + let block_height_contract = ensemble.register(Box::new(BlockHeight)); + let block_height = ensemble + .instantiate( + block_height_contract.id, + &Empty {}, + MockEnv::new(admin, "block_height") + ) + .unwrap() + .instance; + + ensemble + .execute( + &BlockHeightHandle::Set, + MockEnv::new(admin, block_height.address.clone()), + ) + .unwrap(); + + let res: Block = ensemble + .query(&block_height.address, &Empty {}) + .unwrap(); + + assert_eq!( + res, + Block { + height: old_height, + time: old_time + } + ); + + ensemble.block_mut().unfreeze(); + ensemble.block_mut().next(); + + ensemble + .execute( + &BlockHeightHandle::Set, + MockEnv::new(admin, block_height.address.clone()), + ) + .unwrap(); + + let res: Block = ensemble + .query(&block_height.address, &Empty {}) + .unwrap(); + + assert!(res.height > old_height); + assert!(res.time > old_time); +} + +#[test] +fn remove_funds() { + let mut ensemble = ContractEnsemble::new(); + let addr = "address"; + + ensemble.add_funds(addr, vec![Coin::new(1000u128, "uscrt")]); + assert_eq!( + ensemble + .ctx + .state + .bank + .query_balances(&addr, Some("uscrt".to_string())), + vec![Coin::new(1000u128, "uscrt")], + ); + + ensemble + .remove_funds(addr, Coin::new(500u128, "uscrt")) + .unwrap(); + + assert_eq!( + ensemble + .ctx + .state + .bank + .query_balances(&addr, Some("uscrt".to_string())), + vec![Coin::new(500u128, "uscrt")], + ); + + match ensemble.remove_funds(addr, Coin::new(600u128, "uscrt")) { + Err(error) => match error { + EnsembleError::Bank(msg) => assert_eq!( + msg, + "Insufficient balance: account: address, denom: uscrt, balance: 500, required: 600" + ), + _ => panic!("Wrong error message"), + }, + _ => panic!("No error message"), + }; + + match ensemble.remove_funds(addr, Coin::new(300u128, "notscrt")) { + Err(error) => match error { + EnsembleError::Bank(msg) => assert_eq!( + msg, + "Insufficient balance: account: address, denom: notscrt, balance: 0, required: 300" + ), + _ => panic!("Wrong error message"), + }, + _ => panic!("No error message"), + }; + + match ensemble.remove_funds( + "address2", + Coin::new(300u128, "uscrt"), + ) { + Err(error) => match error { + EnsembleError::Bank(msg) => { + assert_eq!(msg, "Account address2 does not exist for remove balance") + } + _ => panic!("Wrong error message"), + }, + _ => panic!("No error message"), + }; +} diff --git a/ensemble/tests/mod.rs b/ensemble/tests/mod.rs new file mode 100644 index 00000000000..622d63521b7 --- /dev/null +++ b/ensemble/tests/mod.rs @@ -0,0 +1,4 @@ +mod interactions; +#[cfg(feature = "ensemble-staking")] +mod staking; +mod submsg; diff --git a/ensemble/tests/staking.rs b/ensemble/tests/staking.rs new file mode 100644 index 00000000000..786f40aef89 --- /dev/null +++ b/ensemble/tests/staking.rs @@ -0,0 +1,396 @@ +use crate::ensemble::{ContractEnsemble, EnsembleError}; +use crate::prelude::*; + +#[test] +fn staking() { + let ensemble_test = ContractEnsemble::new_with_denom("something"); + assert_eq!( + ensemble_test.ctx.delegations.bonded_denom(), + "something".to_string() + ); + + let mut ensemble = ContractEnsemble::new(); + assert_eq!(ensemble.ctx.delegations.bonded_denom(), "uscrt"); + + let addr1 = "addr1"; + let addr2 = "addr2"; + + let val_addr_1 = "validator1"; + let val_addr_2 = "validator2"; + let val_addr_3 = "validator3"; + + let validator1 = Validator { + address: val_addr_1.to_string(), + commission: Decimal::percent(5), + max_commission: Decimal::percent(10), + max_change_rate: Decimal::percent(1), + }; + let validator2 = Validator { + address: val_addr_2.to_string(), + commission: Decimal::percent(7), + max_commission: Decimal::percent(15), + max_change_rate: Decimal::percent(5), + }; + + ensemble.add_funds(addr1.clone(), vec![Coin::new(1100u128, "uscrt")]); + ensemble.add_funds(addr1.clone(), vec![Coin::new(314159u128, "notscrt")]); + ensemble.add_validator(validator1.clone()); + ensemble.add_validator(validator2.clone()); + + // TODO test remove_funds + + assert_eq!( + ensemble.ctx.delegations.validators(), + vec![validator1.clone(), validator2.clone()] + ); + + // Delegating (while replicating structure of the ensemble.rs execute_message() delegate code) + ensemble.ctx.state.push_scope(); + ensemble + .ctx + .state + .bank + .remove_funds(&addr1, Coin::new(1000u128, "uscrt")) + .unwrap(); + + match ensemble.ctx.delegations.delegate( + addr1.to_string(), + val_addr_1.to_string(), + Coin::new(1000u128, "uscrt"), + ) { + Ok(result) => Ok(result), + Err(result) => { + ensemble.ctx.state.revert_scope(); + Err(result) + } + } + .unwrap(); + ensemble.ctx.state.commit(); + + ensemble.ctx.state.push_scope(); + ensemble + .ctx + .state + .bank + .remove_funds(&addr1, Coin::new(314159u128, "notscrt")) + .unwrap(); + + match ensemble.ctx.delegations.delegate( + addr1.to_string(), + val_addr_1.to_string(), + Coin::new(314159u128, "notscrt"), + ) { + Err(error) => { + ensemble.ctx.state.revert_scope(); + + match error { + EnsembleError::Staking(msg) => assert_eq!("Incorrect coin denom", msg), + _ => panic!("Wrong denom error improperly caught"), + }; + } + _ => panic!("Wrong denom error improperly caught"), + }; + ensemble.ctx.state.commit(); + + ensemble.ctx.state.push_scope(); + ensemble + .ctx + .state + .bank + .remove_funds(&addr1, Coin::new(100u128, "uscrt")) + .unwrap(); + + match ensemble + .ctx + .delegations + .delegate(addr1.to_string(), val_addr_3.into(), Coin::new(100u128, "uscrt")) + { + Err(error) => { + ensemble.ctx.state.revert_scope(); + match error { + EnsembleError::Staking(msg) => assert_eq!("Validator not found", msg), + _ => panic!("Invalid validator error improperly caught"), + }; + } + _ => panic!("Invalid validator error improperly caught"), + }; + ensemble.ctx.state.commit(); + + match ensemble.ctx.delegations.delegation(&addr1, &val_addr_1) { + Some(delegation) => assert_eq!( + delegation, + FullDelegation { + delegator: Addr::unchecked(addr1.to_string()), + validator: val_addr_1.to_string(), + amount: Coin::new(1000u128, "uscrt"), + can_redelegate: Coin::new(1000u128, "uscrt"), + accumulated_rewards: vec![Coin::new(0u128, "uscrt")], + } + ), + _ => panic!("Incorrect response from delegation query"), + }; + assert_eq!( + ensemble.ctx.delegations.delegation(&addr1, &val_addr_2), + None + ); + assert_eq!( + ensemble.ctx.delegations.delegation(&addr2, &val_addr_1), + None + ); + + // Undelegating + ensemble + .ctx + .delegations + .undelegate( + addr1.to_string(), + val_addr_1.to_string(), + Coin::new(500u128, "uscrt"), + ) + .unwrap(); + match ensemble.ctx.delegations.delegation(&addr1, &val_addr_1) { + Some(delegation) => assert_eq!( + delegation, + FullDelegation { + delegator: Addr::unchecked(addr1.to_string()), + validator: val_addr_1.to_string(), + amount: Coin::new(500u128, "uscrt"), + can_redelegate: Coin::new(500u128, "uscrt"), + accumulated_rewards: vec![Coin::new(0u128, "uscrt")], + } + ), + None => panic!("Delegation not found"), + }; + match ensemble.ctx.delegations.undelegate( + addr1.to_string(), + val_addr_2.to_string(), + Coin::new(300u128, "uscrt"), + ) { + Err(error) => match error { + EnsembleError::Staking(msg) => assert_eq!("Delegation not found", msg), + _ => panic!("Invalid undelegation error improperly caught"), + }, + _ => panic!("Invalid undelegation error improperly caught"), + }; + match ensemble.ctx.delegations.undelegate( + addr1.to_string(), + val_addr_1.to_string(), + Coin::new(600u128, "uscrt"), + ) { + Err(error) => match error { + EnsembleError::Staking(msg) => assert_eq!("Insufficient funds", msg), + _ => panic!("Undelegate too much error improperly caught"), + }, + _ => panic!("Undelegate too much error improperly caught"), + }; + + // Redelegate + ensemble + .ctx + .delegations + .redelegate( + addr1.to_string(), + val_addr_1.to_string(), + val_addr_2.to_string(), + Coin::new(300u128, "uscrt"), + ) + .unwrap(); + match ensemble.ctx.delegations.delegation(&addr1, &val_addr_1) { + Some(delegation) => assert_eq!( + delegation, + FullDelegation { + delegator: Addr::unchecked(addr1.to_string()), + validator: val_addr_1.to_string(), + amount: Coin::new(200u128, "uscrt"), + can_redelegate: Coin::new(200u128, "uscrt"), + accumulated_rewards: vec![Coin::new(0u128, "uscrt")], + } + ), + None => panic!("Original delegation not found"), + }; + match ensemble.ctx.delegations.delegation(&addr1, &val_addr_2) { + Some(delegation) => assert_eq!( + delegation, + FullDelegation { + delegator: Addr::unchecked(addr1.clone()), + validator: val_addr_2.to_string(), + amount: Coin::new(300u128, "uscrt"), + can_redelegate: Coin::new(0u128, "uscrt"), + accumulated_rewards: vec![Coin::new(0u128, "uscrt")], + } + ), + None => panic!("Redelegation not found"), + }; + + ensemble + .ctx + .state + .bank + .remove_funds(&addr1, Coin::new(100u128, "uscrt")) + .unwrap_err(); + + ensemble + .ctx + .delegations + .delegate( + addr1.to_string(), + val_addr_2.to_string(), + Coin::new(100u128, "uscrt"), + ) + .unwrap(); + + ensemble.ctx.state.commit(); + + ensemble + .ctx + .delegations + .redelegate( + addr1.to_string(), + val_addr_2.to_string(), + val_addr_1.to_string(), + Coin::new(50u128, "uscrt"), + ) + .unwrap(); + + ensemble + .ctx + .delegations + .undelegate( + addr1.to_string(), + val_addr_2.to_string(), + Coin::new(325u128, "uscrt"), + ) + .unwrap(); + match ensemble.ctx.delegations.delegation(&addr1, &val_addr_1) { + Some(delegation) => assert_eq!( + delegation, + FullDelegation { + delegator: Addr::unchecked(addr1.to_string()), + validator: val_addr_1.to_string(), + amount: Coin::new(250u128, "uscrt"), + can_redelegate: Coin::new(200u128, "uscrt"), + accumulated_rewards: vec![Coin::new(0u128, "uscrt")], + } + ), + None => panic!("Validator 1 delegation not found"), + }; + match ensemble.ctx.delegations.delegation(&addr1, &val_addr_2) { + Some(delegation) => assert_eq!( + delegation, + FullDelegation { + delegator: Addr::unchecked(addr1.to_string()), + validator: val_addr_2.to_string(), + amount: Coin::new(25u128, "uscrt"), + can_redelegate: Coin::new(25u128, "uscrt"), + accumulated_rewards: vec![Coin::new(0u128, "uscrt")], + } + ), + None => panic!("Validator 2 delegation not found"), + }; + + // Rewards + ensemble.add_rewards(Uint128::from(50u64)); + match ensemble.ctx.delegations.delegation(&addr1, &val_addr_1) { + Some(delegation) => assert_eq!( + delegation, + FullDelegation { + delegator: Addr::unchecked(addr1.to_string()), + validator: val_addr_1.to_string(), + amount: Coin::new(250u128, "uscrt"), + can_redelegate: Coin::new(200u128, "uscrt"), + accumulated_rewards: vec![Coin::new(50u128, "uscrt")], + } + ), + None => panic!("Validator 1 delegation not found"), + }; + match ensemble.ctx.delegations.delegation(&addr1, &val_addr_2) { + Some(delegation) => assert_eq!( + delegation, + FullDelegation { + delegator: Addr::unchecked(addr1.to_string()), + validator: val_addr_2.to_string(), + amount: Coin::new(25u128, "uscrt"), + can_redelegate: Coin::new(25u128, "uscrt"), + accumulated_rewards: vec![Coin::new(50u128, "uscrt")], + } + ), + None => panic!("Validator 2 delegation not found"), + }; + + // Trying to replicate as much as possible from ctx.execute_messages() since it is a private + // function + let withdraw_amount = ensemble + .ctx + .delegations + .delegation(&addr1.clone(), &val_addr_1.clone()) + .unwrap() + .accumulated_rewards; + + ensemble.add_funds(&addr1, withdraw_amount); + + ensemble + .ctx + .delegations + .withdraw(addr1.to_string(), val_addr_1.to_string()) + .unwrap(); + + match ensemble.ctx.delegations.delegation(&addr1, &val_addr_1) { + Some(delegation) => assert_eq!( + delegation, + FullDelegation { + delegator: Addr::unchecked(addr1.to_string()), + validator: val_addr_1.to_string(), + amount: Coin::new(250u128, "uscrt"), + can_redelegate: Coin::new(200u128, "uscrt"), + accumulated_rewards: vec![Coin::new(0u128, "uscrt")], + } + ), + None => panic!("Delegation not found"), + }; + assert_eq!( + ensemble + .ctx + .state + .bank + .query_balances(&addr1, Some("uscrt".to_string())), + vec![Coin::new(50u128, "uscrt")], + ); + + // Fast forward + ensemble.fast_forward_delegation_waits(); + match ensemble.ctx.delegations.delegation(&addr1, &val_addr_1) { + Some(delegation) => assert_eq!( + delegation, + FullDelegation { + delegator: Addr::unchecked(addr1.to_string()), + validator: val_addr_1.to_string(), + amount: Coin::new(250u128, "uscrt"), + can_redelegate: Coin::new(250u128, "uscrt"), + accumulated_rewards: vec![Coin::new(0u128, "uscrt")], + } + ), + None => panic!("Validator 1 delegation not found"), + }; + match ensemble.ctx.delegations.delegation(&addr1, &val_addr_2) { + Some(delegation) => assert_eq!( + delegation, + FullDelegation { + delegator: Addr::unchecked(addr1.to_string()), + validator: val_addr_2.to_string(), + amount: Coin::new(25u128, "uscrt"), + can_redelegate: Coin::new(25u128, "uscrt"), + accumulated_rewards: vec![Coin::new(50u128, "uscrt")], + } + ), + None => panic!("Validator 2 delegation not found"), + }; + + assert_eq!( + ensemble + .ctx + .state + .bank + .query_balances(&addr1, Some("uscrt".to_string())), + vec![Coin::new(875u128, "uscrt")], // 500 undelegate, 325 undelegate, 50 rewards + ); +} diff --git a/ensemble/tests/submsg.rs b/ensemble/tests/submsg.rs new file mode 100644 index 00000000000..945bfcf3543 --- /dev/null +++ b/ensemble/tests/submsg.rs @@ -0,0 +1,1518 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + bin_serde::adapter::SerdeAdapter, + storage::{SingleItem, ItemSpace, TypedKey}, + ensemble::{ + ContractEnsemble, ContractHarness, + MockEnv, AnyResult, + anyhow::{bail, anyhow}, + response::ResponseVariants, EnsembleResult + } +}; +use crate::prelude::*; + +const SENDER: &str = "sender"; +const A_ADDR: &str = "a"; +const B_ADDR: &str = "b"; +const C_ADDR: &str = "c"; + +const EVENT_TYPE: &str = "MSG_ORDER"; + +struct Contract; + +#[derive(Serialize, Deserialize)] +struct InstantiateMsg { + reply_fail_id: Option +} + +#[derive(Serialize, Deserialize)] +enum ExecuteMsg { + RunMsgs(Vec), + IncrNumber(u32), + IncrAndSend { + amount: u32, + recipient: String + }, + Fail, + ReplyResponse(SubMsg), + ReplyData { + id: u64, + data: Binary + } +} + +#[derive(Serialize, Deserialize)] +enum QueryMsg { + State, + Reply(u64) +} + +#[derive(Serialize, Deserialize)] +struct StateResponse { + num: u32, + balance: Coin +} + +crate::namespace!(ReplyDataNs, b"reply_data_"); +const REPLY_DATA: ItemSpace> = ItemSpace::new(); + +crate::namespace!(ReplyNs, b"reply"); +const REPLY_RESP: SingleItem, ReplyNs> = SingleItem::new(); +const REPLIES: ItemSpace, ReplyNs, TypedKey> = ItemSpace::new(); + +impl ContractHarness for Contract { + fn instantiate(&self, deps: DepsMut, _env: Env, _info: MessageInfo, msg: Binary) -> AnyResult { + let msg: InstantiateMsg = from_binary(&msg)?; + + if let Some(id) = msg.reply_fail_id { + storage::save(deps.storage, b"fail", &id)?; + } + + Ok(Response::default().add_attribute("INSTANTIATE", "test")) + } + + fn execute(&self, deps: DepsMut, env: Env, info: MessageInfo, msg: Binary) -> AnyResult { + let msg: ExecuteMsg = from_binary(&msg)?; + let mut resp = Response::default() + .add_attribute("TEST_ATTR", "test") + .add_attribute("ANOTHER_TEST_ATTR", "test") + .add_event( + Event::new(EVENT_TYPE).add_attribute( + "submsg_execute", + format!("sender: {}\ncontract: {}", info.sender, env.contract.address) + ) + ); + + match msg { + ExecuteMsg::RunMsgs(msgs) => { resp = resp.add_submessages(msgs); } + ExecuteMsg::IncrNumber(amount) => { + resp.data = Some(increment_data(amount)); + increment(deps.storage, amount)?; + } + ExecuteMsg::IncrAndSend { amount, recipient } => { + increment(deps.storage, amount)?; + + resp = resp.add_message(BankMsg::Send { + to_address: recipient, + amount: vec![coin(100, "uscrt")] + }) + } + ExecuteMsg::ReplyResponse(msg) => { + REPLY_RESP.save(deps.storage, &msg.into())?; + } + ExecuteMsg::ReplyData { id, data } => { + REPLY_DATA.save(deps.storage, &id, &data)?; + } + ExecuteMsg::Fail => bail!(StdError::generic_err("Fail")) + } + + Ok(resp) + } + + fn query(&self, deps: Deps, env: Env, msg: Binary) -> AnyResult { + let msg: QueryMsg = from_binary(&msg)?; + + let result = match msg { + QueryMsg::State => { + let num: u32 = storage::load(deps.storage, b"num")?.unwrap_or_default(); + let balance = deps.querier.query_balance(env.contract.address, "uscrt")?; + + let resp = StateResponse { + num, + balance + }; + + to_binary(&resp) + }, + QueryMsg::Reply(id) => { + let resp = REPLIES.load(deps.storage, &id)?; + + if let Some(resp) = resp { + to_binary(&resp.0) + } else { + bail!(no_reply_stored_err(id)) + } + } + }; + + result.map_err(|x| anyhow!(x)) + } + + fn reply(&self, deps: DepsMut, env: Env, reply: Reply) -> AnyResult { + let fail_id: Option = storage::load(deps.storage, b"fail")?; + + if let Some(id) = fail_id { + if id == reply.id { + bail!(StdError::generic_err("Failed in reply.")) + } + } + + let result_ok = reply.result.is_ok(); + if let SubMsgResult::Ok(resp) = reply.result { + REPLIES.save(deps.storage, &reply.id, &resp.into())?; + } + + let mut response = Response::default().add_event( + Event::new(EVENT_TYPE) + .add_attribute( + "submsg_reply", + format!( + "address: {}, id: {}, success: {}", + env.contract.address, + reply.id, + result_ok + ) + ) + ); + + if let Some(msg) = REPLY_RESP.load(deps.storage)? { + response.messages.push(msg.0); + REPLY_RESP.remove(deps.storage); + } + + if let Some(data) = REPLY_DATA.load(deps.storage, &reply.id)? { + response.data = Some(data); + } + + Ok(response) + } +} + +fn increment(storage: &mut dyn Storage, amount: u32) -> StdResult<()> { + let mut num: u32 = storage::load(storage, b"num")?.unwrap_or_default(); + num += amount; + + storage::save(storage, b"num", &num)?; + + if num > 10 { + Err(StdError::generic_err("Number is bigger than 10.")) + } else { + Ok(()) + } +} + +fn no_reply_stored_err(id: u64) -> StdError { + StdError::generic_err(format!("No reply response for id {}", id)) +} + +fn increment_data(amount: u32) -> Binary { + Binary::from(format!("Increment amount: {}", amount).as_bytes()) +} + +fn reply_data(id: u64) -> Binary { + Binary::from(format!("Overwrite data in reply for id: {}", id).as_bytes()) +} + +struct TestContracts { + ensemble: ContractEnsemble, + a: ContractLink, + b: ContractLink, + c: ContractLink +} + +impl TestContracts { + fn a_state(&self) -> StateResponse { + self.ensemble.query(&self.a.address, &QueryMsg::State).unwrap() + } + + fn b_state(&self) -> StateResponse { + self.ensemble.query(&self.b.address, &QueryMsg::State).unwrap() + } + + fn c_state(&self) -> StateResponse { + self.ensemble.query(&self.c.address, &QueryMsg::State).unwrap() + } + + fn a_events(&self, id: u64) -> EnsembleResult { + self.ensemble.query(&self.a.address, &QueryMsg::Reply(id)) + } + + fn b_events(&self, id: u64) -> EnsembleResult { + self.ensemble.query(&self.b.address, &QueryMsg::Reply(id)) + } + + fn c_events(&self, id: u64) -> EnsembleResult { + self.ensemble.query(&self.c.address, &QueryMsg::Reply(id)) + } +} + +#[test] +fn correct_message_order() { + let mut c = init([None, None, None]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::new(a_msg(&ExecuteMsg::IncrNumber(1))) + ]) + ), + 0 + ), + SubMsg::reply_always(b_msg(&ExecuteMsg::IncrNumber(2)), 1), + SubMsg::new(c_msg(&ExecuteMsg::IncrNumber(3))) + ]); + + // https://github.com/CosmWasm/cosmwasm/blob/main/SEMANTICS.md#order-and-rollback + + // Contract A returns submessages S1 and S2, and message M1. + // Submessage S1 returns message N1. + // The order will be: S1, N1, reply(S1), S2, reply(S2), M1 + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + assert!(next.is_execute()); // S1 + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); // N1 + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.sender, B_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); // reply(S1) + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 0); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); // S2 + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); // reply(S2) + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 1); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); //M1 + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, C_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 1); + + let state = c.b_state(); + assert_eq!(state.num, 2); + + let state = c.c_state(); + assert_eq!(state.num, 3); +} + +#[test] +fn replies_chain_correctly() { + let mut c = init([None, None, None]); + + c.ensemble.execute( + &ExecuteMsg::ReplyData { id: 1, data: reply_data(1) }, + MockEnv::new(SENDER, c.b.address.clone()) + ).unwrap(); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always(a_msg(&ExecuteMsg::IncrNumber(1)), 0) + ]) + ), + 1 + ), + SubMsg::reply_always(b_msg(&ExecuteMsg::IncrNumber(2)), 2), + SubMsg::new(c_msg(&ExecuteMsg::IncrNumber(3))) + ]); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.sender, B_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.reply.id, 0); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 1); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 2); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, C_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 1); + + let state = c.b_state(); + assert_eq!(state.num, 2); + + let state = c.c_state(); + assert_eq!(state.num, 3); + + cmp_response_json( + &c.b_events(0).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: a\",\"encrypted\":true}]}],\"data\":\"SW5jcmVtZW50IGFtb3VudDogMQ==\"}" + ); + + cmp_response_json( + &c.a_events(1).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: a\\ncontract: b\",\"encrypted\":true}]},{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: a\",\"encrypted\":true}]},{\"type\":\"reply\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_reply\",\"value\":\"address: b, id: 0, success: true\",\"encrypted\":true}]}],\"data\":null}" + ); + + cmp_response_json( + &c.a_events(2).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: a\\ncontract: b\",\"encrypted\":true}]}],\"data\":\"SW5jcmVtZW50IGFtb3VudDogMg==\"}" + ); + + assert_eq!( + c.a_events(3).unwrap_err().to_string(), + no_reply_stored_err(3).to_string() + ) +} + +#[test] +fn last_data_from_reply_is_passed_to_caller() { + let mut c = init([None, None, None]); + + c.ensemble.execute( + &ExecuteMsg::ReplyData { id: 0, data: reply_data(0) }, + MockEnv::new(SENDER, c.b.address.clone()) + ).unwrap(); + + c.ensemble.execute( + &ExecuteMsg::ReplyData { id: 1, data: reply_data(1) }, + MockEnv::new(SENDER, c.b.address.clone()) + ).unwrap(); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always(a_msg(&ExecuteMsg::IncrNumber(1)), 0), + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(2)), 1) + ]) + ), + 2 + ) + ]); + + c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + + cmp_response_json( + &c.b_events(0).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: a\",\"encrypted\":true}]}],\"data\":\"SW5jcmVtZW50IGFtb3VudDogMQ==\"}" + ); + + cmp_response_json( + &c.b_events(1).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]}],\"data\":\"SW5jcmVtZW50IGFtb3VudDogMg==\"}" + ); + + cmp_response_json( + &c.a_events(2).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: a\\ncontract: b\",\"encrypted\":true}]},{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: a\",\"encrypted\":true}]},{\"type\":\"reply\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_reply\",\"value\":\"address: b, id: 0, success: true\",\"encrypted\":true}]},{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]},{\"type\":\"reply\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_reply\",\"value\":\"address: b, id: 1, success: true\",\"encrypted\":true}]}],\"data\":\"T3ZlcndyaXRlIGRhdGEgaW4gcmVwbHkgZm9yIGlkOiAx\"}" + ); +} + +#[test] +fn reverts_state_when_a_single_message_in_a_submsg_chain_fails() { + let mut c = init([None, None, None]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::new(c_msg(&ExecuteMsg::IncrNumber(1))), + SubMsg::new(c_msg(&ExecuteMsg::Fail)) + ]) + ), + 0 + ), + SubMsg::reply_always(b_msg(&ExecuteMsg::IncrNumber(2)), 1) + ]); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + // reply(A) - ID: 0 - notice that even though it was successful, + // the first sub-message is not included because all state was reverted + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 0); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 1); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 0); + + let state = c.b_state(); + assert_eq!(state.num, 2); + + let state = c.c_state(); + assert_eq!(state.num, 0); +} + +#[test] +fn only_successful_submsg_state_is_committed() { + let mut c = init([None, None, None]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(1)), 0), + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(12)), 1) // This will fail + ]) + ), + 2 + ), + SubMsg::new(b_msg(&ExecuteMsg::IncrNumber(2))) + ]); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, C_ADDR); + assert_eq!(resp.sender, B_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.reply.id, 0); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.reply.id, 1); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 2); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 0); + + let state = c.b_state(); + assert_eq!(state.num, 2); + + let state = c.c_state(); + assert_eq!(state.num, 1); + + cmp_response_json( + &c.b_events(0).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]}],\"data\":\"SW5jcmVtZW50IGFtb3VudDogMQ==\"}" + ); + + assert_eq!( + c.b_events(1).unwrap_err().to_string(), + no_reply_stored_err(1).to_string() + ); + + cmp_response_json( + &c.a_events(2).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: a\\ncontract: b\",\"encrypted\":true}]},{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]},{\"type\":\"reply\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_reply\",\"value\":\"address: b, id: 0, success: true\",\"encrypted\":true}]},{\"type\":\"reply\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_reply\",\"value\":\"address: b, id: 1, success: false\",\"encrypted\":true}]}],\"data\":null}" + ); +} + +#[test] +fn reverts_state_when_reply_in_submsg_fails() { + let mut c = init([None, Some(1), None]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(1)), 0), + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(1)), 1), + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(1)), 2), + ]) + ), + 3 + ), + SubMsg::reply_always(b_msg(&ExecuteMsg::IncrNumber(2)), 4) + ]); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 3); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 4); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 0); + + let state = c.b_state(); + assert_eq!(state.num, 2); + + let state = c.c_state(); + assert_eq!(state.num, 0); +} + +#[test] +fn reply_err_in_root_level_fails_tx() { + let mut c = init([Some(2), None, None]); + c.ensemble.add_funds(C_ADDR, vec![coin(200, "uscrt")]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + c_msg(&ExecuteMsg::IncrAndSend { + amount: 1, recipient: A_ADDR.into() + }), + 0 + ), + SubMsg::reply_always(c_msg(&ExecuteMsg::Fail), 1), + ]) + ), + 2 + ), + SubMsg::reply_always(b_msg(&ExecuteMsg::IncrNumber(2)), 3), + SubMsg::new(c_msg(&ExecuteMsg::IncrNumber(5))) + ]); + + let err = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap_err(); + assert_eq!(err.to_string(), "Generic error: Failed in reply."); + + let state = c.a_state(); + assert_eq!(state.num, 0); + assert_eq!(state.balance.amount.u128(), 0); + + let state = c.b_state(); + assert_eq!(state.num, 0); + assert_eq!(state.balance.amount.u128(), 0); + + let state = c.c_state(); + assert_eq!(state.num, 0); + assert_eq!(state.balance.amount.u128(), 200); +} + +#[test] +fn errors_are_handled_in_submsg_reply_state_is_committed() { + let mut c = init([None, None, None]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::new( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(1)), 0), + SubMsg::reply_always(c_msg(&ExecuteMsg::Fail), 1), + ]) + ) + ), + SubMsg::new(b_msg(&ExecuteMsg::IncrNumber(3))) + ]); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, C_ADDR); + assert_eq!(resp.sender, B_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.reply.id, 0); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.reply.id, 1); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 0); + + let state = c.b_state(); + assert_eq!(state.num, 3); + + let state = c.c_state(); + assert_eq!(state.num, 1); +} + +#[test] +fn errors_in_middle_of_submsg_scope_are_handled_and_execution_continues() { + let mut c = init([None, None, None]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::new( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always(c_msg(&ExecuteMsg::Fail), 0), + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(1)), 1) + ]) + ) + ), + SubMsg::new(b_msg(&ExecuteMsg::IncrNumber(3))) + ]); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.reply.id, 0); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, C_ADDR); + assert_eq!(resp.sender, B_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.reply.id, 1); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 0); + + let state = c.b_state(); + assert_eq!(state.num, 3); + + let state = c.c_state(); + assert_eq!(state.num, 1); +} + +#[test] +fn unhandled_error_in_submsg_is_bubbled_up_to_the_caller() { + let mut c = init([None, None, None]); + c.ensemble.add_funds(C_ADDR, vec![coin(200, "uscrt")]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + c_msg(&ExecuteMsg::IncrAndSend { + amount: 1, + recipient: A_ADDR.into() + }), + 0 + ), + SubMsg::new(c_msg(&ExecuteMsg::Fail)), + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(1)), 1), + ]) + ), + 2 + ), + SubMsg::new( + c_msg(&ExecuteMsg::IncrAndSend { + amount: 3, + recipient: B_ADDR.into() + }) + ), + ]); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 2); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, C_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_bank()); + + if let ResponseVariants::Bank(resp) = next { + assert_eq!(resp.receiver, B_ADDR); + assert_eq!(resp.sender, C_ADDR); + assert_eq!(resp.coins, vec![coin(100, "uscrt")]); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 0); + assert_eq!(state.balance.amount.u128(), 0); + + let state = c.b_state(); + assert_eq!(state.num, 0); + assert_eq!(state.balance.amount.u128(), 100); + + let state = c.c_state(); + assert_eq!(state.num, 3); + assert_eq!(state.balance.amount.u128(), 100); +} + +#[test] +fn unhandled_error_in_nested_message_fails_tx() { + let mut c = init([None, None, None]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::new( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(1)), 0), + SubMsg::new(c_msg(&ExecuteMsg::Fail)) + ]) + ) + ), + SubMsg::new(b_msg(&ExecuteMsg::IncrNumber(3))) + ]); + + let err = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap_err(); + assert_eq!(err.to_string(), "Generic error: Fail"); + + let state = c.a_state(); + assert_eq!(state.num, 0); + + let state = c.b_state(); + assert_eq!(state.num, 0); + + let state = c.c_state(); + assert_eq!(state.num, 0); +} + +#[test] +fn error_bubbles_multiple_levels_up_the_stack() { + let mut c = init([None, None, None]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_on_error( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(1)), 0), + SubMsg::new(c_msg(&ExecuteMsg::RunMsgs(vec![ + SubMsg::new(b_msg(&ExecuteMsg::IncrNumber(1))), + SubMsg::reply_on_success(a_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::new(b_msg(&ExecuteMsg::IncrNumber(15))) // This will fail + ]) + ), + 1), + ]))), + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(1)), 2), + ]) + ), + 3 + ), + SubMsg::new(b_msg(&ExecuteMsg::IncrNumber(3))) + ]); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 3); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 0); + + let state = c.b_state(); + assert_eq!(state.num, 3); + + let state = c.c_state(); + assert_eq!(state.num, 0); + + assert_eq!( + c.b_events(0).unwrap_err().to_string(), + no_reply_stored_err(0).to_string() + ); + + assert_eq!( + c.c_events(1).unwrap_err().to_string(), + no_reply_stored_err(1).to_string() + ); + + assert_eq!( + c.b_events(2).unwrap_err().to_string(), + no_reply_stored_err(2).to_string() + ); + + assert_eq!( + c.a_events(3).unwrap_err().to_string(), + no_reply_stored_err(3).to_string() + ); +} + +#[test] +fn unhandled_error_jumps_to_the_first_reply() { + let mut c = init([None, None, None]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_on_success( + c_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_on_success(a_msg(&ExecuteMsg::IncrNumber(1)), 0), + SubMsg::new(b_msg(&ExecuteMsg::Fail)) + ]) + ), + 1 + ), + SubMsg::new(c_msg(&ExecuteMsg::IncrNumber(1))) + ]) + ), + 2 + ), + SubMsg::new(b_msg(&ExecuteMsg::IncrNumber(2))) + ]); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 2); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 0); + + let state = c.b_state(); + assert_eq!(state.num, 2); + + let state = c.c_state(); + assert_eq!(state.num, 0); + + assert_eq!( + c.c_events(0).unwrap_err().to_string(), + no_reply_stored_err(0).to_string() + ); + + assert_eq!( + c.b_events(1).unwrap_err().to_string(), + no_reply_stored_err(1).to_string() + ); + + assert_eq!( + c.a_events(2).unwrap_err().to_string(), + no_reply_stored_err(2).to_string() + ); +} + +#[test] +fn reply_responses_are_handled_correctly() { + let mut c = init([None, None, None]); + + let msg = ExecuteMsg::ReplyResponse( + SubMsg::reply_always( + a_msg(&ExecuteMsg::IncrNumber(1)), + 2 + ) + ); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.b.address.clone())).unwrap(); + assert!(resp.sent.is_empty()); + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, SENDER); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_on_success(c_msg(&ExecuteMsg::IncrNumber(1)), 0), + SubMsg::reply_on_success(c_msg(&ExecuteMsg::IncrNumber(1)), 1), + ]) + ), + 3 + ), + SubMsg::new(c_msg(&ExecuteMsg::IncrNumber(3))), + SubMsg::reply_always(b_msg(&ExecuteMsg::IncrNumber(2)), 4) + ]); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, C_ADDR); + assert_eq!(resp.sender, B_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.reply.id, 0); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.sender, B_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.reply.id, 2); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, C_ADDR); + assert_eq!(resp.sender, B_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.reply.id, 1); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 3); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, C_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 4); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 1); + + let state = c.b_state(); + assert_eq!(state.num, 2); + + let state = c.c_state(); + assert_eq!(state.num, 5); + + cmp_response_json( + &c.b_events(0).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]}],\"data\":\"SW5jcmVtZW50IGFtb3VudDogMQ==\"}" + ); + + cmp_response_json( + &c.b_events(1).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]}],\"data\":\"SW5jcmVtZW50IGFtb3VudDogMQ==\"}" + ); + + cmp_response_json( + &c.b_events(2).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: a\",\"encrypted\":true}]}],\"data\":\"SW5jcmVtZW50IGFtb3VudDogMQ==\"}" + ); + + cmp_response_json( + &c.a_events(3).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: a\\ncontract: b\",\"encrypted\":true}]},{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]},{\"type\":\"reply\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_reply\",\"value\":\"address: b, id: 0, success: true\",\"encrypted\":true}]},{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"a\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: a\",\"encrypted\":true}]},{\"type\":\"reply\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_reply\",\"value\":\"address: b, id: 2, success: true\",\"encrypted\":true}]},{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]},{\"type\":\"reply\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_reply\",\"value\":\"address: b, id: 1, success: true\",\"encrypted\":true}]}],\"data\":null}" + ); + + cmp_response_json( + &c.a_events(4).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: a\\ncontract: b\",\"encrypted\":true}]}],\"data\":\"SW5jcmVtZW50IGFtb3VudDogMg==\"}" + ); +} + +#[test] +fn reply_response_error_is_handled_properly() { + let mut c = init([None, Some(2), None]); + + let msg = ExecuteMsg::ReplyResponse( + SubMsg::reply_always( + a_msg(&ExecuteMsg::RunMsgs(vec![ + SubMsg::new(c_msg(&ExecuteMsg::IncrNumber(1))), + SubMsg::reply_on_success(b_msg(&ExecuteMsg::IncrNumber(20)), 1) // This will fail + ])), + 2 + ) + ); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.b.address.clone())).unwrap(); + assert!(resp.sent.is_empty()); + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, SENDER); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_on_success(c_msg(&ExecuteMsg::IncrNumber(1)), 0), + SubMsg::new(c_msg(&ExecuteMsg::IncrNumber(1))) + ]) + ), + 3 + ), + SubMsg::reply_always(b_msg(&ExecuteMsg::IncrNumber(2)), 4) + ]); + + let resp = c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + let mut resp = resp.iter(); + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 3); + } + + let next = resp.next().unwrap(); + assert!(next.is_execute()); + + if let ResponseVariants::Execute(resp) = next { + assert_eq!(resp.address, B_ADDR); + assert_eq!(resp.sender, A_ADDR); + } + + let next = resp.next().unwrap(); + assert!(next.is_reply()); + + if let ResponseVariants::Reply(resp) = next { + assert_eq!(resp.address, A_ADDR); + assert_eq!(resp.reply.id, 4); + } + + assert_eq!(resp.next(), None); + + let state = c.a_state(); + assert_eq!(state.num, 0); + + let state = c.b_state(); + assert_eq!(state.num, 2); + + let state = c.c_state(); + assert_eq!(state.num, 0); + + assert_eq!( + c.b_events(0).unwrap_err().to_string(), + no_reply_stored_err(0).to_string() + ); + + assert_eq!( + c.b_events(2).unwrap_err().to_string(), + no_reply_stored_err(2).to_string() + ); + + assert_eq!( + c.a_events(1).unwrap_err().to_string(), + no_reply_stored_err(1).to_string() + ); + + assert_eq!( + c.a_events(3).unwrap_err().to_string(), + no_reply_stored_err(3).to_string() + ); + + cmp_response_json( + &c.a_events(4).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: a\\ncontract: b\",\"encrypted\":true}]}],\"data\":\"SW5jcmVtZW50IGFtb3VudDogMg==\"}" + ); +} + +#[test] +fn correct_events_passed_to_reply() { + let mut c = init([None, None, None]); + c.ensemble.add_funds(C_ADDR, vec![coin(200, "uscrt")]); + + let msg = ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + b_msg( + &ExecuteMsg::RunMsgs(vec![ + SubMsg::reply_always( + c_msg(&ExecuteMsg::IncrAndSend { + amount: 1, + recipient: A_ADDR.into() + }), + 0 + ), + SubMsg::reply_on_error(c_msg(&ExecuteMsg::Fail), 1), + SubMsg::reply_always(c_msg(&ExecuteMsg::IncrNumber(1)), 2), + ]) + ), + 3 + ), + SubMsg::reply_always( + c_msg(&ExecuteMsg::IncrAndSend { + amount: 3, + recipient: B_ADDR.into() + }), + 4 + ), + ]); + + c.ensemble.execute(&msg, MockEnv::new(SENDER, c.a.address.clone())).unwrap(); + + let state = c.a_state(); + assert_eq!(state.num, 0); + assert_eq!(state.balance.amount.u128(), 100); + + let state = c.b_state(); + assert_eq!(state.num, 0); + assert_eq!(state.balance.amount.u128(), 100); + + let state = c.c_state(); + assert_eq!(state.num, 5); + assert_eq!(state.balance.amount.u128(), 0); + + cmp_response_json( + &c.b_events(0).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]},{\"type\":\"coin_spent\",\"attributes\":[{\"key\":\"amount\",\"value\":\"100uscrt\",\"encrypted\":true},{\"key\":\"spender\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"coin_received\",\"attributes\":[{\"key\":\"amount\",\"value\":\"100uscrt\",\"encrypted\":true},{\"key\":\"receiver\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"amount\",\"value\":\"100uscrt\",\"encrypted\":true},{\"key\":\"recipient\",\"value\":\"a\",\"encrypted\":true},{\"key\":\"sender\",\"value\":\"c\",\"encrypted\":true}]}],\"data\":null}" + ); + + assert_eq!( + c.b_events(1).unwrap_err().to_string(), + no_reply_stored_err(1).to_string() + ); + + cmp_response_json( + &c.b_events(2).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]}],\"data\":\"SW5jcmVtZW50IGFtb3VudDogMQ==\"}" + ); + + cmp_response_json( + &c.a_events(3).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: a\\ncontract: b\",\"encrypted\":true}]},{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]},{\"type\":\"coin_spent\",\"attributes\":[{\"key\":\"amount\",\"value\":\"100uscrt\",\"encrypted\":true},{\"key\":\"spender\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"coin_received\",\"attributes\":[{\"key\":\"amount\",\"value\":\"100uscrt\",\"encrypted\":true},{\"key\":\"receiver\",\"value\":\"a\",\"encrypted\":true}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"amount\",\"value\":\"100uscrt\",\"encrypted\":true},{\"key\":\"recipient\",\"value\":\"a\",\"encrypted\":true},{\"key\":\"sender\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"reply\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_reply\",\"value\":\"address: b, id: 0, success: true\",\"encrypted\":true}]},{\"type\":\"reply\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_reply\",\"value\":\"address: b, id: 1, success: false\",\"encrypted\":true}]},{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: b\\ncontract: c\",\"encrypted\":true}]},{\"type\":\"reply\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"submsg_reply\",\"value\":\"address: b, id: 2, success: true\",\"encrypted\":true}]}],\"data\":null}" + ); + + cmp_response_json( + &c.a_events(4).unwrap(), + "{\"events\":[{\"type\":\"execute\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm\",\"attributes\":[{\"key\":\"ANOTHER_TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"TEST_ATTR\",\"value\":\"test\",\"encrypted\":true},{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"wasm-MSG_ORDER\",\"attributes\":[{\"key\":\"contract_address\",\"value\":\"c\",\"encrypted\":true},{\"key\":\"submsg_execute\",\"value\":\"sender: a\\ncontract: c\",\"encrypted\":true}]},{\"type\":\"coin_spent\",\"attributes\":[{\"key\":\"amount\",\"value\":\"100uscrt\",\"encrypted\":true},{\"key\":\"spender\",\"value\":\"c\",\"encrypted\":true}]},{\"type\":\"coin_received\",\"attributes\":[{\"key\":\"amount\",\"value\":\"100uscrt\",\"encrypted\":true},{\"key\":\"receiver\",\"value\":\"b\",\"encrypted\":true}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"amount\",\"value\":\"100uscrt\",\"encrypted\":true},{\"key\":\"recipient\",\"value\":\"b\",\"encrypted\":true},{\"key\":\"sender\",\"value\":\"c\",\"encrypted\":true}]}],\"data\":null}" + ); +} + +fn init(msgs: [Option; 3]) -> TestContracts { + let mut ensemble = ContractEnsemble::new(); + let contract = ensemble.register(Box::new(Contract)); + + let a = ensemble.instantiate( + contract.id, + &InstantiateMsg { + reply_fail_id: msgs[0] + }, + MockEnv::new(SENDER, A_ADDR) + ).unwrap(); + + let b = ensemble.instantiate( + contract.id, + &InstantiateMsg { + reply_fail_id: msgs[1] + }, + MockEnv::new(SENDER, B_ADDR) + ).unwrap(); + + let c = ensemble.instantiate( + contract.id, + &InstantiateMsg { + reply_fail_id: msgs[2] + }, + MockEnv::new(SENDER, C_ADDR) + ).unwrap(); + + TestContracts { + ensemble, + a: a.instance, + b: b.instance, + c: c.instance + } +} + +fn a_msg(msg: &ExecuteMsg) -> WasmMsg { + WasmMsg::Execute { + contract_addr: A_ADDR.into(), + code_hash: "test_contract_0".into(), + msg: to_binary(msg).unwrap(), + funds: vec![] + } +} + +fn b_msg(msg: &ExecuteMsg) -> WasmMsg { + WasmMsg::Execute { + contract_addr: B_ADDR.into(), + code_hash: "test_contract_0".into(), + msg: to_binary(msg).unwrap(), + funds: vec![] + } +} + +fn c_msg(msg: &ExecuteMsg) -> WasmMsg { + WasmMsg::Execute { + contract_addr: C_ADDR.into(), + code_hash: "test_contract_0".into(), + msg: to_binary(msg).unwrap(), + funds: vec![] + } +} + +fn cmp_response_json(resp: &SubMsgResponse, json: &'static str) { + assert_eq!( + String::from_utf8(to_vec(&resp).unwrap()).unwrap(), + json + ) +}