diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index bbe1fc95..00000000 --- a/.cargo/config +++ /dev/null @@ -1,8 +0,0 @@ -[alias] -# Temporarily removed the backtraces feature from the unit-test run due to compilation errors in -# the cosmwasm-std package: -# cosmwasm-std = { git = "https://github.com/scrtlabs/cosmwasm", branch = "secret" } -# unit-test = "test --lib --features backtraces" -unit-test = "test --lib" -integration-test = "test --test integration" -schema = "run --example schema" diff --git a/.cargo/config b/.cargo/config new file mode 120000 index 00000000..ab8b69cb --- /dev/null +++ b/.cargo/config @@ -0,0 +1 @@ +config.toml \ No newline at end of file diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..0bf94f78 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,12 @@ +[alias] +# Temporarily removed the backtraces feature from the unit-test run due to compilation errors in +# the cosmwasm-std package: +# cosmwasm-std = { git = "https://github.com/scrtlabs/cosmwasm", branch = "secret" } +# unit-test = "test --lib --features backtraces" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example schema" + +[features] +gas_tracking = [] +gas_evaporation = [] diff --git a/.gitignore b/.gitignore index ea00b0b0..83987d19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Build results /target +/tests/dwb/dist/ contract.wasm contract.wasm.gz @@ -19,3 +20,10 @@ contract.wasm.gz # IDEs *.iml .idea + +# Packages +node_modules/ + +# Private +.env +scrap/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..0827ff3b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,36 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "bun", + "internalConsoleOptions": "neverOpen", + "request": "launch", + "name": "Debug File", + "program": "${file}", + "cwd": "${workspaceFolder}", + "stopOnEntry": false, + "watchMode": false + }, + { + "type": "bun", + "internalConsoleOptions": "neverOpen", + "request": "launch", + "name": "Run File", + "program": "${file}", + "cwd": "${workspaceFolder}", + "noDebug": true, + "watchMode": false + }, + { + "type": "bun", + "internalConsoleOptions": "neverOpen", + "request": "attach", + "name": "Attach Bun", + "url": "ws://localhost:6499/", + "stopOnEntry": false + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7dae7aa1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,33 @@ +{ + "eslint.enable": true, + "editor.fontSize": 11, + "scm.inputFontSize": 11, + "debug.console.fontSize": 10, + "markdown.preview.fontSize": 11, + "terminal.integrated.fontSize": 10, + "files.exclude": { + "dist": true, + "submodules": true, + "**/.git": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/node_modules": true + }, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.preferences.quoteStyle": "single", + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": false, + "eslint.workingDirectories": [ + "./tests/dwb/src", + ], + "eslint.validate": [ + "javascript", + "typescript", + ], + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + } +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8e3ff6b2..d66b8a9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ "getrandom", "once_cell", @@ -27,9 +37,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -73,9 +83,15 @@ dependencies = [ [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "cfg-if" @@ -84,36 +100,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "const-oid" -version = "0.9.2" +name = "chacha20" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] [[package]] -name = "cosmwasm-crypto" -version = "1.1.9" -source = "git+https://github.com/scrtlabs/cosmwasm/?tag=v1.1.9-secret#e40a15f04dae80680dbe22aef760e5eaab6b0a19" +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "digest 0.10.6", - "ed25519-zebra", - "k256", - "rand_core 0.6.4", - "thiserror", + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + [[package]] name = "cosmwasm-derive" -version = "1.1.9" -source = "git+https://github.com/scrtlabs/cosmwasm/?tag=v1.1.9-secret#e40a15f04dae80680dbe22aef760e5eaab6b0a19" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "242e98e7a231c122e08f300d9db3262d1007b51758a8732cd6210b3e9faa4f3a" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.2.2" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b99f612ccf162940ae2eef9f370ee37cf2ddcf4a9a8f5ee15ec6b46a5ecd2e" +checksum = "7879036156092ad1c22fe0d7316efc5a5eceec2bc3906462a2560215f2a2f929" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -124,47 +170,20 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.2.2" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92ceea61033cb69c336abf673da017ddf251fc4e26e0cdd387eaf8bedb14e49" +checksum = "0bb57855fbfc83327f8445ae0d413b1a05ac0d68c396ab4d122b2abd7bb82cb6" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] -[[package]] -name = "cosmwasm-std" -version = "1.1.9" -source = "git+https://github.com/scrtlabs/cosmwasm/?tag=v1.1.9-secret#e40a15f04dae80680dbe22aef760e5eaab6b0a19" -dependencies = [ - "base64 0.13.1", - "cosmwasm-crypto", - "cosmwasm-derive", - "derivative", - "forward_ref", - "hex", - "schemars", - "serde", - "serde-json-wasm", - "thiserror", - "uint", -] - -[[package]] -name = "cosmwasm-storage" -version = "1.1.9" -source = "git+https://github.com/scrtlabs/cosmwasm/?tag=v1.1.9-secret#e40a15f04dae80680dbe22aef760e5eaab6b0a19" -dependencies = [ - "cosmwasm-std", - "serde", -] - [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -194,6 +213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -242,9 +262,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", @@ -253,9 +273,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.11" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecdsa" @@ -293,7 +313,7 @@ dependencies = [ "base16ct", "crypto-bigint", "der", - "digest 0.10.6", + "digest 0.10.7", "ff", "generic-array", "group", @@ -314,6 +334,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "static_assertions", +] + [[package]] name = "forward_ref" version = "1.0.0" @@ -322,9 +351,9 @@ checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -332,9 +361,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -367,20 +396,38 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", ] [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "k256" @@ -391,26 +438,56 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "sha2 0.10.6", + "sha2 0.10.8", ] [[package]] name = "libc" -version = "0.2.140" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "minicbor" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a20020e8e2d1881d8736f64011bb5ff99f1db9947ce3089706945c8915695cb" +dependencies = [ + "minicbor-derive", +] + +[[package]] +name = "minicbor-derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8608fb1c805b5b6b3d5ab7bd95c40c396df622b64d77b2d621a5eae1eed050ee" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "minicbor-ser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0834b86a9c56311671913d56f640d7f0b6da803df61121661cc890f0edc0eb1" +dependencies = [ + "minicbor", + "serde", +] [[package]] name = "once_cell" -version = "1.17.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "pkcs8" @@ -422,26 +499,47 @@ dependencies = [ "spki", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "uint", +] + [[package]] name = "proc-macro2" -version = "1.0.52" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -482,13 +580,13 @@ dependencies = [ [[package]] name = "remain" -version = "0.2.7" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f4b7d9b4676922ecbbad6d317e0f847762c4b28b935a2db3b44bd4f36c1aa7f" +checksum = "46aef80f842736de545ada6ec65b81ee91504efd6853f4b96de7414c42ae7443" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] @@ -508,20 +606,20 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schemars" -version = "0.8.12" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "schemars_derive", @@ -531,14 +629,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.12" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] @@ -555,16 +653,75 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + +[[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 0.13.1", + "cosmwasm-derive", + "derivative", + "forward_ref", + "hex", + "schemars", + "secret-cosmwasm-crypto", + "serde", + "serde-json-wasm", + "thiserror", + "uint", +] + +[[package]] +name = "secret-cosmwasm-storage" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb43da2cb72a53b16ea1555bca794fb828b48ab24ebeb45f8e26f1881c45a783" +dependencies = [ + "secret-cosmwasm-std", + "serde", +] + [[package]] name = "secret-toolkit" -version = "0.8.0" -source = "git+https://github.com/scrtlabs/secret-toolkit?rev=9b74bdac71c2fedcc12246f18cdfdd94b8991282#9b74bdac71c2fedcc12246f18cdfdd94b8991282" +version = "0.10.0" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" dependencies = [ "secret-toolkit-crypto", + "secret-toolkit-notification", "secret-toolkit-permit", "secret-toolkit-serialization", - "secret-toolkit-snip20", - "secret-toolkit-snip721", "secret-toolkit-storage", "secret-toolkit-utils", "secret-toolkit-viewing-key", @@ -572,93 +729,91 @@ dependencies = [ [[package]] name = "secret-toolkit-crypto" -version = "0.8.0" -source = "git+https://github.com/scrtlabs/secret-toolkit?rev=9b74bdac71c2fedcc12246f18cdfdd94b8991282#9b74bdac71c2fedcc12246f18cdfdd94b8991282" +version = "0.10.0" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" dependencies = [ - "cosmwasm-std", + "hkdf", "rand_chacha", "rand_core 0.6.4", - "sha2 0.10.6", + "secp256k1", + "secret-cosmwasm-std", + "sha2 0.10.8", ] [[package]] -name = "secret-toolkit-permit" -version = "0.8.0" -source = "git+https://github.com/scrtlabs/secret-toolkit?rev=9b74bdac71c2fedcc12246f18cdfdd94b8991282#9b74bdac71c2fedcc12246f18cdfdd94b8991282" +name = "secret-toolkit-notification" +version = "0.10.0" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" dependencies = [ - "bech32", - "cosmwasm-std", - "remain", + "chacha20poly1305", + "generic-array", + "hkdf", + "minicbor-ser", + "primitive-types", "ripemd", "schemars", + "secret-cosmwasm-std", "secret-toolkit-crypto", "serde", + "sha2 0.10.8", ] [[package]] -name = "secret-toolkit-serialization" -version = "0.8.0" -source = "git+https://github.com/scrtlabs/secret-toolkit?rev=9b74bdac71c2fedcc12246f18cdfdd94b8991282#9b74bdac71c2fedcc12246f18cdfdd94b8991282" -dependencies = [ - "bincode2", - "cosmwasm-std", - "schemars", - "serde", -] - -[[package]] -name = "secret-toolkit-snip20" -version = "0.8.0" -source = "git+https://github.com/scrtlabs/secret-toolkit?rev=9b74bdac71c2fedcc12246f18cdfdd94b8991282#9b74bdac71c2fedcc12246f18cdfdd94b8991282" +name = "secret-toolkit-permit" +version = "0.10.0" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" dependencies = [ - "cosmwasm-std", + "bech32", + "remain", + "ripemd", "schemars", - "secret-toolkit-utils", + "secret-cosmwasm-std", + "secret-toolkit-crypto", "serde", ] [[package]] -name = "secret-toolkit-snip721" -version = "0.8.0" -source = "git+https://github.com/scrtlabs/secret-toolkit?rev=9b74bdac71c2fedcc12246f18cdfdd94b8991282#9b74bdac71c2fedcc12246f18cdfdd94b8991282" +name = "secret-toolkit-serialization" +version = "0.10.0" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" dependencies = [ - "cosmwasm-std", + "bincode2", "schemars", - "secret-toolkit-utils", + "secret-cosmwasm-std", "serde", ] [[package]] name = "secret-toolkit-storage" -version = "0.8.0" -source = "git+https://github.com/scrtlabs/secret-toolkit?rev=9b74bdac71c2fedcc12246f18cdfdd94b8991282#9b74bdac71c2fedcc12246f18cdfdd94b8991282" +version = "0.10.0" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" dependencies = [ - "cosmwasm-std", - "cosmwasm-storage", + "secret-cosmwasm-std", + "secret-cosmwasm-storage", "secret-toolkit-serialization", "serde", ] [[package]] name = "secret-toolkit-utils" -version = "0.8.0" -source = "git+https://github.com/scrtlabs/secret-toolkit?rev=9b74bdac71c2fedcc12246f18cdfdd94b8991282#9b74bdac71c2fedcc12246f18cdfdd94b8991282" +version = "0.10.0" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" dependencies = [ - "cosmwasm-std", - "cosmwasm-storage", "schemars", + "secret-cosmwasm-std", + "secret-cosmwasm-storage", "serde", ] [[package]] name = "secret-toolkit-viewing-key" -version = "0.8.0" -source = "git+https://github.com/scrtlabs/secret-toolkit?rev=9b74bdac71c2fedcc12246f18cdfdd94b8991282#9b74bdac71c2fedcc12246f18cdfdd94b8991282" +version = "0.10.0" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" dependencies = [ - "base64 0.21.0", - "cosmwasm-std", - "cosmwasm-storage", + "base64 0.21.7", "schemars", + "secret-cosmwasm-std", + "secret-cosmwasm-storage", "secret-toolkit-crypto", "secret-toolkit-utils", "serde", @@ -667,13 +822,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.158" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde-json-wasm" version = "0.4.1" @@ -685,31 +849,31 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.158" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.66", ] [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -731,13 +895,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -746,23 +910,28 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "rand_core 0.6.4", ] [[package]] name = "snip20-reference-impl" -version = "1.0.0" +version = "2.0.0" dependencies = [ - "base64 0.21.0", + "base64 0.21.7", + "constant_time_eq", "cosmwasm-schema", - "cosmwasm-std", - "cosmwasm-storage", + "minicbor-ser", + "primitive-types", "rand", "schemars", + "secret-cosmwasm-std", + "secret-cosmwasm-storage", "secret-toolkit", "secret-toolkit-crypto", "serde", + "serde-big-array", + "static_assertions", ] [[package]] @@ -783,9 +952,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -800,9 +969,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.4" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -811,29 +980,29 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.39" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.39" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uint" @@ -849,9 +1018,19 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "universal-hash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] [[package]] name = "version_check" @@ -867,6 +1046,6 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "zeroize" -version = "1.5.7" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index e6435002..b63d7f2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "snip20-reference-impl" -version = "1.0.0" -authors = ["Itzik <itzik@keytango.io>"] +version = "2.0.0" +authors = ["@reuvenpo","@toml01","@assafmo","@liorbond","Itzik <itzik@keytango.io>","@darwinzer0","@supdoggie"] edition = "2021" exclude = [ # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. @@ -29,24 +29,27 @@ overflow-checks = true # for more explicit tests, cargo test --features=backtraces #default = ["debug-print"] backtraces = ["cosmwasm-std/backtraces"] +gas_tracking = [] +gas_evaporation = [] # debug-print = ["cosmwasm-std/debug-print"] [dependencies] -cosmwasm-std = { git = "https://github.com/scrtlabs/cosmwasm/", default-features = false, tag = "v1.1.9-secret" } -cosmwasm-storage = { git = "https://github.com/scrtlabs/cosmwasm/", tag = "v1.1.9-secret" } +cosmwasm-std = { package = "secret-cosmwasm-std", version = "1.1.11" } +cosmwasm-storage = { package = "secret-cosmwasm-storage", version = "1.1.11" } rand = { version = "0.8.5", default-features = false } -secret-toolkit = { git = "https://github.com/scrtlabs/secret-toolkit", features = [ - "permit", - "viewing-key", -], rev = "9b74bdac71c2fedcc12246f18cdfdd94b8991282" } -secret-toolkit-crypto = { git = "https://github.com/scrtlabs/secret-toolkit", features = [ - "rand", - "hash", -], rev = "9b74bdac71c2fedcc12246f18cdfdd94b8991282" } +# secret-toolkit = { version = "0.10.0", default-features = false, features = ["permit", "storage", "viewing-key"] } +secret-toolkit = { git = "https://github.com/SolarRepublic/secret-toolkit.git", default-features = false, features = ["permit", "storage", "viewing-key", "notification"], rev = "8aed92d589dc119f69d20f8538d5a6eea8003d95" } +# secret-toolkit-crypto = { version = "0.10.0", default-features = false, features = ["hash"] } +secret-toolkit-crypto = { git = "https://github.com/SolarRepublic/secret-toolkit.git", default-features = false, features = ["hash"], rev = "8aed92d589dc119f69d20f8538d5a6eea8003d95" } +static_assertions = "1.1.0" schemars = "0.8.12" serde = { version = "1.0.158", default-features = false, features = ["derive"] } +serde-big-array = "0.5.1" base64 = "0.21.0" +constant_time_eq = "0.3.0" +primitive-types = { version = "0.12.2", default-features = false } +minicbor-ser = "0.2.0" [dev-dependencies] cosmwasm-schema = { version = "1.1.8" } diff --git a/Makefile b/Makefile index eee79095..9c425118 100644 --- a/Makefile +++ b/Makefile @@ -51,19 +51,26 @@ _compile: cargo build --target wasm32-unknown-unknown --locked cp ./target/wasm32-unknown-unknown/debug/*.wasm ./contract.wasm +.PHONY: compile-integration _compile-integration +compile-integration: _compile-integration contract.wasm.gz +_compile-integration: + DWB_CAPACITY=64 BTBE_CAPACITY=64 RUSTFLAGS='-C link-arg=-s' cargo build --features "gas_tracking" --release --target wasm32-unknown-unknown + @# The following line is not necessary, may work only on linux (extra size optimization) + wasm-opt -Oz ./target/wasm32-unknown-unknown/release/*.wasm --all-features -o ./contract.wasm + .PHONY: compile-optimized _compile-optimized compile-optimized: _compile-optimized contract.wasm.gz _compile-optimized: RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown @# The following line is not necessary, may work only on linux (extra size optimization) - wasm-opt -Oz ./target/wasm32-unknown-unknown/release/*.wasm -o ./contract.wasm + wasm-opt -Oz ./target/wasm32-unknown-unknown/release/*.wasm --all-features -o ./contract.wasm .PHONY: compile-optimized-reproducible compile-optimized-reproducible: docker run --rm -v "$$(pwd)":/contract \ --mount type=volume,source="$$(basename "$$(pwd)")_cache",target=/code/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - enigmampc/secret-contract-optimizer:1.0.7 + enigmampc/secret-contract-optimizer:1.0.10 contract.wasm.gz: contract.wasm cat ./contract.wasm | gzip -9 > ./contract.wasm.gz @@ -76,7 +83,7 @@ start-server: # CTRL+C to stop docker run -it --rm \ -p 9091:9091 -p 26657:26657 -p 26656:26656 -p 1317:1317 -p 5000:5000 \ -v $$(pwd):/root/code \ - --name secretdev ghcr.io/scrtlabs/localsecret:v1.6.0-alpha.4 + --name secretdev docker pull ghcr.io/scrtlabs/localsecret:v1.13.1 .PHONY: schema schema: diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..60de26dd --- /dev/null +++ b/build.rs @@ -0,0 +1,23 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::Path; + +fn main() { + // config parameters + let dwb_capacity = env::var("DWB_CAPACITY").unwrap_or_else(|_| "64".to_string()); + let btbe_capacity = env::var("BTBE_CAPACITY").unwrap_or_else(|_| "64".to_string()); + + // path to destination config.rs file + let out_dir = env::var("OUT_DIR").expect("Missing OUT_DIR"); + let dest_path = Path::new(&out_dir).join("config.rs"); + + // write constants + let mut file = File::create(&dest_path).expect("Failed to write to config.rs"); + write!(file, "pub const DWB_CAPACITY: u16 = {};\n", dwb_capacity).unwrap(); + write!(file, "pub const BTBE_CAPACITY: u16 = {};\n", btbe_capacity).unwrap(); + + // monitor + println!("cargo:rerun-if-env-changed=DWB_CAPACITY"); + println!("cargo:rerun-if-env-changed=BTBE_CAPACITY"); +} diff --git a/src/batch.rs b/src/batch.rs index dbe47fb1..47b4bb09 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -3,11 +3,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use cosmwasm_std::{Addr, Binary, Uint128}; - -pub trait HasDecoy { - fn decoys(&self) -> &Option<Vec<Addr>>; -} +use cosmwasm_std::{Binary, Uint128}; #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] @@ -15,7 +11,6 @@ pub struct TransferAction { pub recipient: String, pub amount: Uint128, pub memo: Option<String>, - pub decoys: Option<Vec<Addr>>, } #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] @@ -26,7 +21,6 @@ pub struct SendAction { pub amount: Uint128, pub msg: Option<Binary>, pub memo: Option<String>, - pub decoys: Option<Vec<Addr>>, } #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] @@ -36,7 +30,6 @@ pub struct TransferFromAction { pub recipient: String, pub amount: Uint128, pub memo: Option<String>, - pub decoys: Option<Vec<Addr>>, } #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] @@ -48,7 +41,6 @@ pub struct SendFromAction { pub amount: Uint128, pub msg: Option<Binary>, pub memo: Option<String>, - pub decoys: Option<Vec<Addr>>, } #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] @@ -57,7 +49,6 @@ pub struct MintAction { pub recipient: String, pub amount: Uint128, pub memo: Option<String>, - pub decoys: Option<Vec<Addr>>, } #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] @@ -66,22 +57,4 @@ pub struct BurnFromAction { pub owner: String, pub amount: Uint128, pub memo: Option<String>, - pub decoys: Option<Vec<Addr>>, } - -macro_rules! impl_decoyable { - ($struct:ty) => { - impl HasDecoy for $struct { - fn decoys(&self) -> &Option<Vec<Addr>> { - &self.decoys - } - } - }; -} - -impl_decoyable!(BurnFromAction); -impl_decoyable!(MintAction); -impl_decoyable!(SendFromAction); -impl_decoyable!(TransferFromAction); -impl_decoyable!(TransferAction); -impl_decoyable!(SendAction); diff --git a/src/btbe.rs b/src/btbe.rs new file mode 100644 index 00000000..c213b44c --- /dev/null +++ b/src/btbe.rs @@ -0,0 +1,915 @@ +//! BTBE stands for bitwise-trie of bucketed entries + +include!(concat!(env!("OUT_DIR"), "/config.rs")); + +use constant_time_eq::constant_time_eq; +use cosmwasm_std::{CanonicalAddr, StdError, StdResult, Storage}; +use primitive_types::U256; +use secret_toolkit::{ + serialization::{Bincode2, Serde}, + storage::Item, +}; +use secret_toolkit_crypto::hkdf_sha_256; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; + +use crate::state::{safe_add_u64, INTERNAL_SECRET}; +use crate::dwb::{amount_u64, DelayedWriteBufferEntry, TxBundle}; +#[cfg(feature = "gas_tracking")] +use crate::gas_tracker::GasTracker; + +pub const KEY_BTBE_ENTRY_HISTORY: &[u8] = b"btbe-entry-hist"; +pub const KEY_BTBE_BUCKETS_COUNT: &[u8] = b"btbe-buckets-cnt"; +pub const KEY_BTBE_BUCKETS: &[u8] = b"btbe-buckets"; +pub const KEY_BTBE_TRIE_NODES: &[u8] = b"btbe-trie-nodes"; +pub const KEY_BTBE_TRIE_NODES_COUNT: &[u8] = b"btbe-trie-nodes-cnt"; + +const BUCKETING_SALT_BYTES: &[u8; 14] = b"bucketing-salt"; + +const U32_BYTES: usize = 4; +const U128_BYTES: usize = 16; + +#[cfg(test)] +const BTBE_BUCKET_ADDRESS_BYTES: usize = 54; +#[cfg(not(test))] +const BTBE_BUCKET_ADDRESS_BYTES: usize = 20; +const BTBE_BUCKET_BALANCE_BYTES: usize = 8; // Max 16 (u128) +const BTBE_BUCKET_HISTORY_BYTES: usize = 4; // Max 4 (u32) + +const_assert!(BTBE_BUCKET_BALANCE_BYTES <= U128_BYTES); +const_assert!(BTBE_BUCKET_HISTORY_BYTES <= U32_BYTES); + +const BTBE_BUCKET_ENTRY_BYTES: usize = + BTBE_BUCKET_ADDRESS_BYTES + BTBE_BUCKET_BALANCE_BYTES + BTBE_BUCKET_HISTORY_BYTES; + +/// canonical address bytes corresponding to the 33-byte null public key, in hexadecimal +#[cfg(test)] +const IMPOSSIBLE_ADDR: [u8; BTBE_BUCKET_ADDRESS_BYTES] = [ + 0x29, 0xCF, 0xC6, 0x37, 0x62, 0x55, 0xA7, 0x84, 0x51, 0xEE, 0xB4, 0xB1, 0x29, 0xED, 0x8E, 0xAC, + 0xFF, 0xA2, 0xFE, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; +#[cfg(not(test))] +const IMPOSSIBLE_ADDR: [u8; BTBE_BUCKET_ADDRESS_BYTES] = [ + 0x29, 0xCF, 0xC6, 0x37, 0x62, 0x55, 0xA7, 0x84, 0x51, 0xEE, 0xB4, 0xB1, 0x29, 0xED, 0x8E, 0xAC, + 0xFF, 0xA2, 0xFE, 0xEF, +]; + +/// A `StoredEntry` consists of the address, balance, and tx bundle history length in a byte array representation. +/// The methods of the struct implementation also handle pushing and getting the tx bundle history in a simplified +/// append store. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] +#[cfg_attr(test, derive(Eq))] +pub struct StoredEntry(#[serde(with = "BigArray")] [u8; BTBE_BUCKET_ENTRY_BYTES]); + +impl StoredEntry { + fn new(address: &CanonicalAddr) -> StdResult<Self> { + let address = address.as_slice(); + + if address.len() != BTBE_BUCKET_ADDRESS_BYTES { + return Err(StdError::generic_err("bucket: invalid address length")); + } + + let mut result = [0u8; BTBE_BUCKET_ENTRY_BYTES]; + result[..BTBE_BUCKET_ADDRESS_BYTES].copy_from_slice(address); + Ok(Self { 0: result }) + } + + fn from( + storage: &mut dyn Storage, + dwb_entry: &DelayedWriteBufferEntry, + amount_spent: Option<u128>, + ) -> StdResult<Self> { + let mut entry = StoredEntry::new(&dwb_entry.recipient()?)?; + + let amount_spent = amount_u64(amount_spent)?; + + // error should never happen because already checked in `settle_sender_or_owner_account` + let balance = if let Some(new_balance) = dwb_entry.amount()?.checked_sub(amount_spent) { + new_balance + } else { + return Err(StdError::generic_err(format!( + "insufficient funds while creating StoredEntry; balance:{}, amount_spent:{}", + dwb_entry.amount()?, + amount_spent, + ))); + }; + + entry.set_balance(balance)?; + entry.push_tx_bundle( + storage, + &TxBundle { + head_node: dwb_entry.head_node()?, + list_len: dwb_entry.list_len()?, + offset: 0, + }, + )?; + + Ok(entry) + } + + fn address_slice(&self) -> &[u8] { + &self.0[..BTBE_BUCKET_ADDRESS_BYTES] + } + + fn address(&self) -> StdResult<CanonicalAddr> { + let result = CanonicalAddr::try_from(self.address_slice()) + .or(Err(StdError::generic_err("Get bucket address error")))?; + Ok(result) + } + + pub fn balance(&self) -> StdResult<u64> { + let start = BTBE_BUCKET_ADDRESS_BYTES; + let end = start + BTBE_BUCKET_BALANCE_BYTES; + let amount_slice = &self.0[start..end]; + let result = amount_slice + .try_into() + .or(Err(StdError::generic_err("Get bucket balance error")))?; + Ok(u64::from_be_bytes(result)) + } + + fn set_balance(&mut self, val: u64) -> StdResult<()> { + let start = BTBE_BUCKET_ADDRESS_BYTES; + let end = start + BTBE_BUCKET_BALANCE_BYTES; + self.0[start..end].copy_from_slice(&val.to_be_bytes()); + Ok(()) + } + + pub fn history_len(&self) -> StdResult<u32> { + let start = BTBE_BUCKET_ADDRESS_BYTES + BTBE_BUCKET_BALANCE_BYTES; + let end = start + BTBE_BUCKET_HISTORY_BYTES; + let history_len_slice = &self.0[start..end]; + let mut result = [0u8; U32_BYTES]; + result[U32_BYTES - BTBE_BUCKET_HISTORY_BYTES..].copy_from_slice(history_len_slice); + Ok(u32::from_be_bytes(result)) + } + + fn set_history_len(&mut self, val: u32) -> StdResult<()> { + let start = BTBE_BUCKET_ADDRESS_BYTES + BTBE_BUCKET_BALANCE_BYTES; + let end = start + BTBE_BUCKET_HISTORY_BYTES; + let val_bytes = &val.to_be_bytes()[U32_BYTES - BTBE_BUCKET_HISTORY_BYTES..]; + if val_bytes.len() != BTBE_BUCKET_HISTORY_BYTES { + return Err(StdError::generic_err("Set bucket history len error")); + } + self.0[start..end].copy_from_slice(val_bytes); + Ok(()) + } + + pub fn merge_dwb_entry( + &mut self, + storage: &mut dyn Storage, + dwb_entry: &DelayedWriteBufferEntry, + amount_spent: Option<u128>, + ) -> StdResult<()> { + let history_len = self.history_len()?; + if history_len == 0 { + return Err(StdError::generic_err( + "use `from` to create new entry from dwb_entry", + )); + } + + let mut balance = self.balance()?; + safe_add_u64(&mut balance, dwb_entry.amount()?); + + let amount_spent = amount_u64(amount_spent)?; + + // error should never happen because already checked in `settle_sender_or_owner_account` + let balance = if let Some(new_balance) = balance.checked_sub(amount_spent) { + new_balance + } else { + return Err(StdError::generic_err(format!( + "insufficient funds while merging entry; balance:{}, amount_spent:{}", + balance, amount_spent + ))); + }; + + self.set_balance(balance)?; + + // peek at the last tx bundle added + let last_tx_bundle = self.get_tx_bundle_at(storage, history_len - 1)?; + let tx_bundle = TxBundle { + head_node: dwb_entry.head_node()?, + list_len: dwb_entry.list_len()?, + offset: last_tx_bundle.offset + u32::from(last_tx_bundle.list_len), + }; + self.push_tx_bundle(storage, &tx_bundle)?; + + Ok(()) + } + + // simplified appendstore impl for tx history + + /// gets the element at pos if within bounds + pub fn get_tx_bundle_at(&self, storage: &dyn Storage, pos: u32) -> StdResult<TxBundle> { + let len = self.history_len()?; + if pos >= len { + return Err(StdError::generic_err("access out of bounds")); + } + self.get_tx_bundle_at_unchecked(storage, pos) + } + + /// tries to get the element at pos + fn get_tx_bundle_at_unchecked(&self, storage: &dyn Storage, pos: u32) -> StdResult<TxBundle> { + let bundle_data = storage.get( + &[ + KEY_BTBE_ENTRY_HISTORY, + self.address_slice(), + pos.to_be_bytes().as_slice(), + ] + .concat(), + ); + let bundle_data = bundle_data.ok_or_else(|| { + return StdError::generic_err("tx bundle not found"); + })?; + Bincode2::deserialize(&bundle_data) + } + + /// Sets data at a given index + fn set_tx_bundle_at_unchecked( + &self, + storage: &mut dyn Storage, + pos: u32, + bundle: &TxBundle, + ) -> StdResult<()> { + let bundle_data = Bincode2::serialize(bundle)?; + storage.set( + &[ + KEY_BTBE_ENTRY_HISTORY, + self.address_slice(), + pos.to_be_bytes().as_slice(), + ] + .concat(), + &bundle_data, + ); + Ok(()) + } + + /// Pushes a tx bundle + fn push_tx_bundle(&mut self, storage: &mut dyn Storage, bundle: &TxBundle) -> StdResult<()> { + let len = self.history_len()?; + self.set_tx_bundle_at_unchecked(storage, len, bundle)?; + self.set_history_len(len.saturating_add(1))?; + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] +pub struct BtbeBucket { + pub capacity: u16, + #[serde(with = "BigArray")] + pub entries: [StoredEntry; BTBE_CAPACITY as usize], +} + +//static BTBE_ENTRY_HISTORY: Item<u64> = Item::new(KEY_BTBE_ENTRY_HISTORY); +static BTBE_BUCKETS_COUNT: Item<u64> = Item::new(KEY_BTBE_BUCKETS_COUNT); +static BTBE_BUCKETS: Item<BtbeBucket> = Item::new(KEY_BTBE_BUCKETS); + +// create type alias to refer to position of a bucket entry, which is its index in the array plus 1 +type BucketEntryPosition = usize; + +impl BtbeBucket { + pub fn new() -> StdResult<Self> { + Ok(Self { + capacity: BTBE_CAPACITY, + entries: [StoredEntry::new(&CanonicalAddr::from(&IMPOSSIBLE_ADDR))?; + BTBE_CAPACITY as usize], + }) + } + + /// Attempts to add an entry to the bucket; returns false if bucket is at capacity, or true on success + pub fn add_entry(&mut self, entry: &StoredEntry) -> bool { + // buffer is at capacity + if self.capacity == 0 { + return false; + } + + // has capacity for a new entry; save entry to bucket + self.entries[(BTBE_CAPACITY - self.capacity) as usize] = entry.clone(); + + // update capacity + self.capacity -= 1; + + // done + true + } + + /// Searches the bucket for an entry containing the given address + pub fn constant_time_find_address( + &self, + address: &CanonicalAddr, + ) -> Option<(usize, StoredEntry)> { + let address = address.as_slice(); + + // contant-time only applies to this part, so that the index of the entry cannot be distinguished + let mut matched_index_p1: BucketEntryPosition = 0; + for (idx, entry) in self.entries.iter().enumerate() { + let equals = constant_time_eq(address, entry.address_slice()) as usize; + matched_index_p1 |= (idx + 1) * equals; + } + + match matched_index_p1 { + 0 => None, + idx => Some((idx - 1, self.entries[idx - 1])), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub struct BitwiseTrieNode { + pub left: u64, + pub right: u64, + pub bucket: u64, +} + +pub static BTBE_TRIE_NODES: Item<BitwiseTrieNode> = Item::new(KEY_BTBE_TRIE_NODES); +pub static BTBE_TRIE_NODES_COUNT: Item<u64> = Item::new(KEY_BTBE_TRIE_NODES_COUNT); + +impl BitwiseTrieNode { + // creates a new leaf node + pub fn new_leaf(storage: &mut dyn Storage, bucket: BtbeBucket) -> StdResult<Self> { + let buckets_count = BTBE_BUCKETS_COUNT.load(storage).unwrap_or_default() + 1; + + // ID for new bucket + let bucket_id = buckets_count; + + // save updated count + BTBE_BUCKETS_COUNT.save(storage, &buckets_count)?; + + // save bucket to storage + BTBE_BUCKETS + .add_suffix(&bucket_id.to_be_bytes()) + .save(storage, &bucket)?; + + // create new node + Ok(Self { + left: 0, + right: 0, + bucket: bucket_id, + }) + } + + // loads the node's bucket from storage + pub fn bucket(self, storage: &dyn Storage) -> StdResult<BtbeBucket> { + if self.bucket == 0 { + return Err(StdError::generic_err( + "btbe: attempted to load bucket of branch node", + )); + } + + // load bucket from storage + BTBE_BUCKETS + .add_suffix(&self.bucket.to_be_bytes()) + .load(storage) + } + + // stores the bucket associated with this node + fn set_and_save_bucket(self, storage: &mut dyn Storage, bucket: BtbeBucket) -> StdResult<()> { + if self.bucket == 0 { + return Err(StdError::generic_err( + "btbe: attempted to store a bucket to a branch node", + )); + } + + BTBE_BUCKETS + .add_suffix(&self.bucket.to_be_bytes()) + .save(storage, &bucket) + } +} + +/// Determines whether a given entry belongs in the left node (true) or right node (false) +fn entry_belongs_in_left_node(secret: &[u8], entry: StoredEntry, bit_pos: u8) -> StdResult<bool> { + // create key bytes + let key_bytes = hkdf_sha_256( + &Some(BUCKETING_SALT_BYTES.to_vec()), + secret, + entry.address_slice(), + 32, + )?; + + // convert to u258 + let key_u256 = U256::from_big_endian(&key_bytes); + + // extract the bit value at the target bit position + return Ok(U256::from(0) == (key_u256 >> (255 - bit_pos)) & U256::from(1)); +} + +/// Locates a btbe node given an address; returns tuple of (node, node_id, bit position) +pub fn locate_btbe_node( + storage: &dyn Storage, + address: &CanonicalAddr, +) -> StdResult<(BitwiseTrieNode, u64, u8)> { + // load internal contract secret + let secret = INTERNAL_SECRET.load(storage)?; + let secret = secret.as_slice(); + + // create key bytes + let hash = hkdf_sha_256( + &Some(BUCKETING_SALT_BYTES.to_vec()), + secret, + address.as_slice(), + 32, + )?; + + // start at root of trie + let mut node_id: u64 = 1; + let mut node = BTBE_TRIE_NODES + .add_suffix(&node_id.to_be_bytes()) + .load(storage)?; + let mut bit_pos: u8 = 0; + + // while the node has children + while node.bucket == 0 { + // calculate bit value at current bit position + let bit_value = (hash[(bit_pos / 8) as usize] >> (7 - (bit_pos % 8))) & 1; + + // increment bit position + bit_pos += 1; + + // choose left or right child depending on bit value + node_id = if bit_value == 0 { + node.left + } else { + node.right + }; + + // load child node + node = BTBE_TRIE_NODES + .add_suffix(&node_id.to_be_bytes()) + .load(storage)?; + } + + Ok((node, node_id, bit_pos)) +} + +/// Does a binary search on the append store to find the bundle where the `start_idx` tx can be found. +/// For a paginated search `start_idx` = `page` * `page_size`. +/// Returns the bundle index, the bundle, and the index in the bundle list to start at +pub fn find_start_bundle( + storage: &dyn Storage, + account: &CanonicalAddr, + start_idx: u32, +) -> StdResult<Option<(u32, TxBundle, u32)>> { + let (node, _, _) = locate_btbe_node(storage, account)?; + let bucket = node.bucket(storage)?; + if let Some((_, entry)) = bucket.constant_time_find_address(account) { + let mut left = 0u32; + let mut right = entry.history_len()?; + + while left <= right { + let mid = (left + right) / 2; + let mid_bundle = entry.get_tx_bundle_at(storage, mid)?; + if start_idx >= mid_bundle.offset + && start_idx < mid_bundle.offset + (mid_bundle.list_len as u32) + { + // we have the correct bundle + // which index in list to start at? + let start_at = (mid_bundle.list_len as u32) - (start_idx - mid_bundle.offset) - 1; + return Ok(Some((mid, mid_bundle, start_at))); + } else if start_idx < mid_bundle.offset { + right = mid - 1; + } else { + left = mid + 1; + } + } + } + + Ok(None) +} + +/// gets the StoredEntry for a given account +pub fn stored_entry( + storage: &dyn Storage, + account: &CanonicalAddr, +) -> StdResult<Option<StoredEntry>> { + let (node, _, _) = locate_btbe_node(storage, account)?; + let bucket = node.bucket(storage)?; + Ok(bucket.constant_time_find_address(account).map(|b| b.1)) +} + +/// returns the current stored balance for an entry +pub fn stored_balance(storage: &dyn Storage, address: &CanonicalAddr) -> StdResult<u128> { + if let Some(entry) = stored_entry(storage, address)? { + Ok(entry.balance()? as u128) + } else { + Ok(0_u128) + } +} + +/// Returns the total number of settled transactions for an account by peeking at last bundle +pub fn stored_tx_count(storage: &dyn Storage, entry: &Option<StoredEntry>) -> StdResult<u32> { + if let Some(entry) = entry { + // peek at last entry + let len = entry.history_len()?; + if len > 0 { + let bundle = entry.get_tx_bundle_at(storage, len - 1)?; + return Ok(bundle.offset + bundle.list_len as u32); + } + } + Ok(0) +} + +// merges a dwb entry into the current node's bucket +// `amount_spent` is any required subtraction due to being sender of tx +pub fn merge_dwb_entry( + storage: &mut dyn Storage, + dwb_entry: &DelayedWriteBufferEntry, + amount_spent: Option<u128>, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<()> { + #[cfg(feature = "gas_tracking")] + let mut group1 = tracker.group("#merge_dwb_entry.1"); + + // locate the node that the given entry belongs in + let (mut node, mut node_id, mut bit_pos) = locate_btbe_node(storage, &dwb_entry.recipient()?)?; + + // load that node's current bucket + let mut bucket = node.bucket(storage)?; + + // bucket ID for logging purposes + let mut bucket_id = node.bucket; + + // search for an existing entry + if let Some((idx, mut found_entry)) = bucket.constant_time_find_address(&dwb_entry.recipient()?) + { + // found existing entry + // merge amount and history from dwb entry + found_entry.merge_dwb_entry(storage, &dwb_entry, amount_spent)?; + bucket.entries[idx] = found_entry; + + #[cfg(feature = "gas_tracking")] + group1.logf(format!( + "merged {} into node #{}, bucket #{} at position {} ", + dwb_entry.recipient()?, + node_id, + bucket_id, + idx + )); + + // save updated bucket to storage + node.set_and_save_bucket(storage, bucket)?; + } else { + // need to insert new entry + // create new stored balance entry + let btbe_entry = StoredEntry::from(storage, &dwb_entry, amount_spent)?; + + // load contract's internal secret + let secret = INTERNAL_SECRET.load(storage)?; + let secret = secret.as_slice(); + + loop { + // looping as many times as needed until the bucket has capacity for a new entry + // try to add to the current bucket + if bucket.add_entry(&btbe_entry) { + #[cfg(feature = "gas_tracking")] + group1.logf(format!( + "inserted into node #{}, bucket #{} (bitpos: {}) at position {}", + node_id, + bucket_id, + bit_pos, + BTBE_CAPACITY - bucket.capacity - 1 + )); + + // bucket has capacity and it added the new entry + // save bucket to storage + node.set_and_save_bucket(storage, bucket)?; + // break out of the loop + break; + } else { + // bucket is full; split on next bit position + // create new left and right buckets + let mut left_bucket = BtbeBucket::new()?; + let mut right_bucket = BtbeBucket::new()?; + + // each entry + for entry in bucket.entries { + // left_bucket.add_entry(&entry); + // route entry + if entry_belongs_in_left_node(secret, entry, bit_pos)? { + left_bucket.add_entry(&entry); + } else { + right_bucket.add_entry(&entry); + } + } + + // save left node's bucket to storage, recycling this node's bucket ID + let left_bucket_id = node.bucket; + BTBE_BUCKETS + .add_suffix(&left_bucket_id.to_be_bytes()) + .save(storage, &left_bucket)?; + + // global count of buckets + let mut buckets_count = BTBE_BUCKETS_COUNT.load(storage).unwrap_or_default(); + + // bucket ID for right node + buckets_count += 1; + let right_bucket_id = buckets_count; + BTBE_BUCKETS + .add_suffix(&right_bucket_id.to_be_bytes()) + .save(storage, &right_bucket)?; + + // save updated count + BTBE_BUCKETS_COUNT.save(storage, &buckets_count)?; + + // global count of trie nodes + let mut nodes_count = BTBE_TRIE_NODES_COUNT.load(storage).unwrap_or_default(); + + // ID for left node + nodes_count += 1; + let left_id = nodes_count; + + // ID for right node + nodes_count += 1; + let right_id = nodes_count; + + // save updated count + BTBE_TRIE_NODES_COUNT.save(storage, &nodes_count)?; + + // create left and right nodes + let left = BitwiseTrieNode { + left: 0, + right: 0, + bucket: left_bucket_id, + }; + let right = BitwiseTrieNode { + left: 0, + right: 0, + bucket: right_bucket_id, + }; + + // save left and right node to storage + BTBE_TRIE_NODES + .add_suffix(&left_id.to_be_bytes()) + .save(storage, &left)?; + BTBE_TRIE_NODES + .add_suffix(&right_id.to_be_bytes()) + .save(storage, &right)?; + + // convert this into a branch node + node.left = left_id; + node.right = right_id; + node.bucket = 0; + + // save node + BTBE_TRIE_NODES + .add_suffix(&node_id.to_be_bytes()) + .save(storage, &node)?; + + #[cfg(feature = "gas_tracking")] + group1.logf(format!( + "split node #{}, bucket #{} at bitpos {}, ", + node_id, bucket_id, bit_pos + )); + + // route entry + if entry_belongs_in_left_node(secret, btbe_entry, bit_pos)? { + node = left; + node_id = left_id; + bucket = left_bucket; + bucket_id = left_bucket_id; + } else { + node = right; + node_id = right_id; + bucket = right_bucket; + bucket_id = right_bucket_id; + } + + // increment bit position for next iteration of the loop + bit_pos += 1; + } + } + } + + Ok(()) +} + +/// initializes the btbe +pub fn initialize_btbe(storage: &mut dyn Storage) -> StdResult<()> { + let bucket = BtbeBucket::new()?; + let node = BitwiseTrieNode::new_leaf(storage, bucket)?; + + // save count + BTBE_TRIE_NODES_COUNT.save(storage, &1)?; + + // save root node to storage + BTBE_TRIE_NODES + .add_suffix(&1_u64.to_be_bytes()) + .save(storage, &node)?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::any::Any; + + use crate::contract::instantiate; + use crate::msg::{InitialBalance, InstantiateMsg, QueryAnswer}; + use cosmwasm_std::{ + from_binary, testing::*, Addr, Api, Binary, OwnedDeps, QueryResponse, Response, Uint128, + }; + + use super::*; + + fn init_helper( + initial_balances: Vec<InitialBalance>, + ) -> ( + StdResult<Response>, + OwnedDeps<MockStorage, MockApi, MockQuerier>, + ) { + let mut deps = mock_dependencies_with_balance(&[]); + let env = mock_env(); + let info = mock_info("instantiator", &[]); + + let init_msg = InstantiateMsg { + name: "sec-sec".to_string(), + admin: Some("admin".to_string()), + symbol: "SECSEC".to_string(), + decimals: 8, + initial_balances: Some(initial_balances), + prng_seed: Binary::from("lolz fun yay".as_bytes()), + config: None, + supported_denoms: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + fn extract_error_msg<T: Any>(error: StdResult<T>) -> String { + match error { + Ok(response) => { + let bin_err = (&response as &dyn Any) + .downcast_ref::<QueryResponse>() + .expect("An error was expected, but no error could be extracted"); + match from_binary(bin_err).unwrap() { + QueryAnswer::ViewingKeyError { msg } => msg, + _ => panic!("Unexpected query answer"), + } + } + Err(err) => match err { + StdError::GenericErr { msg, .. } => msg, + _ => panic!("Unexpected result from init"), + }, + } + } + + #[test] + fn test_stored_entry() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }]); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let _env = mock_env(); + let _info = mock_info("bob", &[]); + + let canonical = deps + .api + .addr_canonicalize(Addr::unchecked("bob".to_string()).as_str()) + .unwrap(); + let entry = StoredEntry::new(&canonical).unwrap(); + assert_eq!(entry.address().unwrap(), canonical); + assert_eq!(entry.balance().unwrap(), 0_u64); + + let dwb_entry = DelayedWriteBufferEntry::new(&canonical).unwrap(); + + // expect error if trying to spend too much + let entry = StoredEntry::from(&mut deps.storage, &dwb_entry, Some(1)); + let error = extract_error_msg(entry); + assert!(error.contains("insufficient funds")); + + let entry = StoredEntry::from(&mut deps.storage, &dwb_entry, None).unwrap(); + assert_eq!(entry.address().unwrap(), canonical); + assert_eq!(entry.balance().unwrap(), 0_u64); + } + + #[test] + fn test_btbe() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }]); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let _env = mock_env(); + let _info = mock_info("bob", &[]); + + let _ = initialize_btbe(&mut deps.storage).unwrap(); + + let btbe_node_count = BTBE_TRIE_NODES_COUNT.load(&deps.storage).unwrap(); + assert_eq!(btbe_node_count, 1); + + for i in 1..=128 { + let canonical = deps + .api + .addr_canonicalize(Addr::unchecked(format!("{i}zzzzzz")).as_str()) + .unwrap(); + let entry = StoredEntry::new(&canonical).unwrap(); + assert_eq!(entry.address().unwrap(), canonical); + assert_eq!(entry.balance().unwrap(), 0_u64); + + let dwb_entry = DelayedWriteBufferEntry::new(&canonical).unwrap(); + + let _result = merge_dwb_entry(&mut deps.storage, &dwb_entry, None); + + let btbe_node_count = BTBE_TRIE_NODES_COUNT.load(&deps.storage).unwrap(); + assert_eq!(btbe_node_count, 1); + + let (node, node_id, bit_pos) = locate_btbe_node(&deps.storage, &canonical).unwrap(); + assert_eq!( + node, + BitwiseTrieNode { + left: 0, + right: 0, + bucket: 2, + } + ); + assert_eq!(node_id, 1); + assert_eq!(bit_pos, 0); + } + + // btbe trie should split nodes when get to 129th entry + let canonical = deps + .api + .addr_canonicalize(Addr::unchecked(format!("bob")).as_str()) + .unwrap(); + let entry = StoredEntry::new(&canonical).unwrap(); + assert_eq!(entry.address().unwrap(), canonical); + assert_eq!(entry.balance().unwrap(), 0_u64); + + let dwb_entry = DelayedWriteBufferEntry::new(&canonical).unwrap(); + + let _result = merge_dwb_entry(&mut deps.storage, &dwb_entry, None); + + let btbe_node_count = BTBE_TRIE_NODES_COUNT.load(&deps.storage).unwrap(); + assert_eq!(btbe_node_count, 3); + let (node, node_id, bit_pos) = locate_btbe_node(&deps.storage, &canonical).unwrap(); + assert_eq!( + node, + BitwiseTrieNode { + left: 0, + right: 0, + bucket: 3, + } + ); + assert_eq!(node_id, 3); + assert_eq!(bit_pos, 1); + + // have other addresses been moved to new nodes + let first = deps + .api + .addr_canonicalize(Addr::unchecked(format!("1zzzzzz")).as_str()) + .unwrap(); + let (node, node_id, bit_pos) = locate_btbe_node(&deps.storage, &first).unwrap(); + assert_eq!( + node, + BitwiseTrieNode { + left: 0, + right: 0, + bucket: 2, + } + ); + assert_eq!(node_id, 2); + assert_eq!(bit_pos, 1); + + let second = deps + .api + .addr_canonicalize(Addr::unchecked(format!("2zzzzzz")).as_str()) + .unwrap(); + let (node, node_id, bit_pos) = locate_btbe_node(&deps.storage, &second).unwrap(); + assert_eq!( + node, + BitwiseTrieNode { + left: 0, + right: 0, + bucket: 3, + } + ); + assert_eq!(node_id, 3); + assert_eq!(bit_pos, 1); + + let canonical_entry = stored_entry(&deps.storage, &canonical).unwrap().unwrap(); + assert_eq!(canonical_entry.balance().unwrap(), 0); + let first_entry = stored_entry(&deps.storage, &first).unwrap().unwrap(); + assert_eq!(first_entry.balance().unwrap(), 0); + let second_entry = stored_entry(&deps.storage, &second).unwrap().unwrap(); + assert_eq!(second_entry.balance().unwrap(), 0); + let not_entry = stored_entry( + &deps.storage, + &deps + .api + .addr_canonicalize(Addr::unchecked("alice".to_string()).as_str()) + .unwrap(), + ) + .unwrap(); + assert_eq!(not_entry, None); + } +} diff --git a/src/contract.rs b/src/contract.rs index f5f89789..29773f15 100644 --- a/src/contract.rs +++ b/src/contract.rs @@ -1,32 +1,48 @@ /// This contract implements SNIP-20 standard: /// https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md use cosmwasm_std::{ - entry_point, to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, - MessageInfo, Response, StdError, StdResult, Storage, Uint128, + entry_point, to_binary, Addr, BankMsg, Binary, BlockInfo, CanonicalAddr, Coin, CosmosMsg, + Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Storage, Uint128, Uint64, }; -use rand::RngCore; +#[cfg(feature = "gas_evaporation")] +use cosmwasm_std::Api; +use secret_toolkit::notification::{get_seed, notification_id, BloomParameters, ChannelInfoData, Descriptor, FlatDescriptor, Notification, NotificationData, StructDescriptor,}; use secret_toolkit::permit::{Permit, RevokedPermits, TokenPermissions}; use secret_toolkit::utils::{pad_handle_result, pad_query_result}; use secret_toolkit::viewing_key::{ViewingKey, ViewingKeyStore}; -use secret_toolkit_crypto::{sha_256, Prng, SHA256_HASH_SIZE}; +use secret_toolkit_crypto::{hkdf_sha_256, sha_256, ContractPrng}; use crate::batch; + +#[cfg(feature = "gas_tracking")] +use crate::dwb::log_dwb; +use crate::dwb::{DelayedWriteBuffer, DWB, TX_NODES}; + +use crate::btbe::{ + find_start_bundle, initialize_btbe, stored_balance, stored_entry, stored_tx_count, +}; +#[cfg(feature = "gas_tracking")] +use crate::gas_tracker::{GasTracker, LoggingExt}; +#[cfg(feature = "gas_evaporation")] +use crate::msg::Evaporator; use crate::msg::{ - AllowanceGivenResult, AllowanceReceivedResult, ContractStatusLevel, Decoyable, ExecuteAnswer, - ExecuteMsg, InstantiateMsg, QueryAnswer, QueryMsg, QueryWithPermit, ResponseStatus::Success, + AllowanceGivenResult, AllowanceReceivedResult, ContractStatusLevel, ExecuteAnswer, ExecuteMsg, + InstantiateMsg, QueryAnswer, QueryMsg, QueryWithPermit, ResponseStatus::Success, }; +use crate::notifications::{multi_received_data, multi_spent_data, AllowanceNotificationData, ReceivedNotificationData, SpentNotificationData, MULTI_RECEIVED_CHANNEL_BLOOM_K, MULTI_RECEIVED_CHANNEL_BLOOM_N, MULTI_RECEIVED_CHANNEL_ID, MULTI_RECEIVED_CHANNEL_PACKET_SIZE, MULTI_SPENT_CHANNEL_BLOOM_K, MULTI_SPENT_CHANNEL_BLOOM_N, MULTI_SPENT_CHANNEL_ID, MULTI_SPENT_CHANNEL_PACKET_SIZE}; use crate::receiver::Snip20ReceiveMsg; use crate::state::{ - safe_add, AllowancesStore, BalancesStore, Config, MintersStore, PrngStore, ReceiverHashStore, - CONFIG, CONTRACT_STATUS, TOTAL_SUPPLY, + safe_add, AllowancesStore, Config, MintersStore, ReceiverHashStore, CHANNELS, CONFIG, CONTRACT_STATUS, INTERNAL_SECRET, TOTAL_SUPPLY }; +use crate::strings::TRANSFER_HISTORY_UNSUPPORTED_MSG; use crate::transaction_history::{ - store_burn, store_deposit, store_mint, store_redeem, store_transfer, StoredExtendedTx, - StoredLegacyTransfer, + store_burn_action, store_deposit_action, store_mint_action, store_redeem_action, + store_transfer_action, Tx, }; /// We make sure that responses from `handle` are padded to a multiple of this size. pub const RESPONSE_BLOCK_SIZE: usize = 256; +pub const NOTIFICATION_BLOCK_SIZE: usize = 36; pub const PREFIX_REVOKED_PERMITS: &str = "revoked_permits"; #[entry_point] @@ -55,49 +71,78 @@ pub fn instantiate( let admin = match msg.admin { Some(admin_addr) => deps.api.addr_validate(admin_addr.as_str())?, - None => info.sender, + None => info.sender.clone(), }; let mut total_supply: u128 = 0; - let prng_seed_hashed = sha_256(&msg.prng_seed.0); - PrngStore::save(deps.storage, prng_seed_hashed)?; - - { - let initial_balances = msg.initial_balances.unwrap_or_default(); - for balance in initial_balances { - let amount = balance.amount.u128(); - let balance_address = deps.api.addr_validate(balance.address.as_str())?; - // Here amount is also the amount to be added because the account has no prior balance - BalancesStore::update_balance( - deps.storage, - &balance_address, - amount, - true, - "", - &None, - &None, - )?; + // initialize the bitwise-trie of bucketed entries + initialize_btbe(deps.storage)?; + + // initialize the delay write buffer + DWB.save(deps.storage, &DelayedWriteBuffer::new()?)?; + + let initial_balances = msg.initial_balances.unwrap_or_default(); + let raw_admin = deps.api.addr_canonicalize(admin.as_str())?; + let rng_seed = env.block.random.as_ref().unwrap(); + + // use entropy and env.random to create an internal secret for the contract + let entropy = msg.prng_seed.0.as_slice(); + let entropy_len = 16 + info.sender.to_string().len() + entropy.len(); + let mut rng_entropy = Vec::with_capacity(entropy_len); + rng_entropy.extend_from_slice(&env.block.height.to_be_bytes()); + rng_entropy.extend_from_slice(&env.block.time.seconds().to_be_bytes()); + rng_entropy.extend_from_slice(info.sender.as_bytes()); + rng_entropy.extend_from_slice(entropy); + + // Create INTERNAL_SECRET + let salt = Some(sha_256(&rng_entropy).to_vec()); + let internal_secret = hkdf_sha_256( + &salt, + rng_seed.0.as_slice(), + "contract_internal_secret".as_bytes(), + 32, + )?; + INTERNAL_SECRET.save(deps.storage, &internal_secret)?; + + // Hard-coded channels + let channels: Vec<String> = vec![ + ReceivedNotificationData::CHANNEL_ID.to_string(), + SpentNotificationData::CHANNEL_ID.to_string(), + AllowanceNotificationData::CHANNEL_ID.to_string(), + MULTI_RECEIVED_CHANNEL_ID.to_string(), + MULTI_SPENT_CHANNEL_ID.to_string(), + ]; + + for channel in channels { + CHANNELS.insert(deps.storage, &channel)?; + } - if let Some(new_total_supply) = total_supply.checked_add(amount) { - total_supply = new_total_supply; - } else { - return Err(StdError::generic_err( - "The sum of all initial balances exceeds the maximum possible total supply", - )); - } + let mut rng = ContractPrng::new(rng_seed.as_slice(), &sha_256(&msg.prng_seed.0)); + for balance in initial_balances { + let amount = balance.amount.u128(); + let balance_address = deps.api.addr_canonicalize(balance.address.as_str())?; + #[cfg(feature = "gas_tracking")] + let mut tracker = GasTracker::new(deps.api); + perform_mint( + deps.storage, + &mut rng, + &raw_admin, + &balance_address, + amount, + msg.symbol.clone(), + Some("Initial Balance".to_string()), + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; - store_mint( - deps.storage, - admin.clone(), - balance_address, - balance.amount, - msg.symbol.clone(), - Some("Initial Balance".to_string()), - &env.block, - &None, - &None, - )?; + if let Some(new_total_supply) = total_supply.checked_add(amount) { + total_supply = new_total_supply; + } else { + return Err(StdError::generic_err( + "The sum of all initial balances exceeds the maximum possible total supply", + )); } } @@ -132,57 +177,35 @@ pub fn instantiate( }; MintersStore::save(deps.storage, minters)?; - ViewingKey::set_seed(deps.storage, &prng_seed_hashed); + let vk_seed = hkdf_sha_256( + &salt, + rng_seed.0.as_slice(), + "contract_viewing_key".as_bytes(), + 32, + )?; + ViewingKey::set_seed(deps.storage, &vk_seed); Ok(Response::default()) } -fn get_address_position( - store: &mut dyn Storage, - decoys_size: usize, - entropy: &[u8; SHA256_HASH_SIZE], -) -> StdResult<usize> { - let mut rng = Prng::new(&PrngStore::load(store)?, entropy); - - let mut new_contract_entropy = [0u8; 20]; - rng.rng.fill_bytes(&mut new_contract_entropy); - - let new_prng_seed = sha_256(&new_contract_entropy); - PrngStore::save(store, new_prng_seed)?; - - // decoys_size is also an accepted output which means: set the account balance after you've set decoys' balanace - Ok(rng.rng.next_u64() as usize % (decoys_size + 1)) -} - #[entry_point] pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> { - let contract_status = CONTRACT_STATUS.load(deps.storage)?; - - let mut account_random_pos: Option<usize> = None; - - let entropy = match msg.clone().get_entropy() { - None => [0u8; SHA256_HASH_SIZE], - Some(e) => sha_256(&e.0), - }; + let mut rng = ContractPrng::from_env(&env); - let decoys_size = msg.get_minimal_decoys_size(); - if decoys_size != 0 { - account_random_pos = Some(get_address_position(deps.storage, decoys_size, &entropy)?); - } + let contract_status = CONTRACT_STATUS.load(deps.storage)?; + #[cfg(feature = "gas_evaporation")] + let api = deps.api; match contract_status { ContractStatusLevel::StopAll | ContractStatusLevel::StopAllButRedeems => { let response = match msg { ExecuteMsg::SetContractStatus { level, .. } => { set_contract_status(deps, info, level) } - ExecuteMsg::Redeem { - amount, - denom, - decoys, - .. - } if contract_status == ContractStatusLevel::StopAllButRedeems => { - try_redeem(deps, env, info, amount, denom, decoys, account_random_pos) + ExecuteMsg::Redeem { amount, denom, .. } + if contract_status == ContractStatusLevel::StopAllButRedeems => + { + try_redeem(deps, env, info, amount, denom) } _ => Err(StdError::generic_err( "This contract is stopped and this action is not allowed", @@ -195,69 +218,43 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S let response = match msg.clone() { // Native - ExecuteMsg::Deposit { decoys, .. } => { - try_deposit(deps, env, info, decoys, account_random_pos) - } - ExecuteMsg::Redeem { - amount, - denom, - decoys, - .. - } => try_redeem(deps, env, info, amount, denom, decoys, account_random_pos), + ExecuteMsg::Deposit { .. } => try_deposit(deps, env, info, &mut rng), + ExecuteMsg::Redeem { amount, denom, .. } => try_redeem(deps, env, info, amount, denom), // Base ExecuteMsg::Transfer { recipient, amount, memo, - decoys, .. - } => try_transfer( - deps, - env, - info, - recipient, - amount, - memo, - decoys, - account_random_pos, - ), + } => try_transfer(deps, env, info, &mut rng, recipient, amount, memo), ExecuteMsg::Send { recipient, recipient_code_hash, amount, msg, memo, - decoys, .. } => try_send( deps, env, info, + &mut rng, recipient, recipient_code_hash, amount, memo, msg, - decoys, - account_random_pos, ), ExecuteMsg::BatchTransfer { actions, .. } => { - try_batch_transfer(deps, env, info, actions, account_random_pos) - } - ExecuteMsg::BatchSend { actions, .. } => { - try_batch_send(deps, env, info, actions, account_random_pos) + try_batch_transfer(deps, env, info, &mut rng, actions) } - ExecuteMsg::Burn { - amount, - memo, - decoys, - .. - } => try_burn(deps, env, info, amount, memo, decoys, account_random_pos), + ExecuteMsg::BatchSend { actions, .. } => try_batch_send(deps, env, info, &mut rng, actions), + ExecuteMsg::Burn { amount, memo, .. } => try_burn(deps, env, info, amount, memo), ExecuteMsg::RegisterReceive { code_hash, .. } => { try_register_receive(deps, info, code_hash) } - ExecuteMsg::CreateViewingKey { entropy, .. } => try_create_key(deps, env, info, entropy), + ExecuteMsg::CreateViewingKey { entropy, .. } => try_create_key(deps, env, info, entropy, &mut rng), ExecuteMsg::SetViewingKey { key, .. } => try_set_key(deps, info, key), // Allowance @@ -278,19 +275,8 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S recipient, amount, memo, - decoys, .. - } => try_transfer_from( - deps, - &env, - info, - owner, - recipient, - amount, - memo, - decoys, - account_random_pos, - ), + } => try_transfer_from(deps, &env, info, &mut rng, owner, recipient, amount, memo), ExecuteMsg::SendFrom { owner, recipient, @@ -298,67 +284,41 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S amount, msg, memo, - decoys, .. } => try_send_from( deps, env, &info, + &mut rng, owner, recipient, recipient_code_hash, amount, memo, msg, - decoys, - account_random_pos, ), ExecuteMsg::BatchTransferFrom { actions, .. } => { - try_batch_transfer_from(deps, &env, info, actions, account_random_pos) + try_batch_transfer_from(deps, &env, info, &mut rng, actions) } ExecuteMsg::BatchSendFrom { actions, .. } => { - try_batch_send_from(deps, env, &info, actions, account_random_pos) + try_batch_send_from(deps, env, &info, &mut rng, actions) } ExecuteMsg::BurnFrom { owner, amount, memo, - decoys, .. - } => try_burn_from( - deps, - &env, - info, - owner, - amount, - memo, - decoys, - account_random_pos, - ), - ExecuteMsg::BatchBurnFrom { actions, .. } => { - try_batch_burn_from(deps, &env, info, actions, account_random_pos) - } + } => try_burn_from(deps, &env, info, owner, amount, memo), + ExecuteMsg::BatchBurnFrom { actions, .. } => try_batch_burn_from(deps, &env, info, actions), // Mint ExecuteMsg::Mint { recipient, amount, memo, - decoys, .. - } => try_mint( - deps, - env, - info, - recipient, - amount, - memo, - decoys, - account_random_pos, - ), - ExecuteMsg::BatchMint { actions, .. } => { - try_batch_mint(deps, env, info, actions, account_random_pos) - } + } => try_mint(deps, env, info, &mut rng, recipient, amount, memo), + ExecuteMsg::BatchMint { actions, .. } => try_batch_mint(deps, env, info, &mut rng, actions), // Other ExecuteMsg::ChangeAdmin { address, .. } => change_admin(deps, info, address), @@ -373,11 +333,16 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S } }; - pad_handle_result(response, RESPONSE_BLOCK_SIZE) + let padded_result = pad_handle_result(response, RESPONSE_BLOCK_SIZE); + + #[cfg(feature = "gas_evaporation")] + let evaporated = msg.evaporate_to_target(api)?; + + padded_result } #[entry_point] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> { pad_query_result( match msg { QueryMsg::TokenInfo {} => query_token_info(deps.storage), @@ -385,14 +350,19 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> { QueryMsg::ContractStatus {} => query_contract_status(deps.storage), QueryMsg::ExchangeRate {} => query_exchange_rate(deps.storage), QueryMsg::Minters { .. } => query_minters(deps), - QueryMsg::WithPermit { permit, query } => permit_queries(deps, permit, query), - _ => viewing_keys_queries(deps, msg), + QueryMsg::ListChannels {} => query_list_channels(deps), + QueryMsg::WithPermit { permit, query } => permit_queries(deps, env, permit, query), + + #[cfg(feature = "gas_tracking")] + QueryMsg::Dwb {} => log_dwb(deps.storage), + + _ => viewing_keys_queries(deps, env, msg), }, RESPONSE_BLOCK_SIZE, ) } -fn permit_queries(deps: Deps, permit: Permit, query: QueryWithPermit) -> Result<Binary, StdError> { +fn permit_queries(deps: Deps, env: Env, permit: Permit, query: QueryWithPermit) -> Result<Binary, StdError> { // Validate permit content let token_address = CONFIG.load(deps.storage)?.contract_address; @@ -416,31 +386,10 @@ fn permit_queries(deps: Deps, permit: Permit, query: QueryWithPermit) -> Result< query_balance(deps, account) } - QueryWithPermit::TransferHistory { - page, - page_size, - should_filter_decoys, - } => { - if !permit.check_permission(&TokenPermissions::History) { - return Err(StdError::generic_err(format!( - "No permission to query history, got permissions {:?}", - permit.params.permissions - ))); - } - - query_transfers( - deps, - account, - page.unwrap_or(0), - page_size, - should_filter_decoys, - ) + QueryWithPermit::TransferHistory { .. } => { + return Err(StdError::generic_err(TRANSFER_HISTORY_UNSUPPORTED_MSG)); } - QueryWithPermit::TransactionHistory { - page, - page_size, - should_filter_decoys, - } => { + QueryWithPermit::TransactionHistory { page, page_size } => { if !permit.check_permission(&TokenPermissions::History) { return Err(StdError::generic_err(format!( "No permission to query history, got permissions {:?}", @@ -448,13 +397,7 @@ fn permit_queries(deps: Deps, permit: Permit, query: QueryWithPermit) -> Result< ))); } - query_transactions( - deps, - account, - page.unwrap_or(0), - page_size, - should_filter_decoys, - ) + query_transactions(deps, account, page.unwrap_or(0), page_size) } QueryWithPermit::Allowance { owner, spender } => { if !permit.check_permission(&TokenPermissions::Allowance) { @@ -517,10 +460,17 @@ fn permit_queries(deps: Deps, permit: Permit, query: QueryWithPermit) -> Result< } query_allowances_received(deps, account, page.unwrap_or(0), page_size) } + QueryWithPermit::ChannelInfo { channels, txhash } => query_channel_info( + deps, + env, + channels, + txhash, + deps.api.addr_canonicalize(account.as_str())?, + ) } } -pub fn viewing_keys_queries(deps: Deps, msg: QueryMsg) -> StdResult<Binary> { +pub fn viewing_keys_queries(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> { let (addresses, key) = msg.get_validation_params(deps.api)?; for address in addresses { @@ -529,32 +479,15 @@ pub fn viewing_keys_queries(deps: Deps, msg: QueryMsg) -> StdResult<Binary> { return match msg { // Base QueryMsg::Balance { address, .. } => query_balance(deps, address), - QueryMsg::TransferHistory { - address, - page, - page_size, - should_filter_decoys, - .. - } => query_transfers( - deps, - address, - page.unwrap_or(0), - page_size, - should_filter_decoys, - ), + QueryMsg::TransferHistory { .. } => { + return Err(StdError::generic_err(TRANSFER_HISTORY_UNSUPPORTED_MSG)); + } QueryMsg::TransactionHistory { address, page, page_size, - should_filter_decoys, .. - } => query_transactions( - deps, - address, - page.unwrap_or(0), - page_size, - should_filter_decoys, - ), + } => query_transactions(deps, address, page.unwrap_or(0), page_size), QueryMsg::Allowance { owner, spender, .. } => query_allowance(deps, owner, spender), QueryMsg::AllowancesGiven { owner, @@ -568,6 +501,17 @@ pub fn viewing_keys_queries(deps: Deps, msg: QueryMsg) -> StdResult<Binary> { page_size, .. } => query_allowances_received(deps, spender, page.unwrap_or(0), page_size), + QueryMsg::ChannelInfo { + channels, + txhash, + viewer, + } => query_channel_info( + deps, + env, + channels, + txhash, + deps.api.addr_canonicalize(viewer.address.as_str())?, + ), _ => panic!("This query type does not require authentication"), }; } @@ -578,6 +522,8 @@ pub fn viewing_keys_queries(deps: Deps, msg: QueryMsg) -> StdResult<Binary> { }) } +// query functions + fn query_exchange_rate(storage: &dyn Storage) -> StdResult<Binary> { let constants = CONFIG.load(storage)?; @@ -639,65 +585,173 @@ fn query_contract_status(storage: &dyn Storage) -> StdResult<Binary> { }) } -pub fn query_transfers( - deps: Deps, - account: String, - page: u32, - page_size: u32, - should_filter_decoys: bool, -) -> StdResult<Binary> { - // Notice that if query_transfers() was called by a viewking-key call, the address of 'account' - // has already been validated. - // The address of 'account' should not be validated if query_transfers() was called by a permit - // call, for compatibility with non-Secret addresses. - let account = Addr::unchecked(account); - - let (txs, total) = StoredLegacyTransfer::get_transfers( - deps.storage, - account, - page, - page_size, - should_filter_decoys, - )?; - - let result = QueryAnswer::TransferHistory { - txs, - total: Some(total), - }; - to_binary(&result) -} - pub fn query_transactions( deps: Deps, account: String, page: u32, page_size: u32, - should_filter_decoys: bool, ) -> StdResult<Binary> { - // Notice that if query_transactions() was called by a viewking-key call, the address of + if page_size == 0 { + return Err(StdError::generic_err("invalid page size")); + } + + // Notice that if query_transactions() was called by a viewing-key call, the address of // 'account' has already been validated. // The address of 'account' should not be validated if query_transactions() was called by a // permit call, for compatibility with non-Secret addresses. let account = Addr::unchecked(account); + let account_raw = deps.api.addr_canonicalize(account.as_str())?; + + let start = page * page_size; + let mut end = start + page_size; // one more than end index + + // first check if there are any transactions in dwb + let dwb = DWB.load(deps.storage)?; + let dwb_index = dwb.recipient_match(&account_raw); + let mut txs_in_dwb = vec![]; + let txs_in_dwb_count = dwb.entries[dwb_index].list_len()?; + if dwb_index > 0 && txs_in_dwb_count > 0 && start < txs_in_dwb_count as u32 { + // skip if start is after buffer entries + let head_node_index = dwb.entries[dwb_index].head_node()?; + if head_node_index > 0 { + let head_node = TX_NODES + .add_suffix(&head_node_index.to_be_bytes()) + .load(deps.storage)?; + txs_in_dwb = head_node.to_vec(deps.storage, deps.api)?; + } + } + + //let account_slice = account_raw.as_slice(); + let account_stored_entry = stored_entry(deps.storage, &account_raw)?; + let settled_tx_count = stored_tx_count(deps.storage, &account_stored_entry)?; + let total = txs_in_dwb_count as u32 + settled_tx_count as u32; + if end > total { + end = total; + } - let (txs, total) = - StoredExtendedTx::get_txs(deps.storage, account, page, page_size, should_filter_decoys)?; + let mut txs: Vec<Tx> = vec![]; + + let txs_in_dwb_count = txs_in_dwb_count as u32; + if start < txs_in_dwb_count && end < txs_in_dwb_count { + // option 1, start and end are both in dwb + //println!("OPTION 1"); + txs = txs_in_dwb[start as usize..end as usize].to_vec(); // reverse chronological + } else if start < txs_in_dwb_count && end >= txs_in_dwb_count { + // option 2, start is in dwb and end is in settled txs + // in this case, we do not need to search for txs, just begin at last bundle and move backwards + //println!("OPTION 2"); + txs = txs_in_dwb[start as usize..].to_vec(); // reverse chronological + let mut txs_left = (end - start).saturating_sub(txs.len() as u32); + if let Some(entry) = account_stored_entry { + let tx_bundles_idx_len = entry.history_len()?; + if tx_bundles_idx_len > 0 { + let mut bundle_idx = tx_bundles_idx_len - 1; + loop { + let tx_bundle = entry.get_tx_bundle_at(deps.storage, bundle_idx.clone())?; + let head_node = TX_NODES + .add_suffix(&tx_bundle.head_node.to_be_bytes()) + .load(deps.storage)?; + let list_len = tx_bundle.list_len as u32; + if txs_left <= list_len { + txs.extend_from_slice( + &head_node.to_vec(deps.storage, deps.api)?[0..txs_left as usize], + ); + break; + } + txs.extend(head_node.to_vec(deps.storage, deps.api)?); + txs_left = txs_left.saturating_sub(list_len); + if bundle_idx > 0 { + bundle_idx -= 1; + } else { + break; + } + } + } + } + } else if start >= txs_in_dwb_count { + // option 3, start is not in dwb + // in this case, search for where the beginning bundle is using binary search + + // bundle tx offsets are chronological, but we need reverse chronological + // so get the settled start index as if order is reversed + //println!("OPTION 3"); + let settled_start = settled_tx_count + .saturating_sub(start - txs_in_dwb_count) + .saturating_sub(1); + + if let Some((bundle_idx, tx_bundle, start_at)) = + find_start_bundle(deps.storage, &account_raw, settled_start)? + { + let mut txs_left = end - start; + + let head_node = TX_NODES + .add_suffix(&tx_bundle.head_node.to_be_bytes()) + .load(deps.storage)?; + let list_len = tx_bundle.list_len as u32; + if start_at + txs_left <= list_len { + // this first bundle has all the txs we need + txs = head_node.to_vec(deps.storage, deps.api)? + [start_at as usize..(start_at + txs_left) as usize] + .to_vec(); + } else { + // get the rest of the txs in this bundle and then go back through history + txs = head_node.to_vec(deps.storage, deps.api)?[start_at as usize..].to_vec(); + txs_left = txs_left.saturating_sub(list_len - start_at); + + if bundle_idx > 0 && txs_left > 0 { + // get the next earlier bundle + let mut bundle_idx = bundle_idx - 1; + if let Some(entry) = account_stored_entry { + loop { + let tx_bundle = + entry.get_tx_bundle_at(deps.storage, bundle_idx.clone())?; + let head_node = TX_NODES + .add_suffix(&tx_bundle.head_node.to_be_bytes()) + .load(deps.storage)?; + let list_len = tx_bundle.list_len as u32; + if txs_left <= list_len { + txs.extend_from_slice( + &head_node.to_vec(deps.storage, deps.api)? + [0..txs_left as usize], + ); + break; + } + txs.extend(head_node.to_vec(deps.storage, deps.api)?); + txs_left = txs_left.saturating_sub(list_len); + if bundle_idx > 0 { + bundle_idx -= 1; + } else { + break; + } + } + } + } + } + } + } let result = QueryAnswer::TransactionHistory { txs, - total: Some(total), + total: Some(total as u64), }; to_binary(&result) } pub fn query_balance(deps: Deps, account: String) -> StdResult<Binary> { - // Notice that if query_balance() was called by a viewking-key call, the address of 'account' + // Notice that if query_balance() was called by a viewing key call, the address of 'account' // has already been validated. // The address of 'account' should not be validated if query_balance() was called by a permit // call, for compatibility with non-Secret addresses. let account = Addr::unchecked(account); + let account = deps.api.addr_canonicalize(account.as_str())?; - let amount = Uint128::new(BalancesStore::load(deps.storage, &account)); + let mut amount = stored_balance(deps.storage, &account)?; + let dwb = DWB.load(deps.storage)?; + let dwb_index = dwb.recipient_match(&account); + if dwb_index > 0 { + amount = amount.saturating_add(dwb.entries[dwb_index].amount()? as u128); + } + let amount = Uint128::new(amount); let response = QueryAnswer::Balance { amount }; to_binary(&response) } @@ -709,6 +763,203 @@ fn query_minters(deps: Deps) -> StdResult<Binary> { to_binary(&response) } +// ***************** +// SNIP-52 query functions +// ***************** + +/// +/// ListChannels query +/// +/// Public query to list all notification channels. +/// +fn query_list_channels(deps: Deps) -> StdResult<Binary> { + let channels: Vec<String> = CHANNELS + .iter(deps.storage)? + .map(|channel| channel.unwrap()) + .collect(); + to_binary(&QueryAnswer::ListChannels { channels }) +} + +/// +/// ChannelInfo query +/// +/// Authenticated query allows clients to obtain the seed, +/// and Notification ID of an event for a specific tx_hash, for a specific channel. +/// +fn query_channel_info( + deps: Deps, + env: Env, + channels: Vec<String>, + txhash: Option<String>, + sender_raw: CanonicalAddr, +) -> StdResult<Binary> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let seed = get_seed(&sender_raw, secret)?; + let mut channels_data = vec![]; + for channel in channels { + let answer_id; + if let Some(tx_hash) = &txhash { + answer_id = Some(notification_id(&seed, &channel, tx_hash)?); + } else { + answer_id = None; + } + match channel.as_str() { + ReceivedNotificationData::CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "txhash".to_string(), + channel, + answer_id, + parameters: None, + data: None, + next_id: None, + counter: None, + cddl: Some(ReceivedNotificationData::CDDL_SCHEMA.to_string()), + }; + channels_data.push(channel_info_data); + } + SpentNotificationData::CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "txhash".to_string(), + channel, + answer_id, + parameters: None, + data: None, + next_id: None, + counter: None, + cddl: Some(SpentNotificationData::CDDL_SCHEMA.to_string()), + }; + channels_data.push(channel_info_data); + } + AllowanceNotificationData::CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "txhash".to_string(), + channel, + answer_id, + parameters: None, + data: None, + next_id: None, + counter: None, + cddl: Some(AllowanceNotificationData::CDDL_SCHEMA.to_string()), + }; + channels_data.push(channel_info_data); + } + MULTI_RECEIVED_CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "bloom".to_string(), + channel, + answer_id, + parameters: Some(BloomParameters { + m: 512, + k: MULTI_RECEIVED_CHANNEL_BLOOM_K, + h: "sha256".to_string(), + }), + data: Some(Descriptor { + r#type: format!("packet[{}]", MULTI_RECEIVED_CHANNEL_BLOOM_N), + version: "1".to_string(), + packet_size: MULTI_RECEIVED_CHANNEL_PACKET_SIZE, + data: StructDescriptor { + r#type: "struct".to_string(), + label: "transfer".to_string(), + members: vec![ + FlatDescriptor { + r#type: "uint128".to_string(), + label: "amount".to_string(), + description: Some( + "The transfer amount in base denomination".to_string(), + ), + }, + FlatDescriptor { + r#type: "bytes8".to_string(), + label: "spender".to_string(), + description: Some( + "The last 8 bytes of the sender's canonical address" + .to_string(), + ), + }, + ], + }, + }), + counter: None, + next_id: None, + cddl: None, + }; + channels_data.push(channel_info_data); + } + MULTI_SPENT_CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "bloom".to_string(), + channel, + answer_id, + parameters: Some(BloomParameters { + m: 512, + k: MULTI_SPENT_CHANNEL_BLOOM_K, + h: "sha256".to_string(), + }), + data: Some(Descriptor { + r#type: format!("packet[{}]", MULTI_SPENT_CHANNEL_BLOOM_N), + version: "1".to_string(), + packet_size: MULTI_SPENT_CHANNEL_PACKET_SIZE, + data: StructDescriptor { + r#type: "struct".to_string(), + label: "transfer".to_string(), + members: vec![ + FlatDescriptor { + r#type: "uint128".to_string(), + label: "amount".to_string(), + description: Some( + "The transfer amount in base denomination".to_string(), + ), + }, + FlatDescriptor { + r#type: "uint128".to_string(), + label: "balance".to_string(), + description: Some( + "Spender's new balance after the transfer".to_string(), + ), + }, + FlatDescriptor { + r#type: "bytes8".to_string(), + label: "recipient".to_string(), + description: Some( + "The last 8 bytes of the recipient's canonical address" + .to_string(), + ), + }, + ], + }, + }), + counter: None, + next_id: None, + cddl: None, + }; + channels_data.push(channel_info_data); + } + _ => { + return Err(StdError::generic_err(format!( + "`{}` channel is undefined", + channel + ))); + } + } + } + + //Ok(Binary(vec![])) + //let schema = CHANNEL_SCHEMATA.get(deps.storage, &channel); + + to_binary(&QueryAnswer::ChannelInfo { + as_of_block: Uint64::from(env.block.height), + channels: channels_data, + seed, + }) +} + +// ***************** +// End SNIP-52 query functions +// ***************** + +// execute functions + fn change_admin(deps: DepsMut, info: MessageInfo, address: String) -> StdResult<Response> { let address = deps.api.addr_validate(address.as_str())?; @@ -780,37 +1031,30 @@ fn remove_supported_denoms( #[allow(clippy::too_many_arguments)] fn try_mint_impl( deps: &mut DepsMut, + rng: &mut ContractPrng, minter: Addr, recipient: Addr, amount: Uint128, denom: String, memo: Option<String>, block: &cosmwasm_std::BlockInfo, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, ) -> StdResult<()> { let raw_amount = amount.u128(); + let raw_recipient = deps.api.addr_canonicalize(recipient.as_str())?; + let raw_minter = deps.api.addr_canonicalize(minter.as_str())?; - BalancesStore::update_balance( + perform_mint( deps.storage, - &recipient, + rng, + &raw_minter, + &raw_recipient, raw_amount, - true, - "", - &decoys, - &account_random_pos, - )?; - - store_mint( - deps.storage, - minter, - recipient, - amount, denom, memo, block, - &decoys, - &account_random_pos, + #[cfg(feature = "gas_tracking")] + tracker, )?; Ok(()) @@ -821,12 +1065,14 @@ fn try_mint( mut deps: DepsMut, env: Env, info: MessageInfo, + rng: &mut ContractPrng, recipient: String, amount: Uint128, memo: Option<String>, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let recipient = deps.api.addr_validate(recipient.as_str())?; let constants = CONFIG.load(deps.storage)?; @@ -848,29 +1094,56 @@ fn try_mint( let minted_amount = safe_add(&mut total_supply, amount.u128()); TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + // Note that even when minted_amount is equal to 0 we still want to perform the operations for logic consistency try_mint_impl( &mut deps, + rng, info.sender, - recipient, + recipient.clone(), Uint128::new(minted_amount), constants.symbol, memo, &env.block, - decoys, - account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::Mint { status: Success })?)) + let received_notification = Notification::new( + recipient, + ReceivedNotificationData { + amount: minted_amount, + sender: None, + }, + ) + .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; + + let resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::Mint { status: Success })?) + .add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ); + + #[cfg(feature = "gas_tracking")] + return Ok(resp.add_gas_tracker(tracker)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) } fn try_batch_mint( mut deps: DepsMut, env: Env, info: MessageInfo, + rng: &mut ContractPrng, actions: Vec<batch::MintAction>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let constants = CONFIG.load(deps.storage)?; if !constants.mint_is_enabled { @@ -888,27 +1161,59 @@ fn try_batch_mint( let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; + let mut notifications = vec![]; // Quick loop to check that the total of amounts is valid for action in actions { let actual_amount = safe_add(&mut total_supply, action.amount.u128()); let recipient = deps.api.addr_validate(action.recipient.as_str())?; + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + try_mint_impl( &mut deps, + rng, info.sender.clone(), - recipient, + recipient.clone(), Uint128::new(actual_amount), constants.symbol.clone(), action.memo, &env.block, - action.decoys, - account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; + notifications.push(Notification::new ( + recipient, + ReceivedNotificationData { + amount: actual_amount, + sender: None, + }, + )); } + let tx_hash = env + .transaction + .clone() + .ok_or(StdError::generic_err("no tx hash found"))? + .hash; + let received_data = multi_received_data( + deps.api, + notifications, + &tx_hash, + env.block.random.unwrap(), + secret, + )?; + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::BatchMint { status: Success })?)) + Ok(Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchMint { status: Success })?) + .add_attribute_plaintext( + format!("snip52:#{}", MULTI_RECEIVED_CHANNEL_ID), + Binary::from(received_data).to_base64(), + ) + ) } pub fn try_set_key(deps: DepsMut, info: MessageInfo, key: String) -> StdResult<Response> { @@ -924,14 +1229,17 @@ pub fn try_create_key( deps: DepsMut, env: Env, info: MessageInfo, - entropy: String, + entropy: Option<String>, + rng: &mut ContractPrng, ) -> StdResult<Response> { + let entropy = [entropy.unwrap_or_default().as_bytes(), &rng.rand_bytes()].concat(); + let key = ViewingKey::create( deps.storage, &info, &env, info.sender.as_str(), - entropy.as_ref(), + &entropy, ); Ok(Response::new().set_data(to_binary(&ExecuteAnswer::CreateViewingKey { key })?)) @@ -1041,8 +1349,7 @@ fn try_deposit( deps: DepsMut, env: Env, info: MessageInfo, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, + rng: &mut ContractPrng, ) -> StdResult<Response> { let constants = CONFIG.load(deps.storage)?; @@ -1075,29 +1382,29 @@ fn try_deposit( raw_amount = safe_add(&mut total_supply, raw_amount); TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - let sender_address = &info.sender; + let sender_address = deps.api.addr_canonicalize(info.sender.as_str())?; - BalancesStore::update_balance( - deps.storage, - sender_address, - raw_amount, - true, - "", - &decoys, - &account_random_pos, - )?; + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); - store_deposit( + perform_deposit( deps.storage, - sender_address, - Uint128::new(raw_amount), + rng, + &sender_address, + raw_amount, "uscrt".to_string(), &env.block, - &decoys, - &account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::Deposit { status: Success })?)) + let resp = Response::new().set_data(to_binary(&ExecuteAnswer::Deposit { status: Success })?); + + #[cfg(feature = "gas_tracking")] + return Ok(resp.add_gas_tracker(tracker)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) } fn try_redeem( @@ -1106,8 +1413,6 @@ fn try_redeem( info: MessageInfo, amount: Uint128, denom: Option<String>, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { let constants = CONFIG.load(deps.storage)?; if !constants.redeem_is_enabled { @@ -1133,19 +1438,30 @@ fn try_redeem( )); }; - let sender_address = &info.sender; + let sender_address = deps.api.addr_canonicalize(info.sender.as_str())?; let amount_raw = amount.u128(); - BalancesStore::update_balance( + let tx_id = store_redeem_action(deps.storage, amount.u128(), constants.symbol, &env.block)?; + + // load delayed write buffer + let mut dwb = DWB.load(deps.storage)?; + + #[cfg(feature = "gas_tracking")] + let mut tracker = GasTracker::new(deps.api); + + // settle the signer's account in buffer + dwb.settle_sender_or_owner_account( deps.storage, - sender_address, + &sender_address, + tx_id, amount_raw, - false, "redeem", - &decoys, - &account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; + DWB.save(deps.storage, &dwb)?; + let total_supply = TOTAL_SUPPLY.load(deps.storage)?; if let Some(total_supply) = total_supply.checked_sub(amount_raw) { TOTAL_SUPPLY.save(deps.storage, &total_supply)?; @@ -1170,16 +1486,6 @@ fn try_redeem( amount, }]; - store_redeem( - deps.storage, - sender_address, - amount, - constants.symbol, - &env.block, - &decoys, - &account_random_pos, - )?; - let message = CosmosMsg::Bank(BankMsg::Send { to_address: info.sender.clone().into_string(), amount: withdrawal_coins, @@ -1192,38 +1498,50 @@ fn try_redeem( #[allow(clippy::too_many_arguments)] fn try_transfer_impl( deps: &mut DepsMut, + rng: &mut ContractPrng, sender: &Addr, recipient: &Addr, amount: Uint128, + denom: String, memo: Option<String>, block: &cosmwasm_std::BlockInfo, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, -) -> StdResult<()> { - perform_transfer( - deps.storage, - sender, - recipient, - amount.u128(), - &decoys, - &account_random_pos, - )?; + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<(Notification<ReceivedNotificationData>, Notification<SpentNotificationData>)> { + let raw_sender = deps.api.addr_canonicalize(sender.as_str())?; + let raw_recipient = deps.api.addr_canonicalize(recipient.as_str())?; - let symbol = CONFIG.load(deps.storage)?.symbol; - store_transfer( + let sender_balance = perform_transfer( deps.storage, - sender, - sender, - recipient, - amount, - symbol, + rng, + &raw_sender, + &raw_recipient, + &raw_sender, + amount.u128(), + denom, memo, block, - &decoys, - &account_random_pos, + #[cfg(feature = "gas_tracking")] + tracker, )?; + let received_notification = Notification::new( + recipient.clone(), + ReceivedNotificationData { + amount: amount.u128(), + sender: Some(sender.clone()), + } + ); - Ok(()) + let spent_notification = Notification::new ( + sender.clone(), + SpentNotificationData { + amount: amount.u128(), + actions: 1, + recipient: Some(recipient.clone()), + balance: sender_balance, + } + ); + + Ok((received_notification, spent_notification)) } #[allow(clippy::too_many_arguments)] @@ -1231,54 +1549,160 @@ fn try_transfer( mut deps: DepsMut, env: Env, info: MessageInfo, + rng: &mut ContractPrng, recipient: String, amount: Uint128, memo: Option<String>, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { - let recipient = deps.api.addr_validate(recipient.as_str())?; + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + + let recipient: Addr = deps.api.addr_validate(recipient.as_str())?; + + let symbol = CONFIG.load(deps.storage)?.symbol; + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); - try_transfer_impl( + let (received_notification, spent_notification) = try_transfer_impl( &mut deps, + rng, &info.sender, &recipient, amount, + symbol, memo, &env.block, - decoys, - account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::Transfer { status: Success })?)) -} + #[cfg(feature = "gas_tracking")] + let mut group1 = tracker.group("try_transfer.rest"); -fn try_batch_transfer( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - actions: Vec<batch::TransferAction>, - account_random_pos: Option<usize>, -) -> StdResult<Response> { + let received_notification = received_notification.to_txhash_notification( + deps.api, + &env, + secret, + Some(NOTIFICATION_BLOCK_SIZE), + )?; + + let spent_notification = spent_notification.to_txhash_notification( + deps.api, + &env, + secret, + Some(NOTIFICATION_BLOCK_SIZE) + )?; + + let resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::Transfer { status: Success })?) + .add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + + #[cfg(feature = "gas_tracking")] + group1.log("rest"); + + #[cfg(feature = "gas_tracking")] + return Ok(resp.add_gas_tracker(tracker)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) +} + +fn try_batch_transfer( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + actions: Vec<batch::TransferAction>, +) -> StdResult<Response> { + let num_actions = actions.len(); + if num_actions == 0 { + return Ok(Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchTransfer { status: Success })?) + ); + } + + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + + let symbol = CONFIG.load(deps.storage)?.symbol; + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + let mut notifications = vec![]; for action in actions { let recipient = deps.api.addr_validate(action.recipient.as_str())?; - try_transfer_impl( + let (received_notification, spent_notification) = try_transfer_impl( &mut deps, + rng, &info.sender, &recipient, action.amount, + symbol.clone(), action.memo, &env.block, - action.decoys, - account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; + notifications.push((received_notification, spent_notification)); } - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::BatchTransfer { - status: Success, - })?), + let tx_hash = env + .transaction + .clone() + .ok_or(StdError::generic_err("no tx hash found"))? + .hash; + let (received_notifications, spent_notifications): ( + Vec<Notification<ReceivedNotificationData>>, + Vec<Notification<SpentNotificationData>>, + ) = notifications.into_iter().unzip(); + let received_data = multi_received_data( + deps.api, + received_notifications, + &tx_hash, + env.block.random.clone().unwrap(), + secret, + )?; + + let total_amount_spent = spent_notifications + .iter() + .fold(0u128, |acc, notification| acc.saturating_add(notification.data.amount)); + + let spent_notification = Notification::new ( + info.sender, + SpentNotificationData { + amount: total_amount_spent, + actions: num_actions as u32, + recipient: spent_notifications[0].data.recipient.clone(), + balance: spent_notifications.last().unwrap().data.balance, + } ) + .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; + + let resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchTransfer { status: Success })?) + .add_attribute_plaintext( + format!("snip52:#{}", MULTI_RECEIVED_CHANNEL_ID), + Binary::from(received_data).to_base64(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + + #[cfg(feature = "gas_tracking")] + return Ok(resp.add_gas_tracker(tracker)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) } #[allow(clippy::too_many_arguments)] @@ -1314,26 +1738,29 @@ fn try_add_receiver_api_callback( #[allow(clippy::too_many_arguments)] fn try_send_impl( deps: &mut DepsMut, + rng: &mut ContractPrng, messages: &mut Vec<CosmosMsg>, sender: Addr, recipient: Addr, recipient_code_hash: Option<String>, amount: Uint128, + denom: String, memo: Option<String>, msg: Option<Binary>, block: &cosmwasm_std::BlockInfo, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, -) -> StdResult<()> { - try_transfer_impl( + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<(Notification<ReceivedNotificationData>, Notification<SpentNotificationData>)> { + let (received_notification, spent_notification) = try_transfer_impl( deps, + rng, &sender, &recipient, amount, + denom, memo.clone(), block, - decoys, - account_random_pos, + #[cfg(feature = "gas_tracking")] + tracker, )?; try_add_receiver_api_callback( @@ -1348,7 +1775,7 @@ fn try_send_impl( memo, )?; - Ok(()) + Ok((received_notification, spent_notification)) } #[allow(clippy::too_many_arguments)] @@ -1356,64 +1783,160 @@ fn try_send( mut deps: DepsMut, env: Env, info: MessageInfo, + rng: &mut ContractPrng, recipient: String, recipient_code_hash: Option<String>, amount: Uint128, memo: Option<String>, msg: Option<Binary>, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let recipient = deps.api.addr_validate(recipient.as_str())?; let mut messages = vec![]; - try_send_impl( + let symbol = CONFIG.load(deps.storage)?.symbol; + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + let (received_notification, spent_notification) = try_send_impl( &mut deps, + rng, &mut messages, info.sender, recipient, recipient_code_hash, amount, + symbol, memo, msg, &env.block, - decoys, - account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; - Ok(Response::new() + let received_notification = received_notification.to_txhash_notification( + deps.api, + &env, + secret, + Some(NOTIFICATION_BLOCK_SIZE) + )?; + let spent_notification = + spent_notification.to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; + + let resp = Response::new() .add_messages(messages) - .set_data(to_binary(&ExecuteAnswer::Send { status: Success })?)) + .set_data(to_binary(&ExecuteAnswer::Send { status: Success })?) + .add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + + #[cfg(feature = "gas_tracking")] + return Ok(resp.add_gas_tracker(tracker)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) } fn try_batch_send( mut deps: DepsMut, env: Env, info: MessageInfo, + rng: &mut ContractPrng, actions: Vec<batch::SendAction>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { + let num_actions = actions.len(); + if num_actions == 0 { + return Ok(Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchSend { status: Success })?) + ); + } + + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let mut messages = vec![]; + + let mut notifications = vec![]; + let num_actions: usize = actions.len(); + + let symbol = CONFIG.load(deps.storage)?.symbol; + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + for action in actions { let recipient = deps.api.addr_validate(action.recipient.as_str())?; - try_send_impl( + let (received_notification, spent_notification) = try_send_impl( &mut deps, + rng, &mut messages, info.sender.clone(), recipient, action.recipient_code_hash, action.amount, + symbol.clone(), action.memo, action.msg, &env.block, - action.decoys, - account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; + notifications.push((received_notification, spent_notification)); } + let tx_hash = env + .transaction + .clone() + .ok_or(StdError::generic_err("no tx hash found"))? + .hash; + + let (received_notifications, spent_notifications): ( + Vec<Notification<ReceivedNotificationData>>, + Vec<Notification<SpentNotificationData>>, + ) = notifications.into_iter().unzip(); + let received_data = multi_received_data( + deps.api, + received_notifications, + &tx_hash, + env.block.random.clone().unwrap(), + secret, + )?; + + let total_amount_spent = spent_notifications + .iter() + .fold(0u128, |acc, notification| acc + notification.data.amount); + + let spent_notification = Notification::new ( + info.sender, + SpentNotificationData { + amount: total_amount_spent, + actions: num_actions as u32, + recipient: spent_notifications[0].data.recipient.clone(), + balance: spent_notifications.last().unwrap().data.balance, + } + ) + .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; + Ok(Response::new() .add_messages(messages) - .set_data(to_binary(&ExecuteAnswer::BatchSend { status: Success })?)) + .set_data(to_binary(&ExecuteAnswer::BatchSend { status: Success })?) + .add_attribute_plaintext( + format!("snip52:#{}", MULTI_RECEIVED_CHANNEL_ID), + Binary::from(received_data).to_base64(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ) + ) } fn try_register_receive( @@ -1461,43 +1984,58 @@ fn use_allowance( #[allow(clippy::too_many_arguments)] fn try_transfer_from_impl( deps: &mut DepsMut, + rng: &mut ContractPrng, env: &Env, spender: &Addr, owner: &Addr, recipient: &Addr, amount: Uint128, + denom: String, memo: Option<String>, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, -) -> StdResult<()> { +) -> StdResult<(Notification<ReceivedNotificationData>, Notification<SpentNotificationData>)> { let raw_amount = amount.u128(); + let raw_spender = deps.api.addr_canonicalize(spender.as_str())?; + let raw_owner = deps.api.addr_canonicalize(owner.as_str())?; + let raw_recipient = deps.api.addr_canonicalize(recipient.as_str())?; use_allowance(deps.storage, env, owner, spender, raw_amount)?; - perform_transfer( - deps.storage, - owner, - recipient, - raw_amount, - &decoys, - &account_random_pos, - )?; + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); - let symbol = CONFIG.load(deps.storage)?.symbol; - store_transfer( + let owner_balance = perform_transfer( deps.storage, - owner, - spender, - recipient, - amount, - symbol, + rng, + &raw_owner, + &raw_recipient, + &raw_spender, + raw_amount, + denom, memo, &env.block, - &decoys, - &account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; - Ok(()) + let received_notification = Notification::new( + recipient.clone(), + ReceivedNotificationData { + amount: amount.u128(), + sender: Some(owner.clone()), + } + ); + + let spent_notification = Notification::new ( + owner.clone(), + SpentNotificationData { + amount: amount.u128(), + actions: 1, + recipient: Some(recipient.clone()), + balance: owner_balance, + } + ); + + Ok((received_notification, spent_notification)) } #[allow(clippy::too_many_arguments)] @@ -1505,57 +2043,123 @@ fn try_transfer_from( mut deps: DepsMut, env: &Env, info: MessageInfo, + rng: &mut ContractPrng, owner: String, recipient: String, amount: Uint128, memo: Option<String>, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let owner = deps.api.addr_validate(owner.as_str())?; let recipient = deps.api.addr_validate(recipient.as_str())?; - try_transfer_from_impl( + let symbol = CONFIG.load(deps.storage)?.symbol; + let (received_notification, spent_notification) = try_transfer_from_impl( &mut deps, + rng, env, &info.sender, &owner, &recipient, amount, + symbol, memo, - decoys, - account_random_pos, + )?; + let received_notification = received_notification.to_txhash_notification( + deps.api, + &env, + secret, + Some(NOTIFICATION_BLOCK_SIZE), + )?; + + let spent_notification = spent_notification.to_txhash_notification( + deps.api, + &env, + secret, + Some(NOTIFICATION_BLOCK_SIZE) )?; - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::TransferFrom { status: Success })?)) + Ok( + Response::new() + .set_data(to_binary(&ExecuteAnswer::TransferFrom { status: Success })?) + .add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ) + ) } fn try_batch_transfer_from( mut deps: DepsMut, env: &Env, info: MessageInfo, + rng: &mut ContractPrng, actions: Vec<batch::TransferFromAction>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + + let mut notifications = vec![]; + + let symbol = CONFIG.load(deps.storage)?.symbol; for action in actions { let owner = deps.api.addr_validate(action.owner.as_str())?; let recipient = deps.api.addr_validate(action.recipient.as_str())?; - try_transfer_from_impl( + let (received_notification, spent_notification) = try_transfer_from_impl( &mut deps, + rng, env, &info.sender, &owner, &recipient, action.amount, + symbol.clone(), action.memo, - action.decoys, - account_random_pos, )?; + notifications.push((received_notification, spent_notification)); } + let tx_hash = env + .transaction + .clone() + .ok_or(StdError::generic_err("no tx hash found"))? + .hash; + + let (received_notifications, spent_notifications): ( + Vec<Notification<ReceivedNotificationData>>, + Vec<Notification<SpentNotificationData>>, + ) = notifications.into_iter().unzip(); + let received_data = multi_received_data( + deps.api, + received_notifications, + &tx_hash, + env.block.random.clone().unwrap(), + secret, + )?; + let spent_data = multi_spent_data( + deps.api, + spent_notifications, + &tx_hash, + env.block.random.clone().unwrap(), + secret, + )?; + Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::BatchTransferFrom { - status: Success, - })?), + Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchTransferFrom {status: Success})?) + .add_attribute_plaintext( + format!("snip52:#{}", MULTI_RECEIVED_CHANNEL_ID), + Binary::from(received_data).to_base64(), + ) + .add_attribute_plaintext( + format!("snip52:#{}", MULTI_SPENT_CHANNEL_ID), + Binary::from(spent_data).to_base64(), + ) ) } @@ -1564,6 +2168,7 @@ fn try_send_from_impl( deps: &mut DepsMut, env: Env, info: &MessageInfo, + rng: &mut ContractPrng, messages: &mut Vec<CosmosMsg>, owner: Addr, recipient: Addr, @@ -1571,20 +2176,19 @@ fn try_send_from_impl( amount: Uint128, memo: Option<String>, msg: Option<Binary>, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, -) -> StdResult<()> { +) -> StdResult<(Notification<ReceivedNotificationData>, Notification<SpentNotificationData>)> { let spender = info.sender.clone(); - try_transfer_from_impl( + let symbol = CONFIG.load(deps.storage)?.symbol; + let (received_notification, spent_notification) = try_transfer_from_impl( deps, + rng, &env, &spender, &owner, &recipient, amount, + symbol, memo.clone(), - decoys, - account_random_pos, )?; try_add_receiver_api_callback( @@ -1599,7 +2203,7 @@ fn try_send_from_impl( memo, )?; - Ok(()) + Ok((received_notification, spent_notification)) } #[allow(clippy::too_many_arguments)] @@ -1607,22 +2211,25 @@ fn try_send_from( mut deps: DepsMut, env: Env, info: &MessageInfo, + rng: &mut ContractPrng, owner: String, recipient: String, recipient_code_hash: Option<String>, amount: Uint128, memo: Option<String>, msg: Option<Binary>, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let owner = deps.api.addr_validate(owner.as_str())?; let recipient = deps.api.addr_validate(recipient.as_str())?; let mut messages = vec![]; - try_send_from_impl( + let (received_notification, spent_notification) = try_send_from_impl( &mut deps, - env, + env.clone(), info, + rng, &mut messages, owner, recipient, @@ -1630,31 +2237,52 @@ fn try_send_from( amount, memo, msg, - decoys, - account_random_pos, )?; + let received_notification = received_notification.to_txhash_notification( + deps.api, + &env, + secret, + Some(NOTIFICATION_BLOCK_SIZE), + )?; + let spent_notification = + spent_notification.to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; + Ok(Response::new() .add_messages(messages) - .set_data(to_binary(&ExecuteAnswer::SendFrom { status: Success })?)) + .set_data(to_binary(&ExecuteAnswer::SendFrom { status: Success })?) + .add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ) + ) } fn try_batch_send_from( mut deps: DepsMut, env: Env, info: &MessageInfo, + rng: &mut ContractPrng, actions: Vec<batch::SendFromAction>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let mut messages = vec![]; + let mut notifications = vec![]; for action in actions { let owner = deps.api.addr_validate(action.owner.as_str())?; let recipient = deps.api.addr_validate(action.recipient.as_str())?; - try_send_from_impl( + let (received_notification, spent_notification) = try_send_from_impl( &mut deps, env.clone(), info, + rng, &mut messages, owner, recipient, @@ -1662,16 +2290,49 @@ fn try_batch_send_from( action.amount, action.memo, action.msg, - action.decoys, - account_random_pos, )?; + notifications.push((received_notification, spent_notification)); } + let tx_hash = env + .transaction + .clone() + .ok_or(StdError::generic_err("no tx hash found"))? + .hash; + + let (received_notifications, spent_notifications): ( + Vec<Notification<ReceivedNotificationData>>, + Vec<Notification<SpentNotificationData>>, + ) = notifications.into_iter().unzip(); + let received_data = multi_received_data( + deps.api, + received_notifications, + &tx_hash, + env.block.random.clone().unwrap(), + secret, + )?; + let spent_data = multi_spent_data( + deps.api, + spent_notifications, + &tx_hash, + env.block.random.clone().unwrap(), + secret, + )?; + Ok(Response::new() .add_messages(messages) .set_data(to_binary(&ExecuteAnswer::BatchSendFrom { status: Success, - })?)) + })?) + .add_attribute_plaintext( + format!("snip52:#{}", MULTI_RECEIVED_CHANNEL_ID), + Binary::from(received_data).to_base64(), + ) + .add_attribute_plaintext( + format!("snip52:#{}", MULTI_SPENT_CHANNEL_ID), + Binary::from(spent_data).to_base64(), + ) + ) } #[allow(clippy::too_many_arguments)] @@ -1682,10 +2343,12 @@ fn try_burn_from( owner: String, amount: Uint128, memo: Option<String>, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let owner = deps.api.addr_validate(owner.as_str())?; + let raw_owner = deps.api.addr_canonicalize(owner.as_str())?; let constants = CONFIG.load(deps.storage)?; if !constants.burn_is_enabled { return Err(StdError::generic_err( @@ -1695,16 +2358,48 @@ fn try_burn_from( let raw_amount = amount.u128(); use_allowance(deps.storage, env, &owner, &info.sender, raw_amount)?; + let raw_burner = deps.api.addr_canonicalize(info.sender.as_str())?; - BalancesStore::update_balance( + let tx_id = store_burn_action( deps.storage, - &owner, + raw_owner.clone(), + raw_burner.clone(), + raw_amount, + constants.symbol, + memo, + &env.block, + )?; + + // load delayed write buffer + let mut dwb = DWB.load(deps.storage)?; + + #[cfg(feature = "gas_tracking")] + let mut tracker = GasTracker::new(deps.api); + + // settle the owner's account in buffer + let owner_balance = dwb.settle_sender_or_owner_account( + deps.storage, + &raw_owner, + tx_id, raw_amount, - false, "burn", - &decoys, - &account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; + if raw_burner != raw_owner { + // also settle sender's account + dwb.settle_sender_or_owner_account( + deps.storage, + &raw_burner, + tx_id, + 0, + "burn", + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + } + + DWB.save(deps.storage, &dwb)?; // remove from supply let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; @@ -1718,19 +2413,25 @@ fn try_burn_from( } TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - store_burn( - deps.storage, + let spent_notification = Notification::new ( owner, - info.sender, - amount, - constants.symbol, - memo, - &env.block, - &decoys, - &account_random_pos, - )?; + SpentNotificationData { + amount: raw_amount, + actions: 1, + recipient: None, + balance: owner_balance, + } + ) + .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::BurnFrom { status: Success })?)) + Ok( + Response::new() + .set_data(to_binary(&ExecuteAnswer::BurnFrom { status: Success })?) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ) + ) } fn try_batch_burn_from( @@ -1738,8 +2439,10 @@ fn try_batch_burn_from( env: &Env, info: MessageInfo, actions: Vec<batch::BurnFromAction>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let constants = CONFIG.load(deps.storage)?; if !constants.burn_is_enabled { return Err(StdError::generic_err( @@ -1747,23 +2450,55 @@ fn try_batch_burn_from( )); } - let spender = info.sender; + let raw_spender = deps.api.addr_canonicalize(info.sender.as_str())?; let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; + let mut spent_notifications = vec![]; for action in actions { let owner = deps.api.addr_validate(action.owner.as_str())?; + let raw_owner = deps.api.addr_canonicalize(owner.as_str())?; let amount = action.amount.u128(); - use_allowance(deps.storage, env, &owner, &spender, amount)?; + use_allowance(deps.storage, env, &owner, &info.sender, amount)?; - BalancesStore::update_balance( + let tx_id = store_burn_action( deps.storage, - &owner, + raw_owner.clone(), + raw_spender.clone(), + amount, + constants.symbol.clone(), + action.memo.clone(), + &env.block, + )?; + + // load delayed write buffer + let mut dwb = DWB.load(deps.storage)?; + + #[cfg(feature = "gas_tracking")] + let mut tracker = GasTracker::new(deps.api); + + // settle the owner's account in buffer + let owner_balance = dwb.settle_sender_or_owner_account( + deps.storage, + &raw_owner, + tx_id, amount, - false, "burn", - &action.decoys, - &account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; + if raw_spender != raw_owner { + dwb.settle_sender_or_owner_account( + deps.storage, + &raw_spender, + tx_id, + 0, + "burn", + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + } + + DWB.save(deps.storage, &dwb)?; // remove from supply if let Some(new_total_supply) = total_supply.checked_sub(amount) { @@ -1774,25 +2509,39 @@ fn try_batch_burn_from( ))); } - store_burn( - deps.storage, - owner, - spender.clone(), - action.amount, - constants.symbol.clone(), - action.memo, - &env.block, - &action.decoys, - &account_random_pos, - )?; + spent_notifications.push(Notification::new ( + info.sender.clone(), + SpentNotificationData { + amount, + actions: 1, + recipient: None, + balance: owner_balance, + } + )); } TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + let tx_hash = env + .transaction + .clone() + .ok_or(StdError::generic_err("no tx hash found"))? + .hash; + let spent_data = multi_spent_data( + deps.api, + spent_notifications, + &tx_hash, + env.block.random.clone().unwrap(), + secret, + )?; + Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::BatchBurnFrom { - status: Success, - })?), + Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchBurnFrom {status: Success,})?) + .add_attribute_plaintext( + format!("snip52:#{}", MULTI_SPENT_CHANNEL_ID), + Binary::from(spent_data).to_base64(), + ) ) } @@ -1804,6 +2553,9 @@ fn try_increase_allowance( amount: Uint128, expiration: Option<u64>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let spender = deps.api.addr_validate(spender.as_str())?; let mut allowance = AllowancesStore::load(deps.storage, &info.sender, &spender); @@ -1823,12 +2575,27 @@ fn try_increase_allowance( let new_amount = allowance.amount; AllowancesStore::save(deps.storage, &info.sender, &spender, &allowance)?; + let notification = Notification::new ( + spender.clone(), + AllowanceNotificationData { + amount: new_amount, + allower: info.sender.clone(), + expiration, + } + ) + .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; + Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::IncreaseAllowance { - owner: info.sender, - spender, - allowance: Uint128::from(new_amount), - })?), + Response::new() + .set_data(to_binary(&ExecuteAnswer::IncreaseAllowance { + owner: info.sender, + spender, + allowance: Uint128::from(new_amount), + })?) + .add_attribute_plaintext( + notification.id_plaintext(), + notification.data_plaintext() + ) ) } @@ -1840,6 +2607,9 @@ fn try_decrease_allowance( amount: Uint128, expiration: Option<u64>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let spender = deps.api.addr_validate(spender.as_str())?; let mut allowance = AllowancesStore::load(deps.storage, &info.sender, &spender); @@ -1859,12 +2629,27 @@ fn try_decrease_allowance( let new_amount = allowance.amount; AllowancesStore::save(deps.storage, &info.sender, &spender, &allowance)?; + let notification = Notification::new ( + spender.clone(), + AllowanceNotificationData { + amount: new_amount, + allower: info.sender.clone(), + expiration, + } + ) + .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; + Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::DecreaseAllowance { - owner: info.sender, - spender, - allowance: Uint128::from(new_amount), - })?), + Response::new() + .set_data(to_binary(&ExecuteAnswer::DecreaseAllowance { + owner: info.sender, + spender, + allowance: Uint128::from(new_amount), + })?) + .add_attribute_plaintext( + notification.id_plaintext(), + notification.data_plaintext() + ) ) } @@ -1952,9 +2737,10 @@ fn try_burn( info: MessageInfo, amount: Uint128, memo: Option<String>, - decoys: Option<Vec<Addr>>, - account_random_pos: Option<usize>, ) -> StdResult<Response> { + let secret = INTERNAL_SECRET.load(deps.storage)?; + let secret = secret.as_slice(); + let constants = CONFIG.load(deps.storage)?; if !constants.burn_is_enabled { return Err(StdError::generic_err( @@ -1963,17 +2749,37 @@ fn try_burn( } let raw_amount = amount.u128(); + let raw_burn_address = deps.api.addr_canonicalize(info.sender.as_str())?; - BalancesStore::update_balance( + let tx_id = store_burn_action( deps.storage, - &info.sender, + raw_burn_address.clone(), + raw_burn_address.clone(), + raw_amount, + constants.symbol, + memo, + &env.block, + )?; + + // load delayed write buffer + let mut dwb = DWB.load(deps.storage)?; + + #[cfg(feature = "gas_tracking")] + let mut tracker = GasTracker::new(deps.api); + + // settle the signer's account in buffer + let owner_balance = dwb.settle_sender_or_owner_account( + deps.storage, + &raw_burn_address, + tx_id, raw_amount, - false, "burn", - &decoys, - &account_random_pos, + #[cfg(feature = "gas_tracking")] + &mut tracker, )?; + DWB.save(deps.storage, &dwb)?; + let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; if let Some(new_total_supply) = total_supply.checked_sub(raw_amount) { total_supply = new_total_supply; @@ -1984,50 +2790,186 @@ fn try_burn( } TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - store_burn( - deps.storage, - info.sender.clone(), + let spent_notification = Notification::new ( info.sender, - amount, - constants.symbol, - memo, - &env.block, - &decoys, - &account_random_pos, - )?; + SpentNotificationData { + amount: raw_amount, + actions: 1, + recipient: None, + balance: owner_balance, + } + ) + .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::Burn { status: Success })?)) + Ok( + Response::new() + .set_data(to_binary(&ExecuteAnswer::Burn { status: Success })?) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ) + ) } fn perform_transfer( store: &mut dyn Storage, - from: &Addr, - to: &Addr, + rng: &mut ContractPrng, + from: &CanonicalAddr, + to: &CanonicalAddr, + sender: &CanonicalAddr, amount: u128, - decoys: &Option<Vec<Addr>>, - account_random_pos: &Option<usize>, -) -> StdResult<()> { - BalancesStore::update_balance(store, from, amount, false, "transfer", &None, &None)?; - BalancesStore::update_balance( + denom: String, + memo: Option<String>, + block: &BlockInfo, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<u128> { + #[cfg(feature = "gas_tracking")] + let mut group1 = tracker.group("perform_transfer.1"); + + // first store the tx information in the global append list of txs and get the new tx id + let tx_id = store_transfer_action(store, from, sender, to, amount, denom, memo, block)?; + + #[cfg(feature = "gas_tracking")] + group1.log("@store_transfer_action"); + + // load delayed write buffer + let mut dwb = DWB.load(store)?; + + #[cfg(feature = "gas_tracking")] + group1.log("DWB.load"); + + let transfer_str = "transfer"; + + // settle the owner's account + let owner_balance = dwb.settle_sender_or_owner_account( + store, + from, + tx_id, + amount, + transfer_str, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + // if this is a *_from action, settle the sender's account, too + if sender != from { + dwb.settle_sender_or_owner_account( + store, + sender, + tx_id, + 0, + transfer_str, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + } + + // add the tx info for the recipient to the buffer + dwb.add_recipient( store, + rng, to, + tx_id, amount, - true, - "transfer", - decoys, - account_random_pos, + #[cfg(feature = "gas_tracking")] + tracker, )?; - Ok(()) -} + #[cfg(feature = "gas_tracking")] + let mut group2 = tracker.group("perform_transfer.2"); -fn revoke_permit(deps: DepsMut, info: MessageInfo, permit_name: String) -> StdResult<Response> { - RevokedPermits::revoke_permit( - deps.storage, - PREFIX_REVOKED_PERMITS, - info.sender.as_str(), - &permit_name, - ); + DWB.save(store, &dwb)?; + + #[cfg(feature = "gas_tracking")] + group2.log("DWB.save"); + + Ok(owner_balance) +} + +fn perform_mint( + store: &mut dyn Storage, + rng: &mut ContractPrng, + minter: &CanonicalAddr, + to: &CanonicalAddr, + amount: u128, + denom: String, + memo: Option<String>, + block: &BlockInfo, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<()> { + // first store the tx information in the global append list of txs and get the new tx id + let tx_id = store_mint_action(store, minter, to, amount, denom, memo, block)?; + + // load delayed write buffer + let mut dwb = DWB.load(store)?; + + // if minter is not recipient, settle them + if minter != to { + dwb.settle_sender_or_owner_account( + store, + minter, + tx_id, + 0, + "mint", + #[cfg(feature = "gas_tracking")] + tracker, + )?; + } + + // add the tx info for the recipient to the buffer + dwb.add_recipient( + store, + rng, + to, + tx_id, + amount, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + DWB.save(store, &dwb)?; + + Ok(()) +} + +fn perform_deposit( + store: &mut dyn Storage, + rng: &mut ContractPrng, + to: &CanonicalAddr, + amount: u128, + denom: String, + block: &BlockInfo, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<()> { + // first store the tx information in the global append list of txs and get the new tx id + let tx_id = store_deposit_action(store, amount, denom, block)?; + + // load delayed write buffer + let mut dwb = DWB.load(store)?; + + // add the tx info for the recipient to the buffer + dwb.add_recipient( + store, + rng, + to, + tx_id, + amount, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + DWB.save(store, &dwb)?; + + Ok(()) +} + +fn revoke_permit(deps: DepsMut, info: MessageInfo, permit_name: String) -> StdResult<Response> { + RevokedPermits::revoke_permit( + deps.storage, + PREFIX_REVOKED_PERMITS, + info.sender.as_str(), + &permit_name, + ); Ok(Response::new().set_data(to_binary(&ExecuteAnswer::RevokePermit { status: Success })?)) } @@ -2067,15 +3009,16 @@ fn is_valid_symbol(symbol: &str) -> bool { mod tests { use std::any::Any; - use cosmwasm_std::testing::*; use cosmwasm_std::{ - from_binary, BlockInfo, ContractInfo, MessageInfo, OwnedDeps, QueryResponse, ReplyOn, - SubMsg, Timestamp, TransactionInfo, WasmMsg, + from_binary, testing::*, Api, BlockInfo, ContractInfo, MessageInfo, OwnedDeps, + QueryResponse, ReplyOn, SubMsg, Timestamp, TransactionInfo, WasmMsg, }; use secret_toolkit::permit::{PermitParams, PermitSignature, PubKey}; - use crate::msg::ResponseStatus; - use crate::msg::{InitConfig, InitialBalance}; + use crate::dwb::TX_NODES_COUNT; + use crate::msg::{InitConfig, InitialBalance, ResponseStatus}; + use crate::state::TX_COUNT; + use crate::transaction_history::TxAction; use super::*; @@ -2301,38 +3244,24 @@ mod tests { } #[test] - fn test_total_supply_overflow() { + fn test_total_supply_overflow_dwb() { + // with this implementation of dwbs the max amount a user can get transferred or minted is u64::MAX + // for 18 digit coins, u128 amounts might be stored in the dwb (see `fn add_amount` in dwb.rs) let (init_result, _deps) = init_helper(vec![InitialBalance { address: "lebron".to_string(), - amount: Uint128::new(u128::max_value()), + amount: Uint128::new(u64::max_value().into()), }]); assert!( init_result.is_ok(), "Init failed: {}", init_result.err().unwrap() ); - - let (init_result, _deps) = init_helper(vec![ - InitialBalance { - address: "lebron".to_string(), - amount: Uint128::new(u128::max_value()), - }, - InitialBalance { - address: "giannis".to_string(), - amount: Uint128::new(1), - }, - ]); - let error = extract_error_msg(init_result); - assert_eq!( - error, - "The sum of all initial balances exceeds the maximum possible total supply" - ); } // Handle tests #[test] - fn test_execute_transfer() { + fn test_execute_transfer_dwb() { let (init_result, mut deps) = init_helper(vec![InitialBalance { address: "bob".to_string(), amount: Uint128::new(5000), @@ -2343,413 +3272,756 @@ mod tests { init_result.err().unwrap() ); + let tx_nodes_count = TX_NODES_COUNT.load(&deps.storage).unwrap_or_default(); + // should be 2 because we minted 5000 to bob at initialization + assert_eq!(2, tx_nodes_count); + let tx_count = TX_COUNT.load(&deps.storage).unwrap_or_default(); + assert_eq!(1, tx_count); // due to mint + let handle_msg = ExecuteMsg::Transfer { recipient: "alice".to_string(), amount: Uint128::new(1000), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[0u8; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); let result = handle_result.unwrap(); assert!(ensure_success(result)); - let bob_addr = Addr::unchecked("bob".to_string()); - let alice_addr = Addr::unchecked("alice".to_string()); - - assert_eq!(5000 - 1000, BalancesStore::load(&deps.storage, &bob_addr)); - assert_eq!(1000, BalancesStore::load(&deps.storage, &alice_addr)); + let bob_addr = deps + .api + .addr_canonicalize(Addr::unchecked("bob").as_str()) + .unwrap(); + let alice_addr = deps + .api + .addr_canonicalize(Addr::unchecked("alice").as_str()) + .unwrap(); + assert_eq!( + 5000 - 1000, + stored_balance(&deps.storage, &bob_addr).unwrap() + ); + // alice has not been settled yet + assert_ne!(1000, stored_balance(&deps.storage, &alice_addr).unwrap()); + + let dwb = DWB.load(&deps.storage).unwrap(); + println!("DWB: {dwb:?}"); + // assert we have decremented empty_space_counter + assert_eq!(62, dwb.empty_space_counter); + // assert first entry has correct information for alice + let alice_entry = dwb.entries[2]; + assert_eq!(1, alice_entry.list_len().unwrap()); + assert_eq!(1000, alice_entry.amount().unwrap()); + // the id of the head_node + assert_eq!(4, alice_entry.head_node().unwrap()); + let tx_count = TX_COUNT.load(&deps.storage).unwrap_or_default(); + assert_eq!(2, tx_count); + + // now send 100 to charlie from bob let handle_msg = ExecuteMsg::Transfer { - recipient: "alice".to_string(), - amount: Uint128::new(10000), + recipient: "charlie".to_string(), + amount: Uint128::new(100), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - let error = extract_error_msg(handle_result); - assert!(error.contains("insufficient funds")); - } - - #[test] - fn test_decoys_balance_stays_on_transfer() { - let (init_result, mut deps) = init_helper(vec![ - InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }, - InitialBalance { - address: "lior".to_string(), - amount: Uint128::new(7000), - }, - ]); - - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); - - let bob_addr = Addr::unchecked("bob".to_string()); - let alice_addr = Addr::unchecked("alice".to_string()); - let lior_addr = Addr::unchecked("lior".to_string()); - let jhon_addr = Addr::unchecked("jhon".to_string()); + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[1u8; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); - let bob_balance = BalancesStore::load(&deps.storage, &bob_addr); - let alice_balance = BalancesStore::load(&deps.storage, &alice_addr); - let lior_balance = BalancesStore::load(&deps.storage, &lior_addr); - let jhon_balance = BalancesStore::load(&deps.storage, &jhon_addr); + let result = handle_result.unwrap(); + assert!(ensure_success(result)); + let charlie_addr = deps + .api + .addr_canonicalize(Addr::unchecked("charlie").as_str()) + .unwrap(); + assert_eq!( + 5000 - 1000 - 100, + stored_balance(&deps.storage, &bob_addr).unwrap() + ); + // alice has not been settled yet + assert_ne!(1000, stored_balance(&deps.storage, &alice_addr).unwrap()); + // charlie has not been settled yet + assert_ne!(100, stored_balance(&deps.storage, &charlie_addr).unwrap()); + + let dwb = DWB.load(&deps.storage).unwrap(); + //println!("DWB: {dwb:?}"); + // assert we have decremented empty_space_counter + assert_eq!(61, dwb.empty_space_counter); + // assert entry has correct information for charlie + let charlie_entry = dwb.entries[3]; + assert_eq!(1, charlie_entry.list_len().unwrap()); + assert_eq!(100, charlie_entry.amount().unwrap()); + // the id of the head_node + assert_eq!(6, charlie_entry.head_node().unwrap()); + let tx_count = TX_COUNT.load(&deps.storage).unwrap_or_default(); + assert_eq!(3, tx_count); + + // send another 500 to alice from bob let handle_msg = ExecuteMsg::Transfer { recipient: "alice".to_string(), - amount: Uint128::new(1000), + amount: Uint128::new(500), memo: None, - decoys: Some(vec![lior_addr.clone(), jhon_addr.clone()]), - entropy: Some(Binary::from_base64("VEVTVFRFU1RURVNUQ0hFQ0tDSEVDSw==").unwrap()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[2u8; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); let result = handle_result.unwrap(); assert!(ensure_success(result)); assert_eq!( - bob_balance - 1000, - BalancesStore::load(&deps.storage, &bob_addr) - ); - assert_eq!( - alice_balance + 1000, - BalancesStore::load(&deps.storage, &alice_addr) - ); - assert_eq!(lior_balance, BalancesStore::load(&deps.storage, &lior_addr)); - assert_eq!(jhon_balance, BalancesStore::load(&deps.storage, &jhon_addr)); - } + 5000 - 1000 - 100 - 500, + stored_balance(&deps.storage, &bob_addr).unwrap() + ); + // make sure alice has not been settled yet + assert_ne!(1500, stored_balance(&deps.storage, &alice_addr).unwrap()); + + let dwb = DWB.load(&deps.storage).unwrap(); + //println!("DWB: {dwb:?}"); + // assert we have not decremented empty_space_counter + assert_eq!(61, dwb.empty_space_counter); + // assert entry has correct information for alice + let alice_entry = dwb.entries[2]; + assert_eq!(2, alice_entry.list_len().unwrap()); + assert_eq!(1500, alice_entry.amount().unwrap()); + // the id of the head_node + assert_eq!(8, alice_entry.head_node().unwrap()); + let tx_count = TX_COUNT.load(&deps.storage).unwrap_or_default(); + assert_eq!(4, tx_count); + + // convert head_node to vec + let alice_nodes = TX_NODES + .add_suffix(&alice_entry.head_node().unwrap().to_be_bytes()) + .load(&deps.storage) + .unwrap() + .to_vec(&deps.storage, &deps.api) + .unwrap(); - #[test] - fn test_handle_send() { - let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }]); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); + let expected_alice_nodes: Vec<Tx> = vec![ + Tx { + id: 4, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + amount: Uint128::from(500_u128), + denom: "SECSEC".to_string(), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + Tx { + id: 2, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + amount: Uint128::from(1000_u128), + denom: "SECSEC".to_string(), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + ]; + assert_eq!(alice_nodes, expected_alice_nodes); - let handle_msg = ExecuteMsg::RegisterReceive { - code_hash: "this_is_a_hash_of_a_code".to_string(), + // now send 200 to ernie from bob + let handle_msg = ExecuteMsg::Transfer { + recipient: "ernie".to_string(), + amount: Uint128::new(200), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("contract", &[]); + let info = mock_info("bob", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[3u8; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); let result = handle_result.unwrap(); assert!(ensure_success(result)); + let ernie_addr = deps + .api + .addr_canonicalize(Addr::unchecked("ernie").as_str()) + .unwrap(); - let handle_msg = ExecuteMsg::Send { - recipient: "contract".to_string(), - recipient_code_hash: None, - amount: Uint128::new(100), - memo: Some("my memo".to_string()), + assert_eq!( + 5000 - 1000 - 100 - 500 - 200, + stored_balance(&deps.storage, &bob_addr).unwrap() + ); + // alice has not been settled yet + assert_ne!(1500, stored_balance(&deps.storage, &alice_addr).unwrap()); + // charlie has not been settled yet + assert_ne!(100, stored_balance(&deps.storage, &charlie_addr).unwrap()); + // ernie has not been settled yet + assert_ne!(200, stored_balance(&deps.storage, &ernie_addr).unwrap()); + + let dwb = DWB.load(&deps.storage).unwrap(); + //println!("DWB: {dwb:?}"); + + // assert we have decremented empty_space_counter + assert_eq!(60, dwb.empty_space_counter); + // assert entry has correct information for ernie + let ernie_entry = dwb.entries[4]; + assert_eq!(1, ernie_entry.list_len().unwrap()); + assert_eq!(200, ernie_entry.amount().unwrap()); + // the id of the head_node + assert_eq!(10, ernie_entry.head_node().unwrap()); + let tx_count = TX_COUNT.load(&deps.storage).unwrap_or_default(); + assert_eq!(5, tx_count); + + // now alice sends 50 to dora + // this should settle alice and create entry for dora + let handle_msg = ExecuteMsg::Transfer { + recipient: "dora".to_string(), + amount: Uint128::new(50), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, - msg: Some(to_binary("hey hey you you").unwrap()), - decoys: None, - entropy: None, }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let info = mock_info("alice", &[]); + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[4u8; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); let result = handle_result.unwrap(); - assert!(ensure_success(result.clone())); - let id = 0; - assert!(result.messages.contains(&SubMsg { - id, - msg: CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "contract".to_string(), - code_hash: "this_is_a_hash_of_a_code".to_string(), - msg: Snip20ReceiveMsg::new( - Addr::unchecked("bob".to_string()), - Addr::unchecked("bob".to_string()), - Uint128::new(100), - Some("my memo".to_string()), - Some(to_binary("hey hey you you").unwrap()) - ) - .into_binary() - .unwrap(), - funds: vec![], - }) - .into(), - reply_on: match id { - 0 => ReplyOn::Never, - _ => ReplyOn::Always, - }, - gas_limit: None, - })); - } + assert!(ensure_success(result)); + let dora_addr = deps + .api + .addr_canonicalize(Addr::unchecked("dora").as_str()) + .unwrap(); - #[test] - fn test_handle_register_receive() { - let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }]); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() + // alice has been settled + assert_eq!( + 1500 - 50, + stored_balance(&deps.storage, &alice_addr).unwrap() + ); + // dora has not been settled + assert_ne!(50, stored_balance(&deps.storage, &dora_addr).unwrap()); + + let dwb = DWB.load(&deps.storage).unwrap(); + //println!("DWB: {dwb:?}"); + + // assert we have decremented empty_space_counter + assert_eq!(59, dwb.empty_space_counter); + // assert entry has correct information for ernie + let dora_entry = dwb.entries[5]; + assert_eq!(1, dora_entry.list_len().unwrap()); + assert_eq!(50, dora_entry.amount().unwrap()); + // the id of the head_node + assert_eq!(12, dora_entry.head_node().unwrap()); + let tx_count = TX_COUNT.load(&deps.storage).unwrap_or_default(); + assert_eq!(6, tx_count); + + // now we will send to 60 more addresses to fill up the buffer + for i in 1..=59 { + let recipient = format!("receipient{i}"); + // now send 1 to recipient from bob + let handle_msg = ExecuteMsg::Transfer { + recipient, + amount: Uint128::new(1), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("bob", &[]); + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[255 - i; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); + + let result = handle_result.unwrap(); + assert!(ensure_success(result)); + } + assert_eq!( + 5000 - 1000 - 100 - 500 - 200 - 59, + stored_balance(&deps.storage, &bob_addr).unwrap() ); - let handle_msg = ExecuteMsg::RegisterReceive { - code_hash: "this_is_a_hash_of_a_code".to_string(), + let dwb = DWB.load(&deps.storage).unwrap(); + //println!("DWB: {dwb:?}"); + + // assert we have filled the buffer + assert_eq!(0, dwb.empty_space_counter); + + let recipient = format!("receipient_over"); + // now send 1 to recipient from bob + let handle_msg = ExecuteMsg::Transfer { + recipient, + amount: Uint128::new(1), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("contract", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let info = mock_info("bob", &[]); + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[50; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); let result = handle_result.unwrap(); assert!(ensure_success(result)); - let hash = - ReceiverHashStore::may_load(&deps.storage, &Addr::unchecked("contract".to_string())) - .unwrap() - .unwrap(); - assert_eq!(hash, "this_is_a_hash_of_a_code".to_string()); - } - - #[test] - fn test_handle_create_viewing_key() { - let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }]); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() + assert_eq!( + 5000 - 1000 - 100 - 500 - 200 - 59 - 1, + stored_balance(&deps.storage, &bob_addr).unwrap() ); - let handle_msg = ExecuteMsg::CreateViewingKey { - entropy: "".to_string(), + //let dwb = DWB.load(&deps.storage).unwrap(); + //println!("DWB: {dwb:?}"); + + let recipient = format!("receipient_over_2"); + // now send 1 to recipient from bob + let handle_msg = ExecuteMsg::Transfer { + recipient, + amount: Uint128::new(1), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[12; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let result = handle_result.unwrap(); + assert!(ensure_success(result)); - assert!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() + assert_eq!( + 5000 - 1000 - 100 - 500 - 200 - 59 - 1 - 1, + stored_balance(&deps.storage, &bob_addr).unwrap() ); - let answer: ExecuteAnswer = from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); - let key = match answer { - ExecuteAnswer::CreateViewingKey { key } => key, - _ => panic!("NOPE"), - }; - // let bob_canonical = deps.as_mut().api.addr_canonicalize("bob").unwrap(); + //let dwb = DWB.load(&deps.storage).unwrap(); + //println!("DWB: {dwb:?}"); - let result = ViewingKey::check(&deps.storage, "bob", key.as_str()); - assert!(result.is_ok()); + // now we send 50 transactions to alice from bob + for i in 1..=50 { + // send 1 to alice from bob + let handle_msg = ExecuteMsg::Transfer { + recipient: "alice".to_string(), + amount: Uint128::new(i.into()), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; - // let saved_vk = read_viewing_key(&deps.storage, &bob_canonical).unwrap(); - // assert!(key.check_viewing_key(saved_vk.as_slice())); - } + let info = mock_info("bob", &[]); + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[125 - i; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); - #[test] - fn test_handle_set_viewing_key() { - let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }]); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); + let result = handle_result.unwrap(); + assert!(ensure_success(result)); - // Set VK - let handle_msg = ExecuteMsg::SetViewingKey { - key: "hi lol".to_string(), + // alice should not settle + assert_eq!( + 1500 - 50, + stored_balance(&deps.storage, &alice_addr).unwrap() + ); + } + + // alice sends 1 to dora to settle + // this should settle alice and create entry for dora + let handle_msg = ExecuteMsg::Transfer { + recipient: "dora".to_string(), + amount: Uint128::new(1), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("bob", &[]); + let info = mock_info("alice", &[]); + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[61; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let result = handle_result.unwrap(); + assert!(ensure_success(result)); - let unwrapped_result: ExecuteAnswer = - from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); - assert_eq!( - to_binary(&unwrapped_result).unwrap(), - to_binary(&ExecuteAnswer::SetViewingKey { - status: ResponseStatus::Success - }) - .unwrap(), - ); + assert_eq!(2724, stored_balance(&deps.storage, &alice_addr).unwrap()); - // Set valid VK - let actual_vk = "x".to_string().repeat(VIEWING_KEY_SIZE); - let handle_msg = ExecuteMsg::SetViewingKey { - key: actual_vk.clone(), - padding: None, - }; - let info = mock_info("bob", &[]); + // now we send 50 more transactions to alice from bob + for i in 1..=50 { + // send 1 to alice from bob + let handle_msg = ExecuteMsg::Transfer { + recipient: "alice".to_string(), + amount: Uint128::new(i.into()), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let info = mock_info("bob", &[]); + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[200 - i; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); - let unwrapped_result: ExecuteAnswer = - from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); - assert_eq!( - to_binary(&unwrapped_result).unwrap(), - to_binary(&ExecuteAnswer::SetViewingKey { status: Success }).unwrap(), - ); + let result = handle_result.unwrap(); + assert!(ensure_success(result)); - let result = ViewingKey::check(&deps.storage, "bob", actual_vk.as_str()); - assert!(result.is_ok()); - } + // alice should not settle + assert_eq!(2724, stored_balance(&deps.storage, &alice_addr).unwrap()); + } - fn revoke_permit( - permit_name: &str, - user_address: &str, - deps: &mut OwnedDeps<cosmwasm_std::MemoryStorage, MockApi, MockQuerier>, - ) -> Result<Response, StdError> { - let handle_msg = ExecuteMsg::RevokePermit { - permit_name: permit_name.to_string(), + let handle_msg = ExecuteMsg::SetViewingKey { + key: "key".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info(user_address, &[]); + let info = mock_info("alice", &[]); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - handle_result - } + let result = handle_result.unwrap(); + assert!(ensure_success(result)); - fn get_balance_with_permit_qry_msg( - permit_name: &str, - chain_id: &str, - pub_key_value: &str, - signature: &str, - ) -> QueryMsg { - let permit = gen_permit_obj( - permit_name, - chain_id, - pub_key_value, - signature, - TokenPermissions::Balance, - ); + // check that alice's balance when queried is correct (includes both settled and dwb amounts) + // settled = 2724 + // dwb = 1275 + // total should be = 3999 + let query_msg = QueryMsg::Balance { + address: "alice".to_string(), + key: "key".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let balance = match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::Balance { amount } => amount, + _ => panic!("Unexpected"), + }; + assert_eq!(balance, Uint128::new(3999)); - QueryMsg::WithPermit { - permit, - query: QueryWithPermit::Balance {}, - } - } + // now we use alice to check query transaction history pagination works - fn gen_permit_obj( - permit_name: &str, - chain_id: &str, - pub_key_value: &str, - signature: &str, - permit_type: TokenPermissions, - ) -> Permit { - let permit: Permit = Permit { - params: PermitParams { - allowed_tokens: vec![MOCK_CONTRACT_ADDR.to_string()], - permit_name: permit_name.to_string(), - chain_id: chain_id.to_string(), - permissions: vec![permit_type], + // + // check last 3 transactions for alice (all in dwb) + // + let query_msg = QueryMsg::TransactionHistory { + address: "alice".to_string(), + key: "key".to_string(), + page: None, + page_size: 3, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let transfers = match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::TransactionHistory { txs, .. } => txs, + other => panic!("Unexpected: {:?}", other), + }; + //println!("transfers: {transfers:?}"); + let expected_transfers = vec![ + Tx { + id: 168, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(50u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, }, - signature: PermitSignature { - pub_key: PubKey { - r#type: "tendermint/PubKeySecp256k1".to_string(), - value: Binary::from_base64(pub_key_value).unwrap(), + Tx { + id: 167, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), }, - signature: Binary::from_base64(signature).unwrap(), + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(49u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, }, - }; - permit - } - - fn get_allowances_given_permit( - permit_name: &str, - chain_id: &str, - pub_key_value: &str, - signature: &str, - spender: String, - ) -> QueryMsg { - let permit = gen_permit_obj( - permit_name, - chain_id, - pub_key_value, - signature, - TokenPermissions::Owner, - ); - - QueryMsg::WithPermit { - permit, - query: QueryWithPermit::AllowancesReceived { - spender, - page: None, - page_size: 0, + Tx { + id: 166, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(48u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, }, - } - } + ]; + assert_eq!(transfers, expected_transfers); - #[test] - fn test_permit_query_allowances_given_should_fail() { - let user_address = "secret18mdrja40gfuftt5yx6tgj0fn5lurplezyp894y"; - let permit_name = "default"; - let chain_id = "secretdev-1"; - let pub_key = "AkZqxdKMtPq2w0kGDGwWGejTAed0H7azPMHtrCX0XYZG"; - let signature = "ZXyFMlAy6guMG9Gj05rFvcMi5/JGfClRtJpVTHiDtQY3GtSfBHncY70kmYiTXkKIxSxdnh/kS8oXa+GSX5su6Q=="; + // + // check 6 transactions for alice that span over end of the 50 in dwb and settled + // page: 8, page size: 6 + // start is index 48 + // + let query_msg = QueryMsg::TransactionHistory { + address: "alice".to_string(), + key: "key".to_string(), + page: Some(8), + page_size: 6, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let transfers = match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::TransactionHistory { txs, .. } => txs, + other => panic!("Unexpected: {:?}", other), + }; + //println!("transfers: {transfers:?}"); + let expected_transfers = vec![ + Tx { + id: 120, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(2u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + Tx { + id: 119, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(1u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + Tx { + id: 118, + action: TxAction::Transfer { + from: Addr::unchecked("alice"), + sender: Addr::unchecked("alice"), + recipient: Addr::unchecked("dora"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(1u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + Tx { + id: 117, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(50u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + Tx { + id: 116, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(49u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + Tx { + id: 115, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(48u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + ]; + assert_eq!(transfers, expected_transfers); - // Init the contract - let (init_result, deps) = init_helper(vec![InitialBalance { - address: user_address.to_string(), - amount: Uint128::new(50000000), - }]); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); + // + // check transactions for alice, starting in settled across different bundles with `end` past the last transaction + // there are 104 transactions total for alice + // page: 3, page size: 99 + // start is index 99 (100th tx) + // + let query_msg = QueryMsg::TransactionHistory { + address: "alice".to_string(), + key: "key".to_string(), + page: Some(3), + page_size: 33, + //page: None, + //page_size: 500, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let transfers = match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::TransactionHistory { txs, .. } => txs, + other => panic!("Unexpected: {:?}", other), + }; + //println!("transfers: {transfers:?}"); + let expected_transfers = vec![ + Tx { + id: 69, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(2u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + Tx { + id: 68, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(1u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + Tx { + id: 6, + action: TxAction::Transfer { + from: Addr::unchecked("alice"), + sender: Addr::unchecked("alice"), + recipient: Addr::unchecked("dora"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(50u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + Tx { + id: 4, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(500u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + Tx { + id: 2, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), + }, + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(1000u128), + }, + memo: None, + block_time: 1571797419, + block_height: 12345, + }, + ]; + //let transfers_len = transfers.len(); + //println!("transfers.len(): {transfers_len}"); + assert_eq!(transfers, expected_transfers); - let msg = get_allowances_given_permit( - permit_name, - chain_id, - pub_key, - signature, - "secret1kmgdagt5efcz2kku0ak9ezfgntg29g2vr88q0e".to_string(), - ); - let query_result = query(deps.as_ref(), mock_env(), msg); + // + // + // + // - assert_eq!(query_result.is_err(), true); + // now try invalid transfer + let handle_msg = ExecuteMsg::Transfer { + recipient: "alice".to_string(), + amount: Uint128::new(10000), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("bob", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient funds")); } #[test] - fn test_permit_query_allowances_given() { - let user_address = "secret18mdrja40gfuftt5yx6tgj0fn5lurplezyp894y"; - let permit_name = "default"; - let chain_id = "secretdev-1"; - let pub_key = "AkZqxdKMtPq2w0kGDGwWGejTAed0H7azPMHtrCX0XYZG"; - let signature = "ZXyFMlAy6guMG9Gj05rFvcMi5/JGfClRtJpVTHiDtQY3GtSfBHncY70kmYiTXkKIxSxdnh/kS8oXa+GSX5su6Q=="; - - // Init the contract - let (init_result, deps) = init_helper(vec![InitialBalance { - address: user_address.to_string(), - amount: Uint128::new(50000000), + fn test_handle_send() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), }]); assert!( init_result.is_ok(), @@ -2757,33 +4029,66 @@ mod tests { init_result.err().unwrap() ); - let msg = get_allowances_given_permit( - permit_name, - chain_id, - pub_key, - signature, - "secret18mdrja40gfuftt5yx6tgj0fn5lurplezyp894y".to_string(), - ); - let query_result = query(deps.as_ref(), mock_env(), msg); + let handle_msg = ExecuteMsg::RegisterReceive { + code_hash: "this_is_a_hash_of_a_code".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("contract", &[]); - assert_eq!(query_result.is_ok(), true); - } + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - #[test] - fn test_permit_revoke() { - let user_address = "secret1kmgdagt5efcz2kku0ak9ezfgntg29g2vr88q0e"; - let permit_name = "to_be_revoked"; - let chain_id = "blabla"; + let result = handle_result.unwrap(); + assert!(ensure_success(result)); - // Note that 'signature'was generated with the specific values of the above: - // user_address, permit_name, chain_id, pub_key_value - let pub_key_value = "Ahlb7vwjo4aTY6dqfgpPmPYF7XhTAIReVwncQwlq8Sct"; - let signature = "VS13F7iv1qxKABxrCAvZQPy2IruLQsIyfTewy/PIhNtybtq417lr3FxsWjV/i9YTqCUxg7weoZwHmYs0YgYX4w=="; + let handle_msg = ExecuteMsg::Send { + recipient: "contract".to_string(), + recipient_code_hash: None, + amount: Uint128::new(100), + memo: Some("my memo".to_string()), + padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + msg: Some(to_binary("hey hey you you").unwrap()), + }; + let info = mock_info("bob", &[]); - // Init the contract + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + let result = handle_result.unwrap(); + assert!(ensure_success(result.clone())); + let id = 0; + assert!(result.messages.contains(&SubMsg { + id, + msg: CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + code_hash: "this_is_a_hash_of_a_code".to_string(), + msg: Snip20ReceiveMsg::new( + Addr::unchecked("bob".to_string()), + Addr::unchecked("bob".to_string()), + Uint128::new(100), + Some("my memo".to_string()), + Some(to_binary("hey hey you you").unwrap()) + ) + .into_binary() + .unwrap(), + funds: vec![], + }) + .into(), + reply_on: match id { + 0 => ReplyOn::Never, + _ => ReplyOn::Always, + }, + gas_limit: None, + })); + } + + #[test] + fn test_handle_register_receive() { let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: user_address.to_string(), - amount: Uint128::new(50000000), + address: "bob".to_string(), + amount: Uint128::new(5000), }]); assert!( init_result.is_ok(), @@ -2791,36 +4096,28 @@ mod tests { init_result.err().unwrap() ); - // Query the account's balance - let balance_with_permit_msg = - get_balance_with_permit_qry_msg(permit_name, chain_id, pub_key_value, signature); - let query_result = query(deps.as_ref(), mock_env(), balance_with_permit_msg); - let balance = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::Balance { amount } => amount, - _ => panic!("Unexpected result from query"), + let handle_msg = ExecuteMsg::RegisterReceive { + code_hash: "this_is_a_hash_of_a_code".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, }; - assert_eq!(balance.u128(), 50000000); + let info = mock_info("contract", &[]); - // Revoke the Balance permit - let handle_result = revoke_permit(permit_name, user_address, &mut deps); - let status = match from_binary(&handle_result.unwrap().data.unwrap()).unwrap() { - ExecuteAnswer::RevokePermit { status } => status, - _ => panic!("NOPE"), - }; - assert_eq!(status, ResponseStatus::Success); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - // Try to query the balance with permit and fail because the permit is now revoked - let balance_with_permit_msg = - get_balance_with_permit_qry_msg(permit_name, chain_id, pub_key_value, signature); - let query_result = query(deps.as_ref(), mock_env(), balance_with_permit_msg); - let error = extract_error_msg(query_result); - assert!( - error.contains(format!("Permit \"{}\" was revoked by account", permit_name).as_str()) - ); + let result = handle_result.unwrap(); + assert!(ensure_success(result)); + + let hash = + ReceiverHashStore::may_load(&deps.storage, &Addr::unchecked("contract".to_string())) + .unwrap() + .unwrap(); + assert_eq!(hash, "this_is_a_hash_of_a_code".to_string()); } #[test] - fn test_execute_transfer_from() { + fn test_handle_create_viewing_key() { let (init_result, mut deps) = init_helper(vec![InitialBalance { address: "bob".to_string(), amount: Uint128::new(5000), @@ -2831,30 +4128,12 @@ mod tests { init_result.err().unwrap() ); - // Transfer before allowance - let handle_msg = ExecuteMsg::TransferFrom { - owner: "bob".to_string(), - recipient: "alice".to_string(), - amount: Uint128::new(2500), - memo: None, - decoys: None, + let handle_msg = ExecuteMsg::CreateViewingKey { entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("alice", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - let error = extract_error_msg(handle_result); - assert!(error.contains("insufficient allowance")); - - // Transfer more than allowance - let handle_msg = ExecuteMsg::IncreaseAllowance { - spender: "alice".to_string(), - amount: Uint128::new(2000), - padding: None, - expiration: Some(1_571_797_420), - }; let info = mock_info("bob", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); @@ -2864,107 +4143,23 @@ mod tests { "handle() failed: {}", handle_result.err().unwrap() ); - let handle_msg = ExecuteMsg::TransferFrom { - owner: "bob".to_string(), - recipient: "alice".to_string(), - amount: Uint128::new(2500), - memo: None, - decoys: None, - entropy: None, - padding: None, + let answer: ExecuteAnswer = from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); + + let key = match answer { + ExecuteAnswer::CreateViewingKey { key } => key, + _ => panic!("NOPE"), }; - let info = mock_info("alice", &[]); + // let bob_canonical = deps.as_mut().api.addr_canonicalize("bob").unwrap(); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let result = ViewingKey::check(&deps.storage, "bob", key.as_str()); + assert!(result.is_ok()); - let error = extract_error_msg(handle_result); - assert!(error.contains("insufficient allowance")); - - // Transfer after allowance expired - let handle_msg = ExecuteMsg::TransferFrom { - owner: "bob".to_string(), - recipient: "alice".to_string(), - amount: Uint128::new(2000), - memo: None, - decoys: None, - entropy: None, - padding: None, - }; - - let info = MessageInfo { - sender: Addr::unchecked("bob".to_string()), - funds: vec![], - }; - - let handle_result = execute( - deps.as_mut(), - Env { - block: BlockInfo { - height: 12_345, - time: Timestamp::from_seconds(1_571_797_420), - chain_id: "cosmos-testnet-14002".to_string(), - }, - transaction: Some(TransactionInfo { index: 3 }), - contract: ContractInfo { - address: Addr::unchecked(MOCK_CONTRACT_ADDR.to_string()), - code_hash: "".to_string(), - }, - }, - info, - handle_msg, - ); - let error = extract_error_msg(handle_result); - assert!(error.contains("insufficient allowance")); - - // Sanity check - let handle_msg = ExecuteMsg::TransferFrom { - owner: "bob".to_string(), - recipient: "alice".to_string(), - amount: Uint128::new(2000), - memo: None, - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("alice", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - assert!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() - ); - let bob_canonical = Addr::unchecked("bob".to_string()); - let alice_canonical = Addr::unchecked("alice".to_string()); - - let bob_balance = BalancesStore::load(&deps.storage, &bob_canonical); - let alice_balance = BalancesStore::load(&deps.storage, &alice_canonical); - assert_eq!(bob_balance, 5000 - 2000); - assert_eq!(alice_balance, 2000); - let total_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); - assert_eq!(total_supply, 5000); - - // Second send more than allowance - let handle_msg = ExecuteMsg::TransferFrom { - owner: "bob".to_string(), - recipient: "alice".to_string(), - amount: Uint128::new(1), - memo: None, - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("alice", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - let error = extract_error_msg(handle_result); - assert!(error.contains("insufficient allowance")); - } + // let saved_vk = read_viewing_key(&deps.storage, &bob_canonical).unwrap(); + // assert!(key.check_viewing_key(saved_vk.as_slice())); + } #[test] - fn test_handle_send_from() { + fn test_handle_set_viewing_key() { let (init_result, mut deps) = init_helper(vec![InitialBalance { address: "bob".to_string(), amount: Uint128::new(5000), @@ -2975,191 +4170,268 @@ mod tests { init_result.err().unwrap() ); - // Send before allowance - let handle_msg = ExecuteMsg::SendFrom { - owner: "bob".to_string(), - recipient: "alice".to_string(), - recipient_code_hash: None, - amount: Uint128::new(2500), - memo: None, - msg: None, - decoys: None, - entropy: None, + // Set VK + let handle_msg = ExecuteMsg::SetViewingKey { + key: "hi lol".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("alice", &[]); + let info = mock_info("bob", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let error = extract_error_msg(handle_result); - assert!(error.contains("insufficient allowance")); + let unwrapped_result: ExecuteAnswer = + from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); + assert_eq!( + to_binary(&unwrapped_result).unwrap(), + to_binary(&ExecuteAnswer::SetViewingKey { + status: ResponseStatus::Success + }) + .unwrap(), + ); - // Send more than allowance - let handle_msg = ExecuteMsg::IncreaseAllowance { - spender: "alice".to_string(), - amount: Uint128::new(2000), + // Set valid VK + let actual_vk = "x".to_string().repeat(VIEWING_KEY_SIZE); + let handle_msg = ExecuteMsg::SetViewingKey { + key: actual_vk.clone(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, - expiration: None, }; let info = mock_info("bob", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() + let unwrapped_result: ExecuteAnswer = + from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); + assert_eq!( + to_binary(&unwrapped_result).unwrap(), + to_binary(&ExecuteAnswer::SetViewingKey { status: Success }).unwrap(), ); - let handle_msg = ExecuteMsg::SendFrom { - owner: "bob".to_string(), - recipient: "alice".to_string(), - recipient_code_hash: None, - amount: Uint128::new(2500), - memo: None, - msg: None, - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("alice", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let error = extract_error_msg(handle_result); - assert!(error.contains("insufficient allowance")); + let result = ViewingKey::check(&deps.storage, "bob", actual_vk.as_str()); + assert!(result.is_ok()); + } - // Sanity check - let handle_msg = ExecuteMsg::RegisterReceive { - code_hash: "lolz".to_string(), + fn revoke_permit( + permit_name: &str, + user_address: &str, + deps: &mut OwnedDeps<cosmwasm_std::MemoryStorage, MockApi, MockQuerier>, + ) -> Result<Response, StdError> { + let handle_msg = ExecuteMsg::RevokePermit { + permit_name: permit_name.to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("contract", &[]); - + let info = mock_info(user_address, &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + handle_result + } - assert!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() - ); - let send_msg = Binary::from(r#"{ "some_msg": { "some_key": "some_val" } }"#.as_bytes()); - let snip20_msg = Snip20ReceiveMsg::new( - Addr::unchecked("alice".to_string()), - Addr::unchecked("bob".to_string()), - Uint128::new(2000), - Some("my memo".to_string()), - Some(send_msg.clone()), + fn get_balance_with_permit_qry_msg( + permit_name: &str, + chain_id: &str, + pub_key_value: &str, + signature: &str, + ) -> QueryMsg { + let permit = gen_permit_obj( + permit_name, + chain_id, + pub_key_value, + signature, + TokenPermissions::Balance, ); - let handle_msg = ExecuteMsg::SendFrom { - owner: "bob".to_string(), - recipient: "contract".to_string(), - recipient_code_hash: None, - amount: Uint128::new(2000), - memo: Some("my memo".to_string()), - msg: Some(send_msg), - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("alice", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() - ); - assert!(handle_result.unwrap().messages.contains( - &into_cosmos_submsg( - snip20_msg, - "lolz".to_string(), - Addr::unchecked("contract".to_string()), - 0 - ) - .unwrap() - )); - let bob_canonical = Addr::unchecked("bob".to_string()); - let contract_canonical = Addr::unchecked("contract".to_string()); - let bob_balance = BalancesStore::load(&deps.storage, &bob_canonical); - let contract_balance = BalancesStore::load(&deps.storage, &contract_canonical); - assert_eq!(bob_balance, 5000 - 2000); - assert_eq!(contract_balance, 2000); - let total_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); - assert_eq!(total_supply, 5000); + QueryMsg::WithPermit { + permit, + query: QueryWithPermit::Balance {}, + } + } - // Second send more than allowance - let handle_msg = ExecuteMsg::SendFrom { - owner: "bob".to_string(), - recipient: "alice".to_string(), - recipient_code_hash: None, - amount: Uint128::new(1), - memo: None, - msg: None, - decoys: None, - entropy: None, - padding: None, + fn gen_permit_obj( + permit_name: &str, + chain_id: &str, + pub_key_value: &str, + signature: &str, + permit_type: TokenPermissions, + ) -> Permit { + let permit: Permit = Permit { + params: PermitParams { + allowed_tokens: vec![MOCK_CONTRACT_ADDR.to_string()], + permit_name: permit_name.to_string(), + chain_id: chain_id.to_string(), + permissions: vec![permit_type], + }, + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64(pub_key_value).unwrap(), + }, + signature: Binary::from_base64(signature).unwrap(), + }, }; - let info = mock_info("alice", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - let error = extract_error_msg(handle_result); - assert!(error.contains("insufficient allowance")); + permit } - #[test] - fn test_handle_burn_from() { - let (init_result, mut deps) = init_helper_with_config( - vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(10000), - }], - false, - false, - false, - true, - 0, - vec![], + fn get_allowances_given_permit( + permit_name: &str, + chain_id: &str, + pub_key_value: &str, + signature: &str, + spender: String, + ) -> QueryMsg { + let permit = gen_permit_obj( + permit_name, + chain_id, + pub_key_value, + signature, + TokenPermissions::Owner, ); + + QueryMsg::WithPermit { + permit, + query: QueryWithPermit::AllowancesReceived { + spender, + page: None, + page_size: 0, + }, + } + } + + #[test] + fn test_permit_query_allowances_given_should_fail() { + let user_address = "secret18mdrja40gfuftt5yx6tgj0fn5lurplezyp894y"; + let permit_name = "default"; + let chain_id = "secretdev-1"; + let pub_key = "AkZqxdKMtPq2w0kGDGwWGejTAed0H7azPMHtrCX0XYZG"; + let signature = "ZXyFMlAy6guMG9Gj05rFvcMi5/JGfClRtJpVTHiDtQY3GtSfBHncY70kmYiTXkKIxSxdnh/kS8oXa+GSX5su6Q=="; + + // Init the contract + let (init_result, deps) = init_helper(vec![InitialBalance { + address: user_address.to_string(), + amount: Uint128::new(50000000), + }]); assert!( init_result.is_ok(), "Init failed: {}", init_result.err().unwrap() ); - let (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(10000), + let msg = get_allowances_given_permit( + permit_name, + chain_id, + pub_key, + signature, + "secret1kmgdagt5efcz2kku0ak9ezfgntg29g2vr88q0e".to_string(), + ); + let query_result = query(deps.as_ref(), mock_env(), msg); + + assert_eq!(query_result.is_err(), true); + } + + #[test] + fn test_permit_query_allowances_given() { + let user_address = "secret18mdrja40gfuftt5yx6tgj0fn5lurplezyp894y"; + let permit_name = "default"; + let chain_id = "secretdev-1"; + let pub_key = "AkZqxdKMtPq2w0kGDGwWGejTAed0H7azPMHtrCX0XYZG"; + let signature = "ZXyFMlAy6guMG9Gj05rFvcMi5/JGfClRtJpVTHiDtQY3GtSfBHncY70kmYiTXkKIxSxdnh/kS8oXa+GSX5su6Q=="; + + // Init the contract + let (init_result, deps) = init_helper(vec![InitialBalance { + address: user_address.to_string(), + amount: Uint128::new(50000000), }]); assert!( - init_result_for_failure.is_ok(), + init_result.is_ok(), "Init failed: {}", - init_result_for_failure.err().unwrap() + init_result.err().unwrap() ); - // test when burn disabled - let handle_msg = ExecuteMsg::BurnFrom { - owner: "bob".to_string(), - amount: Uint128::new(2500), - memo: None, - decoys: None, - entropy: None, - padding: None, + + let msg = get_allowances_given_permit( + permit_name, + chain_id, + pub_key, + signature, + "secret18mdrja40gfuftt5yx6tgj0fn5lurplezyp894y".to_string(), + ); + let query_result = query(deps.as_ref(), mock_env(), msg); + + assert_eq!(query_result.is_ok(), true); + } + + #[test] + fn test_permit_revoke() { + let user_address = "secret1kmgdagt5efcz2kku0ak9ezfgntg29g2vr88q0e"; + let permit_name = "to_be_revoked"; + let chain_id = "blabla"; + + // Note that 'signature'was generated with the specific values of the above: + // user_address, permit_name, chain_id, pub_key_value + let pub_key_value = "Ahlb7vwjo4aTY6dqfgpPmPYF7XhTAIReVwncQwlq8Sct"; + let signature = "VS13F7iv1qxKABxrCAvZQPy2IruLQsIyfTewy/PIhNtybtq417lr3FxsWjV/i9YTqCUxg7weoZwHmYs0YgYX4w=="; + + // Init the contract + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: user_address.to_string(), + amount: Uint128::new(50000000), + }]); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // Query the account's balance + let balance_with_permit_msg = + get_balance_with_permit_qry_msg(permit_name, chain_id, pub_key_value, signature); + let query_result = query(deps.as_ref(), mock_env(), balance_with_permit_msg); + let balance = match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::Balance { amount } => amount, + _ => panic!("Unexpected result from query"), }; - let info = mock_info("alice", &[]); + assert_eq!(balance.u128(), 50000000); - let handle_result = execute(deps_for_failure.as_mut(), mock_env(), info, handle_msg); + // Revoke the Balance permit + let handle_result = revoke_permit(permit_name, user_address, &mut deps); + let status = match from_binary(&handle_result.unwrap().data.unwrap()).unwrap() { + ExecuteAnswer::RevokePermit { status } => status, + _ => panic!("NOPE"), + }; + assert_eq!(status, ResponseStatus::Success); - let error = extract_error_msg(handle_result); - assert!(error.contains("Burn functionality is not enabled for this token.")); + // Try to query the balance with permit and fail because the permit is now revoked + let balance_with_permit_msg = + get_balance_with_permit_qry_msg(permit_name, chain_id, pub_key_value, signature); + let query_result = query(deps.as_ref(), mock_env(), balance_with_permit_msg); + let error = extract_error_msg(query_result); + assert!( + error.contains(format!("Permit \"{}\" was revoked by account", permit_name).as_str()) + ); + } - // Burn before allowance - let handle_msg = ExecuteMsg::BurnFrom { + #[test] + fn test_execute_transfer_from() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }]); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // Transfer before allowance + let handle_msg = ExecuteMsg::TransferFrom { owner: "bob".to_string(), + recipient: "alice".to_string(), amount: Uint128::new(2500), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("alice", &[]); @@ -3169,12 +4441,14 @@ mod tests { let error = extract_error_msg(handle_result); assert!(error.contains("insufficient allowance")); - // Burn more than allowance + // Transfer more than allowance let handle_msg = ExecuteMsg::IncreaseAllowance { spender: "alice".to_string(), amount: Uint128::new(2000), padding: None, - expiration: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + expiration: Some(1_571_797_420), }; let info = mock_info("bob", &[]); @@ -3185,12 +4459,13 @@ mod tests { "handle() failed: {}", handle_result.err().unwrap() ); - let handle_msg = ExecuteMsg::BurnFrom { + let handle_msg = ExecuteMsg::TransferFrom { owner: "bob".to_string(), + recipient: "alice".to_string(), amount: Uint128::new(2500), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("alice", &[]); @@ -3200,13 +4475,57 @@ mod tests { let error = extract_error_msg(handle_result); assert!(error.contains("insufficient allowance")); + // Transfer after allowance expired + let handle_msg = ExecuteMsg::TransferFrom { + owner: "bob".to_string(), + recipient: "alice".to_string(), + amount: Uint128::new(2000), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + + let info = MessageInfo { + sender: Addr::unchecked("bob".to_string()), + funds: vec![], + }; + + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 12_345, + time: Timestamp::from_seconds(1_571_797_420), + chain_id: "cosmos-testnet-14002".to_string(), + random: Some(Binary::from(&[ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + ])), + }, + transaction: Some(TransactionInfo { + index: 3, + hash: "1010".to_string(), + }), + contract: ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR.to_string()), + code_hash: "".to_string(), + }, + }, + info, + handle_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); + // Sanity check - let handle_msg = ExecuteMsg::BurnFrom { + let handle_msg = ExecuteMsg::TransferFrom { owner: "bob".to_string(), + recipient: "alice".to_string(), amount: Uint128::new(2000), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("alice", &[]); @@ -3218,19 +4537,30 @@ mod tests { "handle() failed: {}", handle_result.err().unwrap() ); - let bob_canonical = Addr::unchecked("bob".to_string()); - let bob_balance = BalancesStore::load(&deps.storage, &bob_canonical); - assert_eq!(bob_balance, 10000 - 2000); + let bob_canonical = deps + .api + .addr_canonicalize(Addr::unchecked("bob".to_string()).as_str()) + .unwrap(); + let alice_canonical = deps + .api + .addr_canonicalize(Addr::unchecked("alice".to_string()).as_str()) + .unwrap(); + + let bob_balance = stored_balance(&deps.storage, &bob_canonical).unwrap(); + let alice_balance = stored_balance(&deps.storage, &alice_canonical).unwrap(); + assert_eq!(bob_balance, 5000 - 2000); + assert_ne!(alice_balance, 2000); let total_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); - assert_eq!(total_supply, 10000 - 2000); + assert_eq!(total_supply, 5000); - // Second burn more than allowance - let handle_msg = ExecuteMsg::BurnFrom { + // Second send more than allowance + let handle_msg = ExecuteMsg::TransferFrom { owner: "bob".to_string(), + recipient: "alice".to_string(), amount: Uint128::new(1), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("alice", &[]); @@ -3242,127 +4572,80 @@ mod tests { } #[test] - fn test_handle_batch_burn_from() { - let (init_result, mut deps) = init_helper_with_config( - vec![ - InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(10000), - }, - InitialBalance { - address: "jerry".to_string(), - amount: Uint128::new(10000), - }, - InitialBalance { - address: "mike".to_string(), - amount: Uint128::new(10000), - }, - ], - false, - false, - false, - true, - 0, - vec![], - ); + fn test_handle_send_from() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }]); assert!( init_result.is_ok(), "Init failed: {}", init_result.err().unwrap() ); - let (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(10000), - }]); - assert!( - init_result_for_failure.is_ok(), - "Init failed: {}", - init_result_for_failure.err().unwrap() - ); - // test when burn disabled - let actions: Vec<_> = ["bob", "jerry", "mike"] - .iter() - .map(|name| batch::BurnFromAction { - owner: name.to_string(), - amount: Uint128::new(2500), - memo: None, - decoys: None, - }) - .collect(); - let handle_msg = ExecuteMsg::BatchBurnFrom { - actions, - entropy: None, + // Send before allowance + let handle_msg = ExecuteMsg::SendFrom { + owner: "bob".to_string(), + recipient: "alice".to_string(), + recipient_code_hash: None, + amount: Uint128::new(2500), + memo: None, + msg: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("alice", &[]); - let handle_result = execute( - deps_for_failure.as_mut(), - mock_env(), - info, - handle_msg.clone(), - ); - let error = extract_error_msg(handle_result); - assert!(error.contains("Burn functionality is not enabled for this token.")); - - // Burn before allowance - let info = mock_info("alice", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); let error = extract_error_msg(handle_result); assert!(error.contains("insufficient allowance")); - // Burn more than allowance - let allowance_size = 2000; - for name in &["bob", "jerry", "mike"] { - let handle_msg = ExecuteMsg::IncreaseAllowance { - spender: "alice".to_string(), - amount: Uint128::new(allowance_size), - padding: None, - expiration: None, - }; - let info = mock_info(*name, &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + // Send more than allowance + let handle_msg = ExecuteMsg::IncreaseAllowance { + spender: "alice".to_string(), + amount: Uint128::new(2000), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + expiration: None, + }; + let info = mock_info("bob", &[]); - assert!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() - ); - let handle_msg = ExecuteMsg::BurnFrom { - owner: "name".to_string(), - amount: Uint128::new(2500), - memo: None, - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("alice", &[]); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); + let handle_msg = ExecuteMsg::SendFrom { + owner: "bob".to_string(), + recipient: "alice".to_string(), + recipient_code_hash: None, + amount: Uint128::new(2500), + memo: None, + msg: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("alice", &[]); - let error = extract_error_msg(handle_result); - assert!(error.contains("insufficient allowance")); - } + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - // Burn some of the allowance - let actions: Vec<_> = [("bob", 200_u128), ("jerry", 300), ("mike", 400)] - .iter() - .map(|(name, amount)| batch::BurnFromAction { - owner: name.to_string(), - amount: Uint128::new(*amount), - memo: None, - decoys: None, - }) - .collect(); + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); - let handle_msg = ExecuteMsg::BatchBurnFrom { - actions, - entropy: None, + // Sanity check + let handle_msg = ExecuteMsg::RegisterReceive { + code_hash: "lolz".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("alice", &[]); + let info = mock_info("contract", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); @@ -3371,28 +4654,23 @@ mod tests { "handle() failed: {}", handle_result.err().unwrap() ); - for (name, amount) in &[("bob", 200_u128), ("jerry", 300), ("mike", 400)] { - let name_canon = Addr::unchecked(name.to_string()); - let balance = BalancesStore::load(&deps.storage, &name_canon); - assert_eq!(balance, 10000 - amount); - } - let total_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); - assert_eq!(total_supply, 10000 * 3 - (200 + 300 + 400)); - - // Burn the rest of the allowance - let actions: Vec<_> = [("bob", 200_u128), ("jerry", 300), ("mike", 400)] - .iter() - .map(|(name, amount)| batch::BurnFromAction { - owner: name.to_string(), - amount: Uint128::new(allowance_size - *amount), - memo: None, - decoys: None, - }) - .collect(); - - let handle_msg = ExecuteMsg::BatchBurnFrom { - actions, - entropy: None, + let send_msg = Binary::from(r#"{ "some_msg": { "some_key": "some_val" } }"#.as_bytes()); + let snip20_msg = Snip20ReceiveMsg::new( + Addr::unchecked("alice".to_string()), + Addr::unchecked("bob".to_string()), + Uint128::new(2000), + Some("my memo".to_string()), + Some(send_msg.clone()), + ); + let handle_msg = ExecuteMsg::SendFrom { + owner: "bob".to_string(), + recipient: "contract".to_string(), + recipient_code_hash: None, + amount: Uint128::new(2000), + memo: Some("my memo".to_string()), + msg: Some(send_msg), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("alice", &[]); @@ -3404,27 +4682,42 @@ mod tests { "handle() failed: {}", handle_result.err().unwrap() ); - for name in &["bob", "jerry", "mike"] { - let name_canon = Addr::unchecked(name.to_string()); - let balance = BalancesStore::load(&deps.storage, &name_canon); - assert_eq!(balance, 10000 - allowance_size); - } + assert!(handle_result.unwrap().messages.contains( + &into_cosmos_submsg( + snip20_msg, + "lolz".to_string(), + Addr::unchecked("contract".to_string()), + 0 + ) + .unwrap() + )); + + let bob_canonical = deps + .api + .addr_canonicalize(Addr::unchecked("bob".to_string()).as_str()) + .unwrap(); + let contract_canonical = deps + .api + .addr_canonicalize(Addr::unchecked("contract".to_string()).as_str()) + .unwrap(); + + let bob_balance = stored_balance(&deps.storage, &bob_canonical).unwrap(); + let contract_balance = stored_balance(&deps.storage, &contract_canonical).unwrap(); + assert_eq!(bob_balance, 5000 - 2000); + assert_ne!(contract_balance, 2000); let total_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); - assert_eq!(total_supply, 3 * (10000 - allowance_size)); + assert_eq!(total_supply, 5000); - // Second burn more than allowance - let actions: Vec<_> = ["bob", "jerry", "mike"] - .iter() - .map(|name| batch::BurnFromAction { - owner: name.to_string(), - amount: Uint128::new(1), - memo: None, - decoys: None, - }) - .collect(); - let handle_msg = ExecuteMsg::BatchBurnFrom { - actions, - entropy: None, + // Second send more than allowance + let handle_msg = ExecuteMsg::SendFrom { + owner: "bob".to_string(), + recipient: "alice".to_string(), + recipient_code_hash: None, + amount: Uint128::new(1), + memo: None, + msg: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("alice", &[]); @@ -3436,21 +4729,73 @@ mod tests { } #[test] - fn test_handle_decrease_allowance() { - let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }]); + fn test_handle_burn_from() { + let (init_result, mut deps) = init_helper_with_config( + vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(10000), + }], + false, + false, + false, + true, + 0, + vec![], + ); assert!( init_result.is_ok(), "Init failed: {}", init_result.err().unwrap() ); - let handle_msg = ExecuteMsg::DecreaseAllowance { + let (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(10000), + }]); + assert!( + init_result_for_failure.is_ok(), + "Init failed: {}", + init_result_for_failure.err().unwrap() + ); + // test when burn disabled + let handle_msg = ExecuteMsg::BurnFrom { + owner: "bob".to_string(), + amount: Uint128::new(2500), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("alice", &[]); + + let handle_result = execute(deps_for_failure.as_mut(), mock_env(), info, handle_msg); + + let error = extract_error_msg(handle_result); + assert!(error.contains("Burn functionality is not enabled for this token.")); + + // Burn before allowance + let handle_msg = ExecuteMsg::BurnFrom { + owner: "bob".to_string(), + amount: Uint128::new(2500), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("alice", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); + + // Burn more than allowance + let handle_msg = ExecuteMsg::IncreaseAllowance { spender: "alice".to_string(), amount: Uint128::new(2000), padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, expiration: None, }; let info = mock_info("bob", &[]); @@ -3462,26 +4807,31 @@ mod tests { "handle() failed: {}", handle_result.err().unwrap() ); + let handle_msg = ExecuteMsg::BurnFrom { + owner: "bob".to_string(), + amount: Uint128::new(2500), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("alice", &[]); - let bob_canonical = Addr::unchecked("bob".to_string()); - let alice_canonical = Addr::unchecked("alice".to_string()); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let allowance = AllowancesStore::load(&deps.storage, &bob_canonical, &alice_canonical); - assert_eq!( - allowance, - crate::state::Allowance { - amount: 0, - expiration: None - } - ); + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); - let handle_msg = ExecuteMsg::IncreaseAllowance { - spender: "alice".to_string(), + // Sanity check + let handle_msg = ExecuteMsg::BurnFrom { + owner: "bob".to_string(), amount: Uint128::new(2000), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, - expiration: None, }; - let info = mock_info("bob", &[]); + let info = mock_info("alice", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); @@ -3490,14 +4840,157 @@ mod tests { "handle() failed: {}", handle_result.err().unwrap() ); + let bob_canonical = deps + .api + .addr_canonicalize(Addr::unchecked("bob".to_string()).as_str()) + .unwrap(); - let handle_msg = ExecuteMsg::DecreaseAllowance { - spender: "alice".to_string(), - amount: Uint128::new(50), + let bob_balance = stored_balance(&deps.storage, &bob_canonical).unwrap(); + assert_eq!(bob_balance, 10000 - 2000); + let total_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); + assert_eq!(total_supply, 10000 - 2000); + + // Second burn more than allowance + let handle_msg = ExecuteMsg::BurnFrom { + owner: "bob".to_string(), + amount: Uint128::new(1), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, - expiration: None, }; - let info = mock_info("bob", &[]); + let info = mock_info("alice", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); + } + + #[test] + fn test_handle_batch_burn_from() { + let (init_result, mut deps) = init_helper_with_config( + vec![ + InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(10000), + }, + InitialBalance { + address: "jerry".to_string(), + amount: Uint128::new(10000), + }, + InitialBalance { + address: "mike".to_string(), + amount: Uint128::new(10000), + }, + ], + false, + false, + false, + true, + 0, + vec![], + ); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(10000), + }]); + assert!( + init_result_for_failure.is_ok(), + "Init failed: {}", + init_result_for_failure.err().unwrap() + ); + // test when burn disabled + let actions: Vec<_> = ["bob", "jerry", "mike"] + .iter() + .map(|name| batch::BurnFromAction { + owner: name.to_string(), + amount: Uint128::new(2500), + memo: None, + }) + .collect(); + let handle_msg = ExecuteMsg::BatchBurnFrom { + actions, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("alice", &[]); + let handle_result = execute( + deps_for_failure.as_mut(), + mock_env(), + info, + handle_msg.clone(), + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Burn functionality is not enabled for this token.")); + + // Burn before allowance + let info = mock_info("alice", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); + + // Burn more than allowance + let allowance_size = 2000; + for name in &["bob", "jerry", "mike"] { + let handle_msg = ExecuteMsg::IncreaseAllowance { + spender: "alice".to_string(), + amount: Uint128::new(allowance_size), + padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + expiration: None, + }; + let info = mock_info(*name, &[]); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); + let handle_msg = ExecuteMsg::BurnFrom { + owner: "name".to_string(), + amount: Uint128::new(2500), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("alice", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); + } + + // Burn some of the allowance + let actions: Vec<_> = [("bob", 200_u128), ("jerry", 300), ("mike", 400)] + .iter() + .map(|(name, amount)| batch::BurnFromAction { + owner: name.to_string(), + amount: Uint128::new(*amount), + memo: None, + }) + .collect(); + + let handle_msg = ExecuteMsg::BatchBurnFrom { + actions, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("alice", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); @@ -3506,19 +4999,78 @@ mod tests { "handle() failed: {}", handle_result.err().unwrap() ); + for (name, amount) in &[("bob", 200_u128), ("jerry", 300), ("mike", 400)] { + let name_canon = deps + .api + .addr_canonicalize(Addr::unchecked(name.to_string()).as_str()) + .unwrap(); + let balance = stored_balance(&deps.storage, &name_canon).unwrap(); + assert_eq!(balance, 10000 - amount); + } + let total_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); + assert_eq!(total_supply, 10000 * 3 - (200 + 300 + 400)); - let allowance = AllowancesStore::load(&deps.storage, &bob_canonical, &alice_canonical); - assert_eq!( - allowance, - crate::state::Allowance { - amount: 1950, - expiration: None - } + // Burn the rest of the allowance + let actions: Vec<_> = [("bob", 200_u128), ("jerry", 300), ("mike", 400)] + .iter() + .map(|(name, amount)| batch::BurnFromAction { + owner: name.to_string(), + amount: Uint128::new(allowance_size - *amount), + memo: None, + }) + .collect(); + + let handle_msg = ExecuteMsg::BatchBurnFrom { + actions, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("alice", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() ); + for name in &["bob", "jerry", "mike"] { + let name_canon = deps + .api + .addr_canonicalize(Addr::unchecked(name.to_string()).as_str()) + .unwrap(); + let balance = stored_balance(&deps.storage, &name_canon).unwrap(); + assert_eq!(balance, 10000 - allowance_size); + } + let total_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); + assert_eq!(total_supply, 3 * (10000 - allowance_size)); + + // Second burn more than allowance + let actions: Vec<_> = ["bob", "jerry", "mike"] + .iter() + .map(|name| batch::BurnFromAction { + owner: name.to_string(), + amount: Uint128::new(1), + memo: None, + }) + .collect(); + let handle_msg = ExecuteMsg::BatchBurnFrom { + actions, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("alice", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); } #[test] - fn test_handle_increase_allowance() { + fn test_handle_decrease_allowance() { let (init_result, mut deps) = init_helper(vec![InitialBalance { address: "bob".to_string(), amount: Uint128::new(5000), @@ -3529,10 +5081,12 @@ mod tests { init_result.err().unwrap() ); - let handle_msg = ExecuteMsg::IncreaseAllowance { + let handle_msg = ExecuteMsg::DecreaseAllowance { spender: "alice".to_string(), amount: Uint128::new(2000), padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, expiration: None, }; let info = mock_info("bob", &[]); @@ -3552,7 +5106,7 @@ mod tests { assert_eq!( allowance, crate::state::Allowance { - amount: 2000, + amount: 0, expiration: None } ); @@ -3561,6 +5115,8 @@ mod tests { spender: "alice".to_string(), amount: Uint128::new(2000), padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, expiration: None, }; let info = mock_info("bob", &[]); @@ -3573,33 +5129,15 @@ mod tests { handle_result.err().unwrap() ); - let allowance = AllowancesStore::load(&deps.storage, &bob_canonical, &alice_canonical); - assert_eq!( - allowance, - crate::state::Allowance { - amount: 4000, - expiration: None - } - ); - } - - #[test] - fn test_handle_change_admin() { - let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }]); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); - - let handle_msg = ExecuteMsg::ChangeAdmin { - address: "bob".to_string(), + let handle_msg = ExecuteMsg::DecreaseAllowance { + spender: "alice".to_string(), + amount: Uint128::new(50), padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + expiration: None, }; - let info = mock_info("admin", &[]); + let info = mock_info("bob", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); @@ -3609,14 +5147,20 @@ mod tests { handle_result.err().unwrap() ); - let admin = CONFIG.load(&deps.storage).unwrap().admin; - assert_eq!(admin, Addr::unchecked("bob".to_string())); + let allowance = AllowancesStore::load(&deps.storage, &bob_canonical, &alice_canonical); + assert_eq!( + allowance, + crate::state::Allowance { + amount: 1950, + expiration: None + } + ); } #[test] - fn test_handle_set_contract_status() { + fn test_handle_increase_allowance() { let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "admin".to_string(), + address: "bob".to_string(), amount: Uint128::new(5000), }]); assert!( @@ -3625,11 +5169,15 @@ mod tests { init_result.err().unwrap() ); - let handle_msg = ExecuteMsg::SetContractStatus { - level: ContractStatusLevel::StopAll, + let handle_msg = ExecuteMsg::IncreaseAllowance { + spender: "alice".to_string(), + amount: Uint128::new(2000), padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + expiration: None, }; - let info = mock_info("admin", &[]); + let info = mock_info("bob", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); @@ -3639,7 +5187,107 @@ mod tests { handle_result.err().unwrap() ); - let contract_status = CONTRACT_STATUS.load(&deps.storage).unwrap(); + let bob_canonical = Addr::unchecked("bob".to_string()); + let alice_canonical = Addr::unchecked("alice".to_string()); + + let allowance = AllowancesStore::load(&deps.storage, &bob_canonical, &alice_canonical); + assert_eq!( + allowance, + crate::state::Allowance { + amount: 2000, + expiration: None + } + ); + + let handle_msg = ExecuteMsg::IncreaseAllowance { + spender: "alice".to_string(), + amount: Uint128::new(2000), + padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + expiration: None, + }; + let info = mock_info("bob", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); + + let allowance = AllowancesStore::load(&deps.storage, &bob_canonical, &alice_canonical); + assert_eq!( + allowance, + crate::state::Allowance { + amount: 4000, + expiration: None + } + ); + } + + #[test] + fn test_handle_change_admin() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }]); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let handle_msg = ExecuteMsg::ChangeAdmin { + address: "bob".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("admin", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); + + let admin = CONFIG.load(&deps.storage).unwrap().admin; + assert_eq!(admin, Addr::unchecked("bob".to_string())); + } + + #[test] + fn test_handle_set_contract_status() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "admin".to_string(), + amount: Uint128::new(5000), + }]); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let handle_msg = ExecuteMsg::SetContractStatus { + level: ContractStatusLevel::StopAll, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("admin", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); + + let contract_status = CONTRACT_STATUS.load(&deps.storage).unwrap(); assert!(matches!( contract_status, ContractStatusLevel::StopAll { .. } @@ -3697,8 +5345,8 @@ mod tests { let handle_msg = ExecuteMsg::Redeem { amount: Uint128::new(1000), denom: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("butler", &[]); @@ -3712,8 +5360,8 @@ mod tests { let handle_msg = ExecuteMsg::Redeem { amount: Uint128::new(1000), denom: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("butler", &[]); @@ -3730,9 +5378,9 @@ mod tests { let handle_msg = ExecuteMsg::Redeem { amount: Uint128::new(1000), denom: None, - decoys: None, - entropy: None, padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, }; let info = mock_info("butler", &[]); @@ -3748,8 +5396,8 @@ mod tests { let handle_msg = ExecuteMsg::Redeem { amount: Uint128::new(1000), denom: Option::from("uscrt".to_string()), - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("butler", &[]); @@ -3762,8 +5410,11 @@ mod tests { handle_result.err().unwrap() ); - let canonical = Addr::unchecked("butler".to_string()); - assert_eq!(BalancesStore::load(&deps.storage, &canonical), 3000) + let canonical = deps + .api + .addr_canonicalize(Addr::unchecked("butler".to_string()).as_str()) + .unwrap(); + assert_eq!(stored_balance(&deps.storage, &canonical).unwrap(), 3000) } #[test] @@ -3797,8 +5448,8 @@ mod tests { ); // test when deposit disabled let handle_msg = ExecuteMsg::Deposit { - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info( @@ -3814,8 +5465,8 @@ mod tests { assert!(error.contains("Tried to deposit an unsupported coin uscrt")); let handle_msg = ExecuteMsg::Deposit { - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; @@ -3834,8 +5485,38 @@ mod tests { handle_result.err().unwrap() ); - let canonical = Addr::unchecked("lebron".to_string()); - assert_eq!(BalancesStore::load(&deps.storage, &canonical), 6000) + let canonical = deps + .api + .addr_canonicalize(Addr::unchecked("lebron".to_string()).as_str()) + .unwrap(); + + // stored balance not updated, still in dwb + assert_ne!(stored_balance(&deps.storage, &canonical).unwrap(), 6000); + + let create_vk_msg = ExecuteMsg::CreateViewingKey { + entropy: Some("34".to_string()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("lebron", &[]); + let handle_response = execute(deps.as_mut(), mock_env(), info, create_vk_msg).unwrap(); + let vk = match from_binary(&handle_response.data.unwrap()).unwrap() { + ExecuteAnswer::CreateViewingKey { key } => key, + _ => panic!("Unexpected result from handle"), + }; + + let query_balance_msg = QueryMsg::Balance { + address: "lebron".to_string(), + key: vk, + }; + + let query_response = query(deps.as_ref(), mock_env(), query_balance_msg).unwrap(); + let balance = match from_binary(&query_response).unwrap() { + QueryAnswer::Balance { amount } => amount, + _ => panic!("Unexpected result from query"), + }; + assert_eq!(balance, Uint128::new(6000)); } #[test] @@ -3871,8 +5552,8 @@ mod tests { let handle_msg = ExecuteMsg::Burn { amount: Uint128::new(100), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("lebron", &[]); @@ -3887,8 +5568,8 @@ mod tests { let handle_msg = ExecuteMsg::Burn { amount: Uint128::new(burn_amount), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("lebron", &[]); @@ -3939,8 +5620,8 @@ mod tests { recipient: "lebron".to_string(), amount: Uint128::new(mint_amount), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -3956,8 +5637,8 @@ mod tests { recipient: "lebron".to_string(), amount: Uint128::new(mint_amount), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -3997,6 +5678,8 @@ mod tests { let pause_msg = ExecuteMsg::SetContractStatus { level: ContractStatusLevel::StopAllButRedeems, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("not_admin", &[]); @@ -4008,6 +5691,8 @@ mod tests { let mint_msg = ExecuteMsg::AddMinters { minters: vec!["not_admin".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("not_admin", &[]); @@ -4019,6 +5704,8 @@ mod tests { let mint_msg = ExecuteMsg::RemoveMinters { minters: vec!["admin".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("not_admin", &[]); @@ -4030,6 +5717,8 @@ mod tests { let mint_msg = ExecuteMsg::SetMinters { minters: vec!["not_admin".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("not_admin", &[]); @@ -4041,6 +5730,8 @@ mod tests { let change_admin_msg = ExecuteMsg::ChangeAdmin { address: "not_admin".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("not_admin", &[]); @@ -4073,6 +5764,8 @@ mod tests { let pause_msg = ExecuteMsg::SetContractStatus { level: ContractStatusLevel::StopAllButRedeems, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; @@ -4090,8 +5783,8 @@ mod tests { recipient: "account".to_string(), amount: Uint128::new(123), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4107,8 +5800,8 @@ mod tests { let withdraw_msg = ExecuteMsg::Redeem { amount: Uint128::new(5000), denom: Option::from("uscrt".to_string()), - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("lebron", &[]); @@ -4136,6 +5829,8 @@ mod tests { let pause_msg = ExecuteMsg::SetContractStatus { level: ContractStatusLevel::StopAll, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; @@ -4153,8 +5848,8 @@ mod tests { recipient: "account".to_string(), amount: Uint128::new(123), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4170,8 +5865,8 @@ mod tests { let withdraw_msg = ExecuteMsg::Redeem { amount: Uint128::new(5000), denom: Option::from("uscrt".to_string()), - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("lebron", &[]); @@ -4216,6 +5911,8 @@ mod tests { // try when mint disabled let handle_msg = ExecuteMsg::SetMinters { minters: vec!["bob".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4227,6 +5924,8 @@ mod tests { let handle_msg = ExecuteMsg::SetMinters { minters: vec!["bob".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -4238,6 +5937,8 @@ mod tests { let handle_msg = ExecuteMsg::SetMinters { minters: vec!["bob".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4250,8 +5951,8 @@ mod tests { recipient: "bob".to_string(), amount: Uint128::new(100), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -4264,8 +5965,8 @@ mod tests { recipient: "bob".to_string(), amount: Uint128::new(100), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4307,6 +6008,8 @@ mod tests { // try when mint disabled let handle_msg = ExecuteMsg::AddMinters { minters: vec!["bob".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4318,6 +6021,8 @@ mod tests { let handle_msg = ExecuteMsg::AddMinters { minters: vec!["bob".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -4329,6 +6034,8 @@ mod tests { let handle_msg = ExecuteMsg::AddMinters { minters: vec!["bob".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4341,8 +6048,8 @@ mod tests { recipient: "bob".to_string(), amount: Uint128::new(100), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -4355,8 +6062,8 @@ mod tests { recipient: "bob".to_string(), amount: Uint128::new(100), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4397,6 +6104,8 @@ mod tests { // try when mint disabled let handle_msg = ExecuteMsg::RemoveMinters { minters: vec!["bob".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4408,6 +6117,8 @@ mod tests { let handle_msg = ExecuteMsg::RemoveMinters { minters: vec!["admin".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -4419,6 +6130,8 @@ mod tests { let handle_msg = ExecuteMsg::RemoveMinters { minters: vec!["admin".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4431,8 +6144,8 @@ mod tests { recipient: "bob".to_string(), amount: Uint128::new(100), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -4446,8 +6159,8 @@ mod tests { recipient: "bob".to_string(), amount: Uint128::new(100), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4460,6 +6173,8 @@ mod tests { // Removing another extra time to ensure nothing funky happens let handle_msg = ExecuteMsg::RemoveMinters { minters: vec!["admin".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4472,8 +6187,8 @@ mod tests { recipient: "bob".to_string(), amount: Uint128::new(100), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -4487,8 +6202,8 @@ mod tests { recipient: "bob".to_string(), amount: Uint128::new(100), memo: None, - decoys: None, - entropy: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4525,7 +6240,9 @@ mod tests { ); let create_vk_msg = ExecuteMsg::CreateViewingKey { - entropy: "34".to_string(), + entropy: Some("34".to_string()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("giannis", &[]); @@ -4696,1256 +6413,645 @@ mod tests { let init_name = "sec-sec".to_string(); let init_admin = Addr::unchecked("admin".to_string()); let init_symbol = "SECSEC".to_string(); - let init_decimals = 8; - - let init_supply = Uint128::new(5000); - - let mut deps = mock_dependencies_with_balance(&[]); - let info = mock_info("instantiator", &[]); - let env = mock_env(); - let init_config: InitConfig = from_binary(&Binary::from( - format!( - "{{\"public_total_supply\":{}, - \"enable_deposit\":{}, - \"enable_redeem\":{}, - \"enable_mint\":{}, - \"enable_burn\":{}}}", - true, true, false, false, false - ) - .as_bytes(), - )) - .unwrap(); - let init_msg = InstantiateMsg { - name: init_name.clone(), - admin: Some(init_admin.into_string()), - symbol: init_symbol.clone(), - decimals: init_decimals.clone(), - initial_balances: Some(vec![InitialBalance { - address: "giannis".to_string(), - amount: init_supply, - }]), - prng_seed: Binary::from("lolz fun yay".as_bytes()), - config: Some(init_config), - supported_denoms: Some(vec!["uscrt".to_string()]), - }; - let init_result = instantiate(deps.as_mut(), env, info, init_msg); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); - - let query_msg = QueryMsg::ExchangeRate {}; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - assert!( - query_result.is_ok(), - "Init failed: {}", - query_result.err().unwrap() - ); - let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); - match query_answer { - QueryAnswer::ExchangeRate { rate, denom } => { - assert_eq!(rate, Uint128::new(100)); - assert_eq!(denom, "SCRT"); - } - _ => panic!("unexpected"), - } - - // test same number of decimals as SCRT - let init_name = "sec-sec".to_string(); - let init_admin = Addr::unchecked("admin".to_string()); - let init_symbol = "SECSEC".to_string(); - let init_decimals = 6; - - let init_supply = Uint128::new(5000); - - let mut deps = mock_dependencies_with_balance(&[]); - let info = mock_info("instantiator", &[]); - let env = mock_env(); - let init_config: InitConfig = from_binary(&Binary::from( - format!( - "{{\"public_total_supply\":{}, - \"enable_deposit\":{}, - \"enable_redeem\":{}, - \"enable_mint\":{}, - \"enable_burn\":{}}}", - true, true, false, false, false - ) - .as_bytes(), - )) - .unwrap(); - let init_msg = InstantiateMsg { - name: init_name.clone(), - admin: Some(init_admin.into_string()), - symbol: init_symbol.clone(), - decimals: init_decimals.clone(), - initial_balances: Some(vec![InitialBalance { - address: "giannis".to_string(), - amount: init_supply, - }]), - prng_seed: Binary::from("lolz fun yay".as_bytes()), - config: Some(init_config), - supported_denoms: Some(vec!["uscrt".to_string()]), - }; - let init_result = instantiate(deps.as_mut(), env, info, init_msg); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); - - let query_msg = QueryMsg::ExchangeRate {}; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - assert!( - query_result.is_ok(), - "Init failed: {}", - query_result.err().unwrap() - ); - let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); - match query_answer { - QueryAnswer::ExchangeRate { rate, denom } => { - assert_eq!(rate, Uint128::new(1)); - assert_eq!(denom, "SCRT"); - } - _ => panic!("unexpected"), - } - - // test less decimal places than SCRT - let init_name = "sec-sec".to_string(); - let init_admin = Addr::unchecked("admin".to_string()); - let init_symbol = "SECSEC".to_string(); - let init_decimals = 3; - - let init_supply = Uint128::new(5000); - - let mut deps = mock_dependencies_with_balance(&[]); - let info = mock_info("instantiator", &[]); - let env = mock_env(); - let init_config: InitConfig = from_binary(&Binary::from( - format!( - "{{\"public_total_supply\":{}, - \"enable_deposit\":{}, - \"enable_redeem\":{}, - \"enable_mint\":{}, - \"enable_burn\":{}}}", - true, true, false, false, false - ) - .as_bytes(), - )) - .unwrap(); - let init_msg = InstantiateMsg { - name: init_name.clone(), - admin: Some(init_admin.into_string()), - symbol: init_symbol.clone(), - decimals: init_decimals.clone(), - initial_balances: Some(vec![InitialBalance { - address: "giannis".to_string(), - amount: init_supply, - }]), - prng_seed: Binary::from("lolz fun yay".as_bytes()), - config: Some(init_config), - supported_denoms: Some(vec!["uscrt".to_string()]), - }; - let init_result = instantiate(deps.as_mut(), env, info, init_msg); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); - - let query_msg = QueryMsg::ExchangeRate {}; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - assert!( - query_result.is_ok(), - "Init failed: {}", - query_result.err().unwrap() - ); - let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); - match query_answer { - QueryAnswer::ExchangeRate { rate, denom } => { - assert_eq!(rate, Uint128::new(1000)); - assert_eq!(denom, "SECSEC"); - } - _ => panic!("unexpected"), - } - - // test depost/redeem not enabled - let init_name = "sec-sec".to_string(); - let init_admin = Addr::unchecked("admin".to_string()); - let init_symbol = "SECSEC".to_string(); - let init_decimals = 3; - - let init_supply = Uint128::new(5000); - - let mut deps = mock_dependencies_with_balance(&[]); - let info = mock_info("instantiator", &[]); - let env = mock_env(); - let init_msg = InstantiateMsg { - name: init_name.clone(), - admin: Some(init_admin.into_string()), - symbol: init_symbol.clone(), - decimals: init_decimals.clone(), - initial_balances: Some(vec![InitialBalance { - address: "giannis".to_string(), - amount: init_supply, - }]), - prng_seed: Binary::from("lolz fun yay".as_bytes()), - config: None, - supported_denoms: None, - }; - let init_result = instantiate(deps.as_mut(), env, info, init_msg); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); - - let query_msg = QueryMsg::ExchangeRate {}; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - assert!( - query_result.is_ok(), - "Init failed: {}", - query_result.err().unwrap() - ); - let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); - match query_answer { - QueryAnswer::ExchangeRate { rate, denom } => { - assert_eq!(rate, Uint128::new(0)); - assert_eq!(denom, String::new()); - } - _ => panic!("unexpected"), - } - } - - #[test] - fn test_query_allowance() { - let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "giannis".to_string(), - amount: Uint128::new(5000), - }]); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); - - let handle_msg = ExecuteMsg::IncreaseAllowance { - spender: "lebron".to_string(), - amount: Uint128::new(2000), - padding: None, - expiration: None, - }; - let info = mock_info("giannis", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - assert!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() - ); - - let vk1 = "key1".to_string(); - let vk2 = "key2".to_string(); - - let query_msg = QueryMsg::Allowance { - owner: "giannis".to_string(), - spender: "lebron".to_string(), - key: vk1.clone(), - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - assert!( - query_result.is_ok(), - "Query failed: {}", - query_result.err().unwrap() - ); - let error = extract_error_msg(query_result); - assert!(error.contains("Wrong viewing key")); - - let handle_msg = ExecuteMsg::SetViewingKey { - key: vk1.clone(), - padding: None, - }; - let info = mock_info("lebron", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - let unwrapped_result: ExecuteAnswer = - from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); - assert_eq!( - to_binary(&unwrapped_result).unwrap(), - to_binary(&ExecuteAnswer::SetViewingKey { - status: ResponseStatus::Success - }) - .unwrap(), - ); - - let handle_msg = ExecuteMsg::SetViewingKey { - key: vk2.clone(), - padding: None, - }; - let info = mock_info("giannis", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - let unwrapped_result: ExecuteAnswer = - from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); - assert_eq!( - to_binary(&unwrapped_result).unwrap(), - to_binary(&ExecuteAnswer::SetViewingKey { - status: ResponseStatus::Success - }) - .unwrap(), - ); - - let query_msg = QueryMsg::Allowance { - owner: "giannis".to_string(), - spender: "lebron".to_string(), - key: vk1.clone(), - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - let allowance = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::Allowance { allowance, .. } => allowance, - _ => panic!("Unexpected"), - }; - assert_eq!(allowance, Uint128::new(2000)); - - let query_msg = QueryMsg::Allowance { - owner: "giannis".to_string(), - spender: "lebron".to_string(), - key: vk2.clone(), - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - let allowance = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::Allowance { allowance, .. } => allowance, - _ => panic!("Unexpected"), - }; - assert_eq!(allowance, Uint128::new(2000)); - - let query_msg = QueryMsg::Allowance { - owner: "lebron".to_string(), - spender: "giannis".to_string(), - key: vk2.clone(), - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - let allowance = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::Allowance { allowance, .. } => allowance, - _ => panic!("Unexpected"), - }; - assert_eq!(allowance, Uint128::new(0)); - } - - #[test] - fn test_query_all_allowances() { - let num_owners = 3; - let num_spenders = 20; - let vk = "key".to_string(); - - let initial_balances: Vec<InitialBalance> = (0..num_owners) - .into_iter() - .map(|i| InitialBalance { - address: format!("owner{}", i), - amount: Uint128::new(5000), - }) - .collect(); - let (init_result, mut deps) = init_helper(initial_balances); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); - for i in 0..num_owners { - let handle_msg = ExecuteMsg::SetViewingKey { - key: vk.clone(), - padding: None, - }; - let info = mock_info(format!("owner{}", i).as_str(), &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - let unwrapped_result: ExecuteAnswer = - from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); - assert_eq!( - to_binary(&unwrapped_result).unwrap(), - to_binary(&ExecuteAnswer::SetViewingKey { - status: ResponseStatus::Success - }) - .unwrap(), - ); - } - - for i in 0..num_owners { - for j in 0..num_spenders { - let handle_msg = ExecuteMsg::IncreaseAllowance { - spender: format!("spender{}", j), - amount: Uint128::new(50), - padding: None, - expiration: None, - }; - let info = mock_info(format!("owner{}", i).as_str(), &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() - ); - - let handle_msg = ExecuteMsg::SetViewingKey { - key: vk.clone(), - padding: None, - }; - let info = mock_info(format!("spender{}", j).as_str(), &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - let unwrapped_result: ExecuteAnswer = - from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); - assert_eq!( - to_binary(&unwrapped_result).unwrap(), - to_binary(&ExecuteAnswer::SetViewingKey { - status: ResponseStatus::Success - }) - .unwrap(), - ); - } - } - - let query_msg = QueryMsg::AllowancesGiven { - owner: "owner0".to_string(), - key: vk.clone(), - page: None, - page_size: 5, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::AllowancesGiven { - owner, - allowances, - count, - } => { - assert_eq!(owner, "owner0".to_string()); - assert_eq!(allowances.len(), 5); - assert_eq!(allowances[0].spender, "spender0"); - assert_eq!(allowances[0].allowance, Uint128::from(50_u128)); - assert_eq!(allowances[0].expiration, None); - assert_eq!(count, num_spenders); - } - _ => panic!("Unexpected"), - }; - - let query_msg = QueryMsg::AllowancesGiven { - owner: "owner1".to_string(), - key: vk.clone(), - page: Some(1), - page_size: 5, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::AllowancesGiven { - owner, - allowances, - count, - } => { - assert_eq!(owner, "owner1".to_string()); - assert_eq!(allowances.len(), 5); - assert_eq!(allowances[0].spender, "spender5"); - assert_eq!(allowances[0].allowance, Uint128::from(50_u128)); - assert_eq!(allowances[0].expiration, None); - assert_eq!(count, num_spenders); - } - _ => panic!("Unexpected"), - }; - - let query_msg = QueryMsg::AllowancesGiven { - owner: "owner1".to_string(), - key: vk.clone(), - page: Some(0), - page_size: 23, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::AllowancesGiven { - owner, - allowances, - count, - } => { - assert_eq!(owner, "owner1".to_string()); - assert_eq!(allowances.len(), 20); - assert_eq!(count, num_spenders); - } - _ => panic!("Unexpected"), - }; - - let query_msg = QueryMsg::AllowancesGiven { - owner: "owner1".to_string(), - key: vk.clone(), - page: Some(2), - page_size: 8, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::AllowancesGiven { - owner, - allowances, - count, - } => { - assert_eq!(owner, "owner1".to_string()); - assert_eq!(allowances.len(), 4); - assert_eq!(count, num_spenders); - } - _ => panic!("Unexpected"), - }; - - let query_msg = QueryMsg::AllowancesGiven { - owner: "owner2".to_string(), - key: vk.clone(), - page: Some(5), - page_size: 5, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::AllowancesGiven { - owner, - allowances, - count, - } => { - assert_eq!(owner, "owner2".to_string()); - assert_eq!(allowances.len(), 0); - assert_eq!(count, num_spenders); - } - _ => panic!("Unexpected"), - }; - - let query_msg = QueryMsg::AllowancesReceived { - spender: "spender0".to_string(), - key: vk.clone(), - page: None, - page_size: 10, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::AllowancesReceived { - spender, - allowances, - count, - } => { - assert_eq!(spender, "spender0".to_string()); - assert_eq!(allowances.len(), 3); - assert_eq!(allowances[0].owner, "owner0"); - assert_eq!(allowances[0].allowance, Uint128::from(50_u128)); - assert_eq!(allowances[0].expiration, None); - assert_eq!(count, num_owners); - } - _ => panic!("Unexpected"), - }; - - let query_msg = QueryMsg::AllowancesReceived { - spender: "spender1".to_string(), - key: vk.clone(), - page: Some(1), - page_size: 1, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::AllowancesReceived { - spender, - allowances, - count, - } => { - assert_eq!(spender, "spender1".to_string()); - assert_eq!(allowances.len(), 1); - assert_eq!(allowances[0].owner, "owner1"); - assert_eq!(allowances[0].allowance, Uint128::from(50_u128)); - assert_eq!(allowances[0].expiration, None); - assert_eq!(count, num_owners); - } - _ => panic!("Unexpected"), - }; - } + let init_decimals = 8; - #[test] - fn test_query_balance() { - let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }]); + let init_supply = Uint128::new(5000); + + let mut deps = mock_dependencies_with_balance(&[]); + let info = mock_info("instantiator", &[]); + let env = mock_env(); + let init_config: InitConfig = from_binary(&Binary::from( + format!( + "{{\"public_total_supply\":{}, + \"enable_deposit\":{}, + \"enable_redeem\":{}, + \"enable_mint\":{}, + \"enable_burn\":{}}}", + true, true, false, false, false + ) + .as_bytes(), + )) + .unwrap(); + let init_msg = InstantiateMsg { + name: init_name.clone(), + admin: Some(init_admin.into_string()), + symbol: init_symbol.clone(), + decimals: init_decimals.clone(), + initial_balances: Some(vec![InitialBalance { + address: "giannis".to_string(), + amount: init_supply, + }]), + prng_seed: Binary::from("lolz fun yay".as_bytes()), + config: Some(init_config), + supported_denoms: Some(vec!["uscrt".to_string()]), + }; + let init_result = instantiate(deps.as_mut(), env, info, init_msg); assert!( init_result.is_ok(), "Init failed: {}", init_result.err().unwrap() ); - let handle_msg = ExecuteMsg::SetViewingKey { - key: "key".to_string(), - padding: None, - }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - let unwrapped_result: ExecuteAnswer = - from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); - assert_eq!( - to_binary(&unwrapped_result).unwrap(), - to_binary(&ExecuteAnswer::SetViewingKey { - status: ResponseStatus::Success - }) - .unwrap(), + let query_msg = QueryMsg::ExchangeRate {}; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "Init failed: {}", + query_result.err().unwrap() ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::ExchangeRate { rate, denom } => { + assert_eq!(rate, Uint128::new(100)); + assert_eq!(denom, "SCRT"); + } + _ => panic!("unexpected"), + } - let query_msg = QueryMsg::Balance { - address: "bob".to_string(), - key: "wrong_key".to_string(), - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - let error = extract_error_msg(query_result); - assert!(error.contains("Wrong viewing key")); + // test same number of decimals as SCRT + let init_name = "sec-sec".to_string(); + let init_admin = Addr::unchecked("admin".to_string()); + let init_symbol = "SECSEC".to_string(); + let init_decimals = 6; - let query_msg = QueryMsg::Balance { - address: "bob".to_string(), - key: "key".to_string(), - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - let balance = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::Balance { amount } => amount, - _ => panic!("Unexpected"), - }; - assert_eq!(balance, Uint128::new(5000)); - } + let init_supply = Uint128::new(5000); - #[test] - fn test_query_transfer_history() { - let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }]); + let mut deps = mock_dependencies_with_balance(&[]); + let info = mock_info("instantiator", &[]); + let env = mock_env(); + let init_config: InitConfig = from_binary(&Binary::from( + format!( + "{{\"public_total_supply\":{}, + \"enable_deposit\":{}, + \"enable_redeem\":{}, + \"enable_mint\":{}, + \"enable_burn\":{}}}", + true, true, false, false, false + ) + .as_bytes(), + )) + .unwrap(); + let init_msg = InstantiateMsg { + name: init_name.clone(), + admin: Some(init_admin.into_string()), + symbol: init_symbol.clone(), + decimals: init_decimals.clone(), + initial_balances: Some(vec![InitialBalance { + address: "giannis".to_string(), + amount: init_supply, + }]), + prng_seed: Binary::from("lolz fun yay".as_bytes()), + config: Some(init_config), + supported_denoms: Some(vec!["uscrt".to_string()]), + }; + let init_result = instantiate(deps.as_mut(), env, info, init_msg); assert!( init_result.is_ok(), "Init failed: {}", init_result.err().unwrap() ); - let handle_msg = ExecuteMsg::SetViewingKey { - key: "key".to_string(), - padding: None, - }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - assert!(ensure_success(handle_result.unwrap())); - - let handle_msg = ExecuteMsg::Transfer { - recipient: "alice".to_string(), - amount: Uint128::new(1000), - memo: None, - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let query_msg = QueryMsg::ExchangeRate {}; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "Init failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::ExchangeRate { rate, denom } => { + assert_eq!(rate, Uint128::new(1)); + assert_eq!(denom, "SCRT"); + } + _ => panic!("unexpected"), + } - let result = handle_result.unwrap(); - assert!(ensure_success(result)); - let handle_msg = ExecuteMsg::Transfer { - recipient: "banana".to_string(), - amount: Uint128::new(500), - memo: None, - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("bob", &[]); + // test less decimal places than SCRT + let init_name = "sec-sec".to_string(); + let init_admin = Addr::unchecked("admin".to_string()); + let init_symbol = "SECSEC".to_string(); + let init_decimals = 3; - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let init_supply = Uint128::new(5000); - let result = handle_result.unwrap(); - assert!(ensure_success(result)); - let handle_msg = ExecuteMsg::Transfer { - recipient: "mango".to_string(), - amount: Uint128::new(2500), - memo: None, - decoys: None, - entropy: None, - padding: None, + let mut deps = mock_dependencies_with_balance(&[]); + let info = mock_info("instantiator", &[]); + let env = mock_env(); + let init_config: InitConfig = from_binary(&Binary::from( + format!( + "{{\"public_total_supply\":{}, + \"enable_deposit\":{}, + \"enable_redeem\":{}, + \"enable_mint\":{}, + \"enable_burn\":{}}}", + true, true, false, false, false + ) + .as_bytes(), + )) + .unwrap(); + let init_msg = InstantiateMsg { + name: init_name.clone(), + admin: Some(init_admin.into_string()), + symbol: init_symbol.clone(), + decimals: init_decimals.clone(), + initial_balances: Some(vec![InitialBalance { + address: "giannis".to_string(), + amount: init_supply, + }]), + prng_seed: Binary::from("lolz fun yay".as_bytes()), + config: Some(init_config), + supported_denoms: Some(vec!["uscrt".to_string()]), }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - let result = handle_result.unwrap(); - assert!(ensure_success(result)); + let init_result = instantiate(deps.as_mut(), env, info, init_msg); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); - let query_msg = QueryMsg::TransferHistory { - address: "bob".to_string(), - key: "key".to_string(), - page: None, - page_size: 0, - should_filter_decoys: false, - }; + let query_msg = QueryMsg::ExchangeRate {}; let query_result = query(deps.as_ref(), mock_env(), query_msg); - // let a: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); - // println!("{:?}", a); - let transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, - _ => panic!("Unexpected"), - }; - assert!(transfers.is_empty()); + assert!( + query_result.is_ok(), + "Init failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::ExchangeRate { rate, denom } => { + assert_eq!(rate, Uint128::new(1000)); + assert_eq!(denom, "SECSEC"); + } + _ => panic!("unexpected"), + } - let query_msg = QueryMsg::TransferHistory { - address: "bob".to_string(), - key: "key".to_string(), - page: None, - page_size: 10, - should_filter_decoys: false, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, - _ => panic!("Unexpected"), - }; - assert_eq!(transfers.len(), 3); + // test depost/redeem not enabled + let init_name = "sec-sec".to_string(); + let init_admin = Addr::unchecked("admin".to_string()); + let init_symbol = "SECSEC".to_string(); + let init_decimals = 3; - let query_msg = QueryMsg::TransferHistory { - address: "bob".to_string(), - key: "key".to_string(), - page: None, - page_size: 2, - should_filter_decoys: false, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, - _ => panic!("Unexpected"), - }; - assert_eq!(transfers.len(), 2); + let init_supply = Uint128::new(5000); - let query_msg = QueryMsg::TransferHistory { - address: "bob".to_string(), - key: "key".to_string(), - page: Some(1), - page_size: 2, - should_filter_decoys: false, + let mut deps = mock_dependencies_with_balance(&[]); + let info = mock_info("instantiator", &[]); + let env = mock_env(); + let init_msg = InstantiateMsg { + name: init_name.clone(), + admin: Some(init_admin.into_string()), + symbol: init_symbol.clone(), + decimals: init_decimals.clone(), + initial_balances: Some(vec![InitialBalance { + address: "giannis".to_string(), + amount: init_supply, + }]), + prng_seed: Binary::from("lolz fun yay".as_bytes()), + config: None, + supported_denoms: None, }; + let init_result = instantiate(deps.as_mut(), env, info, init_msg); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let query_msg = QueryMsg::ExchangeRate {}; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, - _ => panic!("Unexpected"), - }; - assert_eq!(transfers.len(), 1); + assert!( + query_result.is_ok(), + "Init failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::ExchangeRate { rate, denom } => { + assert_eq!(rate, Uint128::new(0)); + assert_eq!(denom, String::new()); + } + _ => panic!("unexpected"), + } } #[test] - fn test_query_transfer_history_with_decoys() { - let (init_result, mut deps) = init_helper(vec![ - InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }, - InitialBalance { - address: "jhon".to_string(), - amount: Uint128::new(7000), - }, - ]); + fn test_query_allowance() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "giannis".to_string(), + amount: Uint128::new(5000), + }]); assert!( init_result.is_ok(), "Init failed: {}", init_result.err().unwrap() ); - let handle_msg = ExecuteMsg::SetViewingKey { - key: "key".to_string(), + let handle_msg = ExecuteMsg::IncreaseAllowance { + spender: "lebron".to_string(), + amount: Uint128::new(2000), padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + expiration: None, }; - let info = mock_info("bob", &[]); + let info = mock_info("giannis", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!(ensure_success(handle_result.unwrap())); - let handle_msg = ExecuteMsg::SetViewingKey { - key: "alice_key".to_string(), - padding: None, - }; - let info = mock_info("alice", &[]); + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!(ensure_success(handle_result.unwrap())); + let vk1 = "key1".to_string(); + let vk2 = "key2".to_string(); - let handle_msg = ExecuteMsg::SetViewingKey { - key: "lior_key".to_string(), - padding: None, + let query_msg = QueryMsg::Allowance { + owner: "giannis".to_string(), + spender: "lebron".to_string(), + key: vk1.clone(), }; - let info = mock_info("lior", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!(ensure_success(handle_result.unwrap())); + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "Query failed: {}", + query_result.err().unwrap() + ); + let error = extract_error_msg(query_result); + assert!(error.contains("Wrong viewing key")); let handle_msg = ExecuteMsg::SetViewingKey { - key: "banana_key".to_string(), + key: vk1.clone(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("banana", &[]); + let info = mock_info("lebron", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!(ensure_success(handle_result.unwrap())); - - let lior_addr = Addr::unchecked("lior".to_string()); - let jhon_addr = Addr::unchecked("jhon".to_string()); - let alice_addr = Addr::unchecked("alice".to_string()); - - let handle_msg = ExecuteMsg::Transfer { - recipient: "alice".to_string(), - amount: Uint128::new(1000), - memo: None, - decoys: Some(vec![ - lior_addr.clone(), - jhon_addr.clone(), - alice_addr.clone(), - ]), - - entropy: Some(Binary::from_base64("VEVTVFRFU1RURVNUQ0hFQ0tDSEVDSw==").unwrap()), - padding: None, - }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let unwrapped_result: ExecuteAnswer = + from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); + assert_eq!( + to_binary(&unwrapped_result).unwrap(), + to_binary(&ExecuteAnswer::SetViewingKey { + status: ResponseStatus::Success + }) + .unwrap(), + ); - let result = handle_result.unwrap(); - assert!(ensure_success(result)); - let handle_msg = ExecuteMsg::Transfer { - recipient: "banana".to_string(), - amount: Uint128::new(500), - memo: None, - decoys: None, - entropy: None, + let handle_msg = ExecuteMsg::SetViewingKey { + key: vk2.clone(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("bob", &[]); + let info = mock_info("giannis", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let result = handle_result.unwrap(); - assert!(ensure_success(result)); + let unwrapped_result: ExecuteAnswer = + from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); + assert_eq!( + to_binary(&unwrapped_result).unwrap(), + to_binary(&ExecuteAnswer::SetViewingKey { + status: ResponseStatus::Success + }) + .unwrap(), + ); - let query_msg = QueryMsg::TransferHistory { - address: "bob".to_string(), - key: "key".to_string(), - page: None, - page_size: 10, - should_filter_decoys: true, + let query_msg = QueryMsg::Allowance { + owner: "giannis".to_string(), + spender: "lebron".to_string(), + key: vk1.clone(), }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, + let allowance = match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::Allowance { allowance, .. } => allowance, _ => panic!("Unexpected"), }; - assert_eq!(transfers.len(), 2); + assert_eq!(allowance, Uint128::new(2000)); - let query_msg = QueryMsg::TransferHistory { - address: "alice".to_string(), - key: "alice_key".to_string(), - page: None, - page_size: 10, - should_filter_decoys: false, + let query_msg = QueryMsg::Allowance { + owner: "giannis".to_string(), + spender: "lebron".to_string(), + key: vk2.clone(), }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, + let allowance = match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::Allowance { allowance, .. } => allowance, _ => panic!("Unexpected"), }; - assert_eq!(transfers.len(), 2); + assert_eq!(allowance, Uint128::new(2000)); - let query_msg = QueryMsg::TransferHistory { - address: "alice".to_string(), - key: "alice_key".to_string(), - page: None, - page_size: 10, - should_filter_decoys: true, + let query_msg = QueryMsg::Allowance { + owner: "lebron".to_string(), + spender: "giannis".to_string(), + key: vk2.clone(), }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, + let allowance = match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::Allowance { allowance, .. } => allowance, _ => panic!("Unexpected"), }; - assert_eq!(transfers.len(), 1); + assert_eq!(allowance, Uint128::new(0)); + } + + #[test] + fn test_query_all_allowances() { + let num_owners = 3; + let num_spenders = 20; + let vk = "key".to_string(); + + let initial_balances: Vec<InitialBalance> = (0..num_owners) + .into_iter() + .map(|i| InitialBalance { + address: format!("owner{}", i), + amount: Uint128::new(5000), + }) + .collect(); + let (init_result, mut deps) = init_helper(initial_balances); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + for i in 0..num_owners { + let handle_msg = ExecuteMsg::SetViewingKey { + key: vk.clone(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info(format!("owner{}", i).as_str(), &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + let unwrapped_result: ExecuteAnswer = + from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); + assert_eq!( + to_binary(&unwrapped_result).unwrap(), + to_binary(&ExecuteAnswer::SetViewingKey { + status: ResponseStatus::Success + }) + .unwrap(), + ); + } + + for i in 0..num_owners { + for j in 0..num_spenders { + let handle_msg = ExecuteMsg::IncreaseAllowance { + spender: format!("spender{}", j), + amount: Uint128::new(50), + padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + expiration: None, + }; + let info = mock_info(format!("owner{}", i).as_str(), &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); + + let handle_msg = ExecuteMsg::SetViewingKey { + key: vk.clone(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info(format!("spender{}", j).as_str(), &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + let unwrapped_result: ExecuteAnswer = + from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); + assert_eq!( + to_binary(&unwrapped_result).unwrap(), + to_binary(&ExecuteAnswer::SetViewingKey { + status: ResponseStatus::Success + }) + .unwrap(), + ); + } + } - let query_msg = QueryMsg::TransferHistory { - address: "banana".to_string(), - key: "banana_key".to_string(), + let query_msg = QueryMsg::AllowancesGiven { + owner: "owner0".to_string(), + key: vk.clone(), page: None, - page_size: 10, - should_filter_decoys: true, + page_size: 5, }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, + match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::AllowancesGiven { + owner, + allowances, + count, + } => { + assert_eq!(owner, "owner0".to_string()); + assert_eq!(allowances.len(), 5); + assert_eq!(allowances[0].spender, "spender0"); + assert_eq!(allowances[0].allowance, Uint128::from(50_u128)); + assert_eq!(allowances[0].expiration, None); + assert_eq!(count, num_spenders); + } _ => panic!("Unexpected"), }; - assert_eq!(transfers.len(), 1); - let query_msg = QueryMsg::TransferHistory { - address: "lior".to_string(), - key: "lior_key".to_string(), - page: None, - page_size: 10, - should_filter_decoys: true, + let query_msg = QueryMsg::AllowancesGiven { + owner: "owner1".to_string(), + key: vk.clone(), + page: Some(1), + page_size: 5, }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, + match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::AllowancesGiven { + owner, + allowances, + count, + } => { + assert_eq!(owner, "owner1".to_string()); + assert_eq!(allowances.len(), 5); + assert_eq!(allowances[0].spender, "spender5"); + assert_eq!(allowances[0].allowance, Uint128::from(50_u128)); + assert_eq!(allowances[0].expiration, None); + assert_eq!(count, num_spenders); + } _ => panic!("Unexpected"), }; - assert_eq!(transfers.len(), 0); - let query_msg = QueryMsg::Balance { - address: "bob".to_string(), - key: "key".to_string(), + let query_msg = QueryMsg::AllowancesGiven { + owner: "owner1".to_string(), + key: vk.clone(), + page: Some(0), + page_size: 23, }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let balance = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::Balance { amount } => amount, + match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::AllowancesGiven { + owner, + allowances, + count, + } => { + assert_eq!(owner, "owner1".to_string()); + assert_eq!(allowances.len(), 20); + assert_eq!(count, num_spenders); + } _ => panic!("Unexpected"), }; - assert_eq!(balance, Uint128::new(3500)); - let query_msg = QueryMsg::Balance { - address: "alice".to_string(), - key: "alice_key".to_string(), + let query_msg = QueryMsg::AllowancesGiven { + owner: "owner1".to_string(), + key: vk.clone(), + page: Some(2), + page_size: 8, }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let balance = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::Balance { amount } => amount, + match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::AllowancesGiven { + owner, + allowances, + count, + } => { + assert_eq!(owner, "owner1".to_string()); + assert_eq!(allowances.len(), 4); + assert_eq!(count, num_spenders); + } _ => panic!("Unexpected"), }; - assert_eq!(balance, Uint128::new(1000)); - let query_msg = QueryMsg::Balance { - address: "banana".to_string(), - key: "banana_key".to_string(), + let query_msg = QueryMsg::AllowancesGiven { + owner: "owner2".to_string(), + key: vk.clone(), + page: Some(5), + page_size: 5, }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let balance = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::Balance { amount } => amount, + match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::AllowancesGiven { + owner, + allowances, + count, + } => { + assert_eq!(owner, "owner2".to_string()); + assert_eq!(allowances.len(), 0); + assert_eq!(count, num_spenders); + } _ => panic!("Unexpected"), }; - assert_eq!(balance, Uint128::new(500)); - let query_msg = QueryMsg::Balance { - address: "lior".to_string(), - key: "lior_key".to_string(), + let query_msg = QueryMsg::AllowancesReceived { + spender: "spender0".to_string(), + key: vk.clone(), + page: None, + page_size: 10, }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let balance = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::Balance { amount } => amount, + match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::AllowancesReceived { + spender, + allowances, + count, + } => { + assert_eq!(spender, "spender0".to_string()); + assert_eq!(allowances.len(), 3); + assert_eq!(allowances[0].owner, "owner0"); + assert_eq!(allowances[0].allowance, Uint128::from(50_u128)); + assert_eq!(allowances[0].expiration, None); + assert_eq!(count, num_owners); + } _ => panic!("Unexpected"), }; - assert_eq!(balance, Uint128::new(0)); - } - - #[test] - fn test_query_transaction_history() { - let (init_result, mut deps) = init_helper_with_config( - vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(10000), - }], - true, - true, - true, - true, - 1000, - vec!["uscrt".to_string()], - ); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); - - let handle_msg = ExecuteMsg::SetViewingKey { - key: "key".to_string(), - padding: None, - }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - assert!(ensure_success(handle_result.unwrap())); - - let handle_msg = ExecuteMsg::Burn { - amount: Uint128::new(1), - memo: Some("my burn message".to_string()), - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - assert!( - handle_result.is_ok(), - "Pause handle failed: {}", - handle_result.err().unwrap() - ); - - let handle_msg = ExecuteMsg::Redeem { - amount: Uint128::new(1000), - denom: Option::from("uscrt".to_string()), - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - assert!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() - ); - - let handle_msg = ExecuteMsg::Mint { - recipient: "bob".to_string(), - amount: Uint128::new(100), - memo: Some("my mint message".to_string()), - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("admin", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - - assert!(ensure_success(handle_result.unwrap())); - - let handle_msg = ExecuteMsg::Deposit { - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info( - "bob", - &[Coin { - denom: "uscrt".to_string(), - amount: Uint128::new(1000), - }], - ); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() - ); - - let handle_msg = ExecuteMsg::Transfer { - recipient: "alice".to_string(), - amount: Uint128::new(1000), - memo: Some("my transfer message #1".to_string()), - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let result = handle_result.unwrap(); - assert!(ensure_success(result)); - - let handle_msg = ExecuteMsg::Transfer { - recipient: "banana".to_string(), - amount: Uint128::new(500), - memo: Some("my transfer message #2".to_string()), - decoys: None, - entropy: None, - padding: None, + let query_msg = QueryMsg::AllowancesReceived { + spender: "spender1".to_string(), + key: vk.clone(), + page: Some(1), + page_size: 1, }; - let info = mock_info("bob", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let query_result = query(deps.as_ref(), mock_env(), query_msg); + match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::AllowancesReceived { + spender, + allowances, + count, + } => { + assert_eq!(spender, "spender1".to_string()); + assert_eq!(allowances.len(), 1); + assert_eq!(allowances[0].owner, "owner1"); + assert_eq!(allowances[0].allowance, Uint128::from(50_u128)); + assert_eq!(allowances[0].expiration, None); + assert_eq!(count, num_owners); + } + _ => panic!("Unexpected"), + }; + } - let result = handle_result.unwrap(); - assert!(ensure_success(result)); + #[test] + fn test_query_balance() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }]); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); - let handle_msg = ExecuteMsg::Transfer { - recipient: "mango".to_string(), - amount: Uint128::new(2500), - memo: Some("my transfer message #3".to_string()), - decoys: None, - entropy: None, + let handle_msg = ExecuteMsg::SetViewingKey { + key: "key".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let result = handle_result.unwrap(); - assert!(ensure_success(result)); + let unwrapped_result: ExecuteAnswer = + from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); + assert_eq!( + to_binary(&unwrapped_result).unwrap(), + to_binary(&ExecuteAnswer::SetViewingKey { + status: ResponseStatus::Success + }) + .unwrap(), + ); - let query_msg = QueryMsg::TransferHistory { + let query_msg = QueryMsg::Balance { address: "bob".to_string(), - key: "key".to_string(), - page: None, - page_size: 10, - should_filter_decoys: false, + key: "wrong_key".to_string(), }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, - _ => panic!("Unexpected"), - }; - assert_eq!(transfers.len(), 3); + let error = extract_error_msg(query_result); + assert!(error.contains("Wrong viewing key")); - let query_msg = QueryMsg::TransactionHistory { + let query_msg = QueryMsg::Balance { address: "bob".to_string(), key: "key".to_string(), - page: None, - page_size: 10, - should_filter_decoys: false, }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransactionHistory { txs, .. } => txs, - other => panic!("Unexpected: {:?}", other), + let balance = match from_binary(&query_result.unwrap()).unwrap() { + QueryAnswer::Balance { amount } => amount, + _ => panic!("Unexpected"), }; - - use crate::transaction_history::{ExtendedTx, TxAction}; - let expected_transfers = [ - ExtendedTx { - id: 8, - action: TxAction::Transfer { - from: Addr::unchecked("bob".to_string()), - sender: Addr::unchecked("bob".to_string()), - recipient: Addr::unchecked("mango".to_string()), - }, - coins: Coin { - denom: "SECSEC".to_string(), - amount: Uint128::new(2500), - }, - memo: Some("my transfer message #3".to_string()), - block_time: 1571797419, - block_height: 12345, - }, - ExtendedTx { - id: 7, - action: TxAction::Transfer { - from: Addr::unchecked("bob".to_string()), - sender: Addr::unchecked("bob".to_string()), - recipient: Addr::unchecked("banana".to_string()), - }, - coins: Coin { - denom: "SECSEC".to_string(), - amount: Uint128::new(500), - }, - memo: Some("my transfer message #2".to_string()), - block_time: 1571797419, - block_height: 12345, - }, - ExtendedTx { - id: 6, - action: TxAction::Transfer { - from: Addr::unchecked("bob".to_string()), - sender: Addr::unchecked("bob".to_string()), - recipient: Addr::unchecked("alice".to_string()), - }, - coins: Coin { - denom: "SECSEC".to_string(), - amount: Uint128::new(1000), - }, - memo: Some("my transfer message #1".to_string()), - block_time: 1571797419, - block_height: 12345, - }, - ExtendedTx { - id: 5, - action: TxAction::Deposit {}, - coins: Coin { - denom: "uscrt".to_string(), - amount: Uint128::new(1000), - }, - memo: None, - block_time: 1571797419, - block_height: 12345, - }, - ExtendedTx { - id: 4, - action: TxAction::Mint { - minter: Addr::unchecked("admin".to_string()), - recipient: Addr::unchecked("bob".to_string()), - }, - coins: Coin { - denom: "SECSEC".to_string(), - amount: Uint128::new(100), - }, - memo: Some("my mint message".to_string()), - block_time: 1571797419, - block_height: 12345, - }, - ExtendedTx { - id: 3, - action: TxAction::Redeem {}, - coins: Coin { - denom: "SECSEC".to_string(), - amount: Uint128::new(1000), - }, - memo: None, - block_time: 1571797419, - block_height: 12345, - }, - ExtendedTx { - id: 2, - action: TxAction::Burn { - burner: Addr::unchecked("bob".to_string()), - owner: Addr::unchecked("bob".to_string()), - }, - coins: Coin { - denom: "SECSEC".to_string(), - amount: Uint128::new(1), - }, - memo: Some("my burn message".to_string()), - block_time: 1571797419, - block_height: 12345, - }, - ExtendedTx { - id: 1, - action: TxAction::Mint { - minter: Addr::unchecked("admin".to_string()), - recipient: Addr::unchecked("bob".to_string()), - }, - coins: Coin { - denom: "SECSEC".to_string(), - amount: Uint128::new(10000), - }, - - memo: Some("Initial Balance".to_string()), - block_time: 1571797419, - block_height: 12345, - }, - ]; - - assert_eq!(transfers, expected_transfers); + assert_eq!(balance, Uint128::new(5000)); } #[test] - fn test_query_transaction_history_with_decoys() { + fn test_query_transaction_history() { let (init_result, mut deps) = init_helper_with_config( - vec![ - InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }, - InitialBalance { - address: "jhon".to_string(), - amount: Uint128::new(7000), - }, - ], + vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(10000), + }], true, true, true, @@ -5953,7 +7059,6 @@ mod tests { 1000, vec!["uscrt".to_string()], ); - assert!( init_result.is_ok(), "Init failed: {}", @@ -5962,54 +7067,21 @@ mod tests { let handle_msg = ExecuteMsg::SetViewingKey { key: "key".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!(ensure_success(handle_result.unwrap())); - - let handle_msg = ExecuteMsg::SetViewingKey { - key: "alice_key".to_string(), - padding: None, - }; - let info = mock_info("alice", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!(ensure_success(handle_result.unwrap())); - - let handle_msg = ExecuteMsg::SetViewingKey { - key: "lior_key".to_string(), - padding: None, - }; - let info = mock_info("lior", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!(ensure_success(handle_result.unwrap())); - - let handle_msg = ExecuteMsg::SetViewingKey { - key: "jhon_key".to_string(), - padding: None, - }; - let info = mock_info("jhon", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); assert!(ensure_success(handle_result.unwrap())); - let lior_addr = Addr::unchecked("lior".to_string()); - let jhon_addr = Addr::unchecked("jhon".to_string()); - let alice_addr = Addr::unchecked("alice".to_string()); - let handle_msg = ExecuteMsg::Burn { amount: Uint128::new(1), memo: Some("my burn message".to_string()), - decoys: Some(vec![ - lior_addr.clone(), - jhon_addr.clone(), - alice_addr.clone(), - ]), - entropy: Some(Binary::from_base64("VEVTVFRFU1RURVNUQ0hFQ0tDSEVDSw==").unwrap()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -6025,12 +7097,8 @@ mod tests { let handle_msg = ExecuteMsg::Redeem { amount: Uint128::new(1000), denom: Option::from("uscrt".to_string()), - decoys: Some(vec![ - lior_addr.clone(), - jhon_addr.clone(), - alice_addr.clone(), - ]), - entropy: Some(Binary::from_base64("VEVTVFRFU1RURVNUQ0hFQ0tDSEVDSw==").unwrap()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -6047,12 +7115,8 @@ mod tests { recipient: "bob".to_string(), amount: Uint128::new(100), memo: Some("my mint message".to_string()), - decoys: Some(vec![ - lior_addr.clone(), - jhon_addr.clone(), - alice_addr.clone(), - ]), - entropy: Some(Binary::from_base64("VEVTVFRFU1RURVNUQ0hFQ0tDSEVDSw==").unwrap()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -6062,12 +7126,8 @@ mod tests { assert!(ensure_success(handle_result.unwrap())); let handle_msg = ExecuteMsg::Deposit { - decoys: Some(vec![ - lior_addr.clone(), - jhon_addr.clone(), - alice_addr.clone(), - ]), - entropy: Some(Binary::from_base64("VEVTVFRFU1RURVNUQ0hFQ0tDSEVDSw==").unwrap()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info( @@ -6089,12 +7149,8 @@ mod tests { recipient: "alice".to_string(), amount: Uint128::new(1000), memo: Some("my transfer message #1".to_string()), - decoys: Some(vec![ - lior_addr.clone(), - jhon_addr.clone(), - alice_addr.clone(), - ]), - entropy: Some(Binary::from_base64("VEVTVFRFU1RURVNUQ0hFQ0tDSEVDSw==").unwrap()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -6108,12 +7164,8 @@ mod tests { recipient: "banana".to_string(), amount: Uint128::new(500), memo: Some("my transfer message #2".to_string()), - decoys: Some(vec![ - lior_addr.clone(), - jhon_addr.clone(), - alice_addr.clone(), - ]), - entropy: Some(Binary::from_base64("VEVTVFRFU1RURVNUQ0hFQ0tDSEVDSw==").unwrap()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -6127,12 +7179,8 @@ mod tests { recipient: "mango".to_string(), amount: Uint128::new(2500), memo: Some("my transfer message #3".to_string()), - decoys: Some(vec![ - lior_addr.clone(), - jhon_addr.clone(), - alice_addr.clone(), - ]), - entropy: Some(Binary::from_base64("VEVTVFRFU1RURVNUQ0hFQ0tDSEVDSw==").unwrap()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("bob", &[]); @@ -6142,83 +7190,22 @@ mod tests { let result = handle_result.unwrap(); assert!(ensure_success(result)); - let query_msg = QueryMsg::TransactionHistory { - address: "lior".to_string(), - key: "lior_key".to_string(), - page: None, - page_size: 10, - should_filter_decoys: true, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transactions = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransactionHistory { txs, .. } => txs, - other => panic!("Unexpected: {:?}", other), - }; - - assert!(transactions.is_empty()); - - let query_msg = QueryMsg::TransactionHistory { - address: "alice".to_string(), - key: "alice_key".to_string(), - page: None, - page_size: 10, - should_filter_decoys: false, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transactions = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransactionHistory { txs, .. } => txs, - other => panic!("Unexpected: {:?}", other), - }; - - assert_eq!(transactions.len(), 7); // Transfer from bob - - let query_msg = QueryMsg::TransactionHistory { - address: "alice".to_string(), - key: "alice_key".to_string(), - page: None, - page_size: 10, - should_filter_decoys: true, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transactions = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransactionHistory { txs, .. } => txs, - other => panic!("Unexpected: {:?}", other), - }; - - assert_eq!(transactions.len(), 1); // Transfer from bob - - let query_msg = QueryMsg::TransactionHistory { - address: "jhon".to_string(), - key: "jhon_key".to_string(), - page: None, - page_size: 10, - should_filter_decoys: true, - }; - let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transactions = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransactionHistory { txs, .. } => txs, - other => panic!("Unexpected: {:?}", other), - }; - - assert_eq!(transactions.len(), 1); // Mint on init - let query_msg = QueryMsg::TransactionHistory { address: "bob".to_string(), key: "key".to_string(), page: None, page_size: 10, - should_filter_decoys: true, }; let query_result = query(deps.as_ref(), mock_env(), query_msg); - let transactions = match from_binary(&query_result.unwrap()).unwrap() { + let transfers = match from_binary(&query_result.unwrap()).unwrap() { QueryAnswer::TransactionHistory { txs, .. } => txs, other => panic!("Unexpected: {:?}", other), }; - use crate::transaction_history::{ExtendedTx, TxAction}; - let expected_transactions = [ - ExtendedTx { - id: 9, + use crate::transaction_history::TxAction; + let expected_transfers = [ + Tx { + id: 8, action: TxAction::Transfer { from: Addr::unchecked("bob".to_string()), sender: Addr::unchecked("bob".to_string()), @@ -6232,8 +7219,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 8, + Tx { + id: 7, action: TxAction::Transfer { from: Addr::unchecked("bob".to_string()), sender: Addr::unchecked("bob".to_string()), @@ -6247,8 +7234,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 7, + Tx { + id: 6, action: TxAction::Transfer { from: Addr::unchecked("bob".to_string()), sender: Addr::unchecked("bob".to_string()), @@ -6262,8 +7249,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 6, + Tx { + id: 5, action: TxAction::Deposit {}, coins: Coin { denom: "uscrt".to_string(), @@ -6273,8 +7260,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 5, + Tx { + id: 4, action: TxAction::Mint { minter: Addr::unchecked("admin".to_string()), recipient: Addr::unchecked("bob".to_string()), @@ -6287,8 +7274,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 4, + Tx { + id: 3, action: TxAction::Redeem {}, coins: Coin { denom: "SECSEC".to_string(), @@ -6298,8 +7285,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 3, + Tx { + id: 2, action: TxAction::Burn { burner: Addr::unchecked("bob".to_string()), owner: Addr::unchecked("bob".to_string()), @@ -6312,7 +7299,7 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { + Tx { id: 1, action: TxAction::Mint { minter: Addr::unchecked("admin".to_string()), @@ -6320,7 +7307,7 @@ mod tests { }, coins: Coin { denom: "SECSEC".to_string(), - amount: Uint128::new(5000), + amount: Uint128::new(10000), }, memo: Some("Initial Balance".to_string()), @@ -6329,6 +7316,6 @@ mod tests { }, ]; - assert_eq!(transactions, expected_transactions); + assert_eq!(transfers, expected_transfers); } } diff --git a/src/dwb.rs b/src/dwb.rs new file mode 100644 index 00000000..8b4c7adf --- /dev/null +++ b/src/dwb.rs @@ -0,0 +1,623 @@ +use constant_time_eq::constant_time_eq; +use cosmwasm_std::{Api, CanonicalAddr, StdError, StdResult, Storage}; +use rand::RngCore; +use secret_toolkit::storage::Item; +use secret_toolkit_crypto::ContractPrng; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; + +use crate::btbe::{merge_dwb_entry, stored_balance}; +use crate::state::{safe_add, safe_add_u64}; +use crate::transaction_history::{Tx, TRANSACTIONS}; +#[cfg(feature = "gas_tracking")] +use crate::gas_tracker::GasTracker; +#[cfg(feature = "gas_tracking")] +use cosmwasm_std::{Binary, to_binary}; +#[cfg(feature = "gas_tracking")] +use crate::msg::QueryAnswer; + +include!(concat!(env!("OUT_DIR"), "/config.rs")); + +pub const KEY_DWB: &[u8] = b"dwb"; +pub const KEY_TX_NODES_COUNT: &[u8] = b"dwb-node-cnt"; +pub const KEY_TX_NODES: &[u8] = b"dwb-tx-nodes"; + +pub static DWB: Item<DelayedWriteBuffer> = Item::new(KEY_DWB); +// use with add_suffix tx id (u64) +// does not need to be an AppendStore because we never need to iterate over global list of txs +pub static TX_NODES: Item<TxNode> = Item::new(KEY_TX_NODES); +pub static TX_NODES_COUNT: Item<u64> = Item::new(KEY_TX_NODES_COUNT); + +fn store_new_tx_node(store: &mut dyn Storage, tx_node: TxNode) -> StdResult<u64> { + // tx nodes ids serialized start at 1 + let tx_nodes_serial_id = TX_NODES_COUNT.load(store).unwrap_or_default() + 1; + TX_NODES + .add_suffix(&tx_nodes_serial_id.to_be_bytes()) + .save(store, &tx_node)?; + TX_NODES_COUNT.save(store, &(tx_nodes_serial_id))?; + Ok(tx_nodes_serial_id) +} + +// n entries + 1 "dummy" entry prepended (idx: 0 in DelayedWriteBufferEntry array) +// minimum allowable size: 3 +pub const DWB_LEN: u16 = DWB_CAPACITY + 1; + +// maximum number of tx events allowed in an entry's linked list +pub const DWB_MAX_TX_EVENTS: u16 = u16::MAX; + +#[derive(Serialize, Deserialize, Debug)] +pub struct DelayedWriteBuffer { + pub empty_space_counter: u16, + #[serde(with = "BigArray")] + pub entries: [DelayedWriteBufferEntry; DWB_LEN as usize], +} + +pub fn random_in_range(rng: &mut ContractPrng, a: u32, b: u32) -> StdResult<u32> { + if b <= a { + return Err(StdError::generic_err("invalid range")); + } + let range_size = (b - a) as u64; + // need to make sure random is below threshold to prevent modulo bias + let threshold = u64::MAX - range_size; + loop { + // this loop will almost always run only once since range_size << u64::MAX + let random_u64 = rng.next_u64(); + if random_u64 < threshold { + return Ok((random_u64 % range_size) as u32 + a); + } + } +} + +impl DelayedWriteBuffer { + pub fn new() -> StdResult<Self> { + Ok(Self { + empty_space_counter: DWB_LEN - 1, + // first entry is a dummy entry for constant-time writing + entries: [DelayedWriteBufferEntry::new(&CanonicalAddr::from(&ZERO_ADDR))?; + DWB_LEN as usize], + }) + } + + /// settles a participant's account who may or may not have an entry in the buffer + /// gets balance including any amount in the buffer, and then subtracts amount spent in this tx + pub fn settle_sender_or_owner_account( + &mut self, + store: &mut dyn Storage, + address: &CanonicalAddr, + tx_id: u64, + amount_spent: u128, + op_name: &str, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, + ) -> StdResult<u128> { + #[cfg(feature = "gas_tracking")] + let mut group1 = tracker.group("settle_sender_or_owner_account.1"); + + // release the address from the buffer + let (balance, mut dwb_entry) = self.release_dwb_recipient(store, address)?; + + #[cfg(feature = "gas_tracking")] + group1.log("release_dwb_recipient"); + + let checked_balance = balance.checked_sub(amount_spent); + if checked_balance.is_none() { + return Err(StdError::generic_err(format!( + "insufficient funds to {op_name}: balance={balance}, required={amount_spent}", + ))); + }; + + dwb_entry.add_tx_node(store, tx_id)?; + + #[cfg(feature = "gas_tracking")] + group1.log("add_tx_node"); + + let mut entry = dwb_entry.clone(); + entry.set_recipient(address)?; + + #[cfg(feature = "gas_tracking")] + group1.logf(format!( + "@entry=address:{}, amount:{}", + entry.recipient()?, + entry.amount()? + )); + + merge_dwb_entry( + store, + &entry, + Some(amount_spent), + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + Ok(checked_balance.unwrap()) + } + + /// "releases" a given recipient from the buffer, removing their entry if one exists + /// returns the new balance and the buffer entry + fn release_dwb_recipient( + &mut self, + store: &mut dyn Storage, + address: &CanonicalAddr, + ) -> StdResult<(u128, DelayedWriteBufferEntry)> { + // get the address' stored balance + let mut balance = stored_balance(store, address)?; + + // locate the position of the entry in the buffer + let matched_entry_idx = self.recipient_match(address); + + // get the current entry at the matched index (0 if dummy) + let entry = self.entries[matched_entry_idx]; + + // create a new entry to replace the released one, giving it the same address to avoid introducing random addresses + let replacement_entry = DelayedWriteBufferEntry::new(&entry.recipient()?)?; + + // add entry amount to the stored balance for the address (will be 0 if dummy) + safe_add(&mut balance, entry.amount()? as u128); + + // overwrite the entry idx with replacement + self.entries[matched_entry_idx] = replacement_entry; + + Ok((balance, entry)) + } + + // returns matched index for a given address + pub fn recipient_match(&self, address: &CanonicalAddr) -> usize { + let mut matched_index: usize = 0; + let address = address.as_slice(); + for (idx, entry) in self.entries.iter().enumerate().skip(1) { + let equals = constant_time_eq(address, entry.recipient_slice()) as usize; + // an address can only occur once in the buffer + matched_index |= idx * equals; + } + matched_index + } + + pub fn add_recipient<'a>( + &mut self, + store: &mut dyn Storage, + rng: &mut ContractPrng, + recipient: &CanonicalAddr, + tx_id: u64, + amount: u128, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker<'a>, + ) -> StdResult<()> { + #[cfg(feature = "gas_tracking")] + let mut group1 = tracker.group("add_recipient.1"); + + // check if `recipient` is already a recipient in the delayed write buffer + let recipient_index = self.recipient_match(recipient); + #[cfg(feature = "gas_tracking")] + group1.log("recipient_match"); + + // the new entry will either derive from a prior entry for the recipient or the dummy entry + let mut new_entry = self.entries[recipient_index].clone(); + + new_entry.set_recipient(recipient)?; + #[cfg(feature = "gas_tracking")] + group1.log("set_recipient"); + + new_entry.add_tx_node(store, tx_id)?; + #[cfg(feature = "gas_tracking")] + group1.log("add_tx_node"); + + new_entry.add_amount(amount)?; + #[cfg(feature = "gas_tracking")] + group1.log("add_amount"); + + // whether or not recipient is in the buffer (non-zero index) + // casting to i32 will never overflow, so long as dwb length is limited to a u16 value + let if_recipient_in_buffer = constant_time_is_not_zero(recipient_index as i32); + #[cfg(feature = "gas_tracking")] + group1.logf(format!( + "@if_recipient_in_buffer: {}", + if_recipient_in_buffer + )); + + // whether or not the buffer is fully saturated yet + let if_undersaturated = constant_time_is_not_zero(self.empty_space_counter as i32); + #[cfg(feature = "gas_tracking")] + group1.logf(format!("@if_undersaturated: {}", if_undersaturated)); + + // find the next empty entry in the buffer + let next_empty_index = (DWB_LEN - self.empty_space_counter) as usize; + #[cfg(feature = "gas_tracking")] + group1.logf(format!("@next_empty_index: {}", next_empty_index)); + + // which entry to settle (not yet considering if recipient's entry has capacity in history list) + // if recipient is in buffer or buffer is undersaturated then settle the dummy entry + // otherwise, settle a random entry + let presumptive_settle_index = constant_time_if_else( + if_recipient_in_buffer, + 0, + constant_time_if_else( + if_undersaturated, + 0, + random_in_range(rng, 1, DWB_LEN as u32)? as usize, + ), + ); + #[cfg(feature = "gas_tracking")] + group1.logf(format!( + "@presumptive_settle_index: {}", + presumptive_settle_index + )); + + // check if we have any open slots in the linked list + let if_list_can_grow = constant_time_is_not_zero( + (DWB_MAX_TX_EVENTS - self.entries[recipient_index].list_len()?) as i32, + ); + #[cfg(feature = "gas_tracking")] + group1.logf(format!("@if_list_can_grow: {}", if_list_can_grow)); + + // if we would overflow the list by updating the existing entry, then just settle that recipient + let actual_settle_index = + constant_time_if_else(if_list_can_grow, presumptive_settle_index, recipient_index); + #[cfg(feature = "gas_tracking")] + group1.logf(format!("@actual_settle_index: {}", actual_settle_index)); + + // where to write the new/replacement entry + // if recipient is in buffer then update it + // otherwise, if buffer is undersaturated then put new entry at next open slot + // otherwise, the buffer is saturated so replace the entry that is getting settled + let write_index = constant_time_if_else( + if_recipient_in_buffer, + recipient_index, + constant_time_if_else(if_undersaturated, next_empty_index, actual_settle_index), + ); + #[cfg(feature = "gas_tracking")] + group1.logf(format!("@write_index: {}", write_index)); + + // settle the entry + let dwb_entry = self.entries[actual_settle_index]; + merge_dwb_entry( + store, + &dwb_entry, + None, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + #[cfg(feature = "gas_tracking")] + let mut group2 = tracker.group("add_recipient.2"); + + #[cfg(feature = "gas_tracking")] + group2.log("merge_dwb_entry"); + + // write the new entry, which either overwrites the existing one for the same recipient, + // replaces a randomly settled one, or inserts into an "empty" slot in the buffer + self.entries[write_index] = new_entry; + + // decrement empty space counter if it is undersaturated and the recipient was not already in the buffer + self.empty_space_counter -= constant_time_if_else( + if_undersaturated, + constant_time_if_else(if_recipient_in_buffer, 0, 1), + 0, + ) as u16; + #[cfg(feature = "gas_tracking")] + group2.logf(format!( + "@empty_space_counter: {}", + self.empty_space_counter + )); + + Ok(()) + } +} + +const U16_BYTES: usize = 2; +const U64_BYTES: usize = 8; +const U128_BYTES: usize = 16; + +#[cfg(test)] +const DWB_RECIPIENT_BYTES: usize = 54; // because mock_api creates rando canonical addr that is 54 bytes long +#[cfg(not(test))] +const DWB_RECIPIENT_BYTES: usize = 20; +const DWB_AMOUNT_BYTES: usize = 8; // Max 16 (u128) +const DWB_HEAD_NODE_BYTES: usize = 5; // Max 8 (u64) +const DWB_LIST_LEN_BYTES: usize = 2; // u16 + +const_assert!(DWB_AMOUNT_BYTES <= U128_BYTES); +const_assert!(DWB_HEAD_NODE_BYTES <= U64_BYTES); +const_assert!(DWB_LIST_LEN_BYTES <= U16_BYTES); + +const DWB_ENTRY_BYTES: usize = + DWB_RECIPIENT_BYTES + DWB_AMOUNT_BYTES + DWB_HEAD_NODE_BYTES + DWB_LIST_LEN_BYTES; + +pub const ZERO_ADDR: [u8; DWB_RECIPIENT_BYTES] = [0u8; DWB_RECIPIENT_BYTES]; + +/// A delayed write buffer entry consists of the following bytes in this order: +/// +/// // recipient canonical address +/// recipient - 20 bytes +/// // for sscrt w/ 6 decimals u64 is good for > 18 trillion tokens, far exceeding supply +/// // change to 16 bytes (u128) or other size for tokens with more decimals/higher supply +/// amount - 8 bytes (u64) +/// // global id for head of linked list of transaction nodes +/// // 40 bits allows for over 1 trillion transactions +/// head_node - 5 bytes +/// // length of list (limited to 65535) +/// list_len - 2 byte +/// +/// total: 35 bytes +#[derive(Serialize, Deserialize, Clone, Copy, Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct DelayedWriteBufferEntry(#[serde(with = "BigArray")] [u8; DWB_ENTRY_BYTES]); + +impl DelayedWriteBufferEntry { + pub fn new(recipient: &CanonicalAddr) -> StdResult<Self> { + let recipient = recipient.as_slice(); + if recipient.len() != DWB_RECIPIENT_BYTES { + return Err(StdError::generic_err("dwb: invalid recipient length")); + } + let mut result = [0u8; DWB_ENTRY_BYTES]; + result[..DWB_RECIPIENT_BYTES].copy_from_slice(recipient); + Ok(Self { 0: result }) + } + + pub fn recipient_slice(&self) -> &[u8] { + &self.0[..DWB_RECIPIENT_BYTES] + } + + pub fn recipient(&self) -> StdResult<CanonicalAddr> { + let result = CanonicalAddr::try_from(self.recipient_slice()) + .or(Err(StdError::generic_err("Get dwb recipient error")))?; + Ok(result) + } + + fn set_recipient(&mut self, val: &CanonicalAddr) -> StdResult<()> { + let val_slice = val.as_slice(); + if val_slice.len() != DWB_RECIPIENT_BYTES { + return Err(StdError::generic_err("Set dwb recipient error")); + } + self.0[..DWB_RECIPIENT_BYTES].copy_from_slice(val_slice); + Ok(()) + } + + pub fn amount(&self) -> StdResult<u64> { + let start = DWB_RECIPIENT_BYTES; + let end = start + DWB_AMOUNT_BYTES; + let amount_slice = &self.0[start..end]; + let result = amount_slice + .try_into() + .or(Err(StdError::generic_err("Get dwb amount error")))?; + Ok(u64::from_be_bytes(result)) + } + + fn set_amount(&mut self, val: u64) -> StdResult<()> { + let start = DWB_RECIPIENT_BYTES; + let end = start + DWB_AMOUNT_BYTES; + self.0[start..end].copy_from_slice(&val.to_be_bytes()); + Ok(()) + } + + pub fn head_node(&self) -> StdResult<u64> { + let start = DWB_RECIPIENT_BYTES + DWB_AMOUNT_BYTES; + let end = start + DWB_HEAD_NODE_BYTES; + let head_node_slice = &self.0[start..end]; + let mut result = [0u8; U64_BYTES]; + result[U64_BYTES - DWB_HEAD_NODE_BYTES..].copy_from_slice(head_node_slice); + Ok(u64::from_be_bytes(result)) + } + + fn set_head_node(&mut self, val: u64) -> StdResult<()> { + let start = DWB_RECIPIENT_BYTES + DWB_AMOUNT_BYTES; + let end = start + DWB_HEAD_NODE_BYTES; + let val_bytes = &val.to_be_bytes()[U64_BYTES - DWB_HEAD_NODE_BYTES..]; + if val_bytes.len() != DWB_HEAD_NODE_BYTES { + return Err(StdError::generic_err("Set dwb head node error")); + } + self.0[start..end].copy_from_slice(val_bytes); + Ok(()) + } + + pub fn list_len(&self) -> StdResult<u16> { + let start = DWB_RECIPIENT_BYTES + DWB_AMOUNT_BYTES + DWB_HEAD_NODE_BYTES; + let end = start + DWB_LIST_LEN_BYTES; + let list_len_slice = &self.0[start..end]; + let result = list_len_slice + .try_into() + .or(Err(StdError::generic_err("Get dwb list len error")))?; + Ok(u16::from_be_bytes(result)) + } + + fn set_list_len(&mut self, val: u16) -> StdResult<()> { + let start = DWB_RECIPIENT_BYTES + DWB_AMOUNT_BYTES + DWB_HEAD_NODE_BYTES; + let end = start + DWB_LIST_LEN_BYTES; + self.0[start..end].copy_from_slice(&val.to_be_bytes()); + Ok(()) + } + + /// adds a tx node to the linked list + /// returns: the new head node + fn add_tx_node(&mut self, store: &mut dyn Storage, tx_id: u64) -> StdResult<u64> { + let tx_node = TxNode { + tx_id, + next: self.head_node()?, + }; + + // store the new node on chain + let new_node = store_new_tx_node(store, tx_node)?; + // set the head node to the new node id + self.set_head_node(new_node)?; + // increment the node list length + self.set_list_len(self.list_len()? + 1)?; + + Ok(new_node) + } + + // adds some amount to the total amount for all txs in the entry linked list + // returns: the new amount + fn add_amount(&mut self, add_tx_amount: u128) -> StdResult<u64> { + // change this to safe_add if your coin needs to store amount in buffer as u128 (e.g. 18 decimals) + let mut amount = self.amount()?; + let add_tx_amount_u64 = amount_u64(Some(add_tx_amount))?; + safe_add_u64(&mut amount, add_tx_amount_u64); + self.set_amount(amount)?; + + Ok(amount) + } +} + +pub fn amount_u64(amount_spent: Option<u128>) -> StdResult<u64> { + let amount_spent = amount_spent.unwrap_or_default(); + let amount_spent_u64 = amount_spent + .try_into() + .or_else(|_| return Err(StdError::generic_err("se: spent overflow")))?; + Ok(amount_spent_u64) +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug)] +pub struct TxNode { + /// transaction id in the TRANSACTIONS list + pub tx_id: u64, + /// TX_NODES idx - pointer to the next node in the linked list + /// 0 if next is null + pub next: u64, +} + +impl TxNode { + // converts this and following elements in list to a vec of Tx + pub fn to_vec(&self, store: &dyn Storage, api: &dyn Api) -> StdResult<Vec<Tx>> { + let mut result = vec![]; + let mut cur_node = Some(self.to_owned()); + while cur_node.is_some() { + let node = cur_node.unwrap(); + let stored_tx = TRANSACTIONS + .add_suffix(&node.tx_id.to_be_bytes()) + .load(store)?; + let tx = stored_tx.into_humanized(api, node.tx_id)?; + result.push(tx); + if node.next > 0 { + let next_node = TX_NODES.add_suffix(&node.next.to_be_bytes()).load(store)?; + cur_node = Some(next_node); + } else { + cur_node = None; + } + } + + Ok(result) + } +} + +/// A tx bundle is 1 or more tx nodes added to an account's history. +/// The bundle points to a linked list of transaction nodes, which each reference +/// a transaction record by its global id. +/// used with add_suffix(canonical addr of account) +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TxBundle { + /// TX_NODES idx - pointer to the head tx node in the linked list + pub head_node: u64, + /// length of the tx node linked list for this element + pub list_len: u16, + /// offset of the first tx of this bundle in the history of txs for the account (for pagination) + pub offset: u32, +} + +#[inline] +fn constant_time_is_not_zero(value: i32) -> u32 { + (((value | -value) >> 31) & 1) as u32 +} + +#[inline] +fn constant_time_if_else(condition: u32, then: usize, els: usize) -> usize { + (then * condition as usize) | (els * (1 - condition as usize)) +} + +#[cfg(feature = "gas_tracking")] +pub fn log_dwb(storage: &dyn Storage) -> StdResult<Binary> { + let dwb = DWB.load(storage)?; + to_binary(&QueryAnswer::Dwb { + dwb: format!("{:?}", dwb), + }) +} + +#[cfg(test)] +mod tests { + use crate::contract::instantiate; + use crate::msg::{InitialBalance, InstantiateMsg}; + use crate::transaction_history::{append_new_stored_tx, StoredTxAction}; + use cosmwasm_std::{testing::*, Binary, OwnedDeps, Response, Uint128}; + + use super::*; + + fn init_helper( + initial_balances: Vec<InitialBalance>, + ) -> ( + StdResult<Response>, + OwnedDeps<MockStorage, MockApi, MockQuerier>, + ) { + let mut deps = mock_dependencies_with_balance(&[]); + let env = mock_env(); + let info = mock_info("instantiator", &[]); + + let init_msg = InstantiateMsg { + name: "sec-sec".to_string(), + admin: Some("admin".to_string()), + symbol: "SECSEC".to_string(), + decimals: 8, + initial_balances: Some(initial_balances), + prng_seed: Binary::from("lolz fun yay".as_bytes()), + config: None, + supported_denoms: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + #[test] + fn test_dwb_entry() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }]); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let env = mock_env(); + let _info = mock_info("bob", &[]); + + let recipient = CanonicalAddr::from(ZERO_ADDR); + let mut dwb_entry = DelayedWriteBufferEntry::new(&recipient).unwrap(); + assert_eq!(dwb_entry, DelayedWriteBufferEntry([0u8; DWB_ENTRY_BYTES])); + + assert_eq!( + dwb_entry.recipient().unwrap(), + CanonicalAddr::from(ZERO_ADDR) + ); + assert_eq!(dwb_entry.amount().unwrap(), 0u64); + assert_eq!(dwb_entry.head_node().unwrap(), 0u64); + assert_eq!(dwb_entry.list_len().unwrap(), 0u16); + + let canonical_addr = CanonicalAddr::from(&[1u8; DWB_RECIPIENT_BYTES]); + dwb_entry.set_recipient(&canonical_addr).unwrap(); + dwb_entry.set_amount(1).unwrap(); + dwb_entry.set_head_node(1).unwrap(); + dwb_entry.set_list_len(1).unwrap(); + + assert_eq!( + dwb_entry.recipient().unwrap(), + CanonicalAddr::from(&[1u8; DWB_RECIPIENT_BYTES]) + ); + assert_eq!(dwb_entry.amount().unwrap(), 1u64); + assert_eq!(dwb_entry.head_node().unwrap(), 1u64); + assert_eq!(dwb_entry.list_len().unwrap(), 1u16); + + // first store the tx information in the global append list of txs and get the new tx id + let storage = deps.as_mut().storage; + let from = CanonicalAddr::from(&[2u8; 20]); + let sender = CanonicalAddr::from(&[2u8; 20]); + let to = CanonicalAddr::from(&[1u8; 20]); + let action = StoredTxAction::transfer(from.clone(), sender.clone(), to.clone()); + let tx_id = append_new_stored_tx( + storage, + &action, + 1000u128, + "uscrt".to_string(), + Some("memo".to_string()), + &env.block, + ) + .unwrap(); + + let result = dwb_entry.add_tx_node(storage, tx_id).unwrap(); + assert_eq!(dwb_entry.head_node().unwrap(), result); + } +} diff --git a/src/gas_tracker.rs b/src/gas_tracker.rs new file mode 100644 index 00000000..8783e630 --- /dev/null +++ b/src/gas_tracker.rs @@ -0,0 +1,90 @@ +use cosmwasm_std::{Api, Response}; + +pub struct GasTracker<'a> { + logs: Vec<(String, String)>, + api: &'a dyn Api, +} + +impl<'a> GasTracker<'a> { + pub fn new(api: &'a dyn Api) -> Self { + Self { + logs: Vec::new(), + api, + } + } + + pub fn group<'b>(&'b mut self, name: &str) -> GasGroup<'a, 'b> { + let mut group = GasGroup::new(self, name.to_string()); + group.mark(); + group + } + + // pub fn from<'b>(&'b mut self, other: GasGroup<'b, 'b>) -> GasGroup<'a, 'b> { + // let mut group = GasGroup::new(self, other.name); + // group.index = other.index; + // group + // } + + // pub fn from<'b>(&'b mut self, name: &str, index: usize) -> GasGroup<'a, 'b> { + // let mut group = GasGroup::new(self, name.to_string()); + // group.index = index; + // group + // } + + pub fn add_to_response(self, resp: Response) -> Response { + let mut new_resp = resp.clone(); + for log in self.logs.into_iter() { + new_resp = new_resp.add_attribute_plaintext(log.0, log.1); + } + new_resp + } +} + +pub trait LoggingExt { + fn add_gas_tracker(&self, tracker: GasTracker) -> Response; +} + +impl LoggingExt for Response { + fn add_gas_tracker(&self, tracker: GasTracker) -> Response { + tracker.add_to_response(self.to_owned()) + } +} + +pub struct GasGroup<'a, 'b> { + pub tracker: &'b mut GasTracker<'a>, + pub name: String, + pub index: usize, +} + +impl<'a, 'b> GasGroup<'a, 'b> { + fn new(tracker: &'b mut GasTracker<'a>, name: String) -> Self { + Self { + tracker, + name, + index: 0, + } + } + + pub fn mark(&mut self) { + self.log(""); + } + + pub fn log(&mut self, comment: &str) { + let gas = self.tracker.api.check_gas(); + let log_entry = ( + format!("gas.{}", self.name,), + format!( + "{}:{}:{}", + self.index, + gas.unwrap_or(0u64).to_string(), + comment + ), + ); + self.tracker.logs.push(log_entry); + self.index += 1; + } + + pub fn logf(&mut self, comment: String) { + self.log(comment.as_str()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 9bafd896..b9928515 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,14 @@ +#[macro_use] +extern crate static_assertions as sa; + mod batch; +mod btbe; pub mod contract; +mod dwb; +mod gas_tracker; pub mod msg; pub mod receiver; pub mod state; +mod strings; mod transaction_history; +mod notifications; \ No newline at end of file diff --git a/src/msg.rs b/src/msg.rs index cc583dd7..09d0be73 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -3,11 +3,11 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::batch; -use crate::batch::HasDecoy; -use crate::transaction_history::{ExtendedTx, Tx}; -use cosmwasm_std::{Addr, Api, Binary, StdError, StdResult, Uint128}; -use secret_toolkit::permit::Permit; +use crate::{batch, transaction_history::Tx}; +use cosmwasm_std::{Addr, Api, Binary, StdError, StdResult, Uint128, Uint64,}; +#[cfg(feature = "gas_evaporation")] +use cosmwasm_std::Uint64; +use secret_toolkit::{notification::ChannelInfoData, permit::Permit}; #[cfg_attr(test, derive(Eq, PartialEq))] #[derive(Serialize, Deserialize, Clone, JsonSchema)] @@ -55,7 +55,7 @@ pub struct InitConfig { /// Indicates whether burn functionality should be enabled /// default: False enable_burn: Option<bool>, - /// Indicated whether an admin can modify supported denoms + /// Indicates whether an admin can modify supported denoms /// default: False can_modify_denoms: Option<bool>, } @@ -93,13 +93,13 @@ pub enum ExecuteMsg { Redeem { amount: Uint128, denom: Option<String>, - decoys: Option<Vec<Addr>>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, Deposit { - decoys: Option<Vec<Addr>>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, @@ -108,8 +108,8 @@ pub enum ExecuteMsg { recipient: String, amount: Uint128, memo: Option<String>, - decoys: Option<Vec<Addr>>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, Send { @@ -118,37 +118,45 @@ pub enum ExecuteMsg { amount: Uint128, msg: Option<Binary>, memo: Option<String>, - decoys: Option<Vec<Addr>>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, BatchTransfer { actions: Vec<batch::TransferAction>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, BatchSend { actions: Vec<batch::SendAction>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, Burn { amount: Uint128, memo: Option<String>, - decoys: Option<Vec<Addr>>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, RegisterReceive { code_hash: String, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, CreateViewingKey { - entropy: String, + entropy: Option<String>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, SetViewingKey { key: String, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, @@ -157,12 +165,16 @@ pub enum ExecuteMsg { spender: String, amount: Uint128, expiration: Option<u64>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, DecreaseAllowance { spender: String, amount: Uint128, expiration: Option<u64>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, TransferFrom { @@ -170,8 +182,8 @@ pub enum ExecuteMsg { recipient: String, amount: Uint128, memo: Option<String>, - decoys: Option<Vec<Addr>>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, SendFrom { @@ -181,31 +193,34 @@ pub enum ExecuteMsg { amount: Uint128, msg: Option<Binary>, memo: Option<String>, - decoys: Option<Vec<Addr>>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, BatchTransferFrom { actions: Vec<batch::TransferFromAction>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, BatchSendFrom { actions: Vec<batch::SendFromAction>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, BurnFrom { owner: String, amount: Uint128, memo: Option<String>, - decoys: Option<Vec<Addr>>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, BatchBurnFrom { actions: Vec<batch::BurnFromAction>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, @@ -214,121 +229,70 @@ pub enum ExecuteMsg { recipient: String, amount: Uint128, memo: Option<String>, - decoys: Option<Vec<Addr>>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, BatchMint { actions: Vec<batch::MintAction>, - entropy: Option<Binary>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, AddMinters { minters: Vec<String>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, RemoveMinters { minters: Vec<String>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, SetMinters { minters: Vec<String>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, // Admin ChangeAdmin { address: String, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, SetContractStatus { level: ContractStatusLevel, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, /// Add deposit/redeem support for these coin denoms - AddSupportedDenoms { denoms: Vec<String> }, + AddSupportedDenoms { + denoms: Vec<String>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, + }, /// Remove deposit/redeem support for these coin denoms - RemoveSupportedDenoms { denoms: Vec<String> }, + RemoveSupportedDenoms { + denoms: Vec<String>, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, + }, // Permit RevokePermit { permit_name: String, + #[cfg(feature = "gas_evaporation")] + gas_target: Option<Uint64>, padding: Option<String>, }, } -pub trait Decoyable { - fn get_minimal_decoys_size(&self) -> usize; - fn get_entropy(self) -> Option<Binary>; -} - -impl Decoyable for ExecuteMsg { - fn get_minimal_decoys_size(&self) -> usize { - match self { - ExecuteMsg::Deposit { decoys, .. } - | ExecuteMsg::Redeem { decoys, .. } - | ExecuteMsg::Transfer { decoys, .. } - | ExecuteMsg::Send { decoys, .. } - | ExecuteMsg::Burn { decoys, .. } - | ExecuteMsg::Mint { decoys, .. } - | ExecuteMsg::TransferFrom { decoys, .. } - | ExecuteMsg::SendFrom { decoys, .. } - | ExecuteMsg::BurnFrom { decoys, .. } => { - if let Some(user_decoys) = decoys { - return user_decoys.len(); - } - - 0 - } - ExecuteMsg::BatchSendFrom { actions, .. } => get_min_decoys_count(actions), - ExecuteMsg::BatchTransferFrom { actions, .. } => get_min_decoys_count(actions), - ExecuteMsg::BatchTransfer { actions, .. } => get_min_decoys_count(actions), - ExecuteMsg::BatchSend { actions, .. } => get_min_decoys_count(actions), - ExecuteMsg::BatchBurnFrom { actions, .. } => get_min_decoys_count(actions), - ExecuteMsg::BatchMint { actions, .. } => get_min_decoys_count(actions), - _ => 0, - } - } - - fn get_entropy(self) -> Option<Binary> { - match self { - ExecuteMsg::Deposit { entropy, .. } - | ExecuteMsg::Redeem { entropy, .. } - | ExecuteMsg::Transfer { entropy, .. } - | ExecuteMsg::Send { entropy, .. } - | ExecuteMsg::Burn { entropy, .. } - | ExecuteMsg::Mint { entropy, .. } - | ExecuteMsg::TransferFrom { entropy, .. } - | ExecuteMsg::SendFrom { entropy, .. } - | ExecuteMsg::BurnFrom { entropy, .. } - | ExecuteMsg::BatchTransferFrom { entropy, .. } - | ExecuteMsg::BatchSendFrom { entropy, .. } - | ExecuteMsg::BatchTransfer { entropy, .. } - | ExecuteMsg::BatchSend { entropy, .. } - | ExecuteMsg::BatchBurnFrom { entropy, .. } - | ExecuteMsg::BatchMint { entropy, .. } => entropy, - _ => None, - } - } -} - -fn get_min_decoys_count<T: HasDecoy>(actions: &[T]) -> usize { - let mut min_decoys_count = usize::MAX; - for action in actions { - if let Some(user_decoys) = &action.decoys() { - if user_decoys.len() < min_decoys_count { - min_decoys_count = user_decoys.len(); - } - } - } - - if min_decoys_count == usize::MAX { - 0 - } else { - min_decoys_count - } -} - #[derive(Serialize, Deserialize, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum ExecuteAnswer { @@ -433,6 +397,58 @@ pub enum ExecuteAnswer { }, } +#[cfg(feature = "gas_evaporation")] +pub trait Evaporator { + fn evaporate_to_target(&self, api: &dyn Api) -> StdResult<u64>; +} + +#[cfg(feature = "gas_evaporation")] +impl Evaporator for ExecuteMsg { + fn evaporate_to_target(&self, api: &dyn Api) -> StdResult<u64> { + match self { + ExecuteMsg::Redeem { gas_target, .. } + | ExecuteMsg::Deposit { gas_target, .. } + | ExecuteMsg::Transfer { gas_target, .. } + | ExecuteMsg::Send { gas_target, .. } + | ExecuteMsg::BatchTransfer { gas_target, .. } + | ExecuteMsg::BatchSend { gas_target, .. } + | ExecuteMsg::Burn { gas_target, .. } + | ExecuteMsg::RegisterReceive { gas_target, .. } + | ExecuteMsg::CreateViewingKey { gas_target, .. } + | ExecuteMsg::SetViewingKey { gas_target, .. } + | ExecuteMsg::IncreaseAllowance { gas_target, .. } + | ExecuteMsg::DecreaseAllowance { gas_target, .. } + | ExecuteMsg::TransferFrom { gas_target, .. } + | ExecuteMsg::SendFrom { gas_target, .. } + | ExecuteMsg::BatchTransferFrom { gas_target, .. } + | ExecuteMsg::BatchSendFrom { gas_target, .. } + | ExecuteMsg::BurnFrom { gas_target, .. } + | ExecuteMsg::BatchBurnFrom { gas_target, .. } + | ExecuteMsg::Mint { gas_target, .. } + | ExecuteMsg::BatchMint { gas_target, .. } + | ExecuteMsg::AddMinters { gas_target, .. } + | ExecuteMsg::RemoveMinters { gas_target, .. } + | ExecuteMsg::SetMinters { gas_target, .. } + | ExecuteMsg::ChangeAdmin { gas_target, .. } + | ExecuteMsg::SetContractStatus { gas_target, .. } + | ExecuteMsg::AddSupportedDenoms { gas_target, .. } + | ExecuteMsg::RemoveSupportedDenoms { gas_target, .. } + | ExecuteMsg::RevokePermit { gas_target, .. } => match gas_target { + Some(gas_target) => { + let gas_used = api.check_gas()?; + if gas_used < gas_target.u64() { + let evaporate_amount = gas_target.u64() - gas_used; + // api.gas_evaporate(evaporate_amount as u32)?; + return Ok(evaporate_amount) + } + Ok(0) + } + None => Ok(0), + }, + } + } +} + #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] #[cfg_attr(test, derive(Eq, PartialEq))] #[serde(rename_all = "snake_case")] @@ -467,20 +483,43 @@ pub enum QueryMsg { key: String, page: Option<u32>, page_size: u32, - should_filter_decoys: bool, }, TransactionHistory { address: String, key: String, page: Option<u32>, page_size: u32, - should_filter_decoys: bool, }, Minters {}, + + // SNIP-52 Private Push Notifications + /// Public query to list all notification channels + ListChannels {}, + /// Authenticated query allows clients to obtain the seed + /// and schema for a specific channel. + ChannelInfo { + channels: Vec<String>, + txhash: Option<String>, + viewer: ViewerInfo, + }, + WithPermit { permit: Permit, query: QueryWithPermit, }, + + // for debug purposes only + #[cfg(feature = "gas_tracking")] + Dwb {}, +} + +/// the address and viewing key making an authenticated query request +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct ViewerInfo { + /// querying address + pub address: String, + /// authentication key string + pub viewing_key: String, } impl QueryMsg { @@ -517,6 +556,10 @@ impl QueryMsg { let spender = api.addr_validate(spender.as_str())?; Ok((vec![spender], key.clone())) } + Self::ChannelInfo { viewer, .. } => { + let address = api.addr_validate(viewer.address.as_str())?; + Ok((vec![address], viewer.viewing_key.clone())) + } _ => panic!("This query type does not require authentication"), } } @@ -544,12 +587,15 @@ pub enum QueryWithPermit { TransferHistory { page: Option<u32>, page_size: u32, - should_filter_decoys: bool, }, TransactionHistory { page: Option<u32>, page_size: u32, - should_filter_decoys: bool, + }, + // SNIP-52 Private Push Notifications + ChannelInfo { + channels: Vec<String>, + txhash: Option<String>, }, } @@ -596,12 +642,8 @@ pub enum QueryAnswer { Balance { amount: Uint128, }, - TransferHistory { - txs: Vec<Tx>, - total: Option<u64>, - }, TransactionHistory { - txs: Vec<ExtendedTx>, + txs: Vec<Tx>, total: Option<u64>, }, ViewingKeyError { @@ -610,6 +652,23 @@ pub enum QueryAnswer { Minters { minters: Vec<Addr>, }, + + // SNIP-52 Private Push Notifications + ListChannels { + channels: Vec<String>, + }, + ChannelInfo { + /// scopes validity of this response + as_of_block: Uint64, + /// shared secret in base64 + seed: Binary, + channels: Vec<ChannelInfoData>, + }, + + #[cfg(feature = "gas_tracking")] + Dwb { + dwb: String, + }, } #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] diff --git a/src/notifications.rs b/src/notifications.rs new file mode 100644 index 00000000..38e76f74 --- /dev/null +++ b/src/notifications.rs @@ -0,0 +1,402 @@ +use std::collections::HashMap; + +use cosmwasm_std::{Addr, Api, Binary, CanonicalAddr, StdError, StdResult}; +use primitive_types::{U256, U512}; +use secret_toolkit::notification::{get_seed, notification_id, xor_bytes, Notification, NotificationData}; +use minicbor_ser as cbor; +use secret_toolkit_crypto::{hkdf_sha_512, sha_256}; +use serde::{Deserialize, Serialize}; + +const ZERO_ADDR: [u8; 20] = [0u8; 20]; + +// recvd = [ +// amount: biguint, ; transfer amount in base denomination +// sender: bstr, ; byte sequence of sender's canonical address +// balance: biguint ; recipient's new balance after the transfer +// ] + +#[derive(Serialize, Debug, Deserialize, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct ReceivedNotificationData { + pub amount: u128, + pub sender: Option<Addr>, +} + +impl NotificationData for ReceivedNotificationData { + const CHANNEL_ID: &'static str = "recvd"; + const CDDL_SCHEMA: &'static str = "recvd=[amount:biguint,sender:bstr]"; + + fn to_cbor(&self, api: &dyn Api) -> StdResult<Vec<u8>> { + let received_data; + if let Some(sender) = &self.sender { + let sender_raw = api.addr_canonicalize(sender.as_str())?; + received_data = cbor::to_vec(&(self.amount.to_be_bytes(), sender_raw.as_slice())) + .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + } else { + received_data = cbor::to_vec(&(self.amount.to_be_bytes(), ZERO_ADDR)) + .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + } + Ok(received_data) + } +} + +// spent = [ +// amount: biguint, ; transfer amount in base denomination +// actions: uint ; number of actions the execution performed +// recipient: bstr, ; byte sequence of first recipient's canonical address +// balance: biguint ; sender's new balance aactions: uint ; number of actions the execution performedfter the transfer +// ] + +#[derive(Serialize, Debug, Deserialize, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct SpentNotificationData { + pub amount: u128, + pub actions: u32, + pub recipient: Option<Addr>, + pub balance: u128, +} + +impl NotificationData for SpentNotificationData { + const CHANNEL_ID: &'static str = "spent"; + const CDDL_SCHEMA: &'static str = "spent=[amount:biguint,actions:uint,recipient:bstr,balance:biguint]"; + fn to_cbor(&self, api: &dyn Api) -> StdResult<Vec<u8>> { + let spent_data; + if let Some(recipient) = &self.recipient { + let recipient_raw = api.addr_canonicalize(recipient.as_str())?; + spent_data = cbor::to_vec(&( + self.amount.to_be_bytes(), + self.actions.to_be_bytes(), + recipient_raw.as_slice(), + self.balance.to_be_bytes(), + )) + .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + } else { + spent_data = cbor::to_vec(&( + self.amount.to_be_bytes(), + self.actions.to_be_bytes(), + ZERO_ADDR, + self.balance.to_be_bytes(), + )) + .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + } + Ok(spent_data) + } +} + +//allowance = [ +// amount: biguint, ; allowance amount in base denomination +// allower: bstr, ; byte sequence of allower's canonical address +// expiration: uint, ; epoch seconds of allowance expiration +//] + +#[derive(Serialize, Debug, Deserialize, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct AllowanceNotificationData { + pub amount: u128, + pub allower: Addr, + pub expiration: Option<u64>, +} + +impl NotificationData for AllowanceNotificationData { + const CHANNEL_ID: &'static str = "allowance"; + const CDDL_SCHEMA: &'static str = "allowance=[amount:biguint,allower:bstr,expiration:uint]"; + fn to_cbor(&self, api: &dyn Api) -> StdResult<Vec<u8>> { + let allower_raw = api.addr_canonicalize(self.allower.as_str())?; + + // use CBOR to encode data + let updated_allowance_data = cbor::to_vec(&( + self.amount.to_be_bytes(), + allower_raw.as_slice(), + self.expiration.unwrap_or(0u64), // expiration == 0 means no expiration + )) + .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + Ok(updated_allowance_data) + } +} + +// multi recipient push notifications + +// id for the `multirecvd` channel +pub const MULTI_RECEIVED_CHANNEL_ID: &str = "multirecvd"; +pub const MULTI_RECEIVED_CHANNEL_BLOOM_K: u32 = 15; +pub const MULTI_RECEIVED_CHANNEL_BLOOM_N: u32 = 16; +pub const MULTI_RECEIVED_CHANNEL_PACKET_SIZE: u32 = 24; + +// id for the `multispent` channel +pub const MULTI_SPENT_CHANNEL_ID: &str = "multispent"; +pub const MULTI_SPENT_CHANNEL_BLOOM_K: u32 = 5; +pub const MULTI_SPENT_CHANNEL_BLOOM_N: u32 = 4; +pub const MULTI_SPENT_CHANNEL_PACKET_SIZE: u32 = 40; + +pub fn multi_received_data( + api: &dyn Api, + notifications: Vec<Notification<ReceivedNotificationData>>, + tx_hash: &String, + env_random: Binary, + secret: &[u8], +) -> StdResult<Vec<u8>> { + let mut received_bloom_filter: U512 = U512::from(0); + let mut received_packets: Vec<(Addr, Vec<u8>)> = vec![]; + + // keep track of how often addresses might show up in packet data. + // we need to remove any address that might show up more than once. + let mut recipient_counts: HashMap<Addr, u16> = HashMap::new(); + + for notification in ¬ifications { + recipient_counts.insert( + notification.notification_for.clone(), + recipient_counts + .get(¬ification.notification_for) + .unwrap_or(&0u16) + + 1, + ); + + // we can short circuit this if recipient count > 1, since we will throw out this packet + // anyway, and address has already been added to bloom filter + if *recipient_counts + .get(¬ification.notification_for) + .unwrap() + > 1 + { + continue; + } + + // contribute to received bloom filter + let recipient_addr_raw = api.addr_canonicalize(notification.notification_for.as_str())?; + let seed = get_seed(&recipient_addr_raw, secret)?; + let id = notification_id(&seed, &MULTI_RECEIVED_CHANNEL_ID.to_string(), &tx_hash)?; + let mut hash_bytes = U256::from_big_endian(&sha_256(id.0.as_slice())); + for _ in 0..MULTI_RECEIVED_CHANNEL_BLOOM_K { + let bit_index = (hash_bytes & U256::from(0x01ff)).as_usize(); + received_bloom_filter = received_bloom_filter | (U512::from(1) << bit_index); + hash_bytes = hash_bytes >> 9; + } + + // make the received packet + let mut received_packet_plaintext: Vec<u8> = vec![]; + // amount bytes (u128 == 16 bytes) + received_packet_plaintext.extend_from_slice(¬ification.data.amount.to_be_bytes()); + // sender account last 8 bytes + let sender_bytes: &[u8]; + let sender_raw; + if let Some(sender) = ¬ification.data.sender { + sender_raw = api.addr_canonicalize(sender.as_str())?; + sender_bytes = &sender_raw.as_slice()[sender_raw.0.len() - 8..]; + } else { + sender_bytes = &ZERO_ADDR[ZERO_ADDR.len() - 8..]; + } + // 24 bytes total + received_packet_plaintext.extend_from_slice(sender_bytes); + + let received_packet_id = &id.0.as_slice()[0..8]; + let received_packet_ikm = &id.0.as_slice()[8..32]; + let received_packet_ciphertext = + xor_bytes(received_packet_plaintext.as_slice(), received_packet_ikm); + let received_packet_bytes: Vec<u8> = + [received_packet_id.to_vec(), received_packet_ciphertext].concat(); + + received_packets.push((notification.notification_for.clone(), received_packet_bytes)); + } + + // filter out any notifications for recipients showing up more than once + let mut received_packets: Vec<Vec<u8>> = received_packets + .into_iter() + .filter(|(addr, _)| *recipient_counts.get(addr).unwrap_or(&0u16) <= 1) + .map(|(_, packet)| packet) + .collect(); + if received_packets.len() > MULTI_RECEIVED_CHANNEL_BLOOM_N as usize { + // still too many packets + received_packets = received_packets[0..MULTI_RECEIVED_CHANNEL_BLOOM_N as usize].to_vec(); + } + + // now add extra packets, if needed, to hide number of packets + let padding_size = + MULTI_RECEIVED_CHANNEL_BLOOM_N.saturating_sub(received_packets.len() as u32) as usize; + if padding_size > 0 { + let padding_addresses = hkdf_sha_512( + &Some(vec![0u8; 64]), + &env_random, + format!("{}:decoys", MULTI_RECEIVED_CHANNEL_ID).as_bytes(), + padding_size * 20, // 20 bytes per random addr + )?; + + // handle each padding package + for i in 0..padding_size { + let padding_address = &padding_addresses[i * 20..(i + 1) * 20]; + + // contribute padding packet to bloom filter + let seed = get_seed(&CanonicalAddr::from(padding_address), secret)?; + let id = notification_id(&seed, &MULTI_RECEIVED_CHANNEL_ID.to_string(), &tx_hash)?; + let mut hash_bytes = U256::from_big_endian(&sha_256(id.0.as_slice())); + for _ in 0..MULTI_RECEIVED_CHANNEL_BLOOM_K { + let bit_index = (hash_bytes & U256::from(0x01ff)).as_usize(); + received_bloom_filter = received_bloom_filter | (U512::from(1) << bit_index); + hash_bytes = hash_bytes >> 9; + } + + // padding packet plaintext + let padding_packet_plaintext = [0u8; MULTI_RECEIVED_CHANNEL_PACKET_SIZE as usize]; + let padding_packet_id = &id.0.as_slice()[0..8]; + let padding_packet_ikm = &id.0.as_slice()[8..32]; + let padding_packet_ciphertext = + xor_bytes(padding_packet_plaintext.as_slice(), padding_packet_ikm); + let padding_packet_bytes: Vec<u8> = + [padding_packet_id.to_vec(), padding_packet_ciphertext].concat(); + received_packets.push(padding_packet_bytes); + } + } + + let mut received_bloom_filter_bytes: Vec<u8> = vec![]; + for biguint in received_bloom_filter.0 { + received_bloom_filter_bytes.extend_from_slice(&biguint.to_be_bytes()); + } + for packet in received_packets { + received_bloom_filter_bytes.extend(packet.iter()); + } + + Ok(received_bloom_filter_bytes) +} + +pub fn multi_spent_data( + api: &dyn Api, + notifications: Vec<Notification<SpentNotificationData>>, + tx_hash: &String, + env_random: Binary, + secret: &[u8], +) -> StdResult<Vec<u8>> { + let mut spent_bloom_filter: U512 = U512::from(0); + let mut spent_packets: Vec<(Addr, Vec<u8>)> = vec![]; + + // keep track of how often addresses might show up in packet data. + // we need to remove any address that might show up more than once. + let mut spent_counts: HashMap<Addr, u16> = HashMap::new(); + + for notification in ¬ifications { + spent_counts.insert( + notification.notification_for.clone(), + spent_counts + .get(¬ification.notification_for) + .unwrap_or(&0u16) + + 1, + ); + + // we can short circuit this if recipient count > 1, since we will throw out this packet + // anyway, and address has already been added to bloom filter + if *spent_counts.get(¬ification.notification_for).unwrap() > 1 { + continue; + } + + let spender_addr_raw = api.addr_canonicalize(notification.notification_for.as_str())?; + let seed = get_seed(&spender_addr_raw, secret)?; + let id = notification_id(&seed, &MULTI_SPENT_CHANNEL_ID.to_string(), &tx_hash)?; + let mut hash_bytes = U256::from_big_endian(&sha_256(id.0.as_slice())); + for _ in 0..MULTI_SPENT_CHANNEL_BLOOM_K { + let bit_index = (hash_bytes & U256::from(0x01ff)).as_usize(); + spent_bloom_filter = spent_bloom_filter | (U512::from(1) << bit_index); + hash_bytes = hash_bytes >> 9; + } + + // make the spent packet + let mut spent_packet_plaintext: Vec<u8> = vec![]; + // amount bytes (u128 == 16 bytes) + spent_packet_plaintext.extend_from_slice(¬ification.data.amount.to_be_bytes()); + // balance bytes (u128 == 16 bytes) + spent_packet_plaintext.extend_from_slice(¬ification.data.balance.to_be_bytes()); + // recipient account last 8 bytes + let recipient_bytes: &[u8]; + let recipient_raw; + if let Some(recipient) = ¬ification.data.recipient { + recipient_raw = api.addr_canonicalize(recipient.as_str())?; + recipient_bytes = &recipient_raw.as_slice()[recipient_raw.0.len() - 8..]; + } else { + recipient_bytes = &ZERO_ADDR[ZERO_ADDR.len() - 8..]; + } + // 40 bytes total + spent_packet_plaintext.extend_from_slice(recipient_bytes); + + let spent_packet_size = spent_packet_plaintext.len(); + let spent_packet_id = &id.0.as_slice()[0..8]; + let spent_packet_ikm = &id.0.as_slice()[8..32]; + let spent_packet_key = hkdf_sha_512( + &Some(vec![0u8; 64]), + spent_packet_ikm, + "".as_bytes(), + spent_packet_size, + )?; + let spent_packet_ciphertext = xor_bytes( + spent_packet_plaintext.as_slice(), + spent_packet_key.as_slice(), + ); + let spent_packet_bytes: Vec<u8> = + [spent_packet_id.to_vec(), spent_packet_ciphertext].concat(); + + spent_packets.push((notification.notification_for.clone(), spent_packet_bytes)); + } + + // filter out any notifications for senders showing up more than once + let mut spent_packets: Vec<Vec<u8>> = spent_packets + .into_iter() + .filter(|(addr, _)| *spent_counts.get(addr).unwrap_or(&0u16) <= 1) + .map(|(_, packet)| packet) + .collect(); + if spent_packets.len() > MULTI_SPENT_CHANNEL_BLOOM_N as usize { + // still too many packets + spent_packets = spent_packets[0..MULTI_SPENT_CHANNEL_BLOOM_N as usize].to_vec(); + } + + // now add extra packets, if needed, to hide number of packets + let padding_size = + MULTI_SPENT_CHANNEL_BLOOM_N.saturating_sub(spent_packets.len() as u32) as usize; + if padding_size > 0 { + let padding_addresses = hkdf_sha_512( + &Some(vec![0u8; 64]), + &env_random, + format!("{}:decoys", MULTI_SPENT_CHANNEL_ID).as_bytes(), + padding_size * 20, // 20 bytes per random addr + )?; + + // handle each padding package + for i in 0..padding_size { + let padding_address = &padding_addresses[i * 20..(i + 1) * 20]; + + // contribute padding packet to bloom filter + let seed = get_seed(&CanonicalAddr::from(padding_address), secret)?; + let id = notification_id(&seed, &MULTI_SPENT_CHANNEL_ID.to_string(), &tx_hash)?; + let mut hash_bytes = U256::from_big_endian(&sha_256(id.0.as_slice())); + for _ in 0..MULTI_SPENT_CHANNEL_BLOOM_K { + let bit_index = (hash_bytes & U256::from(0x01ff)).as_usize(); + spent_bloom_filter = spent_bloom_filter | (U512::from(1) << bit_index); + hash_bytes = hash_bytes >> 9; + } + + // padding packet plaintext + let padding_packet_plaintext = [0u8; MULTI_SPENT_CHANNEL_PACKET_SIZE as usize]; + let padding_plaintext_size = MULTI_SPENT_CHANNEL_PACKET_SIZE as usize; + let padding_packet_id = &id.0.as_slice()[0..8]; + let padding_packet_ikm = &id.0.as_slice()[8..32]; + let padding_packet_key = hkdf_sha_512( + &Some(vec![0u8; 64]), + padding_packet_ikm, + "".as_bytes(), + padding_plaintext_size, + )?; + let padding_packet_ciphertext = xor_bytes( + padding_packet_plaintext.as_slice(), + padding_packet_key.as_slice(), + ); + let padding_packet_bytes: Vec<u8> = + [padding_packet_id.to_vec(), padding_packet_ciphertext].concat(); + spent_packets.push(padding_packet_bytes); + } + } + + let mut spent_bloom_filter_bytes: Vec<u8> = vec![]; + for biguint in spent_bloom_filter.0 { + spent_bloom_filter_bytes.extend_from_slice(&biguint.to_be_bytes()); + } + for packet in spent_packets { + spent_bloom_filter_bytes.extend(packet.iter()); + } + + Ok(spent_bloom_filter_bytes) +} \ No newline at end of file diff --git a/src/state.rs b/src/state.rs index ece815a1..3c44b4fb 100644 --- a/src/state.rs +++ b/src/state.rs @@ -4,18 +4,15 @@ use serde::{Deserialize, Serialize}; use cosmwasm_std::{Addr, StdError, StdResult, Storage}; use secret_toolkit::serialization::Json; use secret_toolkit::storage::{Item, Keymap, Keyset}; -use secret_toolkit_crypto::SHA256_HASH_SIZE; use crate::msg::ContractStatusLevel; pub const KEY_CONFIG: &[u8] = b"config"; pub const KEY_TOTAL_SUPPLY: &[u8] = b"total_supply"; pub const KEY_CONTRACT_STATUS: &[u8] = b"contract_status"; -pub const KEY_PRNG: &[u8] = b"prng"; pub const KEY_MINTERS: &[u8] = b"minters"; pub const KEY_TX_COUNT: &[u8] = b"tx-count"; -pub const PREFIX_CONFIG: &[u8] = b"config"; pub const PREFIX_BALANCES: &[u8] = b"balances"; pub const PREFIX_ALLOWANCES: &[u8] = b"allowances"; pub const PREFIX_ALLOWED: &[u8] = b"allowed"; @@ -55,23 +52,10 @@ pub static TOTAL_SUPPLY: Item<u128> = Item::new(KEY_TOTAL_SUPPLY); pub static CONTRACT_STATUS: Item<ContractStatusLevel, Json> = Item::new(KEY_CONTRACT_STATUS); -pub static PRNG: Item<[u8; SHA256_HASH_SIZE]> = Item::new(KEY_PRNG); - pub static MINTERS: Item<Vec<Addr>> = Item::new(KEY_MINTERS); pub static TX_COUNT: Item<u64> = Item::new(KEY_TX_COUNT); -pub struct PrngStore {} -impl PrngStore { - pub fn load(store: &dyn Storage) -> StdResult<[u8; SHA256_HASH_SIZE]> { - PRNG.load(store).map_err(|_err| StdError::generic_err("")) - } - - pub fn save(store: &mut dyn Storage, prng_seed: [u8; SHA256_HASH_SIZE]) -> StdResult<()> { - PRNG.save(store, &prng_seed) - } -} - pub struct MintersStore {} impl MintersStore { pub fn load(store: &dyn Storage) -> StdResult<Vec<Addr>> { @@ -109,7 +93,7 @@ impl MintersStore { // To avoid balance guessing attacks based on balance overflow we need to perform safe addition and don't expose overflows to the caller. // Assuming that max of u128 is probably an unreachable balance, we want the addition to be bounded the max of u128 -// Currently the logic here is very straight forward yet the existence of the function is mendatory for future changes if needed. +// Currently the logic here is very straight forward yet the existence of the function is mandatory for future changes if needed. pub fn safe_add(balance: &mut u128, amount: u128) -> u128 { // Note that new_amount can be equal to base after this operation. // Currently we do nothing maybe on other implementations we will have something to add here @@ -120,95 +104,17 @@ pub fn safe_add(balance: &mut u128, amount: u128) -> u128 { *balance - prev_balance } -pub static BALANCES: Item<u128> = Item::new(PREFIX_BALANCES); -pub struct BalancesStore {} -impl BalancesStore { - fn save(store: &mut dyn Storage, account: &Addr, amount: u128) -> StdResult<()> { - let balances = BALANCES.add_suffix(account.as_str().as_bytes()); - balances.save(store, &amount) - } - - pub fn load(store: &dyn Storage, account: &Addr) -> u128 { - let balances = BALANCES.add_suffix(account.as_str().as_bytes()); - balances.load(store).unwrap_or_default() - } +// To avoid balance guessing attacks based on balance overflow we need to perform safe addition and don't expose overflows to the caller. +// Assuming that max of u64 is probably an unreachable balance, we want the addition to be bounded the max of u64 +// Currently the logic here is very straight forward yet the existence of the function is mandatory for future changes if needed. +pub fn safe_add_u64(balance: &mut u64, amount: u64) -> u64 { + // Note that new_amount can be equal to base after this operation. + // Currently we do nothing maybe on other implementations we will have something to add here + let prev_balance: u64 = *balance; + *balance = balance.saturating_add(amount); - pub fn update_balance( - store: &mut dyn Storage, - account: &Addr, - amount_to_be_updated: u128, - should_add: bool, - operation_name: &str, - decoys: &Option<Vec<Addr>>, - account_random_pos: &Option<usize>, - ) -> StdResult<()> { - match decoys { - None => { - let mut balance = Self::load(store, account); - balance = match should_add { - true => { - safe_add(&mut balance, amount_to_be_updated); - balance - } - false => { - if let Some(balance) = balance.checked_sub(amount_to_be_updated) { - balance - } else { - return Err(StdError::generic_err(format!( - "insufficient funds to {operation_name}: balance={balance}, required={amount_to_be_updated}", - ))); - } - } - }; - - Self::save(store, account, balance) - } - Some(decoys_vec) => { - // It should always be set when decoys_vec is set - let account_pos = account_random_pos.unwrap(); - - let mut accounts_to_be_written: Vec<&Addr> = vec![]; - - let (first_part, second_part) = decoys_vec.split_at(account_pos); - accounts_to_be_written.extend(first_part); - accounts_to_be_written.push(account); - accounts_to_be_written.extend(second_part); - - // In a case where the account is also a decoy somehow - let mut was_account_updated = false; - - for acc in accounts_to_be_written.iter() { - // Always load account balance to obfuscate the real account - // Please note that decoys are not always present in the DB. In this case it is ok beacuse load will return 0. - let mut acc_balance = Self::load(store, acc); - let mut new_balance = acc_balance; - - if *acc == account && !was_account_updated { - was_account_updated = true; - new_balance = match should_add { - true => { - safe_add(&mut acc_balance, amount_to_be_updated); - acc_balance - } - false => { - if let Some(balance) = acc_balance.checked_sub(amount_to_be_updated) - { - balance - } else { - return Err(StdError::generic_err(format!( - "insufficient funds to {operation_name}: balance={acc_balance}, required={amount_to_be_updated}", - ))); - } - } - }; - } - Self::save(store, acc, new_balance)?; - } - - Ok(()) - } - } - } + // Won't underflow as the minimal value possible is 0 + *balance - prev_balance } // Allowances @@ -315,3 +221,9 @@ impl ReceiverHashStore { receiver_hash.save(store, &code_hash) } } + +pub static INTERNAL_SECRET: Item<Vec<u8>> = Item::new(b"internal-secret"); + +// SNIP-52 channels +pub static CHANNELS: Keyset<String> = Keyset::new(b"channel-ids"); + diff --git a/src/strings.rs b/src/strings.rs new file mode 100644 index 00000000..71a574be --- /dev/null +++ b/src/strings.rs @@ -0,0 +1,2 @@ +pub const TRANSFER_HISTORY_UNSUPPORTED_MSG: &str = + "`transfer_history` query is UNSUPPORTED. Use `transaction_history` instead."; diff --git a/src/transaction_history.rs b/src/transaction_history.rs index 0b3c3270..8f7c72cd 100644 --- a/src/transaction_history.rs +++ b/src/transaction_history.rs @@ -1,33 +1,15 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use cosmwasm_std::{Addr, Coin, StdError, StdResult, Storage, Uint128}; +use cosmwasm_std::{ + Addr, Api, BlockInfo, CanonicalAddr, Coin, StdError, StdResult, Storage, Uint128, +}; -use secret_toolkit::storage::AppendStore; +use secret_toolkit::storage::Item; use crate::state::TX_COUNT; const PREFIX_TXS: &[u8] = b"transactions"; -const PREFIX_TRANSFERS: &[u8] = b"transfers"; - -// Note that id is a globally incrementing counter. -// Since it's 64 bits long, even at 50 tx/s it would take -// over 11 billion years for it to rollback. I'm pretty sure -// we'll have bigger issues by then. -#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] -pub struct Tx { - pub id: u64, - pub from: Addr, - pub sender: Addr, - pub receiver: Addr, - pub coins: Coin, - #[serde(skip_serializing_if = "Option::is_none")] - pub memo: Option<String>, - // The block time and block height are optional so that the JSON schema - // reflects that some SNIP-20 contracts may not include this info. - pub block_time: Option<u64>, - pub block_height: Option<u64>, -} #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)] #[serde(rename_all = "snake_case")] @@ -47,23 +29,19 @@ pub enum TxAction { }, Deposit {}, Redeem {}, - Decoy { - address: Addr, - }, } // Note that id is a globally incrementing counter. -// Since it's 64 bits long, even at 50 tx/s it would take -// over 11 billion years for it to rollback. I'm pretty sure -// we'll have bigger issues by then. #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, PartialEq)] #[serde(rename_all = "snake_case")] -pub struct ExtendedTx { +pub struct Tx { pub id: u64, pub action: TxAction, pub coins: Coin, #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option<String>, + // The block time and block height are optional so that the JSON schema + // reflects that some SNIP-20 contracts may not include this info. pub block_time: u64, pub block_height: u64, } @@ -94,81 +72,6 @@ impl From<StoredCoin> for Coin { } } -/// This type is the stored version of the legacy transfers -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "snake_case")] -pub struct StoredLegacyTransfer { - id: u64, - from: Addr, - sender: Addr, - receiver: Addr, - coins: StoredCoin, - memo: Option<String>, - block_time: u64, - block_height: u64, -} -static TRANSFERS: AppendStore<StoredLegacyTransfer> = AppendStore::new(PREFIX_TRANSFERS); - -impl StoredLegacyTransfer { - pub fn into_humanized(self) -> StdResult<Tx> { - let tx = Tx { - id: self.id, - from: self.from, - sender: self.sender, - receiver: self.receiver, - coins: self.coins.into(), - memo: self.memo, - block_time: Some(self.block_time), - block_height: Some(self.block_height), - }; - Ok(tx) - } - - fn append_transfer( - store: &mut dyn Storage, - tx: &StoredLegacyTransfer, - for_address: &Addr, - ) -> StdResult<()> { - let current_addr_store = TRANSFERS.add_suffix(for_address.as_bytes()); - current_addr_store.push(store, tx) - } - - pub fn get_transfers( - storage: &dyn Storage, - for_address: Addr, - page: u32, - page_size: u32, - should_filter_decoys: bool, - ) -> StdResult<(Vec<Tx>, u64)> { - let current_addr_store = TRANSFERS.add_suffix(for_address.as_bytes()); - let len = current_addr_store.get_len(storage)? as u64; - // Take `page_size` txs starting from the latest tx, potentially skipping `page * page_size` - // txs from the start. - let transfer_iter = current_addr_store - .iter(storage)? - .rev() - .skip((page * page_size) as _) - .take(page_size as _); - - // The `and_then` here flattens the `StdResult<StdResult<ExtendedTx>>` to an `StdResult<ExtendedTx>` - let transfers: StdResult<Vec<Tx>> = if should_filter_decoys { - transfer_iter - .filter(|transfer| match transfer { - Err(_) => true, - Ok(t) => t.block_height != 0, - }) - .map(|tx| tx.map(|tx| tx.into_humanized()).and_then(|x| x)) - .collect() - } else { - transfer_iter - .map(|tx| tx.map(|tx| tx.into_humanized()).and_then(|x| x)) - .collect() - }; - - transfers.map(|txs| (txs, len)) - } -} - #[derive(Clone, Copy, Debug)] #[repr(u8)] enum TxCode { @@ -177,7 +80,6 @@ enum TxCode { Burn = 2, Deposit = 3, Redeem = 4, - Decoy = 255, } impl TxCode { @@ -193,9 +95,9 @@ impl TxCode { 2 => Ok(Burn), 3 => Ok(Deposit), 4 => Ok(Redeem), - 255 => Ok(Decoy), other => Err(StdError::generic_err(format!( - "Unexpected Tx code in transaction history: {other} Storage is corrupted.", + "Unexpected Tx code in transaction history: {} Storage is corrupted.", + other ))), } } @@ -203,15 +105,15 @@ impl TxCode { #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "snake_case")] -struct StoredTxAction { +pub struct StoredTxAction { tx_type: u8, - address1: Option<Addr>, - address2: Option<Addr>, - address3: Option<Addr>, + address1: Option<CanonicalAddr>, + address2: Option<CanonicalAddr>, + address3: Option<CanonicalAddr>, } impl StoredTxAction { - fn transfer(from: Addr, sender: Addr, recipient: Addr) -> Self { + pub fn transfer(from: CanonicalAddr, sender: CanonicalAddr, recipient: CanonicalAddr) -> Self { Self { tx_type: TxCode::Transfer.to_u8(), address1: Some(from), @@ -219,7 +121,7 @@ impl StoredTxAction { address3: Some(recipient), } } - fn mint(minter: Addr, recipient: Addr) -> Self { + pub fn mint(minter: CanonicalAddr, recipient: CanonicalAddr) -> Self { Self { tx_type: TxCode::Mint.to_u8(), address1: Some(minter), @@ -227,7 +129,7 @@ impl StoredTxAction { address3: None, } } - fn burn(owner: Addr, burner: Addr) -> Self { + pub fn burn(owner: CanonicalAddr, burner: CanonicalAddr) -> Self { Self { tx_type: TxCode::Burn.to_u8(), address1: Some(burner), @@ -235,7 +137,7 @@ impl StoredTxAction { address3: None, } } - fn deposit() -> Self { + pub fn deposit() -> Self { Self { tx_type: TxCode::Deposit.to_u8(), address1: None, @@ -243,7 +145,7 @@ impl StoredTxAction { address3: None, } } - fn redeem() -> Self { + pub fn redeem() -> Self { Self { tx_type: TxCode::Redeem.to_u8(), address1: None, @@ -251,16 +153,8 @@ impl StoredTxAction { address3: None, } } - fn decoy(recipient: &Addr) -> Self { - Self { - tx_type: TxCode::Decoy.to_u8(), - address1: Some(recipient.clone()), - address2: None, - address3: None, - } - } - fn into_tx_action(self) -> StdResult<TxAction> { + pub fn into_tx_action(self, api: &dyn Api) -> StdResult<TxAction> { let transfer_addr_err = || { StdError::generic_err( "Missing address in stored Transfer transaction. Storage is corrupt", @@ -272,9 +166,6 @@ impl StoredTxAction { let burn_addr_err = || { StdError::generic_err("Missing address in stored Burn transaction. Storage is corrupt") }; - let decoy_addr_err = || { - StdError::generic_err("Missing address in stored decoy transaction. Storage is corrupt") - }; // In all of these, we ignore fields that we don't expect to find populated let action = match TxCode::from_u8(self.tx_type)? { @@ -283,39 +174,42 @@ impl StoredTxAction { let sender = self.address2.ok_or_else(transfer_addr_err)?; let recipient = self.address3.ok_or_else(transfer_addr_err)?; TxAction::Transfer { - from, - sender, - recipient, + from: api.addr_humanize(&from)?, + sender: api.addr_humanize(&sender)?, + recipient: api.addr_humanize(&recipient)?, } } TxCode::Mint => { let minter = self.address1.ok_or_else(mint_addr_err)?; let recipient = self.address2.ok_or_else(mint_addr_err)?; - TxAction::Mint { minter, recipient } + TxAction::Mint { + minter: api.addr_humanize(&minter)?, + recipient: api.addr_humanize(&recipient)?, + } } TxCode::Burn => { let burner = self.address1.ok_or_else(burn_addr_err)?; let owner = self.address2.ok_or_else(burn_addr_err)?; - TxAction::Burn { burner, owner } + TxAction::Burn { + burner: api.addr_humanize(&burner)?, + owner: api.addr_humanize(&owner)?, + } } TxCode::Deposit => TxAction::Deposit {}, TxCode::Redeem => TxAction::Redeem {}, - TxCode::Decoy => { - let address = self.address1.ok_or_else(decoy_addr_err)?; - TxAction::Decoy { address } - } }; Ok(action) } } -static TRANSACTIONS: AppendStore<StoredExtendedTx> = AppendStore::new(PREFIX_TXS); +// use with add_suffix tx id (u64 to_be_bytes) +// does not need to be an AppendStore because we never need to iterate over global list of txs +pub static TRANSACTIONS: Item<StoredTx> = Item::new(PREFIX_TXS); #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "snake_case")] -pub struct StoredExtendedTx { - id: u64, +pub struct StoredTx { action: StoredTxAction, coins: StoredCoin, memo: Option<String>, @@ -323,309 +217,105 @@ pub struct StoredExtendedTx { block_height: u64, } -impl StoredExtendedTx { - fn new( - id: u64, - action: StoredTxAction, - coins: Coin, - memo: Option<String>, - block: &cosmwasm_std::BlockInfo, - ) -> Self { - Self { +impl StoredTx { + pub fn into_humanized(self, api: &dyn Api, id: u64) -> StdResult<Tx> { + Ok(Tx { id, - action, - coins: coins.into(), - memo, - block_time: block.time.seconds(), - block_height: block.height, - } - } - - fn into_humanized(self) -> StdResult<ExtendedTx> { - Ok(ExtendedTx { - id: self.id, - action: self.action.into_tx_action()?, + action: self.action.into_tx_action(api)?, coins: self.coins.into(), memo: self.memo, block_time: self.block_time, block_height: self.block_height, }) } - - fn from_stored_legacy_transfer(transfer: StoredLegacyTransfer) -> Self { - let action = StoredTxAction::transfer(transfer.from, transfer.sender, transfer.receiver); - Self { - id: transfer.id, - action, - coins: transfer.coins, - memo: transfer.memo, - block_time: transfer.block_time, - block_height: transfer.block_height, - } - } - - fn append_tx( - store: &mut dyn Storage, - tx: &StoredExtendedTx, - for_address: &Addr, - ) -> StdResult<()> { - let current_addr_store = TRANSACTIONS.add_suffix(for_address.as_bytes()); - current_addr_store.push(store, tx) - } - - pub fn get_txs( - storage: &dyn Storage, - for_address: Addr, - page: u32, - page_size: u32, - should_filter_decoys: bool, - ) -> StdResult<(Vec<ExtendedTx>, u64)> { - let current_addr_store = TRANSACTIONS.add_suffix(for_address.as_bytes()); - let len = current_addr_store.get_len(storage)? as u64; - - // Take `page_size` txs starting from the latest tx, potentially skipping `page * page_size` - // txs from the start. - let tx_iter = current_addr_store - .iter(storage)? - .rev() - .skip((page * page_size) as _) - .take(page_size as _); - - // The `and_then` here flattens the `StdResult<StdResult<ExtendedTx>>` to an `StdResult<ExtendedTx>` - let txs: StdResult<Vec<ExtendedTx>> = if should_filter_decoys { - tx_iter - .filter(|tx| match tx { - Err(_) => true, - Ok(t) => t.action.tx_type != TxCode::Decoy.to_u8(), - }) - .map(|tx| tx.map(|tx| tx.into_humanized()).and_then(|x| x)) - .collect() - } else { - tx_iter - .map(|tx| tx.map(|tx| tx.into_humanized()).and_then(|x| x)) - .collect() - }; - - txs.map(|txs| (txs, len)) - } } // Storage functions: -fn increment_tx_count(store: &mut dyn Storage) -> StdResult<u64> { - let id = TX_COUNT.load(store).unwrap_or_default() + 1; - TX_COUNT.save(store, &id)?; - Ok(id) -} - -fn store_tx_with_decoys( - store: &mut dyn Storage, - tx: &StoredExtendedTx, - for_address: &Addr, - block: &cosmwasm_std::BlockInfo, - decoys: &Option<Vec<Addr>>, - account_random_pos: &Option<usize>, -) -> StdResult<()> { - let mut index_changer: Option<usize> = None; - match decoys { - None => StoredExtendedTx::append_tx(store, tx, for_address)?, - Some(user_decoys) => { - // It should always be set when decoys_vec is set - let account_pos = account_random_pos.unwrap(); - - for i in 0..user_decoys.len() + 1 { - if i == account_pos { - StoredExtendedTx::append_tx(store, tx, for_address)?; - index_changer = Some(1); - continue; - } - - let index = i - index_changer.unwrap_or_default(); - let decoy_action = StoredTxAction::decoy(&user_decoys[index]); - let decoy_tx = StoredExtendedTx::new( - tx.id, - decoy_action, - tx.coins.clone().into(), - tx.memo.clone(), - block, - ); - StoredExtendedTx::append_tx(store, &decoy_tx, &user_decoys[index])?; - } - } - } - - Ok(()) -} - -fn store_transfer_tx_with_decoys( +pub fn append_new_stored_tx( store: &mut dyn Storage, - transfer: StoredLegacyTransfer, - receiver: &Addr, - decoys: &Option<Vec<Addr>>, - account_random_pos: &Option<usize>, -) -> StdResult<()> { - let mut index_changer: Option<usize> = None; - match decoys { - None => StoredLegacyTransfer::append_transfer(store, &transfer, receiver)?, - Some(user_decoys) => { - // It should always be set when decoys_vec is set - let account_pos = account_random_pos.unwrap(); - - for i in 0..user_decoys.len() + 1 { - if i == account_pos { - StoredLegacyTransfer::append_transfer(store, &transfer, receiver)?; - index_changer = Some(1); - continue; - } - - let index = i - index_changer.unwrap_or_default(); - let decoy_transfer = StoredLegacyTransfer { - id: transfer.id, - from: transfer.from.clone(), - sender: transfer.sender.clone(), - receiver: user_decoys[index].clone(), - coins: transfer.coins.clone(), - memo: transfer.memo.clone(), - block_time: transfer.block_time, - block_height: 0, // To identify the decoy - }; - StoredLegacyTransfer::append_transfer(store, &decoy_transfer, &user_decoys[index])?; - } - } - } - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] // We just need them -pub fn store_transfer( - store: &mut dyn Storage, - owner: &Addr, - sender: &Addr, - receiver: &Addr, - amount: Uint128, + action: &StoredTxAction, + amount: u128, denom: String, memo: Option<String>, - block: &cosmwasm_std::BlockInfo, - decoys: &Option<Vec<Addr>>, - account_random_pos: &Option<usize>, -) -> StdResult<()> { - let id = increment_tx_count(store)?; - let coins = Coin { denom, amount }; - let transfer = StoredLegacyTransfer { - id, - from: owner.clone(), - sender: sender.clone(), - receiver: receiver.clone(), - coins: coins.into(), + block: &BlockInfo, +) -> StdResult<u64> { + // tx ids are serialized starting at 1 + let serial_id = TX_COUNT.load(store).unwrap_or_default() + 1; + let coins = StoredCoin { denom, amount }; + let stored_tx = StoredTx { + action: action.clone(), + coins, memo, block_time: block.time.seconds(), block_height: block.height, }; - let tx = StoredExtendedTx::from_stored_legacy_transfer(transfer.clone()); - // Write to the owners history if it's different from the other two addresses - if owner != sender && owner != receiver { - // cosmwasm_std::debug_print("saving transaction history for owner"); - StoredExtendedTx::append_tx(store, &tx, owner)?; - StoredLegacyTransfer::append_transfer(store, &transfer, owner)?; - } - // Write to the sender's history if it's different from the receiver - if sender != receiver { - // cosmwasm_std::debug_print("saving transaction history for sender"); - StoredExtendedTx::append_tx(store, &tx, sender)?; - StoredLegacyTransfer::append_transfer(store, &transfer, sender)?; - } - - // Always write to the recipient's history - // cosmwasm_std::debug_print("saving transaction history for receiver"); - store_tx_with_decoys(store, &tx, receiver, block, decoys, account_random_pos)?; - store_transfer_tx_with_decoys(store, transfer, receiver, decoys, account_random_pos)?; - - Ok(()) + TRANSACTIONS + .add_suffix(&serial_id.to_be_bytes()) + .save(store, &stored_tx)?; + TX_COUNT.save(store, &(serial_id))?; + Ok(serial_id) } #[allow(clippy::too_many_arguments)] // We just need them -pub fn store_mint( +pub fn store_transfer_action( store: &mut dyn Storage, - minter: Addr, - recipient: Addr, - amount: Uint128, + owner: &CanonicalAddr, + sender: &CanonicalAddr, + receiver: &CanonicalAddr, + amount: u128, + denom: String, + memo: Option<String>, + block: &BlockInfo, +) -> StdResult<u64> { + let action = StoredTxAction::transfer(owner.clone(), sender.clone(), receiver.clone()); + append_new_stored_tx(store, &action, amount, denom, memo, block) +} + +pub fn store_mint_action( + store: &mut dyn Storage, + minter: &CanonicalAddr, + recipient: &CanonicalAddr, + amount: u128, denom: String, memo: Option<String>, block: &cosmwasm_std::BlockInfo, - decoys: &Option<Vec<Addr>>, - account_random_pos: &Option<usize>, -) -> StdResult<()> { - let id = increment_tx_count(store)?; - let coins = Coin { denom, amount }; +) -> StdResult<u64> { let action = StoredTxAction::mint(minter.clone(), recipient.clone()); - let tx = StoredExtendedTx::new(id, action, coins, memo, block); - - if minter != recipient { - store_tx_with_decoys(store, &tx, &recipient, block, decoys, account_random_pos)?; - } - - StoredExtendedTx::append_tx(store, &tx, &minter)?; - - Ok(()) + append_new_stored_tx(store, &action, amount, denom, memo, block) } #[allow(clippy::too_many_arguments)] -pub fn store_burn( +pub fn store_burn_action( store: &mut dyn Storage, - owner: Addr, - burner: Addr, - amount: Uint128, + owner: CanonicalAddr, + burner: CanonicalAddr, + amount: u128, denom: String, memo: Option<String>, block: &cosmwasm_std::BlockInfo, - decoys: &Option<Vec<Addr>>, - account_random_pos: &Option<usize>, -) -> StdResult<()> { - let id = increment_tx_count(store)?; - let coins = Coin { denom, amount }; - let action = StoredTxAction::burn(owner.clone(), burner.clone()); - let tx = StoredExtendedTx::new(id, action, coins, memo, block); - - if burner != owner { - store_tx_with_decoys(store, &tx, &owner, block, decoys, account_random_pos)?; - } - - StoredExtendedTx::append_tx(store, &tx, &burner)?; - Ok(()) +) -> StdResult<u64> { + let action = StoredTxAction::burn(owner, burner); + append_new_stored_tx(store, &action, amount, denom, memo, block) } -pub fn store_deposit( +pub fn store_deposit_action( store: &mut dyn Storage, - recipient: &Addr, - amount: Uint128, + amount: u128, denom: String, block: &cosmwasm_std::BlockInfo, - decoys: &Option<Vec<Addr>>, - account_random_pos: &Option<usize>, -) -> StdResult<()> { - let id = increment_tx_count(store)?; - let coins = Coin { denom, amount }; +) -> StdResult<u64> { let action = StoredTxAction::deposit(); - let tx = StoredExtendedTx::new(id, action, coins, None, block); - - store_tx_with_decoys(store, &tx, recipient, block, decoys, account_random_pos) + append_new_stored_tx(store, &action, amount, denom, None, block) } -pub fn store_redeem( +pub fn store_redeem_action( store: &mut dyn Storage, - redeemer: &Addr, - amount: Uint128, + amount: u128, denom: String, block: &cosmwasm_std::BlockInfo, - decoys: &Option<Vec<Addr>>, - account_random_pos: &Option<usize>, -) -> StdResult<()> { - let id = increment_tx_count(store)?; - let coins = Coin { denom, amount }; +) -> StdResult<u64> { let action = StoredTxAction::redeem(); - let tx = StoredExtendedTx::new(id, action, coins, None, block); - - store_tx_with_decoys(store, &tx, redeemer, block, decoys, account_random_pos) + append_new_stored_tx(store, &action, amount, denom, None, block) } diff --git a/tests/dwb/.env.example b/tests/dwb/.env.example new file mode 100644 index 00000000..0a6b74d9 --- /dev/null +++ b/tests/dwb/.env.example @@ -0,0 +1,4 @@ +SECRET_LCD=http://localhost:1317 +SECRET_RPC=http://localhost:26657 +SECRET_CHAIN=secretdev-1 +ENABLE_EVAPORATION_TESTS=1 diff --git a/tests/dwb/.eslintrc.cjs b/tests/dwb/.eslintrc.cjs new file mode 100644 index 00000000..b045f013 --- /dev/null +++ b/tests/dwb/.eslintrc.cjs @@ -0,0 +1,14 @@ + +module.exports = { + extends: '@blake.regalia/eslint-config-elite', + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + tsconfigRootDir: __dirname, + project: 'tsconfig.json', + }, + rules: { + 'no-console': 'off', + '@typescript-eslint/naming-convention': 'off', + }, +}; diff --git a/tests/dwb/README.md b/tests/dwb/README.md new file mode 100644 index 00000000..42d1eb57 --- /dev/null +++ b/tests/dwb/README.md @@ -0,0 +1,29 @@ +# DWB Integration Test Suite + +## Requirements +The test suite is run using [bun](https://bun.sh/). + +## Setup +From this directory: +```sh +bun install +cp .env.example .env +``` + +Edit the `.env` file (or leave as is) to configure the network to either your localsecret or pulsar-3. + +## Run +```sh +bun run test ## compiles the contract for integration tests and runs the main test suite +``` + + +## Debugging + +In case there is a silent failure, it may help to run the suite using node.js instead of bun. You can compile it and run it and debug it interactively with the following commands: +```sh +bun run build && node --env-file=.env --inspect-brk dist/main.js +``` + +The console output should look something like this: + diff --git a/tests/dwb/bun.lockb b/tests/dwb/bun.lockb new file mode 100755 index 00000000..48417527 Binary files /dev/null and b/tests/dwb/bun.lockb differ diff --git a/tests/dwb/package.json b/tests/dwb/package.json new file mode 100644 index 00000000..2dc0e942 --- /dev/null +++ b/tests/dwb/package.json @@ -0,0 +1,29 @@ +{ + "private": "true", + "type": "module", + "scripts": { + "build": "tsc && tsc-esm-fix --tsconfig tsconfig.tsc-esm-fix.json --target=dist", + "make": "pushd ../../ && make compile-integration && popd", + "test": "bun run make && NODE_ENV=development bun run src/main.ts" + }, + "devDependencies": { + "@blake.regalia/belt": "^0.38.1", + "@blake.regalia/eslint-config-elite": "^0.4.4", + "@blake.regalia/tsconfig": "^0.2.0", + "@solar-republic/types": "^0.2.12", + "@types/chai": "^4.3.17", + "@types/node": "^22.1.0", + "chai": "^5.1.1", + "chai-bites": "^0.2.0", + "eslint": " 8", + "tsc-esm-fix": "^3.0.1", + "typescript": "^5.5.4" + }, + "dependencies": { + "@solar-republic/contractor": "^0.8.17", + "@solar-republic/cosmos-grpc": "^0.17.1", + "@solar-republic/crypto": "^0.2.14", + "@solar-republic/neutrino": "^1.5.3", + "bignumber.js": "^9.1.2" + } +} diff --git a/tests/dwb/src/constants.ts b/tests/dwb/src/constants.ts new file mode 100644 index 00000000..120f6046 --- /dev/null +++ b/tests/dwb/src/constants.ts @@ -0,0 +1,28 @@ +import type {TrustedContextUrl} from '@solar-republic/types'; + +import {base64_to_bytes} from '@blake.regalia/belt'; +import {Wallet} from '@solar-republic/neutrino'; + +export const P_SECRET_LCD = (process.env['SECRET_LCD'] || 'http://localhost:1317') as TrustedContextUrl; +export const P_SECRET_RPC = (process.env['SECRET_RPC'] || 'http://localhost:26656') as TrustedContextUrl; +export const SI_SECRET_CHAIN = (process.env['SECRET_CHAIN'] || 'secretdev-1') as TrustedContextUrl; +export const B_TEST_EVAPORATION = !!parseInt(process.env['ENABLE_EVAPORATION_TESTS'] || '0'); + +export const X_GAS_PRICE = 0.1; + +// import pre-configured wallets +export const [k_wallet_a, k_wallet_b, k_wallet_c, k_wallet_d] = await Promise.all([ + '8Ke2frmnGdVPipv7+xh9jClrl5EaBb9cowSUgj5GvrY=', + 'buqil+tLeeW7VLuugvOdTmkP3+tUwlCoScPZxeteBPE=', + 'UFrCdmofR9iChp6Eg7kE5O3wT+jsOXwJPWwB6kSeuhE=', + 'MM/1ZSbT5RF1BnaY6ui/i7yEN0mukGzvXUv+jOyjD0E=', +].map(sb64_sk => Wallet(base64_to_bytes(sb64_sk), SI_SECRET_CHAIN, P_SECRET_LCD, P_SECRET_RPC, 'secret'))); + +export const H_ADDRS = { + [k_wallet_a.addr]: 'Alice', + [k_wallet_b.addr]: 'Bob', + [k_wallet_c.addr]: 'Carol', + [k_wallet_d.addr]: 'David', +}; + +export const N_DECIMALS = 6; diff --git a/tests/dwb/src/contract.ts b/tests/dwb/src/contract.ts new file mode 100644 index 00000000..a9aa3e74 --- /dev/null +++ b/tests/dwb/src/contract.ts @@ -0,0 +1,109 @@ +import type {JsonObject} from '@blake.regalia/belt'; +import type {EncodedGoogleProtobufAny} from '@solar-republic/cosmos-grpc/google/protobuf/any'; +import type {TxResultTuple, Wallet, WeakSecretAccAddr} from '@solar-republic/neutrino'; +import type {CwHexLower, WeakUintStr} from '@solar-republic/types'; + +import {promisify} from 'node:util'; +import {gunzip} from 'node:zlib'; + +import {base64_to_bytes, bytes_to_hex, bytes_to_text, cast, sha256} from '@blake.regalia/belt'; +import {queryCosmosBankBalance} from '@solar-republic/cosmos-grpc/cosmos/bank/v1beta1/query'; +import {encodeGoogleProtobufAny} from '@solar-republic/cosmos-grpc/google/protobuf/any'; +import {SI_MESSAGE_TYPE_SECRET_COMPUTE_MSG_STORE_CODE, SI_MESSAGE_TYPE_SECRET_COMPUTE_MSG_INSTANTIATE_CONTRACT, encodeSecretComputeMsgStoreCode, encodeSecretComputeMsgInstantiateContract} from '@solar-republic/cosmos-grpc/secret/compute/v1beta1/msg'; +import {querySecretComputeCodeHashByCodeId, querySecretComputeCodes} from '@solar-republic/cosmos-grpc/secret/compute/v1beta1/query'; +import {destructSecretRegistrationKey} from '@solar-republic/cosmos-grpc/secret/registration/v1beta1/msg'; +import {querySecretRegistrationTxKey} from '@solar-republic/cosmos-grpc/secret/registration/v1beta1/query'; +import {SecretWasm, TendermintEventFilter, TendermintWs, broadcast_result, create_and_sign_tx_direct, exec_fees} from '@solar-republic/neutrino'; + +import {X_GAS_PRICE, P_SECRET_LCD, P_SECRET_RPC} from './constants'; + +const k_tef = await TendermintEventFilter(P_SECRET_RPC); + +export async function exec(k_wallet: Wallet, atu8_msg: EncodedGoogleProtobufAny, xg_gas_limit: bigint): Promise<TxResultTuple> { + const [atu8_raw, atu8_signdoc, si_txn] = await create_and_sign_tx_direct( + k_wallet, + [atu8_msg], + exec_fees(xg_gas_limit, X_GAS_PRICE, 'uscrt'), + xg_gas_limit + ); + + return await broadcast_result(k_wallet, atu8_raw, si_txn, k_tef); +} + +export async function upload_code(k_wallet: Wallet, atu8_wasm: Uint8Array): Promise<WeakUintStr> { + let atu8_bytecode = atu8_wasm; + + // gzip-encoded; decompress + if(0x1f === atu8_wasm[0] && 0x8b === atu8_wasm[1]) { + atu8_bytecode = await promisify(gunzip)(atu8_wasm); + } + + // hash + const atu8_hash = await sha256(atu8_bytecode); + const sb16_hash = cast<CwHexLower>(bytes_to_hex(atu8_hash)); + + // fetch all uploaded codes + const [,, g_codes] = await querySecretComputeCodes(P_SECRET_LCD); + + // already uploaded + const g_existing = g_codes?.code_infos?.find(g => g.code_hash! === sb16_hash); + if(g_existing) { + console.info(`Found code ID ${g_existing.code_id} already uploaded to network`); + + return g_existing.code_id as WeakUintStr; + } + + // upload + const [xc_code, sx_res, g_meta, atu8_data, h_events] = await exec(k_wallet, encodeGoogleProtobufAny( + SI_MESSAGE_TYPE_SECRET_COMPUTE_MSG_STORE_CODE, + encodeSecretComputeMsgStoreCode( + k_wallet.addr, + atu8_bytecode + ) + ), 30_000000n); + + if(xc_code) throw Error(sx_res); + + return h_events!['message.code_id'][0] as WeakUintStr; +} + +export async function instantiate_contract(k_wallet: Wallet, sg_code_id: WeakUintStr, h_init_msg: JsonObject): Promise<WeakSecretAccAddr> { + const [,, g_reg] = await querySecretRegistrationTxKey(P_SECRET_LCD); + const [atu8_cons_pk] = destructSecretRegistrationKey(g_reg!); + const k_wasm = SecretWasm(atu8_cons_pk!); + const [,, g_hash] = await querySecretComputeCodeHashByCodeId(P_SECRET_LCD, sg_code_id); + + // @ts-expect-error imported types versioning + const atu8_body = await k_wasm.encodeMsg(g_hash!.code_hash, h_init_msg); + + const [xc_code, sx_res, g_meta, atu8_data, h_events] = await exec(k_wallet, encodeGoogleProtobufAny( + SI_MESSAGE_TYPE_SECRET_COMPUTE_MSG_INSTANTIATE_CONTRACT, + encodeSecretComputeMsgInstantiateContract( + k_wallet.addr, + null, + sg_code_id, + h_init_msg['name'] as string, + atu8_body + ) + ), 10_000_000n); + + if(xc_code) { + const s_error = g_meta?.log ?? sx_res; + + // encrypted error message + const m_response = /(\d+):(?: \w+:)*? encrypted: (.+?): (.+?) contract/.exec(s_error); + if(m_response) { + // destructure match + const [, s_index, sb64_encrypted, si_action] = m_response; + + // decrypt ciphertext + const atu8_plaintext = await k_wasm.decrypt(base64_to_bytes(sb64_encrypted), atu8_body.slice(0, 32)); + + throw Error(bytes_to_text(atu8_plaintext)); + } + + throw Error(sx_res); + } + + return h_events!['message.contract_address'][0] as WeakSecretAccAddr; +} diff --git a/tests/dwb/src/dwb-entry.ts b/tests/dwb/src/dwb-entry.ts new file mode 100644 index 00000000..32372535 --- /dev/null +++ b/tests/dwb/src/dwb-entry.ts @@ -0,0 +1,76 @@ +import type {Nilable} from '@blake.regalia/belt'; +import type {CwSecretAccAddr} from '@solar-republic/neutrino'; + +import {bytes_to_biguint_be, bytes_to_hex} from '@blake.regalia/belt'; +import {bech32_encode} from '@solar-republic/crypto'; +import {BigNumber} from 'bignumber.js'; + +import {H_ADDRS} from './constants'; +import {SX_ANSI_BLUE, SX_ANSI_DIM_ON, SX_ANSI_GREEN, SX_ANSI_RESET, SX_ANSI_YELLOW} from './helper'; + +const NB_ADDR = 20; +const NB_AMOUNT = 8; +const NB_HEAD = 5; +const NB_LEN = 2; + +const NB_ENTRY = NB_ADDR+NB_AMOUNT+NB_HEAD+NB_LEN; + +export class DwbEntry { + constructor(protected _atu8_raw: Uint8Array) { + if(this._atu8_raw.byteLength !== NB_ENTRY) { + throw Error(`DWB entry was not exactly ${NB_ENTRY} bytes in length`); + } + } + + get raw(): Uint8Array { + return this._atu8_raw; + } + + get isNil(): boolean { + return /^0+$/.test(bytes_to_hex(this._atu8_raw)); + } + + get address(): CwSecretAccAddr { + return bech32_encode('secret', this._atu8_raw.subarray(0, NB_ADDR)); + } + + get amount(): bigint { + return bytes_to_biguint_be(this._atu8_raw.subarray(NB_ADDR, NB_ADDR+NB_AMOUNT)); + } + + get head(): bigint { + return bytes_to_biguint_be(this._atu8_raw.subarray(NB_ADDR+NB_AMOUNT, NB_ADDR+NB_AMOUNT+NB_HEAD)); + } + + get listlen(): bigint { + return bytes_to_biguint_be(this._atu8_raw.subarray(NB_ADDR+NB_AMOUNT+NB_HEAD, NB_ADDR+NB_AMOUNT+NB_HEAD+NB_LEN)); + } + + toString(k_prev?: Nilable<DwbEntry>): string { + let s_alias = H_ADDRS[this.address] || ''; + s_alias += s_alias? ` (${this.address.slice(0, 12)+'...'+this.address.slice(-5)})`: this.address; + s_alias = s_alias.padEnd(45, ' '); + + let s_amount = BigNumber(this.amount+'').shiftedBy(-6).toFixed(6).padStart(12, ' '); + + if(k_prev) { + if(this.address !== k_prev.address) { + const sx_color = this.amount? SX_ANSI_GREEN: SX_ANSI_YELLOW; + + s_alias = `${sx_color}${s_alias}${SX_ANSI_RESET}`; + s_amount = `${sx_color}${s_amount}${SX_ANSI_RESET}`; + } + else if(this.amount !== k_prev.amount) { + s_alias = `${SX_ANSI_BLUE}${s_alias}${SX_ANSI_RESET}`; + s_amount = `${SX_ANSI_BLUE}${s_amount}${SX_ANSI_RESET}`; + } + } + + return [ + s_alias, + s_amount, + (this.head+'').padStart(4, ' '), + (this.listlen+'').padStart(4, ' '), + ].map(s => this.amount? s: `${SX_ANSI_DIM_ON}${s}${SX_ANSI_RESET}`).join(' │ '); + } +} diff --git a/tests/dwb/src/dwb.ts b/tests/dwb/src/dwb.ts new file mode 100644 index 00000000..bd1e112c --- /dev/null +++ b/tests/dwb/src/dwb.ts @@ -0,0 +1,248 @@ +import type {SecretApp, WeakSecretAccAddr} from '@solar-republic/neutrino'; + +import {bytes, parse_json} from '@blake.regalia/belt'; +import * as chai from 'chai'; +const {expect} = chai; + + +import {DwbEntry} from './dwb-entry'; +import {SX_ANSI_DIM_ON, SX_ANSI_RESET, fail} from './helper'; + +export type DwbRequirements = { + showDelta?: boolean; + shouldNotContainEntriesFor?: WeakSecretAccAddr[]; +}; + +const R_ENTRY = /\s*DelayedWriteBufferEntry\(([^]*?)\)\s*,?/y; + +export function parse_dwb_dump(sx_dump: string) { + const [, sx_contents] = /DelayedWriteBuffer\s*\{\s*([^]*?)\s*\}\s*$/.exec(sx_dump)!; + const [, sg_empty, sx_entries] = /^\s*empty_space_counter:\s*(\d+),\s*entries:\s*\[([^]*)\]\s*$/.exec(sx_contents)!; + + const a_entries: Uint8Array[] = []; + for(;;) { + const m_entry = R_ENTRY.exec(sx_entries)!; + if(!m_entry) break; + + a_entries.push(bytes(parse_json<number[]>(m_entry[1]))); + } + + return { + empty_space_counter: parse_json(sg_empty), + entries: a_entries, + }; +} + +export class DwbValidator { + protected _a_entries_prev: DwbEntry[] = []; + protected _a_entries: DwbEntry[] = []; + protected _n_empty = 0; + + constructor(protected _k_app: SecretApp) {} + + get entries(): DwbEntry[] { + return this._a_entries.slice(); + } + + get previous(): DwbEntry[] { + return this._a_entries_prev.slice(); + } + + get empty(): number { + return this._n_empty; + } + + async sync() { + // cache previous state + this._a_entries_prev = this._a_entries.slice(); + + // dump dwb contents + const [g_dwb_res] = await this._k_app.query('dwb', {}); + + // parse + const { + empty_space_counter: sg_empty, + entries: a_entries, + } = parse_dwb_dump((g_dwb_res as {dwb: string}).dwb); + + // update cached entries + this._a_entries.length = 0; + this._a_entries.push(...a_entries.map(atu8 => new DwbEntry(atu8))); + + // save empty spaces counter + this._n_empty = parseFloat(sg_empty as string); + + return this._a_entries; + } + + async check(gc_check?: DwbRequirements) { + const a_prev = this._a_entries_prev; + const a_entries = this._a_entries; + + // should exclude entry for given addresses + const a_exclude = gc_check?.shouldNotContainEntriesFor; + if(a_exclude?.length) { + for(const sa_exclude of a_exclude) { + const i_found = a_entries.findIndex(k => sa_exclude === k.address); + + if(i_found > -1) { + fail(`Expected buffer to NOT contain an entry for ${sa_exclude} but found it at position ${i_found}`); + } + } + } + + // count empty spaces + let c_empty_actual = 0; + for(let i_space=a_entries.length-1; i_space>0; i_space--) { + if(!a_entries[i_space].amount) { + c_empty_actual += 1; + } + else { + break; + } + } + + // find changes + for(let i_space=0; i_space<a_entries.length; i_space++) { + const k_prev = a_prev[i_space]; + const k_curr = a_entries[i_space]; + + // same address + if(k_prev.address === k_curr.address) { + // amount changed + if(k_prev.amount && k_curr.amount && k_prev.amount !== k_curr.amount) { + // expect it to only ever increase + if(k_curr.amount < k_prev.amount) { + fail(`Found a negative change in entry amount`); + } + + // expect len to have increased by exactly 1 + if(k_curr.listlen !== k_prev.listlen + 1n) { + fail(`List length change was not exactly 1`); + } + } + } + } + + // assert empty spaces counter + if(c_empty_actual < this._n_empty) { + fail(`Contract reports ${this._n_empty} empty spaces but observed ${c_empty_actual}`); + } + } + + toString(b_show_delta?: boolean): string { + const a_prev = this._a_entries_prev; + + const a_lines: string[] = [ + `┏━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━┯━━━━━━┯━━━━━━┓`, + `┃ idx │ address │ amount │ head │ len ┃`, + `┠─────┴───────────────────────────────────────────────┴──────────────┴──────┴──────┨`, + ]; + const empty_row = (c: number) => `┃ ${SX_ANSI_DIM_ON}`+`...(empty x ${c})`.padEnd(78, ' ')+`${SX_ANSI_RESET}`+' ┃'; + let i_index = 0; + let c_empty = 0; + + for(const k_entry of this._a_entries) { + if(k_entry.isNil) { + c_empty += 1; + } + else { + if(c_empty) { + a_lines.push(empty_row(c_empty)); + c_empty = 0; + } + + a_lines.push(`┃ ${(i_index+'').padStart(3, ' ')} │ ${k_entry.toString(b_show_delta? a_prev[i_index]: null)} ┃`); + } + + i_index += 1; + } + + if(c_empty) a_lines.push(empty_row(c_empty)); + + return [ + a_lines.join('\n'), + `┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛`, + ].join('\n'); + } + + print(b_show_delta?: boolean): void { + console.log(this.toString(b_show_delta)); + } +} + + +// const g_dwb = parse_dwb_dump(` +// DelayedWriteBuffer { +// empty_space_counter: 61, +// entries: [ +// DelayedWriteBufferEntry([30, 64, 27, 13, 80, 9, 191, 112, 225, 11, 76, 117, 251, 233, 171, 52, 62, 116, 221, 165, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([252, 120, 243, 61, 153, 55, 155, 238, 217, 219, 75, 240, 232, 43, 128, 39, 177, 94, 70, 241, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([78, 34, 145, 19, 199, 90, 194, 255, 187, 156, 147, 189, 154, 40, 119, 128, 77, 51, 242, 84, 0, 0, 0, 0, 0, 152, 150, 128, 0, 0, 0, 0, 3, 0, 1]), +// DelayedWriteBufferEntry([236, 133, 74, 220, 71, 232, 157, 194, 70, 160, 113, 10, 155, 74, 105, 192, 216, 151, 180, 80, 0, 0, 0, 0, 0, 30, 132, 128, 0, 0, 0, 0, 7, 0, 2]), +// DelayedWriteBufferEntry([171, 152, 150, 130, 223, 89, 19, 108, 106, 73, 34, 29, 160, 38, 68, 217, 164, 90, 53, 87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), +// DelayedWriteBufferEntry([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) +// ] +// } +// `); + +// console.log(g_dwb.entries.length); diff --git a/tests/dwb/src/gas-checker.ts b/tests/dwb/src/gas-checker.ts new file mode 100644 index 00000000..12b4b898 --- /dev/null +++ b/tests/dwb/src/gas-checker.ts @@ -0,0 +1,69 @@ +import type {GroupedGasLogs} from './snip'; + +import {entries, bigint_abs} from '@blake.regalia/belt'; + +import {SX_ANSI_GREEN, SX_ANSI_RED, SX_ANSI_MAGENTA, SX_ANSI_RESET, SX_ANSI_YELLOW, SX_ANSI_CYAN} from './helper'; + +const delta_color = (xg_delta: bigint, nl_pad=0) => (bigint_abs(xg_delta) >= 1n + ? bigint_abs(xg_delta) > 2n + ? SX_ANSI_RED + : SX_ANSI_YELLOW + : '')+((xg_delta > 0? '+': '')+xg_delta).padStart(nl_pad, ' ')+SX_ANSI_RESET; + +export class GasChecker { + constructor(protected _h_baseline: GroupedGasLogs, protected _xg_used: bigint) {} + + compare(h_local: GroupedGasLogs, xg_used: bigint): void { + const {_h_baseline, _xg_used} = this; + + console.log(` ⚖️ Gas usage relative to baseline: ${xg_used === _xg_used + ? `${SX_ANSI_GREEN}0` + : delta_color(xg_used - _xg_used) + }${SX_ANSI_RESET}`); + + // each group + for(const [si_group, a_logs_local] of entries(h_local)) { + // find group in baseline + const a_logs_baseline = _h_baseline[si_group]; + + // offset + const xg_previous = a_logs_local[0]?.gas; + + // each log + for(let i_log=1; i_log<a_logs_local.length; i_log++) { + // ref log + const { + index: i_local, + gap: xg_gap_local, + comment: s_comment_local, + } = a_logs_local[i_log]; + + const g_log_baseline = a_logs_baseline.find(g => g.index === i_local); + + const xg_gap_baseline = g_log_baseline?.gap || 0n; + + // calculate delta + const xg_delta = xg_gap_local - xg_gap_baseline; + + // comment only + if('#' === si_group[0]) { + if(s_comment_local.trim()) { + console.log([ + ' '.repeat(8)+si_group.slice(0, 20).padEnd(20, ' '), + ' '.repeat(3), + SX_ANSI_CYAN+s_comment_local+SX_ANSI_RESET, + ].join(' │ ')); + } + } + // non-zero delta + else if(xg_delta || '@' === s_comment_local[0]) { + console.log([ + ' '.repeat(8)+si_group.slice(0, 20).padEnd(20, ' '), + delta_color(xg_delta, 3), + ('@' === s_comment_local[0]? SX_ANSI_MAGENTA: '')+s_comment_local+SX_ANSI_RESET, + ].join(' │ ')); + } + } + } + } +} diff --git a/tests/dwb/src/helper.ts b/tests/dwb/src/helper.ts new file mode 100644 index 00000000..60836095 --- /dev/null +++ b/tests/dwb/src/helper.ts @@ -0,0 +1,89 @@ +import type {Promisable} from '@blake.regalia/belt'; + +import {is_string, map_entries} from '@blake.regalia/belt'; + +/* eslint-disable @typescript-eslint/naming-convention */ +export const SX_ANSI_RESET = '\x1b[0m'; +export const SX_ANSI_DIM_ON = '\x1b[2m'; +export const SX_ANSI_UNDERLINE = '\x1b[4m'; +export const SX_ANSI_DIM_OFF = '\x1b[22m'; +export const SX_ANSI_RED = '\x1b[31m'; +export const SX_ANSI_GREEN = '\x1b[32m'; +export const SX_ANSI_YELLOW = '\x1b[33m'; +export const SX_ANSI_BLUE = '\x1b[34m'; +export const SX_ANSI_MAGENTA = '\x1b[35m'; +export const SX_ANSI_CYAN = '\x1b[36m'; +export const SX_ANSI_WHITE = '\x1b[37m'; +export const SX_ANSI_GRAY_BG = '\x1b[100m'; +/* eslint-enable */ + +// polyfill crypto global for node.js env +globalThis.crypto ||= (await import('crypto')).webcrypto; + +export function pass(s_test: string): void { + // eslint-disable-next-line no-console + console.log(`${SX_ANSI_GREEN}✓${SX_ANSI_RESET} ${s_test}`); +} + +function error(s_test: string, ...a_args: Array<string | object>) { + const a_rest = a_args.map(z => is_string(z)? z: map_entries(z, ([si, w]) => `\n\t${si}: ${w}`).join('\n')); + console.error(`${s_test}: ${a_rest.join('; ')}`); +} + +export function fail(s_test: string, ...a_args: Array<string | object>): void { + error(`❌ ${s_test}`, ...a_args); + throw Error(`Exitting on error`); +} + +export function caught(s_test: string, ...a_args: Array<string | object>): void { + error(`💀 ${s_test}`, ...a_args); +} + +interface GroupCallback { + it(s_test: string, f_test: () => Promisable<void>): Promise<void>; +} + +export async function describe(s_group: string, f_group: (g_call: GroupCallback) => Promisable<void>): Promise<void> { + const a_results: Array<{ + type: 'pass'; + name: string; + } | { + type: 'fail'; + name: string; + message: string; + }> = []; + + await f_group({ + async it(s_test: string, f_test: () => Promisable<void>) { + try { + await f_test(); + + a_results.push({ + type: 'pass', + name: s_test, + }); + } + catch(e_run) { + a_results.push({ + type: 'fail', + name: s_test, + message: (e_run as Error).stack || '', + }); + } + }, + }); + + console.log(''); + console.log(`# ${s_group}\n${'='.repeat(2+s_group.length)}`); + + for(const g_result of a_results) { + if('pass' === g_result.type) { + pass(g_result.name); + } + else { + fail(g_result.name, g_result.message); + } + } + + console.log(''); +} diff --git a/tests/dwb/src/main.ts b/tests/dwb/src/main.ts new file mode 100644 index 00000000..e3e8b446 --- /dev/null +++ b/tests/dwb/src/main.ts @@ -0,0 +1,258 @@ +import type {Dict, JsonObject} from '@blake.regalia/belt'; + +import type {SecretContractInterface, FungibleTransferCall, SecretAccAddr, Snip24} from '@solar-republic/contractor'; + +import type {CwUint128, WeakUint128Str} from '@solar-republic/types'; + +import {readFileSync} from 'node:fs'; + +import {bytes, bytes_to_base64, entries, sha256, text_to_bytes, bigint_greater, bigint_abs} from '@blake.regalia/belt'; +import {encodeCosmosBankMsgSend, SI_MESSAGE_TYPE_COSMOS_BANK_MSG_SEND} from '@solar-republic/cosmos-grpc/cosmos/bank/v1beta1/tx'; +import {encodeGoogleProtobufAny} from '@solar-republic/cosmos-grpc/google/protobuf/any'; +import {SecretApp, SecretContract, Wallet, broadcast_result, create_and_sign_tx_direct, random_32, type TxMeta, type WeakSecretAccAddr} from '@solar-republic/neutrino'; +import {BigNumber} from 'bignumber.js'; + +import {B_TEST_EVAPORATION, N_DECIMALS, P_SECRET_LCD, P_SECRET_RPC, SI_SECRET_CHAIN, X_GAS_PRICE, k_wallet_a, k_wallet_b, k_wallet_c, k_wallet_d} from './constants'; +import {upload_code, instantiate_contract} from './contract'; +import {DwbValidator} from './dwb'; +import {GasChecker} from './gas-checker'; +import {transfer, type TransferResult} from './snip'; + +const S_CONTRACT_LABEL = 'snip2x-test_'+bytes_to_base64(crypto.getRandomValues(bytes(6))); + +const atu8_wasm = readFileSync('../../contract.wasm'); + +console.log(k_wallet_a.addr); + +console.debug(`Uploading code...`); +const sg_code_id = await upload_code(k_wallet_a, atu8_wasm); + +console.debug(`Instantiating contract...`); + +const sa_snip = await instantiate_contract(k_wallet_a, sg_code_id, { + name: S_CONTRACT_LABEL, + symbol: 'TKN', + decimals: 6, + admin: k_wallet_a.addr, + initial_balances: entries({ + [k_wallet_a.addr]: 10_000_000000n, + }).map(([sa_account, xg_balance]) => ({ + address: sa_account, + amount: `${xg_balance}`, + })), + prng_seed: bytes_to_base64(random_32()), + config: { + public_total_supply: true, + enable_deposit: true, + enable_redeem: true, + enable_mint: true, + enable_burn: true, + }, +}); + +console.debug(`Running tests against ${sa_snip}...`); + +// @ts-expect-error deep instantiation +const k_contract = await SecretContract<SecretContractInterface<{ + extends: Snip24; + executions: { + transfer: [FungibleTransferCall & { + gas_target?: WeakUint128Str; + }]; + }; +}>>(P_SECRET_LCD, sa_snip); + +const k_app_a = SecretApp(k_wallet_a, k_contract, X_GAS_PRICE); +const k_app_b = SecretApp(k_wallet_b, k_contract, X_GAS_PRICE); +const k_app_c = SecretApp(k_wallet_c, k_contract, X_GAS_PRICE); +const k_app_d = SecretApp(k_wallet_d, k_contract, X_GAS_PRICE); + +const H_APPS = { + a: k_app_a, + b: k_app_b, + c: k_app_c, + d: k_app_d, +}; + +// #ts-expect-error validator! +const k_dwbv = new DwbValidator(k_app_a); + +async function transfer_chain(sx_chain: string) { + const a_lines = sx_chain.split(/\s*\n+\s*/g).filter(s => s && /^\s*(\d+)/.test(s)); + + let k_checker: GasChecker | null = null; + + for(const sx_line of a_lines) { + const [, sx_amount, si_from, si_to] = /^\s*([\d.]+)(?:\s*TKN)?\s+(\w+)(?:\s+to|\s*[-=]*>+)?\s+(\w+)\s*/.exec(sx_line)!; + + const xg_amount = BigInt(BigNumber(sx_amount).shiftedBy(N_DECIMALS).toFixed(0)); + + console.log(sx_amount, si_from, si_to); + + // @ts-expect-error secret app + const g_result = await transfer(k_dwbv, xg_amount, H_APPS[si_from[0].toLowerCase()] as SecretApp, H_APPS[si_to[0].toLowerCase()] as SecretApp, k_checker); + + if(!k_checker) { + k_checker = new GasChecker(g_result.tracking, g_result.gasUsed); + } + } +} + +// evaporation +if(B_TEST_EVAPORATION) { + const xg_post_evaporate_buffer = 50_000n; + const xg_gas_wanted = 150_000n; + const xg_gas_target = xg_gas_wanted - xg_post_evaporate_buffer; + + const [g_exec, xc_code, sx_res, g_meta, h_events, si_txn] = await k_app_a.exec('transfer', { + amount: `${500000n}` as CwUint128, + recipient: k_wallet_b.addr, + gas_target: `${xg_gas_target}`, + }, xg_gas_wanted); + + console.log({g_meta}); + + if(xc_code) { + throw Error(`Failed evaporation test: ${sx_res}`); + } + + const xg_gas_used = BigInt(g_meta?.gas_used || '0'); + if(xg_gas_used < xg_gas_target) { + throw Error(`Expected gas used to be greater than ${xg_gas_target} but only used ${xg_gas_used}`); + } + else if(bigint_abs(xg_gas_wanted, xg_gas_used) > xg_post_evaporate_buffer) { + throw Error(`Expected gas used to be ${xg_gas_wanted} but found ${xg_gas_used}`); + } +} + +{ + console.log('# Initialized'); + await k_dwbv.sync(); + k_dwbv.print(); + console.log('\n'); + + // basic transfers between principals + await transfer_chain(` + 1 TKN Alice => Bob + 2 TKN Alice => Carol + 5 TKN Alice => David + 1 TKN Bob => Carol -- Bob's entire balance; settles Bob for 1st time + 1 TKN Carol => David -- should accumulate; settles Carol for 1st time + 1 TKN David => Alice -- re-adds Alice to buffer; settles David for 1st time + `); + + // extended transfers between principals + await transfer_chain(` + 1 TKN David => Bob + 1 TKN David => Bob -- exact same transfer repeated + 1 TKN Alice => Bob + 1 TKN Bob => Carol + 1 TKN Alice => Carol + 1 TKN Carol => Bob -- yet again + `); + + // gas checker ref + let k_checker: GasChecker | null = null; + + // grant action from previous simultion + let f_grant: undefined | (() => Promise<[w_result: JsonObject | undefined, xc_code: number, s_response: string, g_meta: TxMeta | undefined, h_events: Dict<string[]> | undefined, si_txn: string | undefined]>); + + // number of simulations to perform + const N_SIMULATIONS = 300; + + // record maximum gas used for direct transfers + let xg_max_gas_used_transfer = 0n; + + // simulate many transfers + for(let i_sim=0; i_sim<N_SIMULATIONS; i_sim++) { + const si_receiver = i_sim+''; + + const k_wallet = await Wallet(await sha256(text_to_bytes(si_receiver)), SI_SECRET_CHAIN, P_SECRET_LCD, P_SECRET_RPC, 'secret'); + + const k_app_sim = SecretApp(k_wallet, k_contract, X_GAS_PRICE); + + // label + console.log(`Alice --> ${si_receiver}`); + + // transfer some gas to sim account + const [atu8_raw,, si_txn] = await create_and_sign_tx_direct(k_wallet_b, [ + encodeGoogleProtobufAny( + SI_MESSAGE_TYPE_COSMOS_BANK_MSG_SEND, + encodeCosmosBankMsgSend(k_wallet_b.addr, k_wallet.addr, [[`${1_000000n}`, 'uscrt']]) + ), + ], [[`${5000n}`, 'uscrt']], 50_000n); + + // submit all in parallel + const [ + // @ts-expect-error totally stupid + g_result_transfer, + [xc_send_gas, s_err_send_gas], + a_res_increase, + ] = await Promise.all([ + // #ts-expect-error secret app + transfer(k_dwbv, i_sim % 2? 1_000000n: 2_000000n, k_app_a, k_app_sim, k_checker), + broadcast_result(k_wallet, atu8_raw, si_txn), + f_grant?.(), + ]); + + // send gas error + if(xc_send_gas) { + throw Error(`Failed to transfer gas: ${s_err_send_gas}`); + } + + // increase allowance error + if(f_grant && a_res_increase?.[1]) { + throw Error(`Failed to increase allowance: ${a_res_increase[2]}`); + } + + // approve Alice as spender for future txs + f_grant = () => k_app_sim.exec('increase_allowance', { + spender: k_wallet_a.addr, + amount: `${1_000000n}` as CwUint128, + }, 60_000n); + + if(!k_checker) { + k_checker = new GasChecker((g_result_transfer as TransferResult).tracking, (g_result_transfer as TransferResult).gasUsed); + } + + xg_max_gas_used_transfer = bigint_greater(xg_max_gas_used_transfer, g_result_transfer.gasUsed); + } + + // reset checker + k_checker = null; + + // record maximum gas used for transfer froms + let xg_max_gas_used_transfer_from = 0n; + + // perform transfer_from + for(let i_sim=N_SIMULATIONS-2; i_sim>0; i_sim--) { + const si_owner = i_sim+''; + const si_recipient = (i_sim - 1)+''; + + const k_wallet_owner = await Wallet(await sha256(text_to_bytes(si_owner)), SI_SECRET_CHAIN, P_SECRET_LCD, P_SECRET_RPC, 'secret'); + const k_wallet_recipient = await Wallet(await sha256(text_to_bytes(si_recipient)), SI_SECRET_CHAIN, P_SECRET_LCD, P_SECRET_RPC, 'secret'); + + const k_app_owner = SecretApp(k_wallet_owner, k_contract, X_GAS_PRICE); + const k_app_recipient = SecretApp(k_wallet_recipient, k_contract, X_GAS_PRICE); + + console.log(`${si_owner} --> ${si_recipient}`); + + // #ts-expect-error secret app + const g_result = await transfer(k_dwbv, 1_000000n, k_app_owner, k_app_recipient, k_checker, k_app_a); + + if(!k_checker) { + k_checker = new GasChecker(g_result.tracking, g_result.gasUsed); + } + + xg_max_gas_used_transfer_from = bigint_greater(xg_max_gas_used_transfer_from, g_result.gasUsed); + } + + // report + console.log({ + xg_max_gas_used_transfer, + xg_max_gas_used_transfer_from, + }); + + // done + process.exit(0); +} diff --git a/tests/dwb/src/snip.ts b/tests/dwb/src/snip.ts new file mode 100644 index 00000000..4c3b3a62 --- /dev/null +++ b/tests/dwb/src/snip.ts @@ -0,0 +1,205 @@ +import type {DwbValidator} from './dwb'; +import type {GasChecker} from './gas-checker'; +import type {Dict, Nilable} from '@blake.regalia/belt'; +import type {SecretContractInterface} from '@solar-republic/contractor'; +import type {SecretApp, WeakSecretAccAddr} from '@solar-republic/neutrino'; +import type {CwUint128, SecretQueryPermit, WeakUintStr} from '@solar-republic/types'; + +import {entries, is_bigint, stringify_json} from '@blake.regalia/belt'; +import {queryCosmosBankBalance} from '@solar-republic/cosmos-grpc/cosmos/bank/v1beta1/query'; +import {sign_secret_query_permit} from '@solar-republic/neutrino'; +import BigNumber from 'bignumber.js'; + +import {H_ADDRS, N_DECIMALS, P_SECRET_LCD} from './constants'; +import {fail} from './helper'; + + +export type GasLog = { + index: number; + gas: bigint; + gap: bigint; + comment: string; +}; + +export type GroupedGasLogs = Dict<GasLog[]>; + +export type TransferResult = { + tracking: GroupedGasLogs; + gasUsed: bigint; +}; + +type TokenBalance = SecretContractInterface<{ + queries: { + balance: [{}, { + amount: CwUint128; + }]; + + with_permit: { + variants: [ + { + msg: { + query: { + balance: {}; + }; + permit: SecretQueryPermit; + }; + response: { + balance: { + amount: CwUint128; + }; + }; + }, + ]; + }; + }; +}>; + +export async function scrt_balance(sa_owner: WeakSecretAccAddr): Promise<bigint> { + const [,, g_res] = await queryCosmosBankBalance(P_SECRET_LCD, sa_owner, 'uscrt'); + return BigInt(g_res?.balance?.amount || '0'); +} + +export async function snip_balance(k_app: SecretApp<TokenBalance>) { + const g_permit = await sign_secret_query_permit(k_app.wallet, 'snip-balance', [k_app.contract.addr], ['balance']); + return await k_app.query('balance', {}, g_permit as unknown as null); +} + +export async function transfer( + k_dwbv: DwbValidator, + xg_amount: bigint, + k_app_owner: SecretApp, + k_app_recipient: SecretApp, + k_checker?: Nilable<GasChecker>, + k_app_sender?: SecretApp +): Promise<TransferResult> { + const sa_owner = k_app_owner.wallet.addr; + const sa_recipient = k_app_recipient.wallet.addr; + + // scrt balance of owner before transfer + // @ts-expect-error canonical addr + const xg_scrt_balance_owner_before = await scrt_balance(sa_owner); + + // query balance of owner and recipient + const [ + [g_balance_owner_before], + [g_balance_recipient_before], + ] = await Promise.all([ + // @ts-expect-error secret app + snip_balance(k_app_owner), + // @ts-expect-error secret app + snip_balance(k_app_recipient), + ]); + + // execute transfer + const [g_exec, xc_code, sx_res, g_meta, h_events, si_txn] = k_app_sender + ? await k_app_sender.exec('transfer_from', { + owner: k_app_owner.wallet.addr, + amount: `${xg_amount}` as CwUint128, + recipient: sa_recipient, + }, 250000n) + : await k_app_owner.exec('transfer', { + amount: `${xg_amount}` as CwUint128, + recipient: sa_recipient, + }, 250000n); + + // section header + console.log(`# Transfer ${BigNumber(xg_amount+'').shiftedBy(-N_DECIMALS).toFixed()} TKN ${H_ADDRS[sa_owner] || sa_owner}${k_app_sender? ` (via ${H_ADDRS[k_app_sender.wallet.addr] || k_app_sender.wallet.addr})`: ''} => ${H_ADDRS[sa_recipient] || sa_recipient} | ⏹ ${k_dwbv.empty} spaces | ⛽️ ${g_meta?.gas_used || '0'} gas used`); + + // query balance of owner and recipient again + const [ + [g_balance_owner_after], + [g_balance_recipient_after], + ] = await Promise.all([ + // @ts-expect-error secret app + snip_balance(k_app_owner), + // @ts-expect-error secret app + snip_balance(k_app_recipient), + ]); + + if(xc_code) { + console.warn('Diagnostics', { + scrt_balance_before: xg_scrt_balance_owner_before, + // @ts-expect-error canonical addr + scrt_balance_after: await scrt_balance(sa_owner), + snip_balance_before: g_balance_owner_before?.amount, + snip_balance_after: g_balance_owner_after?.amount, + meta: stringify_json(g_meta), + events: h_events, + exec: g_exec, + }); + + throw Error(`Failed to execute transfer from ${k_app_owner.wallet.addr} [${xc_code}]: ${sx_res}`); + } + + // sync the buffer + await k_dwbv.sync(); + + const h_tracking: GroupedGasLogs = {}; + for(const [si_key, a_values] of entries(h_events!)) { + const m_key = /^wasm\.gas\.(.+)$/.exec(si_key); + if(m_key) { + const [, si_group] = m_key; + + const a_logs: GasLog[] = []; + let xg_previous = 0n; + + for(const sx_value of a_values) { + const [, sg_index, sg_gas, s_comment] = /^(\d+):(\d+):([^]*)$/.exec(sx_value)!; + + const xg_gas = BigInt(sg_gas); + + a_logs.push({ + index: parseInt(sg_index), + gas: xg_gas, + gap: xg_gas - xg_previous, + comment: s_comment, + }); + + xg_previous = xg_gas; + } + + h_tracking[si_group] = a_logs.sort((g_a, g_b) => g_a.index - g_b.index); + } + } + + if(k_checker) { + k_checker.compare(h_tracking, BigInt(g_meta!.gas_used)); + } + else if(null === k_checker) { + console.log(` ⚖️ Setting baseline gas used to ${g_meta!.gas_used}`); + } + + // prit its state + k_dwbv.print(true); + + + // balance queries failed + if(!g_balance_owner_before || !g_balance_recipient_before || !g_balance_owner_after || !g_balance_recipient_after) { + throw fail(`Failed to fetch balances`); + } + + // expect exact amount difference for owner + const xg_owner_loss = BigInt(g_balance_owner_before.amount as string) - BigInt(g_balance_owner_after.amount); + if(xg_owner_loss !== xg_amount) { + fail(`Owner's balance changed by ${-xg_owner_loss}, but the amount sent was ${xg_amount}`); + } + + // expect exact amount difference for recipient + const xg_recipient_gain = BigInt(g_balance_recipient_after.amount) - BigInt(g_balance_recipient_before.amount); + if(xg_recipient_gain !== xg_amount) { + fail(`Recipient's balance changed by ${xg_recipient_gain}, but the amount sent was ${xg_amount}`); + } + + // make assertions + await k_dwbv.check({ + // shouldNotContainEntriesFor: [k_app_owner.wallet.addr], + }); + + // close + console.log('\n'); + + return { + tracking: h_tracking, + gasUsed: BigInt(g_meta!.gas_used), + }; +} diff --git a/tests/dwb/tsconfig.json b/tests/dwb/tsconfig.json new file mode 100644 index 00000000..79f379b2 --- /dev/null +++ b/tests/dwb/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": [ + "@blake.regalia/tsconfig/tsconfig.node.json" + ], + + "compilerOptions": { + "moduleResolution": "Bundler", + "outDir": "dist", + }, +} \ No newline at end of file diff --git a/tests/dwb/tsconfig.tsc-esm-fix.json b/tests/dwb/tsconfig.tsc-esm-fix.json new file mode 100644 index 00000000..64db3f6f --- /dev/null +++ b/tests/dwb/tsconfig.tsc-esm-fix.json @@ -0,0 +1,7 @@ +{ + "extends": "node_modules/@blake.regalia/tsconfig/tsconfig.node.json", + "compilerOptions": { + "moduleResolution": "Bundler", + "outDir": "dist", + }, +} \ No newline at end of file diff --git a/tests/integration.sh b/tests/integration.sh index dba743ee..37846613 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -18,7 +18,7 @@ declare -A FROM=( # In particular, it's not possible to dynamically expand aliases, but `tx_of` dynamically executes whatever # we specify in its arguments. function secretcli() { - docker exec secretdev /usr/bin/secretd "$@" + docker exec localsecret /usr/bin/secretd "$@" } # Just like `echo`, but prints to stderr @@ -587,7 +587,7 @@ function test_permit() { local expected_error="Error: query result: Generic error: Permit doesn't apply to token \"$contract_addr\", allowed tokens: [\"$wrong_contract\"]" for key in "${KEY[@]}"; do log "permit querying balance for \"$key\" with wrong permit for that contract" - permit=$(docker exec secretdev bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit"') --from '$key'") + permit=$(docker exec localsecret bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit"') --from '$key'") permit_query='{"with_permit":{"query":{"balance":{}},"permit":{"params":{"permit_name":"test","chain_id":"blabla","allowed_tokens":["'"$wrong_contract"'"],"permissions":["balance"]},"signature":'"$permit"'}}}' result="$(compute_query "$contract_addr" "$permit_query" 2>&1 | sed 's/\\//g' || true)" assert_eq "$result" "$expected_error" @@ -603,7 +603,7 @@ function test_permit() { tx_hash="$(compute_execute "$contract_addr" '{"revoke_permit":{"permit_name":"to_be_revoked"}}' ${FROM[$key]} --gas 250000)" wait_for_compute_tx "$tx_hash" "waiting for revoke_permit from \"$key\" to process" - permit=$(docker exec secretdev bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit"') --from '$key'") + permit=$(docker exec localsecret bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit"') --from '$key'") permit_query='{"with_permit":{"query":{"balance":{}},"permit":{"params":{"permit_name":"to_be_revoked","chain_id":"blabla","allowed_tokens":["'"$contract_addr"'"],"permissions":["balance"]},"signature":'"$permit"'}}}' expected_error="Error: query result: Generic error: Permit \"to_be_revoked\" was revoked by account \"${ADDRESS[$key]}" result="$(compute_query "$contract_addr" "$permit_query" 2>&1 | sed 's/\\//g' || true)" @@ -617,7 +617,7 @@ function test_permit() { local expected_error for key in "${KEY[@]}"; do log "permit querying balance for \"$key\" with params not matching permit" - permit=$(docker exec secretdev bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit"') --from '$key'") + permit=$(docker exec localsecret bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit"') --from '$key'") permit_query='{"with_permit":{"query":{"balance":{}},"permit":{"params":{"permit_name":"test","chain_id":"not_blabla","allowed_tokens":["'"$contract_addr"'"],"permissions":["balance"]},"signature":'"$permit"'}}}' expected_error="Error: query result: Generic error: Failed to verify signatures for the given permit" result="$(compute_query "$contract_addr" "$permit_query" 2>&1 | sed 's/\\//g' || true)" @@ -632,7 +632,7 @@ function test_permit() { local expected_error for key in "${KEY[@]}"; do log "permit querying balance for \"$key\" without the right permission" - permit=$(docker exec secretdev bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit_conf"') --from '$key'") + permit=$(docker exec localsecret bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit_conf"') --from '$key'") permit_query='{"with_permit":{"query":{"balance":{}},"permit":{"params":{"permit_name":"test","chain_id":"blabla","allowed_tokens":["'"$contract_addr"'"],"permissions":["history"]},"signature":'"$permit"'}}}' expected_error="Error: query result: Generic error: No permission to query balance, got permissions [History]" result="$(compute_query "$contract_addr" "$permit_query" 2>&1 | sed 's/\\//g' || true)" @@ -647,7 +647,7 @@ function test_permit() { local expected_error for key in "${KEY[@]}"; do log "permit querying history for \"$key\" without the right permission" - permit=$(docker exec secretdev bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit_conf"') --from '$key'") + permit=$(docker exec localsecret bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit_conf"') --from '$key'") permit_query='{"with_permit":{"query":{"transfer_history":{"page_size":10, "should_filter_decoys":false}},"permit":{"params":{"permit_name":"test","chain_id":"blabla","allowed_tokens":["'"$contract_addr"'"],"permissions":["balance"]},"signature":'"$permit"'}}}' expected_error="Error: query result: Generic error: No permission to query history, got permissions [Balance]" @@ -668,7 +668,7 @@ function test_permit() { local expected_error for key in "${KEY[@]}"; do log "permit querying allowance for \"$key\" without the right permission" - permit=$(docker exec secretdev bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit_conf"') --from '$key'") + permit=$(docker exec localsecret bash -c "/usr/bin/secretd tx sign-doc <(echo '"$permit_conf"') --from '$key'") permit_query='{"with_permit":{"query":{"allowance":{"owner":"'"${ADDRESS[$key]}"'","spender":"'"${ADDRESS[$key]}"'"}},"permit":{"params":{"permit_name":"test","chain_id":"blabla","allowed_tokens":["'"$contract_addr"'"],"permissions":["history"]},"signature":'"$permit"'}}}' expected_error="Error: query result: Generic error: No permission to query allowance, got permissions [History]" result="$(compute_query "$contract_addr" "$permit_query" 2>&1 | sed 's/\\//g' || true)" @@ -681,7 +681,7 @@ function test_permit() { local permit_query local expected_error log "permit querying allowance without signer being the owner or spender" - permit=$(docker exec secretdev bash -c "/usr/bin/secretd tx sign-doc <(echo '"$wrong_permit"') --from a") + permit=$(docker exec localsecret bash -c "/usr/bin/secretd tx sign-doc <(echo '"$wrong_permit"') --from a") permit_query='{"with_permit":{"query":{"allowance":{"owner":"'"$wrong_contract"'","spender":"'"$wrong_contract"'"}},"permit":{"params":{"permit_name":"test","chain_id":"blabla","allowed_tokens":["'"$contract_addr"'"],"permissions":["allowance"]},"signature":'"$permit"'}}}' expected_error="Error: query result: Generic error: Cannot query allowance. Requires permit for either owner \"$wrong_contract\" or spender \"$wrong_contract\", got permit for \"${ADDRESS[a]}" result="$(compute_query "$contract_addr" "$permit_query" 2>&1 | sed 's/\\//g' || true)" @@ -695,7 +695,7 @@ function test_permit() { local expected_output for key in "${KEY[@]}"; do log "permit querying balance for \"$key\"" - permit=$(docker exec secretdev bash -c "/usr/bin/secretd tx sign-doc <(echo '"$good_permit"') --from '$key'") + permit=$(docker exec localsecret bash -c "/usr/bin/secretd tx sign-doc <(echo '"$good_permit"') --from '$key'") permit_query='{"with_permit":{"query":{"balance":{}},"permit":{"params":{"permit_name":"test","chain_id":"blabla","allowed_tokens":["'"$contract_addr"'"],"permissions":["balance"]},"signature":'"$permit"'}}}' expected_output="{\"balance\":{\"amount\":\"0\"}}" result="$(compute_query "$contract_addr" "$permit_query" 2>&1 | sed 's/\\//g' || true)" @@ -710,7 +710,7 @@ function test_permit() { local expected_output for key in "${KEY[@]}"; do log "permit querying history for \"$key\"" - permit=$(docker exec secretdev bash -c "/usr/bin/secretd tx sign-doc <(echo '"$good_permit"') --from '$key'") + permit=$(docker exec localsecret bash -c "/usr/bin/secretd tx sign-doc <(echo '"$good_permit"') --from '$key'") permit_query='{"with_permit":{"query":{"transfer_history":{"page_size":10, "should_filter_decoys":false}},"permit":{"params":{"permit_name":"test","chain_id":"blabla","allowed_tokens":["'"$contract_addr"'"],"permissions":["history"]},"signature":'"$permit"'}}}' expected_output="{\"transfer_history\":{\"txs\":[],\"total\":0}}" @@ -731,7 +731,7 @@ function test_permit() { local expected_output for key in "${KEY[@]}"; do log "permit querying history for \"$key\"" - permit=$(docker exec secretdev bash -c "/usr/bin/secretd tx sign-doc <(echo '"$good_permit"') --from '$key'") + permit=$(docker exec localsecret bash -c "/usr/bin/secretd tx sign-doc <(echo '"$good_permit"') --from '$key'") permit_query='{"with_permit":{"query":{"allowance":{"owner":"'"${ADDRESS[$key]}"'","spender":"'"${ADDRESS[$key]}"'"}},"permit":{"params":{"permit_name":"test","chain_id":"blabla","allowed_tokens":["'"$contract_addr"'"],"permissions":["allowance"]},"signature":'"$permit"'}}}' expected_output="{\"allowance\":{\"spender\":\"${ADDRESS[$key]}\",\"owner\":\"${ADDRESS[$key]}\",\"allowance\":\"0\",\"expiration\":null}}"