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..a52506f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,18 +1,34 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[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", "version_check", ] +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + [[package]] name = "base16ct" version = "0.1.1" @@ -27,9 +43,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 +89,15 @@ dependencies = [ [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" [[package]] name = "cfg-if" @@ -84,87 +106,99 @@ 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 = "cosmwasm-derive" -version = "1.1.9" -source = "git+https://github.com/scrtlabs/cosmwasm/?tag=v1.1.9-secret#e40a15f04dae80680dbe22aef760e5eaab6b0a19" +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ - "syn 1.0.109", + "num-traits", ] [[package]] -name = "cosmwasm-schema" -version = "1.2.2" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b99f612ccf162940ae2eef9f370ee37cf2ddcf4a9a8f5ee15ec6b46a5ecd2e" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "cosmwasm-schema-derive", - "schemars", - "serde", - "serde_json", - "thiserror", + "crypto-common", + "inout", + "zeroize", ] [[package]] -name = "cosmwasm-schema-derive" -version = "1.2.2" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "cosmwasm-derive" +version = "1.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92ceea61033cb69c336abf673da017ddf251fc4e26e0cdd387eaf8bedb14e49" +checksum = "d67457e4acb04e738788d3489e343957455df2c4643f2b53050eb052ca631d19" 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" +name = "cosmwasm-schema" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9a7b56d154870ec4b57b224509854f706c9744449548d8a3bf91ac75c59192" dependencies = [ - "base64 0.13.1", - "cosmwasm-crypto", - "cosmwasm-derive", - "derivative", - "forward_ref", - "hex", + "cosmwasm-schema-derive", "schemars", "serde", - "serde-json-wasm", + "serde_json", "thiserror", - "uint", ] [[package]] -name = "cosmwasm-storage" -version = "1.1.9" -source = "git+https://github.com/scrtlabs/cosmwasm/?tag=v1.1.9-secret#e40a15f04dae80680dbe22aef760e5eaab6b0a19" +name = "cosmwasm-schema-derive" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd3d80310cd7b86b09dbe886f4f2ca235a5ddb8d478493c6e50e720a3b38a42" dependencies = [ - "cosmwasm-std", - "serde", + "proc-macro2", + "quote", + "syn 2.0.94", ] [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -194,6 +228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -242,9 +277,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 +288,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 +328,7 @@ dependencies = [ "base16ct", "crypto-bigint", "der", - "digest 0.10.6", + "digest 0.10.7", "ff", "generic-array", "group", @@ -314,6 +349,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 +366,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 +376,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 +411,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.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "k256" @@ -391,26 +453,47 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "sha2 0.10.6", + "sha2 0.10.8", ] [[package]] name = "libc" -version = "0.2.140" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minicbor" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0452a60c1863c1f50b5f77cd295e8d2786849f35883f0b9e18e7e6e1b5691b0" + +[[package]] +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] [[package]] name = "once_cell" -version = "1.17.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[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,37 +505,62 @@ 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" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] -name = "proc-macro2" -version = "1.0.52" +name = "primitive-types" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ - "unicode-ident", + "fixed-hash", + "uint 0.9.5", ] [[package]] -name = "quote" -version = "1.0.26" +name = "primitive-types" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" dependencies = [ - "proc-macro2", + "fixed-hash", + "uint 0.10.0", ] [[package]] -name = "rand" -version = "0.8.5" +name = "proc-macro2" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ - "rand_core 0.6.4", + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", ] [[package]] @@ -482,13 +590,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.94", ] [[package]] @@ -508,20 +616,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 +639,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.94", ] [[package]] @@ -555,16 +663,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 0.9.5", +] + +[[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.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=df89b582bc207f4a2f697c31b9a7c64faac10195#df89b582bc207f4a2f697c31b9a7c64faac10195" 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 +739,96 @@ dependencies = [ [[package]] name = "secret-toolkit-crypto" -version = "0.8.0" -source = "git+https://github.com/scrtlabs/secret-toolkit?rev=9b74bdac71c2fedcc12246f18cdfdd94b8991282#9b74bdac71c2fedcc12246f18cdfdd94b8991282" +version = "0.10.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=df89b582bc207f4a2f697c31b9a7c64faac10195#df89b582bc207f4a2f697c31b9a7c64faac10195" dependencies = [ - "cosmwasm-std", + "cc", + "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.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=df89b582bc207f4a2f697c31b9a7c64faac10195#df89b582bc207f4a2f697c31b9a7c64faac10195" dependencies = [ - "bech32", - "cosmwasm-std", - "remain", + "chacha20poly1305", + "generic-array", + "hex", + "hkdf", + "minicbor", + "primitive-types 0.12.2", "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.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=df89b582bc207f4a2f697c31b9a7c64faac10195#df89b582bc207f4a2f697c31b9a7c64faac10195" dependencies = [ - "cosmwasm-std", + "bech32", + "remain", + "ripemd", "schemars", + "secret-cosmwasm-std", + "secret-toolkit-crypto", + "secret-toolkit-storage", "secret-toolkit-utils", "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.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=df89b582bc207f4a2f697c31b9a7c64faac10195#df89b582bc207f4a2f697c31b9a7c64faac10195" 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.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=df89b582bc207f4a2f697c31b9a7c64faac10195#df89b582bc207f4a2f697c31b9a7c64faac10195" 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.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=df89b582bc207f4a2f697c31b9a7c64faac10195#df89b582bc207f4a2f697c31b9a7c64faac10195" dependencies = [ - "cosmwasm-std", - "cosmwasm-storage", + "chrono", "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.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=df89b582bc207f4a2f697c31b9a7c64faac10195#df89b582bc207f4a2f697c31b9a7c64faac10195" 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 +837,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.158" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 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,33 +864,34 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.158" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.94", ] [[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.94", ] [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -731,13 +911,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 +926,30 @@ 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", - "rand", + "hex", + "minicbor", + "primitive-types 0.13.1", + "rand_chacha", + "rand_core 0.6.4", "schemars", + "secret-cosmwasm-std", + "secret-cosmwasm-storage", "secret-toolkit", "secret-toolkit-crypto", "serde", + "serde-big-array", + "static_assertions", ] [[package]] @@ -783,9 +970,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "subtle" -version = "2.4.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -800,9 +987,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.4" +version = "2.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae" +checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" dependencies = [ "proc-macro2", "quote", @@ -811,29 +998,29 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.39" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.39" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.94", ] [[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" @@ -847,17 +1034,39 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -865,8 +1074,29 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + [[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..6ce4ab6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "snip20-reference-impl" -version = "1.0.0" -authors = ["Itzik "] +version = "2.0.0" +authors = ["@reuvenpo","@toml01","@assafmo","@liorbond","Itzik ","@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. @@ -27,26 +27,29 @@ overflow-checks = true [features] # for quicker tests, cargo test --lib # for more explicit tests, cargo test --features=backtraces -#default = ["debug-print"] +default = [] 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" } -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" } +cosmwasm-std = { package = "secret-cosmwasm-std", version = "1.1.11" } +cosmwasm-storage = { package = "secret-cosmwasm-storage", version = "1.1.11" } +cosmwasm-schema = "2.1.5" +# secret-toolkit = { version = "0.10.2", default-features = false, features = ["permit", "storage", "viewing-key", "notification"] } +secret-toolkit = { git = "https://github.com/SolarRepublic/secret-toolkit.git", default-features = false, features = ["permit", "storage", "viewing-key", "notification"], rev = "df89b582bc207f4a2f697c31b9a7c64faac10195" } +# secret-toolkit-crypto = { version = "0.10.2", default-features = false, features = ["hash", "hkdf", "rand"] } +secret-toolkit-crypto = { git = "https://github.com/SolarRepublic/secret-toolkit.git", default-features = false, features = ["hash", "hkdf", "rand"], rev = "df89b582bc207f4a2f697c31b9a7c64faac10195" } +static_assertions = "1.1.0" +rand_core = { version = "0.6.4", default-features = false } +rand_chacha = { version = "0.3.1", default-features = false } schemars = "0.8.12" serde = { version = "1.0.158", default-features = false, features = ["derive"] } +serde-big-array = "0.5.1" base64 = "0.21.0" - -[dev-dependencies] -cosmwasm-schema = { version = "1.1.8" } +constant_time_eq = "0.3.0" +primitive-types = { version = "0.13.1", default-features = false } +minicbor = "0.25.1" +hex = "0.4.3" diff --git a/Makefile b/Makefile index eee79095..687bc1f0 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,10 @@ SECRETCLI = docker exec -it secretdev /usr/bin/secretcli +SECRET_GRPC_PORT ?= 9090 +SECRET_LCD_PORT ?= 1317 +SECRET_RPC_PORT ?= 26657 +LOCALSECRET_VERSION ?= v1.15.0 + .PHONY: all all: clippy test @@ -51,6 +56,13 @@ _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 -o ./contract.wasm + .PHONY: compile-optimized _compile-optimized compile-optimized: _compile-optimized contract.wasm.gz _compile-optimized: @@ -63,7 +75,7 @@ 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 + ghcr.io/scrtlabs/secret-contract-optimizer:1.0.11 contract.wasm.gz: contract.wasm cat ./contract.wasm | gzip -9 > ./contract.wasm.gz @@ -74,9 +86,14 @@ contract.wasm: .PHONY: start-server 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 \ + -e FAST_BLOCKS=true \ + -p $(SECRET_RPC_PORT):26657 \ + -p $(SECRET_LCD_PORT):1317 \ + -p $(SECRET_GRPC_PORT):9090 \ + -p 5000:5000 \ -v $$(pwd):/root/code \ - --name secretdev ghcr.io/scrtlabs/localsecret:v1.6.0-alpha.4 + --name secretdev \ + ghcr.io/scrtlabs/localsecret:$(LOCALSECRET_VERSION) .PHONY: schema schema: diff --git a/README.md b/README.md index 294b59d5..d04670b9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SNIP-20 Reference Implementation -This is an implementation of a [SNIP-20](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md), [SNIP-21](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-21.md), [SNIP-22](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-22.md), [SNIP-23](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-23.md), [SNIP-24](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-24.md), [SNIP-25](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-25.md) and [SNIP-26](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-26.md) compliant token contract. +This is an implementation of a [SNIP-20](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md), [SNIP-21](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-21.md), [SNIP-22](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-22.md), [SNIP-23](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-23.md), [SNIP-24](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-24.md), [~~SNIP-25~~](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-25.md), [SNIP-26](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-26.md), [~~SNIP-50~~](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-50.md) and [SNIP-52](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-52.md) compliant token contract. > **Note:** > The master branch contains new features not covered by officially-released SNIPs and may be subject to change. When releasing a token on mainnet, we recommend you start with a [tagged release](https://github.com/scrtlabs/snip20-reference-impl/tags) to ensure compatibility with SNIP standards. @@ -66,112 +66,27 @@ All transactions are encrypted, so if you want to see the error returned by a fa `secretcli q compute tx ` -# SNIP 25 Security Update - -## Security Changes -1. Implemented the ability to have decoy addresses for every operation that access account's balance -2. Converted every add operation related to account's balance and total supply -3. Started using u128 instead of Uint128 - -## Decoys -### Transaction That Support Decoys -1. Redeem -2. Deposit -3. Transfer -4. TransferFrom -5. Send -6. SendFrom -7. Burn -8. BurnFrom -9. Mint -10. BatchTransfer - For every action (The strength of the decoys will be the minimal strength of all of the actions) -11. BatchSend - For every action (The strength of the decoys will be the minimal strength of all of the actions) -12. BatchTransferFrom - For every action (The strength of the decoys will be the minimal strength of all of the actions) -13. BatchSendFrom - For every action (The strength of the decoys will be the minimal strength of all of the actions) -14. BatchMint - For every action (The strength of the decoys will be the minimal strength of all of the actions) -15. BatchBurnFrom - For every action (The strength of the decoys will be the minimal strength of all of the actions) - -### Example -```secretcli tx compute execute '{"transfer":{"recipient":"
","amount":"", "entropy":"", "decoys":<[addresses_list]>}}' --from ``` - -## Future Work -| Topic | Immediate-term solution | Medium-term solution | Long-term solution | -| --- | --- | --- | --- | -| Receiver privacy | Decoys - offer limited privacy, since it depends a lot on how you choose decoys. There’s probably no way to select decoys effectively enough, and thus it only makes it a bit harder but effectively doesn’t provide receiver privacy to a sophisticated long-term attacker | Some sort of bucketing? - still no clear path forward| ORAM? - still no clear path forward | -| Transfer amount privacy - subtractions (Transfer/Send/Burn) | None | None | Merkle proofs for storage reads - will make it very difficult to simulate transactions and play with storage. | - -# SNIP 25 Other Updates - -## All Allowances -Adds the ability for an owner to query for all allowances they have given out, as well as for a spender to query for all allowances they have received. - -## Queries - -### AllowancesGiven - -This query MUST be authenticated. - -Returns the list of allowances given out by the current account as an owner, as well as the total count of allowances given out. - -Results SHOULD be paginated. Results MUST be sorted in reverse chronological order by the datetime at which the allowance was first created (i.e., order is not determined by expiration, nor by last modified). - -#### Request - -| Name | Type | Description | optional | -| ---- | ---- | ----------- | -------- | -| [with_permit].query.allowances_given.owner | string | Account from which tokens are allowed to be taken | no | -| [with_permit].query.allowances_given.page_size | number | Number of allowances to return, starting from the latest. i.e. n=1 will return only the latest allowance | no | -| [with_permit].query.allowances_given.page | number | Defaults to 0. Specifying a positive number will skip page * page_size txs from the start. | yes | - -#### Response -```json -{ - "allowances_given": { - "owner": "
", - "allowances": [ - { - "spender": "
", - "allowance": "Uint128", - "expiration": 1234, - }, - { "...": "..." } - ], - "count": 200 - } -} -``` - -### AllowancesReceived - -This query MUST be authenticated. - -Returns the list of allowances given to the current account as a spender, as well as the total count of allowances received. - -Results SHOULD be paginated. Results MUST be sorted in reverse chronological order by the datetime at which the allowance was first created (i.e., order is not determined by expiration). - -#### Request - -| Name | Type | Description | optional | -| ---- | ---- | ----------- | -------- | -| [with_permit.]query.allowances_received.spender | string | Account which is allowed to spend tokens on behalf of the owner | no | -| [with_permit.]query.allowances_received.page_size | number | Number of allowances to return, starting from the latest. i.e. n=1 will return only the latest allowance | no | -| [with_permit.]query.allowances_received.page | number | Defaults to 0. Specifying a positive number will skip page * page_size txs from the start. | yes | - -#### Response - -```json -{ - "allowances_received": { - "spender": "
", - "allowances": [ - { - "owner": "
", - "allowance": "Uint128", - "expiration": 1234, - }, - { "...": "..." } - ], - "count": 200 - } -} -``` +## Privacy Enhancements + + - All transfers/sends (including batch and *_from) use the delayed write buffer (dwb) to address "spicy printf" storage access pattern attacks. + - Additionally, a bitwise trie of bucketed entries (dwb) creates dynamic anonymity sets for senders/owners, whose balance must be checked when transferring/sending. It also enhances privacy for recipients. + - When querying for Transaction History, each event's `id` field returned in responses are deterministically obfuscated by `ChaChaRng(XorBytes(ChaChaRng(actual_event_id), internal_secret)) >> (64 - 53)` for better privacy. Without this, an attacker could deduce the number of events that took place between two transactions. + +## SNIP-52: Private Push Notifications + +This contract publishes encrypted messages to the event log which carry data intended to notify recipients of actions that affect them, such as token transfer and allowances. + +Direct channels: + - `recvd` -- emitted to a recipient when their account receives funds via one of `transfer`, `send`, `transfer_from`, or `send_from`. The notification data includes the amount, the sender, and the memo length. + - `spent` -- emitted to an owner when their funds are spent, via one of `transfer`, `send`, `transfer_from` or `send_from`. The notification data includes the amount, the recipient, the owner's new balance, and a few other pieces of information such as memo length, number of actions, and whether the spender was the transaction's sender. + - `allowance` -- emitted to a spender when some allower account has granted them or modified an existing allowance to spend their tokens, via `increase_allowance` or `decrease_allowance`. The notification data includes the amount, the allower, and the expiration of the allowance. + +Group channels: + - `multirecvd` -- emitted to a group of recipients (up to 16) when a `batch_transfer`, `batch_send`, `batch_transfer_from`, or `batch_send_from` has been executed. Each recipient will receive a packet of data containing the amount they received, the last 8 bytes of the owner's address, and some additional metadata. + - `multispent` -- emitted to a group of spenders (up to 16) when a `batch_transfer_from`, or `batch_send_from` has been executed. Each spender will receive a packet of data containing the amount that was spent, the last 8 bytes of the recipient's address, and some additional metadata. + + +## Security Features + + - Transfers to the contract itself will be rejected to prevent accidental loss of funds. + - The migration allows for a one-time processing of refunding any previous transfers made to the contract itself. diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..b45041dc --- /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"); + writeln!(file, "pub const DWB_CAPACITY: u16 = {};", dwb_capacity).unwrap(); + writeln!(file, "pub const BTBE_CAPACITY: u16 = {};", 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>; -} +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, - pub decoys: Option>, } #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] @@ -26,7 +21,6 @@ pub struct SendAction { pub amount: Uint128, pub msg: Option, pub memo: Option, - pub decoys: Option>, } #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] @@ -36,7 +30,6 @@ pub struct TransferFromAction { pub recipient: String, pub amount: Uint128, pub memo: Option, - pub decoys: Option>, } #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] @@ -48,7 +41,6 @@ pub struct SendFromAction { pub amount: Uint128, pub msg: Option, pub memo: Option, - pub decoys: Option>, } #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] @@ -57,7 +49,6 @@ pub struct MintAction { pub recipient: String, pub amount: Uint128, pub memo: Option, - pub decoys: Option>, } #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] @@ -66,22 +57,4 @@ pub struct BurnFromAction { pub owner: String, pub amount: Uint128, pub memo: Option, - pub decoys: Option>, } - -macro_rules! impl_decoyable { - ($struct:ty) => { - impl HasDecoy for $struct { - fn decoys(&self) -> &Option> { - &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..63e2e069 --- /dev/null +++ b/src/btbe.rs @@ -0,0 +1,966 @@ +//! 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 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::constants::{ADDRESS_BYTES_LEN, IMPOSSIBLE_ADDR}; +use crate::dwb::{amount_u64, constant_time_if_else_u32, DelayedWriteBufferEntry, TxBundle}; +#[cfg(feature = "gas_tracking")] +use crate::gas_tracker::GasTracker; +use crate::state::{safe_add, safe_add_u64, INTERNAL_SECRET_SENSITIVE}; + +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; + +const BTBE_BUCKET_ADDRESS_BYTES: usize = ADDRESS_BYTES_LEN; +const BTBE_BUCKET_BALANCE_BYTES: usize = 8; // Max 16 (u64) +const BTBE_BUCKET_HISTORY_BYTES: usize = 4; // Max 4 (u32) +const BTBE_BUCKET_CACHE_BYTES: usize = 0; + +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 + + BTBE_BUCKET_CACHE_BYTES; + +/// 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 { + 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(result)) + } + + fn from( + storage: &mut dyn Storage, + dwb_entry: &DelayedWriteBufferEntry, + amount_spent: Option, + ) -> StdResult { + 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 { + let result = CanonicalAddr::try_from(self.address_slice()) + .or(Err(StdError::generic_err("Get bucket address error")))?; + Ok(result) + } + + pub fn balance(&self) -> StdResult { + 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 { + 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 save_hash_cache(&mut self, storage: &dyn Storage) -> StdResult<()> { + let hash_bytes = hkdf_sha_256( + &Some(BUCKETING_SALT_BYTES.to_vec()), + INTERNAL_SECRET_SENSITIVE.load(storage)?.as_slice(), + self.address_slice(), + 32, + )?; + + let start = + BTBE_BUCKET_ADDRESS_BYTES + BTBE_BUCKET_BALANCE_BYTES + BTBE_BUCKET_HISTORY_BYTES; + let end = start + BTBE_BUCKET_CACHE_BYTES; + self.0[start..end].copy_from_slice(&hash_bytes.as_slice()[0..BTBE_BUCKET_CACHE_BYTES]); + Ok(()) + } + + pub fn routes_to_right_node(&self, bit_pos: usize, secret: &[u8]) -> StdResult { + // target byte value + // bit pos is cached + let byte = if bit_pos < (BTBE_BUCKET_CACHE_BYTES << 3) { + // select the byte from cache corresponding to this bit position + self.0[BTBE_BUCKET_ADDRESS_BYTES + + BTBE_BUCKET_BALANCE_BYTES + + BTBE_BUCKET_HISTORY_BYTES + + (bit_pos >> 3)] + } + // not cached; calculate on the fly + else { + // create key bytes + let key_bytes = hkdf_sha_256( + &Some(BUCKETING_SALT_BYTES.to_vec()), + secret, + self.address_slice(), + 32, + )?; + + // select the byte containing the target bit + key_bytes[bit_pos >> 3] + }; + + // extract value at bit position and turn into bool + Ok(((byte >> (7 - (bit_pos % 8))) & 1) != 0) + } + + pub fn merge_dwb_entry( + &mut self, + storage: &mut dyn Storage, + dwb_entry: &DelayedWriteBufferEntry, + amount_spent: Option, + ) -> StdResult<()> { + // increase account's stored balance + let mut balance = self.balance()?; + safe_add_u64(&mut balance, dwb_entry.amount()?); + + // safety check amount spent before spending from balance + 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 + ))); + }; + + // set new balance to stored entry + self.set_balance(balance)?; + + // retrieve currenty history length + let history_len = self.history_len()?; + + // flag if history is empty + let empty_history = (history_len == 0) as u32; + + // position of last tx bundle to read + let bundle_pos = constant_time_if_else_u32( + empty_history, + 0u32, + history_len.wrapping_sub(1), // constant-time subtraction with underflow + ); + + // peek at the last tx bundle added (read the dummy one if its void) + let last_tx_bundle_result = self.get_tx_bundle_at_unchecked(storage, bundle_pos); + if last_tx_bundle_result.is_err() { + return Err(StdError::generic_err( + "missing tx bundle while merging dwb entry!", + )); + } + + // unwrap + let last_tx_bundle = last_tx_bundle_result?; + + // calculate the appropriate bundle offset to use + let bundle_offset = constant_time_if_else_u32( + empty_history, + 0u32, + last_tx_bundle.offset + (last_tx_bundle.list_len as u32), + ); + + // create new tx bundle + let tx_bundle = TxBundle { + head_node: dwb_entry.head_node()?, + list_len: dwb_entry.list_len()?, + offset: bundle_offset, + }; + + // add to list + 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 { + 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 { + 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)?; + // if the head node is null, then add this as a ghost bundle that does not contribute to len of list, + // and will be overwritten next time + let len_add = constant_time_if_else_u32((bundle.head_node == 0) as u32, 0, 1); + self.set_history_len(len.saturating_add(len_add))?; + 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 = Item::new(KEY_BTBE_ENTRY_HISTORY); +static BTBE_BUCKETS_COUNT: Item = Item::new(KEY_BTBE_BUCKETS_COUNT); +static BTBE_BUCKETS: Item = 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 { + 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 = Item::new(KEY_BTBE_TRIE_NODES); +pub static BTBE_TRIE_NODES_COUNT: Item = 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 { + 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 { + 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) + } +} + +/// 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, usize)> { + // load internal contract secret + let secret = INTERNAL_SECRET_SENSITIVE.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)?; + + // bit position + let mut bit_pos: usize = 0; + + // while the node has children + while node.bucket == 0 { + // calculate bit value at current bit position + let bit_value = (hash[bit_pos / 8] >> (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> { + 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> { + 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 { + 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) -> StdResult { + 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) +} + +// settles a dwb entry into its appropriate bucket +// `amount_spent` is any required subtraction due to being sender of tx +pub fn settle_dwb_entry( + storage: &mut dyn Storage, + dwb_entry: &DelayedWriteBufferEntry, + amount_spent: Option, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<()> { + #[cfg(feature = "gas_tracking")] + let mut group1 = tracker.group("#merge_dwb_entry.1"); + + // ref the entry's recipient address + let address = &dwb_entry.recipient()?; + + // locate the node that the given entry belongs in + let (mut node, mut node_id, mut bit_pos) = locate_btbe_node(storage, address)?; + + // 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(address) { + // 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 {} ", + address, node_id, bucket_id, idx + )); + + // save updated bucket to storage + node.set_and_save_bucket(storage, bucket)?; + } + // nothing was stored yet + else { + // need to insert new entry + // create new stored balance entry + let mut btbe_entry = StoredEntry::from(storage, dwb_entry, amount_spent)?; + + // cache the address + btbe_entry.save_hash_cache(storage)?; + + // load contract's internal secret + let secret = INTERNAL_SECRET_SENSITIVE.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 { + // route entry + if entry.routes_to_right_node(bit_pos, secret)? { + right_bucket.add_entry(&entry); + } else { + left_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 btbe_entry.routes_to_right_node(bit_pos, secret)? { + node = right; + node_id = right_id; + bucket = right_bucket; + bucket_id = right_bucket_id; + } else { + node = left; + node_id = left_id; + bucket = left_bucket; + bucket_id = left_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, + ) -> ( + StdResult, + OwnedDeps, + ) { + 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(error: StdResult) -> String { + match error { + Ok(response) => { + let bin_err = (&response as &dyn Any) + .downcast_ref::() + .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 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 storage = &mut deps.storage; + + let _ = initialize_btbe(storage).unwrap(); + + let btbe_node_count = BTBE_TRIE_NODES_COUNT.load(storage).unwrap(); + assert_eq!(btbe_node_count, 1); + + for i in 1..=64 { + let canonical = deps + .api + .addr_canonicalize(Addr::unchecked(format!("{i}zzzzzz")).as_str()) + .unwrap(); + + let mut entry = StoredEntry::new(&canonical).unwrap(); + let _ = entry.save_hash_cache(storage).unwrap(); + + assert_eq!(entry.address().unwrap(), canonical); + assert_eq!(entry.balance().unwrap(), 0_u64); + + let mut dwb_entry = DelayedWriteBufferEntry::new(&canonical).unwrap(); + + let _result = settle_dwb_entry(storage, &mut dwb_entry, None); + + let btbe_node_count = BTBE_TRIE_NODES_COUNT.load(storage).unwrap(); + assert_eq!(btbe_node_count, 1); + + let (node, node_id, bit_pos) = locate_btbe_node(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 65th entry + let canonical = deps + .api + .addr_canonicalize(Addr::unchecked(format!("bob")).as_str()) + .unwrap(); + let mut entry = StoredEntry::new(&canonical).unwrap(); + let _ = entry.save_hash_cache(storage); + assert_eq!(entry.address().unwrap(), canonical); + assert_eq!(entry.balance().unwrap(), 0_u64); + + let mut dwb_entry = DelayedWriteBufferEntry::new(&canonical).unwrap(); + + let _result = settle_dwb_entry(&mut deps.storage, &mut 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: 2, + } + ); + assert_eq!(node_id, 2); + 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/constants.rs b/src/constants.rs new file mode 100644 index 00000000..d3705aaa --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,18 @@ +#[cfg(test)] +pub const ADDRESS_BYTES_LEN: usize = 54; +#[cfg(not(test))] +pub const ADDRESS_BYTES_LEN: usize = 20; + +/// canonical address bytes corresponding to the 33-byte null public key, in hexadecimal +#[cfg(test)] +pub const IMPOSSIBLE_ADDR: [u8; ADDRESS_BYTES_LEN] = [ + 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))] +pub const IMPOSSIBLE_ADDR: [u8; ADDRESS_BYTES_LEN] = [ + 0x29, 0xCF, 0xC6, 0x37, 0x62, 0x55, 0xA7, 0x84, 0x51, 0xEE, 0xB4, 0xB1, 0x29, 0xED, 0x8E, 0xAC, + 0xFF, 0xA2, 0xFE, 0xEF, +]; diff --git a/src/contract.rs b/src/contract.rs index f5f89789..ba5e1ee1 100644 --- a/src/contract.rs +++ b/src/contract.rs @@ -1,33 +1,46 @@ +#[cfg(feature = "gas_evaporation")] +use cosmwasm_std::Api; /// 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, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, }; -use rand::RngCore; -use secret_toolkit::permit::{Permit, RevokedPermits, TokenPermissions}; +use secret_toolkit::notification::{DirectChannel, GroupChannel}; +use secret_toolkit::permit::{Permit, 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; +use crate::{ + execute, execute_admin, execute_deposit_redeem, execute_mint_burn, execute_transfer_send, query, +}; + +#[cfg(feature = "gas_tracking")] +use crate::dwb::log_dwb; +use crate::dwb::{DelayedWriteBuffer, DWB}; + +use crate::btbe::initialize_btbe; + +#[cfg(feature = "gas_tracking")] +use crate::gas_tracker::GasTracker; +#[cfg(feature = "gas_evaporation")] +use crate::msg::Evaporator; use crate::msg::{ - AllowanceGivenResult, AllowanceReceivedResult, ContractStatusLevel, Decoyable, ExecuteAnswer, - ExecuteMsg, InstantiateMsg, QueryAnswer, QueryMsg, QueryWithPermit, ResponseStatus::Success, + ContractStatusLevel, ExecuteMsg, InstantiateMsg, QueryAnswer, QueryMsg, QueryWithPermit, }; -use crate::receiver::Snip20ReceiveMsg; -use crate::state::{ - safe_add, AllowancesStore, BalancesStore, Config, MintersStore, PrngStore, ReceiverHashStore, - CONFIG, CONTRACT_STATUS, TOTAL_SUPPLY, +use crate::notifications::{ + AllowanceNotification, MultiRecvdNotification, MultiSpentNotification, RecvdNotification, + SpentNotification, }; -use crate::transaction_history::{ - store_burn, store_deposit, store_mint, store_redeem, store_transfer, StoredExtendedTx, - StoredLegacyTransfer, +use crate::state::{ + Config, MintersStore, CHANNELS, CONFIG, CONTRACT_STATUS, INTERNAL_SECRET_RELAXED, + INTERNAL_SECRET_SENSITIVE, NOTIFICATIONS_ENABLED, TOTAL_SUPPLY, }; +use crate::strings::TRANSFER_HISTORY_UNSUPPORTED_MSG; /// We make sure that responses from `handle` are padded to a multiple of this size. pub const RESPONSE_BLOCK_SIZE: usize = 256; -pub const PREFIX_REVOKED_PERMITS: &str = "revoked_permits"; +pub const NOTIFICATION_BLOCK_SIZE: usize = 1; #[entry_point] pub fn instantiate( @@ -55,56 +68,92 @@ 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, - )?; - - 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", - )); - } + // 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 secrets + let salt = Some(sha_256(&rng_entropy).to_vec()); + let internal_secret_sensitive = hkdf_sha_256( + &salt, + rng_seed.0.as_slice(), + "contract_internal_secret_sensitive".as_bytes(), + 32, + )?; + INTERNAL_SECRET_SENSITIVE.save(deps.storage, &internal_secret_sensitive)?; + + let internal_secret_relaxed = hkdf_sha_256( + &salt, + rng_seed.0.as_slice(), + "contract_internal_secret_relaxed".as_bytes(), + 32, + )?; + INTERNAL_SECRET_RELAXED.save(deps.storage, &internal_secret_relaxed)?; + + // Hard-coded channels + let channels: Vec = vec![ + RecvdNotification::CHANNEL_ID.to_string(), + SpentNotification::CHANNEL_ID.to_string(), + AllowanceNotification::CHANNEL_ID.to_string(), + MultiRecvdNotification::CHANNEL_ID.to_string(), + MultiSpentNotification::CHANNEL_ID.to_string(), + ]; + + for channel in channels { + CHANNELS.insert(deps.storage, &channel)?; + } + + NOTIFICATIONS_ENABLED.save(deps.storage, &true)?; + + 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); + execute_mint_burn::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", + )); } } - let supported_denoms = match msg.supported_denoms { - None => vec![], - Some(x) => x, - }; + let supported_denoms = msg.supported_denoms.unwrap_or_default(); CONFIG.save( deps.storage, @@ -132,57 +181,45 @@ 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 { - 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 { - let contract_status = CONTRACT_STATUS.load(deps.storage)?; - - let mut account_random_pos: Option = 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) + // load contract config from storage + let config = CONFIG.load(deps.storage)?; + + // check that message sender is the admin + if config.admin != info.sender { + return Err(StdError::generic_err( + "This is an admin command. Admin commands can only be run from admin address", + )); + } + + execute_admin::set_contract_status(deps, 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 => + { + execute_deposit_redeem::try_redeem(deps, env, info, amount, denom) } _ => Err(StdError::generic_err( "This contract is stopped and this action is not allowed", @@ -195,70 +232,56 @@ 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::Deposit { .. } => { + execute_deposit_redeem::try_deposit(deps, env, info, &mut rng) + } + ExecuteMsg::Redeem { amount, denom, .. } => { + execute_deposit_redeem::try_redeem(deps, env, info, amount, denom) } - ExecuteMsg::Redeem { - amount, - denom, - decoys, - .. - } => try_redeem(deps, env, info, amount, denom, decoys, account_random_pos), // Base ExecuteMsg::Transfer { recipient, amount, memo, - decoys, .. - } => try_transfer( - deps, - env, - info, - recipient, - amount, - memo, - decoys, - account_random_pos, - ), + } => { + execute_transfer_send::try_transfer(deps, env, info, &mut rng, recipient, amount, memo) + } ExecuteMsg::Send { recipient, recipient_code_hash, amount, msg, memo, - decoys, .. - } => try_send( + } => execute_transfer_send::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) + execute_transfer_send::try_batch_transfer(deps, env, info, &mut rng, actions) } ExecuteMsg::BatchSend { actions, .. } => { - try_batch_send(deps, env, info, actions, account_random_pos) + execute_transfer_send::try_batch_send(deps, env, info, &mut rng, actions) + } + ExecuteMsg::Burn { amount, memo, .. } => { + execute_mint_burn::try_burn(deps, env, info, amount, memo) } - ExecuteMsg::Burn { - amount, - memo, - decoys, - .. - } => try_burn(deps, env, info, amount, memo, decoys, account_random_pos), ExecuteMsg::RegisterReceive { code_hash, .. } => { - try_register_receive(deps, info, code_hash) + execute::try_register_receive(deps, info, code_hash) + } + ExecuteMsg::CreateViewingKey { entropy, .. } => { + execute::try_create_key(deps, env, info, entropy, &mut rng) } - ExecuteMsg::CreateViewingKey { entropy, .. } => try_create_key(deps, env, info, entropy), - ExecuteMsg::SetViewingKey { key, .. } => try_set_key(deps, info, key), + ExecuteMsg::SetViewingKey { key, .. } => execute::try_set_key(deps, info, key), // Allowance ExecuteMsg::IncreaseAllowance { @@ -266,30 +289,21 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S amount, expiration, .. - } => try_increase_allowance(deps, env, info, spender, amount, expiration), + } => execute::try_increase_allowance(deps, env, info, spender, amount, expiration), ExecuteMsg::DecreaseAllowance { spender, amount, expiration, .. - } => try_decrease_allowance(deps, env, info, spender, amount, expiration), + } => execute::try_decrease_allowance(deps, env, info, spender, amount, expiration), ExecuteMsg::TransferFrom { owner, recipient, amount, memo, - decoys, .. - } => try_transfer_from( - deps, - &env, - info, - owner, - recipient, - amount, - memo, - decoys, - account_random_pos, + } => execute_transfer_send::try_transfer_from( + deps, &env, info, &mut rng, owner, recipient, amount, memo, ), ExecuteMsg::SendFrom { owner, @@ -298,45 +312,33 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S amount, msg, memo, - decoys, .. - } => try_send_from( + } => execute_transfer_send::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) + execute_transfer_send::try_batch_transfer_from(deps, &env, info, &mut rng, actions) } ExecuteMsg::BatchSendFrom { actions, .. } => { - try_batch_send_from(deps, env, &info, actions, account_random_pos) + execute_transfer_send::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, - ), + } => execute_mint_burn::try_burn_from(deps, &env, info, owner, amount, memo), ExecuteMsg::BatchBurnFrom { actions, .. } => { - try_batch_burn_from(deps, &env, info, actions, account_random_pos) + execute_mint_burn::try_batch_burn_from(deps, &env, info, actions) } // Mint @@ -344,65 +346,111 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S recipient, amount, memo, - decoys, .. - } => try_mint( - deps, - env, - info, - recipient, - amount, - memo, - decoys, - account_random_pos, - ), + } => execute_mint_burn::try_mint(deps, env, info, &mut rng, recipient, amount, memo), ExecuteMsg::BatchMint { actions, .. } => { - try_batch_mint(deps, env, info, actions, account_random_pos) + execute_mint_burn::try_batch_mint(deps, env, info, &mut rng, actions) } - // Other - ExecuteMsg::ChangeAdmin { address, .. } => change_admin(deps, info, address), - ExecuteMsg::SetContractStatus { level, .. } => set_contract_status(deps, info, level), - ExecuteMsg::AddMinters { minters, .. } => add_minters(deps, info, minters), - ExecuteMsg::RemoveMinters { minters, .. } => remove_minters(deps, info, minters), - ExecuteMsg::SetMinters { minters, .. } => set_minters(deps, info, minters), - ExecuteMsg::RevokePermit { permit_name, .. } => revoke_permit(deps, info, permit_name), - ExecuteMsg::AddSupportedDenoms { denoms, .. } => add_supported_denoms(deps, info, denoms), - ExecuteMsg::RemoveSupportedDenoms { denoms, .. } => { - remove_supported_denoms(deps, info, denoms) + // SNIP-24 + ExecuteMsg::RevokePermit { permit_name, .. } => { + execute::revoke_permit(deps, info, permit_name) + } + + // SNIP-24.1 + ExecuteMsg::RevokeAllPermits { interval, .. } => { + execute::revoke_all_permits(deps, info, interval) } + ExecuteMsg::DeletePermitRevocation { revocation_id, .. } => { + execute::delete_permit_revocation(deps, info, revocation_id) + } + + // Admin functions + _ => admin_execute(deps, info, msg), }; - 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 +} + +pub fn admin_execute(deps: DepsMut, info: MessageInfo, msg: ExecuteMsg) -> StdResult { + // load contract config from storage + let mut config = CONFIG.load(deps.storage)?; + + // check that message sender is the admin + if config.admin != info.sender { + return Err(StdError::generic_err( + "This is an admin command. Admin commands can only be run from admin address", + )); + } + + match msg { + ExecuteMsg::ChangeAdmin { address, .. } => { + execute_admin::change_admin(deps, &mut config, address) + } + ExecuteMsg::SetContractStatus { level, .. } => { + execute_admin::set_contract_status(deps, level) + } + ExecuteMsg::AddMinters { minters, .. } => { + execute_admin::add_minters(deps, &config, minters) + } + ExecuteMsg::RemoveMinters { minters, .. } => { + execute_admin::remove_minters(deps, &config, minters) + } + ExecuteMsg::SetMinters { minters, .. } => { + execute_admin::set_minters(deps, &config, minters) + } + ExecuteMsg::AddSupportedDenoms { denoms, .. } => { + execute_admin::add_supported_denoms(deps, &mut config, denoms) + } + ExecuteMsg::RemoveSupportedDenoms { denoms, .. } => { + execute_admin::remove_supported_denoms(deps, &mut config, denoms) + } + + // SNIP-52 + ExecuteMsg::SetNotificationStatus { enabled, .. } => { + execute_admin::set_notification_status(deps, enabled) + } + _ => panic!("This execute type is not an admin function"), + } } #[entry_point] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { pad_query_result( match msg { - QueryMsg::TokenInfo {} => query_token_info(deps.storage), - QueryMsg::TokenConfig {} => query_token_config(deps.storage), - 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::TokenInfo {} => query::query_token_info(deps.storage), + QueryMsg::TokenConfig {} => query::query_token_config(deps.storage), + QueryMsg::ContractStatus {} => query::query_contract_status(deps.storage), + QueryMsg::ExchangeRate {} => query::query_exchange_rate(deps.storage), + QueryMsg::Minters { .. } => query::query_minters(deps), + QueryMsg::ListChannels {} => query::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 { +fn permit_queries( + deps: Deps, + env: Env, + permit: Permit, + query: QueryWithPermit, +) -> Result { // Validate permit content let token_address = CONFIG.load(deps.storage)?.contract_address; - let account = secret_toolkit::permit::validate( - deps, - PREFIX_REVOKED_PERMITS, - &permit, - token_address.into_string(), - None, - )?; + let account = + secret_toolkit::permit::validate(deps, &env, &permit, token_address.into_string(), None)?; // Permit validated! We can now execute the query. match query { @@ -414,33 +462,12 @@ fn permit_queries(deps: Deps, permit: Permit, query: QueryWithPermit) -> Result< ))); } - query_balance(deps, account) + query::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 { .. } => { + 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 +475,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::query_transactions(deps, account, page.unwrap_or(0), page_size) } QueryWithPermit::Allowance { owner, spender } => { if !permit.check_permission(&TokenPermissions::Allowance) { @@ -471,7 +492,7 @@ fn permit_queries(deps: Deps, permit: Permit, query: QueryWithPermit) -> Result< ))); } - query_allowance(deps, owner, spender) + query::query_allowance(deps, owner, spender) } QueryWithPermit::AllowancesGiven { owner, @@ -494,7 +515,7 @@ fn permit_queries(deps: Deps, permit: Permit, query: QueryWithPermit) -> Result< permit.params.permissions ))); } - query_allowances_given(deps, account, page.unwrap_or(0), page_size) + query::query_allowances_given(deps, account, page.unwrap_or(0), page_size) } QueryWithPermit::AllowancesReceived { spender, @@ -515,12 +536,28 @@ fn permit_queries(deps: Deps, permit: Permit, query: QueryWithPermit) -> Result< permit.params.permissions ))); } - query_allowances_received(deps, account, page.unwrap_or(0), page_size) + query::query_allowances_received(deps, account, page.unwrap_or(0), page_size) + } + QueryWithPermit::ChannelInfo { channels, txhash } => query::query_channel_info( + deps, + env, + channels, + txhash, + deps.api.addr_canonicalize(account.as_str())?, + ), + QueryWithPermit::ListPermitRevocations { .. } => { + if !permit.check_permission(&TokenPermissions::Owner) { + return Err(StdError::generic_err(format!( + "No permission to query list permit revocations, got permissions {:?}", + permit.params.permissions + ))); + } + query::query_list_permit_revocations(deps, account.as_str()) } } } -pub fn viewing_keys_queries(deps: Deps, msg: QueryMsg) -> StdResult { +pub fn viewing_keys_queries(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { let (addresses, key) = msg.get_validation_params(deps.api)?; for address in addresses { @@ -528,46 +565,45 @@ pub fn viewing_keys_queries(deps: Deps, msg: QueryMsg) -> StdResult { if result.is_ok() { 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::Balance { address, .. } => query::query_balance(deps, address), + 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, - ), - QueryMsg::Allowance { owner, spender, .. } => query_allowance(deps, owner, spender), + } => query::query_transactions(deps, address, page.unwrap_or(0), page_size), + QueryMsg::Allowance { owner, spender, .. } => { + query::query_allowance(deps, owner, spender) + } QueryMsg::AllowancesGiven { owner, page, page_size, .. - } => query_allowances_given(deps, owner, page.unwrap_or(0), page_size), + } => query::query_allowances_given(deps, owner, page.unwrap_or(0), page_size), QueryMsg::AllowancesReceived { spender, page, page_size, .. - } => query_allowances_received(deps, spender, page.unwrap_or(0), page_size), + } => query::query_allowances_received(deps, spender, page.unwrap_or(0), page_size), + QueryMsg::ChannelInfo { + channels, + txhash, + viewer, + } => query::query_channel_info( + deps, + env, + channels, + txhash, + deps.api.addr_canonicalize(viewer.address.as_str())?, + ), + QueryMsg::ListPermitRevocations { viewer, .. } => { + query::query_list_permit_revocations(deps, viewer.address.as_str()) + } _ => panic!("This query type does not require authentication"), }; } @@ -578,2393 +614,1045 @@ pub fn viewing_keys_queries(deps: Deps, msg: QueryMsg) -> StdResult { }) } -fn query_exchange_rate(storage: &dyn Storage) -> StdResult { - let constants = CONFIG.load(storage)?; - - if constants.deposit_is_enabled || constants.redeem_is_enabled { - let rate: Uint128; - let denom: String; - // if token has more decimals than SCRT, you get magnitudes of SCRT per token - if constants.decimals >= 6 { - rate = Uint128::new(10u128.pow(constants.decimals as u32 - 6)); - denom = "SCRT".to_string(); - // if token has less decimals, you get magnitudes token for SCRT - } else { - rate = Uint128::new(10u128.pow(6 - constants.decimals as u32)); - denom = constants.symbol; - } - return to_binary(&QueryAnswer::ExchangeRate { rate, denom }); - } - to_binary(&QueryAnswer::ExchangeRate { - rate: Uint128::zero(), - denom: String::new(), - }) -} - -fn query_token_info(storage: &dyn Storage) -> StdResult { - let constants = CONFIG.load(storage)?; - - let total_supply = if constants.total_supply_is_public { - Some(Uint128::new(TOTAL_SUPPLY.load(storage)?)) - } else { - None - }; - - to_binary(&QueryAnswer::TokenInfo { - name: constants.name, - symbol: constants.symbol, - decimals: constants.decimals, - total_supply, - }) -} +// pub fn migrate( +// _deps: DepsMut, +// _env: Env, +// _msg: MigrateMsg, +// ) -> StdResult { +// Ok(MigrateResponse::default()) +// Ok(MigrateResponse::default()) +// } -fn query_token_config(storage: &dyn Storage) -> StdResult { - let constants = CONFIG.load(storage)?; +// helper functions - to_binary(&QueryAnswer::TokenConfig { - public_total_supply: constants.total_supply_is_public, - deposit_enabled: constants.deposit_is_enabled, - redeem_enabled: constants.redeem_is_enabled, - mint_enabled: constants.mint_is_enabled, - burn_enabled: constants.burn_is_enabled, - supported_denoms: constants.supported_denoms, - }) +fn is_valid_name(name: &str) -> bool { + let len = name.len(); + (3..=30).contains(&len) } -fn query_contract_status(storage: &dyn Storage) -> StdResult { - let contract_status = CONTRACT_STATUS.load(storage)?; +fn is_valid_symbol(symbol: &str) -> bool { + let len = symbol.len(); + let len_is_valid = (3..=20).contains(&len); - to_binary(&QueryAnswer::ContractStatus { - status: contract_status, - }) + len_is_valid && symbol.bytes().all(|byte| byte.is_ascii_alphabetic()) } -pub fn query_transfers( - deps: Deps, - account: String, - page: u32, - page_size: u32, - should_filter_decoys: bool, -) -> StdResult { - // 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, - )?; +#[cfg(test)] +mod tests { + use std::any::Any; - let result = QueryAnswer::TransferHistory { - txs, - total: Some(total), + use cosmwasm_std::{ + from_binary, testing::*, Addr, Api, BlockInfo, Coin, ContractInfo, CosmosMsg, MessageInfo, + OwnedDeps, QueryResponse, ReplyOn, SubMsg, Timestamp, TransactionInfo, Uint128, WasmMsg, }; - to_binary(&result) -} + use secret_toolkit::permit::{PermitParams, PermitSignature, PubKey}; -pub fn query_transactions( - deps: Deps, - account: String, - page: u32, - page_size: u32, - should_filter_decoys: bool, -) -> StdResult { - // Notice that if query_transactions() 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_transactions() was called by a - // permit call, for compatibility with non-Secret addresses. - let account = Addr::unchecked(account); - - let (txs, total) = - StoredExtendedTx::get_txs(deps.storage, account, page, page_size, should_filter_decoys)?; - - let result = QueryAnswer::TransactionHistory { - txs, - total: Some(total), + use crate::batch; + use crate::btbe::stored_balance; + use crate::dwb::{TX_NODES, TX_NODES_COUNT}; + use crate::msg::{ + ExecuteAnswer, InitConfig, InitialBalance, ResponseStatus, ResponseStatus::Success, }; - to_binary(&result) -} - -pub fn query_balance(deps: Deps, account: String) -> StdResult { - // Notice that if query_balance() 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_balance() was called by a permit - // call, for compatibility with non-Secret addresses. - let account = Addr::unchecked(account); - - let amount = Uint128::new(BalancesStore::load(deps.storage, &account)); - let response = QueryAnswer::Balance { amount }; - to_binary(&response) -} - -fn query_minters(deps: Deps) -> StdResult { - let minters = MintersStore::load(deps.storage)?; - - let response = QueryAnswer::Minters { minters }; - to_binary(&response) -} - -fn change_admin(deps: DepsMut, info: MessageInfo, address: String) -> StdResult { - let address = deps.api.addr_validate(address.as_str())?; - - let mut constants = CONFIG.load(deps.storage)?; - check_if_admin(&constants.admin, &info.sender)?; - - constants.admin = address; - CONFIG.save(deps.storage, &constants)?; + use crate::receiver::Snip20ReceiveMsg; + use crate::state::{AllowancesStore, ReceiverHashStore, TX_COUNT}; + use crate::transaction_history::{Tx, TxAction}; - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::ChangeAdmin { status: Success })?)) -} - -fn add_supported_denoms( - deps: DepsMut, - info: MessageInfo, - denoms: Vec, -) -> StdResult { - let mut config = CONFIG.load(deps.storage)?; - - check_if_admin(&config.admin, &info.sender)?; - if !config.can_modify_denoms { - return Err(StdError::generic_err( - "Cannot modify denoms for this contract", - )); - } + use super::*; - for denom in denoms.iter() { - if !config.supported_denoms.contains(denom) { - config.supported_denoms.push(denom.clone()); - } - } + pub const VIEWING_KEY_SIZE: usize = 32; - CONFIG.save(deps.storage, &config)?; + // Helper functions - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::AddSupportedDenoms { - status: Success, - })?), - ) -} + fn init_helper( + initial_balances: Vec, + ) -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies_with_balance(&[]); + let env = mock_env(); + let info = mock_info("instantiator", &[]); -fn remove_supported_denoms( - deps: DepsMut, - info: MessageInfo, - denoms: Vec, -) -> StdResult { - let mut config = CONFIG.load(deps.storage)?; + 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, + }; - check_if_admin(&config.admin, &info.sender)?; - if !config.can_modify_denoms { - return Err(StdError::generic_err( - "Cannot modify denoms for this contract", - )); + (instantiate(deps.as_mut(), env, info, init_msg), deps) } - for denom in denoms.iter() { - config.supported_denoms.retain(|x| x != denom); - } + fn init_helper_with_config( + initial_balances: Vec, + enable_deposit: bool, + enable_redeem: bool, + enable_mint: bool, + enable_burn: bool, + contract_bal: u128, + supported_denoms: Vec, + ) -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies_with_balance(&[Coin { + denom: "uscrt".to_string(), + amount: Uint128::new(contract_bal), + }]); - CONFIG.save(deps.storage, &config)?; + let env = mock_env(); + let info = mock_info("instantiator", &[]); - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::RemoveSupportedDenoms { - status: Success, - })?), - ) -} - -#[allow(clippy::too_many_arguments)] -fn try_mint_impl( - deps: &mut DepsMut, - minter: Addr, - recipient: Addr, - amount: Uint128, - denom: String, - memo: Option, - block: &cosmwasm_std::BlockInfo, - decoys: Option>, - account_random_pos: Option, -) -> StdResult<()> { - let raw_amount = amount.u128(); - - BalancesStore::update_balance( - deps.storage, - &recipient, - raw_amount, - true, - "", - &decoys, - &account_random_pos, - )?; - - store_mint( - deps.storage, - minter, - recipient, - amount, - denom, - memo, - block, - &decoys, - &account_random_pos, - )?; + let init_config: InitConfig = from_binary(&Binary::from( + format!( + "{{\"public_total_supply\":false, + \"enable_deposit\":{}, + \"enable_redeem\":{}, + \"enable_mint\":{}, + \"enable_burn\":{}}}", + enable_deposit, enable_redeem, enable_mint, enable_burn + ) + .as_bytes(), + )) + .unwrap(); + 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: Some(init_config), + supported_denoms: Some(supported_denoms), + }; - Ok(()) -} + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } -#[allow(clippy::too_many_arguments)] -fn try_mint( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - recipient: String, - amount: Uint128, - memo: Option, - decoys: Option>, - account_random_pos: Option, -) -> StdResult { - let recipient = deps.api.addr_validate(recipient.as_str())?; + fn extract_error_msg(error: StdResult) -> String { + match error { + Ok(response) => { + let bin_err = (&response as &dyn Any) + .downcast_ref::() + .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"), + }, + } + } - let constants = CONFIG.load(deps.storage)?; + fn ensure_success(handle_result: Response) -> bool { + let handle_result: ExecuteAnswer = from_binary(&handle_result.data.unwrap()).unwrap(); - if !constants.mint_is_enabled { - return Err(StdError::generic_err( - "Mint functionality is not enabled for this token.", - )); + match handle_result { + ExecuteAnswer::Deposit { status } + | ExecuteAnswer::Redeem { status } + | ExecuteAnswer::Transfer { status } + | ExecuteAnswer::Send { status } + | ExecuteAnswer::Burn { status } + | ExecuteAnswer::RegisterReceive { status } + | ExecuteAnswer::SetViewingKey { status } + | ExecuteAnswer::TransferFrom { status } + | ExecuteAnswer::SendFrom { status } + | ExecuteAnswer::BurnFrom { status } + | ExecuteAnswer::Mint { status } + | ExecuteAnswer::ChangeAdmin { status } + | ExecuteAnswer::SetContractStatus { status } + | ExecuteAnswer::SetMinters { status } + | ExecuteAnswer::AddMinters { status } + | ExecuteAnswer::RemoveMinters { status } => { + matches!(status, ResponseStatus::Success { .. }) + } + _ => panic!( + "HandleAnswer not supported for success extraction: {:?}", + handle_result + ), + } } - let minters = MintersStore::load(deps.storage)?; - if !minters.contains(&info.sender) { - return Err(StdError::generic_err( - "Minting is allowed to minter accounts only", - )); - } + /// creates a cosmos_msg sending this struct to the named contract + pub fn into_cosmos_submsg( + msg: Snip20ReceiveMsg, + code_hash: String, + contract_addr: Addr, + id: u64, + ) -> StdResult { + let msg = msg.into_binary()?; + let execute = SubMsg { + id, + msg: WasmMsg::Execute { + contract_addr: contract_addr.into_string(), + code_hash, + msg, + funds: vec![], + } + .into(), + // TODO: Discuss the wanted behavior + reply_on: match id { + 0 => ReplyOn::Never, + _ => ReplyOn::Always, + }, + gas_limit: None, + }; - let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; - let minted_amount = safe_add(&mut total_supply, amount.u128()); - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + Ok(execute) + } - // 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, - info.sender, - recipient, - Uint128::new(minted_amount), - constants.symbol, - memo, - &env.block, - decoys, - account_random_pos, - )?; + // Init tests - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::Mint { status: Success })?)) -} + #[test] + fn test_init_sanity() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "lebron".to_string(), + amount: Uint128::new(5000), + }]); + assert_eq!(init_result.unwrap(), Response::default()); -fn try_batch_mint( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - actions: Vec, - account_random_pos: Option, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; + let constants = CONFIG.load(&deps.storage).unwrap(); + assert_eq!(TOTAL_SUPPLY.load(&deps.storage).unwrap(), 5000); + assert_eq!( + CONTRACT_STATUS.load(&deps.storage).unwrap(), + ContractStatusLevel::NormalRun + ); + assert_eq!(constants.name, "sec-sec".to_string()); + assert_eq!(constants.admin, Addr::unchecked("admin".to_string())); + assert_eq!(constants.symbol, "SECSEC".to_string()); + assert_eq!(constants.decimals, 8); + assert_eq!(constants.total_supply_is_public, false); - if !constants.mint_is_enabled { - return Err(StdError::generic_err( - "Mint functionality is not enabled for this token.", - )); + ViewingKey::set(deps.as_mut().storage, "lebron", "lolz fun yay"); + let is_vk_correct = ViewingKey::check(&deps.storage, "lebron", "lolz fun yay"); + assert!( + is_vk_correct.is_ok(), + "Viewing key verification failed!: {}", + is_vk_correct.err().unwrap() + ); } - let minters = MintersStore::load(deps.storage)?; - if !minters.contains(&info.sender) { - return Err(StdError::generic_err( - "Minting is allowed to minter accounts only", - )); - } + #[test] + fn test_init_with_config_sanity() { + let (init_result, mut deps) = init_helper_with_config( + vec![InitialBalance { + address: "lebron".to_string(), + amount: Uint128::new(5000), + }], + true, + true, + true, + true, + 0, + vec!["uscrt".to_string()], + ); + assert_eq!(init_result.unwrap(), Response::default()); - let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; + let constants = CONFIG.load(&deps.storage).unwrap(); + assert_eq!(TOTAL_SUPPLY.load(&deps.storage).unwrap(), 5000); + assert_eq!( + CONTRACT_STATUS.load(&deps.storage).unwrap(), + ContractStatusLevel::NormalRun + ); + assert_eq!(constants.name, "sec-sec".to_string()); + assert_eq!(constants.admin, Addr::unchecked("admin".to_string())); + assert_eq!(constants.symbol, "SECSEC".to_string()); + assert_eq!(constants.decimals, 8); + assert_eq!(constants.total_supply_is_public, false); + assert_eq!(constants.deposit_is_enabled, true); + assert_eq!(constants.redeem_is_enabled, true); + assert_eq!(constants.mint_is_enabled, true); + assert_eq!(constants.burn_is_enabled, true); - // 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()); + ViewingKey::set(deps.as_mut().storage, "lebron", "lolz fun yay"); + let is_vk_correct = ViewingKey::check(&deps.storage, "lebron", "lolz fun yay"); + assert!( + is_vk_correct.is_ok(), + "Viewing key verification failed!: {}", + is_vk_correct.err().unwrap() + ); + } - let recipient = deps.api.addr_validate(action.recipient.as_str())?; - try_mint_impl( - &mut deps, - info.sender.clone(), - recipient, - Uint128::new(actual_amount), - constants.symbol.clone(), - action.memo, - &env.block, - action.decoys, - account_random_pos, - )?; + #[test] + 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(u64::max_value().into()), + }]); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); } - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + // Handle tests - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::BatchMint { status: Success })?)) -} + #[test] + fn test_execute_transfer_dwb() { + 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() + ); -pub fn try_set_key(deps: DepsMut, info: MessageInfo, key: String) -> StdResult { - ViewingKey::set(deps.storage, info.sender.as_str(), key.as_str()); - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::SetViewingKey { - status: Success, - })?), - ) -} + 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 -pub fn try_create_key( - deps: DepsMut, - env: Env, - info: MessageInfo, - entropy: String, -) -> StdResult { - let key = ViewingKey::create( - deps.storage, - &info, - &env, - info.sender.as_str(), - entropy.as_ref(), - ); + let handle_msg = ExecuteMsg::Transfer { + recipient: "alice".to_string(), + amount: Uint128::new(1000), + 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(&[0u8; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::CreateViewingKey { key })?)) -} + let result = handle_result.unwrap(); + assert!(ensure_success(result)); + 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(); -fn set_contract_status( - deps: DepsMut, - info: MessageInfo, - status_level: ContractStatusLevel, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - check_if_admin(&constants.admin, &info.sender)?; + 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: "charlie".to_string(), + amount: Uint128::new(100), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("bob", &[]); - CONTRACT_STATUS.save(deps.storage, &status_level)?; + let mut env = mock_env(); + env.block.random = Some(Binary::from(&[1u8; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::SetContractStatus { - status: Success, - })?), - ) -} + let result = handle_result.unwrap(); + assert!(ensure_success(result)); + let charlie_addr = deps + .api + .addr_canonicalize(Addr::unchecked("charlie").as_str()) + .unwrap(); -pub fn query_allowance(deps: Deps, owner: String, spender: String) -> StdResult { - // Notice that if query_allowance() was called by a viewing-key call, the addresses of 'owner' - // and 'spender' have already been validated. - // The addresses of 'owner' and 'spender' should not be validated if query_allowance() was - // called by a permit call, for compatibility with non-Secret addresses. - let owner = Addr::unchecked(owner); - let spender = Addr::unchecked(spender); - - let allowance = AllowancesStore::load(deps.storage, &owner, &spender); - - let response = QueryAnswer::Allowance { - owner, - spender, - allowance: Uint128::new(allowance.amount), - expiration: allowance.expiration, - }; - to_binary(&response) -} + 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(500), + 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(&[2u8; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); -pub fn query_allowances_given( - deps: Deps, - owner: String, - page: u32, - page_size: u32, -) -> StdResult { - // Notice that if query_all_allowances_given() was called by a viewing-key call, - // the address of 'owner' has already been validated. - // The addresses of 'owner' should not be validated if query_all_allowances_given() was - // called by a permit call, for compatibility with non-Secret addresses. - let owner = Addr::unchecked(owner); - - let all_allowances = - AllowancesStore::all_allowances(deps.storage, &owner, page, page_size).unwrap_or(vec![]); - - let allowances_result = all_allowances - .into_iter() - .map(|(spender, allowance)| AllowanceGivenResult { - spender, - allowance: Uint128::from(allowance.amount), - expiration: allowance.expiration, - }) - .collect(); - - let response = QueryAnswer::AllowancesGiven { - owner: owner.clone(), - allowances: allowances_result, - count: AllowancesStore::num_allowances(deps.storage, &owner), - }; - to_binary(&response) -} + let result = handle_result.unwrap(); + assert!(ensure_success(result)); -pub fn query_allowances_received( - deps: Deps, - spender: String, - page: u32, - page_size: u32, -) -> StdResult { - // Notice that if query_all_allowances_received() was called by a viewing-key call, - // the address of 'spender' has already been validated. - // The addresses of 'spender' should not be validated if query_all_allowances_received() was - // called by a permit call, for compatibility with non-Secret addresses. - let spender = Addr::unchecked(spender); - - let all_allowed = - AllowancesStore::all_allowed(deps.storage, &spender, page, page_size).unwrap_or(vec![]); - - let allowances = all_allowed - .into_iter() - .map(|(owner, allowance)| AllowanceReceivedResult { - owner, - allowance: Uint128::from(allowance.amount), - expiration: allowance.expiration, - }) - .collect(); - - let response = QueryAnswer::AllowancesReceived { - spender: spender.clone(), - allowances, - count: AllowancesStore::num_allowed(deps.storage, &spender), - }; - to_binary(&response) -} - -fn try_deposit( - deps: DepsMut, - env: Env, - info: MessageInfo, - decoys: Option>, - account_random_pos: Option, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - - let mut amount = Uint128::zero(); - - for coin in &info.funds { - if constants.supported_denoms.contains(&coin.denom) { - amount += coin.amount - } else { - return Err(StdError::generic_err(format!( - "Tried to deposit an unsupported coin {}", - coin.denom - ))); - } - } - - if amount.is_zero() { - return Err(StdError::generic_err("No funds were sent to be deposited")); - } - - let mut raw_amount = amount.u128(); - - if !constants.deposit_is_enabled { - return Err(StdError::generic_err( - "Deposit functionality is not enabled.", - )); - } - - let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; - raw_amount = safe_add(&mut total_supply, raw_amount); - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - - let sender_address = &info.sender; - - BalancesStore::update_balance( - deps.storage, - sender_address, - raw_amount, - true, - "", - &decoys, - &account_random_pos, - )?; - - store_deposit( - deps.storage, - sender_address, - Uint128::new(raw_amount), - "uscrt".to_string(), - &env.block, - &decoys, - &account_random_pos, - )?; - - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::Deposit { status: Success })?)) -} - -fn try_redeem( - deps: DepsMut, - env: Env, - info: MessageInfo, - amount: Uint128, - denom: Option, - decoys: Option>, - account_random_pos: Option, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - if !constants.redeem_is_enabled { - return Err(StdError::generic_err( - "Redeem functionality is not enabled for this token.", - )); - } - - // if denom is none and there is only 1 supported denom then we don't need to check anything - let withdraw_denom = if denom.is_none() && constants.supported_denoms.len() == 1 { - constants.supported_denoms.first().unwrap().clone() - // if denom is specified make sure it's on the list before trying to withdraw with it - } else if denom.is_some() && constants.supported_denoms.contains(denom.as_ref().unwrap()) { - denom.unwrap() - // error handling - } else if denom.is_none() { - return Err(StdError::generic_err( - "Tried to redeem without specifying denom, but multiple coins are supported", - )); - } else { - return Err(StdError::generic_err( - "Tried to redeem for an unsupported coin", - )); - }; - - let sender_address = &info.sender; - let amount_raw = amount.u128(); - - BalancesStore::update_balance( - deps.storage, - sender_address, - amount_raw, - false, - "redeem", - &decoys, - &account_random_pos, - )?; - - 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)?; - } else { - return Err(StdError::generic_err( - "You are trying to redeem more tokens than what is available in the total supply", - )); - } - - let token_reserve = deps - .querier - .query_balance(&env.contract.address, &withdraw_denom)? - .amount; - if amount > token_reserve { - return Err(StdError::generic_err(format!( - "You are trying to redeem for more {withdraw_denom} than the contract has in its reserve", - ))); - } - - let withdrawal_coins: Vec = vec![Coin { - denom: withdraw_denom, - 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, - }); - let data = to_binary(&ExecuteAnswer::Redeem { status: Success })?; - let res = Response::new().add_message(message).set_data(data); - Ok(res) -} - -#[allow(clippy::too_many_arguments)] -fn try_transfer_impl( - deps: &mut DepsMut, - sender: &Addr, - recipient: &Addr, - amount: Uint128, - memo: Option, - block: &cosmwasm_std::BlockInfo, - decoys: Option>, - account_random_pos: Option, -) -> StdResult<()> { - perform_transfer( - deps.storage, - sender, - recipient, - amount.u128(), - &decoys, - &account_random_pos, - )?; - - let symbol = CONFIG.load(deps.storage)?.symbol; - store_transfer( - deps.storage, - sender, - sender, - recipient, - amount, - symbol, - memo, - block, - &decoys, - &account_random_pos, - )?; - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn try_transfer( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - recipient: String, - amount: Uint128, - memo: Option, - decoys: Option>, - account_random_pos: Option, -) -> StdResult { - let recipient = deps.api.addr_validate(recipient.as_str())?; - - try_transfer_impl( - &mut deps, - &info.sender, - &recipient, - amount, - memo, - &env.block, - decoys, - account_random_pos, - )?; - - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::Transfer { status: Success })?)) -} + assert_eq!( + 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() + .as_vec(&deps.storage, &deps.api) + .unwrap(); -fn try_batch_transfer( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - actions: Vec, - account_random_pos: Option, -) -> StdResult { - for action in actions { - let recipient = deps.api.addr_validate(action.recipient.as_str())?; - try_transfer_impl( - &mut deps, - &info.sender, - &recipient, - action.amount, - action.memo, - &env.block, - action.decoys, - account_random_pos, - )?; - } + let expected_alice_nodes: Vec = 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); - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::BatchTransfer { - status: Success, - })?), - ) -} + // 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("bob", &[]); -#[allow(clippy::too_many_arguments)] -fn try_add_receiver_api_callback( - storage: &dyn Storage, - messages: &mut Vec, - recipient: Addr, - recipient_code_hash: Option, - msg: Option, - sender: Addr, - from: Addr, - amount: Uint128, - memo: Option, -) -> StdResult<()> { - if let Some(receiver_hash) = recipient_code_hash { - let receiver_msg = Snip20ReceiveMsg::new(sender, from, amount, memo, msg); - let callback_msg = receiver_msg.into_cosmos_msg(receiver_hash, recipient)?; - - messages.push(callback_msg); - return Ok(()); - } + 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 receiver_hash = ReceiverHashStore::may_load(storage, &recipient)?; - if let Some(receiver_hash) = receiver_hash { - let receiver_msg = Snip20ReceiveMsg::new(sender, from, amount, memo, msg); - let callback_msg = receiver_msg.into_cosmos_msg(receiver_hash, recipient)?; + let result = handle_result.unwrap(); + assert!(ensure_success(result)); + let ernie_addr = deps + .api + .addr_canonicalize(Addr::unchecked("ernie").as_str()) + .unwrap(); - messages.push(callback_msg); - } - Ok(()) -} + 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, + }; + 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); -#[allow(clippy::too_many_arguments)] -fn try_send_impl( - deps: &mut DepsMut, - messages: &mut Vec, - sender: Addr, - recipient: Addr, - recipient_code_hash: Option, - amount: Uint128, - memo: Option, - msg: Option, - block: &cosmwasm_std::BlockInfo, - decoys: Option>, - account_random_pos: Option, -) -> StdResult<()> { - try_transfer_impl( - deps, - &sender, - &recipient, - amount, - memo.clone(), - block, - decoys, - account_random_pos, - )?; + let result = handle_result.unwrap(); + assert!(ensure_success(result)); + let dora_addr = deps + .api + .addr_canonicalize(Addr::unchecked("dora").as_str()) + .unwrap(); - try_add_receiver_api_callback( - deps.storage, - messages, - recipient, - recipient_code_hash, - msg, - sender.clone(), - sender, - amount, - memo, - )?; + // 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); - Ok(()) -} + let result = handle_result.unwrap(); + assert!(ensure_success(result)); + } + assert_eq!( + 5000 - 1000 - 100 - 500 - 200 - 59, + stored_balance(&deps.storage, &bob_addr).unwrap() + ); -#[allow(clippy::too_many_arguments)] -fn try_send( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - recipient: String, - recipient_code_hash: Option, - amount: Uint128, - memo: Option, - msg: Option, - decoys: Option>, - account_random_pos: Option, -) -> StdResult { - let recipient = deps.api.addr_validate(recipient.as_str())?; - - let mut messages = vec![]; - try_send_impl( - &mut deps, - &mut messages, - info.sender, - recipient, - recipient_code_hash, - amount, - memo, - msg, - &env.block, - decoys, - account_random_pos, - )?; + let dwb = DWB.load(&deps.storage).unwrap(); + //println!("DWB: {dwb:?}"); - Ok(Response::new() - .add_messages(messages) - .set_data(to_binary(&ExecuteAnswer::Send { status: Success })?)) -} + // assert we have filled the buffer + assert_eq!(0, dwb.empty_space_counter); -fn try_batch_send( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - actions: Vec, - account_random_pos: Option, -) -> StdResult { - let mut messages = vec![]; - for action in actions { - let recipient = deps.api.addr_validate(action.recipient.as_str())?; - try_send_impl( - &mut deps, - &mut messages, - info.sender.clone(), + let recipient = format!("receipient_over"); + // now send 1 to recipient from bob + let handle_msg = ExecuteMsg::Transfer { recipient, - action.recipient_code_hash, - action.amount, - action.memo, - action.msg, - &env.block, - action.decoys, - account_random_pos, - )?; - } + 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(&[50; 32])); + let handle_result = execute(deps.as_mut(), env, info, handle_msg); - Ok(Response::new() - .add_messages(messages) - .set_data(to_binary(&ExecuteAnswer::BatchSend { status: Success })?)) -} + let result = handle_result.unwrap(); + assert!(ensure_success(result)); -fn try_register_receive( - deps: DepsMut, - info: MessageInfo, - code_hash: String, -) -> StdResult { - ReceiverHashStore::save(deps.storage, &info.sender, code_hash)?; + assert_eq!( + 5000 - 1000 - 100 - 500 - 200 - 59 - 1, + stored_balance(&deps.storage, &bob_addr).unwrap() + ); - let data = to_binary(&ExecuteAnswer::RegisterReceive { status: Success })?; - Ok(Response::new() - .add_attribute("register_status", "success") - .set_data(data)) -} + //let dwb = DWB.load(&deps.storage).unwrap(); + //println!("DWB: {dwb:?}"); -fn insufficient_allowance(allowance: u128, required: u128) -> StdError { - StdError::generic_err(format!( - "insufficient allowance: allowance={allowance}, required={required}", - )) -} + 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); -fn use_allowance( - storage: &mut dyn Storage, - env: &Env, - owner: &Addr, - spender: &Addr, - amount: u128, -) -> StdResult<()> { - let mut allowance = AllowancesStore::load(storage, owner, spender); - - if allowance.is_expired_at(&env.block) { - return Err(insufficient_allowance(0, amount)); - } - if let Some(new_allowance) = allowance.amount.checked_sub(amount) { - allowance.amount = new_allowance; - } else { - return Err(insufficient_allowance(allowance.amount, amount)); - } + let result = handle_result.unwrap(); + assert!(ensure_success(result)); - AllowancesStore::save(storage, owner, spender, &allowance)?; + assert_eq!( + 5000 - 1000 - 100 - 500 - 200 - 59 - 1 - 1, + stored_balance(&deps.storage, &bob_addr).unwrap() + ); - Ok(()) -} + //let dwb = DWB.load(&deps.storage).unwrap(); + //println!("DWB: {dwb:?}"); -#[allow(clippy::too_many_arguments)] -fn try_transfer_from_impl( - deps: &mut DepsMut, - env: &Env, - spender: &Addr, - owner: &Addr, - recipient: &Addr, - amount: Uint128, - memo: Option, - decoys: Option>, - account_random_pos: Option, -) -> StdResult<()> { - let raw_amount = amount.u128(); - - use_allowance(deps.storage, env, owner, spender, raw_amount)?; - - perform_transfer( - deps.storage, - owner, - recipient, - raw_amount, - &decoys, - &account_random_pos, - )?; + // 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 symbol = CONFIG.load(deps.storage)?.symbol; - store_transfer( - deps.storage, - owner, - spender, - recipient, - amount, - symbol, - memo, - &env.block, - &decoys, - &account_random_pos, - )?; + 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); - Ok(()) -} + let result = handle_result.unwrap(); + assert!(ensure_success(result)); -#[allow(clippy::too_many_arguments)] -fn try_transfer_from( - mut deps: DepsMut, - env: &Env, - info: MessageInfo, - owner: String, - recipient: String, - amount: Uint128, - memo: Option, - decoys: Option>, - account_random_pos: Option, -) -> StdResult { - let owner = deps.api.addr_validate(owner.as_str())?; - let recipient = deps.api.addr_validate(recipient.as_str())?; - try_transfer_from_impl( - &mut deps, - env, - &info.sender, - &owner, - &recipient, - amount, - memo, - decoys, - account_random_pos, - )?; + // alice should not settle + assert_eq!( + 1500 - 50, + stored_balance(&deps.storage, &alice_addr).unwrap() + ); + } - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::TransferFrom { status: Success })?)) -} + // 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("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); -fn try_batch_transfer_from( - mut deps: DepsMut, - env: &Env, - info: MessageInfo, - actions: Vec, - account_random_pos: Option, -) -> StdResult { - 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( - &mut deps, - env, - &info.sender, - &owner, - &recipient, - action.amount, - action.memo, - action.decoys, - account_random_pos, - )?; - } + let result = handle_result.unwrap(); + assert!(ensure_success(result)); - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::BatchTransferFrom { - status: Success, - })?), - ) -} + assert_eq!(2724, stored_balance(&deps.storage, &alice_addr).unwrap()); -#[allow(clippy::too_many_arguments)] -fn try_send_from_impl( - deps: &mut DepsMut, - env: Env, - info: &MessageInfo, - messages: &mut Vec, - owner: Addr, - recipient: Addr, - recipient_code_hash: Option, - amount: Uint128, - memo: Option, - msg: Option, - decoys: Option>, - account_random_pos: Option, -) -> StdResult<()> { - let spender = info.sender.clone(); - try_transfer_from_impl( - deps, - &env, - &spender, - &owner, - &recipient, - amount, - memo.clone(), - decoys, - account_random_pos, - )?; + // 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, + }; - try_add_receiver_api_callback( - deps.storage, - messages, - recipient, - recipient_code_hash, - msg, - info.sender.clone(), - owner, - amount, - memo, - )?; + 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); - Ok(()) -} + let result = handle_result.unwrap(); + assert!(ensure_success(result)); -#[allow(clippy::too_many_arguments)] -fn try_send_from( - mut deps: DepsMut, - env: Env, - info: &MessageInfo, - owner: String, - recipient: String, - recipient_code_hash: Option, - amount: Uint128, - memo: Option, - msg: Option, - decoys: Option>, - account_random_pos: Option, -) -> StdResult { - 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( - &mut deps, - env, - info, - &mut messages, - owner, - recipient, - recipient_code_hash, - amount, - memo, - msg, - decoys, - account_random_pos, - )?; + // alice should not settle + assert_eq!(2724, stored_balance(&deps.storage, &alice_addr).unwrap()); + } - Ok(Response::new() - .add_messages(messages) - .set_data(to_binary(&ExecuteAnswer::SendFrom { status: Success })?)) -} + let handle_msg = ExecuteMsg::SetViewingKey { + key: "key".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("alice", &[]); -fn try_batch_send_from( - mut deps: DepsMut, - env: Env, - info: &MessageInfo, - actions: Vec, - account_random_pos: Option, -) -> StdResult { - let mut messages = 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( - &mut deps, - env.clone(), - info, - &mut messages, - owner, - recipient, - action.recipient_code_hash, - action.amount, - action.memo, - action.msg, - action.decoys, - account_random_pos, - )?; - } + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let result = handle_result.unwrap(); + assert!(ensure_success(result)); - Ok(Response::new() - .add_messages(messages) - .set_data(to_binary(&ExecuteAnswer::BatchSendFrom { - status: Success, - })?)) -} + // 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)); -#[allow(clippy::too_many_arguments)] -fn try_burn_from( - deps: DepsMut, - env: &Env, - info: MessageInfo, - owner: String, - amount: Uint128, - memo: Option, - decoys: Option>, - account_random_pos: Option, -) -> StdResult { - let owner = deps.api.addr_validate(owner.as_str())?; - let constants = CONFIG.load(deps.storage)?; - if !constants.burn_is_enabled { - return Err(StdError::generic_err( - "Burn functionality is not enabled for this token.", - )); - } + // now we use alice to check query transaction history pagination works - let raw_amount = amount.u128(); - use_allowance(deps.storage, env, &owner, &info.sender, raw_amount)?; - - BalancesStore::update_balance( - deps.storage, - &owner, - raw_amount, - false, - "burn", - &decoys, - &account_random_pos, - )?; - - // remove from supply - 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; - } else { - return Err(StdError::generic_err( - "You're trying to burn more than is available in the total supply", - )); - } - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - - store_burn( - deps.storage, - owner, - info.sender, - amount, - constants.symbol, - memo, - &env.block, - &decoys, - &account_random_pos, - )?; - - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::BurnFrom { status: Success })?)) -} - -fn try_batch_burn_from( - deps: DepsMut, - env: &Env, - info: MessageInfo, - actions: Vec, - account_random_pos: Option, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - if !constants.burn_is_enabled { - return Err(StdError::generic_err( - "Burn functionality is not enabled for this token.", - )); - } - - let spender = info.sender; - let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; - - for action in actions { - let owner = deps.api.addr_validate(action.owner.as_str())?; - let amount = action.amount.u128(); - use_allowance(deps.storage, env, &owner, &spender, amount)?; - - BalancesStore::update_balance( - deps.storage, - &owner, - amount, - false, - "burn", - &action.decoys, - &account_random_pos, - )?; - - // remove from supply - if let Some(new_total_supply) = total_supply.checked_sub(amount) { - total_supply = new_total_supply; - } else { - return Err(StdError::generic_err(format!( - "You're trying to burn more than is available in the total supply: {action:?}", - ))); - } - - store_burn( - deps.storage, - owner, - spender.clone(), - action.amount, - constants.symbol.clone(), - action.memo, - &env.block, - &action.decoys, - &account_random_pos, - )?; - } - - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::BatchBurnFrom { - status: Success, - })?), - ) -} - -fn try_increase_allowance( - deps: DepsMut, - env: Env, - info: MessageInfo, - spender: String, - amount: Uint128, - expiration: Option, -) -> StdResult { - let spender = deps.api.addr_validate(spender.as_str())?; - let mut allowance = AllowancesStore::load(deps.storage, &info.sender, &spender); - - // If the previous allowance has expired, reset the allowance. - // Without this users can take advantage of an expired allowance given to - // them long ago. - if allowance.is_expired_at(&env.block) { - allowance.amount = amount.u128(); - allowance.expiration = None; - } else { - allowance.amount = allowance.amount.saturating_add(amount.u128()); - } - - if expiration.is_some() { - allowance.expiration = expiration; - } - let new_amount = allowance.amount; - AllowancesStore::save(deps.storage, &info.sender, &spender, &allowance)?; - - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::IncreaseAllowance { - owner: info.sender, - spender, - allowance: Uint128::from(new_amount), - })?), - ) -} - -fn try_decrease_allowance( - deps: DepsMut, - env: Env, - info: MessageInfo, - spender: String, - amount: Uint128, - expiration: Option, -) -> StdResult { - let spender = deps.api.addr_validate(spender.as_str())?; - let mut allowance = AllowancesStore::load(deps.storage, &info.sender, &spender); - - // If the previous allowance has expired, reset the allowance. - // Without this users can take advantage of an expired allowance given to - // them long ago. - if allowance.is_expired_at(&env.block) { - allowance.amount = 0; - allowance.expiration = None; - } else { - allowance.amount = allowance.amount.saturating_sub(amount.u128()); - } - - if expiration.is_some() { - allowance.expiration = expiration; - } - let new_amount = allowance.amount; - AllowancesStore::save(deps.storage, &info.sender, &spender, &allowance)?; - - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::DecreaseAllowance { - owner: info.sender, - spender, - allowance: Uint128::from(new_amount), - })?), - ) -} - -fn add_minters( - deps: DepsMut, - info: MessageInfo, - minters_to_add: Vec, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - if !constants.mint_is_enabled { - return Err(StdError::generic_err( - "Mint functionality is not enabled for this token.", - )); - } - - check_if_admin(&constants.admin, &info.sender)?; - - let minters_to_add: Vec = minters_to_add - .iter() - .map(|minter| deps.api.addr_validate(minter.as_str()).unwrap()) - .collect(); - MintersStore::add_minters(deps.storage, minters_to_add)?; - - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::AddMinters { status: Success })?)) -} - -fn remove_minters( - deps: DepsMut, - info: MessageInfo, - minters_to_remove: Vec, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - if !constants.mint_is_enabled { - return Err(StdError::generic_err( - "Mint functionality is not enabled for this token.", - )); - } - - check_if_admin(&constants.admin, &info.sender)?; - - let minters_to_remove: StdResult> = minters_to_remove - .iter() - .map(|minter| deps.api.addr_validate(minter.as_str())) - .collect(); - MintersStore::remove_minters(deps.storage, minters_to_remove?)?; - - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::RemoveMinters { - status: Success, - })?), - ) -} - -fn set_minters( - deps: DepsMut, - info: MessageInfo, - minters_to_set: Vec, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - if !constants.mint_is_enabled { - return Err(StdError::generic_err( - "Mint functionality is not enabled for this token.", - )); - } - - check_if_admin(&constants.admin, &info.sender)?; - - let minters_to_set: Vec = minters_to_set - .iter() - .map(|minter| deps.api.addr_validate(minter.as_str()).unwrap()) - .collect(); - MintersStore::save(deps.storage, minters_to_set)?; - - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::SetMinters { status: Success })?)) -} - -/// Burn tokens -/// -/// Remove `amount` tokens from the system irreversibly, from signer account -/// -/// @param amount the amount of money to burn -fn try_burn( - deps: DepsMut, - env: Env, - info: MessageInfo, - amount: Uint128, - memo: Option, - decoys: Option>, - account_random_pos: Option, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - if !constants.burn_is_enabled { - return Err(StdError::generic_err( - "Burn functionality is not enabled for this token.", - )); - } - - let raw_amount = amount.u128(); - - BalancesStore::update_balance( - deps.storage, - &info.sender, - raw_amount, - false, - "burn", - &decoys, - &account_random_pos, - )?; - - 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; - } else { - return Err(StdError::generic_err( - "You're trying to burn more than is available in the total supply", - )); - } - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - - store_burn( - deps.storage, - info.sender.clone(), - info.sender, - amount, - constants.symbol, - memo, - &env.block, - &decoys, - &account_random_pos, - )?; - - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::Burn { status: Success })?)) -} - -fn perform_transfer( - store: &mut dyn Storage, - from: &Addr, - to: &Addr, - amount: u128, - decoys: &Option>, - account_random_pos: &Option, -) -> StdResult<()> { - BalancesStore::update_balance(store, from, amount, false, "transfer", &None, &None)?; - BalancesStore::update_balance( - store, - to, - amount, - true, - "transfer", - decoys, - account_random_pos, - )?; - - Ok(()) -} - -fn revoke_permit(deps: DepsMut, info: MessageInfo, permit_name: String) -> StdResult { - 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 })?)) -} - -fn check_if_admin(config_admin: &Addr, account: &Addr) -> StdResult<()> { - if config_admin != account { - return Err(StdError::generic_err( - "This is an admin command. Admin commands can only be run from admin address", - )); - } - - Ok(()) -} - -fn is_valid_name(name: &str) -> bool { - let len = name.len(); - (3..=30).contains(&len) -} - -fn is_valid_symbol(symbol: &str) -> bool { - let len = symbol.len(); - let len_is_valid = (3..=20).contains(&len); - - len_is_valid && symbol.bytes().all(|byte| byte.is_ascii_alphabetic()) -} - -// pub fn migrate( -// _deps: DepsMut, -// _env: Env, -// _msg: MigrateMsg, -// ) -> StdResult { -// Ok(MigrateResponse::default()) -// Ok(MigrateResponse::default()) -// } - -#[cfg(test)] -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, - }; - use secret_toolkit::permit::{PermitParams, PermitSignature, PubKey}; - - use crate::msg::ResponseStatus; - use crate::msg::{InitConfig, InitialBalance}; - - use super::*; - - pub const VIEWING_KEY_SIZE: usize = 32; - - // Helper functions - - fn init_helper( - initial_balances: Vec, - ) -> ( - StdResult, - OwnedDeps, - ) { - 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 init_helper_with_config( - initial_balances: Vec, - enable_deposit: bool, - enable_redeem: bool, - enable_mint: bool, - enable_burn: bool, - contract_bal: u128, - supported_denoms: Vec, - ) -> ( - StdResult, - OwnedDeps, - ) { - let mut deps = mock_dependencies_with_balance(&[Coin { - denom: "uscrt".to_string(), - amount: Uint128::new(contract_bal), - }]); - - let env = mock_env(); - let info = mock_info("instantiator", &[]); - - let init_config: InitConfig = from_binary(&Binary::from( - format!( - "{{\"public_total_supply\":false, - \"enable_deposit\":{}, - \"enable_redeem\":{}, - \"enable_mint\":{}, - \"enable_burn\":{}}}", - enable_deposit, enable_redeem, enable_mint, enable_burn - ) - .as_bytes(), - )) - .unwrap(); - 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: Some(init_config), - supported_denoms: Some(supported_denoms), - }; - - (instantiate(deps.as_mut(), env, info, init_msg), deps) - } - - fn extract_error_msg(error: StdResult) -> String { - match error { - Ok(response) => { - let bin_err = (&response as &dyn Any) - .downcast_ref::() - .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"), - }, - } - } - - fn ensure_success(handle_result: Response) -> bool { - let handle_result: ExecuteAnswer = from_binary(&handle_result.data.unwrap()).unwrap(); - - match handle_result { - ExecuteAnswer::Deposit { status } - | ExecuteAnswer::Redeem { status } - | ExecuteAnswer::Transfer { status } - | ExecuteAnswer::Send { status } - | ExecuteAnswer::Burn { status } - | ExecuteAnswer::RegisterReceive { status } - | ExecuteAnswer::SetViewingKey { status } - | ExecuteAnswer::TransferFrom { status } - | ExecuteAnswer::SendFrom { status } - | ExecuteAnswer::BurnFrom { status } - | ExecuteAnswer::Mint { status } - | ExecuteAnswer::ChangeAdmin { status } - | ExecuteAnswer::SetContractStatus { status } - | ExecuteAnswer::SetMinters { status } - | ExecuteAnswer::AddMinters { status } - | ExecuteAnswer::RemoveMinters { status } => { - matches!(status, ResponseStatus::Success { .. }) - } - _ => panic!( - "HandleAnswer not supported for success extraction: {:?}", - handle_result - ), - } - } - - /// creates a cosmos_msg sending this struct to the named contract - pub fn into_cosmos_submsg( - msg: Snip20ReceiveMsg, - code_hash: String, - contract_addr: Addr, - id: u64, - ) -> StdResult { - let msg = msg.into_binary()?; - let execute = SubMsg { - id, - msg: WasmMsg::Execute { - contract_addr: contract_addr.into_string(), - code_hash, - msg, - funds: vec![], - } - .into(), - // TODO: Discuss the wanted behavior - reply_on: match id { - 0 => ReplyOn::Never, - _ => ReplyOn::Always, - }, - gas_limit: None, - }; - - Ok(execute) - } - - // Init tests - - #[test] - fn test_init_sanity() { - let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "lebron".to_string(), - amount: Uint128::new(5000), - }]); - assert_eq!(init_result.unwrap(), Response::default()); - - let constants = CONFIG.load(&deps.storage).unwrap(); - assert_eq!(TOTAL_SUPPLY.load(&deps.storage).unwrap(), 5000); - assert_eq!( - CONTRACT_STATUS.load(&deps.storage).unwrap(), - ContractStatusLevel::NormalRun - ); - assert_eq!(constants.name, "sec-sec".to_string()); - assert_eq!(constants.admin, Addr::unchecked("admin".to_string())); - assert_eq!(constants.symbol, "SECSEC".to_string()); - assert_eq!(constants.decimals, 8); - assert_eq!(constants.total_supply_is_public, false); - - ViewingKey::set(deps.as_mut().storage, "lebron", "lolz fun yay"); - let is_vk_correct = ViewingKey::check(&deps.storage, "lebron", "lolz fun yay"); - assert!( - is_vk_correct.is_ok(), - "Viewing key verification failed!: {}", - is_vk_correct.err().unwrap() - ); - } - - #[test] - fn test_init_with_config_sanity() { - let (init_result, mut deps) = init_helper_with_config( - vec![InitialBalance { - address: "lebron".to_string(), - amount: Uint128::new(5000), - }], - true, - true, - true, - true, - 0, - vec!["uscrt".to_string()], - ); - assert_eq!(init_result.unwrap(), Response::default()); - - let constants = CONFIG.load(&deps.storage).unwrap(); - assert_eq!(TOTAL_SUPPLY.load(&deps.storage).unwrap(), 5000); - assert_eq!( - CONTRACT_STATUS.load(&deps.storage).unwrap(), - ContractStatusLevel::NormalRun - ); - assert_eq!(constants.name, "sec-sec".to_string()); - assert_eq!(constants.admin, Addr::unchecked("admin".to_string())); - assert_eq!(constants.symbol, "SECSEC".to_string()); - assert_eq!(constants.decimals, 8); - assert_eq!(constants.total_supply_is_public, false); - assert_eq!(constants.deposit_is_enabled, true); - assert_eq!(constants.redeem_is_enabled, true); - assert_eq!(constants.mint_is_enabled, true); - assert_eq!(constants.burn_is_enabled, true); - - ViewingKey::set(deps.as_mut().storage, "lebron", "lolz fun yay"); - let is_vk_correct = ViewingKey::check(&deps.storage, "lebron", "lolz fun yay"); - assert!( - is_vk_correct.is_ok(), - "Viewing key verification failed!: {}", - is_vk_correct.err().unwrap() - ); - } - - #[test] - fn test_total_supply_overflow() { - let (init_result, _deps) = init_helper(vec![InitialBalance { - address: "lebron".to_string(), - amount: Uint128::new(u128::max_value()), - }]); - 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() { - 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: "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 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 handle_msg = ExecuteMsg::Transfer { - recipient: "alice".to_string(), - amount: Uint128::new(10000), - 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 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 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 handle_msg = ExecuteMsg::Transfer { - recipient: "alice".to_string(), - amount: Uint128::new(1000), - memo: None, - decoys: Some(vec![lior_addr.clone(), jhon_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 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)); - } - - #[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 handle_msg = ExecuteMsg::RegisterReceive { - code_hash: "this_is_a_hash_of_a_code".to_string(), - padding: None, - }; - let info = mock_info("contract", &[]); - - 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::Send { - recipient: "contract".to_string(), - recipient_code_hash: None, - amount: Uint128::new(100), - memo: Some("my memo".to_string()), - 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 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: "bob".to_string(), - amount: Uint128::new(5000), - }]); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); - - let handle_msg = ExecuteMsg::RegisterReceive { - code_hash: "this_is_a_hash_of_a_code".to_string(), - padding: None, - }; - let info = mock_info("contract", &[]); - - let handle_result = execute(deps.as_mut(), mock_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() - ); - - let handle_msg = ExecuteMsg::CreateViewingKey { - entropy: "".to_string(), - 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 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 result = ViewingKey::check(&deps.storage, "bob", key.as_str()); - assert!(result.is_ok()); - - // let saved_vk = read_viewing_key(&deps.storage, &bob_canonical).unwrap(); - // assert!(key.check_viewing_key(saved_vk.as_slice())); - } - - #[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() - ); - - // Set VK - let handle_msg = ExecuteMsg::SetViewingKey { - key: "hi lol".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(), - ); - - // 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", &[]); - - 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: Success }).unwrap(), - ); - - let result = ViewingKey::check(&deps.storage, "bob", actual_vk.as_str()); - assert!(result.is_ok()); - } - - fn revoke_permit( - permit_name: &str, - user_address: &str, - deps: &mut OwnedDeps, - ) -> Result { - let handle_msg = ExecuteMsg::RevokePermit { - permit_name: permit_name.to_string(), - padding: None, - }; - let info = mock_info(user_address, &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - handle_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, - ); - - QueryMsg::WithPermit { - permit, - query: QueryWithPermit::Balance {}, - } - } - - 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(), - }, - }; - 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, - }, - } - } - - #[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 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.is_ok(), - "Init failed: {}", - 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); - - 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"), - }; - assert_eq!(balance.u128(), 50000000); - - // 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"), + // + // 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, }; - assert_eq!(status, ResponseStatus::Success); - - // 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()) - ); - } - - #[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, - padding: None, + 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 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")); + //println!("transfers: {transfers:?}"); + let expected_transfers = vec![ + Tx { + id: 8845804139732984, + 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: 3692043167097969, + 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: 3808363917805648, + 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); - // 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), + // + // 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 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::TransferFrom { - owner: "bob".to_string(), - recipient: "alice".to_string(), - amount: Uint128::new(2500), - memo: None, - decoys: None, - entropy: None, - padding: None, + 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 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")); + //println!("transfers: {transfers:?}"); + let expected_transfers = vec![ + Tx { + id: 7611337451915155, + 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: 7288023700190802, + 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: 6449330804541894, + 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: 1600285134972748, + 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: 7899356969158249, + 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: 5178919937687208, + 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); - // 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, + // + // 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 info = MessageInfo { - sender: Addr::unchecked("bob".to_string()), - funds: vec![], + 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 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(), + //println!("transfers: {transfers:?}"); + let expected_transfers = vec![ + Tx { + id: 7879504399954008, + action: TxAction::Transfer { + from: Addr::unchecked("bob"), + sender: Addr::unchecked("bob"), + recipient: Addr::unchecked("alice"), }, - transaction: Some(TransactionInfo { index: 3 }), - contract: ContractInfo { - address: Addr::unchecked(MOCK_CONTRACT_ADDR.to_string()), - code_hash: "".to_string(), + coins: Coin { + denom: "SECSEC".to_string(), + amount: Uint128::from(2u128), }, + memo: None, + block_time: 1571797419, + block_height: 12345, }, - 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); + Tx { + id: 7625837293820843, + 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: 2105964828411645, + 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: 5298675660782133, + 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: 3942814133456943, + 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, + }, + ]; - 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 transfers_len = transfers.len(); + //println!("transfers.len(): {transfers_len}"); - 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); + assert_eq!(transfers, expected_transfers); - // Second send more than allowance - let handle_msg = ExecuteMsg::TransferFrom { - owner: "bob".to_string(), + // now try invalid transfer + let handle_msg = ExecuteMsg::Transfer { recipient: "alice".to_string(), - amount: Uint128::new(1), + amount: Uint128::new(10000), memo: None, - decoys: None, - entropy: None, + #[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")); + assert!(error.contains("insufficient funds")); } #[test] - fn test_handle_send_from() { + fn test_handle_send() { let (init_result, mut deps) = init_helper(vec![InitialBalance { address: "bob".to_string(), amount: Uint128::new(5000), @@ -2975,241 +1663,112 @@ 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, - 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")); - - // Send more than allowance - let handle_msg = ExecuteMsg::IncreaseAllowance { - spender: "alice".to_string(), - amount: Uint128::new(2000), - 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 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")); - - // Sanity check let handle_msg = ExecuteMsg::RegisterReceive { - code_hash: "lolz".to_string(), + 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", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - 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()), - ); - let handle_msg = ExecuteMsg::SendFrom { - owner: "bob".to_string(), + let result = handle_result.unwrap(); + assert!(ensure_success(result)); + + let handle_msg = ExecuteMsg::Send { recipient: "contract".to_string(), recipient_code_hash: None, - amount: Uint128::new(2000), + amount: Uint128::new(100), 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); - - // 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, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + msg: Some(to_binary("hey hey you you").unwrap()), }; - 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 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_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 (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { + fn test_handle_register_receive() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { address: "bob".to_string(), - amount: Uint128::new(10000), + amount: Uint128::new(5000), }]); 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, - decoys: None, - entropy: 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, - 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")); - - // Burn more than allowance - let handle_msg = ExecuteMsg::IncreaseAllowance { - spender: "alice".to_string(), - amount: Uint128::new(2000), - 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 handle_msg = ExecuteMsg::BurnFrom { - owner: "bob".to_string(), - amount: Uint128::new(2500), - memo: None, - decoys: None, - entropy: None, + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + 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("alice", &[]); + let info = mock_info("contract", &[]); 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 = handle_result.unwrap(); + assert!(ensure_success(result)); - // Sanity check - let handle_msg = ExecuteMsg::BurnFrom { - owner: "bob".to_string(), - amount: Uint128::new(2000), - memo: None, - decoys: None, + 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() + ); + + let handle_msg = ExecuteMsg::CreateViewingKey { entropy: None, + #[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); @@ -3218,225 +1777,279 @@ 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 total_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); - assert_eq!(total_supply, 10000 - 2000); + let answer: ExecuteAnswer = from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); - // Second burn more than allowance - let handle_msg = ExecuteMsg::BurnFrom { - owner: "bob".to_string(), - amount: Uint128::new(1), - memo: None, - decoys: None, - entropy: None, - padding: None, + 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")); + // let saved_vk = read_viewing_key(&deps.storage, &bob_canonical).unwrap(); + // assert!(key.check_viewing_key(saved_vk.as_slice())); } #[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_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 (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, + // 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("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 }) - .collect(); - let handle_msg = ExecuteMsg::BatchBurnFrom { - actions, - entropy: None, + .unwrap(), + ); + + // 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, }; - let info = mock_info("alice", &[]); - let handle_result = execute( - deps_for_failure.as_mut(), - mock_env(), - info, - handle_msg.clone(), + 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: Success }).unwrap(), ); - 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 result = ViewingKey::check(&deps.storage, "bob", actual_vk.as_str()); + assert!(result.is_ok()); + } + fn revoke_permit( + permit_name: &str, + user_address: &str, + deps: &mut OwnedDeps, + ) -> Result { + let handle_msg = ExecuteMsg::RevokePermit { + permit_name: permit_name.to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info(user_address, &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + handle_result + } - let error = extract_error_msg(handle_result); - assert!(error.contains("insufficient allowance")); + 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, + ); - // 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); + QueryMsg::WithPermit { + permit, + query: QueryWithPermit::Balance {}, + } + } - 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", &[]); + 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], + created: None, + expires: None, + }, + 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(), + }, + }; + 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, + }, + } + } - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + #[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=="; - let error = extract_error_msg(handle_result); - assert!(error.contains("insufficient allowance")); - } + // 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() + ); - // 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 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); - let handle_msg = ExecuteMsg::BatchBurnFrom { - actions, - entropy: None, - padding: None, - }; - let info = mock_info("alice", &[]); + assert_eq!(query_result.is_err(), true); + } - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + #[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!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() + init_result.is_ok(), + "Init failed: {}", + init_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 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::BatchBurnFrom { - actions, - entropy: None, - padding: None, - }; - let info = mock_info("alice", &[]); + 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"; + // 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!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() + init_result.is_ok(), + "Init failed: {}", + init_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); - } - 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, - decoys: None, - }) - .collect(); - let handle_msg = ExecuteMsg::BatchBurnFrom { - actions, - entropy: None, - padding: None, + // 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.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("insufficient allowance")); + // 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()) + ); } #[test] - fn test_handle_decrease_allowance() { + fn test_execute_transfer_from() { let (init_result, mut deps) = init_helper(vec![InitialBalance { address: "bob".to_string(), amount: Uint128::new(5000), @@ -3447,39 +2060,31 @@ mod tests { init_result.err().unwrap() ); - let handle_msg = ExecuteMsg::DecreaseAllowance { - spender: "alice".to_string(), - amount: Uint128::new(2000), + // Transfer before allowance + let handle_msg = ExecuteMsg::TransferFrom { + owner: "bob".to_string(), + recipient: "alice".to_string(), + amount: Uint128::new(2500), + 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); - 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 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")); + // 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", &[]); @@ -3490,52 +2095,76 @@ mod tests { "handle() failed: {}", handle_result.err().unwrap() ); - - let handle_msg = ExecuteMsg::DecreaseAllowance { - spender: "alice".to_string(), - amount: Uint128::new(50), + let handle_msg = ExecuteMsg::TransferFrom { + owner: "bob".to_string(), + recipient: "alice".to_string(), + amount: Uint128::new(2500), + 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); - assert!( - handle_result.is_ok(), - "handle() failed: {}", - handle_result.err().unwrap() - ); + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); - let allowance = AllowancesStore::load(&deps.storage, &bob_canonical, &alice_canonical); - assert_eq!( - allowance, - crate::state::Allowance { - amount: 1950, - expiration: None - } - ); - } + // 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, + }; - #[test] - fn test_handle_increase_allowance() { - 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 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")); - let handle_msg = ExecuteMsg::IncreaseAllowance { - spender: "alice".to_string(), + // Sanity check + 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, - expiration: None, }; - let info = mock_info("bob", &[]); + let info = mock_info("alice", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); @@ -3544,47 +2173,42 @@ mod tests { "handle() failed: {}", handle_result.err().unwrap() ); + 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, 5000); - 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), + // Second send more than allowance + let handle_msg = ExecuteMsg::TransferFrom { + owner: "bob".to_string(), + recipient: "alice".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); - 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 - } - ); + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); } #[test] - fn test_handle_change_admin() { + fn test_handle_send_from() { let (init_result, mut deps) = init_helper(vec![InitialBalance { address: "bob".to_string(), amount: Uint128::new(5000), @@ -3595,41 +2219,35 @@ mod tests { init_result.err().unwrap() ); - let handle_msg = ExecuteMsg::ChangeAdmin { - address: "bob".to_string(), + // 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("admin", &[]); + 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 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 error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); - let handle_msg = ExecuteMsg::SetContractStatus { - level: ContractStatusLevel::StopAll, + // 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("admin", &[]); + let info = mock_info("bob", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); @@ -3638,103 +2256,60 @@ mod tests { "handle() failed: {}", handle_result.err().unwrap() ); - - let contract_status = CONTRACT_STATUS.load(&deps.storage).unwrap(); - assert!(matches!( - contract_status, - ContractStatusLevel::StopAll { .. } - )); - } - - #[test] - fn test_handle_redeem() { - let (init_result, mut deps) = init_helper_with_config( - vec![InitialBalance { - address: "butler".to_string(), - amount: Uint128::new(5000), - }], - false, - true, - false, - false, - 1000, - vec!["uscrt".to_string()], - ); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); - - let (init_result_no_reserve, mut deps_no_reserve) = init_helper_with_config( - vec![InitialBalance { - address: "butler".to_string(), - amount: Uint128::new(5000), - }], - false, - true, - false, - false, - 0, - vec!["uscrt".to_string()], - ); - assert!( - init_result_no_reserve.is_ok(), - "Init failed: {}", - init_result_no_reserve.err().unwrap() - ); - - let (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { - address: "butler".to_string(), - amount: Uint128::new(5000), - }]); - assert!( - init_result_for_failure.is_ok(), - "Init failed: {}", - init_result_for_failure.err().unwrap() - ); - // test when redeem disabled - let handle_msg = ExecuteMsg::Redeem { - amount: Uint128::new(1000), - denom: None, - decoys: None, - entropy: None, + 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("butler", &[]); + let info = mock_info("alice", &[]); - let handle_result = execute(deps_for_failure.as_mut(), mock_env(), info, handle_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); let error = extract_error_msg(handle_result); - assert!(error.contains("Redeem functionality is not enabled for this token.")); + assert!(error.contains("insufficient allowance")); - // try to redeem when contract has 0 balance - let handle_msg = ExecuteMsg::Redeem { - amount: Uint128::new(1000), - denom: None, - decoys: None, - 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("butler", &[]); + let info = mock_info("contract", &[]); - let handle_result = execute(deps_no_reserve.as_mut(), mock_env(), info, handle_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let error = extract_error_msg(handle_result); - assert_eq!( - error, - "You are trying to redeem for more uscrt than the contract has in its reserve" + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() ); - - // test without denom - let handle_msg = ExecuteMsg::Redeem { - amount: Uint128::new(1000), - denom: None, - decoys: None, - 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("butler", &[]); + let info = mock_info("alice", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); @@ -3743,42 +2318,65 @@ mod tests { "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() + )); - // test with denom specified - let handle_msg = ExecuteMsg::Redeem { - amount: Uint128::new(1000), - denom: Option::from("uscrt".to_string()), - decoys: None, - entropy: None, + 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, 5000); + + // 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("butler", &[]); + 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 canonical = Addr::unchecked("butler".to_string()); - assert_eq!(BalancesStore::load(&deps.storage, &canonical), 3000) + let error = extract_error_msg(handle_result); + assert!(error.contains("insufficient allowance")); } #[test] - fn test_handle_deposit() { + fn test_handle_burn_from() { let (init_result, mut deps) = init_helper_with_config( vec![InitialBalance { - address: "lebron".to_string(), - amount: Uint128::new(5000), + address: "bob".to_string(), + amount: Uint128::new(10000), }], - true, false, false, false, + true, 0, - vec!["uscrt".to_string()], + vec![], ); assert!( init_result.is_ok(), @@ -3787,135 +2385,145 @@ mod tests { ); let (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { - address: "lebron".to_string(), - amount: Uint128::new(5000), + 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 deposit disabled - let handle_msg = ExecuteMsg::Deposit { - decoys: None, - entropy: None, + // 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( - "lebron", - &[Coin { - denom: "uscrt".to_string(), - amount: Uint128::new(1000), - }], - ); + 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("Tried to deposit an unsupported coin uscrt")); + assert!(error.contains("Burn functionality is not enabled for this token.")); - let handle_msg = ExecuteMsg::Deposit { - decoys: None, - entropy: None, + // 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 info = mock_info( - "lebron", - &[Coin { - denom: "uscrt".to_string(), - amount: Uint128::new(1000), - }], - ); + 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", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + assert!( handle_result.is_ok(), "handle() failed: {}", handle_result.err().unwrap() ); - - let canonical = Addr::unchecked("lebron".to_string()); - assert_eq!(BalancesStore::load(&deps.storage, &canonical), 6000) - } - - #[test] - fn test_handle_burn() { - let (init_result, mut deps) = init_helper_with_config( - vec![InitialBalance { - address: "lebron".to_string(), - amount: Uint128::new(5000), - }], - 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: "lebron".to_string(), - amount: Uint128::new(5000), - }]); - assert!( - init_result_for_failure.is_ok(), - "Init failed: {}", - init_result_for_failure.err().unwrap() - ); - // test when burn disabled - let handle_msg = ExecuteMsg::Burn { - amount: Uint128::new(100), + let handle_msg = ExecuteMsg::BurnFrom { + owner: "bob".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("lebron", &[]); + let info = mock_info("alice", &[]); - let handle_result = execute(deps_for_failure.as_mut(), mock_env(), info, handle_msg); + let handle_result = execute(deps.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.")); + assert!(error.contains("insufficient allowance")); - let supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); - let burn_amount: u128 = 100; - let handle_msg = ExecuteMsg::Burn { - amount: Uint128::new(burn_amount), + // Sanity check + let handle_msg = ExecuteMsg::BurnFrom { + owner: "bob".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("lebron", &[]); + let info = mock_info("alice", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); assert!( handle_result.is_ok(), - "Pause handle failed: {}", + "handle() failed: {}", handle_result.err().unwrap() ); + let bob_canonical = deps + .api + .addr_canonicalize(Addr::unchecked("bob".to_string()).as_str()) + .unwrap(); - let new_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); - assert_eq!(new_supply, supply - burn_amount); + 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, + }; + 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_mint() { + fn test_handle_batch_burn_from() { let (init_result, mut deps) = init_helper_with_config( - vec![InitialBalance { - address: "lebron".to_string(), - amount: Uint128::new(5000), - }], + 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, - true, false, + true, 0, vec![], ); @@ -3924,208 +2532,373 @@ mod tests { "Init failed: {}", init_result.err().unwrap() ); + let (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { - address: "lebron".to_string(), - amount: Uint128::new(5000), + address: "bob".to_string(), + amount: Uint128::new(10000), }]); assert!( init_result_for_failure.is_ok(), "Init failed: {}", init_result_for_failure.err().unwrap() ); - // try to mint when mint is disabled - let mint_amount: u128 = 100; - let handle_msg = ExecuteMsg::Mint { - recipient: "lebron".to_string(), - amount: Uint128::new(mint_amount), - memo: None, - decoys: None, - entropy: None, + // 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("admin", &[]); + 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_for_failure.as_mut(), mock_env(), info, handle_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let error = extract_error_msg(handle_result); - assert!(error.contains("Mint functionality is not enabled for this token")); + assert!( + handle_result.is_ok(), + "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 supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); - let mint_amount: u128 = 100; - let handle_msg = ExecuteMsg::Mint { - recipient: "lebron".to_string(), - amount: Uint128::new(mint_amount), - memo: None, - decoys: None, - entropy: 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("admin", &[]); + let info = mock_info("alice", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); assert!( handle_result.is_ok(), - "Pause handle failed: {}", + "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)); - let new_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); - assert_eq!(new_supply, supply + mint_amount); + // 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_admin_commands() { - let admin_err = "Admin commands can only be run from admin address".to_string(); - let (init_result, mut deps) = init_helper_with_config( - vec![InitialBalance { - address: "lebron".to_string(), - amount: Uint128::new(5000), - }], - false, - false, - true, - false, - 0, - vec![], - ); + fn test_handle_decrease_allowance() { + 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 pause_msg = ExecuteMsg::SetContractStatus { - level: ContractStatusLevel::StopAllButRedeems, + 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("not_admin", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, pause_msg); + let info = mock_info("bob", &[]); - let error = extract_error_msg(handle_result); - assert!(error.contains(&admin_err.clone())); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let mint_msg = ExecuteMsg::AddMinters { - minters: vec!["not_admin".to_string()], - padding: None, - }; - let info = mock_info("not_admin", &[]); + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); - let handle_result = execute(deps.as_mut(), mock_env(), info, mint_msg); + let bob_canonical = Addr::unchecked("bob".to_string()); + let alice_canonical = Addr::unchecked("alice".to_string()); - let error = extract_error_msg(handle_result); - assert!(error.contains(&admin_err.clone())); + let allowance = AllowancesStore::load(&deps.storage, &bob_canonical, &alice_canonical); + assert_eq!( + allowance, + crate::state::Allowance { + amount: 0, + expiration: None + } + ); - let mint_msg = ExecuteMsg::RemoveMinters { - minters: vec!["admin".to_string()], + 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("not_admin", &[]); + let info = mock_info("bob", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, mint_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let error = extract_error_msg(handle_result); - assert!(error.contains(&admin_err.clone())); + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); - let mint_msg = ExecuteMsg::SetMinters { - minters: vec!["not_admin".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("not_admin", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, mint_msg); - - let error = extract_error_msg(handle_result); - assert!(error.contains(&admin_err.clone())); + let info = mock_info("bob", &[]); - let change_admin_msg = ExecuteMsg::ChangeAdmin { - address: "not_admin".to_string(), - padding: None, - }; - let info = mock_info("not_admin", &[]); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let handle_result = execute(deps.as_mut(), mock_env(), info, change_admin_msg); + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); - let error = extract_error_msg(handle_result); - assert!(error.contains(&admin_err.clone())); + let allowance = AllowancesStore::load(&deps.storage, &bob_canonical, &alice_canonical); + assert_eq!( + allowance, + crate::state::Allowance { + amount: 1950, + expiration: None + } + ); } #[test] - fn test_handle_pause_with_withdrawals() { - let (init_result, mut deps) = init_helper_with_config( - vec![InitialBalance { - address: "lebron".to_string(), - amount: Uint128::new(5000), - }], - false, - true, - false, - false, - 5000, - vec!["uscrt".to_string()], - ); + fn test_handle_increase_allowance() { + 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 pause_msg = ExecuteMsg::SetContractStatus { - level: ContractStatusLevel::StopAllButRedeems, + 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 info = mock_info("admin", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, pause_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); assert!( handle_result.is_ok(), - "Pause handle failed: {}", + "handle() failed: {}", handle_result.err().unwrap() ); - let send_msg = ExecuteMsg::Transfer { - recipient: "account".to_string(), - amount: Uint128::new(123), - memo: None, - decoys: None, - entropy: None, + 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("admin", &[]); + let info = mock_info("bob", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, send_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let error = extract_error_msg(handle_result); + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); + + let allowance = AllowancesStore::load(&deps.storage, &bob_canonical, &alice_canonical); assert_eq!( - error, - "This contract is stopped and this action is not allowed".to_string() + 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 withdraw_msg = ExecuteMsg::Redeem { - amount: Uint128::new(5000), - denom: Option::from("uscrt".to_string()), - decoys: None, - entropy: None, + let handle_msg = ExecuteMsg::ChangeAdmin { + address: "bob".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("lebron", &[]); + let info = mock_info("admin", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, withdraw_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); assert!( handle_result.is_ok(), - "Withdraw failed: {}", + "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_pause_all() { + fn test_handle_set_contract_status() { let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "lebron".to_string(), + address: "admin".to_string(), amount: Uint128::new(5000), }]); assert!( @@ -4134,78 +2907,69 @@ mod tests { init_result.err().unwrap() ); - let pause_msg = ExecuteMsg::SetContractStatus { + 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, pause_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); assert!( handle_result.is_ok(), - "Pause handle failed: {}", + "handle() failed: {}", handle_result.err().unwrap() ); - let send_msg = ExecuteMsg::Transfer { - recipient: "account".to_string(), - amount: Uint128::new(123), - memo: None, - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("admin", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, send_msg); - - let error = extract_error_msg(handle_result); - assert_eq!( - error, - "This contract is stopped and this action is not allowed".to_string() - ); - - let withdraw_msg = ExecuteMsg::Redeem { - amount: Uint128::new(5000), - denom: Option::from("uscrt".to_string()), - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("lebron", &[]); - - let handle_result = execute(deps.as_mut(), mock_env(), info, withdraw_msg); - - let error = extract_error_msg(handle_result); - assert_eq!( - error, - "This contract is stopped and this action is not allowed".to_string() - ); + let contract_status = CONTRACT_STATUS.load(&deps.storage).unwrap(); + assert!(matches!( + contract_status, + ContractStatusLevel::StopAll { .. } + )); } #[test] - fn test_handle_set_minters() { + fn test_handle_redeem() { let (init_result, mut deps) = init_helper_with_config( vec![InitialBalance { - address: "bob".to_string(), + address: "butler".to_string(), amount: Uint128::new(5000), }], false, - false, true, false, - 0, - vec![], + false, + 1000, + vec!["uscrt".to_string()], ); assert!( init_result.is_ok(), "Init failed: {}", init_result.err().unwrap() ); + + let (init_result_no_reserve, mut deps_no_reserve) = init_helper_with_config( + vec![InitialBalance { + address: "butler".to_string(), + amount: Uint128::new(5000), + }], + false, + true, + false, + false, + 0, + vec!["uscrt".to_string()], + ); + assert!( + init_result_no_reserve.is_ok(), + "Init failed: {}", + init_result_no_reserve.err().unwrap() + ); + let (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { - address: "bob".to_string(), + address: "butler".to_string(), amount: Uint128::new(5000), }]); assert!( @@ -4213,90 +2977,104 @@ mod tests { "Init failed: {}", init_result_for_failure.err().unwrap() ); - // try when mint disabled - let handle_msg = ExecuteMsg::SetMinters { - minters: vec!["bob".to_string()], + // test when redeem disabled + let handle_msg = ExecuteMsg::Redeem { + amount: Uint128::new(1000), + denom: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("admin", &[]); + let info = mock_info("butler", &[]); let handle_result = execute(deps_for_failure.as_mut(), mock_env(), info, handle_msg); let error = extract_error_msg(handle_result); - assert!(error.contains("Mint functionality is not enabled for this token")); + assert!(error.contains("Redeem functionality is not enabled for this token.")); - let handle_msg = ExecuteMsg::SetMinters { - minters: vec!["bob".to_string()], + // try to redeem when contract has 0 balance + let handle_msg = ExecuteMsg::Redeem { + amount: Uint128::new(1000), + denom: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("bob", &[]); + let info = mock_info("butler", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let handle_result = execute(deps_no_reserve.as_mut(), mock_env(), info, handle_msg); let error = extract_error_msg(handle_result); - assert!(error.contains("Admin commands can only be run from admin address")); + assert_eq!( + error, + "You are trying to redeem for more uscrt than the contract has in its reserve" + ); - let handle_msg = ExecuteMsg::SetMinters { - minters: vec!["bob".to_string()], + // test without denom + let handle_msg = ExecuteMsg::Redeem { + amount: Uint128::new(1000), + denom: None, padding: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, }; - let info = mock_info("admin", &[]); + let info = mock_info("butler", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!(ensure_success(handle_result.unwrap())); + 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: None, - decoys: None, - entropy: None, + // test with denom specified + let handle_msg = ExecuteMsg::Redeem { + amount: Uint128::new(1000), + denom: Option::from("uscrt".to_string()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("bob", &[]); + let info = mock_info("butler", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!(ensure_success(handle_result.unwrap())); - - let handle_msg = ExecuteMsg::Mint { - recipient: "bob".to_string(), - amount: Uint128::new(100), - memo: None, - decoys: None, - entropy: 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 error = extract_error_msg(handle_result); - assert!(error.contains("allowed to minter accounts only")); + let canonical = deps + .api + .addr_canonicalize(Addr::unchecked("butler".to_string()).as_str()) + .unwrap(); + assert_eq!(stored_balance(&deps.storage, &canonical).unwrap(), 3000) } #[test] - fn test_handle_add_minters() { + fn test_handle_deposit() { let (init_result, mut deps) = init_helper_with_config( vec![InitialBalance { - address: "bob".to_string(), + address: "lebron".to_string(), amount: Uint128::new(5000), }], + true, false, false, - true, false, 0, - vec![], + vec!["uscrt".to_string()], ); 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(), + address: "lebron".to_string(), amount: Uint128::new(5000), }]); assert!( @@ -4304,73 +3082,151 @@ mod tests { "Init failed: {}", init_result_for_failure.err().unwrap() ); - // try when mint disabled - let handle_msg = ExecuteMsg::AddMinters { - minters: vec!["bob".to_string()], + // test when deposit disabled + let handle_msg = ExecuteMsg::Deposit { + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("admin", &[]); + let info = mock_info( + "lebron", + &[Coin { + denom: "uscrt".to_string(), + amount: Uint128::new(1000), + }], + ); let handle_result = execute(deps_for_failure.as_mut(), mock_env(), info, handle_msg); - let error = extract_error_msg(handle_result); - assert!(error.contains("Mint functionality is not enabled for this token")); + assert!(error.contains("Tried to deposit an unsupported coin uscrt")); - let handle_msg = ExecuteMsg::AddMinters { - minters: vec!["bob".to_string()], + let handle_msg = ExecuteMsg::Deposit { + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("bob", &[]); + + let info = mock_info( + "lebron", + &[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 error = extract_error_msg(handle_result); - assert!(error.contains("Admin commands can only be run from admin address")); + let canonical = deps + .api + .addr_canonicalize(Addr::unchecked("lebron".to_string()).as_str()) + .unwrap(); - let handle_msg = ExecuteMsg::AddMinters { - minters: vec!["bob".to_string()], + // 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("admin", &[]); + 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 handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let query_balance_msg = QueryMsg::Balance { + address: "lebron".to_string(), + key: vk, + }; - assert!(ensure_success(handle_result.unwrap())); + 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)); + } - let handle_msg = ExecuteMsg::Mint { - recipient: "bob".to_string(), + #[test] + fn test_handle_burn() { + let (init_result, mut deps) = init_helper_with_config( + vec![InitialBalance { + address: "lebron".to_string(), + amount: Uint128::new(5000), + }], + 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: "lebron".to_string(), + amount: Uint128::new(5000), + }]); + assert!( + init_result_for_failure.is_ok(), + "Init failed: {}", + init_result_for_failure.err().unwrap() + ); + // test when burn disabled + 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("bob", &[]); + let info = mock_info("lebron", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let handle_result = execute(deps_for_failure.as_mut(), mock_env(), info, handle_msg); - assert!(ensure_success(handle_result.unwrap())); + let error = extract_error_msg(handle_result); + assert!(error.contains("Burn functionality is not enabled for this token.")); - let handle_msg = ExecuteMsg::Mint { - recipient: "bob".to_string(), - amount: Uint128::new(100), + let supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); + let burn_amount: u128 = 100; + 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("admin", &[]); + let info = mock_info("lebron", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - assert!(ensure_success(handle_result.unwrap())); + assert!( + handle_result.is_ok(), + "Pause handle failed: {}", + handle_result.err().unwrap() + ); + + let new_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); + assert_eq!(new_supply, supply - burn_amount); } #[test] - fn test_handle_remove_minters() { + fn test_handle_mint() { let (init_result, mut deps) = init_helper_with_config( vec![InitialBalance { - address: "bob".to_string(), + address: "lebron".to_string(), amount: Uint128::new(5000), }], false, @@ -4386,7 +3242,7 @@ mod tests { init_result.err().unwrap() ); let (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { - address: "bob".to_string(), + address: "lebron".to_string(), amount: Uint128::new(5000), }]); assert!( @@ -4394,9 +3250,14 @@ mod tests { "Init failed: {}", init_result_for_failure.err().unwrap() ); - // try when mint disabled - let handle_msg = ExecuteMsg::RemoveMinters { - minters: vec!["bob".to_string()], + // try to mint when mint is disabled + let mint_amount: u128 = 100; + let handle_msg = ExecuteMsg::Mint { + recipient: "lebron".to_string(), + amount: Uint128::new(mint_amount), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; let info = mock_info("admin", &[]); @@ -4406,918 +3267,1013 @@ mod tests { let error = extract_error_msg(handle_result); assert!(error.contains("Mint functionality is not enabled for this token")); - let handle_msg = ExecuteMsg::RemoveMinters { - minters: vec!["admin".to_string()], + let supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); + let mint_amount: u128 = 100; + let handle_msg = ExecuteMsg::Mint { + recipient: "lebron".to_string(), + amount: Uint128::new(mint_amount), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("bob", &[]); + let info = mock_info("admin", &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); - let error = extract_error_msg(handle_result); - assert!(error.contains("Admin commands can only be run from admin address")); - - let handle_msg = ExecuteMsg::RemoveMinters { - minters: vec!["admin".to_string()], - padding: None, - }; - let info = mock_info("admin", &[]); + assert!( + handle_result.is_ok(), + "Pause handle failed: {}", + handle_result.err().unwrap() + ); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let new_supply = TOTAL_SUPPLY.load(&deps.storage).unwrap(); + assert_eq!(new_supply, supply + mint_amount); + } - assert!(ensure_success(handle_result.unwrap())); + #[test] + fn test_handle_admin_commands() { + let admin_err = "Admin commands can only be run from admin address".to_string(); + let (init_result, mut deps) = init_helper_with_config( + vec![InitialBalance { + address: "lebron".to_string(), + amount: Uint128::new(5000), + }], + false, + false, + true, + false, + 0, + vec![], + ); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); - let handle_msg = ExecuteMsg::Mint { - recipient: "bob".to_string(), - amount: Uint128::new(100), - memo: None, - decoys: None, - entropy: None, + let pause_msg = ExecuteMsg::SetContractStatus { + level: ContractStatusLevel::StopAllButRedeems, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("bob", &[]); + let info = mock_info("not_admin", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, pause_msg); let error = extract_error_msg(handle_result); - assert!(error.contains("allowed to minter accounts only")); + assert!(error.contains(&admin_err.clone())); - let handle_msg = ExecuteMsg::Mint { - recipient: "bob".to_string(), - amount: Uint128::new(100), - memo: None, - decoys: None, - entropy: None, + let mint_msg = ExecuteMsg::AddMinters { + minters: vec!["not_admin".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("admin", &[]); + let info = mock_info("not_admin", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, mint_msg); let error = extract_error_msg(handle_result); - assert!(error.contains("allowed to minter accounts only")); + assert!(error.contains(&admin_err.clone())); - // Removing another extra time to ensure nothing funky happens - let handle_msg = ExecuteMsg::RemoveMinters { + let mint_msg = ExecuteMsg::RemoveMinters { minters: vec!["admin".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!(ensure_success(handle_result.unwrap())); - - let handle_msg = ExecuteMsg::Mint { - recipient: "bob".to_string(), - amount: Uint128::new(100), - memo: None, - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("bob", &[]); + let info = mock_info("not_admin", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, mint_msg); let error = extract_error_msg(handle_result); - assert!(error.contains("allowed to minter accounts only")); + assert!(error.contains(&admin_err.clone())); - let handle_msg = ExecuteMsg::Mint { - recipient: "bob".to_string(), - amount: Uint128::new(100), - memo: None, - decoys: None, - entropy: None, + let mint_msg = ExecuteMsg::SetMinters { + minters: vec!["not_admin".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("admin", &[]); + let info = mock_info("not_admin", &[]); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, mint_msg); let error = extract_error_msg(handle_result); - assert!(error.contains("allowed to minter accounts only")); - } - - // Query tests - - #[test] - fn test_authenticated_queries() { - 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 no_vk_yet_query_msg = QueryMsg::Balance { - address: "giannis".to_string(), - key: "no_vk_yet".to_string(), - }; - let query_result = query(deps.as_ref(), mock_env(), no_vk_yet_query_msg); - let error = extract_error_msg(query_result); - assert_eq!( - error, - "Wrong viewing key for this address or viewing key not set".to_string() - ); + assert!(error.contains(&admin_err.clone())); - let create_vk_msg = ExecuteMsg::CreateViewingKey { - entropy: "34".to_string(), + let change_admin_msg = ExecuteMsg::ChangeAdmin { + address: "not_admin".to_string(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("giannis", &[]); - 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: "giannis".to_string(), - key: vk, - }; + let info = mock_info("not_admin", &[]); - 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(5000)); + let handle_result = execute(deps.as_mut(), mock_env(), info, change_admin_msg); - let wrong_vk_query_msg = QueryMsg::Balance { - address: "giannis".to_string(), - key: "wrong_vk".to_string(), - }; - let query_result = query(deps.as_ref(), mock_env(), wrong_vk_query_msg); - let error = extract_error_msg(query_result); - assert_eq!( - error, - "Wrong viewing key for this address or viewing key not set".to_string() - ); + let error = extract_error_msg(handle_result); + assert!(error.contains(&admin_err.clone())); } #[test] - fn test_query_token_info() { - 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_config: InitConfig = from_binary(&Binary::from( - r#"{ "public_total_supply": true }"#.as_bytes(), - )) - .unwrap(); - 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: Some(init_config), - supported_denoms: None, - }; - let init_result = instantiate(deps.as_mut(), env, info, init_msg); + fn test_handle_pause_with_withdrawals() { + let (init_result, mut deps) = init_helper_with_config( + vec![InitialBalance { + address: "lebron".to_string(), + amount: Uint128::new(5000), + }], + false, + true, + false, + false, + 5000, + vec!["uscrt".to_string()], + ); assert!( init_result.is_ok(), "Init failed: {}", init_result.err().unwrap() ); - let query_msg = QueryMsg::TokenInfo {}; - let query_result = query(deps.as_ref(), mock_env(), query_msg); + let pause_msg = ExecuteMsg::SetContractStatus { + level: ContractStatusLevel::StopAllButRedeems, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + + let info = mock_info("admin", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, pause_msg); + assert!( - query_result.is_ok(), - "Init failed: {}", - query_result.err().unwrap() + handle_result.is_ok(), + "Pause handle failed: {}", + handle_result.err().unwrap() ); - let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); - match query_answer { - QueryAnswer::TokenInfo { - name, - symbol, - decimals, - total_supply, - } => { - assert_eq!(name, init_name); - assert_eq!(symbol, init_symbol); - assert_eq!(decimals, init_decimals); - assert_eq!(total_supply, Some(Uint128::new(5000))); - } - _ => panic!("unexpected"), - } - } - #[test] - fn test_query_token_config() { - 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_config: InitConfig = from_binary(&Binary::from( - format!( - "{{\"public_total_supply\":{}, - \"enable_deposit\":{}, - \"enable_redeem\":{}, - \"enable_mint\":{}, - \"enable_burn\":{}}}", - true, false, false, true, false - ) - .as_bytes(), - )) - .unwrap(); + let send_msg = ExecuteMsg::Transfer { + recipient: "account".to_string(), + amount: Uint128::new(123), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("admin", &[]); - let init_supply = Uint128::new(5000); + let handle_result = execute(deps.as_mut(), mock_env(), info, send_msg); - 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: Some(init_config), - 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 error = extract_error_msg(handle_result); + assert_eq!( + error, + "This contract is stopped and this action is not allowed".to_string() ); - let query_msg = QueryMsg::TokenConfig {}; - let query_result = query(deps.as_ref(), mock_env(), query_msg); + let withdraw_msg = ExecuteMsg::Redeem { + amount: Uint128::new(5000), + denom: Option::from("uscrt".to_string()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("lebron", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, withdraw_msg); + assert!( - query_result.is_ok(), - "Init failed: {}", - query_result.err().unwrap() + handle_result.is_ok(), + "Withdraw failed: {}", + handle_result.err().unwrap() ); - let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); - match query_answer { - QueryAnswer::TokenConfig { - public_total_supply, - deposit_enabled, - redeem_enabled, - mint_enabled, - burn_enabled, - supported_denoms, - } => { - assert_eq!(public_total_supply, true); - assert_eq!(deposit_enabled, false); - assert_eq!(redeem_enabled, false); - assert_eq!(mint_enabled, true); - assert_eq!(burn_enabled, false); - assert_eq!(supported_denoms.len(), 0); - } - _ => panic!("unexpected"), - } } #[test] - fn test_query_exchange_rate() { - // test more dec 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 = 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); + fn test_handle_pause_all() { + let (init_result, mut deps) = init_helper(vec![InitialBalance { + address: "lebron".to_string(), + amount: Uint128::new(5000), + }]); 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 pause_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, pause_msg); + assert!( - query_result.is_ok(), - "Init failed: {}", - query_result.err().unwrap() + handle_result.is_ok(), + "Pause handle failed: {}", + handle_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 send_msg = ExecuteMsg::Transfer { + recipient: "account".to_string(), + amount: Uint128::new(123), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("admin", &[]); - let init_supply = Uint128::new(5000); + let handle_result = execute(deps.as_mut(), mock_env(), info, send_msg); - 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 error = extract_error_msg(handle_result); + assert_eq!( + error, + "This contract is stopped and this action is not allowed".to_string() + ); + + let withdraw_msg = ExecuteMsg::Redeem { + amount: Uint128::new(5000), + denom: Option::from("uscrt".to_string()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, }; - let init_result = instantiate(deps.as_mut(), env, info, init_msg); + let info = mock_info("lebron", &[]); + + let handle_result = execute(deps.as_mut(), mock_env(), info, withdraw_msg); + + let error = extract_error_msg(handle_result); + assert_eq!( + error, + "This contract is stopped and this action is not allowed".to_string() + ); + } + + #[test] + fn test_handle_set_minters() { + let (init_result, mut deps) = init_helper_with_config( + vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }], + false, + false, + true, + false, + 0, + vec![], + ); 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 (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }]); assert!( - query_result.is_ok(), + init_result_for_failure.is_ok(), "Init failed: {}", - query_result.err().unwrap() + init_result_for_failure.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"), - } + // 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", &[]); - // 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_for_failure.as_mut(), mock_env(), info, handle_msg); - let init_supply = Uint128::new(5000); + let error = extract_error_msg(handle_result); + assert!(error.contains("Mint functionality is not enabled for this token")); - 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 handle_msg = ExecuteMsg::SetMinters { + minters: vec!["bob".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, }; - let init_result = instantiate(deps.as_mut(), env, info, init_msg); + 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("Admin commands can only be run from admin address")); + + let handle_msg = ExecuteMsg::SetMinters { + minters: vec!["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!(ensure_success(handle_result.unwrap())); + + let handle_msg = ExecuteMsg::Mint { + recipient: "bob".to_string(), + amount: Uint128::new(100), + 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); + + assert!(ensure_success(handle_result.unwrap())); + + let handle_msg = ExecuteMsg::Mint { + recipient: "bob".to_string(), + amount: Uint128::new(100), + memo: None, + #[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); + + let error = extract_error_msg(handle_result); + assert!(error.contains("allowed to minter accounts only")); + } + + #[test] + fn test_handle_add_minters() { + let (init_result, mut deps) = init_helper_with_config( + vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }], + false, + false, + true, + false, + 0, + vec![], + ); 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 (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }]); assert!( - query_result.is_ok(), + init_result_for_failure.is_ok(), "Init failed: {}", - query_result.err().unwrap() + init_result_for_failure.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"), - } + // 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", &[]); - // 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 handle_result = execute(deps_for_failure.as_mut(), mock_env(), info, handle_msg); - let init_supply = Uint128::new(5000); + let error = extract_error_msg(handle_result); + assert!(error.contains("Mint functionality is not enabled for this token")); - 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 handle_msg = ExecuteMsg::AddMinters { + minters: vec!["bob".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, }; - let init_result = instantiate(deps.as_mut(), env, info, init_msg); - assert!( - init_result.is_ok(), - "Init failed: {}", - init_result.err().unwrap() - ); + let info = mock_info("bob", &[]); - let query_msg = QueryMsg::ExchangeRate {}; - let query_result = query(deps.as_ref(), mock_env(), query_msg); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + let error = extract_error_msg(handle_result); + assert!(error.contains("Admin commands can only be run from admin address")); + + let handle_msg = ExecuteMsg::AddMinters { + minters: vec!["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!(ensure_success(handle_result.unwrap())); + + let handle_msg = ExecuteMsg::Mint { + recipient: "bob".to_string(), + amount: Uint128::new(100), + 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); + + assert!(ensure_success(handle_result.unwrap())); + + let handle_msg = ExecuteMsg::Mint { + recipient: "bob".to_string(), + amount: Uint128::new(100), + memo: None, + #[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!(ensure_success(handle_result.unwrap())); + } + + #[test] + fn test_handle_remove_minters() { + let (init_result, mut deps) = init_helper_with_config( + vec![InitialBalance { + address: "bob".to_string(), + amount: Uint128::new(5000), + }], + false, + false, + true, + false, + 0, + vec![], + ); assert!( - query_result.is_ok(), + init_result.is_ok(), "Init failed: {}", - query_result.err().unwrap() + init_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(), + let (init_result_for_failure, mut deps_for_failure) = init_helper(vec![InitialBalance { + address: "bob".to_string(), amount: Uint128::new(5000), }]); assert!( - init_result.is_ok(), + init_result_for_failure.is_ok(), "Init failed: {}", - init_result.err().unwrap() + init_result_for_failure.err().unwrap() ); + // 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", &[]); - let handle_msg = ExecuteMsg::IncreaseAllowance { - spender: "lebron".to_string(), - amount: Uint128::new(2000), + let handle_result = execute(deps_for_failure.as_mut(), mock_env(), info, handle_msg); + + let error = extract_error_msg(handle_result); + assert!(error.contains("Mint functionality is not enabled for this token")); + + let handle_msg = ExecuteMsg::RemoveMinters { + minters: vec!["admin".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, - expiration: None, }; - let info = mock_info("giannis", &[]); + 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 error = extract_error_msg(handle_result); + assert!(error.contains("Admin commands can only be run from admin address")); - let vk1 = "key1".to_string(); - let vk2 = "key2".to_string(); + let handle_msg = ExecuteMsg::RemoveMinters { + minters: vec!["admin".to_string()], + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, + }; + let info = mock_info("admin", &[]); - let query_msg = QueryMsg::Allowance { - owner: "giannis".to_string(), - spender: "lebron".to_string(), - key: vk1.clone(), + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + assert!(ensure_success(handle_result.unwrap())); + + let handle_msg = ExecuteMsg::Mint { + recipient: "bob".to_string(), + amount: Uint128::new(100), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, }; - 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 info = mock_info("bob", &[]); - let handle_msg = ExecuteMsg::SetViewingKey { - key: vk1.clone(), + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + + let error = extract_error_msg(handle_result); + assert!(error.contains("allowed to minter accounts only")); + + let handle_msg = ExecuteMsg::Mint { + recipient: "bob".to_string(), + amount: Uint128::new(100), + memo: None, + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("lebron", &[]); + let info = mock_info("admin", &[]); 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 error = extract_error_msg(handle_result); + assert!(error.contains("allowed to minter accounts only")); - let handle_msg = ExecuteMsg::SetViewingKey { - key: vk2.clone(), + // 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("giannis", &[]); + let info = mock_info("admin", &[]); 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!(ensure_success(handle_result.unwrap())); + + let handle_msg = ExecuteMsg::Mint { + recipient: "bob".to_string(), + amount: Uint128::new(100), + 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("allowed to minter accounts only")); + + let handle_msg = ExecuteMsg::Mint { + recipient: "bob".to_string(), + amount: Uint128::new(100), + memo: None, + #[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); + + let error = extract_error_msg(handle_result); + assert!(error.contains("allowed to minter accounts only")); + } + + // Query tests + + #[test] + fn test_authenticated_queries() { + 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 no_vk_yet_query_msg = QueryMsg::Balance { + address: "giannis".to_string(), + key: "no_vk_yet".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), no_vk_yet_query_msg); + let error = extract_error_msg(query_result); assert_eq!( - to_binary(&unwrapped_result).unwrap(), - to_binary(&ExecuteAnswer::SetViewingKey { - status: ResponseStatus::Success - }) - .unwrap(), + error, + "Wrong viewing key for this address or viewing key not set".to_string() ); - let query_msg = QueryMsg::Allowance { - owner: "giannis".to_string(), - spender: "lebron".to_string(), - key: vk1.clone(), + let create_vk_msg = ExecuteMsg::CreateViewingKey { + entropy: Some("34".to_string()), + #[cfg(feature = "gas_evaporation")] + gas_target: None, + padding: None, }; - 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"), + let info = mock_info("giannis", &[]); + 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"), }; - 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"), + let query_balance_msg = QueryMsg::Balance { + address: "giannis".to_string(), + key: vk, }; - assert_eq!(allowance, Uint128::new(2000)); - let query_msg = QueryMsg::Allowance { - owner: "lebron".to_string(), - spender: "giannis".to_string(), - key: vk2.clone(), + 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"), }; - 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!(balance, Uint128::new(5000)); + + let wrong_vk_query_msg = QueryMsg::Balance { + address: "giannis".to_string(), + key: "wrong_vk".to_string(), }; - assert_eq!(allowance, Uint128::new(0)); + let query_result = query(deps.as_ref(), mock_env(), wrong_vk_query_msg); + let error = extract_error_msg(query_result); + assert_eq!( + error, + "Wrong viewing key for this address or viewing key not set".to_string() + ); } #[test] - fn test_query_all_allowances() { - let num_owners = 3; - let num_spenders = 20; - let vk = "key".to_string(); + fn test_query_token_info() { + 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_config: InitConfig = from_binary(&Binary::from( + r#"{ "public_total_supply": true }"#.as_bytes(), + )) + .unwrap(); + let init_supply = Uint128::new(5000); - let initial_balances: Vec = (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); + 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: Some(init_config), + 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() ); - 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(), - ); + let query_msg = QueryMsg::TokenInfo {}; + 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::TokenInfo { + name, + symbol, + decimals, + total_supply, + } => { + assert_eq!(name, init_name); + assert_eq!(symbol, init_symbol); + assert_eq!(decimals, init_decimals); + assert_eq!(total_supply, Some(Uint128::new(5000))); + } + _ => panic!("unexpected"), } + } - 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); + #[test] + fn test_query_token_config() { + 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_config: InitConfig = from_binary(&Binary::from( + format!( + "{{\"public_total_supply\":{}, + \"enable_deposit\":{}, + \"enable_redeem\":{}, + \"enable_mint\":{}, + \"enable_burn\":{}}}", + true, false, false, true, false + ) + .as_bytes(), + )) + .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: ResponseStatus::Success - }) - .unwrap(), - ); - } - } + let init_supply = Uint128::new(5000); - 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 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: Some(init_config), + 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::AllowancesGiven { - owner: "owner1".to_string(), - key: vk.clone(), - page: Some(1), - page_size: 5, - }; + let query_msg = QueryMsg::TokenConfig {}; let query_result = query(deps.as_ref(), mock_env(), query_msg); - match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::AllowancesGiven { - owner, - allowances, - count, + 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::TokenConfig { + public_total_supply, + deposit_enabled, + redeem_enabled, + mint_enabled, + burn_enabled, + supported_denoms, } => { - 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); + assert_eq!(public_total_supply, true); + assert_eq!(deposit_enabled, false); + assert_eq!(redeem_enabled, false); + assert_eq!(mint_enabled, true); + assert_eq!(burn_enabled, false); + assert_eq!(supported_denoms.len(), 0); } - _ => panic!("Unexpected"), - }; + _ => 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"), - }; + #[test] + fn test_query_exchange_rate() { + // test more dec 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 = 8; - 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 init_supply = Uint128::new(5000); - 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 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::AllowancesReceived { - spender: "spender0".to_string(), - key: vk.clone(), - page: None, - page_size: 10, - }; + let query_msg = QueryMsg::ExchangeRate {}; 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); + 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"), - }; + _ => panic!("unexpected"), + } - let query_msg = QueryMsg::AllowancesReceived { - spender: "spender1".to_string(), - key: vk.clone(), - page: Some(1), - page_size: 1, + // 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); - 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); + 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"), - }; - } + _ => panic!("unexpected"), + } - #[test] - fn test_query_balance() { - let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "bob".to_string(), - amount: Uint128::new(5000), - }]); + // 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 handle_msg = ExecuteMsg::SetViewingKey { - key: "key".to_string(), - padding: None, - }; - let info = mock_info("bob", &[]); + 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"), + } - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + // 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 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 init_supply = Uint128::new(5000); - let query_msg = QueryMsg::Balance { - address: "bob".to_string(), - key: "wrong_key".to_string(), + 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 query_result = query(deps.as_ref(), mock_env(), query_msg); - let error = extract_error_msg(query_result); - assert!(error.contains("Wrong viewing key")); + 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::Balance { - address: "bob".to_string(), - key: "key".to_string(), - }; + let query_msg = QueryMsg::ExchangeRate {}; 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)); + 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() { + fn test_query_allowance() { let (init_result, mut deps) = init_helper(vec![InitialBalance { - address: "bob".to_string(), + address: "giannis".to_string(), amount: Uint128::new(5000), }]); assert!( @@ -5326,626 +4282,412 @@ mod tests { 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())); + 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: None, - decoys: None, - entropy: None, - padding: None, - }; - let info = mock_info("bob", &[]); + let vk1 = "key1".to_string(); + let vk2 = "key2".to_string(); - let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + 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 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: vk1.clone(), + #[cfg(feature = "gas_evaporation")] + gas_target: None, padding: None, }; - let info = mock_info("bob", &[]); + let info = mock_info("lebron", &[]); 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: "mango".to_string(), - amount: Uint128::new(2500), - memo: None, - decoys: None, - entropy: None, + 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(), + #[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 query_msg = QueryMsg::TransferHistory { - address: "bob".to_string(), - key: "key".to_string(), - page: None, - page_size: 0, - should_filter_decoys: false, - }; - 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()); + 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: false, + 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(), 3); + assert_eq!(allowance, Uint128::new(2000)); - let query_msg = QueryMsg::TransferHistory { - address: "bob".to_string(), - key: "key".to_string(), - page: None, - page_size: 2, - 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: "bob".to_string(), - key: "key".to_string(), - page: Some(1), - page_size: 2, - should_filter_decoys: false, + 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_transfer_history_with_decoys() { - let (init_result, mut deps) = init_helper(vec![ - InitialBalance { - address: "bob".to_string(), + fn test_query_all_allowances() { + let num_owners = 3; + let num_spenders = 20; + let vk = "key".to_string(); + + let initial_balances: Vec = (0..num_owners) + .into_iter() + .map(|i| InitialBalance { + address: format!("owner{}", i), amount: Uint128::new(5000), - }, - InitialBalance { - address: "jhon".to_string(), - amount: Uint128::new(7000), - }, - ]); + }) + .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_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::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: "banana_key".to_string(), - padding: None, - }; - let info = mock_info("banana", &[]); - - 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 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", &[]); - - let handle_result = execute(deps.as_mut(), mock_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(), + ); + } - let query_msg = QueryMsg::TransferHistory { - 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 transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, - _ => panic!("Unexpected"), - }; - assert_eq!(transfers.len(), 2); + 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 query_msg = QueryMsg::TransferHistory { - 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 transfers = match from_binary(&query_result.unwrap()).unwrap() { - QueryAnswer::TransferHistory { txs, .. } => txs, - _ => panic!("Unexpected"), - }; - assert_eq!(transfers.len(), 2); + let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); - 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_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); + 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 +4695,6 @@ mod tests { 1000, vec!["uscrt".to_string()], ); - assert!( init_result.is_ok(), "Init failed: {}", @@ -5962,54 +4703,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 +4733,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 +4751,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 +4762,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 +4785,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 +4800,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 +4815,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 +4826,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: 8735437960206903, action: TxAction::Transfer { from: Addr::unchecked("bob".to_string()), sender: Addr::unchecked("bob".to_string()), @@ -6232,8 +4855,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 8, + Tx { + id: 6519057655056815, action: TxAction::Transfer { from: Addr::unchecked("bob".to_string()), sender: Addr::unchecked("bob".to_string()), @@ -6247,8 +4870,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 7, + Tx { + id: 2105964828411645, action: TxAction::Transfer { from: Addr::unchecked("bob".to_string()), sender: Addr::unchecked("bob".to_string()), @@ -6262,8 +4885,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 6, + Tx { + id: 7517649082682890, action: TxAction::Deposit {}, coins: Coin { denom: "uscrt".to_string(), @@ -6273,8 +4896,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 5, + Tx { + id: 5298675660782133, action: TxAction::Mint { minter: Addr::unchecked("admin".to_string()), recipient: Addr::unchecked("bob".to_string()), @@ -6287,8 +4910,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 4, + Tx { + id: 3863562430182029, action: TxAction::Redeem {}, coins: Coin { denom: "SECSEC".to_string(), @@ -6298,8 +4921,8 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 3, + Tx { + id: 3942814133456943, action: TxAction::Burn { burner: Addr::unchecked("bob".to_string()), owner: Addr::unchecked("bob".to_string()), @@ -6312,15 +4935,15 @@ mod tests { block_time: 1571797419, block_height: 12345, }, - ExtendedTx { - id: 1, + Tx { + id: 5746099005188254, action: TxAction::Mint { minter: Addr::unchecked("admin".to_string()), recipient: Addr::unchecked("bob".to_string()), }, coins: Coin { denom: "SECSEC".to_string(), - amount: Uint128::new(5000), + amount: Uint128::new(10000), }, memo: Some("Initial Balance".to_string()), @@ -6329,6 +4952,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..524f8c97 --- /dev/null +++ b/src/dwb.rs @@ -0,0 +1,635 @@ +use constant_time_eq::constant_time_eq; +use cosmwasm_std::{Api, CanonicalAddr, StdError, StdResult, Storage}; +use rand_core::RngCore; +use secret_toolkit::storage::Item; +use secret_toolkit_crypto::ContractPrng; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; + +use crate::btbe::{settle_dwb_entry, stored_balance}; +#[cfg(feature = "gas_tracking")] +use crate::gas_tracker::GasTracker; +#[cfg(feature = "gas_tracking")] +use crate::msg::QueryAnswer; +use crate::state::{safe_add, safe_add_u64}; +use crate::transaction_history::{Tx, TRANSACTIONS}; +#[cfg(feature = "gas_tracking")] +use cosmwasm_std::{to_binary, Binary}; + +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 = 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 = Item::new(KEY_TX_NODES); +pub static TX_NODES_COUNT: Item = Item::new(KEY_TX_NODES_COUNT); + +fn store_new_tx_node(store: &mut dyn Storage, tx_node: TxNode) -> StdResult { + // 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 { + 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 { + 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, + is_from_self: bool, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, + ) -> StdResult { + #[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"); + + // check that the owner has sufficient funds to perform the transfer + 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}", + ))); + }; + + // record the event in the dwb entry + dwb_entry.add_tx_node(store, tx_id)?; + + // *_from action where sender is the owner, repeat the event in history + if is_from_self { + dwb_entry.add_tx_node(store, tx_id)?; + } + + #[cfg(feature = "gas_tracking")] + group1.log("add_tx_node"); + + dwb_entry.set_recipient(address)?; + + #[cfg(feature = "gas_tracking")] + group1.logf(format!( + "@entry=address:{}, amount:{}", + dwb_entry.recipient()?, + dwb_entry.amount()? + )); + + settle_dwb_entry( + store, + &dwb_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]; + + 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]; + settle_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 { + 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(result)) + } + + pub fn recipient_slice(&self) -> &[u8] { + &self.0[..DWB_RECIPIENT_BYTES] + } + + pub fn recipient(&self) -> StdResult { + 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 { + 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 { + 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 { + 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 { + 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 { + // 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) -> StdResult { + let amount_spent = amount_spent.unwrap_or_default(); + let amount_spent_u64 = amount_spent + .try_into() + .map_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 as_vec(&self, store: &dyn Storage, api: &dyn Api) -> StdResult> { + 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] +pub fn constant_time_if_else(condition: u32, then: usize, els: usize) -> usize { + (then * condition as usize) | (els * (1 - condition as usize)) +} + +#[inline] +pub fn constant_time_if_else_u32(condition: u32, then: u32, els: u32) -> u32 { + (then * condition) | (els * (1 - condition)) +} + +#[cfg(feature = "gas_tracking")] +pub fn log_dwb(storage: &dyn Storage) -> StdResult { + 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, + ) -> ( + StdResult, + OwnedDeps, + ) { + 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/execute.rs b/src/execute.rs new file mode 100644 index 00000000..59af4377 --- /dev/null +++ b/src/execute.rs @@ -0,0 +1,230 @@ +use cosmwasm_std::{ + to_binary, Addr, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Storage, Uint128, +}; +use secret_toolkit::notification::Notification; +use secret_toolkit::permit::{AllRevokedInterval, RevokedPermits, RevokedPermitsStore}; +use secret_toolkit::viewing_key::{ViewingKey, ViewingKeyStore}; +use secret_toolkit_crypto::ContractPrng; + +use crate::msg::{ExecuteAnswer, ResponseStatus::Success}; +use crate::notifications::AllowanceNotification; +use crate::state::{ + AllowancesStore, ReceiverHashStore, INTERNAL_SECRET_SENSITIVE, NOTIFICATIONS_ENABLED, +}; + +// viewing key functions + +pub fn try_set_key(deps: DepsMut, info: MessageInfo, key: String) -> StdResult { + ViewingKey::set(deps.storage, info.sender.as_str(), key.as_str()); + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::SetViewingKey { + status: Success, + })?), + ) +} + +pub fn try_create_key( + deps: DepsMut, + env: Env, + info: MessageInfo, + entropy: Option, + rng: &mut ContractPrng, +) -> StdResult { + 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); + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::CreateViewingKey { key })?)) +} + +// register receive function + +pub fn try_register_receive( + deps: DepsMut, + info: MessageInfo, + code_hash: String, +) -> StdResult { + ReceiverHashStore::save(deps.storage, &info.sender, code_hash)?; + + let data = to_binary(&ExecuteAnswer::RegisterReceive { status: Success })?; + Ok(Response::new() + .add_attribute("register_status", "success") + .set_data(data)) +} + +// allowance functions + +fn insufficient_allowance(allowance: u128, required: u128) -> StdError { + StdError::generic_err(format!( + "insufficient allowance: allowance={allowance}, required={required}", + )) +} + +pub fn use_allowance( + storage: &mut dyn Storage, + env: &Env, + owner: &Addr, + spender: &Addr, + amount: u128, +) -> StdResult<()> { + let mut allowance = AllowancesStore::load(storage, owner, spender); + + if allowance.is_expired_at(&env.block) || allowance.amount == 0 { + return Err(insufficient_allowance(0, amount)); + } + if let Some(new_allowance) = allowance.amount.checked_sub(amount) { + allowance.amount = new_allowance; + } else { + return Err(insufficient_allowance(allowance.amount, amount)); + } + + AllowancesStore::save(storage, owner, spender, &allowance)?; + + Ok(()) +} + +pub fn try_increase_allowance( + deps: DepsMut, + env: Env, + info: MessageInfo, + spender: String, + amount: Uint128, + expiration: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.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); + + // If the previous allowance has expired, reset the allowance. + // Without this users can take advantage of an expired allowance given to + // them long ago. + if allowance.is_expired_at(&env.block) { + allowance.amount = amount.u128(); + allowance.expiration = None; + } else { + allowance.amount = allowance.amount.saturating_add(amount.u128()); + } + + if expiration.is_some() { + allowance.expiration = expiration; + } + let new_amount = allowance.amount; + AllowancesStore::save(deps.storage, &info.sender, &spender, &allowance)?; + + let mut resp = Response::new().set_data(to_binary(&ExecuteAnswer::IncreaseAllowance { + owner: info.sender.clone(), + spender: spender.clone(), + allowance: Uint128::from(new_amount), + })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let notification = Notification::new( + spender, + AllowanceNotification { + amount: new_amount, + allower: info.sender, + expiration, + }, + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp + .add_attribute_plaintext(notification.id_plaintext(), notification.data_plaintext()); + } + + Ok(resp) +} + +pub fn try_decrease_allowance( + deps: DepsMut, + env: Env, + info: MessageInfo, + spender: String, + amount: Uint128, + expiration: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.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); + + // If the previous allowance has expired, reset the allowance. + // Without this users can take advantage of an expired allowance given to + // them long ago. + if allowance.is_expired_at(&env.block) { + allowance.amount = 0; + allowance.expiration = None; + } else { + allowance.amount = allowance.amount.saturating_sub(amount.u128()); + } + + if expiration.is_some() { + allowance.expiration = expiration; + } + let new_amount = allowance.amount; + AllowancesStore::save(deps.storage, &info.sender, &spender, &allowance)?; + + let mut resp = Response::new().set_data(to_binary(&ExecuteAnswer::DecreaseAllowance { + owner: info.sender.clone(), + spender: spender.clone(), + allowance: Uint128::from(new_amount), + })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let notification = Notification::new( + spender, + AllowanceNotification { + amount: new_amount, + allower: info.sender, + expiration, + }, + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp + .add_attribute_plaintext(notification.id_plaintext(), notification.data_plaintext()); + } + + Ok(resp) +} + +// SNIP 24, 24.1 permit functions + +pub fn revoke_permit(deps: DepsMut, info: MessageInfo, permit_name: String) -> StdResult { + RevokedPermits::revoke_permit(deps.storage, info.sender.as_str(), &permit_name); + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::RevokePermit { status: Success })?)) +} + +pub fn revoke_all_permits( + deps: DepsMut, + info: MessageInfo, + interval: AllRevokedInterval, +) -> StdResult { + let revocation_id = + RevokedPermits::revoke_all_permits(deps.storage, info.sender.as_str(), &interval)?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::RevokeAllPermits { + status: Success, + revocation_id: Some(revocation_id.to_string()), + })?), + ) +} + +pub fn delete_permit_revocation( + deps: DepsMut, + info: MessageInfo, + revocation_id: String, +) -> StdResult { + RevokedPermits::delete_revocation(deps.storage, info.sender.as_str(), revocation_id.as_str())?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::DeletePermitRevocation { + status: Success, + })?), + ) +} diff --git a/src/execute_admin.rs b/src/execute_admin.rs new file mode 100644 index 00000000..c2bc7e2c --- /dev/null +++ b/src/execute_admin.rs @@ -0,0 +1,157 @@ +use cosmwasm_std::{to_binary, Addr, DepsMut, Response, StdError, StdResult}; + +use crate::msg::ContractStatusLevel; +use crate::msg::{ExecuteAnswer, ResponseStatus::Success}; +use crate::state::{Config, MintersStore, CONFIG, CONTRACT_STATUS, NOTIFICATIONS_ENABLED}; + +// All the functions in this file MUST only be executed after confirming the sender is the admin + +pub fn change_admin(deps: DepsMut, constants: &mut Config, address: String) -> StdResult { + let address = deps.api.addr_validate(address.as_str())?; + + constants.admin = address; + CONFIG.save(deps.storage, constants)?; + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::ChangeAdmin { status: Success })?)) +} + +pub fn add_supported_denoms( + deps: DepsMut, + config: &mut Config, + denoms: Vec, +) -> StdResult { + if !config.can_modify_denoms { + return Err(StdError::generic_err( + "Cannot modify denoms for this contract", + )); + } + + for denom in denoms.iter() { + if !config.supported_denoms.contains(denom) { + config.supported_denoms.push(denom.clone()); + } + } + + CONFIG.save(deps.storage, config)?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::AddSupportedDenoms { + status: Success, + })?), + ) +} + +pub fn remove_supported_denoms( + deps: DepsMut, + config: &mut Config, + denoms: Vec, +) -> StdResult { + if !config.can_modify_denoms { + return Err(StdError::generic_err( + "Cannot modify denoms for this contract", + )); + } + + for denom in denoms.iter() { + config.supported_denoms.retain(|x| x != denom); + } + + CONFIG.save(deps.storage, config)?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::RemoveSupportedDenoms { + status: Success, + })?), + ) +} + +pub fn set_contract_status( + deps: DepsMut, + status_level: ContractStatusLevel, +) -> StdResult { + CONTRACT_STATUS.save(deps.storage, &status_level)?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::SetContractStatus { + status: Success, + })?), + ) +} + +pub fn add_minters( + deps: DepsMut, + constants: &Config, + minters_to_add: Vec, +) -> StdResult { + if !constants.mint_is_enabled { + return Err(StdError::generic_err( + "Mint functionality is not enabled for this token.", + )); + } + + let minters_to_add: Vec = minters_to_add + .iter() + .map(|minter| deps.api.addr_validate(minter.as_str()).unwrap()) + .collect(); + MintersStore::add_minters(deps.storage, minters_to_add)?; + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::AddMinters { status: Success })?)) +} + +pub fn remove_minters( + deps: DepsMut, + constants: &Config, + minters_to_remove: Vec, +) -> StdResult { + if !constants.mint_is_enabled { + return Err(StdError::generic_err( + "Mint functionality is not enabled for this token.", + )); + } + + let minters_to_remove: StdResult> = minters_to_remove + .iter() + .map(|minter| deps.api.addr_validate(minter.as_str())) + .collect(); + MintersStore::remove_minters(deps.storage, minters_to_remove?)?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::RemoveMinters { + status: Success, + })?), + ) +} + +pub fn set_minters( + deps: DepsMut, + constants: &Config, + minters_to_set: Vec, +) -> StdResult { + if !constants.mint_is_enabled { + return Err(StdError::generic_err( + "Mint functionality is not enabled for this token.", + )); + } + + let minters_to_set: Vec = minters_to_set + .iter() + .map(|minter| deps.api.addr_validate(minter.as_str()).unwrap()) + .collect(); + MintersStore::save(deps.storage, minters_to_set)?; + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::SetMinters { status: Success })?)) +} + +// SNIP-52 functions + +pub fn set_notification_status(deps: DepsMut, enabled: bool) -> StdResult { + NOTIFICATIONS_ENABLED.save(deps.storage, &enabled)?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::SetNotificationStatus { + status: Success, + })?), + ) +} + +// end SNIP-52 functions diff --git a/src/execute_deposit_redeem.rs b/src/execute_deposit_redeem.rs new file mode 100644 index 00000000..4784f25d --- /dev/null +++ b/src/execute_deposit_redeem.rs @@ -0,0 +1,202 @@ +use cosmwasm_std::{ + to_binary, BankMsg, BlockInfo, CanonicalAddr, Coin, CosmosMsg, DepsMut, Env, MessageInfo, + Response, StdError, StdResult, Storage, Uint128, +}; +use secret_toolkit_crypto::ContractPrng; + +use crate::dwb::DWB; +use crate::msg::{ExecuteAnswer, ResponseStatus::Success}; +use crate::state::{safe_add, CONFIG, TOTAL_SUPPLY}; +use crate::transaction_history::{store_deposit_action, store_redeem_action}; +#[cfg(feature = "gas_tracking")] +use crate::gas_tracker::GasTracker; + +// deposit functions + +pub fn try_deposit( + deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, +) -> StdResult { + let constants = CONFIG.load(deps.storage)?; + + let mut amount = Uint128::zero(); + + for coin in &info.funds { + if constants.supported_denoms.contains(&coin.denom) { + amount += coin.amount + } else { + return Err(StdError::generic_err(format!( + "Tried to deposit an unsupported coin {}", + coin.denom + ))); + } + } + + if amount.is_zero() { + return Err(StdError::generic_err("No funds were sent to be deposited")); + } + + let mut raw_amount = amount.u128(); + + if !constants.deposit_is_enabled { + return Err(StdError::generic_err( + "Deposit functionality is not enabled.", + )); + } + + let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; + raw_amount = safe_add(&mut total_supply, raw_amount); + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + + let sender_address = deps.api.addr_canonicalize(info.sender.as_str())?; + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + // we know that funds.len() > 0, because amount > 0 + // use the first denom given for tx record + let denom = info.funds.first().unwrap().denom.clone(); + + perform_deposit( + deps.storage, + rng, + &sender_address, + raw_amount, + denom, + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + let resp = Response::new().set_data(to_binary(&ExecuteAnswer::Deposit { status: Success })?); + + #[cfg(feature = "gas_tracking")] + return Ok(tracker.add_to_response(resp)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) +} + +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(()) +} + +// redeem functions + +pub fn try_redeem( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, + denom: Option, +) -> StdResult { + let constants = CONFIG.load(deps.storage)?; + if !constants.redeem_is_enabled { + return Err(StdError::generic_err( + "Redeem functionality is not enabled for this token.", + )); + } + + // if denom is none and there is only 1 supported denom then we don't need to check anything + let withdraw_denom = if denom.is_none() && constants.supported_denoms.len() == 1 { + constants.supported_denoms.first().unwrap().clone() + // if denom is specified make sure it's on the list before trying to withdraw with it + } else if denom.is_some() && constants.supported_denoms.contains(denom.as_ref().unwrap()) { + denom.unwrap() + // error handling + } else if denom.is_none() { + return Err(StdError::generic_err( + "Tried to redeem without specifying denom, but multiple coins are supported", + )); + } else { + return Err(StdError::generic_err( + "Tried to redeem for an unsupported coin", + )); + }; + + let sender_address = deps.api.addr_canonicalize(info.sender.as_str())?; + let amount_raw = amount.u128(); + + 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, + tx_id, + amount_raw, + "redeem", + false, + #[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)?; + } else { + return Err(StdError::generic_err( + "You are trying to redeem more tokens than what is available in the total supply", + )); + } + + let token_reserve = deps + .querier + .query_balance(&env.contract.address, &withdraw_denom)? + .amount; + if amount > token_reserve { + return Err(StdError::generic_err(format!( + "You are trying to redeem for more {withdraw_denom} than the contract has in its reserve", + ))); + } + + let withdrawal_coins: Vec = vec![Coin { + denom: withdraw_denom, + amount, + }]; + + let message = CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.clone().into_string(), + amount: withdrawal_coins, + }); + let data = to_binary(&ExecuteAnswer::Redeem { status: Success })?; + let res = Response::new().add_message(message).set_data(data); + Ok(res) +} diff --git a/src/execute_mint_burn.rs b/src/execute_mint_burn.rs new file mode 100644 index 00000000..1afd90f8 --- /dev/null +++ b/src/execute_mint_burn.rs @@ -0,0 +1,580 @@ +use cosmwasm_std::{ + to_binary, Addr, BlockInfo, CanonicalAddr, DepsMut, Env, MessageInfo, Response, StdError, + StdResult, Storage, Uint128, +}; +use secret_toolkit::notification::Notification; +use secret_toolkit_crypto::ContractPrng; + +use crate::batch; +use crate::dwb::DWB; +use crate::execute::use_allowance; +use crate::msg::{ExecuteAnswer, ResponseStatus::Success}; +use crate::notifications::{ + render_group_notification, MultiRecvdNotification, MultiSpentNotification, RecvdNotification, + SpentNotification, +}; +use crate::state::{ + safe_add, MintersStore, CONFIG, INTERNAL_SECRET_SENSITIVE, NOTIFICATIONS_ENABLED, TOTAL_SUPPLY, +}; +use crate::transaction_history::{store_burn_action, store_mint_action}; +#[cfg(feature = "gas_tracking")] +use crate::gas_tracker::GasTracker; + +// mint functions + +#[allow(clippy::too_many_arguments)] +pub fn try_mint( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + recipient: String, + amount: Uint128, + memo: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let recipient = deps.api.addr_validate(recipient.as_str())?; + + let constants = CONFIG.load(deps.storage)?; + + if !constants.mint_is_enabled { + return Err(StdError::generic_err( + "Mint functionality is not enabled for this token.", + )); + } + + let minters = MintersStore::load(deps.storage)?; + if !minters.contains(&info.sender) { + return Err(StdError::generic_err( + "Minting is allowed to minter accounts only", + )); + } + + let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; + 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); + + let memo_len = memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + // 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.clone(), + Uint128::new(minted_amount), + constants.symbol, + memo, + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + let mut resp = Response::new().set_data(to_binary(&ExecuteAnswer::Mint { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let received_notification = Notification::new( + recipient, + RecvdNotification { + amount: minted_amount, + sender: None, + memo_len, + sender_is_owner: true, + }, + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ); + } + + #[cfg(feature = "gas_tracking")] + return Ok(tracker.add_to_response(resp)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) +} + +pub fn try_batch_mint( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + actions: Vec, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let constants = CONFIG.load(deps.storage)?; + + if !constants.mint_is_enabled { + return Err(StdError::generic_err( + "Mint functionality is not enabled for this token.", + )); + } + + let minters = MintersStore::load(deps.storage)?; + if !minters.contains(&info.sender) { + return Err(StdError::generic_err( + "Minting is allowed to minter accounts only", + )); + } + + 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); + + notifications.push(Notification::new( + recipient.clone(), + RecvdNotification { + amount: actual_amount, + sender: None, + memo_len: action.memo.as_ref().map(|s| s.len()).unwrap_or_default(), + sender_is_owner: true, + }, + )); + + try_mint_impl( + &mut deps, + rng, + info.sender.clone(), + recipient, + Uint128::new(actual_amount), + constants.symbol.clone(), + action.memo, + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + } + + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + + let mut resp = + Response::new().set_data(to_binary(&ExecuteAnswer::BatchMint { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + resp = render_group_notification( + deps.api, + MultiRecvdNotification(notifications), + &env.transaction.unwrap().hash, + env.block.random.unwrap(), + secret, + resp, + )?; + } + + Ok(resp) +} + +#[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, + block: &cosmwasm_std::BlockInfo, + #[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())?; + + perform_mint( + deps.storage, + rng, + &raw_minter, + &raw_recipient, + raw_amount, + denom, + memo, + block, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub fn perform_mint( + store: &mut dyn Storage, + rng: &mut ContractPrng, + minter: &CanonicalAddr, + to: &CanonicalAddr, + amount: u128, + denom: String, + memo: Option, + 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)?; + + // sender and owner are different + if minter != to { + // settle the sender's account too + dwb.settle_sender_or_owner_account( + store, + minter, + tx_id, + 0, + "mint", + false, + #[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(()) +} + +// burn functions + +/// Burn tokens +/// +/// Remove `amount` tokens from the system irreversibly, from signer account +/// +/// @param amount the amount of money to burn +pub fn try_burn( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, + memo: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let constants = CONFIG.load(deps.storage)?; + if !constants.burn_is_enabled { + return Err(StdError::generic_err( + "Burn functionality is not enabled for this token.", + )); + } + + let raw_amount = amount.u128(); + let raw_burn_address = deps.api.addr_canonicalize(info.sender.as_str())?; + + let memo_len = memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + let tx_id = store_burn_action( + deps.storage, + 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, + "burn", + false, + #[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; + } else { + return Err(StdError::generic_err( + "You're trying to burn more than is available in the total supply", + )); + } + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + + let mut resp = Response::new().set_data(to_binary(&ExecuteAnswer::Burn { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let spent_notification = Notification::new( + info.sender, + SpentNotification { + amount: raw_amount, + actions: 1, + recipient: None, + balance: owner_balance, + memo_len, + }, + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + Ok(resp) +} + +#[allow(clippy::too_many_arguments)] +pub fn try_burn_from( + deps: DepsMut, + env: &Env, + info: MessageInfo, + owner: String, + amount: Uint128, + memo: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.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( + "Burn functionality is not enabled for this token.", + )); + } + + 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())?; + + let memo_len = memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + // store the event + let tx_id = store_burn_action( + deps.storage, + 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, + "burn", + raw_burner == raw_owner, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + // sender and owner are different + if raw_burner != raw_owner { + // also settle sender's account + dwb.settle_sender_or_owner_account( + deps.storage, + &raw_burner, + tx_id, + 0, + "burn", + false, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + } + + DWB.save(deps.storage, &dwb)?; + + // remove from supply + 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; + } else { + return Err(StdError::generic_err( + "You're trying to burn more than is available in the total supply", + )); + } + + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + + let mut resp = + Response::new().set_data(to_binary(&ExecuteAnswer::BurnFrom { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let spent_notification = Notification::new( + owner, + SpentNotification { + amount: raw_amount, + actions: 1, + recipient: None, + balance: owner_balance, + memo_len, + }, + ) + .to_txhash_notification(deps.api, env, secret, None)?; + + resp = resp.add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + Ok(resp) +} + +pub fn try_batch_burn_from( + deps: DepsMut, + env: &Env, + info: MessageInfo, + actions: Vec, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let constants = CONFIG.load(deps.storage)?; + if !constants.burn_is_enabled { + return Err(StdError::generic_err( + "Burn functionality is not enabled for this token.", + )); + } + + 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, &info.sender, amount)?; + + let tx_id = store_burn_action( + deps.storage, + 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, + "burn", + raw_spender == raw_owner, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + // sender and owner are different + if raw_spender != raw_owner { + // also settle the sender's account + dwb.settle_sender_or_owner_account( + deps.storage, + &raw_spender, + tx_id, + 0, + "burn", + false, + #[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) { + total_supply = new_total_supply; + } else { + return Err(StdError::generic_err(format!( + "You're trying to burn more than is available in the total supply: {action:?}", + ))); + } + + spent_notifications.push(Notification::new( + info.sender.clone(), + SpentNotification { + amount, + actions: 1, + recipient: None, + balance: owner_balance, + memo_len: action.memo.as_ref().map(|s| s.len()).unwrap_or_default(), + }, + )); + } + + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + + let mut resp = Response::new().set_data(to_binary(&ExecuteAnswer::BatchBurnFrom { + status: Success, + })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + resp = render_group_notification( + deps.api, + MultiSpentNotification(spent_notifications), + &env.transaction.clone().unwrap().hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + } + + Ok(resp) +} diff --git a/src/execute_transfer_send.rs b/src/execute_transfer_send.rs new file mode 100644 index 00000000..77110476 --- /dev/null +++ b/src/execute_transfer_send.rs @@ -0,0 +1,959 @@ +use cosmwasm_std::{ + to_binary, Addr, Binary, BlockInfo, CanonicalAddr, CosmosMsg, DepsMut, Env, MessageInfo, + Response, StdError, StdResult, Storage, Uint128, +}; +use secret_toolkit::notification::Notification; +use secret_toolkit_crypto::ContractPrng; + +use crate::batch; +use crate::dwb::DWB; +use crate::execute::use_allowance; +use crate::msg::{ExecuteAnswer, ResponseStatus::Success}; +use crate::notifications::{ + render_group_notification, MultiRecvdNotification, MultiSpentNotification, RecvdNotification, + SpentNotification, +}; +use crate::receiver::Snip20ReceiveMsg; +use crate::state::{ReceiverHashStore, CONFIG, INTERNAL_SECRET_SENSITIVE, NOTIFICATIONS_ENABLED}; +use crate::strings::SEND_TO_CONTRACT_ERR_MSG; +use crate::transaction_history::store_transfer_action; +#[cfg(feature = "gas_tracking")] +use crate::gas_tracker::GasTracker; + +// transfer functions + +#[allow(clippy::too_many_arguments)] +pub fn try_transfer( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + recipient: String, + amount: Uint128, + memo: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.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; + + // make sure the sender is not accidentally sending tokens to the contract address + if recipient == env.contract.address { + return Err(StdError::generic_err(SEND_TO_CONTRACT_ERR_MSG)); + } + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + // perform the transfer + let (received_notification, spent_notification) = try_transfer_impl( + &mut deps, + rng, + &info.sender, + &recipient, + amount, + symbol, + memo, + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + #[cfg(feature = "gas_tracking")] + let mut group1 = tracker.group("try_transfer.rest"); + + let mut resp = + Response::new().set_data(to_binary(&ExecuteAnswer::Transfer { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + // render the tokens received notification + let received_notification = + received_notification.to_txhash_notification(deps.api, &env, secret, None)?; + + // render the tokens spent notification + let spent_notification = + spent_notification.to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp + .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(tracker.add_to_response(resp)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) +} + +pub fn try_batch_transfer( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + actions: Vec, +) -> StdResult { + 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_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let symbol = CONFIG.load(deps.storage)?.symbol; + + let mut total_memo_len = 0; + + #[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())?; + + // make sure the sender is not accidentally sending tokens to the contract address + if recipient == env.contract.address { + return Err(StdError::generic_err(SEND_TO_CONTRACT_ERR_MSG)); + } + + total_memo_len += action.memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + let (received_notification, spent_notification) = try_transfer_impl( + &mut deps, + rng, + &info.sender, + &recipient, + action.amount, + symbol.clone(), + action.memo, + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + notifications.push((received_notification, spent_notification)); + } + + let (received_notifications, spent_notifications): ( + Vec>, + Vec>, + ) = notifications.into_iter().unzip(); + + let mut resp = Response::new().set_data(to_binary(&ExecuteAnswer::BatchTransfer { + status: Success, + })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + resp = render_group_notification( + deps.api, + MultiRecvdNotification(received_notifications), + &env.transaction.clone().unwrap().hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + + let total_amount_spent = spent_notifications.iter().fold(0u128, |acc, notification| { + acc.saturating_add(notification.data.amount) + }); + + let spent_notification = Notification::new( + info.sender, + SpentNotification { + amount: total_amount_spent, + actions: num_actions as u32, + recipient: spent_notifications[0].data.recipient.clone(), + balance: spent_notifications.last().unwrap().data.balance, + memo_len: total_memo_len, + }, + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + #[cfg(feature = "gas_tracking")] + return Ok(tracker.add_to_response(resp)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) +} + +#[allow(clippy::too_many_arguments)] +pub fn try_transfer_from( + mut deps: DepsMut, + env: &Env, + info: MessageInfo, + rng: &mut ContractPrng, + owner: String, + recipient: String, + amount: Uint128, + memo: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.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 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, + )?; + + let mut resp = + Response::new().set_data(to_binary(&ExecuteAnswer::TransferFrom { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let received_notification = + received_notification.to_txhash_notification(deps.api, env, secret, None)?; + + let spent_notification = + spent_notification.to_txhash_notification(deps.api, env, secret, None)?; + + resp = resp + .add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + Ok(resp) +} + +pub fn try_batch_transfer_from( + mut deps: DepsMut, + env: &Env, + info: MessageInfo, + rng: &mut ContractPrng, + actions: Vec, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.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())?; + + let (received_notification, spent_notification) = try_transfer_from_impl( + &mut deps, + rng, + env, + &info.sender, + &owner, + &recipient, + action.amount, + symbol.clone(), + action.memo, + )?; + + notifications.push((received_notification, spent_notification)); + } + + let mut resp = Response::new().set_data(to_binary(&ExecuteAnswer::BatchTransferFrom { + status: Success, + })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let (received_notifications, spent_notifications): ( + Vec>, + Vec>, + ) = notifications.into_iter().unzip(); + + let tx_hash = env.transaction.clone().unwrap().hash; + + resp = render_group_notification( + deps.api, + MultiRecvdNotification(received_notifications), + &tx_hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + + resp = render_group_notification( + deps.api, + MultiSpentNotification(spent_notifications), + &tx_hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + } + + Ok(resp) +} + +// send functions + +#[allow(clippy::too_many_arguments)] +pub fn try_send( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + recipient: String, + recipient_code_hash: Option, + amount: Uint128, + memo: Option, + msg: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let recipient = deps.api.addr_validate(recipient.as_str())?; + + let mut messages = vec![]; + let symbol = CONFIG.load(deps.storage)?.symbol; + + // make sure the sender is not accidentally sending tokens to the contract address + if recipient == env.contract.address { + return Err(StdError::generic_err(SEND_TO_CONTRACT_ERR_MSG)); + } + + #[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, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + let mut resp = Response::new() + .add_messages(messages) + .set_data(to_binary(&ExecuteAnswer::Send { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let received_notification = + received_notification.to_txhash_notification(deps.api, &env, secret, None)?; + let spent_notification = + spent_notification.to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp + .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(tracker.add_to_response(resp)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) +} + +pub fn try_batch_send( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + actions: Vec, +) -> StdResult { + 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_SENSITIVE.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; + + let mut total_memo_len = 0; + + #[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())?; + + // make sure the sender is not accidentally sending tokens to the contract address + if recipient == env.contract.address { + return Err(StdError::generic_err(SEND_TO_CONTRACT_ERR_MSG)); + } + + total_memo_len += action.memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + 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, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + notifications.push((received_notification, spent_notification)); + } + + let mut resp = Response::new() + .add_messages(messages) + .set_data(to_binary(&ExecuteAnswer::BatchSend { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let (received_notifications, spent_notifications): ( + Vec>, + Vec>, + ) = notifications.into_iter().unzip(); + + resp = render_group_notification( + deps.api, + MultiRecvdNotification(received_notifications), + &env.transaction.clone().unwrap().hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + + let total_amount_spent = spent_notifications + .iter() + .fold(0u128, |acc, notification| acc + notification.data.amount); + + let spent_notification = Notification::new( + info.sender, + SpentNotification { + amount: total_amount_spent, + actions: num_actions as u32, + recipient: spent_notifications[0].data.recipient.clone(), + balance: spent_notifications.last().unwrap().data.balance, + memo_len: total_memo_len, + }, + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + Ok(resp) +} + +#[allow(clippy::too_many_arguments)] +pub fn try_send_from( + mut deps: DepsMut, + env: Env, + info: &MessageInfo, + rng: &mut ContractPrng, + owner: String, + recipient: String, + recipient_code_hash: Option, + amount: Uint128, + memo: Option, + msg: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.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![]; + let (received_notification, spent_notification) = try_send_from_impl( + &mut deps, + env.clone(), + info, + rng, + &mut messages, + owner, + recipient, + recipient_code_hash, + amount, + memo, + msg, + )?; + + let mut resp = Response::new() + .add_messages(messages) + .set_data(to_binary(&ExecuteAnswer::SendFrom { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let received_notification = + received_notification.to_txhash_notification(deps.api, &env, secret, None)?; + let spent_notification = + spent_notification.to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp + .add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ) + } + + Ok(resp) +} + +pub fn try_batch_send_from( + mut deps: DepsMut, + env: Env, + info: &MessageInfo, + rng: &mut ContractPrng, + actions: Vec, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.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())?; + let (received_notification, spent_notification) = try_send_from_impl( + &mut deps, + env.clone(), + info, + rng, + &mut messages, + owner, + recipient, + action.recipient_code_hash, + action.amount, + action.memo, + action.msg, + )?; + notifications.push((received_notification, spent_notification)); + } + + let mut resp = Response::new().add_messages(messages).set_data(to_binary( + &ExecuteAnswer::BatchSendFrom { status: Success }, + )?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let (received_notifications, spent_notifications): ( + Vec>, + Vec>, + ) = notifications.into_iter().unzip(); + + let tx_hash = env.transaction.clone().unwrap().hash; + + resp = render_group_notification( + deps.api, + MultiRecvdNotification(received_notifications), + &tx_hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + + resp = render_group_notification( + deps.api, + MultiSpentNotification(spent_notifications), + &tx_hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + } + + Ok(resp) +} + +// helper functions + +#[allow(clippy::too_many_arguments)] +fn try_transfer_impl( + deps: &mut DepsMut, + rng: &mut ContractPrng, + owner: &Addr, + recipient: &Addr, + amount: Uint128, + denom: String, + memo: Option, + block: &cosmwasm_std::BlockInfo, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<( + Notification, + Notification, +)> { + // canonicalize owner and recipient addresses + let raw_owner = deps.api.addr_canonicalize(owner.as_str())?; + let raw_recipient = deps.api.addr_canonicalize(recipient.as_str())?; + + // memo length + let memo_len = memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + // create the tokens received notification for recipient + let received_notification = Notification::new( + recipient.clone(), + RecvdNotification { + amount: amount.u128(), + sender: Some(owner.clone()), + memo_len, + sender_is_owner: true, + }, + ); + + // perform the transfer from owner to recipient + let owner_balance = perform_transfer( + deps.storage, + rng, + &raw_owner, + &raw_recipient, + &raw_owner, + amount.u128(), + denom, + memo.clone(), + block, + false, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + // create the tokens spent notification for owner + let spent_notification = Notification::new( + owner.clone(), + SpentNotification { + amount: amount.u128(), + actions: 1, + recipient: Some(recipient.clone()), + balance: owner_balance, + memo_len, + }, + ); + + Ok((received_notification, spent_notification)) +} + +#[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, +) -> StdResult<( + Notification, + Notification, +)> { + 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)?; + + // make sure the sender is not accidentally sending tokens to the contract address + if *recipient == env.contract.address { + return Err(StdError::generic_err(SEND_TO_CONTRACT_ERR_MSG)); + } + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + let memo_len = memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + // create tokens received notification for recipient + let received_notification = Notification::new( + recipient.clone(), + RecvdNotification { + amount: amount.u128(), + sender: Some(owner.clone()), + memo_len, + sender_is_owner: spender == owner, + }, + ); + + // perform the transfer from owner to recipient + let owner_balance = perform_transfer( + deps.storage, + rng, + &raw_owner, + &raw_recipient, + &raw_spender, + raw_amount, + denom, + memo, + &env.block, + true, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + // create tokens spent notification for owner + let spent_notification = Notification::new( + owner.clone(), + SpentNotification { + amount: amount.u128(), + actions: 1, + recipient: Some(recipient.clone()), + balance: owner_balance, + memo_len, + }, + ); + + Ok((received_notification, spent_notification)) +} + +#[allow(clippy::too_many_arguments)] +fn try_send_impl( + deps: &mut DepsMut, + rng: &mut ContractPrng, + messages: &mut Vec, + sender: Addr, + recipient: Addr, + recipient_code_hash: Option, + amount: Uint128, + denom: String, + memo: Option, + msg: Option, + block: &cosmwasm_std::BlockInfo, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<( + Notification, + Notification, +)> { + let (received_notification, spent_notification) = try_transfer_impl( + deps, + rng, + &sender, + &recipient, + amount, + denom, + memo.clone(), + block, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + try_add_receiver_api_callback( + deps.storage, + messages, + recipient, + recipient_code_hash, + msg, + sender.clone(), + sender, + amount, + memo, + )?; + + Ok((received_notification, spent_notification)) +} + +#[allow(clippy::too_many_arguments)] +fn try_send_from_impl( + deps: &mut DepsMut, + env: Env, + info: &MessageInfo, + rng: &mut ContractPrng, + messages: &mut Vec, + owner: Addr, + recipient: Addr, + recipient_code_hash: Option, + amount: Uint128, + memo: Option, + msg: Option, +) -> StdResult<( + Notification, + Notification, +)> { + let spender = info.sender.clone(); + 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(), + )?; + + try_add_receiver_api_callback( + deps.storage, + messages, + recipient, + recipient_code_hash, + msg, + info.sender.clone(), + owner, + amount, + memo, + )?; + + Ok((received_notification, spent_notification)) +} + +#[allow(clippy::too_many_arguments)] +fn perform_transfer( + store: &mut dyn Storage, + rng: &mut ContractPrng, + from: &CanonicalAddr, + to: &CanonicalAddr, + sender: &CanonicalAddr, + amount: u128, + denom: String, + memo: Option, + block: &BlockInfo, + is_from_action: bool, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult { + #[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, + is_from_action && sender == from, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + // sender and owner are different + if sender != from { + // settle the sender's account too + dwb.settle_sender_or_owner_account( + store, + sender, + tx_id, + 0, + transfer_str, + false, + #[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, + )?; + + #[cfg(feature = "gas_tracking")] + let mut group2 = tracker.group("perform_transfer.2"); + + DWB.save(store, &dwb)?; + + #[cfg(feature = "gas_tracking")] + group2.log("DWB.save"); + + Ok(owner_balance) +} + +#[allow(clippy::too_many_arguments)] +fn try_add_receiver_api_callback( + storage: &dyn Storage, + messages: &mut Vec, + recipient: Addr, + recipient_code_hash: Option, + msg: Option, + sender: Addr, + from: Addr, + amount: Uint128, + memo: Option, +) -> StdResult<()> { + if let Some(receiver_hash) = recipient_code_hash { + let receiver_msg = Snip20ReceiveMsg::new(sender, from, amount, memo, msg); + let callback_msg = receiver_msg.into_cosmos_msg(receiver_hash, recipient)?; + + messages.push(callback_msg); + return Ok(()); + } + + let receiver_hash = ReceiverHashStore::may_load(storage, &recipient)?; + if let Some(receiver_hash) = receiver_hash { + let receiver_msg = Snip20ReceiveMsg::new(sender, from, amount, memo, msg); + let callback_msg = receiver_msg.into_cosmos_msg(receiver_hash, recipient)?; + + messages.push(callback_msg); + } + Ok(()) +} diff --git a/src/gas_tracker.rs b/src/gas_tracker.rs new file mode 100644 index 00000000..392e4e9b --- /dev/null +++ b/src/gas_tracker.rs @@ -0,0 +1,75 @@ +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 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), 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..7b04ab05 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,21 @@ +#[macro_use] +extern crate static_assertions as sa; + mod batch; +mod btbe; +mod constants; pub mod contract; +mod dwb; +pub mod execute; +pub mod execute_admin; +pub mod execute_deposit_redeem; +pub mod execute_mint_burn; +pub mod execute_transfer_send; +mod gas_tracker; pub mod msg; +mod notifications; +pub mod query; pub mod receiver; pub mod state; +mod strings; mod transaction_history; diff --git a/src/msg.rs b/src/msg.rs index cc583dd7..ed47e5fd 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -3,11 +3,14 @@ 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}; +#[cfg(feature = "gas_evaporation")] +use cosmwasm_std::Uint64; +use cosmwasm_std::{Addr, Api, Binary, StdError, StdResult, Uint128, Uint64}; +use secret_toolkit::{ + notification::ChannelInfoData, + permit::{AllRevocation, AllRevokedInterval, Permit}, +}; #[cfg_attr(test, derive(Eq, PartialEq))] #[derive(Serialize, Deserialize, Clone, JsonSchema)] @@ -55,7 +58,7 @@ pub struct InitConfig { /// Indicates whether burn functionality should be enabled /// default: False enable_burn: Option, - /// Indicated whether an admin can modify supported denoms + /// Indicates whether an admin can modify supported denoms /// default: False can_modify_denoms: Option, } @@ -93,13 +96,13 @@ pub enum ExecuteMsg { Redeem { amount: Uint128, denom: Option, - decoys: Option>, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, Deposit { - decoys: Option>, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, @@ -108,8 +111,8 @@ pub enum ExecuteMsg { recipient: String, amount: Uint128, memo: Option, - decoys: Option>, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, Send { @@ -118,37 +121,45 @@ pub enum ExecuteMsg { amount: Uint128, msg: Option, memo: Option, - decoys: Option>, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, BatchTransfer { actions: Vec, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, BatchSend { actions: Vec, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, Burn { amount: Uint128, memo: Option, - decoys: Option>, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, RegisterReceive { code_hash: String, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, CreateViewingKey { - entropy: String, + entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, SetViewingKey { key: String, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, @@ -157,12 +168,16 @@ pub enum ExecuteMsg { spender: String, amount: Uint128, expiration: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, DecreaseAllowance { spender: String, amount: Uint128, expiration: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, TransferFrom { @@ -170,8 +185,8 @@ pub enum ExecuteMsg { recipient: String, amount: Uint128, memo: Option, - decoys: Option>, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, SendFrom { @@ -181,31 +196,34 @@ pub enum ExecuteMsg { amount: Uint128, msg: Option, memo: Option, - decoys: Option>, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, BatchTransferFrom { actions: Vec, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, BatchSendFrom { actions: Vec, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, BurnFrom { owner: String, amount: Uint128, memo: Option, - decoys: Option>, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, BatchBurnFrom { actions: Vec, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, @@ -214,119 +232,94 @@ pub enum ExecuteMsg { recipient: String, amount: Uint128, memo: Option, - decoys: Option>, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, BatchMint { actions: Vec, - entropy: Option, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, AddMinters { minters: Vec, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, RemoveMinters { minters: Vec, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, SetMinters { minters: Vec, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, // Admin ChangeAdmin { address: String, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, SetContractStatus { level: ContractStatusLevel, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, /// Add deposit/redeem support for these coin denoms - AddSupportedDenoms { denoms: Vec }, + AddSupportedDenoms { + denoms: Vec, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, + }, /// Remove deposit/redeem support for these coin denoms - RemoveSupportedDenoms { denoms: Vec }, + RemoveSupportedDenoms { + denoms: Vec, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, + }, + /// Enable or disable SNIP-52 notifications + SetNotificationStatus { + enabled: bool, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, + }, // Permit RevokePermit { permit_name: String, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, padding: Option, }, -} - -pub trait Decoyable { - fn get_minimal_decoys_size(&self) -> usize; - fn get_entropy(self) -> Option; -} - -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 { - 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(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(); - } - } - } + // SNIP 24.1 Blanket Permits + /// Revokes all permits. Client can supply a datetime for created_after, created_before, both, or neither. + /// * created_before – makes it so any permits using a created value less than this datetime will be rejected + /// * created_after – makes it so any permits using a created value greater than this datetime will be rejected + /// * both created_before and created_after – makes it so any permits using a created value between these two datetimes will be rejected + /// * neither – makes it so ANY permit will be rejected. + /// in this case, the contract MUST return a revocation ID of "REVOKED_ALL". this action is idempotent + RevokeAllPermits { + interval: AllRevokedInterval, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, + }, - if min_decoys_count == usize::MAX { - 0 - } else { - min_decoys_count - } + /// Deletes a previously issued permit revocation. + DeletePermitRevocation { + revocation_id: String, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, + }, } #[derive(Serialize, Deserialize, JsonSchema, Debug)] @@ -426,11 +419,79 @@ pub enum ExecuteAnswer { RemoveSupportedDenoms { status: ResponseStatus, }, + SetNotificationStatus { + status: ResponseStatus, + }, // Permit RevokePermit { status: ResponseStatus, }, + + // SNIP 24.1 - Blanket Permits + RevokeAllPermits { + status: ResponseStatus, + revocation_id: Option, + }, + + DeletePermitRevocation { + status: ResponseStatus, + }, +} + +#[cfg(feature = "gas_evaporation")] +pub trait Evaporator { + fn evaporate_to_target(&self, api: &dyn Api) -> StdResult; +} + +#[cfg(feature = "gas_evaporation")] +impl Evaporator for ExecuteMsg { + fn evaporate_to_target(&self, api: &dyn Api) -> StdResult { + 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::SetNotificationStatus { gas_targe, .. } + | ExecuteMsg::RevokePermit { gas_target, .. } + | ExecuteMsg::RevokeAllPermits { gas_target, .. } + | ExecuteMsg::DeletePermitRevocation { 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)] @@ -467,20 +528,52 @@ pub enum QueryMsg { key: String, page: Option, page_size: u32, - should_filter_decoys: bool, }, TransactionHistory { address: String, key: String, page: Option, 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, + txhash: Option, + viewer: ViewerInfo, + }, + + // SNIP 24.1 + ListPermitRevocations { + // `page` and `page_size` do nothing here because max revocations is only 10 but included + // to satisfy the SNIP24.1 spec + page: Option, + page_size: Option, + 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 +610,14 @@ 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())) + } + Self::ListPermitRevocations { 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 +645,22 @@ pub enum QueryWithPermit { TransferHistory { page: Option, page_size: u32, - should_filter_decoys: bool, }, TransactionHistory { page: Option, page_size: u32, - should_filter_decoys: bool, + }, + // SNIP-52 Private Push Notifications + ChannelInfo { + channels: Vec, + txhash: Option, + }, + // SNIP 24.1 + ListPermitRevocations { + // `page` and `page_size` do nothing here because max revocations is only 10 but included + // to satisfy the SNIP24.1 spec + page: Option, + page_size: Option, }, } @@ -596,12 +707,8 @@ pub enum QueryAnswer { Balance { amount: Uint128, }, - TransferHistory { - txs: Vec, - total: Option, - }, TransactionHistory { - txs: Vec, + txs: Vec, total: Option, }, ViewingKeyError { @@ -610,6 +717,28 @@ pub enum QueryAnswer { Minters { minters: Vec, }, + + // SNIP-52 Private Push Notifications + ListChannels { + channels: Vec, + }, + ChannelInfo { + /// scopes validity of this response + as_of_block: Uint64, + /// shared secret in base64 + seed: Binary, + channels: Vec, + }, + + // SNIP-24.1 + ListPermitRevocations { + revocations: Vec, + }, + + #[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..8c2a321b --- /dev/null +++ b/src/notifications.rs @@ -0,0 +1,438 @@ +use std::collections::HashMap; + +use cosmwasm_std::{Addr, Api, Binary, CanonicalAddr, Response, StdResult}; +use minicbor::Encoder; +use primitive_types::{U256, U512}; +use secret_toolkit::notification::{ + get_seed, notification_id, xor_bytes, DirectChannel, EncoderExt, GroupChannel, Notification, + CBL_ADDRESS, CBL_ARRAY_SHORT, CBL_BIGNUM_U64, CBL_TIMESTAMP, CBL_U8, +}; +use secret_toolkit_crypto::{hkdf_sha_512, sha_256}; +use serde::{Deserialize, Serialize}; + +const ZERO_ADDR: [u8; 20] = [0u8; 20]; + +// maximum value that can be stored in 62 bits +const U62_MAX: u128 = (1 << 62) - 1; + +// maximum value that can be stored in 63 bits +const U63_MAX: u128 = (1 << 63) - 1; + +#[derive(Serialize, Debug, Deserialize, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct RecvdNotification { + pub amount: u128, + pub sender: Option, + pub memo_len: usize, + pub sender_is_owner: bool, +} + +/// ```cddl +/// recvd = [ +/// amount: biguint .size 8, ; transfer amount in base denomination +/// sender: bstr .size 20, ; number of actions the execution performed +/// memo_len: uint .size 1, ; byte sequence of first recipient's canonical address +/// ] +/// ``` +impl DirectChannel for RecvdNotification { + const CHANNEL_ID: &'static str = "recvd"; + #[cfg(test)] + const CDDL_SCHEMA: &'static str = + "recvd=[amount:biguint .size 8,sender:bstr .size 54,memo_len:uint .size 1]"; + #[cfg(not(test))] + const CDDL_SCHEMA: &'static str = + "recvd=[amount:biguint .size 8,sender:bstr .size 20,memo_len:uint .size 1]"; + const ELEMENTS: u64 = 3; + #[cfg(test)] + const PAYLOAD_SIZE: usize = CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + 55 + CBL_U8; + #[cfg(not(test))] + const PAYLOAD_SIZE: usize = CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + CBL_ADDRESS + CBL_U8; + + fn encode_cbor(&self, api: &dyn Api, encoder: &mut Encoder<&mut [u8]>) -> StdResult<()> { + // amount:biguint (8-byte uint) + encoder.ext_u64_from_u128(self.amount)?; + + // sender:bstr (20-byte address) + if let Some(sender) = &self.sender { + let sender_raw = api.addr_canonicalize(sender.as_str())?; + encoder.ext_address(sender_raw)?; + } else { + encoder.ext_bytes(&ZERO_ADDR)?; + } + + // memo_len:uint (1-byte uint) + encoder.ext_u8(self.memo_len.clamp(0, u8::MAX.into()) as u8)?; + + Ok(()) + } +} + +/// ```cddl +/// spent = [ +/// amount: biguint .size 8, ; transfer amount in base denomination +/// actions: uint .size 1, ; number of actions the execution performed +/// recipient: bstr .size 20, ; byte sequence of first recipient's canonical address +/// balance: biguint .size 8, ; sender's new balance aactions +/// ] +/// ``` +#[derive(Serialize, Debug, Deserialize, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct SpentNotification { + pub amount: u128, + pub actions: u32, + pub recipient: Option, + pub balance: u128, + pub memo_len: usize, +} + +impl DirectChannel for SpentNotification { + const CHANNEL_ID: &'static str = "spent"; + #[cfg(test)] + const CDDL_SCHEMA: &'static str = "spent=[amount:biguint .size 8,actions:uint .size 1,recipient:bstr .size 54,balance:biguint .size 8]"; + #[cfg(not(test))] + const CDDL_SCHEMA: &'static str = "spent=[amount:biguint .size 8,actions:uint .size 1,recipient:bstr .size 20,balance:biguint .size 8]"; + const ELEMENTS: u64 = 4; + #[cfg(test)] + const PAYLOAD_SIZE: usize = CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + CBL_U8 + 55 + CBL_BIGNUM_U64; + #[cfg(not(test))] + const PAYLOAD_SIZE: usize = + CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + CBL_U8 + CBL_ADDRESS + CBL_BIGNUM_U64; + + fn encode_cbor(&self, api: &dyn Api, encoder: &mut Encoder<&mut [u8]>) -> StdResult<()> { + // amount:biguint (8-byte uint), actions:uint (1-byte uint) + let mut spent_data = encoder + .ext_u64_from_u128(self.amount)? + .ext_u8(self.actions.clamp(0, u8::MAX.into()) as u8)?; + + // recipient:bstr (20-byte address) + if let Some(recipient) = &self.recipient { + let recipient_raw = api.addr_canonicalize(recipient.as_str())?; + spent_data = spent_data.ext_address(recipient_raw)?; + } else { + spent_data = spent_data.ext_bytes(&ZERO_ADDR)? + } + + // balance:biguint (8-byte uint) + spent_data.ext_u64_from_u128(self.balance)?; + + Ok(()) + } +} + +///```cddl +/// allowance = [ +/// amount: biguint .size 8, ; allowance amount in base denomination +/// allower: bstr .size 20, ; byte sequence of allower's canonical address +/// expiration: uint .size 8, ; epoch seconds of allowance expiration +///] +/// ``` +#[derive(Serialize, Debug, Deserialize, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct AllowanceNotification { + pub amount: u128, + pub allower: Addr, + pub expiration: Option, +} + +impl DirectChannel for AllowanceNotification { + const CHANNEL_ID: &'static str = "allowance"; + #[cfg(test)] + const CDDL_SCHEMA: &'static str = + "allowance=[amount:biguint .size 8,allower:bstr .size 54,expiration:uint .size 8]"; + #[cfg(not(test))] + const CDDL_SCHEMA: &'static str = + "allowance=[amount:biguint .size 8,allower:bstr .size 20,expiration:uint .size 8]"; + const ELEMENTS: u64 = 3; + #[cfg(test)] + const PAYLOAD_SIZE: usize = CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + 55 + CBL_TIMESTAMP; + #[cfg(not(test))] + const PAYLOAD_SIZE: usize = CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + CBL_ADDRESS + CBL_TIMESTAMP; + + fn encode_cbor(&self, api: &dyn Api, encoder: &mut Encoder<&mut [u8]>) -> StdResult<()> { + let allower_raw = api.addr_canonicalize(self.allower.as_str())?; + + // amount:biguint (8-byte uint), allower:bstr (20-byte address), expiration:uint (8-byte timestamp) + encoder + .ext_u64_from_u128(self.amount)? + .ext_bytes(allower_raw.as_slice())? + .ext_timestamp(self.expiration.unwrap_or_default())?; + + Ok(()) + } +} + +pub struct MultiRecvdNotification(pub Vec>); + +impl GroupChannel for MultiRecvdNotification { + const CHANNEL_ID: &'static str = "multirecvd"; + + // bloom parameters for the `multirecvd` channel: + const BLOOM_N: usize = 16; + const BLOOM_M: u32 = 512; + const BLOOM_K: u32 = 22; + + // flagsAndAmount:8 + ownerId:8 == 16 bytes + const PACKET_SIZE: usize = 16; + + fn notifications(&self) -> &Vec> { + &self.0 + } + + fn build_packet(&self, api: &dyn Api, data: &RecvdNotification) -> StdResult> { + // make the received packet + let mut packet_plaintext = [0u8; Self::PACKET_SIZE]; + + // encode flags and amount into 8 bytes (leftmost 2 bits reserved) + let amount_bytes = &(data.amount.clamp(0, U62_MAX) + | (((data.memo_len != 0) as u128) << 63) + | ((data.sender_is_owner as u128) << 62)) + .to_be_bytes()[8..]; + + // packet flag bits and amount bytes (u64 == 8 bytes) + packet_plaintext[0..8].copy_from_slice(amount_bytes); + + // determine owner address + let owner_addr: CanonicalAddr; + let owner_bytes: &[u8]; + if let Some(owner) = &data.sender { + owner_addr = api.addr_canonicalize(owner.as_str())?; + owner_bytes = owner_addr.as_slice() + } else { + owner_bytes = &ZERO_ADDR; + } + + // packet owner address terminal 8 bytes (8 bytes) + packet_plaintext[8..16].copy_from_slice(&owner_bytes[12..]); + + // 16 bytes total + Ok(packet_plaintext.to_vec()) + } +} + +// maximum supported filter size is currently 512 bits +const_assert!(MultiRecvdNotification::BLOOM_M <= 512); + +// ensure m is a power of 2 +const_assert!( + MultiRecvdNotification::BLOOM_M.trailing_zeros() == MultiRecvdNotification::BLOOM_M_LOG2 +); + +// ensure there are enough bits in the 32-byte source hash to provide entropy for the hashes +const_assert!(MultiRecvdNotification::BLOOM_K * MultiRecvdNotification::BLOOM_M_LOG2 <= 256); + +// this implementation is optimized to not check for packet sizes larger than 24 bytes +const_assert!(MultiRecvdNotification::PACKET_SIZE <= 24); + +pub struct MultiSpentNotification(pub Vec>); + +impl GroupChannel for MultiSpentNotification { + const CHANNEL_ID: &str = "multispent"; + + // bloom parameters for the `multispent` channel: + const BLOOM_N: usize = 4; + const BLOOM_M: u32 = 128; + const BLOOM_K: u32 = 22; + + // flagsAndAmount:8 + recipientId:8 + balance:8 == 24 bytes + const PACKET_SIZE: usize = 24; + + fn notifications(&self) -> &Vec> { + &self.0 + } + + fn build_packet(&self, api: &dyn Api, data: &SpentNotification) -> StdResult> { + // prep the packet plaintext + let mut packet_plaintext = [0u8; Self::PACKET_SIZE]; + + // encode flags and amount into 8 bytes (leftmost 2 bits reserved) + let amount_bytes = &(data.amount.clamp(0, U62_MAX) + | (((data.memo_len != 0) as u128) << 63)) + .to_be_bytes()[8..]; + + // packet flags and amount bytes (u64 == 8 bytes) + packet_plaintext[0..8].copy_from_slice(amount_bytes); + + // determine recipient address + let recipient_addr: CanonicalAddr; + let recipient_bytes: &[u8]; + if let Some(recipient) = &data.recipient { + recipient_addr = api.addr_canonicalize(recipient.as_str())?; + recipient_bytes = recipient_addr.as_slice(); + } else { + recipient_bytes = &ZERO_ADDR; + } + + // packet recipient address terminal 8 bytes (8 bytes) + packet_plaintext[8..16].copy_from_slice(&recipient_bytes[12..]); + + // balance bytes (u64 == 8 bytes) + packet_plaintext[16..24] + .copy_from_slice(&data.balance.clamp(0, u64::MAX.into()).to_be_bytes()[8..]); + + // 24 bytes total + Ok(packet_plaintext.to_vec()) + } +} + +// maximum supported filter size is currently 512 bits +const_assert!(MultiSpentNotification::BLOOM_M <= 512); + +// ensure m is a power of 2 +const_assert!( + MultiSpentNotification::BLOOM_M.trailing_zeros() == MultiSpentNotification::BLOOM_M_LOG2 +); + +// ensure there are enough bits in the 32-byte source hash to provide entropy for the hashes +const_assert!(MultiSpentNotification::BLOOM_K * MultiSpentNotification::BLOOM_M_LOG2 <= 256); + +// this implementation is optimized to not check for packet sizes larger than 24 bytes +const_assert!(MultiSpentNotification::PACKET_SIZE <= 24); + +struct BloomFilter { + filter: U512, + tx_hash: String, + secret: Vec, +} + +impl BloomFilter { + fn add>( + &mut self, + recipient: &CanonicalAddr, + packet_plaintext: &[u8], + ) -> StdResult> { + // contribute to received bloom filter + let seed = get_seed(recipient, &self.secret)?; + let id = notification_id(&seed, G::CHANNEL_ID, &self.tx_hash)?; + let hash_bytes = U256::from_big_endian(&sha_256(id.0.as_slice())); + let bloom_mask: U256 = U256::from(G::BLOOM_M - 1); + + // each hash section for up to k times + for i in 0..G::BLOOM_K { + let bit_index = ((hash_bytes >> (256 - G::BLOOM_M_LOG2 - (i * G::BLOOM_M_LOG2))) + & bloom_mask) + .as_usize(); + self.filter |= U512::from(1) << bit_index; + } + + // use top 64 bits of notification ID for packet ID + let packet_id = &id.0.as_slice()[0..8]; + + // take the bottom bits from the notification ID for key material + let packet_ikm = &id.0.as_slice()[8..32]; + + // create ciphertext by XOR'ing the plaintext with the notification ID + let packet_ciphertext = xor_bytes(&packet_plaintext[..], &packet_ikm[0..G::PACKET_SIZE]); + + // construct the packet bytes + let packet_bytes: Vec = [packet_id.to_vec(), packet_ciphertext].concat(); + + Ok(packet_bytes) + } +} + +pub fn render_group_notification>( + api: &dyn Api, + group: G, + tx_hash: &String, + env_random: Binary, + secret: &[u8], + resp: Response, +) -> StdResult { + // bloom filter + let mut bloom_filter = BloomFilter { + filter: U512::from(0), + tx_hash: tx_hash.to_string(), + secret: secret.to_vec(), + }; + + // packet structs + let mut packets: Vec<(CanonicalAddr, Vec)> = vec![]; + + // keep track of how many times an address shows up in packet data + let mut recipient_counts: HashMap = HashMap::new(); + + // each notification + for notification in group.notifications() { + // who notification is intended for + let notification_for = api.addr_canonicalize(notification.notification_for.as_str())?; + let notifyee = notification_for.clone(); + + // increment count of recipient occurrence + recipient_counts.insert( + notification_for, + recipient_counts.get(¬ifyee).unwrap_or(&0u16) + 1, + ); + + // skip adding this packet if recipient was already seen + if *recipient_counts.get(¬ifyee).unwrap() > 1 { + continue; + } + + // build packet + let packet_plaintext = &group.build_packet(api, ¬ification.data)?; + + // add to bloom filter + let packet_bytes = bloom_filter.add::(¬ifyee, packet_plaintext)?; + + // add to packets data + packets.push((notifyee, packet_bytes)); + } + + // filter out any notifications for recipients showing up more than once + let mut packets: Vec> = packets + .into_iter() + .filter(|(addr, _)| *recipient_counts.get(addr).unwrap_or(&0u16) <= 1) + .map(|(_, packet)| packet) + .collect(); + + // still too many packets; trim down to size + if packets.len() > G::BLOOM_N { + packets = packets[0..G::BLOOM_N].to_vec(); + } + + // now add extra packets, if needed, to hide number of packets + let padding_size = G::BLOOM_N.saturating_sub(packets.len()); + if padding_size > 0 { + // fill buffer with secure random bytes + let decoy_addresses = hkdf_sha_512( + &Some(vec![0u8; 64]), + &env_random, + format!("{}:decoys", G::CHANNEL_ID).as_bytes(), + padding_size * 20, // 20 bytes per random addr + )?; + + // handle each padding package + for i in 0..padding_size { + // generate address + let address = CanonicalAddr::from(&decoy_addresses[i * 20..(i + 1) * 20]); + + // nil plaintext + let packet_plaintext = vec![0u8; G::PACKET_SIZE]; + + // produce bytes + let packet_bytes = bloom_filter.add::(&address, &packet_plaintext)?; + + // add to packets list + packets.push(packet_bytes); + } + } + + // prep output bytes + let mut output_bytes: Vec = vec![]; + + // append bloom filter (taking m bottom bits of 512-bit filter) + output_bytes.extend_from_slice( + &bloom_filter.filter.to_big_endian()[((512 - G::BLOOM_M as usize) >> 3)..], + ); + + // append packets + for packet in packets { + output_bytes.extend(packet.iter()); + } + + // Ok(output_bytes) + Ok(resp.add_attribute_plaintext( + format!("snip52:#{}", G::CHANNEL_ID), + Binary::from(output_bytes).to_base64(), + )) +} diff --git a/src/query.rs b/src/query.rs new file mode 100644 index 00000000..777a584e --- /dev/null +++ b/src/query.rs @@ -0,0 +1,586 @@ +use cosmwasm_std::{ + to_binary, Addr, Binary, CanonicalAddr, Deps, Env, StdError, StdResult, Storage, Uint128, + Uint64, +}; +use rand_chacha::ChaChaRng; +use rand_core::{RngCore, SeedableRng}; +use secret_toolkit::notification::{ + get_seed, notification_id, BloomParameters, ChannelInfoData, Descriptor, DirectChannel, + FlatDescriptor, GroupChannel, StructDescriptor, +}; +use secret_toolkit::permit::{RevokedPermits, RevokedPermitsStore}; + +use crate::btbe::{find_start_bundle, stored_balance, stored_entry, stored_tx_count}; +use crate::dwb::{DWB, TX_NODES}; +use crate::msg::{AllowanceGivenResult, AllowanceReceivedResult, QueryAnswer}; +use crate::notifications::{ + AllowanceNotification, MultiRecvdNotification, MultiSpentNotification, RecvdNotification, + SpentNotification, +}; +use crate::state::{ + AllowancesStore, MintersStore, CHANNELS, CONFIG, CONTRACT_STATUS, INTERNAL_SECRET_RELAXED, + INTERNAL_SECRET_SENSITIVE, TOTAL_SUPPLY, +}; +use crate::transaction_history::Tx; + +pub fn query_exchange_rate(storage: &dyn Storage) -> StdResult { + let constants = CONFIG.load(storage)?; + + if constants.deposit_is_enabled || constants.redeem_is_enabled { + let rate: Uint128; + let denom: String; + // if token has more decimals than SCRT, you get magnitudes of SCRT per token + if constants.decimals >= 6 { + rate = Uint128::new(10u128.pow(constants.decimals as u32 - 6)); + denom = "SCRT".to_string(); + // if token has less decimals, you get magnitudes token for SCRT + } else { + rate = Uint128::new(10u128.pow(6 - constants.decimals as u32)); + denom = constants.symbol; + } + return to_binary(&QueryAnswer::ExchangeRate { rate, denom }); + } + to_binary(&QueryAnswer::ExchangeRate { + rate: Uint128::zero(), + denom: String::new(), + }) +} + +pub fn query_token_info(storage: &dyn Storage) -> StdResult { + let constants = CONFIG.load(storage)?; + + let total_supply = if constants.total_supply_is_public { + Some(Uint128::new(TOTAL_SUPPLY.load(storage)?)) + } else { + None + }; + + to_binary(&QueryAnswer::TokenInfo { + name: constants.name, + symbol: constants.symbol, + decimals: constants.decimals, + total_supply, + }) +} + +pub fn query_token_config(storage: &dyn Storage) -> StdResult { + let constants = CONFIG.load(storage)?; + + to_binary(&QueryAnswer::TokenConfig { + public_total_supply: constants.total_supply_is_public, + deposit_enabled: constants.deposit_is_enabled, + redeem_enabled: constants.redeem_is_enabled, + mint_enabled: constants.mint_is_enabled, + burn_enabled: constants.burn_is_enabled, + supported_denoms: constants.supported_denoms, + }) +} + +pub fn query_contract_status(storage: &dyn Storage) -> StdResult { + let contract_status = CONTRACT_STATUS.load(storage)?; + + to_binary(&QueryAnswer::ContractStatus { + status: contract_status, + }) +} + +pub fn query_transactions( + deps: Deps, + account: String, + page: u32, + page_size: u32, +) -> StdResult { + 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()?; + + // only look if head node is not null + 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.as_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 mut txs: Vec = 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)?; + + // only look if head node is not null + if tx_bundle.head_node > 0 { + 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.as_vec(deps.storage, deps.api)?[0..txs_left as usize], + ); + break; + } + txs.extend(head_node.as_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 list_len = tx_bundle.list_len as u32; + if start_at + txs_left <= list_len { + // only look if head node is not null + if tx_bundle.head_node > 0 { + let head_node = TX_NODES + .add_suffix(&tx_bundle.head_node.to_be_bytes()) + .load(deps.storage)?; + // this first bundle has all the txs we need + txs = head_node.as_vec(deps.storage, deps.api)? + [start_at as usize..(start_at + txs_left) as usize] + .to_vec(); + } + } else { + // only look if head node is not null + if tx_bundle.head_node > 0 { + let head_node = TX_NODES + .add_suffix(&tx_bundle.head_node.to_be_bytes()) + .load(deps.storage)?; + // get the rest of the txs in this bundle and then go back through history + txs = head_node.as_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)?; + // only look if head node is not null + if tx_bundle.head_node > 0 { + 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.as_vec(deps.storage, deps.api)? + [0..txs_left as usize], + ); + break; + } + txs.extend(head_node.as_vec(deps.storage, deps.api)?); + txs_left = txs_left.saturating_sub(list_len); + } + if bundle_idx > 0 { + bundle_idx -= 1; + } else { + break; + } + } + } + } + } + } + } + + // deterministically obfuscate ids so they are not serial to prevent metadata leak + let internal_secret = INTERNAL_SECRET_RELAXED.load(deps.storage)?; + let internal_secret_u64: u64 = u64::from_be_bytes(internal_secret[..8].try_into().unwrap()); + let txs = txs + .iter() + .map(|tx| { + // PRNG(PRNG(serial_id) ^ secret) + let mut rng = ChaChaRng::seed_from_u64(tx.id); + let serial_id_rand = rng.next_u64(); + let new_seed = serial_id_rand ^ internal_secret_u64; + let mut rng = ChaChaRng::seed_from_u64(new_seed); + let new_id = rng.next_u64() >> (64 - 53); + Tx { + id: new_id, + action: tx.action.clone(), + coins: tx.coins.clone(), + memo: tx.memo.clone(), + block_height: tx.block_height, + block_time: tx.block_time, + } + }) + .collect(); + + let result = QueryAnswer::TransactionHistory { + txs, + total: Some(total as u64), + }; + to_binary(&result) +} + +pub fn query_balance(deps: Deps, account: String) -> StdResult { + // 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 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) +} + +pub fn query_minters(deps: Deps) -> StdResult { + let minters = MintersStore::load(deps.storage)?; + + let response = QueryAnswer::Minters { minters }; + to_binary(&response) +} + +pub fn query_allowance(deps: Deps, owner: String, spender: String) -> StdResult { + // Notice that if query_allowance() was called by a viewing-key call, the addresses of 'owner' + // and 'spender' have already been validated. + // The addresses of 'owner' and 'spender' should not be validated if query_allowance() was + // called by a permit call, for compatibility with non-Secret addresses. + let owner = Addr::unchecked(owner); + let spender = Addr::unchecked(spender); + + let allowance = AllowancesStore::load(deps.storage, &owner, &spender); + + let response = QueryAnswer::Allowance { + owner, + spender, + allowance: Uint128::new(allowance.amount), + expiration: allowance.expiration, + }; + to_binary(&response) +} + +pub fn query_allowances_given( + deps: Deps, + owner: String, + page: u32, + page_size: u32, +) -> StdResult { + // Notice that if query_all_allowances_given() was called by a viewing-key call, + // the address of 'owner' has already been validated. + // The addresses of 'owner' should not be validated if query_all_allowances_given() was + // called by a permit call, for compatibility with non-Secret addresses. + let owner = Addr::unchecked(owner); + + let all_allowances = + AllowancesStore::all_allowances(deps.storage, &owner, page, page_size).unwrap_or_default(); + + let allowances_result = all_allowances + .into_iter() + .map(|(spender, allowance)| AllowanceGivenResult { + spender, + allowance: Uint128::from(allowance.amount), + expiration: allowance.expiration, + }) + .collect(); + + let response = QueryAnswer::AllowancesGiven { + owner: owner.clone(), + allowances: allowances_result, + count: AllowancesStore::num_allowances(deps.storage, &owner), + }; + to_binary(&response) +} + +pub fn query_allowances_received( + deps: Deps, + spender: String, + page: u32, + page_size: u32, +) -> StdResult { + // Notice that if query_all_allowances_received() was called by a viewing-key call, + // the address of 'spender' has already been validated. + // The addresses of 'spender' should not be validated if query_all_allowances_received() was + // called by a permit call, for compatibility with non-Secret addresses. + let spender = Addr::unchecked(spender); + + let all_allowed = + AllowancesStore::all_allowed(deps.storage, &spender, page, page_size).unwrap_or_default(); + + let allowances = all_allowed + .into_iter() + .map(|(owner, allowance)| AllowanceReceivedResult { + owner, + allowance: Uint128::from(allowance.amount), + expiration: allowance.expiration, + }) + .collect(); + + let response = QueryAnswer::AllowancesReceived { + spender: spender.clone(), + allowances, + count: AllowancesStore::num_allowed(deps.storage, &spender), + }; + to_binary(&response) +} + +// ***************** +// SNIP-24.1 query function +// ***************** + +pub fn query_list_permit_revocations(deps: Deps, account: &str) -> StdResult { + let revocations = RevokedPermits::list_revocations(deps.storage, account)?; + + to_binary(&QueryAnswer::ListPermitRevocations { revocations }) +} + +// ***************** +// SNIP-52 query functions +// ***************** + +/// +/// ListChannels query +/// +/// Public query to list all notification channels. +/// +pub fn query_list_channels(deps: Deps) -> StdResult { + let channels: Vec = 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. +/// +pub fn query_channel_info( + deps: Deps, + env: Env, + channels: Vec, + txhash: Option, + sender_raw: CanonicalAddr, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.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() { + RecvdNotification::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(RecvdNotification::CDDL_SCHEMA.to_string()), + }; + channels_data.push(channel_info_data); + } + SpentNotification::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(SpentNotification::CDDL_SCHEMA.to_string()), + }; + channels_data.push(channel_info_data); + } + AllowanceNotification::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(AllowanceNotification::CDDL_SCHEMA.to_string()), + }; + channels_data.push(channel_info_data); + } + MultiRecvdNotification::CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "bloom".to_string(), + channel, + answer_id, + parameters: Some(BloomParameters { + m: MultiRecvdNotification::BLOOM_M, + k: MultiRecvdNotification::BLOOM_K, + h: "sha256".to_string(), + }), + data: Some(Descriptor { + r#type: format!("packet[{}]", MultiRecvdNotification::BLOOM_N), + version: "1".to_string(), + packet_size: MultiRecvdNotification::PACKET_SIZE as u32, + data: StructDescriptor { + r#type: "struct".to_string(), + label: "transfer".to_string(), + members: vec![ + FlatDescriptor { + r#type: "uint64".to_string(), + label: "flagsAndAmount".to_string(), + description: Some( + "Bit field of [0]: non-empty memo; [2]: sender is owner; [2..]: uint62 transfer amount in base denomination".to_string(), + ), + }, + FlatDescriptor { + r#type: "bytes8".to_string(), + label: "ownerId".to_string(), + description: Some( + "The last 8 bytes of the owner's canonical address".to_string(), + ), + }, + ], + }, + }), + counter: None, + next_id: None, + cddl: None, + }; + channels_data.push(channel_info_data); + } + MultiSpentNotification::CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "bloom".to_string(), + channel, + answer_id, + parameters: Some(BloomParameters { + m: MultiSpentNotification::BLOOM_M, + k: MultiSpentNotification::BLOOM_K, + h: "sha256".to_string(), + }), + data: Some(Descriptor { + r#type: format!("packet[{}]", MultiSpentNotification::BLOOM_N), + version: "1".to_string(), + packet_size: MultiSpentNotification::PACKET_SIZE as u32, + data: StructDescriptor { + r#type: "struct".to_string(), + label: "transfer".to_string(), + members: vec![ + FlatDescriptor { + r#type: "uint64".to_string(), + label: "flagsAndAmount".to_string(), + description: Some( + "Bit field of [0]: non-empty memo; [1]: reserved; [2..] uint62 transfer amount in base denomination".to_string(), + ), + }, + FlatDescriptor { + r#type: "bytes8".to_string(), + label: "recipientId".to_string(), + description: Some( + "The last 8 bytes of the recipient's canonical address".to_string(), + ), + }, + FlatDescriptor { + r#type: "uint64".to_string(), + label: "balance".to_string(), + description: Some( + "Spender's new balance after the transfer".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 + ))); + } + } + } + + to_binary(&QueryAnswer::ChannelInfo { + as_of_block: Uint64::from(env.block.height), + channels: channels_data, + seed, + }) +} + +// ***************** +// End SNIP-52 query functions +// ***************** diff --git a/src/state.rs b/src/state.rs index ece815a1..1a74ab80 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 = Item::new(KEY_TOTAL_SUPPLY); pub static CONTRACT_STATUS: Item = Item::new(KEY_CONTRACT_STATUS); -pub static PRNG: Item<[u8; SHA256_HASH_SIZE]> = Item::new(KEY_PRNG); - pub static MINTERS: Item> = Item::new(KEY_MINTERS); pub static TX_COUNT: Item = 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> { @@ -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 = 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>, - account_random_pos: &Option, - ) -> 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,15 @@ impl ReceiverHashStore { receiver_hash.save(store, &code_hash) } } + +/// internal secret used for sensitive data such as address hashes in the btbe and notifications +pub static INTERNAL_SECRET_SENSITIVE: Item> = Item::new(b"internal-secret-secure"); + +/// internal secret used for less sensitive data such as obfuscating tx IDs +pub static INTERNAL_SECRET_RELAXED: Item> = Item::new(b"internal-secret-prng"); + +/// SNIP-52 channels +pub static CHANNELS: Keyset = Keyset::new(b"channel-ids"); + +/// SNIP-52 status +pub static NOTIFICATIONS_ENABLED: Item = Item::new(b"notify-status"); diff --git a/src/strings.rs b/src/strings.rs new file mode 100644 index 00000000..437eb8a7 --- /dev/null +++ b/src/strings.rs @@ -0,0 +1,3 @@ +pub const TRANSFER_HISTORY_UNSUPPORTED_MSG: &str = + "`transfer_history` query is UNSUPPORTED. Use `transaction_history` instead."; +pub const SEND_TO_CONTRACT_ERR_MSG: &str = "Tokens cannot be sent to token contract."; 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, - // 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, - pub block_height: Option, -} #[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, + // 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 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, - block_time: u64, - block_height: u64, -} -static TRANSFERS: AppendStore = AppendStore::new(PREFIX_TRANSFERS); - -impl StoredLegacyTransfer { - pub fn into_humanized(self) -> StdResult { - 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, 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>` to an `StdResult` - let transfers: StdResult> = 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, - address2: Option, - address3: Option, + address1: Option, + address2: Option, + address3: Option, } 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 { + pub fn into_tx_action(self, api: &dyn Api) -> StdResult { 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 = 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 = 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, @@ -323,309 +217,105 @@ pub struct StoredExtendedTx { block_height: u64, } -impl StoredExtendedTx { - fn new( - id: u64, - action: StoredTxAction, - coins: Coin, - memo: Option, - block: &cosmwasm_std::BlockInfo, - ) -> Self { - Self { +impl StoredTx { + pub fn into_humanized(self, api: &dyn Api, id: u64) -> StdResult { + Ok(Tx { id, - action, - coins: coins.into(), - memo, - block_time: block.time.seconds(), - block_height: block.height, - } - } - - fn into_humanized(self) -> StdResult { - 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, 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>` to an `StdResult` - let txs: StdResult> = 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 { - 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>, - account_random_pos: &Option, -) -> StdResult<()> { - let mut index_changer: Option = 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>, - account_random_pos: &Option, -) -> StdResult<()> { - let mut index_changer: Option = 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, - block: &cosmwasm_std::BlockInfo, - decoys: &Option>, - account_random_pos: &Option, -) -> 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 { + // 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, + block: &BlockInfo, +) -> StdResult { + 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, block: &cosmwasm_std::BlockInfo, - decoys: &Option>, - account_random_pos: &Option, -) -> StdResult<()> { - let id = increment_tx_count(store)?; - let coins = Coin { denom, amount }; +) -> StdResult { 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, block: &cosmwasm_std::BlockInfo, - decoys: &Option>, - account_random_pos: &Option, -) -> 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 { + 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>, - account_random_pos: &Option, -) -> StdResult<()> { - let id = increment_tx_count(store)?; - let coins = Coin { denom, amount }; +) -> StdResult { 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>, - account_random_pos: &Option, -) -> StdResult<()> { - let id = increment_tx_count(store)?; - let coins = Coin { denom, amount }; +) -> StdResult { 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..61c5c87c --- /dev/null +++ b/tests/dwb/.env.example @@ -0,0 +1,10 @@ +# enabling DEV will print every query/execution to the chain +# DEV=1 +NODE_ENV=development +SECRET_LCD=http://localhost:1317 +SECRET_RPC=http://localhost:26657 +SECRET_CHAIN=secretdev-1 +CONTRACT_PATH=../../contract.wasm.gz + +# Secret's evaporation API is currently broken +# ENABLE_EVAPORATION_TESTS=1 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: +![Integration test preview](https://github.com/user-attachments/assets/be2fedda-550c-45e6-aee4-5af45a84d5b8) diff --git a/tests/dwb/bun.lockb b/tests/dwb/bun.lockb new file mode 100755 index 00000000..b81be4e3 Binary files /dev/null and b/tests/dwb/bun.lockb differ diff --git a/tests/dwb/eslint.config.mjs b/tests/dwb/eslint.config.mjs new file mode 100644 index 00000000..52d869ce --- /dev/null +++ b/tests/dwb/eslint.config.mjs @@ -0,0 +1,20 @@ +import elite from '@blake.regalia/eslint-config-elite'; + +export default [ + ...elite, + { + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + + parserOptions: { + tsconfigRootDir: import.meta.dirname, + project: 'tsconfig.json', + }, + }, + rules: { + 'no-console': 'off', + '@typescript-eslint/naming-convention': 'off', + }, + }, +]; diff --git a/tests/dwb/package.json b/tests/dwb/package.json new file mode 100644 index 00000000..b57b9868 --- /dev/null +++ b/tests/dwb/package.json @@ -0,0 +1,30 @@ +{ + "private": "true", + "type": "module", + "scripts": { + "build": "tsc && tsc-esm-fix --tsconfig tsconfig.tsc-esm-fix.json --target=dist", + "make": "pushd ../../ && make compile-integration && popd", + "simulate": "bun run --env-file=.env src/main.ts", + "test": "bun run make && bun run simulate" + }, + "devDependencies": { + "@blake.regalia/belt": "^0.52.1", + "@blake.regalia/eslint-config-elite": "^0.5.11", + "@blake.regalia/tsconfig": "^0.2.0", + "@solar-republic/types": "^0.3.7", + "@types/chai": "^5.0.1", + "@types/node": "^22.10.5", + "chai": "^5.1.2", + "chai-bites": "^0.2.0", + "eslint": "9", + "tsc-esm-fix": "^3.1.2", + "typescript": "^5.7.2" + }, + "dependencies": { + "@solar-republic/contractor": "^0.10.3", + "@solar-republic/cosmos-grpc": "^0.17.2", + "@solar-republic/crypto": "^0.3.2", + "@solar-republic/neutrino": "^1.8.5", + "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..1e2bc4a2 --- /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, [X_GAS_PRICE, 'uscrt'], '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..834e3936 --- /dev/null +++ b/tests/dwb/src/contract.ts @@ -0,0 +1,18 @@ +import type {EncodedGoogleProtobufAny} from '@solar-republic/cosmos-grpc/google/protobuf/any'; +import type {TxResponseTuple, Wallet} from '@solar-republic/neutrino'; + +import {TendermintEventFilter, broadcast_result, create_and_sign_tx_direct} from '@solar-republic/neutrino'; + +import {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 { + const [atu8_raw, sb16_txn, atu8_signdoc] = await create_and_sign_tx_direct( + k_wallet, + [atu8_msg], + xg_gas_limit + ); + + return await broadcast_result(k_wallet, atu8_raw, sb16_txn, k_tef); +} diff --git a/tests/dwb/src/dwb-entry.ts b/tests/dwb/src/dwb-entry.ts new file mode 100644 index 00000000..564b806a --- /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/types'; + +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): 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..b30f7947 --- /dev/null +++ b/tests/dwb/src/dwb.ts @@ -0,0 +1,253 @@ +import type {SecretApp} from '@solar-republic/neutrino'; +import type {WeakSecretAccAddr} from '@solar-republic/types'; + +import {bytes, parse_json} from '@blake.regalia/belt'; + +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(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, xc_code, s_err] = await this._k_app.query('dwb', {}); + + // error + if(xc_code) { + throw Error(s_err); + } + + // 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; + } + + // eslint-disable-next-line @typescript-eslint/require-await + 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 `┃ ${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 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..64de20a0 --- /dev/null +++ b/tests/dwb/src/helper.ts @@ -0,0 +1,88 @@ +import type {Promisable} from '@blake.regalia/belt'; + +import {is_string, map_entries} from '@blake.regalia/belt'; + + +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'; + + +// polyfill crypto global for node.js env +(globalThis as any).crypto ||= (await import('crypto')).webcrypto; + +export function pass(s_test: string): void { + console.log(`${SX_ANSI_GREEN}✓${SX_ANSI_RESET} ${s_test}`); +} + +function error(s_test: string, ...a_args: Array) { + 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): void { + error(`❌ ${s_test}`, ...a_args); + throw Error(`Exitting on error`); +} + +export function caught(s_test: string, ...a_args: Array): void { + error(`💀 ${s_test}`, ...a_args); +} + +interface GroupCallback { + it(s_test: string, f_test: () => Promisable): Promise; +} + +export async function describe(s_group: string, f_group: (g_call: GroupCallback) => Promisable): Promise { + 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) { + 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..166c2ffb --- /dev/null +++ b/tests/dwb/src/main.ts @@ -0,0 +1,259 @@ +import type {JsonObject} from '@blake.regalia/belt'; + +import type {SecretContractInterface, FungibleTransferCall, Snip24} from '@solar-republic/contractor'; + +import type {TxResponseTuple} from '@solar-republic/neutrino'; +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, secret_contract_instantiate, secret_contract_upload_code} 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 {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(process.env['CONTRACT_PATH'] ?? '../../contract.wasm.gz'); + +console.log(k_wallet_a.addr); + + +console.debug(`Uploading code...`); +const [sg_code_id] = await secret_contract_upload_code(k_wallet_a, atu8_wasm, 30_000000n); + +console.debug(`Instantiating contract...`); + +const [[sa_snip]=[]] = await secret_contract_instantiate(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, + }, +}, 10_000_000n); + +console.debug(`Running tests against ${sa_snip}...`); + +// @ts-expect-error deep instantiation +const k_contract = await SecretContract>(P_SECRET_LCD, sa_snip!); + +const k_app_a = SecretApp(k_wallet_a, k_contract); +const k_app_b = SecretApp(k_wallet_b, k_contract); +const k_app_c = SecretApp(k_wallet_c, k_contract); +const k_app_d = SecretApp(k_wallet_d, k_contract); + +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]] = 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, w_resp: any, a6_response: TxResponseTuple]>); + + // 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 ${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']]) + ), + ], 50_000n); + + // submit all in parallel + const [ + // @ts-expect-error totally stupid + g_result_transfer, + [xc_send_gas, s_err_send_gas], + [g_res_increase,, [xc_code, s_err]=[]]=[], + ] = 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 && xc_code) { + throw Error(`Failed to increase allowance: ${s_err}`); + } + + // 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 as bigint); + } + + // 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, [X_GAS_PRICE, 'uscrt'], 'secret'); + const k_wallet_recipient = await Wallet(await sha256(text_to_bytes(si_recipient)), SI_SECRET_CHAIN, P_SECRET_LCD, P_SECRET_RPC, [X_GAS_PRICE, 'uscrt'], 'secret'); + + const k_app_owner = SecretApp(k_wallet_owner, k_contract); + const k_app_recipient = SecretApp(k_wallet_recipient, k_contract); + + 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..39236105 --- /dev/null +++ b/tests/dwb/src/snip.ts @@ -0,0 +1,199 @@ +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} from '@solar-republic/neutrino'; +import type {CwSecretAccAddr, CwUint128, Snip24QueryPermitMsg, WeakSecretAccAddr} from '@solar-republic/types'; + +import {entries, stringify_json} from '@blake.regalia/belt'; +import {queryCosmosBankBalance} from '@solar-republic/cosmos-grpc/cosmos/bank/v1beta1/query'; +import {snip24_amino_sign} 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; + +export type TransferResult = { + tracking: GroupedGasLogs; + gasUsed: bigint; +}; + +type TokenBalance = SecretContractInterface<{ + queries: { + balance: [{}, { + amount: CwUint128; + }]; + + with_permit: { + variants: [ + { + msg: { + query: { + balance: {}; + }; + permit: Snip24QueryPermitMsg; + }; + response: { + balance: { + amount: CwUint128; + }; + }; + }, + ]; + }; + }; +}>; + +export async function scrt_balance(sa_owner: WeakSecretAccAddr): Promise { + 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) { + const g_permit = await snip24_amino_sign(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, + k_app_sender?: SecretApp +): Promise { + const sa_owner = k_app_owner.wallet.addr as CwSecretAccAddr; + const sa_recipient = k_app_recipient.wallet.addr; + + // scrt balance of owner before transfer + 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([ + snip_balance(k_app_owner), + snip_balance(k_app_recipient), + ]); + + // execute transfer + const [g_exec,, [xc_code, sx_res,, g_meta, h_events]] = 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([ + snip_balance(k_app_owner), + snip_balance(k_app_recipient), + ]); + + if(xc_code) { + console.warn('Diagnostics', { + scrt_balance_before: xg_scrt_balance_owner_before, + 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}}"