diff --git a/Cargo.lock b/Cargo.lock index e4d0bd6c..0cf6ec4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,7 +40,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -61,7 +61,7 @@ checksum = "7c7db3d5a9718568e4cf4a537cfd7070e6e6ff7481510d0237fb529ac850f6d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "arbitrary" @@ -219,18 +219,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -394,7 +394,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", "syn_derive", ] @@ -560,9 +560,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" +checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" dependencies = [ "jobserver", "libc", @@ -588,9 +588,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "chrono" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -598,7 +598,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -681,7 +681,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -864,7 +864,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -888,7 +888,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -899,7 +899,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -939,7 +939,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -957,9 +957,9 @@ dependencies = [ [[package]] name = "deunicode" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94" +checksum = "322ef0094744e63628e6f0eb2295517f79276a5b342a4c2ff3042566ca181d4e" [[package]] name = "digest" @@ -1008,6 +1008,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0bc8fbe9441c17c9f46f75dfe27fa1ddb6c68a461ccaed0481419219d4f10d3" +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -1047,9 +1053,9 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "elementtree" @@ -1063,9 +1069,9 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if 1.0.0", ] @@ -1087,7 +1093,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -1286,7 +1292,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -1661,12 +1667,11 @@ dependencies = [ "ed25519-dalek", "fake", "futures", - "itertools 0.12.1", "maplit", "mutants", "near-units", "near-workspaces", - "nitka", + "nitka 0.4.0", "num-format", "pkg-config", "rand 0.8.5", @@ -1703,15 +1708,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" @@ -1720,9 +1716,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2" dependencies = [ "libc", ] @@ -2214,7 +2210,7 @@ checksum = "80fca203c51edd9595ec14db1d13359fb9ede32314990bf296b6c5c4502f6ab7" dependencies = [ "quote", "serde", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -2226,7 +2222,7 @@ dependencies = [ "fs2", "near-rpc-error-core", "serde", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -2301,7 +2297,7 @@ dependencies = [ "serde_json", "strum 0.26.2", "strum_macros 0.26.2", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -2317,7 +2313,18 @@ dependencies = [ "serde_json", "strum 0.26.2", "strum_macros 0.26.2", - "syn 2.0.58", + "syn 2.0.60", +] + +[[package]] +name = "near-self-update" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12d976c41c2c60d0d39fffb16edf08beae461cf6aee3c78c9e6807ebc32cf52" +dependencies = [ + "anyhow", + "near-workspaces", + "nitka 0.2.2", ] [[package]] @@ -2463,9 +2470,25 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nitka" -version = "0.3.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa6e5bd961bfd3ca20dd97cbcc68f3a24107262403cf664238d57a011957d3e" +checksum = "f1b01a4eb588f738a3e5cf72d08308eab105bf6e326c81a5cc55a96f45febcfa" +dependencies = [ + "anyhow", + "dotenv", + "fake", + "futures", + "near-sdk 5.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "near-units", + "near-workspaces", + "tokio", +] + +[[package]] +name = "nitka" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a456e97a688ae7643c470172edef4b8159e0ac9c3e69a4af7fc0bb7096c02e2f" dependencies = [ "anyhow", "base64 0.22.0", @@ -2479,13 +2502,13 @@ dependencies = [ [[package]] name = "nitka-proc" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc4198849943f2c5742165c9c5abdbbf702a3b08a198ec295d1fb8f08673534" +checksum = "98132f2b33daa48f52a5df408379bd5f3a364ceb3e50fce0cbbee38adc16ebc6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -2633,7 +2656,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -2813,7 +2836,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -2934,9 +2957,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -2974,7 +2997,7 @@ checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ "bytes", "heck 0.3.3", - "itertools 0.10.5", + "itertools", "lazy_static", "log", "multimap", @@ -2993,7 +3016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools", "proc-macro2", "quote", "syn 1.0.109", @@ -3017,9 +3040,9 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -3386,7 +3409,7 @@ checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -3442,22 +3465,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -3473,9 +3496,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -3490,7 +3513,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -3532,7 +3555,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -3760,7 +3783,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -3777,25 +3800,26 @@ dependencies = [ "async-trait", "near-sdk 5.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "near-workspaces", - "nitka", + "nitka 0.4.0", "nitka-proc", ] [[package]] name = "sweat-model" version = "0.1.0" -source = "git+https://github.com/sweatco/sweat-near?rev=7fc49145026654404310b42efd0d20eb346e7ae2#7fc49145026654404310b42efd0d20eb346e7ae2" +source = "git+https://github.com/sweatco/sweat-near?rev=537ef7d0aa3bf58d87b77a1c9660b2d0299b6c00#537ef7d0aa3bf58d87b77a1c9660b2d0299b6c00" dependencies = [ "near-contract-standards 5.1.0 (git+https://github.com/sweatco/near-sdk-rs?rev=d5e975e3b224b5758745ab46c25d586a8e0db473)", "near-sdk 5.1.0 (git+https://github.com/sweatco/near-sdk-rs?rev=d5e975e3b224b5758745ab46c25d586a8e0db473)", + "near-self-update", "near-workspaces", - "nitka", + "nitka 0.4.0", "nitka-proc", ] [[package]] name = "sweat_jar" -version = "1.0.1" +version = "2.0.0" dependencies = [ "base64 0.22.0", "crypto-hash", @@ -3865,9 +3889,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -3883,7 +3907,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -3974,7 +3998,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -3989,9 +4013,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -4010,9 +4034,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -4070,7 +4094,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -4266,7 +4290,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -4516,7 +4540,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", "wasm-bindgen-shared", ] @@ -4550,7 +4574,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4647,7 +4671,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -4665,7 +4689,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -4685,17 +4709,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -4706,9 +4731,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -4718,9 +4743,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -4730,9 +4755,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -4742,9 +4773,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -4754,9 +4785,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -4766,9 +4797,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -4778,9 +4809,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" diff --git a/Cargo.toml b/Cargo.toml index 2b099e64..892f1d43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ fake = "2.8.0" rand = "0.8.5" futures = "0.3.28" num-format = "0.4.4" -itertools = "0.12.1" ed25519-dalek = { version = "2.0.0", features = ["rand_core"] } base64 = "0.22.0" sha256 = "1.3.0" @@ -20,11 +19,11 @@ mutants = "0.0.3" serde = "1.0" sha2 = "0.10" -nitka = "0.3.0" -nitka-proc = "0.3.0" +nitka = "0.4.0" +nitka-proc = "0.4.0" sweat-jar-model = { path = "model" } -sweat-model = { git = "https://github.com/sweatco/sweat-near", rev = "7fc49145026654404310b42efd0d20eb346e7ae2" } +sweat-model = { git = "https://github.com/sweatco/sweat-near", rev = "537ef7d0aa3bf58d87b77a1c9660b2d0299b6c00" } near-workspaces = "0.10.0" near-self-update-proc = "0.1.2" diff --git a/contract/Cargo.toml b/contract/Cargo.toml index e28ff64a..3a36d93e 100644 --- a/contract/Cargo.toml +++ b/contract/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sweat_jar" -version = "1.0.1" +version = "2.0.0" authors = ["Sweat Economy"] edition = "2021" diff --git a/contract/src/claim/api.rs b/contract/src/claim/api.rs index 2ac58855..489a23d8 100644 --- a/contract/src/claim/api.rs +++ b/contract/src/claim/api.rs @@ -136,7 +136,7 @@ impl Contract { ) -> PromiseOrValue { use crate::ft_interface::FungibleTokenInterface; self.ft_contract() - .transfer(account_id, claimed_amount.get_total().0, "claim", &None) + .ft_transfer(account_id, claimed_amount.get_total().0, "claim", &None) .then(after_claim_call(claimed_amount, jars_before_transfer, event, now)) .into() } diff --git a/contract/src/claim/tests.rs b/contract/src/claim/tests.rs index 38fac71e..7062971a 100644 --- a/contract/src/claim/tests.rs +++ b/contract/src/claim/tests.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use near_sdk::{json_types::U128, test_utils::accounts, PromiseOrValue}; +use near_sdk::{json_types::U128, test_utils::test_env::alice, PromiseOrValue}; use sweat_jar_model::{ api::{ClaimApi, JarApi, WithdrawApi}, claimed_amount_view::ClaimedAmountView, @@ -11,31 +11,28 @@ use crate::{ common::{test_data::set_test_future_success, tests::Context, udecimal::UDecimal}, jar::model::Jar, product::model::{Apy, Product}, + test_utils::{admin, UnwrapPromise}, }; #[test] fn claim_total_when_nothing_to_claim() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let product = generate_product(); let jar = Jar::generate(0, &alice, &product.id).principal(100_000_000); let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar]); - context.switch_account(&alice); - let result = context.contract.claim_total(None); - - let PromiseOrValue::Value(value) = result else { - panic!(); - }; + context.switch_account(alice); + let value = context.contract().claim_total(None).unwrap(); assert_eq!(0, value.get_total().0); } #[test] fn claim_total_detailed_when_having_tokens() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let product = generate_product(); let jar_0 = Jar::generate(0, &alice, &product.id).principal(100_000_000); @@ -53,7 +50,7 @@ fn claim_total_detailed_when_having_tokens() { context.set_block_timestamp_in_ms(test_duration); context.switch_account(&alice); - let result = context.contract.claim_total(Some(true)); + let result = context.contract().claim_total(Some(true)); let PromiseOrValue::Value(ClaimedAmountView::Detailed(value)) = result else { panic!(); @@ -67,8 +64,8 @@ fn claim_total_detailed_when_having_tokens() { #[test] fn claim_partially_detailed_when_having_tokens() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let product = generate_product(); let jar_0 = Jar::generate(0, &alice, &product.id).principal(100_000_000); @@ -86,7 +83,7 @@ fn claim_partially_detailed_when_having_tokens() { context.set_block_timestamp_in_ms(test_duration); context.switch_account(&alice); - let result = context.contract.claim_jars( + let result = context.contract().claim_jars( vec![U32(jar_0.id), U32(jar_1.id)], Some(U128(jar_0_expected_interest + jar_1_expected_interest)), Some(true), @@ -104,8 +101,8 @@ fn claim_partially_detailed_when_having_tokens() { #[test] fn claim_pending_withdraw_jar() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let product = generate_product(); let jar_0 = Jar::generate(0, &alice, &product.id).principal(100_000_000); @@ -126,7 +123,7 @@ fn claim_pending_withdraw_jar() { context.set_block_timestamp_in_ms(test_duration); context.switch_account(&alice); - let result = context.contract.claim_jars( + let result = context.contract().claim_jars( vec![U32(jar_0.id), U32(jar_1.id)], Some(U128(jar_0_expected_interest.0 + jar_1_expected_interest)), Some(true), @@ -144,8 +141,8 @@ fn claim_pending_withdraw_jar() { #[test] fn claim_partially_detailed_when_having_tokens_and_request_sum_of_single_deposit() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let product = generate_product(); let jar_0 = Jar::generate(0, &alice, &product.id).principal(100_000_000); @@ -162,7 +159,7 @@ fn claim_partially_detailed_when_having_tokens_and_request_sum_of_single_deposit context.set_block_timestamp_in_ms(test_duration); context.switch_account(&alice); - let result = context.contract.claim_jars( + let result = context.contract().claim_jars( vec![U32(jar_0.id), U32(jar_1.id)], Some(U128(jar_0_expected_interest)), Some(true), @@ -180,8 +177,8 @@ fn claim_partially_detailed_when_having_tokens_and_request_sum_of_single_deposit #[test] fn claim_partially_when_having_tokens_to_claim() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let product = generate_product(); let jar = Jar::generate(0, &alice, &product.id).principal(100_000_000_000); @@ -190,20 +187,21 @@ fn claim_partially_when_having_tokens_to_claim() { context.set_block_timestamp_in_days(365); context.switch_account(&alice); - let PromiseOrValue::Value(claimed) = context.contract.claim_jars(vec![U32(jar.id)], Some(U128(100)), None) else { - panic!() - }; + let claimed = context + .contract() + .claim_jars(vec![U32(jar.id)], Some(U128(100)), None) + .unwrap(); assert_eq!(claimed.get_total().0, 100); - let jar = context.contract.get_jar(alice, U32(jar.id)); + let jar = context.contract().get_jar(alice, U32(jar.id)); assert_eq!(100, jar.claimed_balance.0); } #[test] fn dont_delete_jar_on_all_interest_claim() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let product = generate_product().apy(Apy::Constant(UDecimal::new(2, 1))); let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); @@ -213,10 +211,10 @@ fn dont_delete_jar_on_all_interest_claim() { context.switch_account(&alice); context - .contract + .contract() .claim_jars(vec![U32(jar.id)], Some(U128(200_000)), None); - let jar = context.contract.get_jar_internal(&alice, jar.id); + let jar = context.contract().get_jar_internal(&alice, jar.id); assert_eq!(200_000, jar.claimed_balance); let Some(ref cache) = jar.cache else { panic!() }; @@ -228,8 +226,8 @@ fn dont_delete_jar_on_all_interest_claim() { #[test] #[should_panic(expected = "Jar with id: 0 doesn't exist")] fn claim_all_withdraw_all_and_delete_jar() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let product = generate_product().apy(Apy::Constant(UDecimal::new(2, 1))); let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); @@ -243,16 +241,14 @@ fn claim_all_withdraw_all_and_delete_jar() { context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); context.switch_account(&alice); - let PromiseOrValue::Value(claimed) = context - .contract + let claimed = context + .contract() .claim_jars(vec![U32(jar_id)], Some(U128(200_000)), None) - else { - panic!() - }; + .unwrap(); assert_eq!(200_000, claimed.get_total().0); - let jar = context.contract.get_jar_internal(&alice, jar_id); + let jar = context.contract().get_jar_internal(&alice, jar_id); assert_eq!(200_000, jar.claimed_balance); let Some(ref cache) = jar.cache else { panic!() }; @@ -260,21 +256,19 @@ fn claim_all_withdraw_all_and_delete_jar() { assert_eq!(cache.interest, 0); assert_eq!(jar.principal, 1_000_000); - let PromiseOrValue::Value(withdrawn) = context.contract.withdraw(U32(jar_id), None) else { - panic!() - }; + let withdrawn = context.contract().withdraw(U32(jar_id), None).unwrap(); assert_eq!(withdrawn.withdrawn_amount, U128(1_000_000)); assert_eq!(withdrawn.fee, U128(0)); - let _jar = context.contract.get_jar_internal(&alice, jar_id); + let _jar = context.contract().get_jar_internal(&alice, jar_id); } #[test] #[should_panic(expected = "Jar with id: 0 doesn't exist")] fn withdraw_all_claim_all_and_delete_jar() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let product = generate_product().apy(Apy::Constant(UDecimal::new(2, 1))); let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); @@ -289,35 +283,31 @@ fn withdraw_all_claim_all_and_delete_jar() { context.switch_account(&alice); - let PromiseOrValue::Value(withdrawn) = context.contract.withdraw(U32(jar_id), None) else { - panic!() - }; + let withdrawn = context.contract().withdraw(U32(jar_id), None).unwrap(); assert_eq!(withdrawn.withdrawn_amount, U128(1_000_000)); assert_eq!(withdrawn.fee, U128(0)); - let jar = context.contract.get_jar_internal(&alice, jar_id); + let jar = context.contract().get_jar_internal(&alice, jar_id); assert_eq!(jar.principal, 0); - let PromiseOrValue::Value(claimed) = context - .contract + let claimed = context + .contract() .claim_jars(vec![U32(jar_id)], Some(U128(200_000)), None) - else { - panic!(); - }; + .unwrap(); assert_eq!(claimed.get_total(), U128(200_000)); - let _jar = context.contract.get_jar_internal(&alice, jar_id); + let _jar = context.contract().get_jar_internal(&alice, jar_id); } #[test] fn failed_future_claim() { set_test_future_success(false); - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let product = generate_product().apy(Apy::Constant(UDecimal::new(2, 1))); let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); @@ -327,18 +317,16 @@ fn failed_future_claim() { context.switch_account(&alice); - let jar_before_claim = context.contract.get_jar_internal(&alice, jar.id).clone(); + let jar_before_claim = context.contract().get_jar_internal(&alice, jar.id).clone(); - let PromiseOrValue::Value(claimed) = context - .contract + let claimed = context + .contract() .claim_jars(vec![U32(jar.id)], Some(U128(200_000)), None) - else { - panic!() - }; + .unwrap(); assert_eq!(claimed.get_total().0, 0); - let jar_after_claim = context.contract.get_jar_internal(&alice, jar.id); + let jar_after_claim = context.contract().get_jar_internal(&alice, jar.id); assert_eq!(jar_before_claim, jar_after_claim); } diff --git a/contract/src/common/mod.rs b/contract/src/common/mod.rs index 59c82f95..db29e92f 100644 --- a/contract/src/common/mod.rs +++ b/contract/src/common/mod.rs @@ -11,6 +11,9 @@ pub type Duration = u64; pub mod gas_data { use near_sdk::Gas; + /// Const of `ft_transfer` call in token contract + pub(crate) const GAS_FOR_FT_TRANSFER: Gas = Gas::from_tgas(6); + /// Const of after claim call with 1 jar const INITIAL_GAS_FOR_AFTER_CLAIM: Gas = Gas::from_tgas(4); @@ -25,15 +28,23 @@ pub mod gas_data { /// Value is measured with `measure_withdraw_test` /// Average gas for this method call don't exceed 3.4 `TGas`. 4 here just in case. pub(crate) const GAS_FOR_AFTER_WITHDRAW: Gas = Gas::from_tgas(4); + + /// Value is measured with `measure_withdraw_all` + /// 10 `TGas` was enough for 200 jars. 15 here just in case. + pub(crate) const GAS_FOR_BULK_AFTER_WITHDRAW: Gas = Gas::from_tgas(15); } #[cfg(test)] mod test { - use crate::common::gas_data::{GAS_FOR_AFTER_CLAIM, GAS_FOR_AFTER_WITHDRAW}; + use crate::common::gas_data::{ + GAS_FOR_AFTER_CLAIM, GAS_FOR_AFTER_WITHDRAW, GAS_FOR_BULK_AFTER_WITHDRAW, GAS_FOR_FT_TRANSFER, + }; #[test] fn test_gas_methods() { + assert_eq!(GAS_FOR_FT_TRANSFER.as_gas(), 6_000_000_000_000); assert_eq!(GAS_FOR_AFTER_CLAIM.as_gas(), 20_000_000_000_000); assert_eq!(GAS_FOR_AFTER_WITHDRAW.as_gas(), 4_000_000_000_000); + assert_eq!(GAS_FOR_BULK_AFTER_WITHDRAW.as_gas(), 15_000_000_000_000); } } diff --git a/contract/src/common/tests.rs b/contract/src/common/tests.rs index ddf6b302..2efab4ea 100644 --- a/contract/src/common/tests.rs +++ b/contract/src/common/tests.rs @@ -1,15 +1,19 @@ #![cfg(test)] -use std::time::Duration; +use std::{ + borrow::Borrow, + sync::{Arc, Mutex, MutexGuard}, + time::Duration, +}; use near_contract_standards::fungible_token::Balance; use near_sdk::{test_utils::VMContextBuilder, testing_env, AccountId, NearToken}; use sweat_jar_model::{api::InitApi, MS_IN_DAY, MS_IN_MINUTE}; -use crate::{jar::model::Jar, product::model::Product, Contract}; +use crate::{jar::model::Jar, product::model::Product, test_utils::AfterCatchUnwind, Contract}; pub(crate) struct Context { - pub contract: Contract, + contract: Arc>, pub owner: AccountId, ft_contract_id: AccountId, builder: VMContextBuilder, @@ -36,21 +40,25 @@ impl Context { owner, ft_contract_id, builder, - contract, + contract: Arc::new(Mutex::new(contract)), } } - pub(crate) fn with_products(mut self, products: &[Product]) -> Self { + pub(crate) fn contract(&self) -> MutexGuard { + self.contract.lock().unwrap() + } + + pub(crate) fn with_products(self, products: &[Product]) -> Self { for product in products { - self.contract.products.insert(&product.id, product); + self.contract().products.insert(&product.id, product); } self } - pub(crate) fn with_jars(mut self, jars: &[Jar]) -> Self { + pub(crate) fn with_jars(self, jars: &[Jar]) -> Self { for jar in jars { - self.contract + self.contract() .account_jars .entry(jar.account_id.clone()) .or_default() @@ -77,10 +85,11 @@ impl Context { testing_env!(self.builder.build()); } - pub(crate) fn switch_account(&mut self, account_id: &AccountId) { + pub(crate) fn switch_account(&mut self, account_id: impl Borrow) { + let account_id = account_id.borrow().clone(); self.builder .predecessor_account_id(account_id.clone()) - .signer_account_id(account_id.clone()); + .signer_account_id(account_id); testing_env!(self.builder.build()); } @@ -101,3 +110,9 @@ impl Context { testing_env!(self.builder.build()); } } + +impl AfterCatchUnwind for Context { + fn after_catch_unwind(&self) { + self.contract.clear_poison(); + } +} diff --git a/contract/src/event.rs b/contract/src/event.rs index a987031e..aa2f417d 100644 --- a/contract/src/event.rs +++ b/contract/src/event.rs @@ -26,8 +26,10 @@ pub enum EventKind { CreateJar(EventJar), Claim(Vec), Withdraw(WithdrawData), + WithdrawAll(Vec), Migration(Vec), Restake(RestakeData), + RestakeAll(Vec), ApplyPenalty(PenaltyData), BatchApplyPenalty(BatchPenaltyData), EnableProduct(EnableProductData), @@ -85,8 +87,8 @@ pub struct ClaimEventItem { #[serde(crate = "near_sdk::serde")] pub struct WithdrawData { pub id: JarId, - pub fee_amount: U128, - pub withdrawn_amount: U128, + pub fee: U128, + pub amount: U128, } #[derive(Serialize, Deserialize, Debug)] @@ -195,7 +197,7 @@ mod test { .to_json_event_string(), r#"EVENT_JSON:{ "standard": "sweat_jar", - "version": "1.0.1", + "version": "2.0.0", "event": "top_up", "data": { "id": 10, @@ -223,7 +225,7 @@ mod test { .to_json_event_string(), r#"EVENT_JSON:{ "standard": "sweat_jar", - "version": "1.0.1", + "version": "2.0.0", "event": "create_jar", "data": { "id": 555, diff --git a/contract/src/ft_interface.rs b/contract/src/ft_interface.rs index 7761e4f6..493f593a 100644 --- a/contract/src/ft_interface.rs +++ b/contract/src/ft_interface.rs @@ -23,12 +23,12 @@ impl Contract { } pub(crate) trait FungibleTokenInterface { - fn transfer(&self, receiver_id: &AccountId, amount: u128, memo: &str, fee: &Option) -> Promise; + fn ft_transfer(&self, receiver_id: &AccountId, amount: u128, memo: &str, fee: &Option) -> Promise; } impl FungibleTokenInterface for FungibleTokenContract { #[mutants::skip] // Covered by integration tests - fn transfer(&self, receiver_id: &AccountId, amount: u128, memo: &str, fee: &Option) -> Promise { + fn ft_transfer(&self, receiver_id: &AccountId, amount: u128, memo: &str, fee: &Option) -> Promise { if let Some(fee) = fee { Promise::new(self.address.clone()) .ft_transfer(receiver_id, amount - fee.amount, Some(memo.to_string())) @@ -39,11 +39,11 @@ impl FungibleTokenInterface for FungibleTokenContract { } } -trait FtTransferPromise { +trait FungibleTokenPromise { fn ft_transfer(self, receiver_id: &AccountId, amount: TokenAmount, memo: Option) -> Promise; } -impl FtTransferPromise for Promise { +impl FungibleTokenPromise for Promise { fn ft_transfer(self, receiver_id: &AccountId, amount: TokenAmount, memo: Option) -> Promise { let args = serde_json::to_vec(&json!({ "receiver_id": receiver_id, diff --git a/contract/src/ft_receiver.rs b/contract/src/ft_receiver.rs index 945d2379..33846418 100644 --- a/contract/src/ft_receiver.rs +++ b/contract/src/ft_receiver.rs @@ -69,7 +69,11 @@ mod tests { use std::panic::catch_unwind; use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; - use near_sdk::{json_types::U128, serde_json::json, test_utils::accounts}; + use near_sdk::{ + json_types::U128, + serde_json::json, + test_utils::test_env::{alice, bob}, + }; use sweat_jar_model::{api::JarApi, U32}; use crate::{ @@ -79,13 +83,14 @@ mod tests { helpers::MessageSigner, model::{Apy, DowngradableApy, Product}, }, + test_utils::admin, Contract, }; #[test] fn transfer_with_create_jar_message() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let reference_product = Product::generate("test_product").enabled(true); let mut context = Context::new(admin).with_products(&[reference_product.clone()]); @@ -102,17 +107,17 @@ mod tests { context.switch_account_to_ft_contract_account(); context - .contract + .contract() .ft_on_transfer(alice.clone(), U128(1_000_000), msg.to_string()); - let jar = context.contract.get_jar(alice, U32(1)); + let jar = context.contract().get_jar(alice, U32(1)); assert_eq!(jar.id.0, 1); } #[test] fn transfer_with_duplicate_create_jar_message() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let (signer, reference_product) = generate_premium_product_context(); @@ -145,15 +150,15 @@ mod tests { context.switch_account_to_ft_contract_account(); context - .contract + .contract() .ft_on_transfer(alice.clone(), U128(ticket_amount), msg.to_string()); - let jar = context.contract.get_jar(alice.clone(), U32(1)); + let jar = context.contract().get_jar(alice.clone(), U32(1)); assert_eq!(jar.id.0, 1); let result = catch_unwind(move || { context - .contract + .contract() .ft_on_transfer(alice.clone(), U128(ticket_amount), msg.to_string()) }); assert!(result.is_err()); @@ -161,8 +166,8 @@ mod tests { #[test] fn transfer_with_top_up_message_for_refillable_product() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let reference_product = Product::generate("refillable_product") .enabled(true) @@ -184,18 +189,18 @@ mod tests { context.switch_account_to_ft_contract_account(); let top_up_amount = 700; context - .contract + .contract() .ft_on_transfer(alice.clone(), U128(top_up_amount), msg.to_string()); - let jar = context.contract.get_jar(alice, U32(0)); + let jar = context.contract().get_jar(alice, U32(0)); assert_eq!(initial_jar_principal + top_up_amount, jar.principal.0); } #[test] #[should_panic(expected = "The product doesn't allow top-ups")] fn transfer_with_top_up_message_for_not_refillable_product() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let reference_product = Product::generate("not_refillable_product") .enabled(true) @@ -214,13 +219,13 @@ mod tests { }); context.switch_account_to_ft_contract_account(); - context.contract.ft_on_transfer(alice, U128(100), msg.to_string()); + context.contract().ft_on_transfer(alice, U128(100), msg.to_string()); } #[test] fn transfer_with_top_up_message_for_flexible_product() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let reference_product = Product::generate("flexible_product") .enabled(true) @@ -243,18 +248,18 @@ mod tests { let top_up_amount = 1_000; context - .contract + .contract() .ft_on_transfer(alice.clone(), U128(top_up_amount), msg.to_string()); - let jar = context.contract.get_jar(alice, U32(0)); + let jar = context.contract().get_jar(alice, U32(0)); assert_eq!(initial_jar_principal + top_up_amount, jar.principal.0); } #[test] fn transfer_with_migration_message() { - let alice = accounts(0); - let bob = accounts(1); - let admin = accounts(2); + let alice = alice(); + let bob = bob(); + let admin = admin(); let reference_product = Product::generate("product").enabled(true).cap(0, 1_000_000); let reference_restakable_product = Product::generate("restakable_product").enabled(true).cap(0, 1_000_000); @@ -286,14 +291,14 @@ mod tests { context.switch_account_to_ft_contract_account(); context - .contract + .contract() .ft_on_transfer(admin, U128(amount_alice + amount_bob), msg.to_string()); - let alice_jars = context.contract.get_jars_for_account(alice); + let alice_jars = context.contract().get_jars_for_account(alice); assert_eq!(alice_jars.len(), 1); assert_eq!(alice_jars.first().unwrap().principal.0, amount_alice); - let bob_jars = context.contract.get_jars_for_account(bob); + let bob_jars = context.contract().get_jars_for_account(bob); assert_eq!(bob_jars.len(), 1); assert_eq!(bob_jars.first().unwrap().principal.0, amount_bob); } @@ -301,8 +306,8 @@ mod tests { #[test] #[should_panic(expected = "Migration can be performed only by admin")] fn transfer_with_migration_message_by_not_admin() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let reference_product = Product::generate("product").enabled(true).cap(0, 1_000_000); let reference_restakable_product = Product::generate("restakable_product").enabled(true).cap(0, 1_000_000); @@ -325,35 +330,35 @@ mod tests { context.switch_account_to_ft_contract_account(); context - .contract + .contract() .ft_on_transfer(alice, U128(amount_alice), msg.to_string()); } #[test] #[should_panic(expected = "Unable to deserialize msg")] fn transfer_with_unknown_message() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let mut context = Context::new(admin); context.switch_account_to_ft_contract_account(); context - .contract + .contract() .ft_on_transfer(alice, U128(300), "something".to_string()); } #[test] #[should_panic(expected = "Can receive tokens only from token")] fn transfer_by_not_token_account() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let mut context = Context::new(admin); context.switch_account(&alice); context - .contract + .contract() .ft_on_transfer(alice.clone(), U128(300), "something".to_string()); } diff --git a/contract/src/internal.rs b/contract/src/internal.rs index 66b756c0..8ea7d577 100644 --- a/contract/src/internal.rs +++ b/contract/src/internal.rs @@ -76,15 +76,14 @@ impl Contract { #[cfg(test)] mod test { - use near_sdk::test_utils::accounts; - use crate::common::tests::Context; + use crate::{common::tests::Context, test_utils::admin}; #[test] #[should_panic(expected = r#"Can be performed only by admin"#)] fn self_update_without_access() { - let admin = accounts(1); - let mut context = Context::new(admin); - context.contract.update_contract(vec![], None); + let admin = admin(); + let context = Context::new(admin); + context.contract().update_contract(vec![], None); } } diff --git a/contract/src/jar/api.rs b/contract/src/jar/api.rs index 26767165..ee6be114 100644 --- a/contract/src/jar/api.rs +++ b/contract/src/jar/api.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use near_sdk::{env, env::panic_str, json_types::U128, near_bindgen, require, AccountId}; use sweat_jar_model::{ api::JarApi, - jar::{AggregatedInterestView, AggregatedTokenAmountView, JarIdView, JarView}, + jar::{AggregatedInterestView, AggregatedTokenAmountView, JarId, JarIdView, JarView}, TokenAmount, U32, }; @@ -13,6 +13,55 @@ use crate::{ Contract, ContractExt, JarsStorage, }; +impl Contract { + fn can_be_restacked(&self, jar: &Jar, now: u64) -> bool { + let product = self.get_product(&jar.product_id); + !jar.is_empty() && product.is_enabled && product.allows_restaking() && jar.is_liquidable(&product, now) + } + + fn restake_internal(&mut self, jar_id: JarIdView) -> (JarId, JarView) { + let jar_id = jar_id.0; + let account_id = env::predecessor_account_id(); + + let restaked_jar_id = self.increment_and_get_last_jar_id(); + + let jar = self.get_jar_internal(&account_id, jar_id); + + let product = self.get_product(&jar.product_id); + + require!(product.allows_restaking(), "The product doesn't support restaking"); + require!(product.is_enabled, "The product is disabled"); + + let now = env::block_timestamp_ms(); + require!(jar.is_liquidable(&product, now), "The jar is not mature yet"); + require!(!jar.is_empty(), "The jar is empty, nothing to restake"); + + let principal = jar.principal; + + let new_jar = Jar::create( + restaked_jar_id, + jar.account_id.clone(), + jar.product_id.clone(), + principal, + now, + ); + + let withdraw_jar = jar.withdrawn(&product, principal, now); + let should_be_closed = withdraw_jar.should_be_closed(&product, now); + + if should_be_closed { + self.delete_jar(&withdraw_jar.account_id, withdraw_jar.id); + } else { + let jar_id = withdraw_jar.id; + *self.get_jar_mut_internal(&account_id, jar_id) = withdraw_jar; + } + + self.add_new_jar(&account_id, new_jar.clone()); + + (jar_id, new_jar.into()) + } +} + #[near_bindgen] impl JarApi for Contract { // TODO: restore previous version after V2 migration @@ -95,51 +144,51 @@ impl JarApi for Contract { } fn restake(&mut self, jar_id: JarIdView) -> JarView { - let jar_id = jar_id.0; - let account_id = env::predecessor_account_id(); - - self.migrate_account_jars_if_needed(account_id.clone()); + self.migrate_account_jars_if_needed(env::predecessor_account_id()); + let (old_id, jar) = self.restake_internal(jar_id); - let restaked_jar_id = self.increment_and_get_last_jar_id(); + emit(EventKind::Restake(RestakeData { + old_id, + new_id: jar.id.0, + })); - let jar = self.get_jar_internal(&account_id, jar_id); + jar + } - let product = self.get_product(&jar.product_id); + fn restake_all(&mut self) -> Vec { + let account_id = env::predecessor_account_id(); - require!(product.allows_restaking(), "The product doesn't support restaking"); - require!(product.is_enabled, "The product is disabled"); + self.migrate_account_jars_if_needed(account_id.clone()); let now = env::block_timestamp_ms(); - require!(jar.is_liquidable(&product, now), "The jar is not mature yet"); - require!(!jar.is_empty(), "The jar is empty, nothing to restake"); - let principal = jar.principal; - - let new_jar = Jar::create( - restaked_jar_id, - jar.account_id.clone(), - jar.product_id.clone(), - principal, - now, - ); - - let withdraw_jar = jar.withdrawn(&product, principal, now); - let should_be_closed = withdraw_jar.should_be_closed(&product, now); - - if should_be_closed { - self.delete_jar(&withdraw_jar.account_id, withdraw_jar.id); - } else { - let jar_id = withdraw_jar.id; - *self.get_jar_mut_internal(&account_id, jar_id) = withdraw_jar; + let jars: Vec = self + .account_jars + .get(&account_id) + .unwrap_or_else(|| { + panic_str(&format!("Jars for account {account_id} don't exist")); + }) + .jars + .iter() + .filter(|j| self.can_be_restacked(j, now)) + .cloned() + .collect(); + + let mut result = vec![]; + + let mut event_data = vec![]; + + for jar in &jars { + let (old_id, restaked) = self.restake_internal(jar.id.into()); + event_data.push(RestakeData { + old_id, + new_id: restaked.id.0, + }); + result.push(restaked); } - self.add_new_jar(&account_id, new_jar.clone()); - - emit(EventKind::Restake(RestakeData { - old_id: jar_id, - new_id: new_jar.id, - })); + emit(EventKind::RestakeAll(event_data)); - new_jar.into() + result } } diff --git a/contract/src/jar/mod.rs b/contract/src/jar/mod.rs index c8182b19..95388306 100644 --- a/contract/src/jar/mod.rs +++ b/contract/src/jar/mod.rs @@ -1,4 +1,4 @@ pub mod api; pub mod model; -pub mod tests; +mod tests; pub mod view; diff --git a/contract/src/jar/model/v1.rs b/contract/src/jar/model/v1.rs index d9e5c57f..bf14c2a2 100644 --- a/contract/src/jar/model/v1.rs +++ b/contract/src/jar/model/v1.rs @@ -280,7 +280,7 @@ impl Contract { pub(crate) fn get_jar_mut_internal(&mut self, account: &AccountId, id: JarId) -> &mut Jar { self.account_jars .get_mut(account) - .unwrap_or_else(|| panic_str(&format!("Account '{account}' doesn't exist"))) + .unwrap_or_else(|| env::panic_str(&format!("Account '{account}' doesn't exist"))) .get_jar_mut(id) } diff --git a/contract/src/jar/model/versioned.rs b/contract/src/jar/model/versioned.rs index 464b2458..6d88a88b 100644 --- a/contract/src/jar/model/versioned.rs +++ b/contract/src/jar/model/versioned.rs @@ -41,6 +41,14 @@ impl JarVersioned { .into() } + pub fn locked(&self) -> Self { + JarV1 { + is_pending_withdraw: true, + ..self.inner() + } + .into() + } + pub fn unlocked(&self) -> Self { JarV1 { is_pending_withdraw: false, diff --git a/contract/src/jar/tests.rs b/contract/src/jar/tests.rs deleted file mode 100644 index f8606f99..00000000 --- a/contract/src/jar/tests.rs +++ /dev/null @@ -1,432 +0,0 @@ -#![cfg(test)] - -use fake::Fake; -use near_sdk::{test_utils::accounts, Timestamp}; -use sweat_jar_model::MS_IN_YEAR; - -use crate::{ - common::udecimal::UDecimal, - product::model::{Apy, Product}, - Jar, -}; - -#[test] -fn get_interest_before_maturity() { - let product = Product::generate("product") - .apy(Apy::Constant(UDecimal::new(12, 2))) - .lockup_term(2 * MS_IN_YEAR); - let jar = Jar::generate(0, &accounts(0), &product.id).principal(100_000_000); - - let interest = jar.get_interest(&product, MS_IN_YEAR).0; - assert_eq!(12_000_000, interest); -} - -#[test] -fn get_interest_after_maturity() { - let product = Product::generate("product") - .apy(Apy::Constant(UDecimal::new(12, 2))) - .lockup_term(MS_IN_YEAR); - let jar = Jar::generate(0, &accounts(0), &product.id).principal(100_000_000); - - let interest = jar.get_interest(&product, 400 * 24 * 60 * 60 * 1000).0; - assert_eq!(12_000_000, interest); -} - -#[test] -fn interest_precision() { - let product = Product::generate("product") - .apy(Apy::Constant(UDecimal::new(1, 0))) - .lockup_term(MS_IN_YEAR); - let jar = Jar::generate(0, &accounts(0), &product.id).principal(MS_IN_YEAR as u128); - - assert_eq!(jar.get_interest(&product, 10000000000).0, 10000000000); - assert_eq!(jar.get_interest(&product, 10000000001).0, 10000000001); - - for _ in 0..100 { - let time: Timestamp = (10..MS_IN_YEAR).fake(); - assert_eq!(jar.get_interest(&product, time).0, time as u128); - } -} - -#[cfg(test)] -mod signature_tests { - use near_sdk::{ - json_types::{Base64VecU8, U128, U64}, - test_utils::accounts, - }; - use sweat_jar_model::{ - api::{JarApi, ProductApi}, - MS_IN_YEAR, U32, - }; - - use crate::{ - common::{tests::Context, udecimal::UDecimal}, - jar::model::JarTicket, - product::{ - helpers::MessageSigner, - model::{Apy, DowngradableApy, Product}, - }, - Jar, - }; - - #[test] - fn verify_ticket_with_valid_signature_and_date() { - let admin = accounts(0); - - let signer = MessageSigner::new(); - let reference_product = generate_premium_product("premium_product", &signer); - let mut context = Context::new(admin.clone()).with_products(&[reference_product.clone()]); - - let amount = 14_000_000; - let ticket = JarTicket { - product_id: reference_product.id, - valid_until: U64(123000000), - }; - - let signature = signer.sign(context.get_signature_material(&admin, &ticket, amount).as_str()); - - context - .contract - .verify(&admin, amount, &ticket, Some(Base64VecU8(signature))); - } - - #[test] - #[should_panic(expected = "Signature must be 64 bytes")] - fn verify_ticket_with_invalid_signature() { - let alice = accounts(0); - let admin = accounts(1); - - let signer = MessageSigner::new(); - let reference_product = generate_premium_product("premium_product", &signer); - let mut context = Context::new(admin).with_products(&[reference_product.clone()]); - - let amount = 1_000_000; - let ticket = JarTicket { - product_id: reference_product.id, - valid_until: U64(100000000), - }; - - let signature: Vec = vec![0, 1, 2]; - - context - .contract - .verify(&alice, amount, &ticket, Some(Base64VecU8(signature))); - } - - #[test] - #[should_panic(expected = "Not matching signature")] - fn verify_ticket_with_not_matching_signature() { - let admin = accounts(0); - - let signer = MessageSigner::new(); - let product = generate_premium_product("premium_product", &signer); - let another_product = generate_premium_product("another_premium_product", &MessageSigner::new()); - - let mut context = Context::new(admin.clone()).with_products(&[product, another_product.clone()]); - - let amount = 15_000_000; - let ticket_for_another_product = JarTicket { - product_id: another_product.id, - valid_until: U64(100000000), - }; - - // signature made for wrong product - let signature = signer.sign( - context - .get_signature_material(&admin, &ticket_for_another_product, amount) - .as_str(), - ); - - context.contract.verify( - &admin, - amount, - &ticket_for_another_product, - Some(Base64VecU8(signature)), - ); - } - - #[test] - #[should_panic(expected = "Ticket is outdated")] - fn verify_ticket_with_invalid_date() { - let alice = accounts(0); - let admin = accounts(1); - - let signer = MessageSigner::new(); - let reference_product = generate_premium_product("premium_product", &signer); - let mut context = Context::new(admin).with_products(&[reference_product.clone()]); - - context.set_block_timestamp_in_days(365); - - let amount = 5_000_000; - let ticket = JarTicket { - product_id: reference_product.id, - valid_until: U64(100000000), - }; - - let signature = signer.sign(context.get_signature_material(&alice, &ticket, amount).as_str()); - - context - .contract - .verify(&alice, amount, &ticket, Some(Base64VecU8(signature))); - } - - #[test] - #[should_panic(expected = "Product 'not_existing_product' doesn't exist")] - fn verify_ticket_with_not_existing_product() { - let admin = accounts(0); - - let mut context = Context::new(admin.clone()); - - context.switch_account(&admin); - - let signer = MessageSigner::new(); - let not_existing_product = generate_premium_product("not_existing_product", &signer); - - let amount = 500_000; - let ticket = JarTicket { - product_id: not_existing_product.id, - valid_until: U64(100000000), - }; - - let signature = signer.sign(context.get_signature_material(&admin, &ticket, amount).as_str()); - - context - .contract - .verify(&admin, amount, &ticket, Some(Base64VecU8(signature))); - } - - #[test] - #[should_panic(expected = "Signature is required")] - fn verify_ticket_without_signature_when_required() { - let admin = accounts(0); - - let signer = MessageSigner::new(); - let product = generate_premium_product("not_existing_product", &signer); - let mut context = Context::new(admin.clone()).with_products(&[product.clone()]); - - let amount = 3_000_000; - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(100000000), - }; - - context.contract.verify(&admin, amount, &ticket, None); - } - - #[test] - fn verify_ticket_without_signature_when_not_required() { - let admin = accounts(0); - - let product = generate_product("regular_product"); - let mut context = Context::new(admin.clone()).with_products(&[product.clone()]); - - let amount = 4_000_000_000; - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(0), - }; - - context.contract.verify(&admin, amount, &ticket, None); - } - - #[test] - #[should_panic(expected = "Account 'bob' doesn't exist")] - fn restake_by_not_owner() { - let alice = accounts(0); - let admin = accounts(1); - - let product = generate_product("restakable_product").with_allows_restaking(true); - let alice_jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); - let mut context = Context::new(admin.clone()) - .with_products(&[product]) - .with_jars(&[alice_jar.clone()]); - - context.switch_account(&admin); - context.contract.restake(U32(alice_jar.id)); - } - - #[test] - #[should_panic(expected = "The product doesn't support restaking")] - fn restake_when_restaking_is_not_supported() { - let alice = accounts(0); - let admin = accounts(1); - - let product = generate_product("not_restakable_product").with_allows_restaking(false); - let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.switch_account(&alice); - context.contract.restake(U32(jar.id)); - } - - #[test] - #[should_panic(expected = "The jar is not mature yet")] - fn restake_before_maturity() { - let alice = accounts(0); - let admin = accounts(1); - - let product = generate_product("restakable_product").with_allows_restaking(true); - let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.switch_account(&alice); - context.contract.restake(U32(jar.id)); - } - - #[test] - #[should_panic(expected = "The product is disabled")] - fn restake_with_disabled_product() { - let alice = accounts(0); - let admin = accounts(1); - - let product = generate_product("restakable_product").with_allows_restaking(true); - let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); - let mut context = Context::new(admin.clone()) - .with_products(&[product.clone()]) - .with_jars(&[jar.clone()]); - - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| context.contract.set_enabled(product.id, false)); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - context.contract.restake(U32(jar.id)); - } - - #[test] - #[should_panic(expected = "The jar is empty, nothing to restake")] - fn restake_empty_jar() { - let alice = accounts(0); - let admin = accounts(1); - - let product = generate_product("restakable_product") - .lockup_term(MS_IN_YEAR) - .with_allows_restaking(true); - let jar = Jar::generate(0, &alice, &product.id).principal(0); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - context.contract.restake(U32(jar.id)); - } - - #[test] - fn restake_after_maturity_for_restakable_product() { - let alice = accounts(0); - let admin = accounts(1); - - let product = generate_product("restakable_product") - .with_allows_restaking(true) - .lockup_term(MS_IN_YEAR); - let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - context.contract.restake(U32(jar.id)); - - let alice_jars = context.contract.get_jars_for_account(alice); - - assert_eq!(2, alice_jars.len()); - assert_eq!(0, alice_jars.iter().find(|item| item.id.0 == 0).unwrap().principal.0); - assert_eq!( - 1_000_000, - alice_jars.iter().find(|item| item.id.0 == 1).unwrap().principal.0 - ); - } - - #[test] - #[should_panic(expected = "The product doesn't support restaking")] - fn restake_after_maturity_for_not_restakable_product() { - let alice = accounts(0); - let admin = accounts(1); - - let reference_product = generate_product("not_restakable_product").with_allows_restaking(false); - let jar = Jar::generate(0, &alice, &reference_product.id).principal(1_000_000); - let mut context = Context::new(admin.clone()) - .with_products(&[reference_product.clone()]) - .with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - context.contract.restake(U32(jar.id)); - } - - #[test] - #[should_panic(expected = "It's not possible to create new jars for this product")] - fn create_jar_for_disabled_product() { - let alice = accounts(0); - let admin = accounts(1); - - let product = generate_product("restakable_product").enabled(false); - let mut context = Context::new(admin).with_products(&[product.clone()]); - - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(0), - }; - context.contract.create_jar(alice, ticket, U128(1_000_000), None); - } - - fn generate_premium_product(id: &str, signer: &MessageSigner) -> Product { - Product::generate(id) - .enabled(true) - .public_key(signer.public_key()) - .cap(0, 100_000_000_000) - .apy(Apy::Downgradable(DowngradableApy { - default: UDecimal::new(20, 2), - fallback: UDecimal::new(10, 2), - })) - } - - fn generate_product(id: &str) -> Product { - Product::generate(id) - .enabled(true) - .cap(0, 100_000_000_000) - .apy(Apy::Constant(UDecimal::new(20, 2))) - } -} - -mod helpers { - use near_sdk::AccountId; - use sweat_jar_model::TokenAmount; - - use crate::{common::Timestamp, jar::model::JarV1, Jar}; - - impl Jar { - pub(crate) fn generate(id: u32, account_id: &AccountId, product_id: &str) -> Jar { - JarV1 { - id, - account_id: account_id.clone(), - product_id: product_id.to_string(), - created_at: 0, - principal: 0, - cache: None, - claimed_balance: 0, - is_pending_withdraw: false, - is_penalty_applied: false, - claim_remainder: Default::default(), - } - .into() - } - - pub(crate) fn principal(mut self, principal: TokenAmount) -> Jar { - self.principal = principal; - self - } - - pub(crate) fn created_at(mut self, created_at: Timestamp) -> Jar { - self.created_at = created_at; - self - } - - pub(crate) fn pending_withdraw(mut self) -> Jar { - self.is_pending_withdraw = true; - self - } - } -} diff --git a/contract/src/jar/tests/mod.rs b/contract/src/jar/tests/mod.rs new file mode 100644 index 00000000..25fb1651 --- /dev/null +++ b/contract/src/jar/tests/mod.rs @@ -0,0 +1,5 @@ +#![cfg(test)] + +mod restake; +mod restake_all; +mod tests; diff --git a/contract/src/jar/tests/restake.rs b/contract/src/jar/tests/restake.rs new file mode 100644 index 00000000..8931c73f --- /dev/null +++ b/contract/src/jar/tests/restake.rs @@ -0,0 +1,156 @@ +use near_sdk::test_utils::test_env::{alice, bob, carol}; +use sweat_jar_model::{ + api::{JarApi, ProductApi}, + MS_IN_YEAR, U32, +}; + +use crate::{ + common::tests::Context, + jar::model::Jar, + test_utils::{admin, expect_panic, generate_product}, +}; + +#[test] +fn restake_by_not_owner() { + let alice = alice(); + + let product = generate_product("restakable_product").with_allows_restaking(true); + let alice_jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); + let mut ctx = Context::new(admin()) + .with_products(&[product]) + .with_jars(&[alice_jar.clone()]); + + ctx.switch_account(bob()); + expect_panic(&ctx, "Account 'bob.near' doesn't exist", || { + ctx.contract().restake(U32(alice_jar.id)); + }); + + expect_panic(&ctx, "Jars for account bob.near don't exist", || { + ctx.contract().restake_all(); + }); + + ctx.switch_account(carol()); + expect_panic(&ctx, "Account 'carol.near' doesn't exist", || { + ctx.contract().restake(U32(alice_jar.id)); + }); + + expect_panic(&ctx, "Jars for account carol.near don't exist", || { + ctx.contract().restake_all(); + }); +} + +#[test] +#[should_panic(expected = "The jar is not mature yet")] +fn restake_before_maturity() { + let alice = alice(); + let admin = admin(); + + let product = generate_product("restakable_product").with_allows_restaking(true); + let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); + let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); + + context.switch_account(&alice); + assert!(context.contract().restake_all().is_empty()); + context.contract().restake(U32(jar.id)); +} + +#[test] +#[should_panic(expected = "The product doesn't support restaking")] +fn restake_when_restaking_is_not_supported() { + let alice = alice(); + let admin = admin(); + + let product = generate_product("not_restakable_product").with_allows_restaking(false); + let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); + let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); + + context.switch_account(&alice); + assert!(context.contract().restake_all().is_empty()); + context.contract().restake(U32(jar.id)); +} + +#[test] +#[should_panic(expected = "The product is disabled")] +fn restake_with_disabled_product() { + let alice = alice(); + let admin = admin(); + + let product = generate_product("restakable_product").with_allows_restaking(true); + let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); + let mut context = Context::new(admin.clone()) + .with_products(&[product.clone()]) + .with_jars(&[jar.clone()]); + + context.switch_account(&admin); + context.with_deposit_yocto(1, |context| context.contract().set_enabled(product.id, false)); + + context.set_block_timestamp_in_days(366); + + context.switch_account(&alice); + assert!(context.contract().restake_all().is_empty()); + context.contract().restake(U32(jar.id)); +} + +#[test] +#[should_panic(expected = "The jar is empty, nothing to restake")] +fn restake_empty_jar() { + let alice = alice(); + let admin = admin(); + + let product = generate_product("restakable_product") + .lockup_term(MS_IN_YEAR) + .with_allows_restaking(true); + let jar = Jar::generate(0, &alice, &product.id).principal(0); + let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); + + context.set_block_timestamp_in_days(366); + + context.switch_account(&alice); + assert!(context.contract().restake_all().is_empty()); + context.contract().restake(U32(jar.id)); +} + +#[test] +fn restake_after_maturity_for_restakable_product() { + let alice = alice(); + let admin = admin(); + + let product = generate_product("restakable_product") + .with_allows_restaking(true) + .lockup_term(MS_IN_YEAR); + let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); + let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); + + context.set_block_timestamp_in_days(366); + + context.switch_account(&alice); + context.contract().restake(U32(jar.id)); + + let alice_jars = context.contract().get_jars_for_account(alice); + + assert_eq!(2, alice_jars.len()); + assert_eq!(0, alice_jars.iter().find(|item| item.id.0 == 0).unwrap().principal.0); + assert_eq!( + 1_000_000, + alice_jars.iter().find(|item| item.id.0 == 1).unwrap().principal.0 + ); +} + +#[test] +#[should_panic(expected = "The product doesn't support restaking")] +fn restake_after_maturity_for_not_restakable_product() { + let alice = alice(); + let admin = admin(); + + let reference_product = generate_product("not_restakable_product").with_allows_restaking(false); + let jar = Jar::generate(0, &alice, &reference_product.id).principal(1_000_000); + let mut context = Context::new(admin.clone()) + .with_products(&[reference_product.clone()]) + .with_jars(&[jar.clone()]); + + context.set_block_timestamp_in_days(366); + + context.switch_account(&alice); + assert!(context.contract().restake_all().is_empty()); + context.contract().restake(U32(jar.id)); +} diff --git a/contract/src/jar/tests/restake_all.rs b/contract/src/jar/tests/restake_all.rs new file mode 100644 index 00000000..1b51a3f4 --- /dev/null +++ b/contract/src/jar/tests/restake_all.rs @@ -0,0 +1,113 @@ +use fake::Fake; +use near_sdk::test_utils::test_env::alice; +use sweat_jar_model::{api::JarApi, MS_IN_YEAR}; + +use crate::{ + common::tests::Context, + jar::model::Jar, + test_utils::{admin, generate_product, JAR_ID_RANGE, PRINCIPAL}, +}; + +#[test] +fn restake_all() { + let alice = alice(); + let admin = admin(); + + let restackable_product = generate_product("restakable_product") + .with_allows_restaking(true) + .lockup_term(MS_IN_YEAR); + + let disabled_restackable_product = generate_product("disabled_restackable_product") + .with_allows_restaking(true) + .enabled(false) + .lockup_term(MS_IN_YEAR); + + let non_restackable_product = generate_product("non_restakable_product") + .with_allows_restaking(false) + .lockup_term(MS_IN_YEAR); + + let long_term_restackable_product = generate_product("long_term_restackable_product") + .with_allows_restaking(true) + .lockup_term(MS_IN_YEAR * 2); + + let restackable_jar_1 = Jar::generate(JAR_ID_RANGE.fake(), &alice, &restackable_product.id).principal(PRINCIPAL); + let restackable_jar_2 = Jar::generate(JAR_ID_RANGE.fake(), &alice, &restackable_product.id).principal(PRINCIPAL); + + let disabled_jar = + Jar::generate(JAR_ID_RANGE.fake(), &alice, &disabled_restackable_product.id).principal(PRINCIPAL); + + let non_restackable_jar = + Jar::generate(JAR_ID_RANGE.fake(), &alice, &non_restackable_product.id).principal(PRINCIPAL); + + let long_term_jar = + Jar::generate(JAR_ID_RANGE.fake(), &alice, &long_term_restackable_product.id).principal(PRINCIPAL); + + let mut context = Context::new(admin) + .with_products(&[ + restackable_product, + disabled_restackable_product, + non_restackable_product, + long_term_restackable_product, + ]) + .with_jars(&[ + restackable_jar_1.clone(), + restackable_jar_2.clone(), + disabled_jar.clone(), + non_restackable_jar.clone(), + long_term_jar.clone(), + ]); + + context.set_block_timestamp_in_days(366); + + context.switch_account(&alice); + + let restacked_jars = context.contract().restake_all(); + + assert_eq!(restacked_jars.len(), 2); + assert_eq!(restacked_jars.iter().map(|j| j.id.0).collect::>(), vec![1, 2]); + + let all_jars = context.contract().get_jars_for_account(alice); + + assert_eq!( + all_jars.iter().map(|j| j.id.0).collect::>(), + [ + restackable_jar_1.id, + restackable_jar_2.id, + disabled_jar.id, + non_restackable_jar.id, + long_term_jar.id, + 1, + 2, + ] + ) +} + +#[test] +fn restake_all_after_maturity_for_restakable_product_one_jar() { + let alice = alice(); + let admin = admin(); + + let product = generate_product("restakable_product") + .with_allows_restaking(true) + .lockup_term(MS_IN_YEAR); + let jar = Jar::generate(0, &alice, &product.id).principal(PRINCIPAL); + let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); + + context.set_block_timestamp_in_days(366); + + context.switch_account(&alice); + let restaked = context.contract().restake_all().into_iter().next().unwrap(); + + assert_eq!(restaked.account_id, alice); + assert_eq!(restaked.principal.0, PRINCIPAL); + assert_eq!(restaked.claimed_balance.0, 0); + + let alice_jars = context.contract().get_jars_for_account(alice); + + assert_eq!(2, alice_jars.len()); + assert_eq!(0, alice_jars.iter().find(|item| item.id.0 == 0).unwrap().principal.0); + assert_eq!( + PRINCIPAL, + alice_jars.iter().find(|item| item.id.0 == 1).unwrap().principal.0 + ); +} diff --git a/contract/src/jar/tests/tests.rs b/contract/src/jar/tests/tests.rs new file mode 100644 index 00000000..87b53aa4 --- /dev/null +++ b/contract/src/jar/tests/tests.rs @@ -0,0 +1,241 @@ +#![cfg(test)] + +use fake::Fake; +use near_sdk::{test_utils::test_env::alice, Timestamp}; +use sweat_jar_model::MS_IN_YEAR; + +use crate::{ + common::udecimal::UDecimal, + product::model::{Apy, Product}, + Jar, +}; + +#[test] +fn get_interest_before_maturity() { + let product = Product::generate("product") + .apy(Apy::Constant(UDecimal::new(12, 2))) + .lockup_term(2 * MS_IN_YEAR); + let jar = Jar::generate(0, &alice(), &product.id).principal(100_000_000); + + let interest = jar.get_interest(&product, MS_IN_YEAR).0; + assert_eq!(12_000_000, interest); +} + +#[test] +fn get_interest_after_maturity() { + let product = Product::generate("product") + .apy(Apy::Constant(UDecimal::new(12, 2))) + .lockup_term(MS_IN_YEAR); + let jar = Jar::generate(0, &alice(), &product.id).principal(100_000_000); + + let interest = jar.get_interest(&product, 400 * 24 * 60 * 60 * 1000).0; + assert_eq!(12_000_000, interest); +} + +#[test] +fn interest_precision() { + let product = Product::generate("product") + .apy(Apy::Constant(UDecimal::new(1, 0))) + .lockup_term(MS_IN_YEAR); + let jar = Jar::generate(0, &alice(), &product.id).principal(MS_IN_YEAR as u128); + + assert_eq!(jar.get_interest(&product, 10000000000).0, 10000000000); + assert_eq!(jar.get_interest(&product, 10000000001).0, 10000000001); + + for _ in 0..100 { + let time: Timestamp = (10..MS_IN_YEAR).fake(); + assert_eq!(jar.get_interest(&product, time).0, time as u128); + } +} + +#[cfg(test)] +mod signature_tests { + + use near_sdk::{ + json_types::{Base64VecU8, U128, U64}, + test_utils::test_env::alice, + }; + + use crate::{ + common::tests::Context, + jar::model::JarTicket, + product::helpers::MessageSigner, + test_utils::{admin, generate_premium_product, generate_product}, + }; + + #[test] + fn verify_ticket_with_valid_signature_and_date() { + let admin = admin(); + + let signer = MessageSigner::new(); + let reference_product = generate_premium_product("premium_product", &signer); + let context = Context::new(admin.clone()).with_products(&[reference_product.clone()]); + + let amount = 14_000_000; + let ticket = JarTicket { + product_id: reference_product.id, + valid_until: U64(123000000), + }; + + let signature = signer.sign(context.get_signature_material(&admin, &ticket, amount).as_str()); + + context + .contract() + .verify(&admin, amount, &ticket, Some(Base64VecU8(signature))); + } + + #[test] + #[should_panic(expected = "Signature must be 64 bytes")] + fn verify_ticket_with_invalid_signature() { + let alice = alice(); + let admin = admin(); + + let signer = MessageSigner::new(); + let reference_product = generate_premium_product("premium_product", &signer); + let context = Context::new(admin).with_products(&[reference_product.clone()]); + + let amount = 1_000_000; + let ticket = JarTicket { + product_id: reference_product.id, + valid_until: U64(100000000), + }; + + let signature: Vec = vec![0, 1, 2]; + + context + .contract() + .verify(&alice, amount, &ticket, Some(Base64VecU8(signature))); + } + + #[test] + #[should_panic(expected = "Not matching signature")] + fn verify_ticket_with_not_matching_signature() { + let admin = admin(); + + let signer = MessageSigner::new(); + let product = generate_premium_product("premium_product", &signer); + let another_product = generate_premium_product("another_premium_product", &MessageSigner::new()); + + let context = Context::new(admin.clone()).with_products(&[product, another_product.clone()]); + + let amount = 15_000_000; + let ticket_for_another_product = JarTicket { + product_id: another_product.id, + valid_until: U64(100000000), + }; + + // signature made for wrong product + let signature = signer.sign( + context + .get_signature_material(&admin, &ticket_for_another_product, amount) + .as_str(), + ); + + context.contract().verify( + &admin, + amount, + &ticket_for_another_product, + Some(Base64VecU8(signature)), + ); + } + + #[test] + #[should_panic(expected = "Ticket is outdated")] + fn verify_ticket_with_invalid_date() { + let alice = alice(); + let admin = admin(); + + let signer = MessageSigner::new(); + let reference_product = generate_premium_product("premium_product", &signer); + let mut context = Context::new(admin).with_products(&[reference_product.clone()]); + + context.set_block_timestamp_in_days(365); + + let amount = 5_000_000; + let ticket = JarTicket { + product_id: reference_product.id, + valid_until: U64(100000000), + }; + + let signature = signer.sign(context.get_signature_material(&alice, &ticket, amount).as_str()); + + context + .contract() + .verify(&alice, amount, &ticket, Some(Base64VecU8(signature))); + } + + #[test] + #[should_panic(expected = "Product 'not_existing_product' doesn't exist")] + fn verify_ticket_with_not_existing_product() { + let admin = admin(); + + let mut context = Context::new(admin.clone()); + + context.switch_account(&admin); + + let signer = MessageSigner::new(); + let not_existing_product = generate_premium_product("not_existing_product", &signer); + + let amount = 500_000; + let ticket = JarTicket { + product_id: not_existing_product.id, + valid_until: U64(100000000), + }; + + let signature = signer.sign(context.get_signature_material(&admin, &ticket, amount).as_str()); + + context + .contract() + .verify(&admin, amount, &ticket, Some(Base64VecU8(signature))); + } + + #[test] + #[should_panic(expected = "Signature is required")] + fn verify_ticket_without_signature_when_required() { + let admin = admin(); + + let signer = MessageSigner::new(); + let product = generate_premium_product("not_existing_product", &signer); + let context = Context::new(admin.clone()).with_products(&[product.clone()]); + + let amount = 3_000_000; + let ticket = JarTicket { + product_id: product.id, + valid_until: U64(100000000), + }; + + context.contract().verify(&admin, amount, &ticket, None); + } + + #[test] + fn verify_ticket_without_signature_when_not_required() { + let admin = admin(); + + let product = generate_product("regular_product"); + let context = Context::new(admin.clone()).with_products(&[product.clone()]); + + let amount = 4_000_000_000; + let ticket = JarTicket { + product_id: product.id, + valid_until: U64(0), + }; + + context.contract().verify(&admin, amount, &ticket, None); + } + + #[test] + #[should_panic(expected = "It's not possible to create new jars for this product")] + fn create_jar_for_disabled_product() { + let alice = alice(); + let admin = admin(); + + let product = generate_product("product").enabled(false); + let context = Context::new(admin).with_products(&[product.clone()]); + + let ticket = JarTicket { + product_id: product.id, + valid_until: U64(0), + }; + context.contract().create_jar(alice, ticket, U128(1_000_000), None); + } +} diff --git a/contract/src/lib.rs b/contract/src/lib.rs index b089442f..0c6f31f2 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -23,6 +23,7 @@ mod jar; mod migration; mod penalty; mod product; +mod test_utils; mod tests; mod withdraw; diff --git a/contract/src/product/tests.rs b/contract/src/product/tests.rs index bcaac286..bc8b1b80 100644 --- a/contract/src/product/tests.rs +++ b/contract/src/product/tests.rs @@ -2,7 +2,7 @@ use near_sdk::{ json_types::{Base64VecU8, U128, U64}, - test_utils::accounts, + test_utils::test_env::alice, }; use sweat_jar_model::{ api::ProductApi, @@ -19,6 +19,7 @@ use crate::{ helpers::MessageSigner, model::{Apy, DowngradableApy, Product, Terms, WithdrawalFee}, }, + test_utils::admin, }; pub(crate) fn get_register_product_command() -> RegisterProductCommand { @@ -30,44 +31,44 @@ pub(crate) fn get_register_product_command() -> RegisterProductCommand { #[test] fn disable_product_when_enabled() { - let admin = accounts(0); + let admin = admin(); let reference_product = &Product::generate("product").enabled(true); let mut context = Context::new(admin.clone()).with_products(&[reference_product.clone()]); - let mut product = context.contract.get_product(&reference_product.id); + let mut product = context.contract().get_product(&reference_product.id); assert!(product.is_enabled); context.switch_account(&admin); context.with_deposit_yocto(1, |context| { - context.contract.set_enabled(reference_product.id.to_string(), false) + context.contract().set_enabled(reference_product.id.to_string(), false) }); - product = context.contract.get_product(&reference_product.id); + product = context.contract().get_product(&reference_product.id); assert!(!product.is_enabled); } #[test] #[should_panic(expected = "Status matches")] fn enable_product_when_enabled() { - let admin = accounts(0); + let admin = admin(); let reference_product = &Product::generate("product").enabled(true); let mut context = Context::new(admin.clone()).with_products(&[reference_product.clone()]); - let product = context.contract.get_product(&reference_product.id); + let product = context.contract().get_product(&reference_product.id); assert!(product.is_enabled); context.switch_account(&admin); context.with_deposit_yocto(1, |context| { - context.contract.set_enabled(reference_product.id.to_string(), true) + context.contract().set_enabled(reference_product.id.to_string(), true) }); } #[test] #[should_panic(expected = "Product already exists")] fn register_product_with_existing_id() { - let admin = accounts(1); + let admin = admin(); let mut context = Context::new(admin.clone()); @@ -75,25 +76,25 @@ fn register_product_with_existing_id() { context.with_deposit_yocto(1, |context| { let first_command = get_register_product_command(); - context.contract.register_product(first_command) + context.contract().register_product(first_command) }); context.with_deposit_yocto(1, |context| { let second_command = get_register_product_command(); - context.contract.register_product(second_command) + context.contract().register_product(second_command) }); } fn register_product(command: RegisterProductCommand) -> (Product, ProductView) { - let admin = accounts(1); + let admin = admin(); let mut context = Context::new(admin.clone()); context.switch_account(&admin); - context.with_deposit_yocto(1, |context| context.contract.register_product(command)); + context.with_deposit_yocto(1, |context| context.contract().register_product(command)); - let product = context.contract.products.into_iter().last().unwrap().1.clone(); - let view = context.contract.get_products().first().unwrap().clone(); + let product = context.contract().products.into_iter().last().unwrap().1.clone(); + let view = context.contract().get_products().first().unwrap().clone(); (product, view) } @@ -201,7 +202,7 @@ fn register_product_with_flexible_terms() { #[test] fn set_public_key() { - let admin = accounts(1); + let admin = admin(); let signer = MessageSigner::new(); let product = generate_product().public_key(signer.public_key()); @@ -213,19 +214,19 @@ fn set_public_key() { context.switch_account(&admin); context.with_deposit_yocto(1, |context| { context - .contract + .contract() .set_public_key(product.id.clone(), Base64VecU8(new_pk.clone())) }); - let product = context.contract.products.get(&product.id).unwrap(); + let product = context.contract().products.get(&product.id).unwrap(); assert_eq!(&new_pk, product.public_key.as_ref().unwrap()); } #[test] #[should_panic(expected = "Can be performed only by admin")] fn set_public_key_by_not_admin() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let signer = MessageSigner::new(); let product = generate_product().public_key(signer.public_key()); @@ -236,14 +237,14 @@ fn set_public_key_by_not_admin() { context.switch_account(&alice); context.with_deposit_yocto(1, |context| { - context.contract.set_public_key(product.id, Base64VecU8(new_pk)) + context.contract().set_public_key(product.id, Base64VecU8(new_pk)) }); } #[test] #[should_panic(expected = "Requires attached deposit of exactly 1 yoctoNEAR")] fn set_public_key_without_deposit() { - let admin = accounts(1); + let admin = admin(); let signer = MessageSigner::new(); let product = generate_product().public_key(signer.public_key()); @@ -254,7 +255,7 @@ fn set_public_key_without_deposit() { context.switch_account(&admin); - context.contract.set_public_key(product.id, Base64VecU8(new_pk)); + context.contract().set_public_key(product.id, Base64VecU8(new_pk)); } #[test] diff --git a/contract/src/test_utils.rs b/contract/src/test_utils.rs new file mode 100644 index 00000000..d80d6bcd --- /dev/null +++ b/contract/src/test_utils.rs @@ -0,0 +1,127 @@ +#![cfg(test)] + +use std::{ + ops::Range, + panic::{catch_unwind, UnwindSafe}, +}; + +use near_sdk::{AccountId, PromiseOrValue}; +use sweat_jar_model::TokenAmount; + +use crate::{ + common::{udecimal::UDecimal, Timestamp}, + jar::model::{Jar, JarV1}, + product::{ + helpers::MessageSigner, + model::{Apy, DowngradableApy, Product}, + }, +}; + +pub const PRINCIPAL: u128 = 1_000_000; +pub const JAR_ID_RANGE: Range = 0..100_000_000; + +pub fn admin() -> AccountId { + "admin".parse().unwrap() +} + +impl Jar { + pub(crate) fn generate(id: u32, account_id: &AccountId, product_id: &str) -> Jar { + JarV1 { + id, + account_id: account_id.clone(), + product_id: product_id.to_string(), + created_at: 0, + principal: 0, + cache: None, + claimed_balance: 0, + is_pending_withdraw: false, + is_penalty_applied: false, + claim_remainder: Default::default(), + } + .into() + } + + pub(crate) fn principal(mut self, principal: TokenAmount) -> Jar { + self.principal = principal; + self + } + + pub(crate) fn created_at(mut self, created_at: Timestamp) -> Jar { + self.created_at = created_at; + self + } + + pub(crate) fn pending_withdraw(mut self) -> Jar { + self.is_pending_withdraw = true; + self + } +} + +pub fn generate_premium_product(id: &str, signer: &MessageSigner) -> Product { + Product::generate(id) + .enabled(true) + .public_key(signer.public_key()) + .cap(0, 100_000_000_000) + .apy(Apy::Downgradable(DowngradableApy { + default: UDecimal::new(20, 2), + fallback: UDecimal::new(10, 2), + })) +} + +pub fn generate_product(id: &str) -> Product { + Product::generate(id) + .enabled(true) + .cap(0, 100_000_000_000) + .apy(Apy::Constant(UDecimal::new(20, 2))) +} + +pub trait AfterCatchUnwind { + fn after_catch_unwind(&self); +} + +pub fn expect_panic(ctx: &impl AfterCatchUnwind, msg: &str, action: impl FnOnce() + UnwindSafe) { + let res = catch_unwind(move || action()); + + let panic_msg = res.err().expect(&format!( + "Contract didn't panic when expected to.\nExpected message: {msg}" + )); + + let panic_msg = panic_msg + .downcast_ref::() + .expect(&format!("Contract didn't panic with String.\nExpected message: {msg}")); + + assert!( + panic_msg.contains(msg), + "Expected panic message to contain: {msg}.\nPanic message: {panic_msg}" + ); + + ctx.after_catch_unwind(); +} + +pub trait UnwrapPromise { + fn unwrap(self) -> T; +} + +impl UnwrapPromise for PromiseOrValue { + fn unwrap(self) -> T { + let PromiseOrValue::Value(t) = self else { + panic!("Failed to unwrap PromiseOrValue") + }; + t + } +} + +#[test] +#[should_panic(expected = "Contract didn't panic when expected to.\nExpected message: Something went wrong")] +fn test_expect_panic() { + struct Ctx; + impl AfterCatchUnwind for Ctx { + fn after_catch_unwind(&self) {} + } + + expect_panic(&Ctx, "Something went wrong", || { + panic!("{}", "Something went wrong"); + }); + + expect_panic(&Ctx, "Something went wrong", || {}); +} diff --git a/contract/src/tests.rs b/contract/src/tests.rs index e4a0ace8..fbb87fe3 100644 --- a/contract/src/tests.rs +++ b/contract/src/tests.rs @@ -7,8 +7,7 @@ use fake::Fake; use near_sdk::{ json_types::U128, serde_json::{from_str, to_string}, - test_utils::accounts, - PromiseOrValue, + test_utils::test_env::{alice, bob}, }; use sweat_jar_model::{ api::{ClaimApi, JarApi, PenaltyApi, ProductApi, WithdrawApi}, @@ -21,19 +20,20 @@ use super::*; use crate::{ common::{test_data::set_test_log_events, udecimal::UDecimal}, product::{helpers::MessageSigner, model::DowngradableApy, tests::get_register_product_command}, + test_utils::{admin, UnwrapPromise}, }; #[test] fn add_product_to_list_by_admin() { - let admin = accounts(0); + let admin = admin(); let mut context = Context::new(admin.clone()); context.switch_account(&admin); context.with_deposit_yocto(1, |context| { - context.contract.register_product(get_register_product_command()) + context.contract().register_product(get_register_product_command()) }); - let products = context.contract.get_products(); + let products = context.contract().get_products(); assert_eq!(products.len(), 1); assert_eq!(products.first().unwrap().id, "product".to_string()); } @@ -41,28 +41,28 @@ fn add_product_to_list_by_admin() { #[test] #[should_panic(expected = "Can be performed only by admin")] fn add_product_to_list_by_not_admin() { - let admin = accounts(0); + let admin = admin(); let mut context = Context::new(admin); context.with_deposit_yocto(1, |context| { - context.contract.register_product(get_register_product_command()) + context.contract().register_product(get_register_product_command()) }); } #[test] fn get_principle_with_no_jars() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let context = Context::new(admin); - let principal = context.contract.get_total_principal(alice); + let principal = context.contract().get_total_principal(alice); assert_eq!(principal.total.0, 0); } #[test] fn get_principal_with_single_jar() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let reference_product = generate_product(); let reference_jar = Jar::generate(0, &alice, &reference_product.id).principal(100); @@ -70,14 +70,14 @@ fn get_principal_with_single_jar() { .with_products(&[reference_product]) .with_jars(&[reference_jar]); - let principal = context.contract.get_total_principal(alice).total.0; + let principal = context.contract().get_total_principal(alice).total.0; assert_eq!(principal, 100); } #[test] fn get_principal_with_multiple_jars() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let reference_product = generate_product(); let jars = &[ @@ -88,18 +88,18 @@ fn get_principal_with_multiple_jars() { let context = Context::new(admin).with_products(&[reference_product]).with_jars(jars); - let principal = context.contract.get_total_principal(alice).total.0; + let principal = context.contract().get_total_principal(alice).total.0; assert_eq!(principal, 700); } #[test] fn get_total_interest_with_no_jars() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let context = Context::new(admin); - let interest = context.contract.get_total_interest(alice); + let interest = context.contract().get_total_interest(alice); assert_eq!(interest.amount.total.0, 0); assert_eq!(interest.amount.detailed, HashMap::new()); @@ -107,8 +107,8 @@ fn get_total_interest_with_no_jars() { #[test] fn get_total_interest_with_single_jar_after_30_minutes() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let reference_product = generate_product(); @@ -118,12 +118,12 @@ fn get_total_interest_with_single_jar_after_30_minutes() { .with_products(&[reference_product]) .with_jars(&[jar.clone()]); - let contract_jar = JarView::from(context.contract.account_jars.get(&alice).unwrap().get_jar(jar_id)); + let contract_jar = JarView::from(context.contract().account_jars.get(&alice).unwrap().get_jar(jar_id)); assert_eq!(JarView::from(jar), contract_jar); context.set_block_timestamp_in_minutes(30); - let interest = context.contract.get_total_interest(alice); + let interest = context.contract().get_total_interest(alice); assert_eq!(interest.amount.total.0, 684); assert_eq!(interest.amount.detailed, HashMap::from([(U32(0), U128(684))])) @@ -131,8 +131,8 @@ fn get_total_interest_with_single_jar_after_30_minutes() { #[test] fn get_total_interest_with_single_jar_on_maturity() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let reference_product = generate_product(); @@ -142,12 +142,12 @@ fn get_total_interest_with_single_jar_on_maturity() { .with_products(&[reference_product]) .with_jars(&[jar.clone()]); - let contract_jar = JarView::from(context.contract.account_jars.get(&alice).unwrap().get_jar(jar_id)); + let contract_jar = JarView::from(context.contract().account_jars.get(&alice).unwrap().get_jar(jar_id)); assert_eq!(JarView::from(jar), contract_jar); context.set_block_timestamp_in_days(365); - let interest = context.contract.get_total_interest(alice); + let interest = context.contract().get_total_interest(alice); assert_eq!( interest.amount, @@ -160,8 +160,8 @@ fn get_total_interest_with_single_jar_on_maturity() { #[test] fn get_total_interest_with_single_jar_after_maturity() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let reference_product = generate_product(); @@ -171,19 +171,19 @@ fn get_total_interest_with_single_jar_after_maturity() { .with_products(&[reference_product]) .with_jars(&[jar.clone()]); - let contract_jar = JarView::from(context.contract.account_jars.get(&alice).unwrap().get_jar(jar_id)); + let contract_jar = JarView::from(context.contract().account_jars.get(&alice).unwrap().get_jar(jar_id)); assert_eq!(JarView::from(jar), contract_jar); context.set_block_timestamp_in_days(400); - let interest = context.contract.get_total_interest(alice).amount.total.0; + let interest = context.contract().get_total_interest(alice).amount.total.0; assert_eq!(interest, 12_000_000); } #[test] fn get_total_interest_with_single_jar_after_claim_on_half_term_and_maturity() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let reference_product = generate_product(); @@ -193,28 +193,28 @@ fn get_total_interest_with_single_jar_after_claim_on_half_term_and_maturity() { .with_products(&[reference_product]) .with_jars(&[jar.clone()]); - let contract_jar = JarView::from(context.contract.account_jars.get(&alice).unwrap().get_jar(jar_id)); + let contract_jar = JarView::from(context.contract().account_jars.get(&alice).unwrap().get_jar(jar_id)); assert_eq!(JarView::from(jar), contract_jar); context.set_block_timestamp_in_days(182); - let mut interest = context.contract.get_total_interest(alice.clone()).amount.total.0; + let mut interest = context.contract().get_total_interest(alice.clone()).amount.total.0; assert_eq!(interest, 5_983_561); context.switch_account(&alice); - context.contract.claim_total(None); + context.contract().claim_total(None); context.set_block_timestamp_in_days(365); - interest = context.contract.get_total_interest(alice.clone()).amount.total.0; + interest = context.contract().get_total_interest(alice.clone()).amount.total.0; assert_eq!(interest, 6_016_439); } #[test] #[should_panic(expected = "Penalty is not applicable for constant APY")] fn penalty_is_not_applicable_for_constant_apy() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let signer = MessageSigner::new(); let reference_product = Product::generate("premium_product") @@ -228,13 +228,13 @@ fn penalty_is_not_applicable_for_constant_apy() { .with_jars(&[reference_jar]); context.switch_account(&admin); - context.contract.set_penalty(alice, U32(0), true); + context.contract().set_penalty(alice, U32(0), true); } #[test] fn get_total_interest_for_premium_with_penalty_after_half_term() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let signer = MessageSigner::new(); let reference_product = Product::generate("premium_product") @@ -252,22 +252,22 @@ fn get_total_interest_for_premium_with_penalty_after_half_term() { context.set_block_timestamp_in_ms(15_768_000_000); - let mut interest = context.contract.get_total_interest(alice.clone()).amount.total.0; + let mut interest = context.contract().get_total_interest(alice.clone()).amount.total.0; assert_eq!(interest, 10_000_000); context.switch_account(&admin); - context.contract.set_penalty(alice.clone(), U32(0), true); + context.contract().set_penalty(alice.clone(), U32(0), true); context.set_block_timestamp_in_ms(31_536_000_000); - interest = context.contract.get_total_interest(alice).amount.total.0; + interest = context.contract().get_total_interest(alice).amount.total.0; assert_eq!(interest, 15_000_000); } #[test] fn get_total_interest_for_premium_with_multiple_penalties_applied() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let signer = MessageSigner::new(); let reference_product = Product::generate("lux_product") @@ -284,31 +284,31 @@ fn get_total_interest_for_premium_with_multiple_penalties_applied() { .with_products(&[reference_product]) .with_jars(&[reference_jar]); - let products = context.contract.get_products(); + let products = context.contract().get_products(); assert!(matches!(products.first().unwrap().apy, ApyView::Downgradable(_))); context.switch_account(&admin); context.set_block_timestamp_in_ms(270_000); - context.contract.set_penalty(alice.clone(), U32(0), true); + context.contract().set_penalty(alice.clone(), U32(0), true); context.set_block_timestamp_in_ms(390_000); - context.contract.set_penalty(alice.clone(), U32(0), false); + context.contract().set_penalty(alice.clone(), U32(0), false); context.set_block_timestamp_in_ms(1_264_000); - context.contract.set_penalty(alice.clone(), U32(0), true); + context.contract().set_penalty(alice.clone(), U32(0), true); context.set_block_timestamp_in_ms(3_700_000); - let interest = context.contract.get_total_interest(alice.clone()).amount.total.0; + let interest = context.contract().get_total_interest(alice.clone()).amount.total.0; assert_eq!(interest, 1_613_140_537_798_072_042); } #[test] fn apply_penalty_in_batch() { - let admin = accounts(0); - let alice = accounts(1); - let bob = accounts(2); + let admin = admin(); + let alice = alice(); + let bob = bob(); let product_id = "premium_product"; @@ -330,51 +330,49 @@ fn apply_penalty_in_batch() { context.set_block_timestamp_in_days(182); - let interest = context.contract.get_total_interest(alice.clone()).amount.total.0; + let interest = context.contract().get_total_interest(alice.clone()).amount.total.0; assert_eq!(interest, 997_260_200); - let interest = context.contract.get_total_interest(bob.clone()).amount.total.0; + let interest = context.contract().get_total_interest(bob.clone()).amount.total.0; assert_eq!(interest, 498_630_100); context.switch_account(&admin); let alice_jars = context - .contract + .contract() .get_jars_for_account(alice.clone()) .into_iter() .map(|j| j.id) .collect(); let bob_jars = context - .contract + .contract() .get_jars_for_account(bob.clone()) .into_iter() .map(|j| j.id) .collect(); context - .contract + .contract() .batch_set_penalty(vec![(alice.clone(), alice_jars), (bob.clone(), bob_jars)], true); context.set_block_timestamp_in_days(365); - let interest = context.contract.get_total_interest(alice.clone()).amount.total.0; + let interest = context.contract().get_total_interest(alice.clone()).amount.total.0; assert_eq!(interest, 1_498_630_000); - let interest = context.contract.get_total_interest(bob.clone()).amount.total.0; + let interest = context.contract().get_total_interest(bob.clone()).amount.total.0; assert_eq!(interest, 749_315_000); - assert!(context - .contract - .get_jars_for_account(alice) - .into_iter() - .chain(context.contract.get_jars_for_account(bob).into_iter()) - .all(|jar| jar.is_penalty_applied == true)) + let alice_jars = context.contract().get_jars_for_account(alice); + let bob_jars = context.contract().get_jars_for_account(bob); + + assert!(alice_jars.into_iter().chain(bob_jars).all(|jar| jar.is_penalty_applied)); } #[test] fn get_interest_after_withdraw() { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let product = generate_product(); let jar = Jar::generate(0, &alice, &product.id).principal(100_000_000); @@ -384,9 +382,9 @@ fn get_interest_after_withdraw() { context.set_block_timestamp_in_days(400); context.switch_account(&alice); - context.contract.withdraw(U32(jar.id), None); + context.contract().withdraw(U32(jar.id), None); - let interest = context.contract.get_total_interest(alice.clone()); + let interest = context.contract().get_total_interest(alice.clone()); assert_eq!(12_000_000, interest.amount.total.0); } @@ -429,15 +427,11 @@ fn claim_often_vs_claim_once() { for day in 0..days { context.set_block_timestamp_in_days(day); - - let PromiseOrValue::Value(claimed) = context.contract.claim_total(None) else { - panic!() - }; - + let claimed = context.contract().claim_total(None).unwrap(); bobs_claimed += claimed.get_total().0; } - let alice_interest = context.contract.get_total_interest(alice.clone()).amount.total.0; + let alice_interest = context.contract().get_total_interest(alice.clone()).amount.total.0; assert_eq!(alice_interest, bobs_claimed); } @@ -446,7 +440,7 @@ fn claim_often_vs_claim_once() { test(product.clone(), 10_000_000_000_000_000_000_000_000_000, 365, 0); - for n in 1..100 { + for n in 1..10 { test( product.clone(), (1..10_000_000_000_000_000_000_000_000_000).fake(), diff --git a/contract/src/withdraw/api.rs b/contract/src/withdraw/api.rs index b7c594eb..1cca4da1 100644 --- a/contract/src/withdraw/api.rs +++ b/contract/src/withdraw/api.rs @@ -1,11 +1,27 @@ -use near_sdk::{ext_contract, is_promise_success, json_types::U128, near_bindgen, PromiseOrValue}; +use near_sdk::{ + env::panic_str, + ext_contract, is_promise_success, + json_types::U128, + near_bindgen, + serde::{Deserialize, Serialize}, + PromiseOrValue, +}; use sweat_jar_model::{ api::WithdrawApi, jar::{JarId, JarIdView}, - withdraw::{Fee, WithdrawView}, + withdraw::{BulkWithdrawView, Fee, WithdrawView}, TokenAmount, }; +#[derive(Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub struct JarWithdraw { + pub jar: Jar, + pub should_be_closed: bool, + pub amount: u128, + pub fee: Option, +} + #[cfg(not(test))] use crate::ft_interface::FungibleTokenInterface; use crate::{ @@ -17,6 +33,12 @@ use crate::{ AccountId, Contract, ContractExt, Product, }; +impl Contract { + fn can_be_withdrawn(jar: &Jar, product: &Product, now: u64) -> bool { + !jar.is_pending_withdraw && jar.is_liquidable(product, now) + } +} + #[ext_contract(ext_self)] pub trait WithdrawCallbacks { fn after_withdraw( @@ -27,6 +49,8 @@ pub trait WithdrawCallbacks { withdrawn_amount: TokenAmount, fee: Option, ) -> WithdrawView; + + fn after_bulk_withdraw(&mut self, account_id: AccountId, jars: Vec) -> BulkWithdrawView; } #[near_bindgen] @@ -56,6 +80,48 @@ impl WithdrawApi for Contract { self.transfer_withdraw(&account_id, amount, &jar, close_jar) } + + fn withdraw_all(&mut self) -> PromiseOrValue { + let account_id = env::predecessor_account_id(); + self.migrate_account_jars_if_needed(account_id.clone()); + let now = env::block_timestamp_ms(); + + let jars: Vec = self + .account_jars + .get(&account_id) + .unwrap_or_else(|| { + panic_str(&format!("Jars for account '{account_id}' don't exist")); + }) + .jars + .clone() + .into_iter() + .filter_map(|jar| { + let product = self.get_product(&jar.product_id); + + if !Self::can_be_withdrawn(&jar, &product, now) { + return None; + } + + let amount = jar.principal; + + let mut withdrawn_jar = jar.withdrawn(&product, amount, now); + let should_be_closed = withdrawn_jar.should_be_closed(&product, now); + + withdrawn_jar.lock(); + *self.get_jar_mut_internal(&jar.account_id, jar.id) = withdrawn_jar; + + JarWithdraw { + jar, + should_be_closed, + amount, + fee: None, + } + .into() + }) + .collect(); + + self.transfer_bulk_withdraw(&account_id, jars) + } } impl Contract { @@ -86,14 +152,60 @@ impl Contract { emit(EventKind::Withdraw(WithdrawData { id: jar_id, - withdrawn_amount: withdrawal_result.withdrawn_amount, - fee_amount: withdrawal_result.fee, + amount: withdrawal_result.withdrawn_amount, + fee: withdrawal_result.fee, })); withdrawal_result } - fn get_fee(&self, product: &Product, jar: &Jar) -> Option { + pub(crate) fn after_bulk_withdraw_internal( + &mut self, + account_id: AccountId, + jars: Vec, + is_promise_success: bool, + ) -> BulkWithdrawView { + let mut withdrawal_result = BulkWithdrawView { + total_amount: 0.into(), + jars: vec![], + }; + + if !is_promise_success { + for withdraw in jars { + let jar = self.get_jar_mut_internal(&account_id, withdraw.jar.id); + jar.principal += withdraw.amount; + jar.unlock(); + } + return withdrawal_result; + } + + let mut event_data = vec![]; + + for withdraw in jars { + if withdraw.should_be_closed { + self.delete_jar(&account_id, withdraw.jar.id); + } else { + self.get_jar_mut_internal(&account_id, withdraw.jar.id).unlock(); + } + + let jar_result = WithdrawView::new(withdraw.amount, self.make_fee(withdraw.fee)); + + event_data.push(WithdrawData { + id: withdraw.jar.id, + amount: jar_result.withdrawn_amount, + fee: jar_result.fee, + }); + + withdrawal_result.total_amount.0 += jar_result.withdrawn_amount.0; + withdrawal_result.jars.push(jar_result); + } + + emit(EventKind::WithdrawAll(event_data)); + + withdrawal_result + } + + fn get_fee(product: &Product, jar: &Jar) -> Option { let fee = product.withdrawal_fee.as_ref()?; let amount = match fee { @@ -101,10 +213,15 @@ impl Contract { WithdrawalFee::Percent(percent) => percent * jar.principal, }; - Some(Fee { + amount.into() + } + + fn make_fee(&self, amount: Option) -> Option { + Fee { beneficiary_id: self.fee_account_id.clone(), - amount, - }) + amount: amount?, + } + .into() } } @@ -119,20 +236,55 @@ impl Contract { close_jar: bool, ) -> PromiseOrValue { let product = self.get_product(&jar.product_id); - let fee = self.get_fee(&product, jar); + let fee = Self::get_fee(&product, jar); self.ft_contract() - .transfer(account_id, amount, "withdraw", &fee) + .ft_transfer(account_id, amount, "withdraw", &self.make_fee(fee)) .then(Self::after_withdraw_call( account_id.clone(), jar.id, close_jar, amount, - &fee, + &self.make_fee(fee), )) .into() } + fn transfer_bulk_withdraw( + &mut self, + account_id: &AccountId, + jars: Vec, + ) -> PromiseOrValue { + let total_fee: TokenAmount = jars + .iter() + .filter_map(|j| { + let product = self.get_product(&j.jar.product_id); + Self::get_fee(&product, &j.jar) + }) + .sum(); + + let total_fee = match total_fee { + 0 => None, + _ => self.make_fee(total_fee.into()), + }; + + let total_amount = jars.iter().map(|j| j.amount).sum(); + + let gas_left = crate::env::prepaid_gas().as_gas() - crate::env::used_gas().as_gas(); + + if gas_left + < crate::common::gas_data::GAS_FOR_FT_TRANSFER.as_gas() + + crate::common::gas_data::GAS_FOR_BULK_AFTER_WITHDRAW.as_gas() + { + panic_str("Not enough gas left to complete transfer_bulk_withdraw."); + } + + self.ft_contract() + .ft_transfer(account_id, total_amount, "bulk_withdraw", &total_fee) + .then(Self::after_bulk_withdraw_call(account_id.clone(), jars)) + .into() + } + fn after_withdraw_call( account_id: AccountId, jar_id: JarId, @@ -144,6 +296,12 @@ impl Contract { .with_static_gas(crate::common::gas_data::GAS_FOR_AFTER_WITHDRAW) .after_withdraw(account_id, jar_id, close_jar, withdrawn_balance, fee.clone()) } + + fn after_bulk_withdraw_call(account_id: AccountId, jars: Vec) -> near_sdk::Promise { + ext_self::ext(env::current_account_id()) + .with_static_gas(crate::common::gas_data::GAS_FOR_BULK_AFTER_WITHDRAW) + .after_bulk_withdraw(account_id, jars) + } } #[cfg(test)] @@ -156,14 +314,28 @@ impl Contract { close_jar: bool, ) -> PromiseOrValue { let product = self.get_product(&jar.product_id); - let fee = self.get_fee(&product, jar); + let fee = Self::get_fee(&product, jar); let withdrawn = self.after_withdraw_internal( account_id.clone(), jar.id, close_jar, amount, - fee, + self.make_fee(fee), + crate::common::test_data::get_test_future_success(), + ); + + PromiseOrValue::Value(withdrawn) + } + + fn transfer_bulk_withdraw( + &mut self, + account_id: &AccountId, + jars: Vec, + ) -> PromiseOrValue { + let withdrawn = self.after_bulk_withdraw_internal( + account_id.clone(), + jars, crate::common::test_data::get_test_future_success(), ); @@ -191,4 +363,9 @@ impl WithdrawCallbacks for Contract { is_promise_success(), ) } + + #[private] + fn after_bulk_withdraw(&mut self, account_id: AccountId, jars: Vec) -> BulkWithdrawView { + self.after_bulk_withdraw_internal(account_id, jars, is_promise_success()) + } } diff --git a/contract/src/withdraw/tests.rs b/contract/src/withdraw/tests.rs index d0fea6a2..74a1051f 100644 --- a/contract/src/withdraw/tests.rs +++ b/contract/src/withdraw/tests.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use near_sdk::{json_types::U128, test_utils::accounts, AccountId, PromiseOrValue}; +use near_sdk::{json_types::U128, test_utils::test_env::alice, AccountId}; use sweat_jar_model::{ api::{ClaimApi, JarApi, WithdrawApi}, MS_IN_YEAR, U32, @@ -10,11 +10,13 @@ use crate::{ common::{test_data::set_test_future_success, tests::Context, udecimal::UDecimal, Timestamp}, jar::model::Jar, product::model::{Apy, Product, WithdrawalFee}, + test_utils::{admin, expect_panic, UnwrapPromise, PRINCIPAL}, + withdraw::api::JarWithdraw, }; fn prepare_jar(product: &Product) -> (AccountId, Jar, Context) { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000); let context = Context::new(admin) @@ -25,8 +27,8 @@ fn prepare_jar(product: &Product) -> (AccountId, Jar, Context) { } fn prepare_jar_created_at(product: &Product, created_at: Timestamp) -> (AccountId, Jar, Context) { - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let jar = Jar::generate(0, &alice, &product.id) .created_at(created_at) @@ -39,32 +41,47 @@ fn prepare_jar_created_at(product: &Product, created_at: Timestamp) -> (AccountI } #[test] -#[should_panic(expected = "Account 'owner' doesn't exist")] fn withdraw_locked_jar_before_maturity_by_not_owner() { - let (_, _, mut context) = prepare_jar(&generate_product()); + let (_, _, context) = prepare_jar(&generate_product()); + + expect_panic(&context, "Account 'owner' doesn't exist", || { + context.contract().withdraw(U32(0), None); + }); - context.contract.withdraw(U32(0), None); + expect_panic(&context, "Jars for account 'owner' don't exist", || { + context.contract().withdraw_all(); + }); } #[test] -#[should_panic(expected = "The jar is not mature yet")] fn withdraw_locked_jar_before_maturity_by_owner() { let (alice, jar, mut context) = prepare_jar_created_at(&generate_product().lockup_term(200), 100); context.set_block_timestamp_in_ms(120); context.switch_account(&alice); - context.contract.withdraw(U32(jar.id), None); + + expect_panic(&context, "The jar is not mature yet", || { + context.contract().withdraw(U32(jar.id), None); + }); + + assert!(context.contract().withdraw_all().unwrap().jars.is_empty()); } #[test] -#[should_panic(expected = "Account 'owner' doesn't exist")] fn withdraw_locked_jar_after_maturity_by_not_owner() { let product = generate_product(); let (_, jar, mut context) = prepare_jar(&product); context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - context.contract.withdraw(U32(jar.id), None); + + expect_panic(&context, "Account 'owner' doesn't exist", || { + context.contract().withdraw(U32(jar.id), None); + }); + + expect_panic(&context, "Jars for account 'owner' don't exist", || { + context.contract().withdraw_all(); + }); } #[test] @@ -74,7 +91,7 @@ fn withdraw_locked_jar_after_maturity_by_owner() { context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); context.switch_account(&alice); - context.contract.withdraw(U32(jar.id), None); + context.contract().withdraw(U32(jar.id), None); } #[test] @@ -84,7 +101,7 @@ fn withdraw_flexible_jar_by_not_owner() { let (_, jar, mut context) = prepare_jar(&product); context.set_block_timestamp_in_days(1); - context.contract.withdraw(U32(jar.id), None); + context.contract().withdraw(U32(jar.id), None); } #[test] @@ -95,19 +112,17 @@ fn withdraw_flexible_jar_by_owner_full() { context.set_block_timestamp_in_days(1); context.switch_account(&alice); - context.contract.withdraw(U32(reference_jar.id), None); + context.contract().withdraw(U32(reference_jar.id), None); let interest = context - .contract + .contract() .get_interest(vec![reference_jar.id.into()], alice.clone()); - let PromiseOrValue::Value(claimed) = context.contract.claim_total(None) else { - panic!(); - }; + let claimed = context.contract().claim_total(None).unwrap(); assert_eq!(interest.amount.total, claimed.get_total()); - let jar = context.contract.get_jar(alice.clone(), U32(reference_jar.id)); + let jar = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); assert_eq!(0, jar.principal.0); } @@ -119,20 +134,27 @@ fn withdraw_flexible_jar_by_owner_with_sufficient_balance() { context.set_block_timestamp_in_days(1); context.switch_account(&alice); - context.contract.withdraw(U32(0), Some(U128(100_000))); - let jar = context.contract.get_jar(alice.clone(), U32(reference_jar.id)); + context.contract().withdraw(U32(0), Some(U128(100_000))); + let jar = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); assert_eq!(900_000, jar.principal.0); } #[test] -#[should_panic(expected = "Insufficient balance")] fn withdraw_flexible_jar_by_owner_with_insufficient_balance() { let product = generate_flexible_product(); let (alice, jar, mut context) = prepare_jar(&product); context.set_block_timestamp_in_days(1); context.switch_account(&alice); - context.contract.withdraw(U32(jar.id), Some(U128(2_000_000))); + + expect_panic(&context, "Insufficient balance", || { + context.contract().withdraw(U32(jar.id), Some(U128(2_000_000))); + }); + + let withdrawn = context.contract().withdraw_all().unwrap(); + + assert_eq!(withdrawn.jars.len(), 1); + assert_eq!(withdrawn.jars[0].withdrawn_amount.0, 1_000_000); } #[test] @@ -146,23 +168,17 @@ fn dont_delete_jar_after_withdraw_with_interest_left() { context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); context.switch_account(&alice); - let jar = context.contract.get_jar_internal(&alice, 0); + let jar = context.contract().get_jar_internal(&alice, 0); - let PromiseOrValue::Value(withdrawn) = context.contract.withdraw(U32(jar.id), Some(U128(1_000_000))) else { - panic!(); - }; + let withdrawn = context.contract().withdraw(U32(jar.id), Some(U128(1_000_000))).unwrap(); assert_eq!(withdrawn.withdrawn_amount, U128(1_000_000)); assert_eq!(withdrawn.fee, U128(0)); - let jar = context.contract.get_jar_internal(&alice, 0); + let jar = context.contract().get_jar_internal(&alice, 0); assert_eq!(jar.principal, 0); - let Some(ref cache) = jar.cache else { - panic!(); - }; - - assert_eq!(cache.interest, 200_000); + assert_eq!(jar.cache.as_ref().unwrap().interest, 200_000); } #[test] @@ -177,14 +193,15 @@ fn product_with_fixed_fee() { context.switch_account(&alice); let withdraw_amount = 100_000; - let PromiseOrValue::Value(withdraw) = context.contract.withdraw(U32(0), Some(U128(withdraw_amount))) else { - panic!("Invalid promise type"); - }; + let withdraw = context + .contract() + .withdraw(U32(0), Some(U128(withdraw_amount))) + .unwrap(); assert_eq!(withdraw.withdrawn_amount, U128(withdraw_amount - fee)); assert_eq!(withdraw.fee, U128(fee)); - let jar = context.contract.get_jar(alice, U32(reference_jar.id)); + let jar = context.contract().get_jar(alice, U32(reference_jar.id)); assert_eq!(jar.principal, U128(initial_principal - withdraw_amount)); } @@ -202,15 +219,16 @@ fn product_with_percent_fee() { context.switch_account(&alice); let withdrawn_amount = 100_000; - let PromiseOrValue::Value(withdraw) = context.contract.withdraw(U32(0), Some(U128(withdrawn_amount))) else { - panic!("Invalid promise type"); - }; + let withdraw = context + .contract() + .withdraw(U32(0), Some(U128(withdrawn_amount))) + .unwrap(); let reference_fee = fee_value * initial_principal; assert_eq!(withdraw.withdrawn_amount, U128(withdrawn_amount - reference_fee)); assert_eq!(withdraw.fee, U128(reference_fee)); - let jar = context.contract.get_jar(alice, U32(reference_jar.id)); + let jar = context.contract().get_jar(alice, U32(reference_jar.id)); assert_eq!(jar.principal, U128(initial_principal - withdrawn_amount)); } @@ -225,15 +243,13 @@ fn test_failed_withdraw_promise() { context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); context.switch_account(&alice); - let jar_before_withdrawal = context.contract.get_jar(alice.clone(), U32(reference_jar.id)); + let jar_before_withdrawal = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); - let PromiseOrValue::Value(withdrawn) = context.contract.withdraw(U32(0), Some(U128(100_000))) else { - panic!() - }; + let withdrawn = context.contract().withdraw(U32(0), Some(U128(100_000))).unwrap(); assert_eq!(withdrawn.withdrawn_amount.0, 0); - let jar_after_withdrawal = context.contract.get_jar(alice.clone(), U32(reference_jar.id)); + let jar_after_withdrawal = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); assert_eq!(jar_before_withdrawal, jar_after_withdrawal); } @@ -241,52 +257,151 @@ fn test_failed_withdraw_promise() { #[test] fn test_failed_withdraw_internal() { let product = generate_product(); - let (alice, reference_jar, mut context) = prepare_jar(&product); + let (alice, reference_jar, context) = prepare_jar(&product); let withdrawn_amount = 1_234; - let jar_view = context.contract.get_jar(alice.clone(), U32(reference_jar.id)); - let jar = context - .contract + let mut contract = context.contract(); + + let jar_view = contract.get_jar(alice.clone(), U32(reference_jar.id)); + let jar = contract .account_jars .get(&alice) .unwrap() .iter() .next() - .unwrap(); + .unwrap() + .clone(); let withdraw = - context - .contract - .after_withdraw_internal(jar.account_id.clone(), jar.id, true, withdrawn_amount, None, false); + contract.after_withdraw_internal(jar.account_id.clone(), jar.id, true, withdrawn_amount, None, false); assert_eq!(withdraw.withdrawn_amount, U128(0)); assert_eq!(withdraw.fee, U128(0)); assert_eq!( jar_view.principal.0 + withdrawn_amount, - context.contract.get_jar(alice, U32(0)).principal.0 + contract.get_jar(alice, U32(0)).principal.0 + ); +} + +#[test] +fn test_failed_bulk_withdraw_internal() { + let product = generate_product(); + let (alice, reference_jar, context) = prepare_jar(&product); + + let mut contract = context.contract(); + + let jar_view = contract.get_jar(alice.clone(), U32(reference_jar.id)); + let jar = contract + .account_jars + .get(&alice) + .unwrap() + .iter() + .next() + .unwrap() + .clone(); + + let withdraw = contract.after_bulk_withdraw_internal( + jar.account_id.clone(), + vec![JarWithdraw { + jar: jar.clone(), + should_be_closed: true, + amount: jar.principal, + fee: None, + }], + false, + ); + + assert!(withdraw.jars.is_empty()); + assert_eq!(withdraw.total_amount.0, 0); + + assert_eq!( + jar_view.principal.0 + jar_view.principal.0, + contract.get_jar(alice, U32(0)).principal.0 ); } #[test] -#[should_panic(expected = "Another operation on this Jar is in progress")] fn withdraw_from_locked_jar() { let product = Product::generate("product") .apy(Apy::Constant(UDecimal::new(1, 0))) .lockup_term(MS_IN_YEAR); - let mut jar = Jar::generate(0, &accounts(0), &product.id).principal(MS_IN_YEAR as u128); + let mut jar = Jar::generate(0, &alice(), &product.id).principal(MS_IN_YEAR as u128); jar.lock(); - let alice = accounts(0); - let admin = accounts(1); + let alice = alice(); + let admin = admin(); let mut context = Context::new(admin).with_products(&[product.clone()]).with_jars(&[jar]); context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); context.switch_account(&alice); - _ = context.contract.withdraw(U32(0), Some(U128(100_000))); + expect_panic(&context, "Another operation on this Jar is in progress", || { + _ = context.contract().withdraw(U32(0), Some(U128(100_000))); + }); + + assert!(context.contract().withdraw_all().unwrap().jars.is_empty()); +} + +#[test] +fn withdraw_all() { + let alice = alice(); + let admin = admin(); + + let product = Product::generate("product").enabled(true).lockup_term(MS_IN_YEAR); + let long_term_product = Product::generate("long_term_product") + .enabled(true) + .lockup_term(MS_IN_YEAR * 2); + + let mature_unclaimed_jar = Jar::generate(0, &alice, &product.id).principal(PRINCIPAL + 1); + let mature_claimed_jar = Jar::generate(1, &alice, &product.id).principal(PRINCIPAL + 2); + + let immature_jar = Jar::generate(2, &alice, &long_term_product.id).principal(PRINCIPAL + 3); + let locked_jar = Jar::generate(3, &alice, &product.id).principal(PRINCIPAL + 4).locked(); + + let mut context = Context::new(admin) + .with_products(&[product, long_term_product]) + .with_jars(&[ + mature_unclaimed_jar.clone(), + mature_claimed_jar.clone(), + immature_jar.clone(), + locked_jar.clone(), + ]); + + context.set_block_timestamp_in_days(366); + + context.switch_account(&alice); + + context + .contract() + .claim_jars(vec![mature_claimed_jar.id.into()], None, None); + + let withdrawn_jars = context.contract().withdraw_all().unwrap(); + + assert_eq!(withdrawn_jars.total_amount.0, 2000003); + + assert_eq!( + withdrawn_jars + .jars + .iter() + .map(|j| j.withdrawn_amount.0) + .collect::>(), + vec![mature_unclaimed_jar.principal, mature_claimed_jar.principal] + ); + + let all_jars = context.contract().get_jars_for_account(alice); + + assert_eq!( + all_jars.iter().map(|j| j.principal.0).collect::>(), + vec![0, locked_jar.principal, immature_jar.principal] + ); + + assert_eq!( + all_jars.iter().map(|j| j.id.0).collect::>(), + vec![mature_unclaimed_jar.id, locked_jar.id, immature_jar.id,] + ); } pub(crate) fn generate_product() -> Product { diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 51badddc..fc4e6be7 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -15,7 +15,6 @@ sha2 = { workspace = true } fake = { workspace = true } rand = { workspace = true } futures = { workspace = true } -itertools = { workspace = true } ed25519-dalek = { workspace = true } base64 = { workspace = true } mutants = { workspace = true } diff --git a/integration-tests/src/context.rs b/integration-tests/src/context.rs index 50abb817..3aa23e87 100644 --- a/integration-tests/src/context.rs +++ b/integration-tests/src/context.rs @@ -1,7 +1,10 @@ use anyhow::Result; use near_workspaces::Account; use nitka::{misc::ToNear, near_sdk::json_types::U128}; -use sweat_jar_model::api::{InitApiIntegration, ProductApiIntegration, SweatJarContract}; +use sweat_jar_model::{ + api::{InitApiIntegration, JarApiIntegration, ProductApiIntegration, SweatJarContract}, + jar::JarView, +}; use sweat_model::{StorageManagementIntegration, SweatApiIntegration, SweatContract}; use crate::product::RegisterProductCommand; @@ -17,6 +20,7 @@ pub trait IntegrationContext { async fn fee(&mut self) -> Result; fn sweat_jar(&self) -> SweatJarContract; fn ft_contract(&self) -> SweatContract; + async fn last_jar_for(&self, account: &Account) -> Result; } impl IntegrationContext for Context { @@ -43,15 +47,25 @@ impl IntegrationContext for Context { contract: &self.contracts[FT_CONTRACT], } } + + async fn last_jar_for(&self, account: &Account) -> Result { + Ok(self + .sweat_jar() + .get_jars_for_account(account.to_near()) + .await? + .into_iter() + .last() + .unwrap()) + } } pub(crate) async fn prepare_contract( - custom_jar: Option>, + custom_jar_contract: Option>, products: impl IntoIterator, ) -> Result { let mut context = Context::new(&[FT_CONTRACT, SWEAT_JAR], true, "build-integration".into()).await?; - if let Some(custom_jar) = custom_jar { + if let Some(custom_jar) = custom_jar_contract { let contract = context .sweat_jar() .contract diff --git a/integration-tests/src/fast_forward.rs b/integration-tests/src/fast_forward.rs index f26144fc..682f344c 100644 --- a/integration-tests/src/fast_forward.rs +++ b/integration-tests/src/fast_forward.rs @@ -20,12 +20,8 @@ async fn fast_forward() -> anyhow::Result<()> { passed.push(context.sweat_jar().block_timestamp_ms().await? - start_timestamp) } - dbg!(&passed); - let avg = passed.iter().sum::() / passed.len() as Timestamp; - dbg!(avg); - // Yeah this looks weird but workspace block skipping is very volatile assert!(52_000 < avg && avg < 76_000); diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index e1bb6b92..59c59f0e 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -13,4 +13,5 @@ mod premium_product; mod product; mod product_actions; mod restake; +mod withdraw_all; mod withdraw_fee; diff --git a/integration-tests/src/measure/after_claim.rs b/integration-tests/src/measure/after_claim.rs index 550fbbd4..1f801e88 100644 --- a/integration-tests/src/measure/after_claim.rs +++ b/integration-tests/src/measure/after_claim.rs @@ -1,8 +1,5 @@ -#![cfg(test)] - use std::collections::HashMap; -use itertools::Itertools; use near_workspaces::types::Gas; use sweat_jar_model::api::ClaimApiIntegration; @@ -26,22 +23,18 @@ async fn measure_after_claim_total_test() -> anyhow::Result<()> { RegisterProductCommand::Locked6Months6Percents, RegisterProductCommand::Locked6Months6PercentsWithWithdrawFee, ], - &(1..5).collect_vec(), + &(1..5).collect::>(), ), measure_after_claim_total, ) .await?; - dbg!(&measured); - let mut map: HashMap> = HashMap::new(); for measure in measured { map.entry(measure.0 .0).or_default().push(measure.1); } - dbg!(&map); - let map: HashMap = map .into_iter() .map(|(key, gas_cost)| { diff --git a/integration-tests/src/measure/after_withdraw.rs b/integration-tests/src/measure/after_withdraw.rs index 29a914af..1503a8d8 100644 --- a/integration-tests/src/measure/after_withdraw.rs +++ b/integration-tests/src/measure/after_withdraw.rs @@ -1,7 +1,4 @@ -#![cfg(test)] - use anyhow::Result; -use itertools::Itertools; use near_workspaces::types::Gas; use sweat_jar_model::{api::WithdrawApiIntegration, U32}; @@ -28,9 +25,7 @@ async fn measure_withdraw_test() -> Result<()> { ) .await?; - dbg!(&result); - - let all_gas = result.into_iter().map(|res| res.1).collect_vec(); + let all_gas: Vec<_> = result.into_iter().map(|res| res.1).collect(); dbg!(&all_gas); diff --git a/integration-tests/src/measure/batch_penalty.rs b/integration-tests/src/measure/batch_penalty.rs index e486ff69..45effd4c 100644 --- a/integration-tests/src/measure/batch_penalty.rs +++ b/integration-tests/src/measure/batch_penalty.rs @@ -1,5 +1,3 @@ -#![cfg(test)] - use std::collections::HashMap; use anyhow::Result; diff --git a/integration-tests/src/measure/claim.rs b/integration-tests/src/measure/claim.rs index cc916a93..7158614d 100644 --- a/integration-tests/src/measure/claim.rs +++ b/integration-tests/src/measure/claim.rs @@ -1,5 +1,3 @@ -#![cfg(test)] - use std::collections::HashMap; use anyhow::Result; diff --git a/integration-tests/src/measure/measure.rs b/integration-tests/src/measure/measure.rs index 5040e2b6..f2ca0b64 100644 --- a/integration-tests/src/measure/measure.rs +++ b/integration-tests/src/measure/measure.rs @@ -1,9 +1,6 @@ -#![cfg(test)] - use std::future::Future; use futures::future::join_all; -use itertools::Itertools; use near_workspaces::types::Gas; use tokio::spawn; @@ -30,10 +27,10 @@ where Fut: Future> + Send + 'static, Command: FnMut(Input) -> Fut + Copy, { - let inputs = inputs.into_iter().collect_vec(); + let inputs: Vec<_> = inputs.into_iter().collect(); // async concurrent execution - let all = inputs.iter().map(|inp| command(*inp)).collect_vec(); + let all: Vec<_> = inputs.iter().map(|inp| command(*inp)).collect(); let res: Vec<_> = join_all(all).await.into_iter().collect::>()?; @@ -54,7 +51,7 @@ where // res.extend(chunk_result); // } - let res = inputs.into_iter().zip(res.into_iter()).collect_vec(); + let res = inputs.into_iter().zip(res.into_iter()).collect(); Ok(res) } @@ -65,7 +62,7 @@ async fn _redundant_command_measure(mut command: impl FnMut() -> Fut) -> an where Fut: Future> + Send + 'static, { - let futures = (0..1).into_iter().map(|_| spawn(command())).collect_vec(); + let futures: Vec<_> = (0..1).into_iter().map(|_| spawn(command())).collect(); let all_gas: Vec = join_all(futures) .await diff --git a/integration-tests/src/measure/mod.rs b/integration-tests/src/measure/mod.rs index f2c650f5..5a8630fc 100644 --- a/integration-tests/src/measure/mod.rs +++ b/integration-tests/src/measure/mod.rs @@ -6,7 +6,9 @@ pub(crate) mod measure; pub(crate) mod random_element; mod register_product; mod restake; +mod restake_all; mod stake; mod top_up; pub(crate) mod utils; mod withdraw; +mod withdraw_all; diff --git a/integration-tests/src/measure/register_product.rs b/integration-tests/src/measure/register_product.rs index d34d45db..13e50754 100644 --- a/integration-tests/src/measure/register_product.rs +++ b/integration-tests/src/measure/register_product.rs @@ -1,5 +1,3 @@ -#![cfg(test)] - use near_workspaces::types::Gas; use sweat_jar_model::api::ProductApiIntegration; diff --git a/integration-tests/src/measure/restake.rs b/integration-tests/src/measure/restake.rs index 5e6f0db9..79a7e6ab 100644 --- a/integration-tests/src/measure/restake.rs +++ b/integration-tests/src/measure/restake.rs @@ -1,5 +1,3 @@ -#![cfg(test)] - use std::collections::HashMap; use anyhow::Result; diff --git a/integration-tests/src/measure/restake_all.rs b/integration-tests/src/measure/restake_all.rs new file mode 100644 index 00000000..9f7dcd41 --- /dev/null +++ b/integration-tests/src/measure/restake_all.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use nitka::{measure::utils::pretty_gas_string, set_integration_logs_enabled}; +use sweat_jar_model::api::{ClaimApiIntegration, JarApiIntegration}; + +use crate::{ + context::{prepare_contract, IntegrationContext}, + measure::utils::add_jar, + product::RegisterProductCommand, +}; + +#[ignore] +#[tokio::test] +#[mutants::skip] +async fn measure_restake_all() -> Result<()> { + set_integration_logs_enabled(false); + + let product = RegisterProductCommand::Locked5Minutes60000Percents; + let mut context = prepare_contract(None, [product]).await?; + let alice = context.alice().await?; + + for _ in 0..200 { + add_jar(&context, &alice, product, 10_000).await?; + } + + context.fast_forward_minutes(6).await?; + + context.sweat_jar().claim_total(None).with_user(&alice).await?; + + let gas = context + .sweat_jar() + .restake_all() + .with_user(&alice) + .result() + .await? + .total_gas_burnt; + dbg!(pretty_gas_string(gas)); + + // 1 jar - 6 TGas 225 GGas total: 6225437862976 + // 100 jars - 50 TGas 709 GGas total: 50709431315947 + // 200 jars - 86 TGas 607 GGas total: 86607517267105 + + Ok(()) +} diff --git a/integration-tests/src/measure/stake.rs b/integration-tests/src/measure/stake.rs index 7941ebdc..4803d825 100644 --- a/integration-tests/src/measure/stake.rs +++ b/integration-tests/src/measure/stake.rs @@ -1,5 +1,3 @@ -#![cfg(test)] - use std::collections::HashMap; use anyhow::Result; diff --git a/integration-tests/src/measure/top_up.rs b/integration-tests/src/measure/top_up.rs index a2e6b48c..a1b9903f 100644 --- a/integration-tests/src/measure/top_up.rs +++ b/integration-tests/src/measure/top_up.rs @@ -1,5 +1,3 @@ -#![cfg(test)] - use std::collections::HashMap; use anyhow::Result; diff --git a/integration-tests/src/measure/withdraw.rs b/integration-tests/src/measure/withdraw.rs index cbbae650..2d8eea81 100644 --- a/integration-tests/src/measure/withdraw.rs +++ b/integration-tests/src/measure/withdraw.rs @@ -1,5 +1,3 @@ -#![cfg(test)] - use std::collections::HashMap; use anyhow::Result; diff --git a/integration-tests/src/measure/withdraw_all.rs b/integration-tests/src/measure/withdraw_all.rs new file mode 100644 index 00000000..d4feae12 --- /dev/null +++ b/integration-tests/src/measure/withdraw_all.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use nitka::{measure::utils::pretty_gas_string, set_integration_logs_enabled}; +use sweat_jar_model::api::{ClaimApiIntegration, WithdrawApiIntegration}; + +use crate::{ + context::{prepare_contract, IntegrationContext}, + measure::utils::add_jar, + product::RegisterProductCommand, +}; + +#[ignore] +#[tokio::test] +#[mutants::skip] +async fn measure_withdraw_all() -> Result<()> { + set_integration_logs_enabled(false); + + let product = RegisterProductCommand::Locked5Minutes60000Percents; + let mut context = prepare_contract(None, [product]).await?; + let alice = context.alice().await?; + + for _ in 0..200 { + add_jar(&context, &alice, product, 10_000).await?; + } + + context.fast_forward_minutes(6).await?; + + context.sweat_jar().claim_total(None).with_user(&alice).await?; + + let gas = context + .sweat_jar() + .withdraw_all() + .with_user(&alice) + .result() + .await? + .total_gas_burnt; + dbg!(pretty_gas_string(gas)); + + // 1 jar - 18 TGas 208 GGas total: 18208042945131 + // 100 jars - 65 TGas 547 GGas total: 65547362403008 + // 200 jars - 111 TGas 935 GGas total: 111935634284610 + + Ok(()) +} diff --git a/integration-tests/src/migrations/new_sdk.rs b/integration-tests/src/migrations/new_sdk.rs index 77c97d71..e6f5d008 100644 --- a/integration-tests/src/migrations/new_sdk.rs +++ b/integration-tests/src/migrations/new_sdk.rs @@ -56,7 +56,7 @@ async fn migrate_to_near_sdk_5() -> Result<()> { } let products_old = old_jar_contract.get_products().with_user(&ft_account).await?; - assert_eq!(products_old.len(), 8); + assert_eq!(products_old.len(), 9); let bob_jars = old_jar_contract.get_jars_for_account(bob.to_near()).await?; assert!(bob_jars.is_empty()); @@ -78,8 +78,6 @@ async fn migrate_to_near_sdk_5() -> Result<()> { assert_eq!(ft_contract.ft_balance_of(bob.to_near()).await?.0, 900_000); - dbg!(ft_contract.ft_balance_of(bob.to_near()).await?); - drop(old_jar_contract); let new_jar_contract = jar_account.deploy(&jar_new_code).await?.into_result()?; @@ -101,7 +99,7 @@ async fn migrate_to_near_sdk_5() -> Result<()> { .await?; let products = new_jar_contract.get_products().with_user(&ft_account).await?; - assert_eq!(products.len(), 9); + assert_eq!(products.len(), 10); let staked = new_jar_contract .create_jar( diff --git a/integration-tests/src/product.rs b/integration-tests/src/product.rs index 5d74f7ec..ec032c72 100644 --- a/integration-tests/src/product.rs +++ b/integration-tests/src/product.rs @@ -8,6 +8,7 @@ pub(crate) enum RegisterProductCommand { Flexible6Months6Percents, Locked6Months6PercentsWithWithdrawFee, Locked10Minutes6Percents, + Locked5Minutes60000Percents, Locked10Minutes60000Percents, Locked10Minutes6PercentsTopUp, Locked10Minutes6PercentsWithFixedWithdrawFee, @@ -15,13 +16,14 @@ pub(crate) enum RegisterProductCommand { } impl RegisterProductCommand { - pub(crate) fn all() -> [Self; 9] { + pub(crate) fn all() -> [Self; 10] { [ Self::Locked12Months12Percents, Self::Locked6Months6Percents, Self::Flexible6Months6Percents, Self::Locked6Months6PercentsWithWithdrawFee, Self::Locked10Minutes6Percents, + Self::Locked5Minutes60000Percents, Self::Locked10Minutes60000Percents, Self::Locked10Minutes6PercentsTopUp, Self::Locked10Minutes6PercentsWithFixedWithdrawFee, @@ -121,8 +123,23 @@ impl RegisterProductCommand { }, "is_enabled": true, }), + RegisterProductCommand::Locked5Minutes60000Percents => json!({ + "id": "flexible_5_minutes_60000_percents", + "apy_default": ["60000", 2], + "cap_min": "10000", + "cap_max": "100000000000", + "terms": { + "type": "fixed", + "data": { + "lockup_term": "300000", + "allows_top_up": false, + "allows_restaking": true, + } + }, + "is_enabled": true, + }), RegisterProductCommand::Locked10Minutes60000Percents => json!({ - "id": "flexible_6_months_60000_percents", + "id": "flexible_10_minutes_60000_percents", "apy_default": ["60000", 2], "cap_min": "100000", "cap_max": "100000000000", @@ -131,7 +148,7 @@ impl RegisterProductCommand { "data": { "lockup_term": "600000", "allows_top_up": false, - "allows_restaking": false, + "allows_restaking": true, } }, "is_enabled": true, diff --git a/integration-tests/src/product_actions.rs b/integration-tests/src/product_actions.rs index e3caefa9..cd4bb470 100644 --- a/integration-tests/src/product_actions.rs +++ b/integration-tests/src/product_actions.rs @@ -1,5 +1,4 @@ use base64::{engine::general_purpose::STANDARD, Engine}; -use itertools::Itertools; use sweat_jar_model::api::ProductApiIntegration; use crate::{ @@ -56,7 +55,7 @@ async fn product_actions() -> anyhow::Result<()> { .sweat_jar() .set_public_key( RegisterProductCommand::Locked12Months12Percents.id(), - pk_base64.as_bytes().into_iter().copied().collect_vec().into(), + pk_base64.as_bytes().into_iter().copied().collect::>().into(), ) .with_user(&manager) .await?; diff --git a/integration-tests/src/restake.rs b/integration-tests/src/restake.rs index a1f08bc5..ff016b9e 100644 --- a/integration-tests/src/restake.rs +++ b/integration-tests/src/restake.rs @@ -1,4 +1,5 @@ -use nitka::misc::ToNear; +use anyhow::Result; +use nitka::{misc::ToNear, set_integration_logs_enabled}; use sweat_jar_model::api::{ClaimApiIntegration, JarApiIntegration}; use crate::{ @@ -9,20 +10,19 @@ use crate::{ #[tokio::test] #[mutants::skip] -async fn restake() -> anyhow::Result<()> { +async fn restake() -> Result<()> { println!("👷🏽 Run test for restaking"); - let product_command = RegisterProductCommand::Locked10Minutes6Percents; - let product_id = product_command.id(); + let product = RegisterProductCommand::Locked10Minutes6Percents; - let mut context = prepare_contract(None, [product_command]).await?; + let mut context = prepare_contract(None, [product]).await?; let alice = context.alice().await?; let amount = 1_000_000; context .sweat_jar() - .create_jar(&alice, product_id, amount, &context.ft_contract()) + .create_jar(&alice, product.id(), amount, &context.ft_contract()) .await?; let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; @@ -63,3 +63,68 @@ async fn restake() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +#[mutants::skip] +async fn restake_all() -> Result<()> { + const PRINCIPAL: u128 = 1_000_000; + + println!("👷🏽 Run test for restake all"); + + set_integration_logs_enabled(false); + + let product_5_min = RegisterProductCommand::Locked5Minutes60000Percents; + let product_10_min = RegisterProductCommand::Locked10Minutes60000Percents; + + let mut context = prepare_contract(None, [product_5_min, product_10_min]).await?; + + let alice = context.alice().await?; + + let amount = context + .sweat_jar() + .create_jar(&alice, product_5_min.id(), PRINCIPAL + 1, &context.ft_contract()) + .await?; + assert_eq!(amount.0, PRINCIPAL + 1); + + let jar_5_min_1 = context.last_jar_for(&alice).await?; + assert_eq!(jar_5_min_1.principal.0, PRINCIPAL + 1); + + context + .sweat_jar() + .create_jar(&alice, product_5_min.id(), PRINCIPAL + 2, &context.ft_contract()) + .await?; + let jar_5_min_2 = context.last_jar_for(&alice).await?; + assert_eq!(jar_5_min_2.principal.0, PRINCIPAL + 2); + + context + .sweat_jar() + .create_jar(&alice, product_10_min.id(), PRINCIPAL + 3, &context.ft_contract()) + .await?; + let jar_10_min = context.last_jar_for(&alice).await?; + assert_eq!(jar_10_min.principal.0, PRINCIPAL + 3); + + let claimed = context.sweat_jar().claim_total(None).await?; + assert_eq!(claimed.get_total().0, 0); + + context.fast_forward_minutes(6).await?; + + context.sweat_jar().claim_total(None).with_user(&alice).await?; + + let restacked = context.sweat_jar().restake_all().with_user(&alice).await?; + + assert_eq!( + restacked.into_iter().map(|j| j.principal).collect::>(), + vec![jar_5_min_1.principal, jar_5_min_2.principal] + ); + + let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; + + assert_eq!(jars.iter().map(|j| j.id.0).collect::>(), vec![3, 4, 5]); + + assert_eq!( + jars.iter().map(|j| j.principal.0).collect::>(), + vec![PRINCIPAL + 3, PRINCIPAL + 1, PRINCIPAL + 2] + ); + + Ok(()) +} diff --git a/integration-tests/src/withdraw_all.rs b/integration-tests/src/withdraw_all.rs new file mode 100644 index 00000000..f61c144a --- /dev/null +++ b/integration-tests/src/withdraw_all.rs @@ -0,0 +1,101 @@ +use anyhow::Result; +use near_workspaces::types::Gas; +use nitka::{misc::ToNear, set_integration_logs_enabled}; +use sweat_jar_model::api::{ClaimApiIntegration, JarApiIntegration, WithdrawApiIntegration}; +use sweat_model::FungibleTokenCoreIntegration; + +use crate::{ + context::{prepare_contract, IntegrationContext}, + jar_contract_extensions::JarContractExtensions, + product::RegisterProductCommand, +}; + +#[tokio::test] +#[mutants::skip] +async fn withdraw_all() -> Result<()> { + const PRINCIPAL: u128 = 1_000_000; + + println!("👷🏽 Run test for withdraw all"); + + set_integration_logs_enabled(false); + + let product_5_min = RegisterProductCommand::Locked5Minutes60000Percents; + let product_10_min = RegisterProductCommand::Locked10Minutes60000Percents; + + let mut context = prepare_contract(None, [product_5_min, product_10_min]).await?; + + let alice = context.alice().await?; + + let amount = context + .sweat_jar() + .create_jar(&alice, product_5_min.id(), PRINCIPAL + 1, &context.ft_contract()) + .await?; + assert_eq!(amount.0, PRINCIPAL + 1); + + let jar_5_min_1 = context.last_jar_for(&alice).await?; + assert_eq!(jar_5_min_1.principal.0, PRINCIPAL + 1); + + context + .sweat_jar() + .create_jar(&alice, product_5_min.id(), PRINCIPAL + 2, &context.ft_contract()) + .await?; + let jar_5_min_2 = context.last_jar_for(&alice).await?; + assert_eq!(jar_5_min_2.principal.0, PRINCIPAL + 2); + + context + .sweat_jar() + .create_jar(&alice, product_10_min.id(), PRINCIPAL + 3, &context.ft_contract()) + .await?; + let jar_10_min = context.last_jar_for(&alice).await?; + assert_eq!(jar_10_min.principal.0, PRINCIPAL + 3); + + let claimed = context.sweat_jar().claim_total(None).await?; + assert_eq!(claimed.get_total().0, 0); + + context.fast_forward_minutes(6).await?; + + context.sweat_jar().claim_total(None).with_user(&alice).await?; + + let alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?; + let jar_balance = context + .ft_contract() + .ft_balance_of(context.sweat_jar().contract.as_account().to_near()) + .await?; + + context + .sweat_jar() + .withdraw_all() + .with_user(&alice) + .gas(Gas::from_tgas(5)) + .expect_error("Not enough gas left to complete transfer_bulk_withdraw") + .await?; + + let withdrawn = context.sweat_jar().withdraw_all().with_user(&alice).await?; + + let alice_balance_after = context.ft_contract().ft_balance_of(alice.to_near()).await?; + let jar_balance_after = context + .ft_contract() + .ft_balance_of(context.sweat_jar().contract.as_account().to_near()) + .await?; + + assert_eq!(alice_balance_after.0 - alice_balance.0, 2000003); + assert_eq!(jar_balance.0 - jar_balance_after.0, 2000003); + + assert_eq!(withdrawn.total_amount.0, 2000003); + + assert_eq!( + withdrawn.jars.iter().map(|j| j.withdrawn_amount).collect::>(), + vec![jar_5_min_1.principal, jar_5_min_2.principal] + ); + + let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; + + assert_eq!(jars.len(), 1); + + let jar = jars.into_iter().next().unwrap(); + + assert_eq!(jar.id, jar_10_min.id); + assert_eq!(jar.principal, jar_10_min.principal); + + Ok(()) +} diff --git a/model/src/api.rs b/model/src/api.rs index 107f6561..66469970 100644 --- a/model/src/api.rs +++ b/model/src/api.rs @@ -10,7 +10,7 @@ use crate::{ claimed_amount_view::ClaimedAmountView, jar::{AggregatedInterestView, AggregatedTokenAmountView, JarIdView, JarView}, product::{ProductView, RegisterProductCommand}, - withdraw::WithdrawView, + withdraw::{BulkWithdrawView, WithdrawView}, ProductId, }; @@ -160,6 +160,8 @@ pub trait JarApi { /// - If the function is called by an account other than the owner of the original jar. /// - If the original jar is not yet mature. fn restake(&mut self, jar_id: JarIdView) -> JarView; + + fn restake_all(&mut self) -> Vec; } #[make_integration_version] @@ -282,6 +284,8 @@ pub trait WithdrawApi { /// - If the withdrawal amount exceeds the available balance in the jar. /// - If attempting to withdraw from a Fixed jar that is not yet mature. fn withdraw(&mut self, jar_id: JarIdView, amount: Option) -> ::near_sdk::PromiseOrValue; + + fn withdraw_all(&mut self) -> ::near_sdk::PromiseOrValue; } #[cfg(feature = "integration-methods")] diff --git a/model/src/withdraw.rs b/model/src/withdraw.rs index 378211a9..1143b8df 100644 --- a/model/src/withdraw.rs +++ b/model/src/withdraw.rs @@ -1,5 +1,6 @@ use near_sdk::{ json_types::U128, + near, serde::{Deserialize, Serialize}, AccountId, }; @@ -7,8 +8,8 @@ use near_sdk::{ use crate::TokenAmount; /// The `WithdrawView` struct represents the result of a deposit jar withdrawal operation. -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(crate = "near_sdk::serde")] +#[derive(Debug, PartialEq)] +#[near(serializers=[borsh, json])] pub struct WithdrawView { /// The amount of tokens that has been transferred to the user's account as part of the withdrawal. pub withdrawn_amount: U128, @@ -17,6 +18,13 @@ pub struct WithdrawView { pub fee: U128, } +#[derive(Debug)] +#[near(serializers=[borsh, json])] +pub struct BulkWithdrawView { + pub total_amount: U128, + pub jars: Vec, +} + impl WithdrawView { #[must_use] pub fn new(amount: TokenAmount, fee: Option) -> Self { diff --git a/res/sweat_jar.wasm b/res/sweat_jar.wasm index 270e1fce..dc36a9ff 100755 Binary files a/res/sweat_jar.wasm and b/res/sweat_jar.wasm differ diff --git a/scripts/mutation.sh b/scripts/mutation.sh index 8bfc63b3..5332c30e 100755 --- a/scripts/mutation.sh +++ b/scripts/mutation.sh @@ -4,4 +4,4 @@ set -eox pipefail echo ">> Mutation tests" cargo install --locked cargo-mutants -cargo mutants +cargo mutants -p sweat_jar -- --release